@intentius/chant 0.91.0 → 0.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/graph.d.ts.map +1 -1
  4. package/dist/cli/handlers/operator.d.ts +6 -0
  5. package/dist/cli/handlers/operator.d.ts.map +1 -1
  6. package/dist/cli/handlers/run.d.ts.map +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/mcp/workspace-tools.d.ts +2 -1
  9. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  10. package/dist/cli/plugins.d.ts.map +1 -1
  11. package/dist/cli/registry.d.ts +12 -2
  12. package/dist/cli/registry.d.ts.map +1 -1
  13. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  14. package/dist/config.d.ts +22 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/lexicon-module.d.ts +22 -0
  17. package/dist/lexicon-module.d.ts.map +1 -1
  18. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  19. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  20. package/dist/lifecycle/run-ledger.d.ts +2 -0
  21. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  22. package/dist/lifecycle/symptoms.d.ts +51 -0
  23. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  24. package/dist/lint/rules/index.d.ts +4 -2
  25. package/dist/lint/rules/index.d.ts.map +1 -1
  26. package/dist/lint/rules/op/index.d.ts +14 -3
  27. package/dist/lint/rules/op/index.d.ts.map +1 -1
  28. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  29. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  31. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  33. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  35. package/dist/op/activities/activity-contracts.d.ts +201 -0
  36. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  37. package/dist/op/activities/apply.d.ts.map +1 -1
  38. package/dist/op/activities/change-coverage.d.ts +44 -0
  39. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  40. package/dist/op/activities/converge.d.ts +24 -1
  41. package/dist/op/activities/converge.d.ts.map +1 -1
  42. package/dist/op/activities/decide.d.ts +107 -0
  43. package/dist/op/activities/decide.d.ts.map +1 -0
  44. package/dist/op/activities/index.d.ts +10 -0
  45. package/dist/op/activities/index.d.ts.map +1 -1
  46. package/dist/op/activities/shell.d.ts +32 -0
  47. package/dist/op/activities/shell.d.ts.map +1 -1
  48. package/dist/op/activities/source-release.d.ts +86 -0
  49. package/dist/op/activities/source-release.d.ts.map +1 -0
  50. package/dist/op/activities/source-rollback.d.ts +100 -0
  51. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  52. package/dist/op/activities/work-evidence.d.ts +42 -0
  53. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  54. package/dist/op/activity-contract.d.ts +1 -0
  55. package/dist/op/activity-contract.d.ts.map +1 -1
  56. package/dist/op/activity-profiles.d.ts +16 -0
  57. package/dist/op/activity-profiles.d.ts.map +1 -1
  58. package/dist/op/builders.d.ts +84 -2
  59. package/dist/op/builders.d.ts.map +1 -1
  60. package/dist/op/composites/converge-op.d.ts +31 -4
  61. package/dist/op/composites/converge-op.d.ts.map +1 -1
  62. package/dist/op/decide-backend.d.ts +116 -0
  63. package/dist/op/decide-backend.d.ts.map +1 -0
  64. package/dist/op/decide-config.d.ts +120 -0
  65. package/dist/op/decide-config.d.ts.map +1 -0
  66. package/dist/op/decide-read-inputs.d.ts +40 -0
  67. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  68. package/dist/op/gate-wait.d.ts +43 -0
  69. package/dist/op/gate-wait.d.ts.map +1 -0
  70. package/dist/op/gate.d.ts +5 -2
  71. package/dist/op/gate.d.ts.map +1 -1
  72. package/dist/op/index.d.ts +11 -4
  73. package/dist/op/index.d.ts.map +1 -1
  74. package/dist/op/local-executor.d.ts +29 -4
  75. package/dist/op/local-executor.d.ts.map +1 -1
  76. package/dist/op/local-output.d.ts +2 -0
  77. package/dist/op/local-output.d.ts.map +1 -1
  78. package/dist/op/op-ir.d.ts +2 -0
  79. package/dist/op/op-ir.d.ts.map +1 -1
  80. package/dist/op/op-verb-class.d.ts.map +1 -1
  81. package/dist/op/operator.d.ts +97 -1
  82. package/dist/op/operator.d.ts.map +1 -1
  83. package/dist/op/runtime.d.ts +37 -5
  84. package/dist/op/runtime.d.ts.map +1 -1
  85. package/dist/op/runtimes/local.d.ts.map +1 -1
  86. package/dist/op/source-archive.d.ts +63 -0
  87. package/dist/op/source-archive.d.ts.map +1 -0
  88. package/dist/op/steward-points.d.ts +136 -0
  89. package/dist/op/steward-points.d.ts.map +1 -0
  90. package/dist/op/steward-turn.d.ts +53 -0
  91. package/dist/op/steward-turn.d.ts.map +1 -0
  92. package/dist/op/steward.d.ts +22 -1
  93. package/dist/op/steward.d.ts.map +1 -1
  94. package/dist/op/types.d.ts +7 -0
  95. package/dist/op/types.d.ts.map +1 -1
  96. package/dist/op/work-lease-run.d.ts +1 -1
  97. package/dist/op/work-lease-run.d.ts.map +1 -1
  98. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  100. package/dist/workspace/answers-ledger.d.ts +76 -0
  101. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  102. package/dist/workspace/changes-cli.d.ts +16 -0
  103. package/dist/workspace/changes-cli.d.ts.map +1 -0
  104. package/dist/workspace/changes.d.ts +200 -0
  105. package/dist/workspace/changes.d.ts.map +1 -0
  106. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  107. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  108. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  118. package/dist/workspace/chant-migrations.d.ts +99 -0
  119. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  120. package/dist/workspace/checks/boxes.d.ts +34 -1
  121. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  122. package/dist/workspace/checks/diagrams.d.ts +31 -0
  123. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  124. package/dist/workspace/checks/records.d.ts +7 -0
  125. package/dist/workspace/checks/records.d.ts.map +1 -1
  126. package/dist/workspace/checks.d.ts +3 -0
  127. package/dist/workspace/checks.d.ts.map +1 -1
  128. package/dist/workspace/compose-graph.d.ts +2 -0
  129. package/dist/workspace/compose-graph.d.ts.map +1 -1
  130. package/dist/workspace/conformance/index.d.ts.map +1 -1
  131. package/dist/workspace/decide.d.ts +49 -4
  132. package/dist/workspace/decide.d.ts.map +1 -1
  133. package/dist/workspace/declaration.d.ts +52 -0
  134. package/dist/workspace/declaration.d.ts.map +1 -1
  135. package/dist/workspace/declaration.schema.json +114 -1
  136. package/dist/workspace/declared-kinds.d.ts +20 -0
  137. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  138. package/dist/workspace/generated-files.d.ts +7 -0
  139. package/dist/workspace/generated-files.d.ts.map +1 -1
  140. package/dist/workspace/intent.d.ts.map +1 -1
  141. package/dist/workspace/lineage-check.d.ts +3 -0
  142. package/dist/workspace/lineage-check.d.ts.map +1 -1
  143. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  144. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  145. package/dist/workspace/ls.d.ts +39 -1
  146. package/dist/workspace/ls.d.ts.map +1 -1
  147. package/dist/workspace/point-answer.schema.json +95 -0
  148. package/dist/workspace/points-cli.d.ts.map +1 -1
  149. package/dist/workspace/points.d.ts +1 -1
  150. package/dist/workspace/reason-codes.d.ts +14 -2
  151. package/dist/workspace/reason-codes.d.ts.map +1 -1
  152. package/dist/workspace/records-cli.d.ts +11 -1
  153. package/dist/workspace/records-cli.d.ts.map +1 -1
  154. package/dist/workspace/records-close.d.ts +1 -1
  155. package/dist/workspace/records-close.d.ts.map +1 -1
  156. package/dist/workspace/records-write.d.ts +11 -5
  157. package/dist/workspace/records-write.d.ts.map +1 -1
  158. package/dist/workspace/records.d.ts +34 -4
  159. package/dist/workspace/records.d.ts.map +1 -1
  160. package/dist/workspace/status-gates.d.ts +7 -2
  161. package/dist/workspace/status-gates.d.ts.map +1 -1
  162. package/dist/workspace/status-stewards.d.ts +59 -1
  163. package/dist/workspace/status-stewards.d.ts.map +1 -1
  164. package/dist/workspace/status.d.ts +16 -0
  165. package/dist/workspace/status.d.ts.map +1 -1
  166. package/dist/workspace/work-cli.d.ts +113 -2
  167. package/dist/workspace/work-cli.d.ts.map +1 -1
  168. package/dist/workspace/work-evidence.d.ts +74 -0
  169. package/dist/workspace/work-evidence.d.ts.map +1 -0
  170. package/dist/workspace/work.d.ts +43 -1
  171. package/dist/workspace/work.d.ts.map +1 -1
  172. package/package.json +1 -1
  173. package/src/cli/commands/audit.test.ts +9 -1
  174. package/src/cli/commands/build.ts +3 -2
  175. package/src/cli/commands/lint.ts +7 -1
  176. package/src/cli/handlers/graph.test.ts +20 -0
  177. package/src/cli/handlers/graph.ts +16 -2
  178. package/src/cli/handlers/operator.ts +34 -2
  179. package/src/cli/handlers/run.test.ts +154 -2
  180. package/src/cli/handlers/run.ts +121 -2
  181. package/src/cli/main.test.ts +10 -0
  182. package/src/cli/main.ts +19 -0
  183. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  184. package/src/cli/mcp/workspace-tools.ts +62 -4
  185. package/src/cli/plugins.ts +11 -3
  186. package/src/cli/registry.ts +12 -2
  187. package/src/codegen/pinned-upgrade.ts +2 -1
  188. package/src/codegen/release-wiring.test.ts +49 -2
  189. package/src/components/capability-plugin-loader.ts +3 -3
  190. package/src/components/cli-support.ts +2 -2
  191. package/src/config.ts +11 -0
  192. package/src/lexicon-module.ts +56 -1
  193. package/src/lexicon-package.test.ts +96 -0
  194. package/src/lifecycle/converge-ledger.ts +20 -0
  195. package/src/lifecycle/run-ledger.ts +6 -0
  196. package/src/lifecycle/symptoms.ts +95 -0
  197. package/src/lint/rules/index.ts +6 -2
  198. package/src/lint/rules/op/index.ts +17 -4
  199. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  200. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  201. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  202. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  203. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  204. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  205. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  206. package/src/op/activities/activity-contracts.ts +190 -1
  207. package/src/op/activities/apply.ts +7 -6
  208. package/src/op/activities/change-coverage.ts +60 -0
  209. package/src/op/activities/converge.test.ts +55 -1
  210. package/src/op/activities/converge.ts +208 -29
  211. package/src/op/activities/decide.live.test.ts +78 -0
  212. package/src/op/activities/decide.test.ts +300 -0
  213. package/src/op/activities/decide.ts +267 -0
  214. package/src/op/activities/index.ts +21 -0
  215. package/src/op/activities/shell.test.ts +91 -1
  216. package/src/op/activities/shell.ts +91 -2
  217. package/src/op/activities/source-release.test.ts +118 -0
  218. package/src/op/activities/source-release.ts +168 -0
  219. package/src/op/activities/source-rollback.test.ts +131 -0
  220. package/src/op/activities/source-rollback.ts +239 -0
  221. package/src/op/activities/work-evidence.ts +62 -0
  222. package/src/op/activity-contract-registry.test.ts +2 -0
  223. package/src/op/activity-contract-registry.ts +2 -2
  224. package/src/op/activity-contract.test.ts +12 -0
  225. package/src/op/activity-contract.ts +6 -1
  226. package/src/op/activity-profiles.ts +29 -0
  227. package/src/op/activity-registry.test.ts +1 -1
  228. package/src/op/activity-registry.ts +2 -2
  229. package/src/op/builders.ts +119 -5
  230. package/src/op/command-gate.test.ts +192 -0
  231. package/src/op/composites/converge-op.test.ts +27 -1
  232. package/src/op/composites/converge-op.ts +69 -15
  233. package/src/op/decide-backend.test.ts +44 -0
  234. package/src/op/decide-backend.ts +212 -0
  235. package/src/op/decide-config.test.ts +52 -0
  236. package/src/op/decide-config.ts +80 -0
  237. package/src/op/decide-read-inputs.ts +117 -0
  238. package/src/op/gate-approval.ts +2 -2
  239. package/src/op/gate-wait.ts +57 -0
  240. package/src/op/gate.ts +8 -3
  241. package/src/op/generate-pipeline.ts +2 -2
  242. package/src/op/index.ts +18 -3
  243. package/src/op/local-executor.ts +151 -15
  244. package/src/op/local-output.test.ts +33 -0
  245. package/src/op/local-output.ts +20 -2
  246. package/src/op/op-ir.test.ts +10 -0
  247. package/src/op/op-ir.ts +4 -0
  248. package/src/op/op-verb-class.ts +1 -0
  249. package/src/op/operator.ts +290 -11
  250. package/src/op/runtime.ts +31 -8
  251. package/src/op/runtimes/local.ts +3 -1
  252. package/src/op/source-archive.ts +161 -0
  253. package/src/op/steward-points.test.ts +313 -0
  254. package/src/op/steward-points.ts +194 -0
  255. package/src/op/steward-turn.ts +79 -0
  256. package/src/op/steward.test.ts +87 -3
  257. package/src/op/steward.ts +25 -1
  258. package/src/op/types.ts +7 -0
  259. package/src/op/work-lease-run.test.ts +20 -1
  260. package/src/op/work-lease-run.ts +2 -2
  261. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  262. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  263. package/src/workspace/answers-ledger.ts +118 -0
  264. package/src/workspace/changes-cli.ts +56 -0
  265. package/src/workspace/changes.schema.json +472 -0
  266. package/src/workspace/changes.test.ts +315 -0
  267. package/src/workspace/changes.ts +512 -0
  268. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  269. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  270. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  271. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  272. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  273. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  274. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  275. package/src/workspace/chant-migrations.ts +164 -0
  276. package/src/workspace/check-contract.test.ts +4 -2
  277. package/src/workspace/check.schema.json +15 -5
  278. package/src/workspace/checks/boxes.test.ts +80 -1
  279. package/src/workspace/checks/boxes.ts +136 -1
  280. package/src/workspace/checks/diagrams.test.ts +134 -0
  281. package/src/workspace/checks/diagrams.ts +108 -0
  282. package/src/workspace/checks/records.ts +47 -0
  283. package/src/workspace/checks.ts +8 -1
  284. package/src/workspace/compose-graph.ts +2 -0
  285. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  286. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  287. package/src/workspace/conformance/index.ts +5 -0
  288. package/src/workspace/decide.test.ts +1 -1
  289. package/src/workspace/decide.ts +156 -23
  290. package/src/workspace/declaration.schema.json +114 -1
  291. package/src/workspace/declaration.test.ts +58 -0
  292. package/src/workspace/declaration.ts +104 -0
  293. package/src/workspace/declared-kinds.test.ts +5 -5
  294. package/src/workspace/declared-kinds.ts +46 -1
  295. package/src/workspace/generated-files.ts +14 -0
  296. package/src/workspace/graph-cli.ts +1 -1
  297. package/src/workspace/graph.schema.json +9 -1
  298. package/src/workspace/intent.schema.json +8 -2
  299. package/src/workspace/intent.ts +3 -10
  300. package/src/workspace/lineage-check.ts +16 -1
  301. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  302. package/src/workspace/lineage-upgrade.test.ts +70 -0
  303. package/src/workspace/lineage-upgrade.ts +81 -10
  304. package/src/workspace/ls-contract.test.ts +57 -0
  305. package/src/workspace/ls.schema.json +46 -1
  306. package/src/workspace/ls.ts +62 -3
  307. package/src/workspace/points-cli.ts +14 -3
  308. package/src/workspace/points-write.schema.json +11 -1
  309. package/src/workspace/points.schema.json +33 -0
  310. package/src/workspace/points.test.ts +1 -1
  311. package/src/workspace/points.ts +1 -1
  312. package/src/workspace/read-contract.test.ts +1 -1
  313. package/src/workspace/reason-codes.test.ts +11 -1
  314. package/src/workspace/reason-codes.ts +18 -2
  315. package/src/workspace/records-amend.schema.json +2 -0
  316. package/src/workspace/records-cli.ts +13 -3
  317. package/src/workspace/records-close.schema.json +2 -0
  318. package/src/workspace/records-new.schema.json +2 -0
  319. package/src/workspace/records-review.schema.json +2 -0
  320. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  321. package/src/workspace/records-sessions-write.test.ts +2 -34
  322. package/src/workspace/records-write.e2e.test.ts +108 -0
  323. package/src/workspace/records-write.test.ts +32 -41
  324. package/src/workspace/records-write.ts +33 -15
  325. package/src/workspace/records.schema.json +36 -2
  326. package/src/workspace/records.test.ts +48 -3
  327. package/src/workspace/records.ts +109 -3
  328. package/src/workspace/status-contract.test.ts +13 -8
  329. package/src/workspace/status-gates.ts +9 -4
  330. package/src/workspace/status-stewards.ts +108 -4
  331. package/src/workspace/status.schema.json +111 -7
  332. package/src/workspace/status.ts +36 -1
  333. package/src/workspace/work-acceptance.test.ts +274 -0
  334. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  335. package/src/workspace/work-cli.ts +193 -10
  336. package/src/workspace/work-evidence.schema.json +212 -0
  337. package/src/workspace/work-evidence.ts +169 -0
  338. package/src/workspace/work-history.schema.json +349 -0
  339. package/src/workspace/work-history.test.ts +156 -0
  340. package/src/workspace/work.test.ts +3 -2
  341. package/src/workspace/work.ts +100 -1
  342. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  343. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  344. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -16,6 +16,9 @@
16
16
  *
17
17
  * `chant workspace upgrade` runs the same checks in its staging worktree
18
18
  * before it reaches its gate.
19
+ *
20
+ * `--changes <base>..<head>` runs the forward coverage check instead
21
+ * (`changes-cli.ts`, #2773), which prints a document of its own.
19
22
  */
20
23
 
21
24
  import { realpathSync } from "node:fs";
@@ -84,7 +87,8 @@ export function findingKey(f: CheckFinding): string {
84
87
  return `${f.code}\0${f.scope ?? ""}\0${f.path ?? ""}`;
85
88
  }
86
89
 
87
- const USAGE = "chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]";
90
+ const USAGE =
91
+ "chant workspace check [--at <rev>] [--json] [--format stylish|json|sarif] [--generated] [--kind <kind file>]\n chant workspace check --changes <base>..<head> [--work <id>] [--severity off|warn|fail] [--kind <kind file>...] [--json]";
88
92
  const FORMATS = ["stylish", "json", "sarif"] as const;
89
93
 
90
94
  /** A lock finding as a lint diagnostic, for `--format json` and `--format sarif`. */
@@ -223,6 +227,17 @@ export async function runWorkspaceCheck(ctx: CommandContext): Promise<number> {
223
227
  console.error(formatError({ message: `chant workspace check takes no argument (got ${ctx.args.extraPositional})`, hint: USAGE }));
224
228
  return 1;
225
229
  }
230
+ // The forward coverage check (#2773) is its own document.
231
+ if (ctx.args.changes !== undefined) {
232
+ if (ctx.args.at !== undefined) {
233
+ console.error(formatError({ message: "--changes reads the records at the range's head, so it takes no --at", hint: USAGE }));
234
+ return 1;
235
+ }
236
+ const { handToRootChant } = await import("./which-chant");
237
+ const handed = await handToRootChant(root, undefined);
238
+ if (handed !== undefined) return handed;
239
+ return (await import("./changes-cli")).runWorkspaceChanges(ctx, root);
240
+ }
226
241
  const format = (ctx.args.format || "stylish") as (typeof FORMATS)[number];
227
242
  if (!FORMATS.includes(format)) {
228
243
  console.error(formatError({ message: `--format ${format} is not a check format; use stylish, json or sarif`, hint: USAGE }));
@@ -71,7 +71,7 @@ export async function upgradeCommand(opts: UpgradeCommandOptions): Promise<Upgra
71
71
  }),
72
72
  );
73
73
  }
74
- if (!staged.changed) {
74
+ if (!staged.changed && staged.checksOk) {
75
75
  if (!opts.json) console.error(formatSuccess(`scope "${staged.scope}" is already at ${staged.to ?? "its source"}; nothing to upgrade`));
76
76
  return report("up-to-date", 0);
77
77
  }
@@ -124,6 +124,76 @@ describe("chant workspace upgrade", () => {
124
124
  expect(git(proj, ["worktree", "list"]).split("\n")).toHaveLength(1);
125
125
  });
126
126
 
127
+ test("builds and lints the chant member chant.workspace.json declares, not the scope root (#2804)", async () => {
128
+ release("v2.0.0", {
129
+ "chant.workspace.json": JSON.stringify({ name: "proj", schema: 1, members: [{ name: "delivery", dir: "delivery", kind: "chant" }] }, null, 2) + "\n",
130
+ "delivery/chant.config.ts": "export default {};\n",
131
+ });
132
+ const calls: Array<{ command: string; cwd: string }> = [];
133
+ const recording: ChantRunner = async (command, cwd) => {
134
+ calls.push({ command, cwd });
135
+ return { exitCode: 0, output: "" };
136
+ };
137
+
138
+ const staged = await stageUpgrade({ root: proj, to: "v2.0.0", runChant: recording });
139
+ try {
140
+ // Never at the scope root: it has no chant.config once the project is a workspace.
141
+ expect(calls.every((c) => c.cwd === join(staged.worktreeProject, "delivery"))).toBe(true);
142
+ expect(calls.map((c) => c.command).sort()).toEqual(["build", "lint"]);
143
+ expect(staged.checks).toEqual(
144
+ expect.arrayContaining([
145
+ { name: "build", status: "passed", member: "delivery" },
146
+ { name: "lint", status: "passed", member: "delivery" },
147
+ ]),
148
+ );
149
+ } finally {
150
+ staged.dispose();
151
+ }
152
+ });
153
+
154
+ test("a member's own node_modules is linked into the staging worktree for its checks, and stays out of the patch (#2847)", async () => {
155
+ release("v2.0.0", {
156
+ "chant.workspace.json": JSON.stringify({ name: "proj", schema: 1, members: [{ name: "delivery", dir: "delivery", kind: "chant" }] }, null, 2) + "\n",
157
+ "delivery/chant.config.ts": "export default {};\n",
158
+ });
159
+ // The member installs its packages itself; git never sees them.
160
+ put(proj, "delivery/node_modules/member-only/package.json", JSON.stringify({ name: "member-only", version: "1.0.0" }) + "\n");
161
+ writeFileSync(join(proj, ".git", "info", "exclude"), "node_modules\n");
162
+ const seen: string[] = [];
163
+ const recording: ChantRunner = async (command, cwd) => {
164
+ if (existsSync(join(cwd, "node_modules", "member-only", "package.json"))) seen.push(command);
165
+ return { exitCode: 0, output: "" };
166
+ };
167
+
168
+ const staged = await stageUpgrade({ root: proj, to: "v2.0.0", runChant: recording });
169
+ try {
170
+ expect(seen.sort()).toEqual(["build", "lint"]);
171
+ expect(staged.changedPaths.some((p) => p.includes("node_modules"))).toBe(false);
172
+ expect(staged.patch).not.toContain("node_modules");
173
+ } finally {
174
+ staged.dispose();
175
+ }
176
+ // Disposing the worktree removes the link, never the member's own packages.
177
+ expect(existsSync(join(proj, "delivery", "node_modules", "member-only", "package.json"))).toBe(true);
178
+ });
179
+
180
+ test("a member's failing build fails the upgrade's checks before the gate (#2804)", async () => {
181
+ release("v2.0.0", {
182
+ "chant.workspace.json": JSON.stringify({ name: "proj", schema: 1, members: [{ name: "delivery", dir: "delivery", kind: "chant" }] }, null, 2) + "\n",
183
+ "delivery/chant.config.ts": "export default {};\n",
184
+ });
185
+ const failingBuild: ChantRunner = async (command) => (command === "build" ? { exitCode: 1, output: "boom\n" } : { exitCode: 0, output: "" });
186
+
187
+ const result = await upgradeCommand({ root: proj, to: "v2.0.0", runChant: failingBuild });
188
+ expect(result.outcome).toBe("checks-failed");
189
+ expect(result.exitCode).toBe(1);
190
+ expect(result.staged!.checks).toEqual(
191
+ expect.arrayContaining([{ name: "build", status: "failed", detail: "boom", member: "delivery" }]),
192
+ );
193
+ // The tree is unchanged: an unbuildable migration never reaches the gate.
194
+ expect(git(proj, ["status", "--porcelain"])).toBe("");
195
+ });
196
+
127
197
  test("gates on the patch digest, then applies exactly the approved patch", async () => {
128
198
  release("v2.0.0", { "README.md": "starter v2\n" });
129
199
  const port = ledger();
@@ -34,9 +34,9 @@
34
34
  */
35
35
 
36
36
  import { execFileSync, spawn } from "node:child_process";
37
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
37
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
38
38
  import { tmpdir } from "node:os";
39
- import { basename, join, relative, resolve, sep } from "node:path";
39
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
40
40
  import { computePlanDigest } from "../lifecycle/plan-digest";
41
41
  import type { ResolvedGateApproval } from "../op/gate-approval";
42
42
  import { checkLineage, findingKey, type CheckFinding } from "./lineage-check";
@@ -56,10 +56,13 @@ import {
56
56
  type ManualStep,
57
57
  } from "./lineage-lock";
58
58
  import { mergeFile } from "./lineage-merge";
59
+ import { reportPlan, runChantMigrations, type ChantMigrationReport } from "./chant-migrations";
60
+ import { readDeclaration, readerVersion, WorkspaceReadError } from "./declaration";
59
61
  import { assertCodeAllowed, planMigrations, runMigration, splitMigrations, type LoadedMigration } from "./lineage-migrations";
60
62
  import { applyUpstream, type UpdateResult } from "./lineage-update";
61
63
  import { carryParameters, readManifest, substituteParameters } from "./template-manifest";
62
64
  import { repinSubstituted, type RepinnedRecord } from "./template-pins";
65
+ import { workingTree } from "./tree";
63
66
 
64
67
  /** The kind the patch digest is taken under, so it never collides with another kind of plan. */
65
68
  export const UPGRADE_PLAN_KIND = "workspace-upgrade";
@@ -74,10 +77,12 @@ export class UpgradeError extends LockError {
74
77
  // ── Checks ───────────────────────────────────────────────────────────────────
75
78
 
76
79
  export interface UpgradeCheck {
77
- name: "build" | "lint" | "workspace check";
80
+ name: "build" | "lint" | "workspace check" | "migration";
78
81
  status: "passed" | "failed" | "skipped";
79
82
  /** Why a check was skipped, or the tail of a failed command's output. */
80
83
  detail?: string;
84
+ /** The chant member `build` or `lint` ran in ("." for the scope root), when it is not the only one. */
85
+ member?: string;
81
86
  }
82
87
 
83
88
  /** Runs `chant build` or `chant lint` in a directory. */
@@ -141,6 +146,8 @@ export interface StagedUpgrade {
141
146
  to: string | null;
142
147
  commit?: { from?: string; to: string };
143
148
  migrations: string[];
149
+ /** The migrations chant ships (./chant-migrations.ts) that found something to move, applied or not. */
150
+ chantMigrations: ChantMigrationReport[];
144
151
  written: string[];
145
152
  merged: string[];
146
153
  removed: string[];
@@ -460,6 +467,25 @@ function tail(text: string, lines = 30): string {
460
467
  return text.trimEnd().split("\n").slice(-lines).join("\n");
461
468
  }
462
469
 
470
+ /**
471
+ * The `chant` members `chant.workspace.json` declares in `dir` (#2804): a
472
+ * template built as a workspace, such as the studio kit's, keeps its chant
473
+ * project in a member (`delivery`), not at the scope's root, and the root
474
+ * itself never has a `chant.config`. Empty when `dir` declares no workspace,
475
+ * or the declaration cannot be read: the caller then falls back to `dir`
476
+ * itself.
477
+ */
478
+ function chantMemberDirs(dir: string): string[] {
479
+ try {
480
+ return readDeclaration(workingTree(dir))
481
+ .members.filter((m) => m.kind === "chant")
482
+ .map((m) => m.dir);
483
+ } catch (err) {
484
+ if (err instanceof WorkspaceReadError) return [];
485
+ throw err;
486
+ }
487
+ }
488
+
463
489
  /**
464
490
  * Stage an upgrade: steps 1 to 5 and the digest of step 6. Nothing in the
465
491
  * project's tree changes. The caller decides the gate, then applies the patch
@@ -568,6 +594,12 @@ export async function stageUpgrade(options: UpgradeOptions): Promise<StagedUpgra
568
594
  rmSync(scratch, { recursive: true, force: true });
569
595
  }
570
596
 
597
+ // chant's own migrations (#2737): planned from what the scope holds, and
598
+ // applied only when the plan has no conflict. A conflict fails the checks.
599
+ // Each is planned from the tree the ones before it left.
600
+ const chantRuns = lineage.kind === "template" ? runChantMigrations({ dir: scopeDir, lineage: staged, chantVersion: readerVersion() }) : [];
601
+ const chantMigrations: ChantMigrationReport[] = chantRuns.map((r) => reportPlan(r.plan, r.applied));
602
+
571
603
  // 4. Merge per file.
572
604
  const result = applyUpstream(scopeDir, staged, upstream.files, { base, merge: mergeFile });
573
605
  for (const path of result.written) if (upstream.executable.has(path)) chmodSync(join(scopeDir, path), 0o755);
@@ -607,12 +639,40 @@ export async function stageUpgrade(options: UpgradeOptions): Promise<StagedUpgra
607
639
 
608
640
  // 5. Checks.
609
641
  const checks: UpgradeCheck[] = [];
642
+ for (const m of chantMigrations.filter((m) => !m.applied)) {
643
+ checks.push({
644
+ name: "migration",
645
+ status: "failed",
646
+ detail: `${m.id} was not applied: ${m.conflicts.map((c) => `${c.path}: ${c.reason}`).join("\n")}`,
647
+ });
648
+ }
610
649
  if (changed) {
611
- if (isChantProject(worktreeProject)) {
650
+ // Every `chant` member chant.workspace.json declares, so a template
651
+ // whose chant project sits in a member (the studio kit's `delivery/`)
652
+ // is built and linted there, not skipped for having no chant.config at
653
+ // the scope's root (#2804). A scope with no workspace declaration falls
654
+ // back to the root, as before.
655
+ const members = chantMemberDirs(worktreeProject);
656
+ const targets = members.length > 0 ? members : isChantProject(worktreeProject) ? ["."] : [];
657
+ if (targets.length > 0) {
612
658
  const run = options.runChant ?? spawnChant;
613
- for (const command of ["build", "lint"] as const) {
614
- const r = await run(command, worktreeProject);
615
- checks.push(r.exitCode === 0 ? { name: command, status: "passed" } : { name: command, status: "failed", detail: tail(r.output) });
659
+ const namedMember = targets.length > 1 || targets[0] !== ".";
660
+ // A member that installs its packages in its own directory resolves them from its node_modules, which is not
661
+ // an ancestor of the staging worktree: link it in from the project (#2847). The patch and its digest are
662
+ // computed above, so the links never reach them, and dispose() removes them with the worktree.
663
+ for (const member of targets) {
664
+ if (member === ".") continue;
665
+ const from = join(root, member, "node_modules");
666
+ const to = join(worktreeProject, member, "node_modules");
667
+ if (existsSync(from) && !existsSync(to) && existsSync(dirname(to))) symlinkSync(from, to, "dir");
668
+ }
669
+ for (const member of targets) {
670
+ const dir = member === "." ? worktreeProject : join(worktreeProject, member);
671
+ const at = namedMember ? { member } : {};
672
+ for (const command of ["build", "lint"] as const) {
673
+ const r = await run(command, dir);
674
+ checks.push(r.exitCode === 0 ? { name: command, status: "passed", ...at } : { name: command, status: "failed", detail: tail(r.output), ...at });
675
+ }
616
676
  }
617
677
  } else {
618
678
  for (const name of ["build", "lint"] as const) checks.push({ name, status: "skipped", detail: "not a chant project (no chant.config)" });
@@ -627,7 +687,8 @@ export async function stageUpgrade(options: UpgradeOptions): Promise<StagedUpgra
627
687
  from: fromDir ? sourcePin(lineage.source) : lineage.ref ?? null,
628
688
  to: fromDir ? sourcePin(staged.source) : staged.ref ?? null,
629
689
  ...(upstream.commit ? { commit: { ...(lineage.address?.commit ? { from: lineage.address.commit } : {}), to: upstream.commit } } : {}),
630
- migrations: plan.chain.map((m) => m.migration.id),
690
+ migrations: [...plan.chain.map((m) => m.migration.id), ...chantMigrations.filter((m) => m.applied).map((m) => m.id)],
691
+ chantMigrations,
631
692
  written: result.written,
632
693
  merged: result.merged,
633
694
  removed: result.removed,
@@ -721,7 +782,14 @@ export function describeStaged(staged: StagedUpgrade): string[] {
721
782
  const lines: string[] = [];
722
783
  const at = (p: string) => (staged.scope === "." ? p : `${staged.scope}/${p}`);
723
784
  lines.push(`${staged.scope} ${staged.template} ${staged.from ?? "(no ref)"} -> ${staged.to ?? "(no ref)"}`);
724
- for (const id of staged.migrations) lines.push(` migration: ${id}`);
785
+ const chantIds = new Set(staged.chantMigrations.map((m) => m.id));
786
+ for (const id of staged.migrations) if (!chantIds.has(id)) lines.push(` migration: ${id}`);
787
+ for (const m of staged.chantMigrations) {
788
+ lines.push(` chant migration: ${m.id} (${m.applied ? "applied" : "not applied: it has conflicts"}), to ${m.description}`);
789
+ for (const c of m.changes) lines.push(` ${c.action === "delete" ? "delete" : "write"}: ${at(c.path)} (${c.why}${c.edited ? "; it was edited since the template, and the edit stays in git history" : ""})`);
790
+ for (const n of m.notMoved) lines.push(` not moved: ${n.what} -> ${n.where}`);
791
+ for (const c of m.conflicts) lines.push(` conflict: ${at(c.path)}: ${c.reason}`);
792
+ }
725
793
  for (const p of staged.written) lines.push(` updated: ${at(p)}`);
726
794
  for (const p of staged.merged) lines.push(` merged: ${at(p)}`);
727
795
  for (const p of staged.removed) lines.push(` removed: ${at(p)}`);
@@ -735,7 +803,10 @@ export function describeStaged(staged: StagedUpgrade): string[] {
735
803
  ` governance: ${staged.governance.paths.join(", ")}; needs ${q.count} human approval(s)${q.roles ? ` with a role in ${q.roles.join(", ")}` : ""}, from the rules at ${staged.governance.rules.slice(0, 12)}`,
736
804
  );
737
805
  }
738
- for (const c of staged.checks) lines.push(` ${c.name}: ${c.status}${c.status === "skipped" && c.detail ? ` (${c.detail})` : ""}`);
806
+ for (const c of staged.checks) {
807
+ const label = c.member !== undefined ? `${c.name} (${c.member})` : c.name;
808
+ lines.push(` ${label}: ${c.status}${c.status === "skipped" && c.detail ? ` (${c.detail})` : ""}`);
809
+ }
739
810
  lines.push(` patch: ${staged.changedPaths.length} file(s)${staged.lock.tracked ? "" : ` and the untracked ${LOCK_FILE}`}, ${staged.digest}`);
740
811
  return lines;
741
812
  }
@@ -135,6 +135,63 @@ describe("chant workspace ls on built workspaces", () => {
135
135
  expect(doc.summary).toEqual({ members: 5, unreadable: 3, groups: 1, matches: 0 });
136
136
  });
137
137
 
138
+ test("lists declared diagrams flattened across the workspace and its members (#2764)", () => {
139
+ const root = repo({
140
+ "chant.workspace.json": JSON.stringify({
141
+ name: "acme",
142
+ schema: 1,
143
+ members: [
144
+ {
145
+ name: "docs",
146
+ dir: "docs",
147
+ kind: "other",
148
+ because: "the docs site",
149
+ diagrams: [
150
+ {
151
+ name: "architecture",
152
+ title: "Studio architecture",
153
+ source: "docs/diagrams/architecture.d2",
154
+ render: "docs/diagrams/architecture.svg",
155
+ renderer: { tool: "d2", version: "0.9.0", args: ["--layout=elk", "--theme=0", "--pad=40", "--omit-version"] },
156
+ sourceHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
157
+ },
158
+ ],
159
+ },
160
+ ],
161
+ diagrams: [
162
+ {
163
+ name: "boundary",
164
+ title: "chant and hud",
165
+ source: null,
166
+ render: "docs/diagrams/boundary.svg",
167
+ renderer: { tool: "graphviz", version: "9.0.0", args: [] },
168
+ },
169
+ ],
170
+ }),
171
+ "docs/README.md": "",
172
+ });
173
+ const doc = result(listWorkspace({ cwd: root }));
174
+ expectValid(doc);
175
+ expect(doc.diagrams).toEqual([
176
+ {
177
+ name: "boundary",
178
+ title: "chant and hud",
179
+ source: null,
180
+ render: "docs/diagrams/boundary.svg",
181
+ renderer: { tool: "graphviz", version: "9.0.0", args: [] },
182
+ member: null,
183
+ },
184
+ {
185
+ name: "architecture",
186
+ title: "Studio architecture",
187
+ source: "docs/diagrams/architecture.d2",
188
+ render: "docs/diagrams/architecture.svg",
189
+ renderer: { tool: "d2", version: "0.9.0", args: ["--layout=elk", "--theme=0", "--pad=40", "--omit-version"] },
190
+ member: "docs",
191
+ },
192
+ ]);
193
+ });
194
+
138
195
  test("a nested workspace member is readable by its own declaration, and ls inside it lists the inner one", () => {
139
196
  const root = repo({
140
197
  "chant.workspace.json": declaration([{ name: "kit", dir: "vendor/kit", kind: "workspace" }]),
@@ -8,7 +8,7 @@
8
8
  "result": {
9
9
  "description": "The declaration was read. Exit code 0, even when some members can't be read.",
10
10
  "type": "object",
11
- "required": ["$schema", "contract", "chant", "at", "workspace", "members", "groups", "summary"],
11
+ "required": ["$schema", "contract", "chant", "at", "workspace", "members", "groups", "diagrams", "summary"],
12
12
  "properties": {
13
13
  "$schema": { "const": "https://intentius.io/chant/schemas/workspace/ls/v1/ls.schema.json" },
14
14
  "contract": { "const": 1 },
@@ -61,6 +61,11 @@
61
61
  "type": "array",
62
62
  "items": { "$ref": "#/$defs/group" }
63
63
  },
64
+ "diagrams": {
65
+ "description": "Added in contract 1 by #2764. Every declared diagram artifact, flattened across the workspace and its members: the workspace's own first, then each member's, in file order.",
66
+ "type": "array",
67
+ "items": { "$ref": "#/$defs/diagram" }
68
+ },
64
69
  "summary": {
65
70
  "type": "object",
66
71
  "required": ["members", "unreadable", "groups", "matches"],
@@ -138,9 +143,49 @@
138
143
  }
139
144
  }
140
145
  ]
146
+ },
147
+ "acceptance": {
148
+ "description": "Added in contract 1 by #2772. For a work kind whose work block names acceptance criteria: each current work item that lists some, in path order, with how many of its criteria passing evidence meets, read in the tree read. Null for any other kind, and when the kind or its records can't be read.",
149
+ "oneOf": [
150
+ { "type": "null" },
151
+ {
152
+ "type": "array",
153
+ "items": {
154
+ "type": "object",
155
+ "required": ["item", "state", "met", "total"],
156
+ "properties": {
157
+ "item": { "type": "string", "description": "The work item's id." },
158
+ "state": { "type": ["string", "null"], "description": "The item's state." },
159
+ "met": { "type": "integer", "minimum": 0, "description": "Criteria met by evidence naming them, with result pass and the criterion's verification; a manual verdict counts only from someone other than the implementer." },
160
+ "total": { "type": "integer", "minimum": 0, "description": "Criteria the item lists." }
161
+ }
162
+ }
163
+ }
164
+ ]
141
165
  }
142
166
  }
143
167
  },
168
+ "diagram": {
169
+ "description": "A diagram artifact the declaration names (#2764), pinned to the renderer its render was made with. chant runs no renderer; everything here comes straight from the declaration.",
170
+ "type": "object",
171
+ "required": ["name", "title", "source", "render", "renderer", "member"],
172
+ "properties": {
173
+ "name": { "type": "string" },
174
+ "title": { "type": "string" },
175
+ "source": { "type": ["string", "null"], "description": "From the workspace root, with / separators. Null for an SVG with no source." },
176
+ "render": { "type": "string", "description": "From the workspace root, with / separators." },
177
+ "renderer": {
178
+ "type": "object",
179
+ "required": ["tool", "version", "args"],
180
+ "properties": {
181
+ "tool": { "enum": ["d2", "mermaid", "graphviz"] },
182
+ "version": { "type": "string", "description": "The exact release the render was made with, such as 0.9.0." },
183
+ "args": { "type": "array", "items": { "type": "string" }, "description": "Passed before the input and output paths, in order." }
184
+ }
185
+ },
186
+ "member": { "type": ["string", "null"], "description": "The member that declares it, or null for the workspace's own." }
187
+ }
188
+ },
144
189
  "group": {
145
190
  "type": "object",
146
191
  "required": ["name", "kind", "glob", "matches", "skipped", "reason"],
@@ -13,6 +13,11 @@
13
13
  * and each member's, with each kind file's own name when it loads (#2680).
14
14
  * Loading one imports it, so {@link listWorkspace} lists them unloaded and
15
15
  * {@link listWorkspaceWithKinds}, which the command runs, loads them.
16
+ *
17
+ * The declared diagram artifacts are listed too, flattened across the
18
+ * workspace and its members into one `diagrams` array (#2764). Unlike record
19
+ * kinds, nothing about a diagram is loaded: everything printed comes
20
+ * straight from the declaration, and chant runs no renderer to list it.
16
21
  */
17
22
 
18
23
  import { existsSync } from "node:fs";
@@ -20,11 +25,13 @@ import { resolve } from "node:path";
20
25
  import { formatError } from "../cli/format";
21
26
  import type { CommandContext } from "../cli/registry";
22
27
  import {
28
+ declaredDiagrams,
23
29
  readDeclaration,
24
30
  readerVersion,
25
31
  resolveGroups,
26
32
  WorkspaceReadError,
27
33
  type Declaration,
34
+ type DiagramDeclaration,
28
35
  type ErrorLocation,
29
36
  type Member,
30
37
  type WorkspaceErrorCode,
@@ -74,6 +81,20 @@ export interface LsRecordKind {
74
81
  kind: string | null;
75
82
  /** Why the kind file can't be loaded, or null. */
76
83
  reason: { code: DeclaredKindReasonCode; message: string } | null;
84
+ /**
85
+ * For a work kind with acceptance criteria (#2772): each current work item
86
+ * that lists criteria, with how many passing evidence meets. Null for any
87
+ * other kind, and when the kind or its records can't be read.
88
+ */
89
+ acceptance: LsAcceptance[] | null;
90
+ }
91
+
92
+ /** A work item's acceptance criteria, counted (#2772). */
93
+ export interface LsAcceptance {
94
+ item: string;
95
+ state: string | null;
96
+ met: number;
97
+ total: number;
77
98
  }
78
99
 
79
100
  export interface LsMember {
@@ -89,6 +110,25 @@ export interface LsMember {
89
110
  records: LsRecordKind[];
90
111
  }
91
112
 
113
+ /**
114
+ * A diagram artifact the declaration names (#2764), flattened across the
115
+ * workspace and its members: hud reads one list, the way it already reads
116
+ * `records --json`'s flat `assets[]`. `sourceHash`, the declaration's own
117
+ * bookkeeping for `chant workspace check`'s drift finding, is not printed
118
+ * here; a reader that draws diagrams has no use for it.
119
+ */
120
+ export interface LsDiagram {
121
+ name: string;
122
+ title: string;
123
+ /** From the workspace root, with / separators. Null for an SVG with no source. */
124
+ source: string | null;
125
+ /** From the workspace root, with / separators. */
126
+ render: string;
127
+ renderer: DiagramDeclaration["renderer"];
128
+ /** The member that declares it, or null for the workspace's own. */
129
+ member: string | null;
130
+ }
131
+
92
132
  export interface LsGroup {
93
133
  name: string;
94
134
  kind: "examples";
@@ -117,6 +157,8 @@ export type LsDocument =
117
157
  };
118
158
  members: LsMember[];
119
159
  groups: LsGroup[];
160
+ /** Every declared diagram, the workspace's own first, then each member's, in file order (#2764). */
161
+ diagrams: LsDiagram[];
120
162
  summary: { members: number; unreadable: number; groups: number; matches: number };
121
163
  }
122
164
  | {
@@ -162,11 +204,12 @@ export async function listWorkspaceWithKinds(query: LsQuery): Promise<LsDocument
162
204
  const { doc, declaration, tree, rootOnDisk } = readListing(query);
163
205
  if (!declaration || "error" in doc) return doc;
164
206
  const { loadDeclaredKinds } = await import("./declared-kinds");
165
- const loaded = await loadDeclaredKinds(declaration, tree!, rootOnDisk!);
207
+ const loaded = await loadDeclaredKinds(declaration, tree!, rootOnDisk!, { acceptance: true, ...(doc.at !== null ? { at: doc.at } : {}) });
166
208
  const byPath = new Map(loaded.map((k) => [k.declared.path, k]));
167
209
  const fill = (r: LsRecordKind): LsRecordKind => {
168
210
  const k = byPath.get(r.path)!;
169
- return { ...r, name: r.name ?? k.kind, kind: k.kind, reason: k.reason };
211
+ const acceptance = k.acceptance ? k.acceptance.map((a) => ({ item: a.item, state: a.state, met: a.met, total: a.total })) : null;
212
+ return { ...r, name: r.name ?? k.kind, kind: k.kind, reason: k.reason, acceptance };
170
213
  };
171
214
  return {
172
215
  ...doc,
@@ -175,7 +218,7 @@ export async function listWorkspaceWithKinds(query: LsQuery): Promise<LsDocument
175
218
  };
176
219
  }
177
220
 
178
- const unloaded = (records: Declaration["records"]): LsRecordKind[] => records.map((r) => ({ name: r.name, path: r.path, kind: null, reason: null }));
221
+ const unloaded = (records: Declaration["records"]): LsRecordKind[] => records.map((r) => ({ name: r.name, path: r.path, kind: null, reason: null, acceptance: null }));
179
222
 
180
223
  function readListing(query: LsQuery): { doc: LsDocument; declaration?: Declaration; tree?: WorkspaceTree; rootOnDisk?: string } {
181
224
  const chant = readerVersion();
@@ -213,6 +256,14 @@ function readListing(query: LsQuery): { doc: LsDocument; declaration?: Declarati
213
256
  ? { code: "no-matches", message: `no directory ${g.group.globs.join(" or ")} matches${tree.label} holds a chant project` }
214
257
  : null,
215
258
  }));
259
+ const diagrams: LsDiagram[] = declaredDiagrams(declaration).map((d) => ({
260
+ name: d.name,
261
+ title: d.title,
262
+ source: d.source,
263
+ render: d.render,
264
+ renderer: d.renderer,
265
+ member: d.member,
266
+ }));
216
267
  const doc: LsDocument = {
217
268
  ...head,
218
269
  at,
@@ -227,6 +278,7 @@ function readListing(query: LsQuery): { doc: LsDocument; declaration?: Declarati
227
278
  },
228
279
  members,
229
280
  groups: lsGroups,
281
+ diagrams,
230
282
  summary: {
231
283
  members: members.length,
232
284
  unreadable: members.filter((m) => !m.readable).length,
@@ -296,8 +348,15 @@ function formatLs(doc: Extract<LsDocument, { members: unknown }>): string {
296
348
  kinds.forEach(({ r }, i) => {
297
349
  lines.push(rendered[i + 1]);
298
350
  if (r.reason) lines.push(` ${r.reason.code}: ${r.reason.message}`);
351
+ for (const a of r.acceptance ?? []) lines.push(` ${a.item} ${a.state ?? "-"}: ${a.met}/${a.total} criteria met`);
299
352
  });
300
353
  }
354
+ if (doc.diagrams.length > 0) {
355
+ lines.push("");
356
+ const rows = [["DIAGRAM", "MEMBER", "SOURCE", "RENDER", "RENDERER"]];
357
+ for (const d of doc.diagrams) rows.push([d.name, d.member ?? "(workspace)", d.source ?? "-", d.render, `${d.renderer.tool} ${d.renderer.version}`]);
358
+ lines.push(...table(rows));
359
+ }
301
360
  if (doc.groups.length > 0) {
302
361
  lines.push("");
303
362
  const rows = [["GROUP", "KIND", "GLOB", "PROJECTS"]];
@@ -15,7 +15,7 @@
15
15
  */
16
16
 
17
17
  import { readFileSync } from "node:fs";
18
- import { resolve } from "node:path";
18
+ import { relative, resolve, sep } from "node:path";
19
19
  import { formatError } from "../cli/format";
20
20
  import type { CommandContext } from "../cli/registry";
21
21
  import { declaredRecordKinds, readDeclaration, readerVersion, WorkspaceReadError, type ErrorLocation, type WorkspaceErrorCode } from "./declaration";
@@ -24,6 +24,7 @@ import { answerPoint, askPoint, questionView, POINTS_WRITE_CONTRACT_VERSION, POI
24
24
  import { candidates, inputOutput, pointsFileOf, pointVersion, quorumOf, readPointsThrough, type Decider, type ModelAsk, type WireAnswer } from "./points";
25
25
  import type { ReasonCode } from "./reason-codes";
26
26
  import { gitRevisionSource, workingTreeSource } from "./record-source";
27
+ import { readLedgerAnswers, withLedgerAnswers, type LedgerAnswers } from "./answers-ledger";
27
28
  import { loadRecordKind, RecordReadError, type ReadErrorCode } from "./records";
28
29
  import { readRecordsFor } from "./records-cli";
29
30
  import { locateWorkspace } from "./which-chant";
@@ -136,7 +137,17 @@ export async function workspacePoints(query: PointsQuery): Promise<PointsDocumen
136
137
  if (k.given) sources.push({ kind: k.shown, points: null, reason: { code: "kind-invalid", message: `the ${loaded.kind.name} kind has no answers block, so it holds no answers to decision points` } });
137
138
  continue;
138
139
  }
139
- const read = await readRecordsFor({ kind: k.file, cwd: query.cwd, at: query.at });
140
+ // Without --at, the questions a steward keeps on the lifecycle ledger are read too (#2786).
141
+ let ledger = undefined as LedgerAnswers | undefined;
142
+ const read = await readRecordsFor({
143
+ kind: k.file,
144
+ cwd: query.cwd,
145
+ at: query.at,
146
+ overlay: async (base, { loaded: l, root }) => {
147
+ ledger = await readLedgerAnswers({ file: l.file, name: l.kind.name, dirRel: relative(root, l.dir).split(sep).join("/") || "." });
148
+ return withLedgerAnswers(base, ledger);
149
+ },
150
+ });
140
151
  const source = read.at !== null && read.top ? gitRevisionSource(read.top, read.at) : workingTreeSource(read.root);
141
152
  const file = pointsFileOf(read.loaded, read.root);
142
153
  const parsed = readPointsThrough(source, file);
@@ -158,7 +169,7 @@ export async function workspacePoints(query: PointsQuery): Promise<PointsDocumen
158
169
  });
159
170
  }
160
171
  for (const e of read.result.records) {
161
- const view = questionView(e, typeof e.data?.point === "string" ? declared[e.data.point] : undefined);
172
+ const view = questionView(e, typeof e.data?.point === "string" ? declared[e.data.point] : undefined, ledger?.byPath.get(e.path)?.ledger ?? null);
162
173
  if (view && (!query.open || view.open)) questions.push(view);
163
174
  }
164
175
  } catch (err) {