@intentius/chant 0.124.27 → 0.124.28

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 (450) hide show
  1. package/README.md +1 -1
  2. package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
  3. package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
  4. package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
  5. package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  6. package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
  7. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  8. package/dist/__golden__/wave-loop/op-held-on.json +2 -0
  9. package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
  10. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  11. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  12. package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
  13. package/dist/__golden__/wave-loop/op-per-run.json +7 -0
  14. package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
  15. package/dist/__golden__/wave-loop/op-sealed.json +3 -0
  16. package/dist/__golden__/wave-loop/op-spend.json +3 -0
  17. package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
  18. package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
  19. package/dist/change-set-graph.d.ts +56 -0
  20. package/dist/change-set-graph.d.ts.map +1 -0
  21. package/dist/change-set-graph.js +128 -0
  22. package/dist/change-set.d.ts +52 -3
  23. package/dist/change-set.d.ts.map +1 -1
  24. package/dist/change-set.js +35 -4
  25. package/dist/chant-version.d.ts +9 -0
  26. package/dist/chant-version.d.ts.map +1 -0
  27. package/dist/chant-version.js +16 -0
  28. package/dist/cli/commands/build.d.ts +8 -0
  29. package/dist/cli/commands/build.d.ts.map +1 -1
  30. package/dist/cli/commands/build.js +9 -8
  31. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  32. package/dist/cli/commands/check-lexicon-docs.js +30 -2
  33. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  34. package/dist/cli/commands/check-lexicon.js +13 -2
  35. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
  36. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  37. package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
  38. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  39. package/dist/cli/commands/init-lexicon.js +17 -8
  40. package/dist/cli/handlers/approve.d.ts.map +1 -1
  41. package/dist/cli/handlers/approve.js +19 -4
  42. package/dist/cli/handlers/build.d.ts.map +1 -1
  43. package/dist/cli/handlers/build.js +28 -0
  44. package/dist/cli/handlers/change-set.d.ts +10 -0
  45. package/dist/cli/handlers/change-set.d.ts.map +1 -1
  46. package/dist/cli/handlers/change-set.js +51 -1
  47. package/dist/cli/handlers/fan-out.d.ts +1 -1
  48. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  49. package/dist/cli/handlers/fan-out.js +14 -3
  50. package/dist/cli/handlers/pr.d.ts +8 -2
  51. package/dist/cli/handlers/pr.d.ts.map +1 -1
  52. package/dist/cli/handlers/pr.js +148 -24
  53. package/dist/cli/handlers/promote.d.ts.map +1 -1
  54. package/dist/cli/handlers/promote.js +256 -21
  55. package/dist/cli/handlers/release.d.ts +34 -0
  56. package/dist/cli/handlers/release.d.ts.map +1 -0
  57. package/dist/cli/handlers/release.js +203 -0
  58. package/dist/cli/handlers/run.d.ts +28 -2
  59. package/dist/cli/handlers/run.d.ts.map +1 -1
  60. package/dist/cli/handlers/run.js +170 -13
  61. package/dist/cli/main.d.ts.map +1 -1
  62. package/dist/cli/main.js +88 -13
  63. package/dist/cli/mcp/workspace-tools.js +1 -1
  64. package/dist/cli/parse-args.d.ts.map +1 -1
  65. package/dist/cli/parse-args.js +49 -3
  66. package/dist/cli/registry.d.ts +29 -3
  67. package/dist/cli/registry.d.ts.map +1 -1
  68. package/dist/cli/run-events.d.ts +29 -0
  69. package/dist/cli/run-events.d.ts.map +1 -0
  70. package/dist/cli/run-events.js +47 -0
  71. package/dist/cli/version.d.ts +3 -5
  72. package/dist/cli/version.d.ts.map +1 -1
  73. package/dist/cli/version.js +3 -13
  74. package/dist/codegen/docs-in-site.d.ts +71 -0
  75. package/dist/codegen/docs-in-site.d.ts.map +1 -0
  76. package/dist/codegen/docs-in-site.js +177 -0
  77. package/dist/codegen/docs-types.d.ts +14 -1
  78. package/dist/codegen/docs-types.d.ts.map +1 -1
  79. package/dist/codegen/docs.d.ts +18 -1
  80. package/dist/codegen/docs.d.ts.map +1 -1
  81. package/dist/codegen/docs.js +40 -7
  82. package/dist/codegen/rehype-base-url.mjs +15 -9
  83. package/dist/components/auto-release.d.ts +4 -1
  84. package/dist/components/auto-release.d.ts.map +1 -1
  85. package/dist/components/auto-release.js +2 -0
  86. package/dist/components/cli-support.d.ts +6 -0
  87. package/dist/components/cli-support.d.ts.map +1 -1
  88. package/dist/components/cli-support.js +10 -9
  89. package/dist/components/driver.d.ts +42 -9
  90. package/dist/components/driver.d.ts.map +1 -1
  91. package/dist/components/driver.js +81 -54
  92. package/dist/components/fan-out-run.d.ts +7 -0
  93. package/dist/components/fan-out-run.d.ts.map +1 -1
  94. package/dist/components/fan-out-run.js +35 -26
  95. package/dist/components/pr-run.d.ts +11 -0
  96. package/dist/components/pr-run.d.ts.map +1 -1
  97. package/dist/components/pr-run.js +20 -1
  98. package/dist/components/promote-order.d.ts +38 -0
  99. package/dist/components/promote-order.d.ts.map +1 -0
  100. package/dist/components/promote-order.js +62 -0
  101. package/dist/components/promote.d.ts +15 -2
  102. package/dist/components/promote.d.ts.map +1 -1
  103. package/dist/components/promote.js +17 -0
  104. package/dist/components/release-compile.d.ts +133 -0
  105. package/dist/components/release-compile.d.ts.map +1 -0
  106. package/dist/components/release-compile.js +376 -0
  107. package/dist/components/release-gate.d.ts +125 -0
  108. package/dist/components/release-gate.d.ts.map +1 -0
  109. package/dist/components/release-gate.js +282 -0
  110. package/dist/components/release-promote.d.ts +96 -0
  111. package/dist/components/release-promote.d.ts.map +1 -0
  112. package/dist/components/release-promote.js +204 -0
  113. package/dist/components/run-progress.d.ts +7 -0
  114. package/dist/components/run-progress.d.ts.map +1 -1
  115. package/dist/components/wave-plan.d.ts +3 -1
  116. package/dist/components/wave-plan.d.ts.map +1 -1
  117. package/dist/components/wave-plan.js +1 -1
  118. package/dist/config.d.ts +7 -0
  119. package/dist/config.d.ts.map +1 -1
  120. package/dist/config.js +1 -0
  121. package/dist/index.d.ts +1 -0
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +2 -0
  124. package/dist/lifecycle/change-set-store.d.ts +67 -0
  125. package/dist/lifecycle/change-set-store.d.ts.map +1 -0
  126. package/dist/lifecycle/change-set-store.js +96 -0
  127. package/dist/lifecycle/commit-sha.d.ts +13 -0
  128. package/dist/lifecycle/commit-sha.d.ts.map +1 -0
  129. package/dist/lifecycle/commit-sha.js +21 -0
  130. package/dist/lifecycle/gate-change-set.d.ts +69 -0
  131. package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
  132. package/dist/lifecycle/gate-change-set.js +106 -0
  133. package/dist/lifecycle/gate-ledger.d.ts +49 -1
  134. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  135. package/dist/lifecycle/gate-ledger.js +18 -0
  136. package/dist/lifecycle/release-cut.d.ts +107 -0
  137. package/dist/lifecycle/release-cut.d.ts.map +1 -0
  138. package/dist/lifecycle/release-cut.js +273 -0
  139. package/dist/lifecycle/release-ledger.d.ts +54 -0
  140. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  141. package/dist/lifecycle/release-ledger.js +53 -20
  142. package/dist/lifecycle/release-store.d.ts +190 -0
  143. package/dist/lifecycle/release-store.d.ts.map +1 -0
  144. package/dist/lifecycle/release-store.js +318 -0
  145. package/dist/lifecycle/run-ledger.d.ts +4 -0
  146. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  147. package/dist/lifecycle/run-ledger.js +2 -0
  148. package/dist/op/activities/change-coverage.d.ts.map +1 -1
  149. package/dist/op/activities/change-coverage.js +4 -2
  150. package/dist/op/activities/source-release.d.ts.map +1 -1
  151. package/dist/op/activities/source-release.js +7 -3
  152. package/dist/op/activities/source-rollback.d.ts.map +1 -1
  153. package/dist/op/activities/source-rollback.js +7 -3
  154. package/dist/op/gate-name.d.ts +11 -2
  155. package/dist/op/gate-name.d.ts.map +1 -1
  156. package/dist/op/gate-name.js +12 -3
  157. package/dist/op/gate-run-locator.d.ts +22 -0
  158. package/dist/op/gate-run-locator.d.ts.map +1 -1
  159. package/dist/op/gate-run-locator.js +31 -0
  160. package/dist/op/gate.d.ts +27 -3
  161. package/dist/op/gate.d.ts.map +1 -1
  162. package/dist/op/gate.js +57 -3
  163. package/dist/op/local-executor.d.ts +36 -1
  164. package/dist/op/local-executor.d.ts.map +1 -1
  165. package/dist/op/local-executor.js +158 -52
  166. package/dist/op/op-waves-run.d.ts.map +1 -1
  167. package/dist/op/op-waves-run.js +4 -0
  168. package/dist/op/run-live.d.ts +2 -0
  169. package/dist/op/run-live.d.ts.map +1 -1
  170. package/dist/op/run-live.js +1 -1
  171. package/dist/op/runtime.d.ts +46 -0
  172. package/dist/op/runtime.d.ts.map +1 -1
  173. package/dist/op/runtimes/local.d.ts.map +1 -1
  174. package/dist/op/runtimes/local.js +8 -0
  175. package/dist/point-decision.d.ts +2 -0
  176. package/dist/point-decision.d.ts.map +1 -1
  177. package/dist/point-decision.js +9 -2
  178. package/dist/run-event-store.d.ts +77 -0
  179. package/dist/run-event-store.d.ts.map +1 -0
  180. package/dist/run-event-store.js +204 -0
  181. package/dist/run-event-stream.d.ts +132 -0
  182. package/dist/run-event-stream.d.ts.map +1 -0
  183. package/dist/run-event-stream.js +238 -0
  184. package/dist/run-event.d.ts +267 -0
  185. package/dist/run-event.d.ts.map +1 -0
  186. package/dist/run-event.js +207 -0
  187. package/dist/wave-runtime.mjs +204 -199
  188. package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
  189. package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
  190. package/dist/workspace/__fixtures__/run-event-check.js +59 -0
  191. package/dist/workspace/change-set-show.d.ts +56 -0
  192. package/dist/workspace/change-set-show.d.ts.map +1 -0
  193. package/dist/workspace/change-set-show.js +54 -0
  194. package/dist/workspace/change-set-show.schema.json +102 -0
  195. package/dist/workspace/change-set.schema.json +28 -1
  196. package/dist/workspace/changes-cli.d.ts.map +1 -1
  197. package/dist/workspace/changes-cli.js +10 -1
  198. package/dist/workspace/changes.d.ts +37 -0
  199. package/dist/workspace/changes.d.ts.map +1 -1
  200. package/dist/workspace/changes.js +49 -2
  201. package/dist/workspace/changes.schema.json +135 -0
  202. package/dist/workspace/ci-green-cli.d.ts +19 -4
  203. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  204. package/dist/workspace/ci-green-cli.js +75 -12
  205. package/dist/workspace/ci-green-forge.d.ts +20 -3
  206. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  207. package/dist/workspace/ci-green-forge.js +6 -4
  208. package/dist/workspace/ci-green-workflow.d.ts +11 -0
  209. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  210. package/dist/workspace/ci-green-workflow.js +6 -0
  211. package/dist/workspace/ci-green.d.ts +273 -13
  212. package/dist/workspace/ci-green.d.ts.map +1 -1
  213. package/dist/workspace/ci-green.js +449 -38
  214. package/dist/workspace/ci-last-green.schema.json +32 -2
  215. package/dist/workspace/ci-list.schema.json +525 -0
  216. package/dist/workspace/commit-links.d.ts +85 -0
  217. package/dist/workspace/commit-links.d.ts.map +1 -0
  218. package/dist/workspace/commit-links.js +163 -0
  219. package/dist/workspace/declaration.d.ts +19 -0
  220. package/dist/workspace/declaration.d.ts.map +1 -1
  221. package/dist/workspace/declaration.js +1 -1
  222. package/dist/workspace/declaration.schema.json +12 -0
  223. package/dist/workspace/identity-gate-rule.d.ts +37 -0
  224. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  225. package/dist/workspace/identity-gate-rule.js +72 -7
  226. package/dist/workspace/identity.d.ts +12 -0
  227. package/dist/workspace/identity.d.ts.map +1 -1
  228. package/dist/workspace/identity.js +13 -1
  229. package/dist/workspace/intent.d.ts +6 -1
  230. package/dist/workspace/intent.d.ts.map +1 -1
  231. package/dist/workspace/intent.js +51 -1
  232. package/dist/workspace/ls.d.ts +4 -0
  233. package/dist/workspace/ls.d.ts.map +1 -1
  234. package/dist/workspace/ls.js +2 -0
  235. package/dist/workspace/ls.schema.json +3 -1
  236. package/dist/workspace/member-pipeline.d.ts +6 -0
  237. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  238. package/dist/workspace/member-pipeline.js +6 -0
  239. package/dist/workspace/promotion-order.d.ts +41 -0
  240. package/dist/workspace/promotion-order.d.ts.map +1 -0
  241. package/dist/workspace/promotion-order.js +95 -0
  242. package/dist/workspace/reason-codes.d.ts +2 -0
  243. package/dist/workspace/reason-codes.d.ts.map +1 -1
  244. package/dist/workspace/reason-codes.js +4 -1
  245. package/dist/workspace/releases.d.ts +72 -0
  246. package/dist/workspace/releases.d.ts.map +1 -0
  247. package/dist/workspace/releases.js +94 -0
  248. package/dist/workspace/releases.schema.json +391 -0
  249. package/dist/workspace/run-event.schema.json +288 -0
  250. package/dist/workspace/status-gates.d.ts +26 -1
  251. package/dist/workspace/status-gates.d.ts.map +1 -1
  252. package/dist/workspace/status-gates.js +31 -2
  253. package/dist/workspace/status-stewards.d.ts +4 -0
  254. package/dist/workspace/status-stewards.d.ts.map +1 -1
  255. package/dist/workspace/status-stewards.js +2 -0
  256. package/dist/workspace/status.d.ts +39 -0
  257. package/dist/workspace/status.d.ts.map +1 -1
  258. package/dist/workspace/status.js +48 -1
  259. package/dist/workspace/status.schema.json +70 -1
  260. package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
  261. package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
  262. package/dist/workspace/trust/ci-tag-statement.js +123 -0
  263. package/dist/workspace/trust/op-run-statement.d.ts +91 -0
  264. package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
  265. package/dist/workspace/trust/op-run-statement.js +143 -0
  266. package/dist/workspace/trust/rotation.d.ts +18 -2
  267. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  268. package/dist/workspace/trust/rotation.js +65 -16
  269. package/dist/workspace/trust/seal.d.ts +11 -0
  270. package/dist/workspace/trust/seal.d.ts.map +1 -1
  271. package/dist/workspace/trust/seal.js +9 -0
  272. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  273. package/dist/workspace/trust/ssh-commit.js +10 -10
  274. package/package.json +1 -1
  275. package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
  276. package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
  277. package/src/__golden__/wave-loop/fan-out-only.json +6 -0
  278. package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  279. package/src/__golden__/wave-loop/op-cost-member.json +3 -0
  280. package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  281. package/src/__golden__/wave-loop/op-held-on.json +2 -0
  282. package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
  283. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  284. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  285. package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
  286. package/src/__golden__/wave-loop/op-per-run.json +7 -0
  287. package/src/__golden__/wave-loop/op-pr-review.json +7 -0
  288. package/src/__golden__/wave-loop/op-sealed.json +3 -0
  289. package/src/__golden__/wave-loop/op-spend.json +3 -0
  290. package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
  291. package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
  292. package/src/change-set-graph.test.ts +72 -0
  293. package/src/change-set-graph.ts +133 -0
  294. package/src/change-set.ts +76 -6
  295. package/src/chant-version.ts +16 -0
  296. package/src/cli/commands/build.ts +19 -8
  297. package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
  298. package/src/cli/commands/check-lexicon-docs.ts +39 -2
  299. package/src/cli/commands/check-lexicon.ts +14 -2
  300. package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
  301. package/src/cli/commands/init-lexicon.test.ts +25 -0
  302. package/src/cli/commands/init-lexicon.ts +18 -8
  303. package/src/cli/handlers/approve-release.test.ts +80 -0
  304. package/src/cli/handlers/approve.ts +18 -2
  305. package/src/cli/handlers/build.test.ts +52 -0
  306. package/src/cli/handlers/build.ts +29 -0
  307. package/src/cli/handlers/change-set.ts +49 -1
  308. package/src/cli/handlers/fan-out.test.ts +48 -0
  309. package/src/cli/handlers/fan-out.ts +15 -3
  310. package/src/cli/handlers/operator.test.ts +37 -0
  311. package/src/cli/handlers/pr.test.ts +194 -0
  312. package/src/cli/handlers/pr.ts +168 -34
  313. package/src/cli/handlers/promote.test.ts +389 -5
  314. package/src/cli/handlers/promote.ts +279 -20
  315. package/src/cli/handlers/release-implicit.e2e.test.ts +153 -0
  316. package/src/cli/handlers/release.e2e.test.ts +85 -0
  317. package/src/cli/handlers/release.ts +206 -0
  318. package/src/cli/handlers/run-auto-release.test.ts +71 -0
  319. package/src/cli/handlers/run-events-cmd.test.ts +92 -0
  320. package/src/cli/handlers/run.test.ts +30 -0
  321. package/src/cli/handlers/run.ts +176 -15
  322. package/src/cli/main.test.ts +7 -0
  323. package/src/cli/main.ts +88 -13
  324. package/src/cli/mcp/never-over-mcp.test.ts +2 -0
  325. package/src/cli/mcp/workspace-tools.ts +1 -1
  326. package/src/cli/parse-args.ts +37 -3
  327. package/src/cli/registry.ts +29 -3
  328. package/src/cli/run-events.ts +54 -0
  329. package/src/cli/version.ts +3 -13
  330. package/src/codegen/docs-in-site.test.ts +166 -0
  331. package/src/codegen/docs-in-site.ts +182 -0
  332. package/src/codegen/docs-site/AgentPrompt.astro +26 -1
  333. package/src/codegen/docs-types.ts +14 -1
  334. package/src/codegen/docs.ts +60 -7
  335. package/src/codegen/rehype-base-url.d.mts +3 -1
  336. package/src/codegen/rehype-base-url.mjs +15 -9
  337. package/src/codegen/rehype-base-url.test.ts +34 -0
  338. package/src/components/auto-release.test.ts +21 -0
  339. package/src/components/auto-release.ts +6 -1
  340. package/src/components/cli-support.test.ts +82 -0
  341. package/src/components/cli-support.ts +16 -9
  342. package/src/components/driver.ts +129 -56
  343. package/src/components/fan-out-run.ts +56 -35
  344. package/src/components/pr-run.ts +30 -1
  345. package/src/components/promote-order.test.ts +90 -0
  346. package/src/components/promote-order.ts +75 -0
  347. package/src/components/promote.test.ts +10 -1
  348. package/src/components/promote.ts +26 -1
  349. package/src/components/release-compile.test.ts +147 -0
  350. package/src/components/release-compile.ts +445 -0
  351. package/src/components/release-gate.test.ts +215 -0
  352. package/src/components/release-gate.ts +341 -0
  353. package/src/components/release-promote.test.ts +159 -0
  354. package/src/components/release-promote.ts +232 -0
  355. package/src/components/run-progress.ts +7 -0
  356. package/src/components/wave-plan.ts +1 -1
  357. package/src/config.ts +7 -1
  358. package/src/index.ts +2 -0
  359. package/src/lifecycle/change-set-store.test.ts +217 -0
  360. package/src/lifecycle/change-set-store.ts +112 -0
  361. package/src/lifecycle/commit-sha.ts +23 -0
  362. package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
  363. package/src/lifecycle/gate-change-set.ts +133 -0
  364. package/src/lifecycle/gate-ledger.test.ts +33 -0
  365. package/src/lifecycle/gate-ledger.ts +66 -1
  366. package/src/lifecycle/release-cut-signed.test.ts +180 -0
  367. package/src/lifecycle/release-cut.test.ts +411 -0
  368. package/src/lifecycle/release-cut.ts +367 -0
  369. package/src/lifecycle/release-ledger.test.ts +14 -0
  370. package/src/lifecycle/release-ledger.ts +85 -17
  371. package/src/lifecycle/release-store.ts +423 -0
  372. package/src/lifecycle/run-ledger.test.ts +10 -0
  373. package/src/lifecycle/run-ledger.ts +6 -0
  374. package/src/op/activities/change-coverage.ts +4 -1
  375. package/src/op/activities/source-release.ts +8 -3
  376. package/src/op/activities/source-rollback.ts +8 -4
  377. package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
  378. package/src/op/gate-name.ts +13 -3
  379. package/src/op/gate-release.test.ts +126 -0
  380. package/src/op/gate-run-locator.test.ts +110 -0
  381. package/src/op/gate-run-locator.ts +35 -0
  382. package/src/op/gate.ts +88 -6
  383. package/src/op/local-executor.ts +207 -52
  384. package/src/op/op-waves-run.ts +4 -0
  385. package/src/op/run-events.test.ts +166 -0
  386. package/src/op/run-live.ts +1 -1
  387. package/src/op/runtime.ts +39 -0
  388. package/src/op/runtimes/local.test.ts +16 -0
  389. package/src/op/runtimes/local.ts +9 -0
  390. package/src/point-decision.ts +12 -2
  391. package/src/run-event-store.test.ts +144 -0
  392. package/src/run-event-store.ts +213 -0
  393. package/src/run-event-stream.test.ts +175 -0
  394. package/src/run-event-stream.ts +306 -0
  395. package/src/run-event.test.ts +277 -0
  396. package/src/run-event.ts +451 -0
  397. package/src/wave-loop-golden.test.ts +9 -0
  398. package/src/workspace/__fixtures__/run-event-check.ts +58 -0
  399. package/src/workspace/ancestry-index.test.ts +93 -0
  400. package/src/workspace/change-set-show.schema.json +102 -0
  401. package/src/workspace/change-set-show.ts +72 -0
  402. package/src/workspace/change-set.schema.json +28 -1
  403. package/src/workspace/changes-cli.ts +10 -1
  404. package/src/workspace/changes.schema.json +135 -0
  405. package/src/workspace/changes.test.ts +1 -1
  406. package/src/workspace/changes.ts +84 -2
  407. package/src/workspace/ci-green-cli.ts +75 -13
  408. package/src/workspace/ci-green-forge.ts +24 -4
  409. package/src/workspace/ci-green-runs.test.ts +242 -0
  410. package/src/workspace/ci-green-signed.test.ts +302 -0
  411. package/src/workspace/ci-green-workflow.test.ts +16 -0
  412. package/src/workspace/ci-green-workflow.ts +12 -0
  413. package/src/workspace/ci-green.test.ts +131 -2
  414. package/src/workspace/ci-green.ts +624 -42
  415. package/src/workspace/ci-last-green.schema.json +32 -2
  416. package/src/workspace/ci-list.schema.json +525 -0
  417. package/src/workspace/commit-links.test.ts +279 -0
  418. package/src/workspace/commit-links.ts +183 -0
  419. package/src/workspace/declaration.schema.json +12 -0
  420. package/src/workspace/declaration.ts +22 -2
  421. package/src/workspace/identity-gate-rule.ts +91 -7
  422. package/src/workspace/identity.ts +14 -1
  423. package/src/workspace/intent.ts +48 -1
  424. package/src/workspace/ls.schema.json +3 -1
  425. package/src/workspace/ls.ts +6 -0
  426. package/src/workspace/member-pipeline.test.ts +17 -0
  427. package/src/workspace/member-pipeline.ts +9 -0
  428. package/src/workspace/promotion-order.ts +97 -0
  429. package/src/workspace/read-contract.e2e.test.ts +4 -1
  430. package/src/workspace/reason-codes.test.ts +2 -1
  431. package/src/workspace/reason-codes.ts +4 -1
  432. package/src/workspace/releases.schema.json +391 -0
  433. package/src/workspace/releases.test.ts +108 -0
  434. package/src/workspace/releases.ts +122 -0
  435. package/src/workspace/run-event.schema.json +288 -0
  436. package/src/workspace/status-contract.test.ts +52 -1
  437. package/src/workspace/status-gates.ts +56 -1
  438. package/src/workspace/status-stewards.ts +6 -0
  439. package/src/workspace/status.schema.json +70 -1
  440. package/src/workspace/status.ts +80 -1
  441. package/src/workspace/trust/ci-tag-statement.ts +151 -0
  442. package/src/workspace/trust/op-run-statement.ts +148 -0
  443. package/src/workspace/trust/rotation.ts +61 -14
  444. package/src/workspace/trust/seal.ts +10 -0
  445. package/src/workspace/trust/ssh-commit.ts +10 -9
  446. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
  447. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
  448. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
  449. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
  450. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Place a change set's entries on the graph (#4022).
3
+ *
4
+ * A reader such as behold draws the graph `chant graph --format ir` and
5
+ * `chant workspace graph` print, and wants each change of a change set on the
6
+ * node it changes. The planner's address is not that node's id: a terraform
7
+ * plan names an instance, `module.app.terraform_data.instance[0]`, where the
8
+ * graph has one node per block, `<root>/module.app/terraform_data.instance`
9
+ * (the terraform lexicon's entity key, `hcl/parse.ts`). So chant maps the
10
+ * address itself, against the node ids of the graph it read, and a reader
11
+ * never derives an id.
12
+ *
13
+ * {@link changeSetNodeCandidates} lists the ids an entry's address can have,
14
+ * by planner. {@link mapChangeSetToGraph} keeps the first that the graph
15
+ * holds and puts the member's name in front, as `chant workspace graph`
16
+ * does. An entry whose address the graph does not hold gets `node: null`:
17
+ * a delete of a block the source no longer declares, or a warden or sql
18
+ * change outside the graph.
19
+ *
20
+ * Like `./change-set.ts`, this imports nothing at run time, so it bundles
21
+ * into one file with the change-set code.
22
+ */
23
+
24
+ import type { ChangeSetDocument, ChangeSetEntry } from "./change-set";
25
+
26
+ /** The graph a document is mapped onto: the node ids of one member's own IR. */
27
+ export interface ChangeSetGraphNodes {
28
+ /** The workspace member whose graph it is, its path of names joined with `/` when nested, or null outside a workspace. */
29
+ member: string | null;
30
+ /** The commit the graph was read at, or null when unknown. */
31
+ at: string | null;
32
+ /** Every node id of the member's `chant graph --format ir`, without the member in front. */
33
+ ids: Iterable<string>;
34
+ }
35
+
36
+ /**
37
+ * Split a terraform address into its dot-separated parts, dropping every
38
+ * `[...]` index. A quoted key may hold `.` or `]`, so quotes are honoured.
39
+ */
40
+ function terraformParts(address: string): string[] {
41
+ const parts: string[] = [];
42
+ let current = "";
43
+ let depth = 0;
44
+ let quoted = false;
45
+ for (let i = 0; i < address.length; i++) {
46
+ const c = address[i];
47
+ if (depth > 0) {
48
+ if (quoted) {
49
+ if (c === "\\") i++;
50
+ else if (c === '"') quoted = false;
51
+ } else if (c === '"') quoted = true;
52
+ else if (c === "]") depth--;
53
+ continue;
54
+ }
55
+ if (c === "[") depth++;
56
+ else if (c === ".") {
57
+ parts.push(current);
58
+ current = "";
59
+ } else current += c;
60
+ }
61
+ parts.push(current);
62
+ return parts.filter((p) => p.length > 0);
63
+ }
64
+
65
+ /**
66
+ * The block key the terraform lexicon gives an address in its root:
67
+ * `module.<call>/` for each module call, then the block's own address with
68
+ * no index. `module.app.terraform_data.instance[0]` is
69
+ * `module.app/terraform_data.instance`.
70
+ */
71
+ export function terraformBlockKey(address: string): string | undefined {
72
+ const parts = terraformParts(address);
73
+ const scope: string[] = [];
74
+ let i = 0;
75
+ while (parts[i] === "module" && i + 1 < parts.length) {
76
+ scope.push(`module.${parts[i + 1]}`);
77
+ i += 2;
78
+ }
79
+ const rest = parts.slice(i);
80
+ let block: string | undefined;
81
+ if (rest[0] === "data" && rest.length >= 3) block = `data.${rest[1]}.${rest[2]}`;
82
+ else if (rest.length >= 2) block = `${rest[0]}.${rest[1]}`;
83
+ else if (rest.length === 0 && scope.length > 0) {
84
+ // The module call itself is a block of its caller.
85
+ const call = scope.pop()!;
86
+ block = call;
87
+ }
88
+ if (block === undefined) return undefined;
89
+ return [...scope, block].join("/");
90
+ }
91
+
92
+ /**
93
+ * The ids, in the order tried, that an entry's address can have in its
94
+ * member's graph IR:
95
+ *
96
+ * - `chant`: the address, which is the entity name the IR uses as its id.
97
+ * - `terraform`, `tofu`, `choudoufu`: `<root>/<block key>`, the root being
98
+ * the change-set member (`terraformBlockKey`), then the address.
99
+ * - `warden`, `sql`: the address.
100
+ */
101
+ export function changeSetNodeCandidates(entry: Pick<ChangeSetEntry, "member" | "planner" | "address">): string[] {
102
+ switch (entry.planner) {
103
+ case "terraform":
104
+ case "tofu":
105
+ case "choudoufu": {
106
+ const key = terraformBlockKey(entry.address);
107
+ return key === undefined ? [entry.address] : [`${entry.member}/${key}`, entry.address];
108
+ }
109
+ default:
110
+ return [entry.address];
111
+ }
112
+ }
113
+
114
+ /**
115
+ * The document with each entry's `node` set from `graph`, and `graph`
116
+ * recording which graph it was. A null `graph` (none could be read) gives
117
+ * every entry `node: null`. The digest is unchanged: `node` is not hashed.
118
+ */
119
+ export function mapChangeSetToGraph(doc: ChangeSetDocument, graph: ChangeSetGraphNodes | null): ChangeSetDocument {
120
+ if (graph === null) {
121
+ return { ...doc, entries: doc.entries.map((e) => ({ ...e, node: null })), graph: null };
122
+ }
123
+ const ids = new Set(graph.ids);
124
+ const prefix = graph.member ? `${graph.member}/` : "";
125
+ return {
126
+ ...doc,
127
+ entries: doc.entries.map((e) => {
128
+ const id = changeSetNodeCandidates(e).find((c) => ids.has(c));
129
+ return { ...e, node: id === undefined ? null : `${prefix}${id}` };
130
+ }),
131
+ graph: { member: graph.member, at: graph.at },
132
+ };
133
+ }
package/src/change-set.ts CHANGED
@@ -113,6 +113,15 @@ export interface ChangeSetEntry {
113
113
  /** The estate, environment or org the change lands in. */
114
114
  scope?: string;
115
115
  attributes: ChangeSetAttribute[];
116
+ /**
117
+ * The graph node the entry changes, as `chant graph --format ir` and
118
+ * `chant workspace graph` name it (#4022): `<member>/<id>` inside a
119
+ * workspace. Null when the graph holds no node for the address, such as
120
+ * a delete of a block the source no longer declares. Absent until the
121
+ * document is mapped onto a graph (`./change-set-graph.ts`), and never
122
+ * part of the digest.
123
+ */
124
+ node?: string | null;
116
125
  }
117
126
 
118
127
  /** Something a planner could not read, so the plan says nothing about it. */
@@ -201,6 +210,30 @@ export interface ChangeSetDocument {
201
210
  /** Triggered actions, sorted by member and address. Absent when no member runs one. */
202
211
  sideEffects?: ChangeSetSideEffect[];
203
212
  summary: ChangeSetSummary;
213
+ /**
214
+ * The graph the entries' `node` ids come from (#4022): the workspace member
215
+ * whose graph it is, or null outside a workspace, and the commit it was
216
+ * read at, or null when that is unknown. Null when no graph could be read,
217
+ * so every `node` is null. Absent on a document never mapped. Not hashed.
218
+ */
219
+ graph?: ChangeSetGraph | null;
220
+ /**
221
+ * The release this change set was planned for (#4045): the `releaseDigest`
222
+ * of the release a `components promote --release` deploys. Set with
223
+ * `environment` on a release gate's document, absent on every other. Part
224
+ * of the digest when present, so a stored document names the one release
225
+ * and environment its gate bound, and two releases whose plans read the
226
+ * same never share a document or an approval.
227
+ */
228
+ releaseDigest?: string;
229
+ /** The environment this change set was planned for (#4045), set with `releaseDigest`. Part of the digest when present. */
230
+ environment?: string;
231
+ }
232
+
233
+ /** Where a document's node ids come from. */
234
+ export interface ChangeSetGraph {
235
+ member: string | null;
236
+ at: string | null;
204
237
  }
205
238
 
206
239
  const byString = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
@@ -235,22 +268,39 @@ export function changeSetDigest(members: ReadonlyArray<Pick<ChangeSetMember, "me
235
268
  /**
236
269
  * The document's digest: `computePlanDigest("change-set-document", …)` over
237
270
  * the set digest ({@link changeSetDigest}), each member's name, status and
238
- * holes, the entries and the side effects. Members are sorted by name and
271
+ * holes, the entries (without their graph `node`) and the side effects.
272
+ * Members are sorted by name and
239
273
  * entries and side effects by the keys {@link composeChangeSet} sorts them by,
240
274
  * so order makes no difference.
241
275
  *
242
276
  * A provisional member is left out, with its entries and side effects, as
243
277
  * the set digest leaves it out: no approval of the document covers it.
278
+ *
279
+ * A release gate's document (#4045) also hashes the `releaseDigest` and
280
+ * `environment` it was planned for. A document without them hashes as it
281
+ * always has, so no earlier digest moves.
244
282
  */
245
- export function changeSetDocumentDigest(doc: Pick<ChangeSetDocument, "members" | "entries" | "sideEffects">): string {
283
+ export function changeSetDocumentDigest(
284
+ doc: Pick<ChangeSetDocument, "members" | "entries" | "sideEffects" | "releaseDigest" | "environment">,
285
+ ): string {
246
286
  const provisional = new Set(doc.members.filter((m) => m.provisional === true).map((m) => m.member));
247
287
  const members = doc.members
248
288
  .filter((m) => !provisional.has(m.member))
249
289
  .map((m) => ({ member: m.member, status: m.status, holes: m.holes }))
250
290
  .sort((a, b) => byString(a.member, b.member));
251
- const entries = doc.entries.filter((e) => !provisional.has(e.member)).sort((a, b) => byString(entryKey(a), entryKey(b)));
291
+ // An entry's graph node (#4022) says where on the graph the change is,
292
+ // not what the plan does, so it stays outside the digest: mapping a stored
293
+ // document onto a graph never moves the value its gate bound.
294
+ const entries = doc.entries
295
+ .filter((e) => !provisional.has(e.member))
296
+ .map(({ node: _node, ...e }) => e)
297
+ .sort((a, b) => byString(entryKey(a), entryKey(b)));
252
298
  const sideEffects = (doc.sideEffects ?? []).filter((s) => !provisional.has(s.member)).sort(bySideEffect);
253
- return computePlanDigest("change-set-document", { set: changeSetDigest(doc.members), members, entries, sideEffects });
299
+ const planned =
300
+ doc.releaseDigest !== undefined || doc.environment !== undefined
301
+ ? { planned: { releaseDigest: doc.releaseDigest ?? null, environment: doc.environment ?? null } }
302
+ : {};
303
+ return computePlanDigest("change-set-document", { set: changeSetDigest(doc.members), members, entries, sideEffects, ...planned });
254
304
  }
255
305
 
256
306
  /** The summary of a set of members and entries. */
@@ -296,6 +346,8 @@ function sortKeys<T>(o: Record<string, T>): Record<string, T> {
296
346
  export interface ComposeChangeSetOptions {
297
347
  /** The chant version to record in `chant`. */
298
348
  chant?: string;
349
+ /** The release and environment a release gate's change set was planned for (#4045), recorded and hashed. */
350
+ planned?: { releaseDigest: string; environment: string };
299
351
  }
300
352
 
301
353
  /**
@@ -317,12 +369,14 @@ export function composeChangeSet(parts: ReadonlyArray<ChangeSetPart>, options: C
317
369
  const sideEffects: ChangeSetSideEffect[] = parts
318
370
  .flatMap((p) => (p.sideEffects ?? []).map((s) => ({ member: p.member.member, ...s })))
319
371
  .sort(bySideEffect);
320
- const digest = changeSetDocumentDigest({ members, entries, sideEffects });
372
+ const planned = options.planned ? { releaseDigest: options.planned.releaseDigest, environment: options.planned.environment } : {};
373
+ const digest = changeSetDocumentDigest({ members, entries, sideEffects, ...planned });
321
374
  return {
322
375
  $schema: CHANGE_SET_SCHEMA_ID,
323
376
  contract: CHANGE_SET_CONTRACT,
324
377
  ...(options.chant !== undefined ? { chant: options.chant } : {}),
325
378
  digest,
379
+ ...planned,
326
380
  members,
327
381
  entries,
328
382
  ...(sideEffects.length > 0 ? { sideEffects } : {}),
@@ -335,7 +389,9 @@ export function composeChangeSet(parts: ReadonlyArray<ChangeSetPart>, options: C
335
389
  * effects give ({@link changeSetDocumentDigest}). A document that names a
336
390
  * member twice has no digest and is not verified.
337
391
  */
338
- export function verifyChangeSetDigest(doc: Pick<ChangeSetDocument, "digest" | "members" | "entries" | "sideEffects">): boolean {
392
+ export function verifyChangeSetDigest(
393
+ doc: Pick<ChangeSetDocument, "digest" | "members" | "entries" | "sideEffects" | "releaseDigest" | "environment">,
394
+ ): boolean {
339
395
  try {
340
396
  return changeSetDocumentDigest(doc) === doc.digest;
341
397
  } catch {
@@ -343,6 +399,20 @@ export function verifyChangeSetDigest(doc: Pick<ChangeSetDocument, "digest" | "m
343
399
  }
344
400
  }
345
401
 
402
+ /**
403
+ * Whether a change set changes nothing (ws-104 question 6, #4045): it has at
404
+ * least one member, every member planned, none failed or provisional, no
405
+ * member has a hole, and every entry is a `no-op` or a `read`. A release
406
+ * gate over such a document passes without an approval.
407
+ */
408
+ export function isEmptyChangeSet(doc: Pick<ChangeSetDocument, "members" | "entries">): boolean {
409
+ if (doc.members.length === 0) return false;
410
+ for (const m of doc.members) {
411
+ if (m.status !== "planned" || m.provisional === true || m.holes.length > 0) return false;
412
+ }
413
+ return doc.entries.every((e) => e.action === "no-op" || e.action === "read");
414
+ }
415
+
346
416
  // ── chant lifecycle plan ───────────────────────────────────────────────────────
347
417
 
348
418
  export interface LifecyclePlanPartInput {
@@ -0,0 +1,16 @@
1
+ import { createRequire } from "node:module";
2
+
3
+ /**
4
+ * The installed chant's version, read from `@intentius/chant`'s own
5
+ * package.json, or "0.0.0" when it can't be read. The path is the same from
6
+ * `src/` and `dist/`, so it holds whether chant runs from source or from its
7
+ * build. Kept outside `cli/` so that a module the Op entry loads, such as the
8
+ * run-event writer (#4041), can name the version without loading the CLI.
9
+ */
10
+ export const CHANT_VERSION: string = (() => {
11
+ try {
12
+ return (createRequire(import.meta.url)("../package.json") as { version?: string }).version ?? "0.0.0";
13
+ } catch {
14
+ return "0.0.0";
15
+ }
16
+ })();
@@ -118,6 +118,20 @@ export interface BuildOptions {
118
118
  * outputs of its own `serializers`, so the sources are evaluated once.
119
119
  */
120
120
  sharedBuild?: { serializers: Serializer[]; result?: ReturnType<typeof build> };
121
+
122
+ /**
123
+ * Where each output file goes: called with its absolute path and its
124
+ * content in place of creating the directory and writing the file. A
125
+ * release cut (#4046) passes one that keeps the files in memory, so it
126
+ * records exactly what this build writes and nothing left on disk by an
127
+ * earlier one.
128
+ */
129
+ writeFile?: (path: string, content: string) => void;
130
+ }
131
+
132
+ function writeFileCreatingDir(path: string, content: string): void {
133
+ mkdirSync(dirname(path), { recursive: true });
134
+ writeFileSync(path, content);
121
135
  }
122
136
 
123
137
  /**
@@ -735,6 +749,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
735
749
  }
736
750
 
737
751
  // Handle output
752
+ const writeOutput = options.writeFile ?? writeFileCreatingDir;
738
753
  if (result.errors.length === 0 && errors.length === 0) {
739
754
  // Extract primary content and collect additional files from SerializerResult.
740
755
  // Each entry remembers which lexicon produced it, so a same-basename
@@ -866,8 +881,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
866
881
  additionalFiles.delete(filename);
867
882
  try {
868
883
  const targetPath = join(projectDist, filename);
869
- mkdirSync(dirname(targetPath), { recursive: true });
870
- writeFileSync(targetPath, entry.content);
884
+ writeOutput(targetPath, entry.content);
871
885
  opsWritten += 1;
872
886
  } catch (err) {
873
887
  errors.push(
@@ -893,8 +907,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
893
907
  if (!config?.name) continue;
894
908
  try {
895
909
  const targetPath = join(projectDist, "ops", config.name, "op.json");
896
- mkdirSync(dirname(targetPath), { recursive: true });
897
- writeFileSync(targetPath, serializeOpIR(config));
910
+ writeOutput(targetPath, serializeOpIR(config));
898
911
  irWritten += 1;
899
912
  } catch (err) {
900
913
  errors.push(
@@ -913,8 +926,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
913
926
  // output path and any nested additional-file paths (e.g. ops/<name>/...).
914
927
  try {
915
928
  const outputPath = resolve(options.output);
916
- mkdirSync(dirname(outputPath), { recursive: true });
917
- writeFileSync(outputPath, output);
929
+ writeOutput(outputPath, output);
918
930
 
919
931
  // Write additional files (e.g. nested stack templates) alongside the primary output
920
932
  if (additionalFiles.size > 0) {
@@ -935,8 +947,7 @@ export async function buildCommand(options: BuildOptions): Promise<BuildResult>
935
947
  }
936
948
  }
937
949
  const targetPath = join(outputDir, filename);
938
- mkdirSync(dirname(targetPath), { recursive: true });
939
- writeFileSync(targetPath, fileContent);
950
+ writeOutput(targetPath, fileContent);
940
951
  }
941
952
  }
942
953
  } catch (err) {
@@ -88,3 +88,54 @@ describe("auditDocsReachability", () => {
88
88
  expect(auditDocsReachability(d).unreachable).toEqual(["a", "c"]);
89
89
  });
90
90
  });
91
+
92
+ /**
93
+ * chant #3993: inside the chant repository a lexicon's pages are generated
94
+ * into the main docs site with a _sidebar.json beside them, and the audit
95
+ * reads those instead of a docs app the lexicon no longer has.
96
+ */
97
+ describe("auditDocsReachability for a section of the main site", () => {
98
+ function mainSite(): void {
99
+ mkdirSync(join(dir, "docs"), { recursive: true });
100
+ writeFileSync(join(dir, "docs", "package.json"), JSON.stringify({ name: "@intentius/chant-docs" }));
101
+ writeFileSync(join(dir, "docs", "astro.config.mjs"), "export default {};\n");
102
+ }
103
+
104
+ function repoWithSection(pages: string[], group: unknown, hidden: string[] = []): string {
105
+ mainSite();
106
+ const section = join(dir, "docs", "src", "content", "docs", "lexicons", "fixture");
107
+ mkdirSync(section, { recursive: true });
108
+ for (const page of pages) writeFileSync(join(section, `${page}.mdx`), "body\n");
109
+ writeFileSync(join(section, "_sidebar.json"), JSON.stringify({ group, hidden }));
110
+ const lex = join(dir, "lexicons", "fixture");
111
+ mkdirSync(join(lex, "docs", "pages"), { recursive: true });
112
+ return lex;
113
+ }
114
+
115
+ test("every page in the fragment, or hidden by it, is reachable", () => {
116
+ const lex = repoWithSection(
117
+ ["index", "start", "secret"],
118
+ {
119
+ label: "Fixture",
120
+ items: [
121
+ { label: "Overview", slug: "lexicons/fixture" },
122
+ { label: "Tutorials", items: [{ label: "Start", slug: "lexicons/fixture/start" }] },
123
+ ],
124
+ },
125
+ ["lexicons/fixture/secret"],
126
+ );
127
+ expect(auditDocsReachability(lex)).toEqual({ hasSite: true, pages: ["index", "secret", "start"], unreachable: [] });
128
+ });
129
+
130
+ test("a page the fragment does not name is unreachable", () => {
131
+ const lex = repoWithSection(["index", "orphan"], { label: "Fixture", items: [{ label: "Overview", slug: "lexicons/fixture" }] });
132
+ expect(auditDocsReachability(lex).unreachable).toEqual(["orphan"]);
133
+ });
134
+
135
+ test("before the section is generated there is nothing to audit", () => {
136
+ mainSite();
137
+ const lex = join(dir, "lexicons", "fixture");
138
+ mkdirSync(lex, { recursive: true });
139
+ expect(auditDocsReachability(lex).hasSite).toBe(false);
140
+ });
141
+ });
@@ -15,7 +15,10 @@
15
15
  */
16
16
 
17
17
  import { existsSync, readdirSync, readFileSync } from "fs";
18
- import { join } from "path";
18
+ import { basename, join, resolve } from "path";
19
+
20
+ import { collectSidebarSlugs } from "../../codegen/docs";
21
+ import { findChantDocsSite, sectionContentDir } from "../../codegen/docs-in-site";
19
22
 
20
23
  export interface DocsReachability {
21
24
  /** Whether this lexicon ships a Starlight site at all. */
@@ -55,7 +58,7 @@ export function auditDocsReachability(lexiconDir: string): DocsReachability {
55
58
  const configPath = join(lexiconDir, "docs", "astro.config.mjs");
56
59
 
57
60
  if (!existsSync(contentDir) || !existsSync(configPath)) {
58
- return { hasSite: false, pages: [], unreachable: [] };
61
+ return auditSectionReachability(lexiconDir);
59
62
  }
60
63
 
61
64
  const slugs = sidebarSlugs(readFileSync(configPath, "utf-8"));
@@ -99,3 +102,37 @@ export function auditDocsClassification(lexiconDir: string): DocsClassification
99
102
  }
100
103
  return { hasPages: true, unclassified };
101
104
  }
105
+
106
+ /**
107
+ * Reachability for a lexicon whose docs are a section of the chant main site
108
+ * (chant #3993): its generated pages under
109
+ * `docs/src/content/docs/lexicons/<name>/` against the `_sidebar.json` the
110
+ * pipeline wrote beside them. A page the fragment lists as hidden is
111
+ * deliberately URL-only and does not count. Before the section has been
112
+ * generated there is nothing to audit, which reads as no site, like a
113
+ * lexicon with no docs.
114
+ */
115
+ function auditSectionReachability(lexiconDir: string): DocsReachability {
116
+ const site = findChantDocsSite(lexiconDir);
117
+ const name = basename(resolve(lexiconDir));
118
+ const dir = site ? sectionContentDir(site, name) : undefined;
119
+ const fragmentPath = dir ? join(dir, "_sidebar.json") : undefined;
120
+ if (!dir || !fragmentPath || !existsSync(fragmentPath)) return { hasSite: false, pages: [], unreachable: [] };
121
+
122
+ const fragment = JSON.parse(readFileSync(fragmentPath, "utf-8")) as {
123
+ group: Record<string, unknown>;
124
+ hidden?: string[];
125
+ };
126
+ const prefix = `lexicons/${name}/`;
127
+ const reached = new Set(
128
+ [...collectSidebarSlugs([fragment.group]), ...(fragment.hidden ?? [])].map((slug) =>
129
+ slug.startsWith(prefix) ? slug.slice(prefix.length) : slug,
130
+ ),
131
+ );
132
+ const pages = readdirSync(dir)
133
+ .filter((f) => f.endsWith(".mdx") || f.endsWith(".md"))
134
+ .map((f) => f.replace(/\.mdx?$/, ""))
135
+ .sort();
136
+ const unreachable = pages.filter((slug) => slug !== "index" && !reached.has(slug));
137
+ return { hasSite: true, pages, unreachable };
138
+ }
@@ -457,12 +457,24 @@ export async function checkLexicon(dir: string): Promise<CheckResult> {
457
457
  pass: hasSerializerTest,
458
458
  });
459
459
 
460
+ // A lexicon whose src/codegen/docs.ts runs docsPipeline always has a page:
461
+ // the pipeline writes the overview (index.mdx) from the manifest. Inside the
462
+ // chant repository that page is generated into the main docs site rather
463
+ // than committed under docs/ (chant #3993), so k3d, whose docs are all
464
+ // generated, has no .mdx of its own to count.
460
465
  const mdxFiles = findFiles(join(dir, "docs"), (n) => n.endsWith(".mdx"));
466
+ const docsTs = join(dir, "src", "codegen", "docs.ts");
467
+ const pipelineOverview = existsSync(docsTs) && readFileSync(docsTs, "utf-8").includes("docsPipeline(");
461
468
  items.push({
462
469
  name: "At least 1 .mdx doc page",
463
470
  tier: 1,
464
- pass: mdxFiles.length > 0,
465
- detail: mdxFiles.length > 0 ? `${mdxFiles.length} page(s)` : undefined,
471
+ pass: mdxFiles.length > 0 || pipelineOverview,
472
+ detail:
473
+ mdxFiles.length > 0
474
+ ? `${mdxFiles.length} page(s)`
475
+ : pipelineOverview
476
+ ? "the docs pipeline's generated overview"
477
+ : undefined,
466
478
  });
467
479
 
468
480
  // Counting pages says nothing about whether a reader can find them.
@@ -1,7 +1,12 @@
1
1
  /**
2
- * Docs site template generators for init-lexicon scaffold.
2
+ * Docs site template generators for init-lexicon scaffold. The app files
3
+ * (package.json, tsconfig, Astro config, content config) are written only for a
4
+ * lexicon outside the chant repository; inside it, the lexicon's docs are a
5
+ * section of the main site (chant #3993).
3
6
  */
4
7
 
8
+ import { DOCS_SITE_DEPENDENCIES } from "../../../../codegen/docs-in-site";
9
+
5
10
  export function generateDocsPackageJson(name: string): string {
6
11
  return JSON.stringify(
7
12
  {
@@ -14,11 +19,7 @@ export function generateDocsPackageJson(name: string): string {
14
19
  build: "astro build",
15
20
  preview: "astro preview",
16
21
  },
17
- dependencies: {
18
- "@astrojs/starlight": "^0.37.6",
19
- astro: "^5.6.1",
20
- sharp: "^0.34.2",
21
- },
22
+ dependencies: { ...DOCS_SITE_DEPENDENCIES },
22
23
  },
23
24
  null,
24
25
  2,
@@ -78,6 +78,31 @@ describe("initLexiconCommand", () => {
78
78
  }
79
79
  });
80
80
 
81
+ // chant #3993: inside the chant repository the lexicon's docs are a section
82
+ // of the main site, so only docs/pages/ is scaffolded.
83
+ test("inside the chant repository it scaffolds docs/pages/ and no docs app", async () => {
84
+ mkdirSync(join(tmpDir, "docs"), { recursive: true });
85
+ writeFileSync(join(tmpDir, "docs", "package.json"), JSON.stringify({ name: "@intentius/chant-docs" }));
86
+ writeFileSync(join(tmpDir, "docs", "astro.config.mjs"), "export default {};\n");
87
+ const targetDir = join(tmpDir, "lexicons", "in-repo");
88
+ const result = await initLexiconCommand({ name: "in-repo", path: targetDir });
89
+
90
+ expect(result.success).toBe(true);
91
+ expect(existsSync(join(targetDir, "docs/pages/getting-started.mdx"))).toBe(true);
92
+ for (const file of ["docs/package.json", "docs/tsconfig.json", "docs/astro.config.mjs", "docs/src"]) {
93
+ expect(existsSync(join(targetDir, file)), file).toBe(false);
94
+ }
95
+ });
96
+
97
+ test("a standalone docs app pins the main site's Starlight and Astro", async () => {
98
+ const targetDir = join(tmpDir, "pins");
99
+ await initLexiconCommand({ name: "pins", path: targetDir });
100
+ const docsPkg = JSON.parse(readFileSync(join(targetDir, "docs/package.json"), "utf-8"));
101
+ const mainPkg = JSON.parse(readFileSync(join(__dirname, "..", "..", "..", "..", "..", "docs", "package.json"), "utf-8"));
102
+ expect(docsPkg.dependencies["@astrojs/starlight"]).toBe(mainPkg.dependencies["@astrojs/starlight"]);
103
+ expect(docsPkg.dependencies.astro).toBe(mainPkg.dependencies.astro);
104
+ });
105
+
81
106
  test("plugin.ts contains all 5 required lifecycle methods", async () => {
82
107
  const targetDir = join(tmpDir, "lifecycle");
83
108
  await initLexiconCommand({ name: "k8s", path: targetDir });
@@ -12,6 +12,7 @@ import { generatePackageJson, generateTsConfig, generateTsConfigBuild, generateJ
12
12
  import { generatePluginTestTs, generateSerializerTestTs, generateCompletionsTestTs, generateHoverTestTs } from "./init-lexicon/templates/tests";
13
13
  import { generateDocsPackageJson, generateDocsTsConfig, generateDocsAstroConfig, generateDocsContentConfig, generateDocsIndexMdx, generateDocsGettingStartedMdx } from "./init-lexicon/templates/docs";
14
14
  import { generateExamplePackageJson, generateExampleInfraTs } from "./init-lexicon/templates/examples";
15
+ import { findChantDocsSite } from "../../codegen/docs-in-site";
15
16
 
16
17
  /**
17
18
  * Init-lexicon command options
@@ -101,6 +102,13 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
101
102
  }
102
103
  }
103
104
 
105
+ // Inside the chant repository a lexicon's docs are a section of the main
106
+ // site (chant #3993): the pipeline writes its pages into docs/, so the
107
+ // lexicon gets only docs/pages/. Anywhere else it gets a standalone
108
+ // Starlight app of its own, as before.
109
+ const standaloneDocs = findChantDocsSite(targetDir) === undefined;
110
+ const standaloneDocsDirs = ["docs/src", "docs/src/content", "docs/src/content/docs"];
111
+
104
112
  // Create directory structure
105
113
  const dirs = [
106
114
  "",
@@ -112,9 +120,7 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
112
120
  "src/lsp",
113
121
  "src/generated",
114
122
  "docs",
115
- "docs/src",
116
- "docs/src/content",
117
- "docs/src/content/docs",
123
+ ...(standaloneDocs ? standaloneDocsDirs : []),
118
124
  "docs/pages",
119
125
  "examples/getting-started",
120
126
  "examples/getting-started/src",
@@ -157,11 +163,15 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
157
163
  "justfile": generateJustfile(name),
158
164
  ".gitignore": generateGitignore(),
159
165
  "README.md": generateReadme(name, names),
160
- "docs/package.json": generateDocsPackageJson(name),
161
- "docs/tsconfig.json": generateDocsTsConfig(),
162
- "docs/astro.config.mjs": generateDocsAstroConfig(name),
163
- "docs/src/content.config.ts": generateDocsContentConfig(),
164
- "docs/src/content/docs/index.mdx": generateDocsIndexMdx(name),
166
+ ...(standaloneDocs
167
+ ? {
168
+ "docs/package.json": generateDocsPackageJson(name),
169
+ "docs/tsconfig.json": generateDocsTsConfig(),
170
+ "docs/astro.config.mjs": generateDocsAstroConfig(name),
171
+ "docs/src/content.config.ts": generateDocsContentConfig(),
172
+ "docs/src/content/docs/index.mdx": generateDocsIndexMdx(name),
173
+ }
174
+ : {}),
165
175
  "docs/pages/getting-started.mdx": generateDocsGettingStartedMdx(name),
166
176
  "examples/getting-started/package.json": generateExamplePackageJson(name),
167
177
  "examples/getting-started/src/infra.ts": generateExampleInfraTs(name, names),