@intentius/chant 0.124.30 → 0.124.32

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 (346) hide show
  1. package/bin/chant +7 -2
  2. package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
  3. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  4. package/dist/cli/commands/check-lexicon-plugin.js +10 -7
  5. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  6. package/dist/cli/commands/check-lexicon.js +18 -12
  7. package/dist/cli/commands/import.d.ts +10 -0
  8. package/dist/cli/commands/import.d.ts.map +1 -1
  9. package/dist/cli/commands/import.js +37 -0
  10. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  11. package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
  12. package/dist/cli/commands/onboard.d.ts.map +1 -1
  13. package/dist/cli/commands/onboard.js +8 -4
  14. package/dist/cli/commands/update.d.ts.map +1 -1
  15. package/dist/cli/commands/update.js +2 -1
  16. package/dist/cli/dependencies.d.ts +18 -0
  17. package/dist/cli/dependencies.d.ts.map +1 -0
  18. package/dist/cli/dependencies.js +120 -0
  19. package/dist/cli/handlers/build.d.ts.map +1 -1
  20. package/dist/cli/handlers/build.js +23 -4
  21. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  22. package/dist/cli/handlers/fan-out.js +12 -4
  23. package/dist/cli/handlers/graph.d.ts.map +1 -1
  24. package/dist/cli/handlers/graph.js +16 -6
  25. package/dist/cli/handlers/lint.d.ts.map +1 -1
  26. package/dist/cli/handlers/lint.js +6 -0
  27. package/dist/cli/handlers/operator.d.ts +2 -2
  28. package/dist/cli/handlers/operator.js +3 -3
  29. package/dist/cli/handlers/pr.d.ts +14 -0
  30. package/dist/cli/handlers/pr.d.ts.map +1 -1
  31. package/dist/cli/handlers/pr.js +488 -32
  32. package/dist/cli/handlers/promote.d.ts.map +1 -1
  33. package/dist/cli/handlers/promote.js +3 -1
  34. package/dist/cli/handlers/run.d.ts +13 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +38 -15
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +74 -12
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +65 -0
  41. package/dist/cli/registry.d.ts +29 -2
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/registry.js +6 -2
  44. package/dist/codegen/generate-post-synth-barrel.js +2 -2
  45. package/dist/codegen/lexicon-regen.d.ts +4 -0
  46. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  47. package/dist/codegen/lexicon-regen.js +1 -1
  48. package/dist/codegen/pinned-upgrade.js +2 -2
  49. package/dist/components/capability.d.ts +44 -3
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/capability.js +21 -0
  52. package/dist/components/changed-files.d.ts +7 -1
  53. package/dist/components/changed-files.d.ts.map +1 -1
  54. package/dist/components/changed-files.js +9 -1
  55. package/dist/components/driver.d.ts +38 -0
  56. package/dist/components/driver.d.ts.map +1 -1
  57. package/dist/components/driver.js +47 -6
  58. package/dist/components/fan-out-record.d.ts +6 -1
  59. package/dist/components/fan-out-record.d.ts.map +1 -1
  60. package/dist/components/fan-out-record.js +24 -0
  61. package/dist/components/fan-out-run.d.ts +20 -0
  62. package/dist/components/fan-out-run.d.ts.map +1 -1
  63. package/dist/components/fan-out-run.js +21 -1
  64. package/dist/components/pr-pipeline.d.ts +65 -2
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +107 -15
  67. package/dist/components/pr-run.d.ts +127 -29
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +290 -70
  70. package/dist/components/wave-plan.d.ts +5 -2
  71. package/dist/components/wave-plan.d.ts.map +1 -1
  72. package/dist/components/wave-plan.js +12 -1
  73. package/dist/discovery/fold-import.d.ts.map +1 -1
  74. package/dist/discovery/fold-import.js +16 -6
  75. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  76. package/dist/discovery/sandbox/bundle.js +6 -0
  77. package/dist/discovery/specifier-resolve.d.ts.map +1 -1
  78. package/dist/discovery/specifier-resolve.js +8 -2
  79. package/dist/fold/fold.d.ts.map +1 -1
  80. package/dist/fold/fold.js +68 -1
  81. package/dist/fold/subset.d.ts +37 -0
  82. package/dist/fold/subset.d.ts.map +1 -1
  83. package/dist/fold/subset.js +88 -0
  84. package/dist/lexicon-module.d.ts +10 -0
  85. package/dist/lexicon-module.d.ts.map +1 -1
  86. package/dist/lexicon-module.js +53 -8
  87. package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
  88. package/dist/lexicon-plugin-helpers.js +6 -4
  89. package/dist/lexicon.d.ts +2 -0
  90. package/dist/lexicon.d.ts.map +1 -1
  91. package/dist/lifecycle/affected.d.ts.map +1 -1
  92. package/dist/lifecycle/affected.js +7 -1
  93. package/dist/lifecycle/release-ledger.d.ts +21 -0
  94. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  95. package/dist/lifecycle/release-ledger.js +24 -0
  96. package/dist/lifecycle/work-dir.d.ts +45 -0
  97. package/dist/lifecycle/work-dir.d.ts.map +1 -0
  98. package/dist/lifecycle/work-dir.js +78 -0
  99. package/dist/lint/discover.d.ts +17 -9
  100. package/dist/lint/discover.d.ts.map +1 -1
  101. package/dist/lint/discover.js +62 -54
  102. package/dist/module-source.d.ts +57 -0
  103. package/dist/module-source.d.ts.map +1 -0
  104. package/dist/module-source.js +104 -0
  105. package/dist/op/activities/factory.js +1 -1
  106. package/dist/op/discover.d.ts.map +1 -1
  107. package/dist/op/discover.js +13 -3
  108. package/dist/op/gate.d.ts.map +1 -1
  109. package/dist/op/gate.js +3 -0
  110. package/dist/op/local-executor.d.ts +12 -0
  111. package/dist/op/local-executor.d.ts.map +1 -1
  112. package/dist/op/local-executor.js +22 -3
  113. package/dist/op/op-waves-run.d.ts.map +1 -1
  114. package/dist/op/op-waves-run.js +20 -5
  115. package/dist/op/operator.d.ts +2 -2
  116. package/dist/op/operator.js +2 -2
  117. package/dist/op/redact.d.ts +34 -0
  118. package/dist/op/redact.d.ts.map +1 -0
  119. package/dist/op/redact.js +94 -0
  120. package/dist/op/step-console.d.ts +2 -0
  121. package/dist/op/step-console.d.ts.map +1 -1
  122. package/dist/op/step-console.js +5 -2
  123. package/dist/op/steward.d.ts +6 -4
  124. package/dist/op/steward.d.ts.map +1 -1
  125. package/dist/op/steward.js +5 -3
  126. package/dist/op/work-lease-run.d.ts +20 -7
  127. package/dist/op/work-lease-run.d.ts.map +1 -1
  128. package/dist/op/work-lease-run.js +56 -12
  129. package/dist/pr-forge.d.ts +27 -0
  130. package/dist/pr-forge.d.ts.map +1 -1
  131. package/dist/pr-forge.js +51 -6
  132. package/dist/pr-loop.d.ts +141 -4
  133. package/dist/pr-loop.d.ts.map +1 -1
  134. package/dist/pr-loop.js +122 -6
  135. package/dist/pr-report-url.d.ts +10 -2
  136. package/dist/pr-report-url.d.ts.map +1 -1
  137. package/dist/pr-report-url.js +52 -6
  138. package/dist/ts-loader.d.ts +38 -4
  139. package/dist/ts-loader.d.ts.map +1 -1
  140. package/dist/ts-loader.js +190 -16
  141. package/dist/wave-runtime.mjs +208 -208
  142. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  143. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  144. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  145. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  146. package/dist/workspace/chant-migrations.js +2 -0
  147. package/dist/workspace/checks/boxes.d.ts +6 -6
  148. package/dist/workspace/checks/boxes.js +9 -9
  149. package/dist/workspace/ci-green-forge.d.ts +6 -4
  150. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  151. package/dist/workspace/ci-green.d.ts +28 -12
  152. package/dist/workspace/ci-green.d.ts.map +1 -1
  153. package/dist/workspace/ci-green.js +49 -21
  154. package/dist/workspace/ci-list.schema.json +12 -6
  155. package/dist/workspace/ci-report-url.d.ts +18 -0
  156. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  157. package/dist/workspace/ci-report-url.js +33 -0
  158. package/dist/workspace/ci-run-index.d.ts +108 -0
  159. package/dist/workspace/ci-run-index.d.ts.map +1 -0
  160. package/dist/workspace/ci-run-index.js +164 -0
  161. package/dist/workspace/ci-run-index.schema.json +52 -0
  162. package/dist/workspace/ci-run.d.ts +159 -9
  163. package/dist/workspace/ci-run.d.ts.map +1 -1
  164. package/dist/workspace/ci-run.js +419 -50
  165. package/dist/workspace/ci-runner-key.d.ts +133 -0
  166. package/dist/workspace/ci-runner-key.d.ts.map +1 -0
  167. package/dist/workspace/ci-runner-key.js +237 -0
  168. package/dist/workspace/ci-secrets.d.ts +65 -0
  169. package/dist/workspace/ci-secrets.d.ts.map +1 -0
  170. package/dist/workspace/ci-secrets.js +211 -0
  171. package/dist/workspace/ci-serve.d.ts +67 -4
  172. package/dist/workspace/ci-serve.d.ts.map +1 -1
  173. package/dist/workspace/ci-serve.js +240 -35
  174. package/dist/workspace/declaration.d.ts +8 -0
  175. package/dist/workspace/declaration.d.ts.map +1 -1
  176. package/dist/workspace/declaration.js +11 -2
  177. package/dist/workspace/declaration.schema.json +7 -2
  178. package/dist/workspace/export.d.ts +1 -1
  179. package/dist/workspace/export.d.ts.map +1 -1
  180. package/dist/workspace/export.js +7 -3
  181. package/dist/workspace/ls.d.ts +6 -1
  182. package/dist/workspace/ls.d.ts.map +1 -1
  183. package/dist/workspace/ls.js +1 -0
  184. package/dist/workspace/ls.schema.json +7 -1
  185. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  186. package/dist/workspace/pin-integrity.js +3 -2
  187. package/dist/workspace/pr-report.schema.json +74 -7
  188. package/dist/workspace/reason-codes.d.ts +7 -3
  189. package/dist/workspace/reason-codes.d.ts.map +1 -1
  190. package/dist/workspace/reason-codes.js +9 -4
  191. package/dist/workspace/run-event.schema.json +3 -1
  192. package/dist/workspace/status.schema.json +3 -1
  193. package/dist/workspace/trust/trust-write.d.ts +109 -0
  194. package/dist/workspace/trust/trust-write.d.ts.map +1 -0
  195. package/dist/workspace/trust/trust-write.js +320 -0
  196. package/dist/workspace/trust-write.schema.json +265 -0
  197. package/dist/workspace/write-scope.d.ts +9 -0
  198. package/dist/workspace/write-scope.d.ts.map +1 -1
  199. package/dist/workspace/write-scope.js +11 -1
  200. package/package.json +1 -1
  201. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  202. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  203. package/src/cli/commands/audit.test.ts +2 -2
  204. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  205. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  206. package/src/cli/commands/check-lexicon.test.ts +1 -1
  207. package/src/cli/commands/check-lexicon.ts +16 -11
  208. package/src/cli/commands/import.ts +40 -0
  209. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  210. package/src/cli/commands/init-lexicon.test.ts +8 -5
  211. package/src/cli/commands/onboard.test.ts +45 -0
  212. package/src/cli/commands/onboard.ts +8 -4
  213. package/src/cli/commands/update.test.ts +6 -0
  214. package/src/cli/commands/update.ts +4 -1
  215. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  216. package/src/cli/dependencies.e2e.test.ts +101 -0
  217. package/src/cli/dependencies.test.ts +93 -0
  218. package/src/cli/dependencies.ts +122 -0
  219. package/src/cli/handlers/build.test.ts +69 -0
  220. package/src/cli/handlers/build.ts +20 -4
  221. package/src/cli/handlers/fan-out.test.ts +73 -0
  222. package/src/cli/handlers/fan-out.ts +11 -4
  223. package/src/cli/handlers/graph.test.ts +76 -0
  224. package/src/cli/handlers/graph.ts +19 -12
  225. package/src/cli/handlers/lint.test.ts +42 -0
  226. package/src/cli/handlers/lint.ts +6 -0
  227. package/src/cli/handlers/operator.ts +3 -3
  228. package/src/cli/handlers/pr-waves.test.ts +449 -0
  229. package/src/cli/handlers/pr.test.ts +134 -0
  230. package/src/cli/handlers/pr.ts +523 -31
  231. package/src/cli/handlers/promote.test.ts +26 -1
  232. package/src/cli/handlers/promote.ts +3 -2
  233. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  234. package/src/cli/handlers/run-signer.test.ts +79 -0
  235. package/src/cli/handlers/run.ts +34 -16
  236. package/src/cli/main.test.ts +19 -0
  237. package/src/cli/main.ts +73 -12
  238. package/src/cli/parse-args.ts +48 -0
  239. package/src/cli/registry.ts +35 -4
  240. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  241. package/src/codegen/lexicon-regen.ts +1 -1
  242. package/src/codegen/pinned-upgrade.ts +2 -2
  243. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  244. package/src/components/capability.ts +51 -3
  245. package/src/components/changed-files.test.ts +16 -1
  246. package/src/components/changed-files.ts +9 -1
  247. package/src/components/driver.test.ts +36 -1
  248. package/src/components/driver.ts +76 -4
  249. package/src/components/fan-out-record.ts +29 -1
  250. package/src/components/fan-out-run.test.ts +35 -0
  251. package/src/components/fan-out-run.ts +40 -1
  252. package/src/components/pr-pipeline.test.ts +100 -0
  253. package/src/components/pr-pipeline.ts +132 -17
  254. package/src/components/pr-run.test.ts +323 -18
  255. package/src/components/pr-run.ts +354 -75
  256. package/src/components/wave-plan.ts +16 -4
  257. package/src/discovery/fold-host-method.test.ts +87 -0
  258. package/src/discovery/fold-import.ts +15 -5
  259. package/src/discovery/fold-package-composite.test.ts +27 -0
  260. package/src/discovery/fold-specifiers.test.ts +8 -0
  261. package/src/discovery/sandbox/bundle.ts +6 -0
  262. package/src/discovery/specifier-resolve.ts +8 -2
  263. package/src/fold/fold.test.ts +72 -0
  264. package/src/fold/fold.ts +71 -0
  265. package/src/fold/subset.test.ts +31 -0
  266. package/src/fold/subset.ts +99 -0
  267. package/src/lexicon-dist.e2e.test.ts +254 -0
  268. package/src/lexicon-module.test.ts +47 -0
  269. package/src/lexicon-module.ts +49 -7
  270. package/src/lexicon-plugin-helpers.ts +6 -4
  271. package/src/lexicon.ts +2 -0
  272. package/src/lifecycle/affected.test.ts +7 -0
  273. package/src/lifecycle/affected.ts +6 -1
  274. package/src/lifecycle/release-ledger.ts +34 -0
  275. package/src/lifecycle/work-dir.test.ts +77 -0
  276. package/src/lifecycle/work-dir.ts +82 -0
  277. package/src/lint/discover.test.ts +46 -2
  278. package/src/lint/discover.ts +55 -49
  279. package/src/module-source.test.ts +85 -0
  280. package/src/module-source.ts +112 -0
  281. package/src/op/activities/factory.ts +1 -1
  282. package/src/op/discover-root.test.ts +31 -2
  283. package/src/op/discover.ts +14 -4
  284. package/src/op/gate.test.ts +30 -0
  285. package/src/op/gate.ts +3 -0
  286. package/src/op/local-executor.ts +31 -3
  287. package/src/op/op-waves-run.ts +20 -5
  288. package/src/op/op-waves.test.ts +49 -0
  289. package/src/op/operator.ts +2 -2
  290. package/src/op/redact.ts +93 -0
  291. package/src/op/step-console.ts +7 -2
  292. package/src/op/steward.ts +6 -4
  293. package/src/op/work-lease-run.test.ts +88 -2
  294. package/src/op/work-lease-run.ts +52 -12
  295. package/src/pr-forge-bundle.test.ts +2 -2
  296. package/src/pr-forge.test.ts +71 -0
  297. package/src/pr-forge.ts +70 -6
  298. package/src/pr-loop-bundle.test.ts +2 -1
  299. package/src/pr-loop.test.ts +117 -1
  300. package/src/pr-loop.ts +252 -10
  301. package/src/pr-report-url.ts +58 -6
  302. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  303. package/src/ts-loader.test.ts +89 -1
  304. package/src/ts-loader.ts +180 -13
  305. package/src/workspace/changes.test.ts +1 -1
  306. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  307. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  308. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  309. package/src/workspace/chant-migrations.ts +2 -0
  310. package/src/workspace/checks/boxes.ts +9 -9
  311. package/src/workspace/ci-green-forge.ts +6 -4
  312. package/src/workspace/ci-green-runs.test.ts +53 -11
  313. package/src/workspace/ci-green.test.ts +1 -1
  314. package/src/workspace/ci-green.ts +50 -22
  315. package/src/workspace/ci-list.schema.json +12 -6
  316. package/src/workspace/ci-report-url.test.ts +220 -0
  317. package/src/workspace/ci-report-url.ts +33 -0
  318. package/src/workspace/ci-run-index.schema.json +52 -0
  319. package/src/workspace/ci-run-index.test.ts +392 -0
  320. package/src/workspace/ci-run-index.ts +206 -0
  321. package/src/workspace/ci-run.test.ts +520 -42
  322. package/src/workspace/ci-run.ts +483 -51
  323. package/src/workspace/ci-runner-key.test.ts +204 -0
  324. package/src/workspace/ci-runner-key.ts +275 -0
  325. package/src/workspace/ci-secrets.test.ts +106 -0
  326. package/src/workspace/ci-secrets.ts +213 -0
  327. package/src/workspace/ci-serve.test.ts +282 -39
  328. package/src/workspace/ci-serve.ts +231 -34
  329. package/src/workspace/declaration.schema.json +7 -2
  330. package/src/workspace/declaration.test.ts +23 -0
  331. package/src/workspace/declaration.ts +19 -3
  332. package/src/workspace/export.ts +6 -3
  333. package/src/workspace/ls.schema.json +7 -1
  334. package/src/workspace/ls.ts +7 -2
  335. package/src/workspace/pin-integrity.test.ts +5 -0
  336. package/src/workspace/pin-integrity.ts +3 -2
  337. package/src/workspace/pr-report.schema.json +74 -7
  338. package/src/workspace/reason-codes.test.ts +2 -0
  339. package/src/workspace/reason-codes.ts +9 -4
  340. package/src/workspace/run-event.schema.json +3 -1
  341. package/src/workspace/status.schema.json +3 -1
  342. package/src/workspace/trust/trust-write.test.ts +179 -0
  343. package/src/workspace/trust/trust-write.ts +377 -0
  344. package/src/workspace/trust-write.schema.json +265 -0
  345. package/src/workspace/write-scope.test.ts +29 -1
  346. package/src/workspace/write-scope.ts +12 -1
@@ -148,6 +148,75 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)",
148
148
  vi.restoreAllMocks();
149
149
  });
150
150
 
151
+ test("--pr-loop without --report-url bakes the workspace's ci.reportUrl into the jobs, and the flag wins (#4160)", async () => {
152
+ const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import("node:fs");
153
+ const { tmpdir } = await import("node:os");
154
+ const { join } = await import("node:path");
155
+ const { execFileSync } = await import("node:child_process");
156
+ const root = mkdtempSync(join(tmpdir(), "chant-build-report-url-"));
157
+ try {
158
+ execFileSync("git", ["init", "-q"], { cwd: root });
159
+ mkdirSync(join(root, "app"));
160
+ writeFileSync(
161
+ join(root, "chant.workspace.json"),
162
+ JSON.stringify({ name: "acme", schema: 1, ci: { reportUrl: "s3://acme-reports/prod/" }, members: [{ name: "app", dir: "app", kind: "chant", because: "the app" }] }),
163
+ );
164
+ generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "jobs: {}", stages: [], jobs: [] });
165
+ vi.spyOn(console, "log").mockImplementation(() => {});
166
+ const path = join(root, "app");
167
+ const output = join(root, "out.yml");
168
+
169
+ expect(await runBuild({ args: makeArgs({ path, output, generate: "github", prLoop: true }), plugins: [], serializers: [] })).toBe(0);
170
+ expect(generateComponentsPipelineMock.mock.calls[0][2].prLoop).toEqual({ reportUrl: "s3://acme-reports/prod" });
171
+
172
+ expect(await runBuild({ args: makeArgs({ path, output, generate: "github", prLoop: true, reportUrl: "file:///srv/reports" }), plugins: [], serializers: [] })).toBe(0);
173
+ expect(generateComponentsPipelineMock.mock.calls[1][2].prLoop).toEqual({ reportUrl: "file:///srv/reports" });
174
+
175
+ // Without --pr-loop the declaration's URL is not read and nothing changes.
176
+ expect(await runBuild({ args: makeArgs({ path, output, generate: "github" }), plugins: [], serializers: [] })).toBe(0);
177
+ expect(generateComponentsPipelineMock.mock.calls[2][2]).not.toHaveProperty("prLoop");
178
+ } finally {
179
+ rmSync(root, { recursive: true, force: true });
180
+ vi.restoreAllMocks();
181
+ }
182
+ });
183
+
184
+ test("--pr-loop outside any workspace has no report URL unless the flag gives one (#4160)", async () => {
185
+ const { mkdtempSync, rmSync } = await import("node:fs");
186
+ const { tmpdir } = await import("node:os");
187
+ const { join } = await import("node:path");
188
+ const plain = mkdtempSync(join(tmpdir(), "chant-build-plain-"));
189
+ try {
190
+ generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "jobs: {}", stages: [], jobs: [] });
191
+ vi.spyOn(console, "log").mockImplementation(() => {});
192
+ expect(await runBuild({ args: makeArgs({ path: plain, generate: "github", prLoop: true }), plugins: [], serializers: [] })).toBe(0);
193
+ expect(generateComponentsPipelineMock).toHaveBeenCalledWith(plain, "github", { env: undefined, prLoop: {} }, undefined, []);
194
+ } finally {
195
+ rmSync(plain, { recursive: true, force: true });
196
+ vi.restoreAllMocks();
197
+ }
198
+ });
199
+
200
+ test("an invalid ci.reportUrl stops --pr-loop with its reason code, and generates nothing (#4160)", async () => {
201
+ const { mkdtempSync, mkdirSync, writeFileSync, rmSync } = await import("node:fs");
202
+ const { tmpdir } = await import("node:os");
203
+ const { join } = await import("node:path");
204
+ const { execFileSync } = await import("node:child_process");
205
+ const root = mkdtempSync(join(tmpdir(), "chant-build-report-url-bad-"));
206
+ try {
207
+ execFileSync("git", ["init", "-q"], { cwd: root });
208
+ mkdirSync(join(root, "app"));
209
+ writeFileSync(join(root, "chant.workspace.json"), JSON.stringify({ name: "acme", schema: 1, ci: { reportUrl: "https://reports.example" }, members: [{ name: "app", dir: "app", kind: "chant", because: "the app" }] }));
210
+ const stderr = makeStderrSpy();
211
+ expect(await runBuild({ args: makeArgs({ path: join(root, "app"), generate: "github", prLoop: true }), plugins: [], serializers: [] })).toBe(1);
212
+ expect(generateComponentsPipelineMock).not.toHaveBeenCalled();
213
+ expect(stderr.join("\n")).toMatch(/ci\.reportUrl https:\/\/reports\.example: .*\[declaration-invalid\]/);
214
+ } finally {
215
+ rmSync(root, { recursive: true, force: true });
216
+ vi.restoreAllMocks();
217
+ }
218
+ });
219
+
151
220
  test("--pr-build and --pr-build-command become the prLoop build options (#4088)", async () => {
152
221
  generateComponentsPipelineMock.mockResolvedValue({ success: true, yaml: "jobs: {}", stages: [], jobs: [] });
153
222
  vi.spyOn(console, "log").mockImplementation(() => {});
@@ -2,6 +2,7 @@ import { writeFileSync, mkdirSync } from "node:fs";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { buildCommand, buildCommandWatch, printErrors, printWarnings, resolveBuildFormat } from "../commands/build";
4
4
  import { formatError, formatInfo, formatSuccess, formatBold } from "../format";
5
+ import { reportUninstalledDependencies } from "../dependencies";
5
6
  import type { CommandContext } from "../registry";
6
7
  import { generateComponentsPipeline } from "../../components/cli-support";
7
8
  import { loadChantConfigUpward, type ChantConfig } from "../../config";
@@ -15,13 +16,14 @@ import type { PrLoopPipelineOptions } from "../../components/pr-pipeline";
15
16
  * and the branch, and with `--events-json` and `--report-url` (#4007) the
16
17
  * flags the generated jobs pass to pr-plan and pr-apply. `--pr-build` and
17
18
  * `--pr-build-command` (#4088) add the build and lint steps before both.
19
+ * `reportUrl` is `--report-url`, else the workspace's `ci.reportUrl` (#4160).
18
20
  */
19
- function prLoopOption(args: CommandContext["args"]): PrLoopPipelineOptions {
21
+ function prLoopOption(args: CommandContext["args"], reportUrl: string | undefined): PrLoopPipelineOptions {
20
22
  return {
21
23
  ...(args.gate ? { gate: args.gate } : {}),
22
24
  ...(args.branch ? { branch: args.branch } : {}),
23
25
  ...(args.eventsJson ? { events: true } : {}),
24
- ...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
26
+ ...(reportUrl ? { reportUrl } : {}),
25
27
  ...(args.prBuild ? { build: true } : {}),
26
28
  ...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
27
29
  };
@@ -90,6 +92,16 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
90
92
  const found = findWorkspaceRoot(resolve(args.path));
91
93
  let plan: import("../../workspace/member-pipeline").MemberComponentPlan | undefined;
92
94
  let memberPipeline: typeof import("../../workspace/member-pipeline") | undefined;
95
+ // #4160: without --report-url, the declaration's ci.reportUrl. Read only inside a workspace.
96
+ let reportUrl = args.reportUrl;
97
+ if (found && args.prLoop && !reportUrl) {
98
+ try {
99
+ reportUrl = await (await import("../../workspace/ci-report-url")).declaredReportUrl(resolve(args.path));
100
+ } catch (err) {
101
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
102
+ return 1;
103
+ }
104
+ }
93
105
  if (found) {
94
106
  memberPipeline = await import("../../workspace/member-pipeline");
95
107
  try {
@@ -104,7 +116,7 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
104
116
  params: args.param,
105
117
  paramsFile: args.paramsFile,
106
118
  ...(args.waveGate ? { waveGate: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } } : {}),
107
- ...(args.prLoop ? { prLoop: prLoopOption(args) } : {}),
119
+ ...(args.prLoop ? { prLoop: prLoopOption(args, reportUrl) } : {}),
108
120
  ...(args.image ? { image: args.image } : {}),
109
121
  ...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
110
122
  });
@@ -134,7 +146,7 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
134
146
  ? { gatedWaves: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } }
135
147
  : {}),
136
148
  // #3183: a plan job per pull request and an apply job per merge.
137
- ...(args.prLoop ? { prLoop: prLoopOption(args) } : {}),
149
+ ...(args.prLoop ? { prLoop: prLoopOption(args, reportUrl) } : {}),
138
150
  // #4002: the job image and the lines that install what it lacks, chant itself included.
139
151
  ...(args.image ? { image: args.image } : {}),
140
152
  ...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
@@ -284,6 +296,8 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
284
296
  const own = result.warnings.filter((w) => !w.includes("No serializer found for lexicon") && !seenWarnings.has(w));
285
297
  for (const w of own) seenWarnings.add(w);
286
298
  printWarnings(own);
299
+ // #4116: once, before the first failing group's errors.
300
+ if (!result.success && !failed) reportUninstalledDependencies(result.errors, resolve(args.path));
287
301
  printErrors(result.errors);
288
302
  if (!result.success) failed = true;
289
303
  }
@@ -333,6 +347,8 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
333
347
  warnings = warnings.filter((w) => !w.includes('No serializer found for lexicon'));
334
348
  }
335
349
  printWarnings(warnings);
350
+ // #4116: errors that are imports of uninstalled packages are led by the dependencies message.
351
+ if (!result.success) reportUninstalledDependencies(result.errors, resolve(args.path));
336
352
  printErrors(result.errors);
337
353
 
338
354
  return result.success ? 0 : 1;
@@ -78,6 +78,18 @@ const ESTATE = [
78
78
 
79
79
  let ran: string[] = [];
80
80
  let failing: string[] = [];
81
+ /** The error a component's step throws, in place of applying (#3997). */
82
+ let refusal: Record<string, Error> = {};
83
+
84
+ /** A step's refusal of a plan that moved since it was approved, marked `planMoved` as cfn-deploy marks it. */
85
+ function planMovedError(component: string): Error {
86
+ return Object.assign(new Error(`${component}: refusing to apply, the plan moved`), { planMoved: true });
87
+ }
88
+
89
+ /** A step error carrying an `exitCode`, as kubectl-apply's carried one before #3997's follow-up. */
90
+ function exitCodeError(component: string, exitCode: number): Error {
91
+ return Object.assign(new Error(`${component}: refused`), { exitCode });
92
+ }
81
93
  /** What each component's step was handed, after wiring resolved. */
82
94
  let inputs: Record<string, Record<string, unknown>> = {};
83
95
  /** Stack outputs a component's step returns, as cfn-deploy's `outputs` does. */
@@ -118,6 +130,7 @@ beforeEach(() => {
118
130
  vi.clearAllMocks();
119
131
  ran = [];
120
132
  failing = [];
133
+ refusal = {};
121
134
  inputs = {};
122
135
  stepOutputs = {};
123
136
  onStart = () => {};
@@ -155,6 +168,7 @@ beforeEach(() => {
155
168
  onStart(runCtx.component);
156
169
  ran.push(runCtx.component);
157
170
  inputs[runCtx.component] = input;
171
+ if (refusal[runCtx.component]) throw refusal[runCtx.component];
158
172
  if (failing.includes(runCtx.component)) throw new Error(`${runCtx.component} failed`);
159
173
  return { ok: true, ...(stepOutputs[runCtx.component] ? { outputs: stepOutputs[runCtx.component] } : {}) };
160
174
  },
@@ -310,6 +324,31 @@ describe("proof: a mid-run failure blocks its subtree and leaves the independent
310
324
  });
311
325
  });
312
326
 
327
+ describe("a step that refuses its plan because the plan moved exits 3 (#3997)", () => {
328
+ test("marked planMoved: exit 3, and its subtree is blocked as for any failure", async () => {
329
+ refusal = { "cluster-a": planMovedError("cluster-a") };
330
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(3);
331
+ expect(ran).not.toContain("app-one");
332
+ expect(ran).toContain("app-three");
333
+ });
334
+
335
+ test("an exitCode of 3 on the step's error is the same mark", async () => {
336
+ refusal = { "cluster-a": exitCodeError("cluster-a", 3) };
337
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(3);
338
+ });
339
+
340
+ test("any other exitCode is ignored, and the failure exits 1", async () => {
341
+ refusal = { "cluster-a": exitCodeError("cluster-a", 7) };
342
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(1);
343
+ });
344
+
345
+ test("one moved plan beside another failure still exits 3", async () => {
346
+ refusal = { "cluster-a": planMovedError("cluster-a") };
347
+ failing = ["cluster-b"];
348
+ expect(await runComponentsFanOut(ctx({ base: "main", env: "test" }))).toBe(3);
349
+ });
350
+ });
351
+
313
352
  describe("proof: the re-run prints already-applied for what it skips, and finishes", () => {
314
353
  test("repeating the identical command finishes the fan-out, and the approval is not re-asked", async () => {
315
354
  const dir = await mkdtemp(join(tmpdir(), "fan-out-"));
@@ -523,6 +562,7 @@ describe("gated waves (#3049): one gate per wave, bound to the wave's set digest
523
562
  },
524
563
  async run(runCtx: { component: string }) {
525
564
  ran.push(runCtx.component);
565
+ if (refusal[runCtx.component]) throw refusal[runCtx.component];
526
566
  return { ok: true };
527
567
  },
528
568
  } as never);
@@ -530,6 +570,39 @@ describe("gated waves (#3049): one gate per wave, bound to the wave's set digest
530
570
  });
531
571
  });
532
572
 
573
+ /** Run the gated fan-out once to record wave 1's pending fact, then approve that digest. */
574
+ async function approveWave1(run: () => Promise<number>): Promise<void> {
575
+ await run();
576
+ const first = gateLedger.appended[0];
577
+ gateLedger = memoryGateLedgerPort({
578
+ pending: [first],
579
+ resolutions: [
580
+ {
581
+ version: 1,
582
+ kind: "resolution",
583
+ op: "fan-out",
584
+ gate: "release-wave-1",
585
+ resolvedBy: "operator",
586
+ timestamp: new Date(Date.parse(first.timestamp) + 1000).toISOString(),
587
+ planDigest: first.planDigest,
588
+ },
589
+ ],
590
+ });
591
+ }
592
+
593
+ test.each([
594
+ ["marked planMoved", 3, () => planMovedError("net")],
595
+ ["with exitCode 3", 3, () => exitCodeError("net", 3)],
596
+ ["with exitCode 7", 1, () => exitCodeError("net", 7)],
597
+ ["unmarked", 1, () => new Error("net failed")],
598
+ ])("an approved wave whose step fails with an error %s exits %i (#3997)", async (_label, code, error) => {
599
+ const run = () => runComponentsFanOut(ctx({ base: "main", env: "test", waveGate: "release" }));
600
+ await approveWave1(run);
601
+ refusal = { net: error() };
602
+ expect(await run()).toBe(code);
603
+ expect(ran).toEqual(["net"]);
604
+ });
605
+
533
606
  test("wave 1 stops at its own gate, and the record names its digest", async () => {
534
607
  const dir = await mkdtemp(join(tmpdir(), "fan-out-waves-"));
535
608
  try {
@@ -68,9 +68,10 @@ import { approveCommand, gateIsSealed } from "../../op/gate";
68
68
  import { getHeadCommit } from "../../lifecycle/git";
69
69
  import { remainingFanOut, type ChangedUnits, type FanOutProgress } from "../../components/fan-out";
70
70
  import { readDeployedOutputs } from "../../components/wave-plan";
71
- import { deployContext } from "../../components/driver";
71
+ import { deployContext, failedRunExitCode } from "../../components/driver";
72
72
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
73
73
  import { formatError, formatInfo, formatWarning } from "../format";
74
+ import { reportUninstalledDependencies } from "../dependencies";
74
75
  import { FAN_OUT_GATE_OP } from "../../op/gate-name";
75
76
  import { resolveBuildRoot } from "./lifecycle";
76
77
  import { GATED_EXIT_CODE, autoReleaseRun, recordAutoReleasesForRun } from "./run";
@@ -237,10 +238,14 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
237
238
  } catch (err) {
238
239
  // A cycle or an unknown `dependsOn` refuses here, over the whole graph and
239
240
  // before anything is selected (#2417). The message names the members.
240
- console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
241
+ const message = err instanceof Error ? err.message : String(err);
242
+ reportUninstalledDependencies([message], projectPath); // #4116
243
+ console.error(formatError({ message }));
241
244
  return 1;
242
245
  }
243
246
  if (!derived.success) {
247
+ // #4116: component files that import packages the project has not installed.
248
+ if (derived.error) reportUninstalledDependencies([derived.error], projectPath);
244
249
  console.error(formatError({ message: derived.error ?? "Could not discover components" }));
245
250
  return 1;
246
251
  }
@@ -522,8 +527,10 @@ export async function runComponentsFanOut(ctx: CommandContext): Promise<number>
522
527
  ...(await summaryLedgerPrefix()),
523
528
  ...(pending.environment ? { environment: pending.environment } : {}),
524
529
  });
525
- return result.status === "gated" ? GATED_EXIT_CODE : 1;
530
+ return result.status === "gated" ? GATED_EXIT_CODE : failedRunExitCode(result.results);
526
531
  }
527
532
 
528
- return result.status === "ok" ? 0 : 1;
533
+ // A step that refused the wave's plan because its target moved since the
534
+ // approval exits 3, the code of a wave whose plan changed (#3997).
535
+ return result.status === "ok" ? 0 : failedRunExitCode(result.results);
529
536
  }
@@ -1,4 +1,7 @@
1
1
  import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
2
5
  import type { ParsedArgs } from "../registry";
3
6
  import { DECLARABLE_MARKER, type Declarable } from "../../declarable";
4
7
  import { AttrRef } from "../../attrref";
@@ -468,6 +471,79 @@ describe("runGraph", () => {
468
471
  expect(discoverMock).not.toHaveBeenCalled();
469
472
  });
470
473
 
474
+ // #4116: a clone of examples/fan-out-estate with no node_modules. Every
475
+ // source file fails to import its lexicon, and lint reports each import.
476
+ describe("lint gate: dependencies not installed (#4116)", () => {
477
+ let project: string;
478
+ beforeEach(() => {
479
+ project = realpathSync(mkdtempSync(join(tmpdir(), "chant-4116-graph-")));
480
+ });
481
+ afterEach(() => {
482
+ rmSync(project, { recursive: true, force: true });
483
+ });
484
+ const missing = (pkg: string, file: string) => ({
485
+ file: join(project, file), line: 1, column: 1, ruleId: "LEX002", severity: "error",
486
+ message: `Cannot find package '${pkg}' imported from ${join(project, file)}`,
487
+ });
488
+
489
+ test("every lint error an uninstalled import: the dependencies message, not lint errors", async () => {
490
+ lintMock.mockResolvedValue({
491
+ success: false,
492
+ diagnostics: [
493
+ missing("@intentius/chant-lexicon-aws", "src/network/outputs.ts"),
494
+ { ...missing("@intentius/chant", "network.component.ts"), ruleId: "COMP000", message: `Component discovery error: ${missing("@intentius/chant", "network.component.ts").message}` },
495
+ { ...missing("@intentius/chant", "x.ts"), ruleId: "COR004", severity: "warning", message: "a warning" },
496
+ ],
497
+ });
498
+ for (const components of [false, true]) {
499
+ stderrBuf.length = 0;
500
+ const exit = await runGraph({ args: makeArgs({ format: "ir", path: project, components }), plugins: [], serializers: [] });
501
+ expect(exit).toBe(1);
502
+ const err = stderrBuf.join("\n");
503
+ expect(err).toContain(`dependencies not installed: @intentius/chant-lexicon-aws does not resolve from ${project}`);
504
+ expect(err).toContain("run npm install (or the project's package manager)");
505
+ expect(err).not.toMatch(/lint errors/i);
506
+ }
507
+ });
508
+
509
+ test("an uninstalled import beside a real lint error: both are said", async () => {
510
+ lintMock.mockResolvedValue({
511
+ success: false,
512
+ diagnostics: [
513
+ missing("@intentius/chant-lexicon-aws", "src/a.ts"),
514
+ { file: join(project, "src/b.ts"), line: 3, column: 1, ruleId: "COR001", severity: "error", message: "a real problem" },
515
+ ],
516
+ });
517
+ const exit = await runGraph({ args: makeArgs({ format: "ir", path: project }), plugins: [], serializers: [] });
518
+ expect(exit).toBe(1);
519
+ const err = stderrBuf.join("\n");
520
+ expect(err).toContain("dependencies not installed: @intentius/chant-lexicon-aws");
521
+ expect(err).toMatch(/source has lint errors/);
522
+ });
523
+
524
+ test("real lint errors still say lint errors, with no dependencies message", async () => {
525
+ lintMock.mockResolvedValue({
526
+ success: false,
527
+ diagnostics: [{ file: join(project, "src/b.ts"), line: 3, column: 1, ruleId: "COR001", severity: "error", message: "a real problem" }],
528
+ });
529
+ const exit = await runGraph({ args: makeArgs({ format: "ir", path: project }), plugins: [], serializers: [] });
530
+ expect(exit).toBe(1);
531
+ const err = stderrBuf.join("\n");
532
+ expect(err).toMatch(/source has lint errors/);
533
+ expect(err).not.toContain("dependencies not installed");
534
+ });
535
+
536
+ test("an import the project has installed is a lint error, not a missing dependency", async () => {
537
+ mkdirSync(join(project, "node_modules", "@intentius", "chant-lexicon-aws"), { recursive: true });
538
+ lintMock.mockResolvedValue({ success: false, diagnostics: [missing("@intentius/chant-lexicon-aws", "src/a.ts")] });
539
+ const exit = await runGraph({ args: makeArgs({ format: "ir", path: project }), plugins: [], serializers: [] });
540
+ expect(exit).toBe(1);
541
+ const err = stderrBuf.join("\n");
542
+ expect(err).toMatch(/source has lint errors/);
543
+ expect(err).not.toContain("dependencies not installed");
544
+ });
545
+ });
546
+
471
547
  test("--format mermaid emits a flowchart", async () => {
472
548
  lintClean(); discovered();
473
549
  const exit = await runGraph({ args: makeArgs({ format: "mermaid" }), plugins: [], serializers: [] });
@@ -19,7 +19,8 @@ import { applyLens, parseLens } from "../../graph-lens";
19
19
  import { toMermaid } from "../../graph-mermaid";
20
20
  import { toDot } from "../../graph-dot";
21
21
  import { getLayoutEngine, toLayoutInput, type NodeSize } from "../../graph-layout";
22
- import { lintCommand } from "../commands/lint";
22
+ import { lintCommand, type LintResult } from "../commands/lint";
23
+ import { reportUninstalledDependencies } from "../dependencies";
23
24
  import { loadPlugins, resolveProjectLexicons, collectBuildRootContributors } from "../plugins";
24
25
  import { isNoLexiconDetected } from "../../detectLexicon";
25
26
  import { readFileSync } from "node:fs";
@@ -555,6 +556,21 @@ async function runComponentGraph(ctx: CommandContext): Promise<number> {
555
556
  return 0;
556
557
  }
557
558
 
559
+ /**
560
+ * The lint gate's refusal. When the lint errors are imports of packages the
561
+ * project has not installed (#4116), the dependencies message leads, and it
562
+ * stands alone when every error is one: the source is not what is wrong.
563
+ */
564
+ function refuseLintGate(lint: LintResult, projectPath: string): void {
565
+ const errors = (lint.diagnostics ?? []).filter((d) => d.severity === "error").map((d) => d.message);
566
+ if (reportUninstalledDependencies(errors, projectPath) === "all") return;
567
+ console.error(
568
+ formatError({
569
+ message: "Refusing to emit graph: source has lint errors. Run `chant lint` and fix them first.",
570
+ }),
571
+ );
572
+ }
573
+
558
574
  /**
559
575
  * `chant graph --components --format ir|mermaid|dot|layout` — the component DAG
560
576
  * projected into the renderable IR (nodes = components, `groups.byWave` = the
@@ -596,11 +612,7 @@ async function runComponentGraphView(
596
612
  buildParams: componentParams,
597
613
  });
598
614
  if (!lint.success) {
599
- console.error(
600
- formatError({
601
- message: "Refusing to emit graph: source has lint errors. Run `chant lint` and fix them first.",
602
- }),
603
- );
615
+ refuseLintGate(lint, projectPath);
604
616
  return 1;
605
617
  }
606
618
 
@@ -869,12 +881,7 @@ async function runGraphView(
869
881
  buildParams,
870
882
  });
871
883
  if (!lint.success) {
872
- console.error(
873
- formatError({
874
- message:
875
- "Refusing to emit graph: source has lint errors. Run `chant lint` and fix them first.",
876
- }),
877
- );
884
+ refuseLintGate(lint, projectPath);
878
885
  return 1;
879
886
  }
880
887
 
@@ -16,6 +16,9 @@
16
16
  * #1108 gap in generate mode.
17
17
  */
18
18
  import { describe, test, expect, vi, beforeEach, afterEach } from "vitest";
19
+ import { mkdtempSync, realpathSync, rmSync } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
19
22
  import type { ParsedArgs } from "../registry";
20
23
 
21
24
  const lintCommandMock = vi.fn();
@@ -105,3 +108,42 @@ describe("runLint build-time parameters (#2251)", () => {
105
108
  expect(lintCommandMock).not.toHaveBeenCalled();
106
109
  });
107
110
  });
111
+
112
+ describe("runLint on a project whose dependencies are not installed (#4116)", () => {
113
+ let project: string;
114
+ let stderr: string[];
115
+
116
+ beforeEach(() => {
117
+ project = realpathSync(mkdtempSync(join(tmpdir(), "chant-4116-lint-")));
118
+ stderr = [];
119
+ loadChantConfigUpwardMock.mockReset().mockResolvedValue({ config: {} });
120
+ vi.spyOn(console, "error").mockImplementation((s: string) => void stderr.push(s));
121
+ });
122
+
123
+ afterEach(() => {
124
+ vi.restoreAllMocks();
125
+ rmSync(project, { recursive: true, force: true });
126
+ });
127
+
128
+ const failing = (message: string) =>
129
+ lintCommandMock.mockReset().mockResolvedValue({
130
+ success: false,
131
+ errorCount: 1,
132
+ warningCount: 0,
133
+ diagnostics: [{ file: join(project, "src/a.ts"), line: 1, column: 1, ruleId: "LEX002", severity: "error", message }],
134
+ });
135
+
136
+ test("an uninstalled import leads with the dependencies message", async () => {
137
+ failing(`Cannot find package '@intentius/chant-lexicon-aws' imported from ${join(project, "src/a.ts")}`);
138
+ const exit = await runLint({ args: makeArgs({ path: project }), plugins: [], serializers: [] });
139
+ expect(exit).toBe(1);
140
+ expect(stderr.join("\n")).toContain(`dependencies not installed: @intentius/chant-lexicon-aws does not resolve from ${project}`);
141
+ });
142
+
143
+ test("a real lint error prints no dependencies message", async () => {
144
+ failing("Exported declarable 'x' is broken");
145
+ const exit = await runLint({ args: makeArgs({ path: project }), plugins: [], serializers: [] });
146
+ expect(exit).toBe(1);
147
+ expect(stderr.join("\n")).not.toContain("dependencies not installed");
148
+ });
149
+ });
@@ -1,6 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import { lintCommand, lintCommandWatch, printLintResult } from "../commands/lint";
3
3
  import { formatError, formatInfo } from "../format";
4
+ import { reportUninstalledDependencies } from "../dependencies";
4
5
  import type { CommandContext } from "../registry";
5
6
  import { commandBuildParams } from "../build-params-cli";
6
7
  import { loadChantConfigUpward, type ChantConfig } from "../../config";
@@ -63,6 +64,11 @@ export async function runLint(ctx: CommandContext): Promise<number> {
63
64
  buildParams,
64
65
  });
65
66
 
67
+ // #4116: errors that are imports of uninstalled packages are led by the dependencies message.
68
+ if (!result.success) {
69
+ const errors = result.diagnostics.filter((d) => d.severity === "error").map((d) => d.message);
70
+ reportUninstalledDependencies(errors, resolve(args.path));
71
+ }
66
72
  printLintResult(result);
67
73
  return result.success ? 0 : 1;
68
74
  }
@@ -221,8 +221,8 @@ export async function runOperator(ctx: CommandContext): Promise<number> {
221
221
  *
222
222
  * Refuses, before ticking anything:
223
223
  * - an environment whose form is `fountain`, because there the composite's
224
- * Agent and Teammate are the steward and this process would be a second
225
- * writer beside them;
224
+ * Agent and its conversation are the steward and this process would be a
225
+ * second writer beside them;
226
226
  * - a steward whose lease another live process holds (a second local
227
227
  * operator for it).
228
228
  */
@@ -242,7 +242,7 @@ export async function runStewardOperator(ctx: CommandContext): Promise<number> {
242
242
  if (form !== "local") {
243
243
  console.error(formatError({
244
244
  message: `steward "${steward.name}" runs on ${form} in environment "${env}", so a local operator for it would be a second writer`,
245
- hint: `Its turns run on the fountain teammate "${steward.name}". Run this in an environment the declaration's form makes local, or change the form.`,
245
+ hint: `Its turns run in the fountain steward "${steward.name}"'s conversation. Run this in an environment the declaration's form makes local, or change the form.`,
246
246
  }));
247
247
  return 1;
248
248
  }