@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
@@ -461,6 +461,98 @@ export function unsupportedExpressionMessage(node: ts.Node): string {
461
461
  return `unsupported expression: ${ts.SyntaxKind[node.kind]}`;
462
462
  }
463
463
 
464
+ /**
465
+ * chant#4011 — S-CallMethod's closed table of HOST methods, by receiver type.
466
+ * PROVISIONAL until typescript-as-data narrows F-Eval-CallMethod to the same
467
+ * table (INTENTIUS/typescript-as-data#264; see the module doc's provisional path).
468
+ *
469
+ * Since #1966 `fold()` called any function-valued property on a real
470
+ * receiver, so `[].constructor.constructor("return Date.now()")` reached
471
+ * `Function` and a `[fold:fold]` file could put the clock, `process.env` or
472
+ * `Math.random()` in its output. A method the JavaScript engine provides
473
+ * (a native function) now folds only when it is listed here for the
474
+ * receiver's type AND is the very function that type's prototype carries, so
475
+ * an own property cannot shadow a listed name. Every entry is deterministic
476
+ * given its receiver and arguments: no callbacks (`map`, `filter`), no
477
+ * mutation (`push`, `sort`), and no `toLocale*` or `localeCompare`, whose
478
+ * answer follows the host's locale and time zone.
479
+ *
480
+ * A method defined by a lexicon package's own code (the github lexicon's
481
+ * `Expression.prototype.toString`) is not a host method and is not looked up
482
+ * here; it is the same class of package code as an eager intrinsic call.
483
+ */
484
+ export const FOLDABLE_HOST_METHODS: {
485
+ readonly string: ReadonlySet<string>;
486
+ readonly number: ReadonlySet<string>;
487
+ readonly boolean: ReadonlySet<string>;
488
+ readonly array: ReadonlySet<string>;
489
+ readonly object: ReadonlySet<string>;
490
+ } = {
491
+ string: new Set([
492
+ "at",
493
+ "charAt",
494
+ "charCodeAt",
495
+ "codePointAt",
496
+ "concat",
497
+ "endsWith",
498
+ "includes",
499
+ "indexOf",
500
+ "lastIndexOf",
501
+ "padEnd",
502
+ "padStart",
503
+ "repeat",
504
+ "replace",
505
+ "replaceAll",
506
+ "slice",
507
+ "split",
508
+ "startsWith",
509
+ "substring",
510
+ "toLowerCase",
511
+ "toString",
512
+ "toUpperCase",
513
+ "trim",
514
+ "trimEnd",
515
+ "trimStart",
516
+ "valueOf",
517
+ ]),
518
+ number: new Set(["toExponential", "toFixed", "toPrecision", "toString", "valueOf"]),
519
+ boolean: new Set(["toString", "valueOf"]),
520
+ array: new Set(["at", "concat", "flat", "includes", "indexOf", "join", "lastIndexOf", "slice", "toString"]),
521
+ object: new Set(["hasOwnProperty", "toString", "valueOf"]),
522
+ };
523
+
524
+ /**
525
+ * chant#4011 — method names `fold()` refuses on every receiver, lexicon
526
+ * objects included. These are the names that reach `Function` (`constructor`,
527
+ * and `call`/`apply`/`bind` on whatever it returns) or read the host locale.
528
+ * Unlike {@link FOLDABLE_HOST_METHODS}, which needs the receiver's runtime
529
+ * type, a name in this set is decidable from syntax, so EVL001 refuses it too.
530
+ */
531
+ export const REFUSED_METHOD_NAMES: ReadonlySet<string> = new Set([
532
+ "constructor",
533
+ "call",
534
+ "apply",
535
+ "bind",
536
+ "toLocaleString",
537
+ "toLocaleDateString",
538
+ "toLocaleTimeString",
539
+ "toLocaleUpperCase",
540
+ "toLocaleLowerCase",
541
+ "localeCompare",
542
+ "__defineGetter__",
543
+ "__defineSetter__",
544
+ "__lookupGetter__",
545
+ "__lookupSetter__",
546
+ ]);
547
+
548
+ /** The refusal for a method name in {@link REFUSED_METHOD_NAMES}, shared by `fold()` and EVL001. */
549
+ export function refusedMethodMessage(methodName: string): string {
550
+ return (
551
+ `method call \`.${methodName}(...)\` is not foldable: it reaches \`Function\` or the host locale, ` +
552
+ "so its result is not determined by the source (chant#4011) — falls back to run"
553
+ );
554
+ }
555
+
464
556
  /**
465
557
  * Classify one object-literal member (`{ a: 1 }`'s `a: 1`, `{ ...x }`'s
466
558
  * `...x`, or a shorthand `{ a }`). Checks the key's shape before the
@@ -720,7 +812,14 @@ export function findSubsetViolation(
720
812
  // a same-file `new` used as a value (#1169). Accepted here regardless of
721
813
  // the intrinsics registry, unlike the two cases just above: nothing about
722
814
  // whether a method call folds depends on a lexicon's registration.
815
+ //
816
+ // chant#4011 — except a name `fold()` refuses on every receiver, which is
817
+ // decidable here. The per-type host table needs the receiver's value, so
818
+ // it stays on `fold()`'s side, in the direction F-Direction allows.
723
819
  if (ts.isPropertyAccessExpression(node.expression)) {
820
+ if (REFUSED_METHOD_NAMES.has(node.expression.name.text)) {
821
+ return violation(node, refusedMethodMessage(node.expression.name.text));
822
+ }
724
823
  const v = findSubsetViolation(node.expression.expression, intrinsics);
725
824
  if (v) return v;
726
825
  for (const arg of node.arguments) {
@@ -87,6 +87,63 @@ describe("applyDetail", () => {
87
87
  expect(ir.imports).toEqual([{ name: "pClusterArn", node: "pClusterArn" }]);
88
88
  });
89
89
 
90
+ test("T1 keeps exports, each on the node that now holds its producer (#4178)", () => {
91
+ const withExports: GraphIR = {
92
+ ...base,
93
+ version: 2,
94
+ meta: { _behaviour: { engine: "x" } },
95
+ exports: [
96
+ { name: "DbEndpoint", node: "dbDeployment", attr: "status" },
97
+ { name: "SubnetLink", node: "subnet", attr: "selfLink" },
98
+ { name: "Literal" },
99
+ ],
100
+ };
101
+ const ir = applyDetail(withExports, DETAIL.COMPOSITES);
102
+ expect(ir.exports).toEqual([
103
+ { name: "DbEndpoint", node: "db", attr: "status" },
104
+ { name: "SubnetLink", node: "subnet", attr: "selfLink" },
105
+ { name: "Literal" },
106
+ ]);
107
+ expect(ir.version).toBe(2);
108
+ expect(ir.meta).toEqual({ _behaviour: { engine: "x" } });
109
+ });
110
+
111
+ test("T1 keeps two same-named composites from different composite types apart (#4178)", () => {
112
+ const twoWebs: GraphIR = {
113
+ nodes: [
114
+ { id: "webAlb", kind: "AWS::ElasticLoadBalancingV2::LoadBalancer", lexicon: "aws", compositeParent: "FargateAlb", compositeInstance: "web", attrs: {} },
115
+ { id: "webService", kind: "AWS::ECS::Service", lexicon: "aws", compositeParent: "FargateAlb", compositeInstance: "web", attrs: {} },
116
+ { id: "webRun", kind: "RunService", lexicon: "gcp", compositeParent: "CloudRunServiceComposite", compositeInstance: "web", attrs: {} },
117
+ { id: "record", kind: "AWS::Route53::RecordSet", lexicon: "aws", attrs: {} },
118
+ ],
119
+ edges: [
120
+ { from: "record", to: "webAlb", kind: "ref", viaAttr: "AliasTarget" },
121
+ { from: "record", to: "webRun", kind: "ref", viaAttr: "ResourceRecords" },
122
+ { from: "webService", to: "webAlb", kind: "ref", viaAttr: "LoadBalancers" },
123
+ ],
124
+ groups: {},
125
+ exports: [{ name: "WebUri", node: "webRun", attr: "uri" }],
126
+ };
127
+ const ir = applyDetail(twoWebs, DETAIL.COMPOSITES);
128
+ expect(ir.nodes.map((n) => n.id)).toEqual(["record", "web@CloudRunServiceComposite", "web@FargateAlb"]);
129
+ expect(ir.nodes.find((n) => n.id === "web@FargateAlb")).toMatchObject({ kind: "FargateAlb", lexicon: "aws", attrs: { members: 2 } });
130
+ expect(ir.nodes.find((n) => n.id === "web@CloudRunServiceComposite")).toMatchObject({ kind: "CloudRunServiceComposite", lexicon: "gcp", attrs: { members: 1 } });
131
+ expect(ir.edges).toEqual(expect.arrayContaining([
132
+ { from: "record", to: "web@FargateAlb", kind: "ref" },
133
+ { from: "record", to: "web@CloudRunServiceComposite", kind: "ref" },
134
+ ]));
135
+ expect(ir.edges).toHaveLength(2);
136
+ expect(ir.exports).toEqual([{ name: "WebUri", node: "web@CloudRunServiceComposite", attr: "uri" }]);
137
+ });
138
+
139
+ test("T1 keeps one composite whose members span lexicons as one node", () => {
140
+ const ir = applyDetail({
141
+ ...base,
142
+ nodes: base.nodes.map((n) => (n.id === "dbNamespace" ? { ...n, lexicon: "gcp" } : n)),
143
+ }, DETAIL.COMPOSITES);
144
+ expect(ir.nodes.find((n) => n.id === "db")).toMatchObject({ kind: "DbStack", lexicon: "multi", attrs: { members: 2 } });
145
+ });
146
+
90
147
  test("T1 node count is between T0 and T2", () => {
91
148
  const t0 = applyDetail(base, DETAIL.STACKS).nodes.length;
92
149
  const t1 = applyDetail(base, DETAIL.COMPOSITES).nodes.length;
@@ -127,27 +127,46 @@ function toDeclarables(ir: GraphIR): GraphIR {
127
127
  /** T1 — collapse each composite instance to one node; internal edges disappear.
128
128
  * Topology view: surviving plain nodes keep only overlay paint, not properties. */
129
129
  function toComposites(ir: GraphIR): GraphIR {
130
- const idMap = new Map<string, string>();
131
- for (const n of ir.nodes) idMap.set(n.id, n.compositeInstance ?? n.id);
132
-
130
+ // A composite instance is its export name plus the composite that made it:
131
+ // an AWS FargateAlb and a GCP CloudRunServiceComposite both exported as
132
+ // `web` are two instances (#4178). One composite whose members span
133
+ // lexicons stays one instance.
134
+ const instanceKey = (n: IRNode) => `${n.compositeInstance}\0${n.compositeParent ?? ""}`;
133
135
  const instances = new Map<string, IRNode[]>();
134
136
  const plain: IRNode[] = [];
135
137
  for (const n of ir.nodes) {
136
138
  if (n.compositeInstance) {
137
- const arr = instances.get(n.compositeInstance) ?? [];
139
+ const key = instanceKey(n);
140
+ const arr = instances.get(key) ?? [];
138
141
  arr.push(n);
139
- instances.set(n.compositeInstance, arr);
142
+ instances.set(key, arr);
140
143
  } else {
141
144
  plain.push({ ...n, attrs: paintAttrs(n.attrs) });
142
145
  }
143
146
  }
144
147
 
148
+ // The collapsed id is the instance name, or `<instance>@<composite>` when
149
+ // two composites share the name.
150
+ const sharing = new Map<string, number>();
151
+ for (const members of instances.values()) {
152
+ const inst = members[0].compositeInstance!;
153
+ sharing.set(inst, (sharing.get(inst) ?? 0) + 1);
154
+ }
155
+ const collapsedId = new Map<string, string>();
156
+ for (const [key, members] of instances) {
157
+ const { compositeInstance: inst, compositeParent: parent } = members[0];
158
+ collapsedId.set(key, sharing.get(inst!)! > 1 && parent ? `${inst}@${parent}` : inst!);
159
+ }
160
+
161
+ const idMap = new Map<string, string>();
162
+ for (const n of ir.nodes) idMap.set(n.id, n.compositeInstance ? collapsedId.get(instanceKey(n))! : n.id);
163
+
145
164
  const nodes: IRNode[] = [...plain];
146
- for (const [inst, members] of instances) {
165
+ for (const [key, members] of instances) {
147
166
  const types = new Set(members.map((m) => m.compositeParent).filter(Boolean) as string[]);
148
167
  const lexicons = new Set(members.map((m) => m.lexicon));
149
168
  nodes.push({
150
- id: inst,
169
+ id: collapsedId.get(key)!,
151
170
  kind: types.size === 1 ? [...types][0] : "Composite",
152
171
  lexicon: lexicons.size === 1 ? [...lexicons][0] : "multi",
153
172
  attrs: { members: members.length },
@@ -197,11 +216,18 @@ function toComposites(ir: GraphIR): GraphIR {
197
216
  }
198
217
  if (Object.keys(byPipeline).length) groups.byPipeline = byPipeline;
199
218
  }
219
+ // Exports (#4178) stay at this tier, each on the node that now holds its
220
+ // producer, so a viewer can still match another member's import to it.
221
+ const exports = ir.exports?.map((x) => (x.node ? { ...x, node: idMap.get(x.node) ?? x.node } : { ...x }));
200
222
  return {
223
+ ...(ir.version !== undefined ? { version: ir.version } : {}),
201
224
  nodes,
202
225
  edges: sortEdges([...seen.values()]),
203
226
  groups,
227
+ ...(ir.meta ? { meta: ir.meta } : {}),
228
+ ...(exports && exports.length ? { exports } : {}),
204
229
  ...(imports && imports.length ? { imports } : {}),
230
+ ...(ir.pipeline ? { pipeline: ir.pipeline } : {}),
205
231
  ...(dangling && dangling.length ? { dangling } : {}),
206
232
  };
207
233
  }
@@ -0,0 +1,191 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://intentius.io/chant/schemas/health/v1/health.schema.json",
4
+ "title": "chant-health/v1",
5
+ "description": "What `chant lifecycle health <env> --json` prints (#4177): whether each declared resource that has a health read is up, read separately from drift. Every entity a read applies to lands in exactly one of `entities` (read, with a state), `unobserved` (the read failed, never folded into unhealthy) or `absent` (the provider says it is not deployed). Entity names are the graph's node ids. A single-stack project puts `lexicons` at the top level; a multi-stack one nests them under `stacks`. Written by the first chant release after 0.124.32. Readers ignore fields they do not know; a field is only ever added within a version.",
6
+ "type": "object",
7
+ "required": [
8
+ "schema",
9
+ "chantVersion",
10
+ "environment",
11
+ "observedAt",
12
+ "summary"
13
+ ],
14
+ "properties": {
15
+ "schema": {
16
+ "const": "chant-health/v1"
17
+ },
18
+ "chantVersion": {
19
+ "type": "string",
20
+ "description": "The chant that ran the read."
21
+ },
22
+ "environment": {
23
+ "type": "string",
24
+ "description": "The environment named on the command line."
25
+ },
26
+ "observedAt": {
27
+ "type": "string",
28
+ "description": "When the command started reading, ISO 8601 UTC."
29
+ },
30
+ "summary": {
31
+ "type": "object",
32
+ "required": [
33
+ "healthy",
34
+ "unhealthy",
35
+ "unknown",
36
+ "unobserved",
37
+ "absent"
38
+ ],
39
+ "description": "Counts over every lexicon and stack.",
40
+ "properties": {
41
+ "healthy": {
42
+ "type": "integer",
43
+ "minimum": 0
44
+ },
45
+ "unhealthy": {
46
+ "type": "integer",
47
+ "minimum": 0
48
+ },
49
+ "unknown": {
50
+ "type": "integer",
51
+ "minimum": 0
52
+ },
53
+ "unobserved": {
54
+ "type": "integer",
55
+ "minimum": 0
56
+ },
57
+ "absent": {
58
+ "type": "integer",
59
+ "minimum": 0
60
+ }
61
+ }
62
+ }
63
+ },
64
+ "oneOf": [
65
+ {
66
+ "required": [
67
+ "lexicons"
68
+ ],
69
+ "properties": {
70
+ "lexicons": {
71
+ "$ref": "#/$defs/byLexicon"
72
+ }
73
+ }
74
+ },
75
+ {
76
+ "required": [
77
+ "stacks"
78
+ ],
79
+ "properties": {
80
+ "stacks": {
81
+ "type": "object",
82
+ "description": "Per stack name, the same per-lexicon results.",
83
+ "additionalProperties": {
84
+ "$ref": "#/$defs/byLexicon"
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ],
90
+ "$defs": {
91
+ "byLexicon": {
92
+ "type": "object",
93
+ "description": "Per lexicon name, that lexicon's read. A lexicon with no applicable entity is left out.",
94
+ "additionalProperties": {
95
+ "$ref": "#/$defs/read"
96
+ }
97
+ },
98
+ "read": {
99
+ "type": "object",
100
+ "required": [
101
+ "entities",
102
+ "unobserved",
103
+ "absent"
104
+ ],
105
+ "properties": {
106
+ "entities": {
107
+ "type": "object",
108
+ "additionalProperties": {
109
+ "$ref": "#/$defs/entry"
110
+ }
111
+ },
112
+ "unobserved": {
113
+ "type": "object",
114
+ "additionalProperties": {
115
+ "$ref": "#/$defs/hole"
116
+ }
117
+ },
118
+ "absent": {
119
+ "type": "array",
120
+ "items": {
121
+ "type": "string"
122
+ },
123
+ "description": "Entity names the provider says are not deployed."
124
+ }
125
+ }
126
+ },
127
+ "entry": {
128
+ "type": "object",
129
+ "required": [
130
+ "type",
131
+ "state",
132
+ "detail",
133
+ "observedAt"
134
+ ],
135
+ "properties": {
136
+ "type": {
137
+ "type": "string",
138
+ "description": "Entity type, such as AWS::ECS::Service."
139
+ },
140
+ "state": {
141
+ "enum": [
142
+ "healthy",
143
+ "unhealthy",
144
+ "unknown"
145
+ ],
146
+ "description": "healthy and unhealthy are what the provider reported. unknown means the read succeeded and the answer decides neither way: a rollout in progress, a service scaled to zero, a condition pending."
147
+ },
148
+ "detail": {
149
+ "type": "string",
150
+ "description": "One short line saying why, in the provider's terms."
151
+ },
152
+ "observedAt": {
153
+ "type": "string",
154
+ "description": "When chant read it, ISO 8601."
155
+ },
156
+ "checkedAt": {
157
+ "type": "string",
158
+ "description": "When the provider last checked or changed the state, when it says, ISO 8601."
159
+ }
160
+ }
161
+ },
162
+ "hole": {
163
+ "type": "object",
164
+ "required": [
165
+ "reason"
166
+ ],
167
+ "description": "A read that failed. Never counted as unhealthy.",
168
+ "properties": {
169
+ "type": {
170
+ "type": "string"
171
+ },
172
+ "reason": {
173
+ "enum": [
174
+ "read-failed",
175
+ "no-credentials",
176
+ "no-binding",
177
+ "unsupported-kind",
178
+ "filtered"
179
+ ]
180
+ },
181
+ "detail": {
182
+ "type": "string"
183
+ },
184
+ "queried": {
185
+ "type": "string",
186
+ "description": "The address the read was issued against, when the lexicon reports it."
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
@@ -0,0 +1,62 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { healthApplicable, observeHealthFor, summarizeHealth, totalHealth, type HealthLexicon } from "./health";
3
+
4
+ const entities = new Map([
5
+ ["web", { entityType: "AWS::ECS::Service", props: {} }],
6
+ ["check", { entityType: "AWS::Route53::HealthCheck", props: {} }],
7
+ ]);
8
+
9
+ describe("health reads (#4177)", () => {
10
+ test("healthApplicable keeps only the types the lexicon names", () => {
11
+ const all = new Map([...entities, ["bucket", { entityType: "AWS::S3::Bucket", props: {} }]]);
12
+ const picked = healthApplicable({ name: "aws", healthTypes: ["AWS::ECS::Service"] }, all);
13
+ expect([...picked.keys()]).toEqual(["web"]);
14
+ });
15
+
16
+ test("a thrown reader is read-failed for every entity, never unhealthy", async () => {
17
+ const plugin: HealthLexicon = {
18
+ name: "aws",
19
+ healthTypes: ["AWS::ECS::Service"],
20
+ observeHealth: async () => {
21
+ throw new Error("connect ECONNREFUSED");
22
+ },
23
+ };
24
+ const r = await observeHealthFor(plugin, { environment: "dev", buildOutput: "", entities });
25
+ expect(r.entities).toEqual({});
26
+ expect(r.unobserved.web).toEqual({ reason: "read-failed", type: "AWS::ECS::Service", detail: "connect ECONNREFUSED" });
27
+ expect(Object.keys(r.unobserved)).toEqual(["web", "check"]);
28
+ });
29
+
30
+ test("a lexicon without a reader reports unsupported-kind", async () => {
31
+ const r = await observeHealthFor({ name: "aws" }, { environment: "dev", buildOutput: "", entities });
32
+ expect(r.unobserved.web?.reason).toBe("unsupported-kind");
33
+ });
34
+
35
+ test("the result is made total, and entities win over absent and unobserved", () => {
36
+ const r = totalHealth(["web", "check", "gone", "forgot"], {
37
+ entities: { web: { type: "AWS::ECS::Service", state: "healthy", detail: "1/1 running", observedAt: "t" } },
38
+ unobserved: { web: { reason: "read-failed" }, check: { reason: "no-credentials" } },
39
+ absent: ["gone", "web"],
40
+ });
41
+ expect(Object.keys(r.entities)).toEqual(["web"]);
42
+ expect(r.absent).toEqual(["gone"]);
43
+ expect(r.unobserved.check?.reason).toBe("no-credentials");
44
+ expect(r.unobserved.forgot?.reason).toBe("read-failed");
45
+ expect(r.unobserved.web).toBeUndefined();
46
+ });
47
+
48
+ test("summarizeHealth counts each place", () => {
49
+ const s = summarizeHealth([
50
+ {
51
+ entities: {
52
+ a: { type: "t", state: "healthy", detail: "", observedAt: "" },
53
+ b: { type: "t", state: "unhealthy", detail: "", observedAt: "" },
54
+ c: { type: "t", state: "unknown", detail: "", observedAt: "" },
55
+ },
56
+ unobserved: { d: { reason: "read-failed" } },
57
+ absent: ["e", "f"],
58
+ },
59
+ ]);
60
+ expect(s).toEqual({ healthy: 1, unhealthy: 1, unknown: 1, unobserved: 1, absent: 2 });
61
+ });
62
+ });
package/src/health.ts ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Runtime health (#4177): whether a declared thing is up, read separately from
3
+ * drift.
4
+ *
5
+ * The observation family answers what is deployed (`describeResources`), what
6
+ * its properties are (`observeResourcesDeep`) and what it would do
7
+ * (`predictBehaviour`). None of them says whether a service is serving. A
8
+ * lexicon that can read that implements {@link HealthLexicon.observeHealth}
9
+ * for the types it names in `healthTypes`, and `chant lifecycle health` reports
10
+ * the answers in the {@link HEALTH_SCHEMA} shape.
11
+ *
12
+ * The verdicts follow #1089's rule. Every entity the caller asks about lands in
13
+ * exactly one place: `entities` (read, with a state), `unobserved` (the read
14
+ * failed, a hole with a reason) or `absent` (the provider says it is not
15
+ * deployed). A failed read is never `unhealthy`: `unhealthy` is something the
16
+ * provider said.
17
+ *
18
+ * Routing is not modelled. A consumer that wants rotation (a latency record
19
+ * whose health check is unhealthy is out of rotation) derives it from the
20
+ * graph's edges and these states.
21
+ */
22
+
23
+ import type { UnobservedEntity, UnobservedReason } from "./observation";
24
+ import { unobservedAll } from "./observation";
25
+
26
+ /** The versioned shape `chant lifecycle health --json` prints. The JSON Schema is `./health.schema.json`. */
27
+ export const HEALTH_SCHEMA = "chant-health/v1";
28
+
29
+ /** The `$id` of `./health.schema.json`, which the package exports as `@intentius/chant/health.schema.json`. */
30
+ export const HEALTH_SCHEMA_ID = "https://intentius.io/chant/schemas/health/v1/health.schema.json";
31
+
32
+ /**
33
+ * `healthy` and `unhealthy` are what the provider reported. `unknown` means the
34
+ * read succeeded and the provider's answer does not decide either way (a
35
+ * deployment still rolling, a service scaled to zero, a condition pending).
36
+ */
37
+ export type HealthState = "healthy" | "unhealthy" | "unknown";
38
+
39
+ export interface HealthEntry {
40
+ /** Entity type, e.g. `AWS::ECS::Service`. */
41
+ type: string;
42
+ state: HealthState;
43
+ /** One short line saying why, in the provider's terms. */
44
+ detail: string;
45
+ /** When chant read it (ISO 8601). */
46
+ observedAt: string;
47
+ /** When the provider last checked or changed the state, when it says (ISO 8601). */
48
+ checkedAt?: string;
49
+ }
50
+
51
+ /** What one lexicon's health read returns. */
52
+ export interface HealthReadResult {
53
+ /** Read, keyed by chant entity name, which is the graph's node id. */
54
+ entities: Record<string, HealthEntry>;
55
+ /** Could not be read. Never folded into `unhealthy`. */
56
+ unobserved: Record<string, UnobservedEntity>;
57
+ /** The provider says these are not deployed. */
58
+ absent: string[];
59
+ }
60
+
61
+ /** The options core passes a lexicon's health read. They mirror `observeResourcesDeep`'s. */
62
+ export interface HealthReadOptions {
63
+ environment: string;
64
+ buildOutput: string;
65
+ /** Only entities whose type is in the lexicon's `healthTypes`. */
66
+ entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
67
+ stack?: string;
68
+ region?: string;
69
+ }
70
+
71
+ /** The two plugin members a lexicon implements to answer a health read. */
72
+ export interface HealthLexicon {
73
+ name: string;
74
+ /** Entity types {@link observeHealth} can read. Data, so core picks the entities without calling the reader. */
75
+ healthTypes?: readonly string[];
76
+ observeHealth?(options: HealthReadOptions): Promise<HealthReadResult>;
77
+ }
78
+
79
+ /**
80
+ * The entities of one lexicon a health read applies to: those whose type the
81
+ * lexicon names in `healthTypes`.
82
+ */
83
+ export function healthApplicable(
84
+ plugin: HealthLexicon,
85
+ entities: Map<string, { entityType: string; props: Record<string, unknown> }>,
86
+ ): Map<string, { entityType: string; props: Record<string, unknown> }> {
87
+ const types = new Set(plugin.healthTypes ?? []);
88
+ const out = new Map<string, { entityType: string; props: Record<string, unknown> }>();
89
+ for (const [name, entity] of entities) if (types.has(entity.entityType)) out.set(name, entity);
90
+ return out;
91
+ }
92
+
93
+ /**
94
+ * Run one lexicon's health read. Never throws: a thrown reader reports every
95
+ * applicable entity `read-failed`. The result is made total, so an entity the
96
+ * reader forgot comes back `unobserved` rather than missing.
97
+ */
98
+ export async function observeHealthFor(
99
+ plugin: HealthLexicon,
100
+ options: HealthReadOptions,
101
+ ): Promise<HealthReadResult> {
102
+ const names = [...options.entities.keys()];
103
+ if (names.length === 0) return { entities: {}, unobserved: {}, absent: [] };
104
+ if (!plugin.observeHealth) {
105
+ return {
106
+ entities: {},
107
+ unobserved: unobservedAll(names, "unsupported-kind", `${plugin.name} has no health read`, options.entities),
108
+ absent: [],
109
+ };
110
+ }
111
+ let result: HealthReadResult;
112
+ try {
113
+ result = await plugin.observeHealth(options);
114
+ } catch (err) {
115
+ const message = err instanceof Error ? err.message : String(err);
116
+ return { entities: {}, unobserved: unobservedAll(names, "read-failed", message, options.entities), absent: [] };
117
+ }
118
+ return totalHealth(names, result, options.entities);
119
+ }
120
+
121
+ /**
122
+ * Put every asked-about name in exactly one of the three places. A name in
123
+ * `entities` wins over the others, then `absent`, then `unobserved`; a name in
124
+ * none of them becomes `read-failed`.
125
+ */
126
+ export function totalHealth(
127
+ names: string[],
128
+ result: HealthReadResult,
129
+ types?: Map<string, { entityType: string }>,
130
+ ): HealthReadResult {
131
+ const entities: Record<string, HealthEntry> = {};
132
+ const unobserved: Record<string, UnobservedEntity> = {};
133
+ const absentSet = new Set(result.absent ?? []);
134
+ const absent: string[] = [];
135
+ for (const name of names) {
136
+ const entry = result.entities?.[name];
137
+ if (entry) entities[name] = entry;
138
+ else if (absentSet.has(name)) absent.push(name);
139
+ else if (result.unobserved?.[name]) unobserved[name] = result.unobserved[name];
140
+ else Object.assign(unobserved, unobservedAll([name], "read-failed", "the health read returned no answer for this entity", types));
141
+ }
142
+ return { entities, unobserved, absent };
143
+ }
144
+
145
+ /** Counts across a health read, for the summary line and `--json`. */
146
+ export interface HealthSummary {
147
+ healthy: number;
148
+ unhealthy: number;
149
+ unknown: number;
150
+ unobserved: number;
151
+ absent: number;
152
+ }
153
+
154
+ export function summarizeHealth(results: Iterable<HealthReadResult>): HealthSummary {
155
+ const summary: HealthSummary = { healthy: 0, unhealthy: 0, unknown: 0, unobserved: 0, absent: 0 };
156
+ for (const r of results) {
157
+ for (const entry of Object.values(r.entities)) summary[entry.state]++;
158
+ summary.unobserved += Object.keys(r.unobserved).length;
159
+ summary.absent += r.absent.length;
160
+ }
161
+ return summary;
162
+ }
163
+
164
+ /** A health entry, for a reader building its result. */
165
+ export function healthEntry(
166
+ type: string,
167
+ state: HealthState,
168
+ detail: string,
169
+ observedAt: string,
170
+ checkedAt?: string,
171
+ ): HealthEntry {
172
+ return { type, state, detail, observedAt, ...(checkedAt ? { checkedAt } : {}) };
173
+ }
174
+
175
+ /** A hole, for a reader building its result. */
176
+ export function healthHole(type: string, reason: UnobservedReason, detail: string): UnobservedEntity {
177
+ return { type, reason, detail };
178
+ }