@intentius/chant 0.124.31 → 0.124.33

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 (509) hide show
  1. package/bin/chant +7 -2
  2. package/dist/ci-jobs.d.ts +4 -0
  3. package/dist/ci-jobs.d.ts.map +1 -1
  4. package/dist/ci-jobs.js +9 -2
  5. package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
  6. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  7. package/dist/cli/commands/check-lexicon-plugin.js +10 -7
  8. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  9. package/dist/cli/commands/check-lexicon.js +18 -12
  10. package/dist/cli/commands/import.d.ts +10 -0
  11. package/dist/cli/commands/import.d.ts.map +1 -1
  12. package/dist/cli/commands/import.js +37 -0
  13. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  14. package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
  15. package/dist/cli/commands/onboard.d.ts.map +1 -1
  16. package/dist/cli/commands/onboard.js +8 -4
  17. package/dist/cli/commands/update.d.ts.map +1 -1
  18. package/dist/cli/commands/update.js +2 -1
  19. package/dist/cli/dependencies.d.ts +18 -0
  20. package/dist/cli/dependencies.d.ts.map +1 -0
  21. package/dist/cli/dependencies.js +120 -0
  22. package/dist/cli/handlers/build.d.ts.map +1 -1
  23. package/dist/cli/handlers/build.js +38 -4
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/fan-out.js +12 -4
  26. package/dist/cli/handlers/graph.d.ts.map +1 -1
  27. package/dist/cli/handlers/graph.js +16 -6
  28. package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
  29. package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
  30. package/dist/cli/handlers/lifecycle-health.js +150 -0
  31. package/dist/cli/handlers/lifecycle.d.ts +26 -0
  32. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  33. package/dist/cli/handlers/lifecycle.js +1 -1
  34. package/dist/cli/handlers/lint.d.ts.map +1 -1
  35. package/dist/cli/handlers/lint.js +6 -0
  36. package/dist/cli/handlers/operator.d.ts +2 -2
  37. package/dist/cli/handlers/operator.js +3 -3
  38. package/dist/cli/handlers/pr.d.ts +16 -0
  39. package/dist/cli/handlers/pr.d.ts.map +1 -1
  40. package/dist/cli/handlers/pr.js +610 -32
  41. package/dist/cli/handlers/promote.d.ts.map +1 -1
  42. package/dist/cli/handlers/promote.js +19 -2
  43. package/dist/cli/handlers/release.d.ts +16 -1
  44. package/dist/cli/handlers/release.d.ts.map +1 -1
  45. package/dist/cli/handlers/release.js +83 -11
  46. package/dist/cli/handlers/run.d.ts +13 -1
  47. package/dist/cli/handlers/run.d.ts.map +1 -1
  48. package/dist/cli/handlers/run.js +38 -15
  49. package/dist/cli/main.d.ts.map +1 -1
  50. package/dist/cli/main.js +121 -33
  51. package/dist/cli/parse-args.d.ts.map +1 -1
  52. package/dist/cli/parse-args.js +37 -0
  53. package/dist/cli/registry.d.ts +29 -6
  54. package/dist/cli/registry.d.ts.map +1 -1
  55. package/dist/cli/registry.js +6 -2
  56. package/dist/codegen/generate-post-synth-barrel.js +2 -2
  57. package/dist/codegen/lexicon-regen.d.ts +4 -0
  58. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  59. package/dist/codegen/lexicon-regen.js +1 -1
  60. package/dist/codegen/pinned-upgrade.js +2 -2
  61. package/dist/components/capability.d.ts +44 -3
  62. package/dist/components/capability.d.ts.map +1 -1
  63. package/dist/components/capability.js +21 -0
  64. package/dist/components/changed-files.d.ts +7 -1
  65. package/dist/components/changed-files.d.ts.map +1 -1
  66. package/dist/components/changed-files.js +9 -1
  67. package/dist/components/deploy-inputs.d.ts +91 -0
  68. package/dist/components/deploy-inputs.d.ts.map +1 -0
  69. package/dist/components/deploy-inputs.js +184 -0
  70. package/dist/components/driver.d.ts +38 -0
  71. package/dist/components/driver.d.ts.map +1 -1
  72. package/dist/components/driver.js +56 -6
  73. package/dist/components/fan-out-record.d.ts +6 -1
  74. package/dist/components/fan-out-record.d.ts.map +1 -1
  75. package/dist/components/fan-out-record.js +24 -0
  76. package/dist/components/fan-out-run.d.ts +20 -0
  77. package/dist/components/fan-out-run.d.ts.map +1 -1
  78. package/dist/components/fan-out-run.js +21 -1
  79. package/dist/components/gate-plan.d.ts +8 -0
  80. package/dist/components/gate-plan.d.ts.map +1 -1
  81. package/dist/components/gate-plan.js +6 -0
  82. package/dist/components/pr-pipeline.d.ts +98 -12
  83. package/dist/components/pr-pipeline.d.ts.map +1 -1
  84. package/dist/components/pr-pipeline.js +146 -23
  85. package/dist/components/pr-run.d.ts +128 -30
  86. package/dist/components/pr-run.d.ts.map +1 -1
  87. package/dist/components/pr-run.js +290 -70
  88. package/dist/components/release-compile.d.ts +37 -2
  89. package/dist/components/release-compile.d.ts.map +1 -1
  90. package/dist/components/release-compile.js +64 -8
  91. package/dist/components/release-promote.d.ts +17 -3
  92. package/dist/components/release-promote.d.ts.map +1 -1
  93. package/dist/components/release-promote.js +79 -18
  94. package/dist/components/verbs/bom-writer.d.ts +1 -1
  95. package/dist/components/verbs/bom-writer.d.ts.map +1 -1
  96. package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
  97. package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
  98. package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
  99. package/dist/components/wave-plan.d.ts +15 -2
  100. package/dist/components/wave-plan.d.ts.map +1 -1
  101. package/dist/components/wave-plan.js +44 -5
  102. package/dist/discovery/fold-import.d.ts.map +1 -1
  103. package/dist/discovery/fold-import.js +16 -6
  104. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  105. package/dist/discovery/sandbox/bundle.js +6 -0
  106. package/dist/discovery/specifier-resolve.d.ts.map +1 -1
  107. package/dist/discovery/specifier-resolve.js +8 -2
  108. package/dist/fold/fold.d.ts.map +1 -1
  109. package/dist/fold/fold.js +68 -1
  110. package/dist/fold/subset.d.ts +37 -0
  111. package/dist/fold/subset.d.ts.map +1 -1
  112. package/dist/fold/subset.js +88 -0
  113. package/dist/graph-detail.d.ts.map +1 -1
  114. package/dist/graph-detail.js +32 -7
  115. package/dist/health.d.ts +110 -0
  116. package/dist/health.d.ts.map +1 -0
  117. package/dist/health.js +105 -0
  118. package/dist/health.schema.json +191 -0
  119. package/dist/lexicon-module.d.ts +10 -0
  120. package/dist/lexicon-module.d.ts.map +1 -1
  121. package/dist/lexicon-module.js +53 -8
  122. package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
  123. package/dist/lexicon-plugin-helpers.js +6 -4
  124. package/dist/lexicon.d.ts +29 -0
  125. package/dist/lexicon.d.ts.map +1 -1
  126. package/dist/lifecycle/affected.d.ts.map +1 -1
  127. package/dist/lifecycle/affected.js +7 -1
  128. package/dist/lifecycle/merge-record.d.ts +104 -0
  129. package/dist/lifecycle/merge-record.d.ts.map +1 -0
  130. package/dist/lifecycle/merge-record.js +102 -0
  131. package/dist/lifecycle/publish-attachments.d.ts +8 -5
  132. package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
  133. package/dist/lifecycle/publish-attachments.js +27 -13
  134. package/dist/lifecycle/publish-npm.d.ts +3 -2
  135. package/dist/lifecycle/publish-npm.d.ts.map +1 -1
  136. package/dist/lifecycle/publish-npm.js +4 -3
  137. package/dist/lifecycle/release-at.d.ts +40 -0
  138. package/dist/lifecycle/release-at.d.ts.map +1 -0
  139. package/dist/lifecycle/release-at.js +83 -0
  140. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  141. package/dist/lifecycle/release-cut.js +23 -12
  142. package/dist/lifecycle/release-ledger.d.ts +21 -0
  143. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  144. package/dist/lifecycle/release-ledger.js +24 -0
  145. package/dist/lifecycle/release-publish.d.ts +28 -12
  146. package/dist/lifecycle/release-publish.d.ts.map +1 -1
  147. package/dist/lifecycle/release-publish.js +29 -17
  148. package/dist/lifecycle/release-store.d.ts +25 -2
  149. package/dist/lifecycle/release-store.d.ts.map +1 -1
  150. package/dist/lifecycle/release-store.js +40 -13
  151. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  152. package/dist/lifecycle/run-ledger.js +1 -0
  153. package/dist/lifecycle/work-dir.d.ts +45 -0
  154. package/dist/lifecycle/work-dir.d.ts.map +1 -0
  155. package/dist/lifecycle/work-dir.js +78 -0
  156. package/dist/lint/discover.d.ts +17 -9
  157. package/dist/lint/discover.d.ts.map +1 -1
  158. package/dist/lint/discover.js +62 -54
  159. package/dist/lint/rules/op/index.d.ts +2 -1
  160. package/dist/lint/rules/op/index.d.ts.map +1 -1
  161. package/dist/lint/rules/op/index.js +4 -2
  162. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
  163. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
  164. package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
  165. package/dist/module-source.d.ts +57 -0
  166. package/dist/module-source.d.ts.map +1 -0
  167. package/dist/module-source.js +104 -0
  168. package/dist/op/activities/activity-contracts.d.ts +5 -0
  169. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  170. package/dist/op/activities/activity-contracts.js +3 -0
  171. package/dist/op/activities/factory.js +1 -1
  172. package/dist/op/activities/shell.d.ts +38 -0
  173. package/dist/op/activities/shell.d.ts.map +1 -1
  174. package/dist/op/activities/shell.js +89 -3
  175. package/dist/op/builders.d.ts +7 -0
  176. package/dist/op/builders.d.ts.map +1 -1
  177. package/dist/op/builders.js +7 -0
  178. package/dist/op/discover.d.ts.map +1 -1
  179. package/dist/op/discover.js +13 -3
  180. package/dist/op/gate.d.ts.map +1 -1
  181. package/dist/op/gate.js +3 -0
  182. package/dist/op/op-waves-run.d.ts.map +1 -1
  183. package/dist/op/op-waves-run.js +20 -5
  184. package/dist/op/operator.d.ts +2 -2
  185. package/dist/op/operator.js +2 -2
  186. package/dist/op/runtime.d.ts +2 -0
  187. package/dist/op/runtime.d.ts.map +1 -1
  188. package/dist/op/steward.d.ts +6 -4
  189. package/dist/op/steward.d.ts.map +1 -1
  190. package/dist/op/steward.js +5 -3
  191. package/dist/op/work-lease-run.d.ts +20 -7
  192. package/dist/op/work-lease-run.d.ts.map +1 -1
  193. package/dist/op/work-lease-run.js +56 -12
  194. package/dist/pr-forge.d.ts +10 -0
  195. package/dist/pr-forge.d.ts.map +1 -1
  196. package/dist/pr-forge.js +39 -0
  197. package/dist/pr-loop.d.ts +155 -7
  198. package/dist/pr-loop.d.ts.map +1 -1
  199. package/dist/pr-loop.js +137 -11
  200. package/dist/pr-report-url.d.ts +4 -4
  201. package/dist/pr-report-url.d.ts.map +1 -1
  202. package/dist/pr-report-url.js +5 -5
  203. package/dist/runner-start.d.ts +179 -0
  204. package/dist/runner-start.d.ts.map +1 -0
  205. package/dist/runner-start.js +277 -0
  206. package/dist/ts-loader.d.ts +38 -4
  207. package/dist/ts-loader.d.ts.map +1 -1
  208. package/dist/ts-loader.js +190 -16
  209. package/dist/wave-runtime.mjs +238 -231
  210. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  211. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  212. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  213. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  214. package/dist/workspace/chant-migrations.js +2 -0
  215. package/dist/workspace/check.schema.json +70 -2
  216. package/dist/workspace/checks/boxes.d.ts +6 -6
  217. package/dist/workspace/checks/boxes.js +9 -9
  218. package/dist/workspace/checks/security.d.ts +35 -0
  219. package/dist/workspace/checks/security.d.ts.map +1 -0
  220. package/dist/workspace/checks/security.js +305 -0
  221. package/dist/workspace/checks.d.ts +2 -0
  222. package/dist/workspace/checks.d.ts.map +1 -1
  223. package/dist/workspace/checks.js +5 -0
  224. package/dist/workspace/ci-builds.d.ts +163 -0
  225. package/dist/workspace/ci-builds.d.ts.map +1 -0
  226. package/dist/workspace/ci-builds.js +315 -0
  227. package/dist/workspace/ci-green-cli.d.ts +2 -1
  228. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  229. package/dist/workspace/ci-green-cli.js +25 -5
  230. package/dist/workspace/ci-green-forge.d.ts +18 -4
  231. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  232. package/dist/workspace/ci-green-forge.js +12 -0
  233. package/dist/workspace/ci-green-workflow.d.ts +55 -1
  234. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  235. package/dist/workspace/ci-green-workflow.js +128 -1
  236. package/dist/workspace/ci-green.d.ts +117 -16
  237. package/dist/workspace/ci-green.d.ts.map +1 -1
  238. package/dist/workspace/ci-green.js +358 -43
  239. package/dist/workspace/ci-last-green.schema.json +49 -0
  240. package/dist/workspace/ci-list.schema.json +47 -7
  241. package/dist/workspace/ci-report-url.d.ts +18 -0
  242. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  243. package/dist/workspace/ci-report-url.js +33 -0
  244. package/dist/workspace/ci-run-index.d.ts +13 -1
  245. package/dist/workspace/ci-run-index.d.ts.map +1 -1
  246. package/dist/workspace/ci-run-index.js +6 -4
  247. package/dist/workspace/ci-run-index.schema.json +6 -4
  248. package/dist/workspace/ci-run.d.ts +85 -6
  249. package/dist/workspace/ci-run.d.ts.map +1 -1
  250. package/dist/workspace/ci-run.js +262 -64
  251. package/dist/workspace/ci-runner-key.d.ts +16 -3
  252. package/dist/workspace/ci-runner-key.d.ts.map +1 -1
  253. package/dist/workspace/ci-runner-key.js +30 -10
  254. package/dist/workspace/ci-runner-start.d.ts +77 -0
  255. package/dist/workspace/ci-runner-start.d.ts.map +1 -0
  256. package/dist/workspace/ci-runner-start.js +185 -0
  257. package/dist/workspace/ci-serve.d.ts +130 -11
  258. package/dist/workspace/ci-serve.d.ts.map +1 -1
  259. package/dist/workspace/ci-serve.js +323 -40
  260. package/dist/workspace/declaration.d.ts +25 -0
  261. package/dist/workspace/declaration.d.ts.map +1 -1
  262. package/dist/workspace/declaration.js +14 -2
  263. package/dist/workspace/declaration.schema.json +30 -2
  264. package/dist/workspace/export.d.ts +1 -1
  265. package/dist/workspace/export.d.ts.map +1 -1
  266. package/dist/workspace/export.js +7 -3
  267. package/dist/workspace/ls.d.ts +8 -1
  268. package/dist/workspace/ls.d.ts.map +1 -1
  269. package/dist/workspace/ls.js +2 -1
  270. package/dist/workspace/ls.schema.json +9 -2
  271. package/dist/workspace/member-pipeline.d.ts +1 -0
  272. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  273. package/dist/workspace/member-pipeline.js +2 -0
  274. package/dist/workspace/merge-record.schema.json +49 -0
  275. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  276. package/dist/workspace/pin-integrity.js +3 -2
  277. package/dist/workspace/pr-report.schema.json +75 -8
  278. package/dist/workspace/reason-codes.d.ts +18 -1
  279. package/dist/workspace/reason-codes.d.ts.map +1 -1
  280. package/dist/workspace/reason-codes.js +20 -2
  281. package/dist/workspace/release-candidates.d.ts +129 -0
  282. package/dist/workspace/release-candidates.d.ts.map +1 -0
  283. package/dist/workspace/release-candidates.js +233 -0
  284. package/dist/workspace/release-candidates.schema.json +353 -0
  285. package/dist/workspace/run-event.schema.json +3 -1
  286. package/dist/workspace/security.d.ts +40 -0
  287. package/dist/workspace/security.d.ts.map +1 -0
  288. package/dist/workspace/security.js +99 -0
  289. package/dist/workspace/status.schema.json +3 -1
  290. package/dist/workspace/write-scope.d.ts +9 -0
  291. package/dist/workspace/write-scope.d.ts.map +1 -1
  292. package/dist/workspace/write-scope.js +11 -1
  293. package/package.json +8 -1
  294. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  295. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
  296. package/src/ci-jobs.ts +11 -2
  297. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  298. package/src/cli/commands/audit.test.ts +2 -2
  299. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  300. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  301. package/src/cli/commands/check-lexicon.test.ts +1 -1
  302. package/src/cli/commands/check-lexicon.ts +16 -11
  303. package/src/cli/commands/import.ts +40 -0
  304. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  305. package/src/cli/commands/init-lexicon.test.ts +8 -5
  306. package/src/cli/commands/onboard.test.ts +45 -0
  307. package/src/cli/commands/onboard.ts +8 -4
  308. package/src/cli/commands/update.test.ts +6 -0
  309. package/src/cli/commands/update.ts +4 -1
  310. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  311. package/src/cli/dependencies.e2e.test.ts +101 -0
  312. package/src/cli/dependencies.test.ts +93 -0
  313. package/src/cli/dependencies.ts +122 -0
  314. package/src/cli/handlers/build.test.ts +105 -0
  315. package/src/cli/handlers/build.ts +35 -4
  316. package/src/cli/handlers/fan-out.test.ts +73 -0
  317. package/src/cli/handlers/fan-out.ts +11 -4
  318. package/src/cli/handlers/graph.test.ts +76 -0
  319. package/src/cli/handlers/graph.ts +19 -12
  320. package/src/cli/handlers/lifecycle-health.test.ts +123 -0
  321. package/src/cli/handlers/lifecycle-health.ts +157 -0
  322. package/src/cli/handlers/lifecycle.ts +1 -1
  323. package/src/cli/handlers/lint.test.ts +42 -0
  324. package/src/cli/handlers/lint.ts +6 -0
  325. package/src/cli/handlers/operator.ts +3 -3
  326. package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
  327. package/src/cli/handlers/pr-record.test.ts +201 -0
  328. package/src/cli/handlers/pr-waves.test.ts +449 -0
  329. package/src/cli/handlers/pr.test.ts +156 -0
  330. package/src/cli/handlers/pr.ts +648 -33
  331. package/src/cli/handlers/promote.test.ts +43 -1
  332. package/src/cli/handlers/promote.ts +19 -3
  333. package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
  334. package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
  335. package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
  336. package/src/cli/handlers/release.e2e.test.ts +19 -28
  337. package/src/cli/handlers/release.ts +81 -11
  338. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  339. package/src/cli/handlers/run-signer.test.ts +79 -0
  340. package/src/cli/handlers/run.ts +34 -16
  341. package/src/cli/main.test.ts +18 -0
  342. package/src/cli/main.ts +120 -33
  343. package/src/cli/parse-args.ts +28 -0
  344. package/src/cli/registry.ts +35 -8
  345. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  346. package/src/codegen/lexicon-regen.ts +1 -1
  347. package/src/codegen/pinned-upgrade.ts +2 -2
  348. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  349. package/src/components/capability.ts +51 -3
  350. package/src/components/changed-files.test.ts +16 -1
  351. package/src/components/changed-files.ts +9 -1
  352. package/src/components/deploy-inputs.test.ts +108 -0
  353. package/src/components/deploy-inputs.ts +225 -0
  354. package/src/components/driver.test.ts +36 -1
  355. package/src/components/driver.ts +87 -4
  356. package/src/components/fan-out-record.ts +29 -1
  357. package/src/components/fan-out-run.test.ts +35 -0
  358. package/src/components/fan-out-run.ts +40 -1
  359. package/src/components/gate-plan.ts +9 -0
  360. package/src/components/pr-pipeline.test.ts +100 -0
  361. package/src/components/pr-pipeline.ts +184 -28
  362. package/src/components/pr-run.test.ts +323 -18
  363. package/src/components/pr-run.ts +355 -76
  364. package/src/components/release-compile.test.ts +34 -0
  365. package/src/components/release-compile.ts +89 -8
  366. package/src/components/release-promote.test.ts +55 -2
  367. package/src/components/release-promote.ts +93 -23
  368. package/src/components/verbs/bom-writer.ts +1 -1
  369. package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
  370. package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
  371. package/src/components/wave-plan.ts +48 -8
  372. package/src/discovery/fold-host-method.test.ts +87 -0
  373. package/src/discovery/fold-import.ts +15 -5
  374. package/src/discovery/fold-package-composite.test.ts +27 -0
  375. package/src/discovery/fold-specifiers.test.ts +8 -0
  376. package/src/discovery/sandbox/bundle.ts +6 -0
  377. package/src/discovery/specifier-resolve.ts +8 -2
  378. package/src/fold/fold.test.ts +72 -0
  379. package/src/fold/fold.ts +71 -0
  380. package/src/fold/subset.test.ts +31 -0
  381. package/src/fold/subset.ts +99 -0
  382. package/src/graph-detail.test.ts +57 -0
  383. package/src/graph-detail.ts +33 -7
  384. package/src/health.schema.json +191 -0
  385. package/src/health.test.ts +62 -0
  386. package/src/health.ts +178 -0
  387. package/src/lexicon-dist.e2e.test.ts +254 -0
  388. package/src/lexicon-module.test.ts +47 -0
  389. package/src/lexicon-module.ts +49 -7
  390. package/src/lexicon-plugin-helpers.ts +6 -4
  391. package/src/lexicon.ts +43 -1
  392. package/src/lifecycle/affected.test.ts +7 -0
  393. package/src/lifecycle/affected.ts +6 -1
  394. package/src/lifecycle/merge-record.test.ts +111 -0
  395. package/src/lifecycle/merge-record.ts +154 -0
  396. package/src/lifecycle/publish-attachments.ts +25 -13
  397. package/src/lifecycle/publish-npm.ts +4 -3
  398. package/src/lifecycle/publish-targets.test.ts +34 -4
  399. package/src/lifecycle/release-at.test.ts +48 -0
  400. package/src/lifecycle/release-at.ts +100 -0
  401. package/src/lifecycle/release-cut.test.ts +41 -0
  402. package/src/lifecycle/release-cut.ts +22 -11
  403. package/src/lifecycle/release-ledger.ts +34 -0
  404. package/src/lifecycle/release-publish.test.ts +43 -13
  405. package/src/lifecycle/release-publish.ts +38 -18
  406. package/src/lifecycle/release-store.ts +57 -13
  407. package/src/lifecycle/run-ledger.test.ts +13 -0
  408. package/src/lifecycle/run-ledger.ts +1 -0
  409. package/src/lifecycle/work-dir.test.ts +77 -0
  410. package/src/lifecycle/work-dir.ts +82 -0
  411. package/src/lint/discover.test.ts +46 -2
  412. package/src/lint/discover.ts +55 -49
  413. package/src/lint/rules/op/index.ts +4 -2
  414. package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
  415. package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
  416. package/src/module-source.test.ts +85 -0
  417. package/src/module-source.ts +112 -0
  418. package/src/op/activities/activity-contracts.ts +3 -0
  419. package/src/op/activities/factory.ts +1 -1
  420. package/src/op/activities/shell-container.e2e.test.ts +67 -0
  421. package/src/op/activities/shell.test.ts +44 -1
  422. package/src/op/activities/shell.ts +110 -3
  423. package/src/op/builders.ts +7 -0
  424. package/src/op/discover-root.test.ts +31 -2
  425. package/src/op/discover.ts +14 -4
  426. package/src/op/gate.test.ts +30 -0
  427. package/src/op/gate.ts +3 -0
  428. package/src/op/op-waves-run.ts +20 -5
  429. package/src/op/op-waves.test.ts +49 -0
  430. package/src/op/operator.ts +2 -2
  431. package/src/op/runtime.ts +2 -0
  432. package/src/op/step-console.test.ts +4 -2
  433. package/src/op/steward.ts +6 -4
  434. package/src/op/work-lease-run.test.ts +88 -2
  435. package/src/op/work-lease-run.ts +52 -12
  436. package/src/pr-forge-bundle.test.ts +2 -2
  437. package/src/pr-forge.test.ts +42 -0
  438. package/src/pr-forge.ts +40 -0
  439. package/src/pr-loop-bundle.test.ts +2 -1
  440. package/src/pr-loop.test.ts +146 -1
  441. package/src/pr-loop.ts +278 -17
  442. package/src/pr-report-url.ts +7 -7
  443. package/src/runner-start-bundle.test.ts +29 -0
  444. package/src/runner-start.test.ts +179 -0
  445. package/src/runner-start.ts +329 -0
  446. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  447. package/src/ts-loader.test.ts +89 -1
  448. package/src/ts-loader.ts +180 -13
  449. package/src/workspace/changes.test.ts +1 -1
  450. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  451. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  452. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  453. package/src/workspace/chant-migrations.ts +2 -0
  454. package/src/workspace/check-contract.test.ts +3 -2
  455. package/src/workspace/check.schema.json +70 -2
  456. package/src/workspace/checks/boxes.ts +9 -9
  457. package/src/workspace/checks/security.test.ts +138 -0
  458. package/src/workspace/checks/security.ts +305 -0
  459. package/src/workspace/checks.ts +5 -0
  460. package/src/workspace/ci-builds.e2e.test.ts +208 -0
  461. package/src/workspace/ci-builds.test.ts +174 -0
  462. package/src/workspace/ci-builds.ts +385 -0
  463. package/src/workspace/ci-green-builds.test.ts +180 -0
  464. package/src/workspace/ci-green-cli.test.ts +54 -0
  465. package/src/workspace/ci-green-cli.ts +26 -5
  466. package/src/workspace/ci-green-forge.ts +30 -4
  467. package/src/workspace/ci-green-runs.test.ts +53 -11
  468. package/src/workspace/ci-green-workflow.test.ts +84 -1
  469. package/src/workspace/ci-green-workflow.ts +139 -2
  470. package/src/workspace/ci-green.test.ts +6 -3
  471. package/src/workspace/ci-green.ts +408 -48
  472. package/src/workspace/ci-last-green.schema.json +49 -0
  473. package/src/workspace/ci-list.schema.json +47 -7
  474. package/src/workspace/ci-report-url.test.ts +220 -0
  475. package/src/workspace/ci-report-url.ts +33 -0
  476. package/src/workspace/ci-run-index.schema.json +6 -4
  477. package/src/workspace/ci-run-index.test.ts +42 -3
  478. package/src/workspace/ci-run-index.ts +19 -5
  479. package/src/workspace/ci-run.test.ts +203 -8
  480. package/src/workspace/ci-run.ts +295 -63
  481. package/src/workspace/ci-runner-key.test.ts +55 -17
  482. package/src/workspace/ci-runner-key.ts +36 -11
  483. package/src/workspace/ci-runner-start.test.ts +151 -0
  484. package/src/workspace/ci-runner-start.ts +193 -0
  485. package/src/workspace/ci-serve-builds.test.ts +86 -0
  486. package/src/workspace/ci-serve.test.ts +209 -6
  487. package/src/workspace/ci-serve.ts +341 -42
  488. package/src/workspace/declaration.schema.json +30 -2
  489. package/src/workspace/declaration.test.ts +40 -0
  490. package/src/workspace/declaration.ts +40 -3
  491. package/src/workspace/export.ts +6 -3
  492. package/src/workspace/ls.schema.json +9 -2
  493. package/src/workspace/ls.ts +10 -4
  494. package/src/workspace/member-pipeline.ts +2 -1
  495. package/src/workspace/merge-record.schema.json +49 -0
  496. package/src/workspace/pin-integrity.test.ts +5 -0
  497. package/src/workspace/pin-integrity.ts +3 -2
  498. package/src/workspace/pr-report.schema.json +75 -8
  499. package/src/workspace/reason-codes.test.ts +2 -0
  500. package/src/workspace/reason-codes.ts +20 -2
  501. package/src/workspace/release-candidates.schema.json +353 -0
  502. package/src/workspace/release-candidates.test.ts +106 -0
  503. package/src/workspace/release-candidates.ts +315 -0
  504. package/src/workspace/run-event.schema.json +3 -1
  505. package/src/workspace/security.test.ts +70 -0
  506. package/src/workspace/security.ts +118 -0
  507. package/src/workspace/status.schema.json +3 -1
  508. package/src/workspace/write-scope.test.ts +29 -1
  509. package/src/workspace/write-scope.ts +12 -1
@@ -1,7 +1,9 @@
1
- import { describe, test, expect } from "vitest";
1
+ import { describe, test, expect, beforeAll, afterAll } from "vitest";
2
2
  import { join, dirname } from "path";
3
3
  import { fileURLToPath } from "url";
4
- import { discoverLintRules, discoverPostSynthChecks } from "./discover";
4
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
5
+ import { tmpdir } from "os";
6
+ import { discoverLintRules, discoverPostSynthChecks, listRuleFiles } from "./discover";
5
7
 
6
8
  const thisDir = dirname(fileURLToPath(import.meta.url));
7
9
 
@@ -67,3 +69,45 @@ describe("discoverPostSynthChecks", () => {
67
69
  expect(checks.length).toBe(0);
68
70
  });
69
71
  });
72
+
73
+ describe("rule modules compiled to JS (#3981)", () => {
74
+ let dir: string;
75
+
76
+ beforeAll(() => {
77
+ // A compiled rules directory: ES modules in a "type": "module" package, each with its declarations beside it.
78
+ dir = mkdtempSync(join(tmpdir(), "chant-discover-js-"));
79
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ type: "module" }));
80
+ writeFileSync(join(dir, "alpha.js"), `export const alpha = { id: "JS002", severity: "warning", category: "style", check: () => [] };\n`);
81
+ writeFileSync(join(dir, "alpha.d.ts"), `export declare const alpha: unknown;\n`);
82
+ writeFileSync(join(dir, "beta.js"), `export const beta = { id: "JS001", severity: "error", category: "security", check: () => [] };\n`);
83
+ writeFileSync(join(dir, "beta.d.ts"), `export declare const beta: unknown;\n`);
84
+ writeFileSync(join(dir, "gamma.js"), `export const gamma = { id: "JSP001", description: "a post-synth check", check: () => [] };\n`);
85
+ writeFileSync(join(dir, "index.js"), `export * from "./alpha.js";\n`);
86
+ writeFileSync(join(dir, "rule-helpers.js"), `export const helper = 1;\n`);
87
+ writeFileSync(join(dir, "alpha.test.js"), `throw new Error("a test is not a rule");\n`);
88
+ });
89
+
90
+ afterAll(() => rmSync(dir, { recursive: true, force: true }));
91
+
92
+ test("listRuleFiles lists .js modules and leaves out declarations, tests, helpers and the index", () => {
93
+ expect(listRuleFiles(dir).sort()).toEqual(["alpha.js", "beta.js", "gamma.js"]);
94
+ });
95
+
96
+ test("a source module and its compiled twin list once, as the source", () => {
97
+ const both = mkdtempSync(join(tmpdir(), "chant-discover-both-"));
98
+ try {
99
+ for (const f of ["one.ts", "one.js", "one.d.ts", "two.js"]) writeFileSync(join(both, f), "");
100
+ expect(listRuleFiles(both).sort()).toEqual(["one.ts", "two.js"]);
101
+ } finally {
102
+ rmSync(both, { recursive: true, force: true });
103
+ }
104
+ });
105
+
106
+ test("discoverLintRules loads compiled rules", () => {
107
+ expect(discoverLintRules(dir, import.meta.url).map((r) => r.id)).toEqual(["JS001", "JS002"]);
108
+ });
109
+
110
+ test("discoverPostSynthChecks loads compiled checks", () => {
111
+ expect(discoverPostSynthChecks(dir, import.meta.url).map((c) => c.id)).toEqual(["JSP001"]);
112
+ });
113
+ });
@@ -1,8 +1,11 @@
1
1
  /**
2
2
  * Auto-discovery for lint rules and post-synth checks.
3
3
  *
4
- * Scans directories for .ts files and loads exported rules/checks,
5
- * eliminating the need to manually list every rule in plugin.ts.
4
+ * Scans directories for rule modules and loads exported rules/checks,
5
+ * eliminating the need to manually list every rule in plugin.ts. A lexicon
6
+ * run from source has `.ts` rule modules; one run compiled has `.js` modules
7
+ * beside `.d.ts` declarations (#3981). Both are found, and tsx is registered
8
+ * only when a module found is TypeScript.
6
9
  */
7
10
 
8
11
  import { readdirSync } from "fs";
@@ -11,6 +14,7 @@ import { createRequire } from "module";
11
14
  import type { LintRule } from "./rule";
12
15
  import type { PostSynthCheck } from "./post-synth";
13
16
  import { ensureTypeScriptLoader } from "../ts-loader";
17
+ import { isTypeScriptModule } from "../module-source";
14
18
 
15
19
  let cjsTypeScript = false;
16
20
 
@@ -22,7 +26,8 @@ let cjsTypeScript = false;
22
26
  * From source this runs when the module loads, as it always has: vitest and
23
27
  * the source CLI rely on tsx's CommonJS hook being in place from then on. The
24
28
  * compiled CLI imports this module on every start and most commands read no
25
- * rule directory, so there it waits for the first one (#3878).
29
+ * rule directory, so there it waits for the first directory that holds a
30
+ * TypeScript rule module (#3878, #3981).
26
31
  */
27
32
  function registerTypeScriptRequire(): void {
28
33
  if (cjsTypeScript) return;
@@ -40,82 +45,83 @@ function registerTypeScriptRequire(): void {
40
45
  if (!import.meta.url.endsWith(".js")) registerTypeScriptRequire();
41
46
 
42
47
  /**
43
- * Discover lint rules from a directory.
44
- *
45
- * Scans the directory for .ts files (excluding tests, helpers, and index files),
46
- * loads each module, and collects any exported objects that look like a LintRule
47
- * (has `id`, `severity`, `category`, and `check` function).
48
- *
49
- * @param dir - Absolute path to the rules directory
50
- * @param importMetaUrl - The caller's import.meta.url (for createRequire)
48
+ * Load every rule module in `dir` and collect the exports `accept` takes.
49
+ * Registers tsx first only when one of the modules is TypeScript.
51
50
  */
52
- export function discoverLintRules(dir: string, importMetaUrl: string): LintRule[] {
53
- registerTypeScriptRequire();
51
+ function discoverExports<T>(dir: string, importMetaUrl: string, accept: (value: unknown) => value is T): T[] {
52
+ const files = listRuleFiles(dir);
53
+ if (files.some(isTypeScriptModule)) registerTypeScriptRequire();
54
54
  const require = createRequire(importMetaUrl);
55
- const rules: LintRule[] = [];
56
-
57
- for (const file of listRuleFiles(dir)) {
55
+ const found: T[] = [];
56
+ for (const file of files) {
58
57
  try {
59
- // Strip .ts extension — require() resolves without it
60
- const modPath = join(dir, file.replace(/\.ts$/, ""));
61
- const mod = require(modPath);
58
+ const mod = require(join(dir, file)) as Record<string, unknown>;
62
59
  for (const exp of Object.values(mod)) {
63
- if (isLintRule(exp)) rules.push(exp);
60
+ if (accept(exp)) found.push(exp);
64
61
  }
65
62
  } catch {
66
63
  // Skip files that fail to load
67
64
  }
68
65
  }
66
+ return found;
67
+ }
69
68
 
70
- return rules.sort((a, b) => a.id.localeCompare(b.id));
69
+ /**
70
+ * Discover lint rules from a directory.
71
+ *
72
+ * Scans the directory for rule modules (see {@link listRuleFiles}), loads
73
+ * each one, and collects any exported objects that look like a LintRule (has
74
+ * `id`, `severity`, `category`, and `check` function).
75
+ *
76
+ * @param dir - Absolute path to the rules directory
77
+ * @param importMetaUrl - The caller's import.meta.url (for createRequire)
78
+ */
79
+ export function discoverLintRules(dir: string, importMetaUrl: string): LintRule[] {
80
+ return discoverExports(dir, importMetaUrl, isLintRule).sort((a, b) => a.id.localeCompare(b.id));
71
81
  }
72
82
 
73
83
  /**
74
84
  * Discover post-synth checks from a directory.
75
85
  *
76
- * Scans the directory for .ts files (excluding tests, helpers, and index files),
77
- * loads each module, and collects any exported objects that look like a PostSynthCheck
78
- * (has `id`, `description`, and `check` function).
86
+ * Scans the directory for rule modules (see {@link listRuleFiles}), loads
87
+ * each one, and collects any exported objects that look like a
88
+ * PostSynthCheck (has `id`, `description`, and `check` function).
79
89
  *
80
90
  * @param dir - Absolute path to the post-synth rules directory
81
91
  * @param importMetaUrl - The caller's import.meta.url (for createRequire)
82
92
  */
83
93
  export function discoverPostSynthChecks(dir: string, importMetaUrl: string): PostSynthCheck[] {
84
- registerTypeScriptRequire();
85
- const require = createRequire(importMetaUrl);
86
- const checks: PostSynthCheck[] = [];
87
-
88
- for (const file of listRuleFiles(dir)) {
89
- try {
90
- // Strip .ts extension — require() resolves without it
91
- const modPath = join(dir, file.replace(/\.ts$/, ""));
92
- const mod = require(modPath);
93
- for (const exp of Object.values(mod)) {
94
- if (isPostSynthCheck(exp)) checks.push(exp);
95
- }
96
- } catch {
97
- // Skip files that fail to load
98
- }
99
- }
94
+ return discoverExports(dir, importMetaUrl, isPostSynthCheck).sort((a, b) => a.id.localeCompare(b.id));
95
+ }
100
96
 
101
- return checks.sort((a, b) => a.id.localeCompare(b.id));
97
+ /** A rule module's name without its extension: `foo` for `foo.ts` and `foo.js`. */
98
+ export function ruleModuleBase(file: string): string {
99
+ return file.replace(/\.(?:ts|js)$/, "");
102
100
  }
103
101
 
104
102
  /**
105
- * List rule files in a directory (excluding tests, helpers, index).
103
+ * List rule modules in a directory: `.ts` source or compiled `.js`, leaving
104
+ * out declarations (`.d.ts`), tests, helpers and the index. A directory
105
+ * holding both `foo.ts` and `foo.js` lists `foo.ts` alone, so a rule is
106
+ * never loaded twice.
106
107
  */
107
108
  export function listRuleFiles(dir: string): string[] {
109
+ let entries: string[];
108
110
  try {
109
- return readdirSync(dir).filter((f) => {
110
- if (!f.endsWith(".ts")) return false;
111
- if (f.endsWith(".test.ts")) return false;
112
- if (f === "index.ts") return false;
113
- if (f.includes("helper")) return false;
114
- return true;
115
- });
111
+ entries = readdirSync(dir);
116
112
  } catch {
117
113
  return [];
118
114
  }
115
+ const modules = entries.filter((f) => {
116
+ if (!f.endsWith(".ts") && !f.endsWith(".js")) return false;
117
+ if (f.endsWith(".d.ts")) return false;
118
+ if (/\.test\.[tj]s$/.test(f)) return false;
119
+ if (f === "index.ts" || f === "index.js") return false;
120
+ if (f.includes("helper")) return false;
121
+ return true;
122
+ });
123
+ const sources = new Set(modules.filter((f) => f.endsWith(".ts")).map(ruleModuleBase));
124
+ return modules.filter((f) => f.endsWith(".ts") || !sources.has(ruleModuleBase(f)));
119
125
  }
120
126
 
121
127
  function isLintRule(obj: unknown): obj is LintRule {
@@ -16,6 +16,7 @@ import { ops014 } from "./ops014-converge-rule-refusals";
16
16
  import { ops015 } from "./ops015-gate-approval";
17
17
  import { ops016 } from "./ops016-gate-resume";
18
18
  import { ops017Check } from "./ops017-gate-alias";
19
+ import { ops018 } from "./ops018-shell-image-digest";
19
20
  import { sys010Check } from "./sys010-key-over-plain-http";
20
21
  import type { DecideBackend } from "../../../op/decide-config";
21
22
 
@@ -25,6 +26,7 @@ export { ops014 } from "./ops014-converge-rule-refusals";
25
26
  export { ops015 } from "./ops015-gate-approval";
26
27
  export { ops016 } from "./ops016-gate-resume";
27
28
  export { ops017, ops017Check, workspaceIdentityGates } from "./ops017-gate-alias";
29
+ export { ops018 } from "./ops018-shell-image-digest";
28
30
  export { sys010, sys010Check } from "./sys010-key-over-plain-http";
29
31
 
30
32
  export interface CoreOpCheckOptions {
@@ -36,9 +38,9 @@ export interface CoreOpCheckOptions {
36
38
 
37
39
  /**
38
40
  * Core's own post-synth checks over the Op model: OPS012, OPS013, OPS014,
39
- * OPS015, OPS016, OPS017 over the workspace's `identity.gates`, and SYS010
41
+ * OPS015, OPS016, OPS017 over the workspace's `identity.gates`, OPS018, and SYS010
40
42
  * over the `decide` steps and the configured decide backends.
41
43
  */
42
44
  export function coreOpChecks(opts: CoreOpCheckOptions = {}): PostSynthCheck[] {
43
- return [ops012, ops013, ops014, ops015, ops016, ops017Check(opts.identityGates), sys010Check(opts.decideBackends)];
45
+ return [ops012, ops013, ops014, ops015, ops016, ops017Check(opts.identityGates), ops018, sys010Check(opts.decideBackends)];
44
46
  }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * OPS018 (#4015): a shell step's image is pinned by digest.
3
+ */
4
+ import { describe, test, expect } from "vitest";
5
+ import type { PostSynthContext } from "../../post-synth";
6
+ import { DECLARABLE_MARKER } from "../../../declarable";
7
+ import { stepOutput } from "../../../op/step-output-ref";
8
+ import { ops018 } from "./ops018-shell-image-digest";
9
+ import { coreOpChecks } from "./index";
10
+
11
+ const PINNED = `node:24@sha256:${"d".repeat(64)}`;
12
+
13
+ function ctxWith(props: Record<string, unknown>): PostSynthContext {
14
+ const entities = new Map<string, unknown>([["build", {
15
+ [DECLARABLE_MARKER]: true,
16
+ entityType: "Chant::Op",
17
+ lexicon: "chant",
18
+ kind: "resource",
19
+ props: { name: "build", overview: "test", ...props },
20
+ attributes: {},
21
+ }]]);
22
+ return {
23
+ outputs: new Map([["chant", ""]]),
24
+ entities: entities as Map<string, never>,
25
+ buildResult: { outputs: new Map([["chant", ""]]), entities: entities as Map<string, never>, warnings: [], errors: [], sourceFileCount: 1 },
26
+ };
27
+ }
28
+
29
+ const sh = (args: Record<string, unknown>) => ({ kind: "activity", fn: "shellCmd", args: { cmd: "npm test", ...args } });
30
+
31
+ describe("OPS018", () => {
32
+ test("is one of core's Op checks", () => {
33
+ expect(coreOpChecks().map((c) => c.id)).toContain("OPS018");
34
+ });
35
+
36
+ test("a pinned image and a step with no image pass", () => {
37
+ expect(ops018.check(ctxWith({ phases: [{ name: "Build", steps: [sh({ image: PINNED }), sh({})] }] }))).toEqual([]);
38
+ });
39
+
40
+ test("a tag is refused, naming the Op, the phase and the fix", () => {
41
+ const [diag, ...rest] = ops018.check(ctxWith({ phases: [{ name: "Build", steps: [sh({ image: "node:24" })] }] }));
42
+ expect(rest).toEqual([]);
43
+ expect(diag?.checkId).toBe("OPS018");
44
+ expect(diag?.severity).toBe("error");
45
+ expect(diag?.message).toBe('Op "build", phase "Build", shellCmd: image "node:24" is not pinned by digest: name it as <ref>@sha256:<digest>');
46
+ });
47
+
48
+ test("an image read from another step is refused, since the record could not say what ran", () => {
49
+ const [diag] = ops018.check(ctxWith({ phases: [{ name: "Build", steps: [sh({ image: stepOutput("pick", "stdout") })] }] }));
50
+ expect(diag?.message).toContain("image must be a literal string");
51
+ });
52
+
53
+ test("onFailure and effect steps are checked too", () => {
54
+ const diags = ops018.check(ctxWith({
55
+ phases: [{ name: "Apply", steps: [{ kind: "effect", receipt: {}, steps: [sh({ image: "alpine" })] }] }],
56
+ onFailure: [{ name: "Rollback", steps: [sh({ image: "alpine:latest" })] }],
57
+ }));
58
+ expect(diags.map((d) => d.message.split(",")[1]?.trim())).toEqual(['phase "Apply"', 'phase "Rollback"']);
59
+ });
60
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * OPS018: a `shellCmd` step that runs in a container (#4015) names its image
3
+ * pinned by digest, `<ref>@sha256:<digest>`, as a literal string. A tag can
4
+ * move between two runs of the same Op, and then the step record's image no
5
+ * longer says what ran. Checked over the main phases, `onFailure`, and the
6
+ * steps inside an effect.
7
+ */
8
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "../../post-synth";
9
+ import type { OpConfig, PhaseDefinition, StepDefinition } from "../../../op";
10
+ import { imageProblem } from "../../../op/activities/shell";
11
+ import { isOpEntity } from "./support";
12
+
13
+ function activitySteps(steps: readonly StepDefinition[]): Array<{ fn: string; args?: Record<string, unknown> }> {
14
+ return steps.flatMap((step) =>
15
+ step.kind === "activity" ? [step] : step.kind === "effect" ? step.steps.filter((s) => s.kind === "activity") : [],
16
+ ) as Array<{ fn: string; args?: Record<string, unknown> }>;
17
+ }
18
+
19
+ export const ops018: PostSynthCheck = {
20
+ id: "OPS018",
21
+ description: "A shell step with an image names it pinned by digest, as a literal string",
22
+
23
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
24
+ const diagnostics: PostSynthDiagnostic[] = [];
25
+ for (const [entityKey, entity] of ctx.entities) {
26
+ if (!isOpEntity(entity)) continue;
27
+ const rec = entity as unknown as Record<string, unknown>;
28
+ const props = ((entity as { props?: Record<string, unknown> }).props ?? {}) as unknown as OpConfig;
29
+ if (typeof props.name !== "string" || !Array.isArray(props.phases)) continue;
30
+ const phases: PhaseDefinition[] = [...props.phases, ...(props.onFailure ?? [])];
31
+ for (const phase of phases) {
32
+ for (const step of activitySteps(phase.steps ?? [])) {
33
+ if (step.fn !== "shellCmd" || step.args?.image === undefined) continue;
34
+ const problem = imageProblem(step.args.image);
35
+ if (!problem) continue;
36
+ diagnostics.push({
37
+ checkId: "OPS018",
38
+ severity: "error",
39
+ message: `Op "${props.name}", phase "${phase.name}", shellCmd: ${problem}`,
40
+ entity: entityKey,
41
+ lexicon: typeof rec.lexicon === "string" ? rec.lexicon : undefined,
42
+ });
43
+ }
44
+ }
45
+ }
46
+ return diagnostics;
47
+ },
48
+ };
@@ -0,0 +1,85 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { join, relative, resolve, sep } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { describe, expect, test } from "vitest";
5
+ import { isJavaScriptModule, isTypeScriptModule, moduleSourceDir, moduleSourceUrl, SOURCE_EXPORT_CONDITIONS } from "./module-source";
6
+
7
+ describe("module-source (#3981)", () => {
8
+ test("from source, chant's own resolution adds the development condition", () => {
9
+ expect(SOURCE_EXPORT_CONDITIONS).toEqual(["development"]);
10
+ });
11
+
12
+ const pkg = join(sep, "x", "node_modules", "@intentius", "chant-lexicon-aws");
13
+
14
+ test("moduleSourceDir maps a compiled module's directory to the source directory", () => {
15
+ expect(moduleSourceDir(pathToFileURL(join(pkg, "dist", "lint", "post-synth", "ext001.js")).href)).toBe(join(pkg, "src", "lint", "post-synth"));
16
+ expect(moduleSourceDir(pathToFileURL(join(pkg, "dist", "taggable.js")).href)).toBe(join(pkg, "src"));
17
+ });
18
+
19
+ test("moduleSourceDir leaves a source module's directory alone, even under a dist/ directory", () => {
20
+ expect(moduleSourceDir(pathToFileURL(join(pkg, "src", "taggable.ts")).href)).toBe(join(pkg, "src"));
21
+ const checkout = join(sep, "home", "dist", "chant", "lexicons", "aws", "src", "taggable.ts");
22
+ expect(moduleSourceDir(pathToFileURL(checkout).href)).toBe(join(sep, "home", "dist", "chant", "lexicons", "aws", "src"));
23
+ });
24
+
25
+ test("moduleSourceUrl is a URL in the source directory, for createRequire", () => {
26
+ const url = moduleSourceUrl(pathToFileURL(join(pkg, "dist", "lsp", "hover.js")).href);
27
+ expect(url).toBe(pathToFileURL(join(pkg, "src", "lsp", "hover.js")).href);
28
+ expect(new URL("../generated/lexicon-aws.json", url).href).toBe(pathToFileURL(join(pkg, "src", "generated", "lexicon-aws.json")).href);
29
+ });
30
+
31
+ test("isTypeScriptModule and isJavaScriptModule", () => {
32
+ expect(isTypeScriptModule("/p/src/index.ts")).toBe(true);
33
+ expect(isTypeScriptModule("file:///p/src/index.mts?x=1")).toBe(true);
34
+ expect(isTypeScriptModule("/p/dist/index.d.ts")).toBe(false);
35
+ expect(isTypeScriptModule("/p/dist/index.js")).toBe(false);
36
+ expect(isJavaScriptModule("file:///p/dist/index.js")).toBe(true);
37
+ expect(isJavaScriptModule("/p/dist/index.mjs")).toBe(true);
38
+ expect(isJavaScriptModule("/p/src/index.ts")).toBe(false);
39
+ // No extension: only tsx's resolver completes it, so it is not plain JavaScript.
40
+ expect(isJavaScriptModule("/p/lexicon/index")).toBe(false);
41
+ });
42
+ });
43
+
44
+ describe("package sources run under plain node ESM (#3981)", () => {
45
+ const repo = resolve(import.meta.dirname, "..", "..", "..");
46
+ const SKIP_DIRS = new Set(["node_modules", "generated", "testdata", "fixtures", "__fixtures__", "test-fixtures"]);
47
+
48
+ function sources(dir: string, out: string[] = []): string[] {
49
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
50
+ if (entry.isDirectory()) {
51
+ if (!SKIP_DIRS.has(entry.name)) sources(join(dir, entry.name), out);
52
+ } else if (/\.ts$/.test(entry.name) && !/\.(test|d)\.ts$/.test(entry.name)) {
53
+ out.push(join(dir, entry.name));
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+
59
+ test("no lexicon or package source uses the CommonJS __dirname or __filename", () => {
60
+ // Both are undefined in an ES module under plain node, and under tsx too: a
61
+ // read built on one throws, and the catch around it hides the data.
62
+ // moduleSourceDir(import.meta.url) names the source directory from src/ and dist/ alike.
63
+ const roots = ["lexicons", "packages"].flatMap((top) =>
64
+ readdirSync(join(repo, top), { withFileTypes: true })
65
+ .filter((e) => e.isDirectory())
66
+ .map((e) => join(repo, top, e.name, "src")),
67
+ );
68
+ const offenders: string[] = [];
69
+ for (const root of roots) {
70
+ let files: string[];
71
+ try {
72
+ files = sources(root);
73
+ } catch {
74
+ continue;
75
+ }
76
+ for (const file of files) {
77
+ const code = readFileSync(file, "utf-8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/gm, "");
78
+ code.split("\n").forEach((line, i) => {
79
+ if (/\b__(?:dirname|filename)\b/.test(line)) offenders.push(`${relative(repo, file)}:${i + 1}: ${line.trim()}`);
80
+ });
81
+ }
82
+ }
83
+ expect(offenders).toEqual([]);
84
+ });
85
+ });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * #3981 — paths for a module that may run from `src/*.ts` or from compiled
3
+ * `dist/*.js`.
4
+ *
5
+ * A package compiles `src/` to `dist/` with the same layout (`rootDir: src`,
6
+ * `outDir: dist`), so a compiled module sits at the same relative path under
7
+ * `dist/` as its source does under `src/`. tsc compiles only the modules: a
8
+ * file a module reads beside itself (generated JSON, skills markdown,
9
+ * snapshots) stays under `src/`, which every package publishes. A module that
10
+ * reads such a file names it from {@link moduleSourceDir}, not from its own
11
+ * directory, and finds it from either layout.
12
+ *
13
+ * String arithmetic only, no filesystem access, so it is safe in a sandboxed
14
+ * child and costs nothing at module scope.
15
+ */
16
+
17
+ import { dirname, sep } from "node:path";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
19
+
20
+ const DIST = `${sep}dist${sep}`;
21
+
22
+ /**
23
+ * The path a module's source has: `path` itself for a source module, and the
24
+ * matching path under `<package>/src` for a module under `<package>/dist`.
25
+ */
26
+ function sourcePathOf(path: string): string {
27
+ if (!/\.[cm]?js$/.test(path)) return path;
28
+ const at = path.lastIndexOf(DIST);
29
+ if (at < 0) return path;
30
+ return path.slice(0, at) + `${sep}src${sep}` + path.slice(at + DIST.length);
31
+ }
32
+
33
+ /**
34
+ * #3981 — the source file a compiled module was built from:
35
+ * `<package>/dist/x/y.js` is `<package>/src/x/y.ts` (`.mjs` from `.mts`, `.cjs`
36
+ * from `.cts`). `undefined` for a path that is not a compiled module under a
37
+ * `dist/`. The file is not checked for.
38
+ */
39
+ export function compiledModuleSource(path: string): string | undefined {
40
+ const ext = /\.([cm]?)js$/.exec(path);
41
+ if (!ext || path.lastIndexOf(DIST) < 0) return undefined;
42
+ return sourcePathOf(path).slice(0, -ext[0].length) + `.${ext[1]}ts`;
43
+ }
44
+
45
+ /**
46
+ * The directory a module's source sits in: `dirname` of `moduleUrl` when it
47
+ * runs from source, and the matching directory under `<package>/src` when it
48
+ * runs compiled from `<package>/dist`.
49
+ */
50
+ export function moduleSourceDir(moduleUrl: string): string {
51
+ return dirname(sourcePathOf(fileURLToPath(moduleUrl)));
52
+ }
53
+
54
+ /**
55
+ * A URL in the module's source directory, for `createRequire` to read a file
56
+ * that sits beside the source (`require("./generated/lexicon-aws.json")`). The
57
+ * file name keeps the compiled extension; only the directory matters to
58
+ * `createRequire`.
59
+ */
60
+ export function moduleSourceUrl(moduleUrl: string): string {
61
+ return pathToFileURL(sourcePathOf(fileURLToPath(moduleUrl))).href;
62
+ }
63
+
64
+ /**
65
+ * Whether `pathOrUrl` names a TypeScript module, which plain node cannot
66
+ * import without a loader: `.ts`, `.mts`, `.cts` or `.tsx`, and not a
67
+ * declaration file. A query or hash is ignored.
68
+ */
69
+ export function isTypeScriptModule(pathOrUrl: string): boolean {
70
+ const path = pathOrUrl.replace(/[?#].*$/, "");
71
+ return /\.(?:[cm]?ts|tsx)$/.test(path) && !/\.d\.[cm]?ts$/.test(path);
72
+ }
73
+
74
+ /**
75
+ * Whether plain node imports `pathOrUrl` with no loader: `.js`, `.mjs`,
76
+ * `.cjs` or `.json`. Anything else, a module with no extension included, is
77
+ * left to tsx, whose resolver completes an extensionless path.
78
+ */
79
+ export function isJavaScriptModule(pathOrUrl: string): boolean {
80
+ return /\.(?:[cm]?js|json)$/.test(pathOrUrl.replace(/[?#].*$/, ""));
81
+ }
82
+
83
+ /**
84
+ * #3981 — whether this process resolves chant's packages through their
85
+ * `development` export condition: always when chant runs from source (the
86
+ * CLI's `main()` installs a resolve hook that does so, and vitest's resolver
87
+ * adds the condition outside production mode), and otherwise when node runs
88
+ * with `--conditions=development` (or `-C development`, on the command line
89
+ * or in `NODE_OPTIONS`). Not from the environment: a compiled CLI that a test
90
+ * spawns inherits vitest's variables.
91
+ */
92
+ function resolvesDevelopment(): boolean {
93
+ if (isTypeScriptModule(import.meta.url)) return true;
94
+ if (typeof process === "undefined") return false;
95
+ const flags = [...process.execArgv, ...(process.env.NODE_OPTIONS ?? "").split(/\s+/)];
96
+ return flags.some(
97
+ (flag, i) =>
98
+ flag === "--conditions=development" ||
99
+ flag === "-C=development" ||
100
+ ((flag === "--conditions" || flag === "-C") && flags[i + 1] === "development"),
101
+ );
102
+ }
103
+
104
+ /**
105
+ * #3981 — the package export conditions chant adds to `import`, `node` and
106
+ * `default` when it resolves a package itself (fold's import resolution, the
107
+ * sandbox bundle), so it lands on the module this process's `import()` would.
108
+ * Each chant package maps `development` to `src/*.ts` and `default` to the
109
+ * compiled `dist/*.js` it publishes. `["development"]` when the process
110
+ * resolves with it ({@link resolvesDevelopment}), else empty.
111
+ */
112
+ export const SOURCE_EXPORT_CONDITIONS: readonly string[] = resolvesDevelopment() ? ["development"] : [];
@@ -59,6 +59,9 @@ export const shellCmdContract = activityContract(
59
59
  gatedExit: z.number().optional(),
60
60
  gate: z.strictObject({ op: z.string(), gate: z.string() }).optional(),
61
61
  json: z.boolean().optional(),
62
+ image: z.string().optional(),
63
+ mounts: z.strictObject({ workspace: z.enum(["ro", "rw"]).optional() }).optional(),
64
+ outputs: z.array(z.string()).optional(),
62
65
  }),
63
66
  // `json` is stdout parsed, when the step sets `json` (#2787): whatever the
64
67
  // command printed, so its shape is the author's and only the whole value is
@@ -357,7 +357,7 @@ function inWorktree(kindFile: string, checkout: string, worktree: string): strin
357
357
  }
358
358
  };
359
359
  const [file, top, tree] = [real(kindFile), real(checkout), real(worktree)];
360
- // Read from the worktree already (it sits under the checkout's .git), or outside the checkout: as it is.
360
+ // Read from the worktree already (it sits in the checkout's .chant/work, #2893), or outside the checkout: as it is.
361
361
  if (file.startsWith(`${tree}/`) || !file.startsWith(`${top}/`)) return file;
362
362
  return join(tree, file.slice(top.length + 1));
363
363
  }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * A `shell()` step in a container (#4015), run for real with `docker run
3
+ * --rm`. Skipped when Docker is not running. The containers are `--rm` and
4
+ * mount only host directories, so nothing is left behind.
5
+ */
6
+ import { execSync } from "node:child_process";
7
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
11
+ import { shellCmd } from "./shell";
12
+
13
+ // alpine 3.21, pinned by its index digest.
14
+ const IMAGE = process.env.CHANT_SHELL_E2E_IMAGE ?? "alpine:3.21@sha256:ce64758a109eb420d874a118f87920e625e12d3634e03b4a5573fd9f6e5d3507";
15
+
16
+ function available(cmd: string): boolean {
17
+ try {
18
+ execSync(cmd, { stdio: "ignore" });
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ const hasDocker = available("docker info");
26
+ const skipReason = hasDocker ? "" : " (skipped: Docker is not running)";
27
+
28
+ describe.skipIf(!hasDocker)(`shellCmd with an image${skipReason}`, () => {
29
+ let dir: string;
30
+
31
+ beforeAll(async () => {
32
+ dir = await mkdtemp(join(tmpdir(), "chant-shell-e2e-"));
33
+ await writeFile(join(dir, "input.txt"), "from the host\n");
34
+ execSync(`docker pull -q ${IMAGE}`, { stdio: "ignore" });
35
+ }, 120_000);
36
+
37
+ afterAll(async () => {
38
+ await rm(dir, { recursive: true, force: true });
39
+ });
40
+
41
+ test("runs in the image, reads the workspace read-only, sees env by name, and writes its output directory on the host", async () => {
42
+ process.env.CHANT_SHELL_E2E_HOST_ONLY = "leaked";
43
+ try {
44
+ const result = await shellCmd({
45
+ cmd: 'cat /etc/alpine-release >/dev/null && echo "$GREETING:$CHANT_SHELL_E2E_HOST_ONLY" && cat input.txt > out/copy.txt; touch input.txt 2>/dev/null || echo read-only',
46
+ image: IMAGE,
47
+ cwd: dir,
48
+ env: { GREETING: "hi" },
49
+ outputs: ["out"],
50
+ });
51
+ expect(result.stdout.split("\n")).toEqual(["hi:", "read-only"]);
52
+ expect(await readFile(join(dir, "out", "copy.txt"), "utf8")).toBe("from the host\n");
53
+ } finally {
54
+ delete process.env.CHANT_SHELL_E2E_HOST_ONLY;
55
+ }
56
+ }, 60_000);
57
+
58
+ test("a declared output directory the command left empty fails the step", async () => {
59
+ await expect(shellCmd({ cmd: "true", image: IMAGE, cwd: dir, outputs: ["empty"] })).rejects.toThrow(/left it empty/);
60
+ }, 60_000);
61
+
62
+ test("keeps the command's exit status, and okExit still applies", async () => {
63
+ await expect(shellCmd({ cmd: "exit 4", image: IMAGE, cwd: dir })).rejects.toThrow(/command exited 4/);
64
+ const ok = await shellCmd({ cmd: "echo done; exit 1", image: IMAGE, cwd: dir, okExit: [0, 1] });
65
+ expect(ok).toMatchObject({ stdout: "done", exitCode: 1 });
66
+ }, 60_000);
67
+ });