@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,152 @@
1
+ /**
2
+ * chant#2578 — init, onboard, audit and import-agents name a lexicon declared
3
+ * by path (#2520) by that path, and print no install line for it. A lexicon
4
+ * named by package keeps its package name and `npm i` line.
5
+ */
6
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
7
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import {
11
+ lexiconPackagesToInstall,
12
+ lexiconSourceLabel,
13
+ registerLexiconDeclarations,
14
+ resetLexiconModules,
15
+ } from "../lexicon-module";
16
+ import { initCommand } from "./commands/init";
17
+ import { onboardCommand } from "./commands/onboard";
18
+ import { installLine } from "./commands/audit";
19
+ import { importAgentsCommand } from "./commands/import-agents";
20
+ import { auditFiles, MissingLexiconError, type AuditInput } from "../audit/core";
21
+
22
+ let dir: string;
23
+
24
+ beforeEach(() => {
25
+ dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-2578-")));
26
+ resetLexiconModules();
27
+ });
28
+
29
+ afterEach(() => {
30
+ resetLexiconModules();
31
+ rmSync(dir, { recursive: true, force: true });
32
+ });
33
+
34
+ describe("the shared helpers", () => {
35
+ test("a path lexicon is labelled by its path and has no package to install", () => {
36
+ registerLexiconDeclarations(["aws", { name: "site", module: "./lexicon/index.ts" }], dir);
37
+ expect(lexiconSourceLabel("site", dir)).toBe("./lexicon/index.ts");
38
+ expect(lexiconSourceLabel("aws", dir)).toBe("@intentius/chant-lexicon-aws");
39
+ expect(lexiconPackagesToInstall(["aws", "site"])).toEqual(["@intentius/chant-lexicon-aws"]);
40
+ });
41
+
42
+ test("a path outside the directory is shown absolute", () => {
43
+ registerLexiconDeclarations([{ name: "site", module: "/elsewhere/site.ts" }], dir);
44
+ expect(lexiconSourceLabel("site", dir)).toBe("/elsewhere/site.ts");
45
+ });
46
+ });
47
+
48
+ describe("init", () => {
49
+ function writeConfig(entry: string): void {
50
+ writeFileSync(join(dir, "chant.config.ts"), `export default { lexicons: [${entry}] };\n`);
51
+ }
52
+
53
+ test("a lexicon the existing config declares by path gets no dependency and no type stub", async () => {
54
+ writeConfig(`{ name: "site", module: "./lexicon/index.ts" }`);
55
+ const result = await initCommand({ path: dir, lexicon: "site", force: true, skipMcp: true, skipInstall: true });
56
+
57
+ expect(result.success).toBe(true);
58
+ expect(result.lexiconModule).toBe("./lexicon/index.ts");
59
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
60
+ expect(Object.keys(pkg.dependencies)).toEqual(["@intentius/chant"]);
61
+ expect(existsSync(join(dir, ".chant", "types", "lexicon-site"))).toBe(false);
62
+ });
63
+
64
+ test("a package lexicon is unchanged", async () => {
65
+ writeConfig(`"aws"`);
66
+ const result = await initCommand({ path: dir, lexicon: "aws", force: true, skipMcp: true, skipInstall: true });
67
+
68
+ expect(result.success).toBe(true);
69
+ expect(result.lexiconModule).toBeUndefined();
70
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
71
+ expect(Object.keys(pkg.dependencies)).toEqual(["@intentius/chant", "@intentius/chant-lexicon-aws"]);
72
+ expect(existsSync(join(dir, ".chant", "types", "lexicon-aws", "package.json"))).toBe(true);
73
+ });
74
+ });
75
+
76
+ describe("onboard", () => {
77
+ test("a lexicon declared by path is named by its path, with nothing patched", () => {
78
+ registerLexiconDeclarations([{ name: "site", module: "./lexicon/index.ts" }], process.cwd());
79
+ const result = onboardCommand({ name: "site", root: dir });
80
+
81
+ expect(result.success).toBe(false);
82
+ expect(result.error).toContain("./lexicon/index.ts");
83
+ expect(result.error).not.toMatch(/npm (i|install)\b/);
84
+ expect(result.patched).toEqual([]);
85
+ });
86
+
87
+ test("a package lexicon still patches", () => {
88
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "root", dependencies: {} }));
89
+ const result = onboardCommand({ name: "site", root: dir });
90
+
91
+ expect(result.success).toBe(true);
92
+ expect(result.patched.some((p) => p.startsWith("package.json"))).toBe(true);
93
+ });
94
+ });
95
+
96
+ describe("audit", () => {
97
+ test("the npx line leaves out a lexicon declared by path", () => {
98
+ registerLexiconDeclarations([{ name: "github", module: "./gh/index.ts" }], dir);
99
+ expect(installLine(["github", "gitlab"], ".")).toBe(
100
+ "npx -p @intentius/chant -p @intentius/chant-lexicon-gitlab chant audit .",
101
+ );
102
+ });
103
+
104
+ test("a path lexicon that fails to load is named by its path, with no npm line", async () => {
105
+ registerLexiconDeclarations([{ name: "helm", module: join(dir, "missing.ts") }], dir);
106
+ const input = { lexicon: "helm", path: "Chart.yaml", content: "" } as AuditInput;
107
+
108
+ const error = await auditFiles([input]).then(
109
+ () => undefined,
110
+ (err: unknown) => err,
111
+ );
112
+ expect(error).toBeInstanceOf(MissingLexiconError);
113
+ expect((error as Error).message).toContain(join(dir, "missing.ts"));
114
+ expect((error as Error).message).not.toContain("npm i");
115
+ });
116
+ });
117
+
118
+ describe("import-agents", () => {
119
+ function seedAgentConfig(home: string): void {
120
+ mkdirSync(join(home, ".claude"), { recursive: true });
121
+ writeFileSync(join(home, ".claude", "mcp.json"), JSON.stringify({ mcpServers: { a: { command: "x" } } }));
122
+ }
123
+
124
+ test("a path lexicon that fails to load is named by its path, with no npm line", async () => {
125
+ const home = join(dir, "home");
126
+ seedAgentConfig(home);
127
+ registerLexiconDeclarations([{ name: "fountain", module: "./agents/missing.ts" }], dir);
128
+
129
+ const result = await importAgentsCommand({ home, platform: "linux", projectRoots: [], output: join(dir, "out") });
130
+
131
+ expect(result.success).toBe(false);
132
+ expect(result.error).toContain("declared by path");
133
+ expect(result.error).toContain("missing.ts");
134
+ expect(result.error).not.toContain("npm i");
135
+ });
136
+
137
+ test("a package lexicon still gets its npm line", async () => {
138
+ const home = join(dir, "home");
139
+ seedAgentConfig(home);
140
+
141
+ const result = await importAgentsCommand({
142
+ home,
143
+ platform: "linux",
144
+ projectRoots: [],
145
+ output: join(dir, "out"),
146
+ lexicon: "no-such-lexicon",
147
+ });
148
+
149
+ expect(result.success).toBe(false);
150
+ expect(result.error).toContain("npm i @intentius/chant-lexicon-no-such-lexicon");
151
+ });
152
+ });
@@ -1,10 +1,30 @@
1
+ import { importLexiconModule, lexiconModulePath, lexiconNames, registerLexiconDeclarations } from "../lexicon-module";
2
+ import { readLexiconDeclarationsStatically, type StaticLexiconRead } from "../config-static";
3
+ export { unknownPathLexiconsNotice } from "../config-static";
1
4
  import { createRequire } from "node:module";
2
5
  import { dirname, join } from "node:path";
3
6
  import { existsSync, readFileSync } from "node:fs";
4
7
  import { isLexiconPlugin, type LexiconPlugin } from "../lexicon";
5
8
  import { loadChantConfigUpward } from "../config";
6
9
  import { findInfraFiles, detectLexicons } from "../index";
7
- import { checkConflicts } from "./conflict-check";
10
+ import { checkConflicts, describeConflict } from "./conflict-check";
11
+
12
+ /**
13
+ * chant#2591 — record the lexicons the chant.config nearest `dir` declares by
14
+ * path (../lexicon-module.ts), for a command that does not otherwise read the
15
+ * config: `init --force`, `dev onboard` and `import --agents`.
16
+ *
17
+ * The config is read statically (../config-static.ts) and never evaluated,
18
+ * so these commands run no project code to learn it. When the `lexicons`
19
+ * value cannot be read that way, nothing is recorded, every lexicon keeps its
20
+ * package behaviour, and the returned read carries the reason; the caller
21
+ * prints {@link unknownPathLexiconsNotice} for it.
22
+ */
23
+ export function recordProjectLexicons(dir: string): StaticLexiconRead {
24
+ const read = readLexiconDeclarationsStatically(dir);
25
+ if (read.status === "read") registerLexiconDeclarations(read.entries, dirname(read.configPath));
26
+ return read;
27
+ }
8
28
 
9
29
  /**
10
30
  * Load a single lexicon plugin by lexicon name.
@@ -13,6 +33,23 @@ import { checkConflicts } from "./conflict-check";
13
33
  * LexiconPlugin export. Falls back to wrapping a raw Serializer export.
14
34
  */
15
35
  export async function loadPlugin(lexiconName: string): Promise<LexiconPlugin> {
36
+ // chant #2520 — a lexicon declared by module path loads from that module.
37
+ const modulePath = lexiconModulePath(lexiconName);
38
+ if (modulePath !== undefined) {
39
+ const mod = (await importLexiconModule(lexiconName))!;
40
+ for (const value of Object.values(mod)) {
41
+ if (isLexiconPlugin(value)) {
42
+ if (value.name !== lexiconName) {
43
+ throw new Error(
44
+ `lexicon "${lexiconName}" is declared with module ${modulePath}, but the plugin it exports is named "${value.name}"`,
45
+ );
46
+ }
47
+ return value;
48
+ }
49
+ }
50
+ throw new Error(`Module ${modulePath} (lexicon "${lexiconName}") does not export a LexiconPlugin`);
51
+ }
52
+
16
53
  const packageName = `@intentius/chant-lexicon-${lexiconName}`;
17
54
  const mod = await import(packageName);
18
55
 
@@ -67,6 +104,9 @@ export function resolveLexiconVersions(lexiconNames: readonly string[]): Record<
67
104
  const versions: Record<string, string> = {};
68
105
 
69
106
  for (const name of lexiconNames) {
107
+ // chant #2520 — a lexicon declared by module path is not an installed
108
+ // package, so it has no package version to report.
109
+ if (lexiconModulePath(name) !== undefined) continue;
70
110
  const packageName = `@intentius/chant-lexicon-${name}`;
71
111
  try {
72
112
  let dir = dirname(require_.resolve(packageName));
@@ -177,9 +217,7 @@ export async function loadPlugins(lexiconNames: string[]): Promise<LexiconPlugin
177
217
  }
178
218
 
179
219
  if (report.conflicts.length > 0) {
180
- const details = report.conflicts
181
- .map((c) => ` ${c.type} "${c.key}" from: ${c.plugins.join(", ")}`)
182
- .join("\n");
220
+ const details = report.conflicts.map(describeConflict).join("\n");
183
221
  throw new Error(
184
222
  `Cross-lexicon conflicts detected:\n${details}`,
185
223
  );
@@ -222,7 +260,7 @@ export async function resolveProjectLexicons(projectPath: string): Promise<strin
222
260
  const { config } = await loadChantConfigUpward(projectPath);
223
261
 
224
262
  if (config.lexicons && config.lexicons.length > 0) {
225
- return config.lexicons;
263
+ return lexiconNames(config.lexicons);
226
264
  }
227
265
 
228
266
  // Fallback: detect from source imports — a pure text scan, no execution.
@@ -129,6 +129,8 @@ export interface ParsedArgs {
129
129
  * ignore it.
130
130
  */
131
131
  namespace?: string;
132
+ /** `chant workspace upgrade --allow-code` (#2550) — run migrations whose body is the template's own code. */
133
+ allowCode?: boolean;
132
134
  /** `chant lifecycle rollback --dry-run` — compute the rollback delta and print it; open no PR, push nothing, leave no branch. */
133
135
  dryRun?: boolean;
134
136
  /** `chant lifecycle teardown <env> --yes` — execute the planned deletion
@@ -145,6 +147,8 @@ export interface ParsedArgs {
145
147
  /** `--env <name>` — active environment: sets CHANT_ENV so env-aware source
146
148
  * re-evaluates for that environment (`build` + `graph`), and drives policy. */
147
149
  env?: string;
150
+ /** `chant build --components --generate <lexicon> --promote-to <env>` (#2575) — add a promote job to the generated pipeline. */
151
+ promoteTo?: string;
148
152
  /** `chant graph --stacks` — render the cross-stack apply-ordering graph */
149
153
  stacks?: boolean;
150
154
  /** `chant graph --live --overlay` — classify the provisioned graph against
@@ -175,8 +179,13 @@ export interface ParsedArgs {
175
179
  components?: boolean;
176
180
  /** `chant build --components --generate <lexicon>` — generate mode (#563):
177
181
  * synthesize CI YAML from discovered components instead of running a normal
178
- * lexicon build. Only "gitlab" is implemented for v1. */
182
+ * lexicon build. Also `chant run --generate <provider>` (#2533): one CI
183
+ * pipeline file per scheduled Op, through `generateOpsPipeline`. */
179
184
  generate?: string;
185
+ /** `chant run --generate <provider> --spec <file>` (#2533): a JSON file of
186
+ * `ScheduledOpSpec`s (or `{ ops, options }`) to generate instead of every
187
+ * discovered Op that declares a `schedule`. */
188
+ opsSpec?: string;
180
189
  /** `chant graph --format ir --detail <0..3>` — graph IR detail tier */
181
190
  detail?: number;
182
191
  /** `chant graph --lens <kind>:<target>` — focus the graph IR on a slice */
@@ -242,6 +251,8 @@ export interface ParsedArgs {
242
251
  allProjects?: boolean;
243
252
  /** `chant audit --fail-on merge-worthy|warning|error|none` */
244
253
  failOn?: string;
254
+ /** `chant audit --max-files <n>` (#2528) — files the local walk takes before it stops (default 1000). */
255
+ maxFiles?: number;
245
256
  /** `chant audit --theme <file>` — JSON theme knobs for the HTML report */
246
257
  theme?: string;
247
258
  /** `chant dev surface-diff --update-snapshot` — write the fresh snapshot as the new baseline */
@@ -270,6 +281,29 @@ export interface ParsedArgs {
270
281
  * should win.
271
282
  */
272
283
  at?: string;
284
+ /** `chant workspace records --kind <path>` (#2546): the record kind file to read records through. */
285
+ kind?: string;
286
+ /** `chant workspace records --current` (#2546): leave out records a closed record supersedes. */
287
+ current?: boolean;
288
+ /** `chant workspace records|verify --require attested` (#2547): the provenance level a gate requires. */
289
+ require?: string;
290
+ /**
291
+ * `chant build --root-only` and `chant lint --root-only` (#2537): run on the
292
+ * root project of a declared workspace, members left out, instead of
293
+ * refusing with `WSP000`.
294
+ */
295
+ rootOnly?: boolean;
296
+ /**
297
+ * `chant workspace check --generated` (#2641): run each declared generator
298
+ * and compare its output with the file in the tree. Off by default, since
299
+ * generators run member code.
300
+ */
301
+ generated?: boolean;
302
+ /**
303
+ * `chant workspace build|lint|audit|graph --member <name>` (#2537): run only
304
+ * the named members or example groups. Repeatable, and a comma list works too.
305
+ */
306
+ members?: string[];
273
307
  /**
274
308
  * `chant search "<q>" --ambient --live --env <name>` (#1278) — also report
275
309
  * resources of a kind this estate manages that exist in the account without
@@ -312,6 +346,8 @@ export interface ParsedArgs {
312
346
  component?: string;
313
347
  /** `chant components release record --digest <sha256:...>` (#568) — artifact digest to record, joining this release to the build archive/ledger. Also `chant components export --digest <manifestDigest>` (#929) — a build archive manifest digest to export directly, bypassing env/component resolution. */
314
348
  digest?: string;
349
+ /** Every `--digest` value, in order (#2602). `chant components promote --digest <component>=<sha256:...>` is repeatable, one per component; the other commands read the single {@link digest}. */
350
+ digests?: string[];
315
351
  /** `chant components release record --git-sha <sha>` (#568) — git commit the deploy was built from. */
316
352
  gitSha?: string;
317
353
  /** `chant components release record --run-id <id>` (#568) — orchestrator/CI run identifier. */
@@ -328,6 +364,8 @@ export interface ParsedArgs {
328
364
  noReleaseRecord?: boolean;
329
365
  /** `chant run --components <name> --dump-outputs <file>` — after the run, write the accumulated cross-component/cross-stack outputs (JSON, keyed by component name) to `<file>`, for a downstream job to `--seed-outputs`. */
330
366
  dumpOutputs?: string;
367
+ /** `chant run --components <name> --digest-file <file>` (#2602) — after a successful run, write one `<component>=<digest>` line per release the run recorded to `<file>`, the form `chant components promote --digest` takes, so a generated promote job promotes exactly the release its pipeline run built. Written empty when nothing was recorded. */
368
+ digestFile?: string;
331
369
  /** `chant run --components <name> --seed-outputs <file>` (repeatable) — before the run, load each JSON outputs file (as written by `--dump-outputs`) and seed cross-component/cross-stack resolution with it, so a `stackOutput()`/`@<dep>.publish.*` reference to a component that ran in an earlier job resolves. */
332
370
  seedOutputs?: string[];
333
371
  /** `chant build --fold` (#1022/#1023, epic #1019) — opt-in: fold source modules statically instead of importing/running them; folds resource constructors and composite factory calls, falling back to run per-file for anything the folder can't represent (a cross-file-only reference, a re-export, `export default`, …). Also settable project-wide via `chant.config.ts`'s `build.fold: true`; the flag always wins when set. Default (flag omitted): the existing run path, unchanged. */
@@ -388,6 +426,13 @@ export interface CommandDef {
388
426
  name: string;
389
427
  /** If true, load lexicon plugins before calling handler */
390
428
  requiresPlugins?: boolean;
429
+ /**
430
+ * chant#2591, chant#2589 — the command never evaluates the project's
431
+ * `chant.config.ts`, so `main` does not load it before dispatch either. Such
432
+ * a command reads what it needs from the config statically
433
+ * (../config-static.ts). A function decides per invocation.
434
+ */
435
+ runsNoConfig?: boolean | ((args: ParsedArgs) => boolean);
391
436
  /** Command handler — returns exit code */
392
437
  handler: (ctx: CommandContext) => Promise<number>;
393
438
  }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * chant#2591, chant#2589 — `init --force`, `dev onboard`, `import --agents`
3
+ * and `audit` learn the lexicons a project declares by path without running
4
+ * its `chant.config.ts`.
5
+ *
6
+ * The CLI is spawned the way `bin/chant` runs it, because `main.ts` loads the
7
+ * config before dispatch for most commands, and a test calling the command
8
+ * functions would miss that load. Each fixture's config writes a marker file
9
+ * when it runs. The marker must never appear, and each command's output must
10
+ * still name the lexicon's path.
11
+ */
12
+ import { describe, test, expect, beforeEach, afterEach } from "vitest";
13
+ import { spawnSync } from "node:child_process";
14
+ import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import { dirname, join, resolve } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import { auditCommand } from "./commands/audit";
19
+ import { runCommandInProcess } from "./main";
20
+ import { resetLexiconModules } from "../lexicon-module";
21
+ import { captureRun } from "../workspace/member-run";
22
+
23
+ const thisDir = dirname(fileURLToPath(import.meta.url));
24
+ const mainTs = resolve(thisDir, "main.ts");
25
+ const repoRoot = resolve(thisDir, "../../../..");
26
+ const tsxLoader = join(repoRoot, "node_modules", "tsx", "dist", "loader.mjs");
27
+
28
+ let root: string;
29
+ let project: string;
30
+ let marker: string;
31
+ let home: string;
32
+ let moduleMarker: string;
33
+
34
+ function writeConfig(lexicons: string): void {
35
+ writeFileSync(
36
+ join(project, "chant.config.ts"),
37
+ `import { writeFileSync } from "node:fs";\n` +
38
+ `writeFileSync(${JSON.stringify(marker)}, "ran");\n` +
39
+ `export default { lexicons: ${lexicons} };\n`,
40
+ );
41
+ }
42
+
43
+ beforeEach(() => {
44
+ resetLexiconModules();
45
+ root = realpathSync(mkdtempSync(join(tmpdir(), "chant-2591-cli-")));
46
+ project = join(root, "project");
47
+ marker = join(root, "config-ran");
48
+ home = join(root, "home");
49
+ moduleMarker = join(root, "module-ran");
50
+ mkdirSync(join(project, ".github", "workflows"), { recursive: true });
51
+ mkdirSync(join(project, "gh"), { recursive: true });
52
+ mkdirSync(join(project, ".git"));
53
+ // The path lexicon's module is project code too. Audit must never import it.
54
+ writeFileSync(
55
+ join(project, "gh", "index.ts"),
56
+ `import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(moduleMarker)}, "ran");\nexport {};\n`,
57
+ );
58
+ writeFileSync(
59
+ join(project, ".github", "workflows", "ci.yml"),
60
+ "on: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n",
61
+ );
62
+ mkdirSync(join(home, ".claude"), { recursive: true });
63
+ writeFileSync(join(home, ".claude", "mcp.json"), JSON.stringify({ mcpServers: { a: { command: "x" } } }));
64
+ writeConfig(`[{ name: "github", module: "./gh/index.ts" }]`);
65
+ });
66
+
67
+ afterEach(() => {
68
+ resetLexiconModules();
69
+ rmSync(root, { recursive: true, force: true });
70
+ });
71
+
72
+ function chant(args: string[], cwd: string): string {
73
+ const result = spawnSync(process.execPath, ["--import", tsxLoader, mainTs, ...args], {
74
+ cwd,
75
+ env: { PATH: process.env.PATH ?? "", HOME: home },
76
+ encoding: "utf-8",
77
+ input: "n\n",
78
+ timeout: 60_000,
79
+ });
80
+ return `${result.stdout}\n${result.stderr}`;
81
+ }
82
+
83
+ const COMMANDS: Array<{ name: string; args: () => string[]; cwd: () => string }> = [
84
+ { name: "init --force", args: () => ["init", project, "--lexicon", "github", "--force", "--skip-mcp"], cwd: () => root },
85
+ { name: "dev onboard", args: () => ["dev", "onboard", "github"], cwd: () => project },
86
+ { name: "import --agents", args: () => ["import", "--agents", "--lexicon", "github", "--output", join(root, "out")], cwd: () => project },
87
+ ];
88
+
89
+ describe("commands that read path lexicons statically run no project code", () => {
90
+ for (const command of COMMANDS) {
91
+ for (const sandbox of [false, true]) {
92
+ test(`${command.name}${sandbox ? " --sandbox" : ""} names the path and never runs chant.config.ts`, () => {
93
+ const output = chant([...command.args(), ...(sandbox ? ["--sandbox"] : [])], command.cwd());
94
+ expect(output).toContain("./gh/index.ts");
95
+ expect(existsSync(marker)).toBe(false);
96
+ }, 90_000);
97
+ }
98
+ }
99
+
100
+ test("audit never runs chant.config.ts or the path lexicon's module", () => {
101
+ chant(["audit", project], root);
102
+ expect(existsSync(marker)).toBe(false);
103
+ expect(existsSync(moduleMarker)).toBe(false);
104
+ }, 90_000);
105
+
106
+ test("a config it cannot read is reported, and still not run", () => {
107
+ writeConfig(`process.env.CI ? ["github"] : [{ name: "github", module: "./gh/index.ts" }]`);
108
+ const output = chant(["dev", "onboard", "github"], project);
109
+ expect(output).toContain("could not read the lexicons in");
110
+ expect(output).toContain("treated as a package");
111
+ expect(existsSync(marker)).toBe(false);
112
+
113
+ const audit = chant(["audit", project], root);
114
+ expect(audit).toContain("could not read the lexicons in");
115
+ expect(existsSync(marker)).toBe(false);
116
+ }, 180_000);
117
+
118
+ test("control: build does run chant.config.ts", () => {
119
+ chant(["build", "."], project);
120
+ expect(existsSync(marker)).toBe(true);
121
+ }, 90_000);
122
+ });
123
+
124
+ describe("audit names a path lexicon by its path and leaves it out of the install line (chant#2589)", () => {
125
+ test("text output", async () => {
126
+ const result = await auditCommand({ path: project, plugins: [] });
127
+ expect(result.status).toBe("no-lexicons");
128
+ expect(result.output).toContain("github is declared by path: ");
129
+ expect(result.output).toContain("gh/index.ts");
130
+ const install = result.output.split("\n").find((line) => line.includes("npx "));
131
+ expect(install).toBeDefined();
132
+ expect(install).not.toContain("@intentius/chant-lexicon-github");
133
+ expect(existsSync(marker)).toBe(false);
134
+ });
135
+
136
+ test("json output", async () => {
137
+ const result = await auditCommand({ path: project, plugins: [], format: "json" });
138
+ const report = JSON.parse(result.output) as { install: string };
139
+ expect(report.install).toMatch(/^npx -p @intentius\/chant( |$)/);
140
+ expect(report.install).not.toContain("@intentius/chant-lexicon-github");
141
+ expect(existsSync(marker)).toBe(false);
142
+ });
143
+
144
+ test("a partial run names the path in its coverage note", async () => {
145
+ // Some other audit lexicon loaded, github not: the workflow is skipped and the note names the path.
146
+ const { loadPlugin } = await import("./plugins");
147
+ const docker = await loadPlugin("docker");
148
+ const result = await auditCommand({ path: project, plugins: [docker] });
149
+ expect(result.output).toContain("github is declared by path: ");
150
+ expect(result.output).not.toContain("npm i @intentius/chant-lexicon-github");
151
+ expect(existsSync(marker)).toBe(false);
152
+ expect(existsSync(moduleMarker)).toBe(false);
153
+ });
154
+ });
155
+
156
+ /**
157
+ * chant#2618 — the other ways into `audit`: main's dispatch run in-process
158
+ * (what `workspace member-run` calls once per member), and `workspace audit`
159
+ * through the CLI, which reaches it through that member-run process.
160
+ */
161
+ describe("chant audit never evaluates the audited repository's config (chant#2618)", () => {
162
+ test("main's in-process dispatch runs neither the config nor the path lexicon's module", async () => {
163
+ const run = await captureRun(() => runCommandInProcess(["audit", project]));
164
+ expect(run.exitCode).toBe(0);
165
+ expect(existsSync(marker)).toBe(false);
166
+ expect(existsSync(moduleMarker)).toBe(false);
167
+ }, 90_000);
168
+
169
+ test("workspace audit runs no member's config and no path lexicon's module", () => {
170
+ // The project becomes the root member of a workspace, with a second
171
+ // member whose config and path lexicon leave markers of their own.
172
+ const apiMarker = join(root, "api-config-ran");
173
+ const apiModuleMarker = join(root, "api-module-ran");
174
+ const api = join(project, "api");
175
+ mkdirSync(join(api, ".github", "workflows"), { recursive: true });
176
+ mkdirSync(join(api, "gh"), { recursive: true });
177
+ writeFileSync(
178
+ join(api, "chant.config.ts"),
179
+ `import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(apiMarker)}, "ran");\n` +
180
+ `export default { lexicons: [{ name: "github", module: "./gh/index.ts" }] };\n`,
181
+ );
182
+ writeFileSync(
183
+ join(api, "gh", "index.ts"),
184
+ `import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(apiModuleMarker)}, "ran");\nexport {};\n`,
185
+ );
186
+ writeFileSync(
187
+ join(api, ".github", "workflows", "ci.yml"),
188
+ "on: push\njobs:\n a:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n",
189
+ );
190
+ writeFileSync(
191
+ join(project, "chant.workspace.json"),
192
+ JSON.stringify({
193
+ name: "acme",
194
+ schema: 1,
195
+ members: [
196
+ { name: "root", dir: ".", kind: "chant" },
197
+ { name: "api", dir: "api", kind: "chant" },
198
+ ],
199
+ }),
200
+ );
201
+ const output = chant(["workspace", "audit", "--json"], project);
202
+ expect(output).toContain('"api"');
203
+ expect(existsSync(marker)).toBe(false);
204
+ expect(existsSync(moduleMarker)).toBe(false);
205
+ expect(existsSync(apiMarker)).toBe(false);
206
+ expect(existsSync(apiModuleMarker)).toBe(false);
207
+ }, 180_000);
208
+ });
@@ -1,5 +1,8 @@
1
1
  import { describe, test, expect, vi, afterEach } from "vitest";
2
- import { extractFromTar, fetchWithRetry, TransientFetchError } from "./fetch";
2
+ import { extractFromTar, fetchWithCache, fetchWithRetry, TransientFetchError } from "./fetch";
3
+ import { mkdtempSync, utimesSync, writeFileSync } from "fs";
4
+ import { tmpdir } from "os";
5
+ import { join } from "path";
3
6
 
4
7
  /**
5
8
  * Build a minimal valid tar buffer with a single file entry.
@@ -318,3 +321,62 @@ describe("fetchWithRetry", () => {
318
321
  expect((err as TransientFetchError).message).toContain("1 attempt");
319
322
  });
320
323
  });
324
+
325
+ describe("fetchWithCache", () => {
326
+ afterEach(() => {
327
+ vi.unstubAllGlobals();
328
+ vi.restoreAllMocks();
329
+ });
330
+
331
+ function cacheAt(ageMs: number, content = "stale bytes"): string {
332
+ const file = join(mkdtempSync(join(tmpdir(), "chant-fetch-cache-")), "spec.bin");
333
+ writeFileSync(file, content);
334
+ const then = (Date.now() - ageMs) / 1000;
335
+ utimesSync(file, then, then);
336
+ return file;
337
+ }
338
+
339
+ test("returns the stale cached copy when every download attempt fails transiently", async () => {
340
+ const cacheFile = cacheAt(2 * 24 * 60 * 60 * 1000);
341
+ const fetchMock = vi.fn().mockRejectedValue(new Error("ECONNRESET"));
342
+ vi.stubGlobal("fetch", fetchMock);
343
+ const errors: string[] = [];
344
+ vi.spyOn(console, "error").mockImplementation((m: unknown) => { errors.push(String(m)); });
345
+
346
+ const data = await fetchWithCache({ url: "https://example.test/spec", cacheFile, retries: 1, backoffMs: 1 });
347
+
348
+ expect(data.toString()).toBe("stale bytes");
349
+ expect(fetchMock).toHaveBeenCalledTimes(2);
350
+ expect(errors.join("\n")).toContain("Using the cached copy");
351
+ });
352
+
353
+ test("force ignores the stale copy and throws", async () => {
354
+ const cacheFile = cacheAt(2 * 24 * 60 * 60 * 1000);
355
+ vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("ECONNRESET")));
356
+
357
+ await expect(
358
+ fetchWithCache({ url: "https://example.test/spec", cacheFile, retries: 1, backoffMs: 1 }, true),
359
+ ).rejects.toBeInstanceOf(TransientFetchError);
360
+ });
361
+
362
+ test("a permanent failure with a stale copy still throws", async () => {
363
+ const cacheFile = cacheAt(2 * 24 * 60 * 60 * 1000);
364
+ vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("gone", { status: 404 })));
365
+
366
+ await expect(
367
+ fetchWithCache({ url: "https://example.test/spec", cacheFile, retries: 1, backoffMs: 1 }),
368
+ ).rejects.toThrow();
369
+ });
370
+
371
+ test("passes attemptTimeoutMs through as the attempt signal", async () => {
372
+ const cacheFile = cacheAt(2 * 24 * 60 * 60 * 1000);
373
+ const fetchMock = vi.fn().mockResolvedValue(new Response("fresh"));
374
+ vi.stubGlobal("fetch", fetchMock);
375
+
376
+ const data = await fetchWithCache({ url: "https://example.test/spec", cacheFile, attemptTimeoutMs: 120_000 });
377
+
378
+ expect(data.toString()).toBe("fresh");
379
+ const init = fetchMock.mock.calls[0][1] as RequestInit;
380
+ expect(init.signal).toBeInstanceOf(AbortSignal);
381
+ });
382
+ });
@@ -18,6 +18,16 @@ export interface FetchConfig {
18
18
  cacheFile: string;
19
19
  /** Cache TTL in milliseconds (default: 24 hours). */
20
20
  cacheTtlMs?: number;
21
+ /**
22
+ * Per-attempt timeout for the download in milliseconds (default: 15 seconds).
23
+ * A source archive of tens of megabytes needs more than the default, which
24
+ * is sized for a JSON schema.
25
+ */
26
+ attemptTimeoutMs?: number;
27
+ /** Retries after the first attempt (default: 4). */
28
+ retries?: number;
29
+ /** Base back-off between attempts in milliseconds (default: 1 second). */
30
+ backoffMs?: number;
21
31
  }
22
32
 
23
33
  // ── Transient-aware fetch ──────────────────────────────────────────
@@ -198,7 +208,26 @@ export async function fetchWithCache(config: FetchConfig, force = false): Promis
198
208
  }
199
209
  }
200
210
 
201
- const response = await fetchWithRetry(config.url);
211
+ let response: Response;
212
+ try {
213
+ response = await fetchWithRetry(
214
+ config.url,
215
+ config.retries ?? DEFAULT_RETRIES,
216
+ config.backoffMs ?? DEFAULT_BACKOFF_MS,
217
+ undefined,
218
+ config.attemptTimeoutMs ?? DEFAULT_ATTEMPT_TIMEOUT_MS,
219
+ );
220
+ } catch (e) {
221
+ // A stale copy beats a failed build. The archives this caches are pinned
222
+ // by tag or by version in the URL, so a copy past its TTL is the same
223
+ // bytes; the TTL exists to notice an unpinned URL moving, not to expire
224
+ // good data. `force` means the caller needs fresh bytes, so it still throws.
225
+ if (!force && e instanceof TransientFetchError && existsSync(config.cacheFile)) {
226
+ console.error(`${e.message} Using the cached copy at ${config.cacheFile} from ${new Date(statSync(config.cacheFile).mtimeMs).toISOString()}.`);
227
+ return readFileSync(config.cacheFile) as unknown as Buffer;
228
+ }
229
+ throw e;
230
+ }
202
231
  const arrayBuffer = await response.arrayBuffer();
203
232
  const data = Buffer.from(arrayBuffer);
204
233