@intentius/chant 0.91.0 → 0.94.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 (361) 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-plugins.d.ts +6 -6
  9. package/dist/cli/mcp/workspace-tools.d.ts +2 -1
  10. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  11. package/dist/cli/plugins.d.ts.map +1 -1
  12. package/dist/cli/registry.d.ts +12 -2
  13. package/dist/cli/registry.d.ts.map +1 -1
  14. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  15. package/dist/config.d.ts +22 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/lexicon-module.d.ts +22 -0
  18. package/dist/lexicon-module.d.ts.map +1 -1
  19. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  20. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  21. package/dist/lifecycle/run-ledger.d.ts +2 -0
  22. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  23. package/dist/lifecycle/symptoms.d.ts +51 -0
  24. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  25. package/dist/lint/rules/index.d.ts +4 -2
  26. package/dist/lint/rules/index.d.ts.map +1 -1
  27. package/dist/lint/rules/op/index.d.ts +14 -3
  28. package/dist/lint/rules/op/index.d.ts.map +1 -1
  29. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  31. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  33. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  35. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  36. package/dist/op/activities/activity-contracts.d.ts +201 -0
  37. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  38. package/dist/op/activities/apply.d.ts.map +1 -1
  39. package/dist/op/activities/change-coverage.d.ts +44 -0
  40. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  41. package/dist/op/activities/converge.d.ts +24 -1
  42. package/dist/op/activities/converge.d.ts.map +1 -1
  43. package/dist/op/activities/decide.d.ts +107 -0
  44. package/dist/op/activities/decide.d.ts.map +1 -0
  45. package/dist/op/activities/index.d.ts +10 -0
  46. package/dist/op/activities/index.d.ts.map +1 -1
  47. package/dist/op/activities/shell.d.ts +32 -0
  48. package/dist/op/activities/shell.d.ts.map +1 -1
  49. package/dist/op/activities/source-release.d.ts +86 -0
  50. package/dist/op/activities/source-release.d.ts.map +1 -0
  51. package/dist/op/activities/source-rollback.d.ts +100 -0
  52. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  53. package/dist/op/activities/work-evidence.d.ts +42 -0
  54. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  55. package/dist/op/activity-contract.d.ts +1 -0
  56. package/dist/op/activity-contract.d.ts.map +1 -1
  57. package/dist/op/activity-profiles.d.ts +16 -0
  58. package/dist/op/activity-profiles.d.ts.map +1 -1
  59. package/dist/op/builders.d.ts +84 -2
  60. package/dist/op/builders.d.ts.map +1 -1
  61. package/dist/op/composites/converge-op.d.ts +31 -4
  62. package/dist/op/composites/converge-op.d.ts.map +1 -1
  63. package/dist/op/decide-backend.d.ts +116 -0
  64. package/dist/op/decide-backend.d.ts.map +1 -0
  65. package/dist/op/decide-config.d.ts +120 -0
  66. package/dist/op/decide-config.d.ts.map +1 -0
  67. package/dist/op/decide-read-inputs.d.ts +40 -0
  68. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  69. package/dist/op/gate-wait.d.ts +43 -0
  70. package/dist/op/gate-wait.d.ts.map +1 -0
  71. package/dist/op/gate.d.ts +5 -2
  72. package/dist/op/gate.d.ts.map +1 -1
  73. package/dist/op/index.d.ts +11 -4
  74. package/dist/op/index.d.ts.map +1 -1
  75. package/dist/op/local-executor.d.ts +29 -4
  76. package/dist/op/local-executor.d.ts.map +1 -1
  77. package/dist/op/local-output.d.ts +2 -0
  78. package/dist/op/local-output.d.ts.map +1 -1
  79. package/dist/op/op-ir.d.ts +2 -0
  80. package/dist/op/op-ir.d.ts.map +1 -1
  81. package/dist/op/op-verb-class.d.ts.map +1 -1
  82. package/dist/op/operator.d.ts +97 -1
  83. package/dist/op/operator.d.ts.map +1 -1
  84. package/dist/op/runtime.d.ts +37 -5
  85. package/dist/op/runtime.d.ts.map +1 -1
  86. package/dist/op/runtimes/local.d.ts.map +1 -1
  87. package/dist/op/source-archive.d.ts +63 -0
  88. package/dist/op/source-archive.d.ts.map +1 -0
  89. package/dist/op/steward-points.d.ts +136 -0
  90. package/dist/op/steward-points.d.ts.map +1 -0
  91. package/dist/op/steward-turn.d.ts +53 -0
  92. package/dist/op/steward-turn.d.ts.map +1 -0
  93. package/dist/op/steward.d.ts +22 -1
  94. package/dist/op/steward.d.ts.map +1 -1
  95. package/dist/op/types.d.ts +7 -0
  96. package/dist/op/types.d.ts.map +1 -1
  97. package/dist/op/work-lease-run.d.ts +1 -1
  98. package/dist/op/work-lease-run.d.ts.map +1 -1
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  100. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  101. package/dist/workspace/answers-ledger.d.ts +76 -0
  102. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  103. package/dist/workspace/box-intent.d.ts +85 -0
  104. package/dist/workspace/box-intent.d.ts.map +1 -0
  105. package/dist/workspace/changes-cli.d.ts +16 -0
  106. package/dist/workspace/changes-cli.d.ts.map +1 -0
  107. package/dist/workspace/changes.d.ts +200 -0
  108. package/dist/workspace/changes.d.ts.map +1 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  118. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  119. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  120. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  121. package/dist/workspace/chant-migrations.d.ts +104 -0
  122. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  123. package/dist/workspace/checks/boxes.d.ts +47 -1
  124. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  125. package/dist/workspace/checks/diagrams.d.ts +31 -0
  126. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  127. package/dist/workspace/checks/records.d.ts +7 -0
  128. package/dist/workspace/checks/records.d.ts.map +1 -1
  129. package/dist/workspace/checks.d.ts +7 -0
  130. package/dist/workspace/checks.d.ts.map +1 -1
  131. package/dist/workspace/compose-graph.d.ts +2 -0
  132. package/dist/workspace/compose-graph.d.ts.map +1 -1
  133. package/dist/workspace/conformance/index.d.ts.map +1 -1
  134. package/dist/workspace/decide.d.ts +49 -4
  135. package/dist/workspace/decide.d.ts.map +1 -1
  136. package/dist/workspace/decision-points.schema.json +3 -3
  137. package/dist/workspace/declaration.d.ts +58 -0
  138. package/dist/workspace/declaration.d.ts.map +1 -1
  139. package/dist/workspace/declaration.schema.json +119 -1
  140. package/dist/workspace/declared-kinds.d.ts +20 -0
  141. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  142. package/dist/workspace/generated-files.d.ts +7 -0
  143. package/dist/workspace/generated-files.d.ts.map +1 -1
  144. package/dist/workspace/intent-joins.d.ts +5 -5
  145. package/dist/workspace/intent.d.ts.map +1 -1
  146. package/dist/workspace/lineage-check.d.ts +3 -0
  147. package/dist/workspace/lineage-check.d.ts.map +1 -1
  148. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  149. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  150. package/dist/workspace/ls.d.ts +39 -1
  151. package/dist/workspace/ls.d.ts.map +1 -1
  152. package/dist/workspace/point-answer.schema.json +95 -0
  153. package/dist/workspace/points-cli.d.ts +2 -0
  154. package/dist/workspace/points-cli.d.ts.map +1 -1
  155. package/dist/workspace/points.d.ts +4 -6
  156. package/dist/workspace/points.d.ts.map +1 -1
  157. package/dist/workspace/reason-codes.d.ts +16 -2
  158. package/dist/workspace/reason-codes.d.ts.map +1 -1
  159. package/dist/workspace/records-cli.d.ts +11 -1
  160. package/dist/workspace/records-cli.d.ts.map +1 -1
  161. package/dist/workspace/records-close.d.ts +1 -1
  162. package/dist/workspace/records-close.d.ts.map +1 -1
  163. package/dist/workspace/records-write.d.ts +11 -5
  164. package/dist/workspace/records-write.d.ts.map +1 -1
  165. package/dist/workspace/records.d.ts +34 -4
  166. package/dist/workspace/records.d.ts.map +1 -1
  167. package/dist/workspace/status-gates.d.ts +7 -2
  168. package/dist/workspace/status-gates.d.ts.map +1 -1
  169. package/dist/workspace/status-stewards.d.ts +65 -1
  170. package/dist/workspace/status-stewards.d.ts.map +1 -1
  171. package/dist/workspace/status.d.ts +24 -0
  172. package/dist/workspace/status.d.ts.map +1 -1
  173. package/dist/workspace/work-cli.d.ts +113 -2
  174. package/dist/workspace/work-cli.d.ts.map +1 -1
  175. package/dist/workspace/work-evidence.d.ts +74 -0
  176. package/dist/workspace/work-evidence.d.ts.map +1 -0
  177. package/dist/workspace/work.d.ts +43 -1
  178. package/dist/workspace/work.d.ts.map +1 -1
  179. package/package.json +1 -1
  180. package/src/cli/commands/audit.test.ts +9 -1
  181. package/src/cli/commands/build.ts +3 -2
  182. package/src/cli/commands/lint.ts +7 -1
  183. package/src/cli/handlers/graph.test.ts +20 -0
  184. package/src/cli/handlers/graph.ts +16 -2
  185. package/src/cli/handlers/operator.ts +34 -2
  186. package/src/cli/handlers/run.test.ts +154 -2
  187. package/src/cli/handlers/run.ts +121 -2
  188. package/src/cli/main.test.ts +10 -0
  189. package/src/cli/main.ts +23 -3
  190. package/src/cli/mcp/workspace-plugins.ts +6 -6
  191. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  192. package/src/cli/mcp/workspace-tools.ts +62 -4
  193. package/src/cli/plugins.ts +11 -3
  194. package/src/cli/registry.ts +12 -2
  195. package/src/cli/serve-mcp-workspace.test.ts +6 -6
  196. package/src/codegen/pinned-upgrade.ts +2 -1
  197. package/src/codegen/release-wiring.test.ts +49 -2
  198. package/src/components/capability-plugin-loader.ts +3 -3
  199. package/src/components/cli-support.ts +2 -2
  200. package/src/config.ts +11 -0
  201. package/src/lexicon-module.ts +56 -1
  202. package/src/lexicon-package.test.ts +96 -0
  203. package/src/lifecycle/converge-ledger.ts +20 -0
  204. package/src/lifecycle/run-ledger.ts +6 -0
  205. package/src/lifecycle/symptoms.ts +95 -0
  206. package/src/lint/rules/index.ts +6 -2
  207. package/src/lint/rules/op/index.ts +17 -4
  208. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  209. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  210. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  211. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  212. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  213. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  214. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  215. package/src/op/activities/activity-contracts.ts +190 -1
  216. package/src/op/activities/apply.ts +7 -6
  217. package/src/op/activities/change-coverage.ts +60 -0
  218. package/src/op/activities/converge.test.ts +55 -1
  219. package/src/op/activities/converge.ts +208 -29
  220. package/src/op/activities/decide.live.test.ts +78 -0
  221. package/src/op/activities/decide.test.ts +308 -0
  222. package/src/op/activities/decide.ts +267 -0
  223. package/src/op/activities/index.ts +21 -0
  224. package/src/op/activities/shell.test.ts +91 -1
  225. package/src/op/activities/shell.ts +91 -2
  226. package/src/op/activities/source-release.test.ts +118 -0
  227. package/src/op/activities/source-release.ts +168 -0
  228. package/src/op/activities/source-rollback.test.ts +131 -0
  229. package/src/op/activities/source-rollback.ts +239 -0
  230. package/src/op/activities/work-evidence.ts +62 -0
  231. package/src/op/activity-contract-registry.test.ts +2 -0
  232. package/src/op/activity-contract-registry.ts +2 -2
  233. package/src/op/activity-contract.test.ts +12 -0
  234. package/src/op/activity-contract.ts +6 -1
  235. package/src/op/activity-profiles.ts +29 -0
  236. package/src/op/activity-registry.test.ts +1 -1
  237. package/src/op/activity-registry.ts +2 -2
  238. package/src/op/builders.ts +119 -5
  239. package/src/op/command-gate.test.ts +192 -0
  240. package/src/op/composites/converge-op.test.ts +27 -1
  241. package/src/op/composites/converge-op.ts +69 -15
  242. package/src/op/decide-backend.test.ts +44 -0
  243. package/src/op/decide-backend.ts +212 -0
  244. package/src/op/decide-config.test.ts +52 -0
  245. package/src/op/decide-config.ts +80 -0
  246. package/src/op/decide-read-inputs.ts +117 -0
  247. package/src/op/gate-approval.ts +2 -2
  248. package/src/op/gate-wait.ts +57 -0
  249. package/src/op/gate.ts +8 -3
  250. package/src/op/generate-pipeline.ts +2 -2
  251. package/src/op/index.ts +18 -3
  252. package/src/op/local-executor.ts +151 -15
  253. package/src/op/local-output.test.ts +33 -0
  254. package/src/op/local-output.ts +20 -2
  255. package/src/op/op-ir.test.ts +10 -0
  256. package/src/op/op-ir.ts +4 -0
  257. package/src/op/op-verb-class.ts +1 -0
  258. package/src/op/operator.ts +290 -11
  259. package/src/op/runtime.ts +31 -8
  260. package/src/op/runtimes/local.ts +3 -1
  261. package/src/op/source-archive.ts +161 -0
  262. package/src/op/steward-points.test.ts +364 -0
  263. package/src/op/steward-points.ts +194 -0
  264. package/src/op/steward-turn.ts +79 -0
  265. package/src/op/steward.test.ts +87 -3
  266. package/src/op/steward.ts +25 -1
  267. package/src/op/types.ts +7 -0
  268. package/src/op/work-lease-run.test.ts +20 -1
  269. package/src/op/work-lease-run.ts +2 -2
  270. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  271. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  272. package/src/workspace/answers-ledger.ts +118 -0
  273. package/src/workspace/box-intent.test.ts +205 -0
  274. package/src/workspace/box-intent.ts +159 -0
  275. package/src/workspace/changes-cli.ts +56 -0
  276. package/src/workspace/changes.schema.json +472 -0
  277. package/src/workspace/changes.test.ts +315 -0
  278. package/src/workspace/changes.ts +512 -0
  279. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  280. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  281. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  282. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  283. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  284. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  285. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  286. package/src/workspace/chant-migrations.ts +169 -0
  287. package/src/workspace/check-contract.test.ts +4 -2
  288. package/src/workspace/check.schema.json +19 -5
  289. package/src/workspace/checks/boxes.test.ts +82 -2
  290. package/src/workspace/checks/boxes.ts +202 -1
  291. package/src/workspace/checks/diagrams.test.ts +134 -0
  292. package/src/workspace/checks/diagrams.ts +108 -0
  293. package/src/workspace/checks/records.ts +47 -0
  294. package/src/workspace/checks.ts +20 -2
  295. package/src/workspace/compose-graph.ts +2 -0
  296. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  297. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  298. package/src/workspace/conformance/index.ts +5 -0
  299. package/src/workspace/decide.test.ts +1 -1
  300. package/src/workspace/decide.ts +156 -23
  301. package/src/workspace/decision-points.schema.json +3 -3
  302. package/src/workspace/declaration.schema.json +119 -1
  303. package/src/workspace/declaration.test.ts +58 -0
  304. package/src/workspace/declaration.ts +112 -0
  305. package/src/workspace/declared-kinds.test.ts +31 -5
  306. package/src/workspace/declared-kinds.ts +46 -1
  307. package/src/workspace/generated-files.ts +14 -0
  308. package/src/workspace/graph-cli.ts +1 -1
  309. package/src/workspace/graph.schema.json +9 -1
  310. package/src/workspace/intent-joins.test.ts +5 -5
  311. package/src/workspace/intent-joins.ts +5 -5
  312. package/src/workspace/intent.schema.json +8 -2
  313. package/src/workspace/intent.ts +3 -10
  314. package/src/workspace/lineage-check.ts +16 -1
  315. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  316. package/src/workspace/lineage-upgrade.test.ts +70 -0
  317. package/src/workspace/lineage-upgrade.ts +81 -10
  318. package/src/workspace/ls-contract.test.ts +57 -0
  319. package/src/workspace/ls.schema.json +46 -1
  320. package/src/workspace/ls.ts +62 -3
  321. package/src/workspace/points-cli.ts +17 -3
  322. package/src/workspace/points-write.schema.json +11 -1
  323. package/src/workspace/points.schema.json +51 -0
  324. package/src/workspace/points.test.ts +16 -15
  325. package/src/workspace/points.ts +5 -17
  326. package/src/workspace/read-contract.test.ts +9 -1
  327. package/src/workspace/reason-codes.test.ts +18 -8
  328. package/src/workspace/reason-codes.ts +21 -2
  329. package/src/workspace/records-amend.schema.json +2 -0
  330. package/src/workspace/records-cli.ts +27 -4
  331. package/src/workspace/records-close.schema.json +2 -0
  332. package/src/workspace/records-formats.test.ts +15 -15
  333. package/src/workspace/records-new.schema.json +2 -0
  334. package/src/workspace/records-review.schema.json +2 -0
  335. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  336. package/src/workspace/records-sessions-write.test.ts +2 -34
  337. package/src/workspace/records-since.test.ts +8 -7
  338. package/src/workspace/records-write.e2e.test.ts +108 -0
  339. package/src/workspace/records-write.test.ts +32 -41
  340. package/src/workspace/records-write.ts +33 -15
  341. package/src/workspace/records.schema.json +36 -2
  342. package/src/workspace/records.test.ts +49 -4
  343. package/src/workspace/records.ts +109 -3
  344. package/src/workspace/status-contract.test.ts +14 -8
  345. package/src/workspace/status-gates.ts +9 -4
  346. package/src/workspace/status-stewards.ts +155 -5
  347. package/src/workspace/status.schema.json +129 -8
  348. package/src/workspace/status.ts +46 -1
  349. package/src/workspace/work-acceptance.test.ts +274 -0
  350. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  351. package/src/workspace/work-cli.ts +193 -10
  352. package/src/workspace/work-evidence.schema.json +212 -0
  353. package/src/workspace/work-evidence.ts +169 -0
  354. package/src/workspace/work-history.schema.json +349 -0
  355. package/src/workspace/work-history.test.ts +156 -0
  356. package/src/workspace/{work-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
  357. package/src/workspace/work.test.ts +20 -2
  358. package/src/workspace/work.ts +100 -1
  359. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  360. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  361. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -66,6 +66,10 @@ import { policyAtBase, resolveBase } from "./trust/provenance";
66
66
  import { emptyPolicy, type TrustPolicy } from "./trust/policy";
67
67
  import { AGENT_ROLE } from "./records-cli";
68
68
  import type { SourceVia } from "./source-block";
69
+ import type { RecordSource } from "./record-source";
70
+ import { currentStewardTurn } from "../op/steward-turn";
71
+ import { RefCASConflictError } from "../lifecycle/git";
72
+ import { readLedgerAnswers, refreshLedger, withLedgerAnswers, writeLedgerAnswer, type LedgerAnswers } from "./answers-ledger";
69
73
 
70
74
  /** The version of the write documents `points ask` and `points answer` print. */
71
75
  export const POINTS_WRITE_CONTRACT_VERSION = 1;
@@ -93,6 +97,8 @@ export const POINTS_WRITE_ERROR_CODES = [
93
97
  "record-not-found",
94
98
  "record-closed",
95
99
  "record-id-taken",
100
+ /** The answer was given during a steward's turn (#2749): a steward never answers a question, its own or another's. */
101
+ "answer-in-steward-turn",
96
102
  ...RECORD_REASON_CODES,
97
103
  ] as const satisfies readonly ReasonCode[];
98
104
  export type PointsWriteErrorCode = (typeof POINTS_WRITE_ERROR_CODES)[number];
@@ -141,12 +147,47 @@ export interface QuestionView {
141
147
  answeredBy: string[];
142
148
  askedOn: string | null;
143
149
  answeredOn: string | null;
150
+ /**
151
+ * The steward whose turn asked the question (#2749), and the run it was in,
152
+ * or null when no steward asked it. The steward waits on the question and
153
+ * never answers it: a person does, through hud or `points answer`.
154
+ */
155
+ askedBy: { steward: string; run: string | null } | null;
156
+ /**
157
+ * Where the record is held when it is on the lifecycle ledger rather than
158
+ * in the working tree (#2786): `chant/lifecycle:<path>`, which `git show`
159
+ * reads. A question asked in a steward's turn is held there, since a
160
+ * steward never writes the checkout. null for a record in the tree; `path`
161
+ * is where the record reads as being either way.
162
+ */
163
+ ledger: string | null;
144
164
  valid: boolean;
145
165
  warnings: RecordWarning[];
146
166
  }
147
167
 
148
- /** One answer record as a {@link QuestionView}, or null when its front matter can't be read. `point` is its point as declared now, when there is one. */
149
- export function questionView(entry: RecordEntry, point: Point | undefined): QuestionView | null {
168
+ /** The harness a steward's question names in its source block (#2749). */
169
+ export const STEWARD_HARNESS = "chant-steward";
170
+
171
+ /** The steward that asked, from a record's source block, or null. */
172
+ export function askedByOf(data: Record<string, unknown>): QuestionView["askedBy"] {
173
+ const source = data.source;
174
+ if (source === null || typeof source !== "object" || Array.isArray(source)) return null;
175
+ const s = source as Record<string, unknown>;
176
+ if (s.harness !== STEWARD_HARNESS) return null;
177
+ const client = s.client as Record<string, unknown> | undefined;
178
+ const steward = client && typeof client.name === "string" ? client.name : null;
179
+ if (steward === null) return null;
180
+ const session = s.session as Record<string, unknown> | string | undefined;
181
+ const run = typeof session === "string" ? session : session && typeof session === "object" && typeof session.id === "string" ? session.id : null;
182
+ return { steward, run };
183
+ }
184
+
185
+ /**
186
+ * One answer record as a {@link QuestionView}, or null when its front matter
187
+ * can't be read. `point` is its point as declared now, when there is one, and
188
+ * `ledger` where the record is held when it is on the lifecycle ledger.
189
+ */
190
+ export function questionView(entry: RecordEntry, point: Point | undefined, ledger: string | null = null): QuestionView | null {
150
191
  const d = entry.data;
151
192
  if (d === null || entry.id === null) return null;
152
193
  const str = (v: unknown): string | null => (typeof v === "string" ? v : null);
@@ -187,6 +228,8 @@ export function questionView(entry: RecordEntry, point: Point | undefined): Ques
187
228
  answeredBy: Array.isArray(d.answered_by) ? d.answered_by.filter((b): b is string => typeof b === "string") : [],
188
229
  askedOn: str(d.asked_on),
189
230
  answeredOn: str(d.answered_on),
231
+ askedBy: askedByOf(d),
232
+ ledger,
190
233
  valid: entry.valid,
191
234
  warnings: entry.warnings,
192
235
  };
@@ -276,6 +319,21 @@ export interface AskPointOptions {
276
319
  via?: SourceVia;
277
320
  /** The client that asked, for its source block. */
278
321
  client?: { name: string; version?: string };
322
+ /**
323
+ * The steward whose turn asks (#2749), and its run. Recorded in the source
324
+ * block (harness `chant-steward`, the steward as the client, the run as the
325
+ * session), so the question says who is waiting on it and `points answer`
326
+ * never counts the steward toward its quorum. Takes the place of `client`.
327
+ */
328
+ steward?: { name: string; run?: string };
329
+ /**
330
+ * Where a new record goes: the answer kind's directory in the working tree,
331
+ * or the lifecycle ledger (#2786). Defaults to the ledger in a steward's
332
+ * turn (`steward` given, or this process a steward's turn), since a steward
333
+ * never writes the checkout, and to the tree otherwise. A question already
334
+ * held on the ledger is rewritten there whatever this says.
335
+ */
336
+ store?: "tree" | "ledger";
279
337
  /** The date written as asked_on, and answered_on for a table's answer, YYYY-MM-DD. Defaults to today, in UTC. */
280
338
  on?: string;
281
339
  dryRun?: boolean;
@@ -319,9 +377,34 @@ function checkInputs(name: string, point: Point, inputs: unknown): Record<string
319
377
  return inputs as Record<string, unknown>;
320
378
  }
321
379
 
322
- /** Write `data` as the record at `path`, after reading it back with every other record. */
323
- async function write(o: Opened, before: RecordEntry[], path: string, text: string, create: boolean, dryRun: boolean): Promise<RecordWarning[]> {
324
- const warnings = await validateWrite(o, before, path, text);
380
+ /** Where a write goes: the working tree, or the lifecycle ledger (#2786). */
381
+ interface WriteTarget {
382
+ /** The records as read: the tree with the ledger's answers laid over it. */
383
+ source: RecordSource;
384
+ ledger: LedgerAnswers;
385
+ /** Write to the ledger rather than the tree. */
386
+ onLedger: boolean;
387
+ message: string;
388
+ }
389
+
390
+ /**
391
+ * Write `text` as the record at `path`, after reading it back with every
392
+ * other record: into the working tree, or onto the ledger. Returns the
393
+ * record's warnings and, for a ledger write, where it went.
394
+ */
395
+ async function write(o: Opened, before: RecordEntry[], path: string, text: string, create: boolean, dryRun: boolean, target: WriteTarget): Promise<{ warnings: RecordWarning[]; ledger: string | null }> {
396
+ const warnings = await validateWrite(o, before, path, text, target.source);
397
+ const held = target.ledger.byPath.get(path);
398
+ if (target.onLedger) {
399
+ if (dryRun) return { warnings, ledger: held?.ledger ?? null };
400
+ try {
401
+ const { ledger } = await writeLedgerAnswer(target.ledger, path, text, target.message, held?.sha ?? null);
402
+ return { warnings, ledger };
403
+ } catch (err) {
404
+ if (err instanceof RefCASConflictError) throw new PointsWriteError("record-id-taken", `${path} was written on chant/lifecycle by another ask or answer at the same time: ask again to read it`);
405
+ throw err;
406
+ }
407
+ }
325
408
  if (!dryRun) {
326
409
  try {
327
410
  writeFileSync(abs(o, path), text, create ? { flag: "wx" } : undefined);
@@ -330,7 +413,14 @@ async function write(o: Opened, before: RecordEntry[], path: string, text: strin
330
413
  throw err;
331
414
  }
332
415
  }
333
- return warnings;
416
+ return { warnings, ledger: null };
417
+ }
418
+
419
+ /** The kind's answers on the ledger, and its records read with them laid over the tree. */
420
+ async function readWithLedger(o: Opened): Promise<{ ledger: LedgerAnswers; source: RecordSource; before: RecordEntry[] }> {
421
+ const ledger = await readLedgerAnswers({ file: o.loaded.file, name: o.loaded.kind.name, dirRel: o.dirRel });
422
+ const source = withLedgerAnswers(o.source, ledger);
423
+ return { ledger, source, before: await readAll(o, source) };
334
424
  }
335
425
 
336
426
  function recordData(o: Opened, fields: Record<string, unknown>): Record<string, unknown> {
@@ -355,15 +445,21 @@ function resultFields(result: ChainResult): Record<string, unknown> {
355
445
  */
356
446
  export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocument> {
357
447
  try {
448
+ // A steward's turn (#2749): named by the caller, or this process's, as when an Op shells out to `points ask`.
449
+ const turn = opts.steward ? undefined : currentStewardTurn();
450
+ const steward = opts.steward ?? (turn ? { name: turn.steward, ...(turn.run ? { run: turn.run } : {}) } : undefined);
451
+ const toLedger = opts.store !== undefined ? opts.store === "ledger" : steward !== undefined;
358
452
  const kinds = await answerKindFiles(opts.cwd, opts.kind);
359
453
  const { o, point } = await findPoint(kinds, opts.point, opts.cwd);
360
454
  const inputs = checkInputs(opts.point, point, opts.inputs);
361
455
  const version = pointVersion(point);
362
456
  const hash = inputsHash(opts.point, version, inputs);
363
457
  const id = answerId(opts.point, hash);
364
- const before = await readAll(o, o.source);
458
+ if (toLedger && !opts.dryRun) await refreshLedger(o.loaded.file);
459
+ const { ledger, source: read, before } = await readWithLedger(o);
365
460
  const existing = before.find((e) => e.id === id);
366
- const done = (entry: RecordEntry, extra: { reused: boolean; written: boolean; text?: string }): PointsWriteDocument => ({
461
+ const heldAt = (entry: RecordEntry): string | null => ledger.byPath.get(entry.path)?.ledger ?? null;
462
+ const done = (entry: RecordEntry, extra: { reused: boolean; written: boolean; text?: string; ledger?: string | null }): PointsWriteDocument => ({
367
463
  $schema: POINTS_WRITE_SCHEMA_ID,
368
464
  contract: POINTS_WRITE_CONTRACT_VERSION,
369
465
  verb: "ask",
@@ -373,7 +469,7 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
373
469
  reused: extra.reused,
374
470
  written: extra.written,
375
471
  dryRun: !!opts.dryRun,
376
- question: questionView(entry, point)!,
472
+ question: questionView(entry, point, extra.ledger !== undefined ? extra.ledger : heldAt(entry))!,
377
473
  ...(extra.text !== undefined ? { text: extra.text } : {}),
378
474
  });
379
475
  if (existing && existing.data !== null && (existing.state === "answered" || existing.state === "proposed")) return done(existing, { reused: true, written: false });
@@ -393,7 +489,14 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
393
489
  const answer = result.status === "escalated" ? undefined : result.answer;
394
490
  const title = titleFor(point, opts.subject, state, answer);
395
491
  const modelId = result.status === "proposed" ? result.decider.model : undefined;
396
- const source = { via: opts.via ?? "cli", ...(opts.client ? { client: opts.client } : {}), ...(modelId ? { model: modelId } : {}) };
492
+ const client = steward ? { name: steward.name } : opts.client;
493
+ const source = {
494
+ via: opts.via ?? "cli",
495
+ ...(steward ? { harness: STEWARD_HARNESS } : {}),
496
+ ...(client ? { client } : {}),
497
+ ...(modelId ? { model: modelId } : {}),
498
+ ...(steward?.run ? { session: { id: steward.run } } : {}),
499
+ };
397
500
  const data = recordData(o, {
398
501
  id,
399
502
  title,
@@ -411,16 +514,19 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
411
514
  });
412
515
  const path = existing ? existing.path : o.dirRel === "." ? `${id}.md` : `${o.dirRel}/${id}.md`;
413
516
  const text = renderRecord(data, body(point, title));
414
- const warnings = await write(o, before, path, text, !existing, !!opts.dryRun);
415
- const entry: RecordEntry = { ...(existing ?? emptyEntry(path)), id, path, state, data, valid: true, reasons: [], warnings };
416
- return done(entry, { reused: false, written: !opts.dryRun, ...(opts.dryRun ? { text } : {}) });
517
+ // A steward's question goes on the ledger, and one already held there stays there (#2786).
518
+ const onLedger = toLedger || ledger.byPath.has(path);
519
+ const message = `Decision point ${opts.point}: ${state}${opts.subject ? ` (${opts.subject})` : ""}${steward ? `, asked by ${steward.name}` : ""}`;
520
+ const wrote = await write(o, before, path, text, !existing, !!opts.dryRun, { source: read, ledger, onLedger, message });
521
+ const entry: RecordEntry = { ...(existing ?? emptyEntry(path)), id, path, state, data, valid: true, reasons: [], warnings: wrote.warnings };
522
+ return done(entry, { reused: false, written: !opts.dryRun, ledger: wrote.ledger, ...(opts.dryRun ? { text } : {}) });
417
523
  } catch (err) {
418
524
  return failure("ask", err);
419
525
  }
420
526
  }
421
527
 
422
528
  function emptyEntry(path: string): RecordEntry {
423
- return { id: null, path, state: null, valid: true, reasons: [], supersededBy: null, data: null, assets: [], warnings: [], digest: "" };
529
+ return { id: null, path, state: null, valid: true, reasons: [], supersededBy: null, remediatedBy: [], data: null, assets: [], warnings: [], digest: "" };
424
530
  }
425
531
 
426
532
  // ── points answer ────────────────────────────────────────────────────────────
@@ -453,9 +559,16 @@ function policyFor(root: string): TrustPolicy {
453
559
  * leaving out anyone holding the agent role in the trust policy at base, and,
454
560
  * when the quorum names roles, anyone holding none of them.
455
561
  */
456
- export function tallyQuorum(by: string[], quorum: { count: number; roles?: string[] }, policy: TrustPolicy): { counted: string[]; left: { name: string; why: string }[]; met: boolean } {
562
+ export function tallyQuorum(
563
+ by: string[],
564
+ quorum: { count: number; roles?: string[] },
565
+ policy: TrustPolicy,
566
+ /** The steward that asked the question (#2749): it never counts toward the answer. */
567
+ steward?: string,
568
+ ): { counted: string[]; left: { name: string; why: string }[]; met: boolean } {
457
569
  const holders = (role: string) => new Set((policy.roles[role] ?? []).map(normalisePrincipal));
458
570
  const agents = holders(AGENT_ROLE);
571
+ const asker = steward !== undefined ? normalisePrincipal(steward) : undefined;
459
572
  const counted: string[] = [];
460
573
  const seen = new Set<string>();
461
574
  const left: { name: string; why: string }[] = [];
@@ -463,6 +576,10 @@ export function tallyQuorum(by: string[], quorum: { count: number; roles?: strin
463
576
  const p = normalisePrincipal(name);
464
577
  if (p === "" || seen.has(p)) continue;
465
578
  seen.add(p);
579
+ if (asker !== undefined && p === asker) {
580
+ left.push({ name, why: "is the steward that asked" });
581
+ continue;
582
+ }
466
583
  if (agents.has(p)) {
467
584
  left.push({ name, why: "holds the agent role" });
468
585
  continue;
@@ -484,20 +601,34 @@ export function tallyQuorum(by: string[], quorum: { count: number; roles?: strin
484
601
  */
485
602
  export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWriteDocument> {
486
603
  try {
604
+ // A steward waits on a question and never answers one (#2749), as it never
605
+ // clears a gate: the answer has to come from a person, through hud or a
606
+ // shell, not from the steward's own turn or a process it started.
607
+ const turn = currentStewardTurn();
608
+ if (turn) {
609
+ throw new PointsWriteError(
610
+ "answer-in-steward-turn",
611
+ `this is the steward ${turn.steward}'s turn, and a steward never answers a decision point: a person answers ${opts.id} through hud or \`chant workspace points answer\` at a shell`,
612
+ );
613
+ }
487
614
  const kinds = await answerKindFiles(opts.cwd, opts.kind);
488
615
  if (kinds.length === 0) throw new PointsWriteError("points-undeclared", "no record kind with an answers block is declared: name one with --kind, or declare one in chant.workspace.json");
489
- let found: { opened: OpenedPoints; before: RecordEntry[]; target: RecordEntry & { data: Record<string, unknown> } } | undefined;
616
+ let found:
617
+ | { opened: OpenedPoints; before: RecordEntry[]; read: Awaited<ReturnType<typeof readWithLedger>>; target: RecordEntry & { data: Record<string, unknown> } }
618
+ | undefined;
490
619
  for (const k of kinds) {
491
620
  const opened = await openAnswers(k, opts.cwd);
492
- const before = await readAll(opened.o, opened.o.source);
493
- const target = before.find((e) => e.id === opts.id);
621
+ // A question a steward asked is on the ledger (#2786), and people's answer goes back there.
622
+ const read = await readWithLedger(opened.o);
623
+ const target = read.before.find((e) => e.id === opts.id);
494
624
  if (target && target.data !== null) {
495
- found = { opened, before, target: target as RecordEntry & { data: Record<string, unknown> } };
625
+ found = { opened, before: read.before, read, target: target as RecordEntry & { data: Record<string, unknown> } };
496
626
  break;
497
627
  }
498
628
  }
499
629
  if (!found) throw new PointsWriteError("record-not-found", `no answer record has id ${opts.id}`);
500
630
  const { opened, before, target } = found;
631
+ const ledger = found.read.ledger;
501
632
  const { o } = opened;
502
633
  const d = target.data;
503
634
  if (target.state === "answered") throw new PointsWriteError("record-closed", `${opts.id} is answered, and an answer never changes: ask again with other inputs, or change the point, which asks the question anew`);
@@ -511,7 +642,7 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
511
642
  throw new PointsWriteError("answer-not-candidate", `${opts.id} takes one of ${allowed.map((c) => JSON.stringify(c)).join(", ")}, not ${JSON.stringify(opts.answer)}`);
512
643
  }
513
644
  const quorum = quorumOf(point);
514
- const tally = tallyQuorum(opts.by, quorum, policyFor(o.root));
645
+ const tally = tallyQuorum(opts.by, quorum, policyFor(o.root), askedByOf(d)?.steward);
515
646
  if (!tally.met) {
516
647
  const got = tally.counted.length;
517
648
  const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
@@ -555,8 +686,10 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
555
686
  fields.title = titleFor(point, subject, "answered", value);
556
687
  const data = recordData(o, fields);
557
688
  const text = renderRecord(data, body(point, String(fields.title)));
558
- const warnings = await write(o, before, target.path, text, false, !!opts.dryRun);
559
- const entry: RecordEntry = { ...target, state: "answered", data, valid: true, reasons: [], warnings };
689
+ const onLedger = ledger.byPath.has(target.path);
690
+ const message = `Decision point ${name}: answered ${show(value, type)} by ${tally.counted.join(", ")}`;
691
+ const wrote = await write(o, before, target.path, text, false, !!opts.dryRun, { source: found.read.source, ledger, onLedger, message });
692
+ const entry: RecordEntry = { ...target, state: "answered", data, valid: true, reasons: [], warnings: wrote.warnings };
560
693
  return {
561
694
  $schema: POINTS_WRITE_SCHEMA_ID,
562
695
  contract: POINTS_WRITE_CONTRACT_VERSION,
@@ -567,7 +700,7 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
567
700
  reused: false,
568
701
  written: !opts.dryRun,
569
702
  dryRun: !!opts.dryRun,
570
- question: questionView(entry, point)!,
703
+ question: questionView(entry, point, onLedger ? (wrote.ledger ?? ledger.byPath.get(target.path)?.ledger ?? null) : null)!,
571
704
  ...(opts.dryRun ? { text } : {}),
572
705
  };
573
706
  } catch (err) {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://intentius.io/chant/schemas/workspace/decision-points/v1/decision-points.schema.json",
4
4
  "title": "Decision points",
5
- "description": "A workspace's decision points (ws-058, #2738): the recurring questions it asks of its own graph, declared as data in a JSON file that an answer record kind names in `answers.points`. Each point is a typed question (`noul`, `choice` with 2 to 255 options, or `score` with 2 to 10 ordered levels, the question types of the POST /v1/systemone wire format, #2491), the inputs it reads, each named as a read-contract output, and an ordered chain of deciders: `table` rows, `model` deciders with a backend, a pinned model id and a threshold, and a `quorum` of people, always last. Taken from chud's decision-points.schema.json; `boolean` is read as `noul`. What JSON Schema cannot say is checked in code (`points.ts`): an input's output is one the read contract has, a table row tests declared inputs and answers a candidate, a field belongs to its decider's kind, a model id is pinned rather than an alias, and the chain ends in its one quorum. Unknown fields are refused, except fields whose names start with x-.",
5
+ "description": "A workspace's decision points (ws-058, #2738): the recurring questions it asks of its own graph, declared as data in a JSON file that an answer record kind names in `answers.points`. Each point is a typed question (`noul`, `choice` with 2 to 255 options, or `score` with 2 to 10 ordered levels, the question types of the POST /v1/systemone wire format, #2491), the inputs it reads, each named as a read-contract output, and an ordered chain of deciders: `table` rows, `model` deciders with a backend, a pinned model id and a threshold, and a `quorum` of people, always last. What JSON Schema cannot say is checked in code (`points.ts`): an input's output is one the read contract has, a table row tests declared inputs and answers a candidate, a field belongs to its decider's kind, a model id is pinned rather than an alias, and the chain ends in its one quorum. Unknown fields are refused, except fields whose names start with x-.",
6
6
  "type": "object",
7
7
  "required": ["points"],
8
8
  "properties": {
@@ -44,7 +44,7 @@
44
44
  "type": "object",
45
45
  "required": ["type", "instructions", "criteria"],
46
46
  "properties": {
47
- "type": { "enum": ["noul", "boolean", "choice", "score"], "description": "boolean is chud's name for noul, read as noul." },
47
+ "type": { "enum": ["noul", "choice", "score"] },
48
48
  "instructions": { "$ref": "#/$defs/text" },
49
49
  "criteria": {
50
50
  "description": "The candidates. noul: {true, false} descriptions. choice: option to description, 2 to 255 options. score: 2 to 10 ordered levels.",
@@ -54,7 +54,7 @@
54
54
  "additionalProperties": false,
55
55
  "allOf": [
56
56
  {
57
- "if": { "properties": { "type": { "enum": ["noul", "boolean"] } } },
57
+ "if": { "properties": { "type": { "const": "noul" } } },
58
58
  "then": {
59
59
  "properties": {
60
60
  "criteria": {
@@ -63,6 +63,13 @@
63
63
  "$ref": "#/$defs/host"
64
64
  }
65
65
  },
66
+ "diagrams": {
67
+ "type": "array",
68
+ "description": "The workspace's own diagram artifacts (#2764): the diagrams that belong to no one member, such as a diagrams directory at the root. A member names its own in its diagrams. chant workspace ls lists every declared diagram, and chant workspace check reports one whose render is stale for its source when the entry records a sourceHash. Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
69
+ "items": {
70
+ "$ref": "#/$defs/diagram"
71
+ }
72
+ },
66
73
  "checks": {
67
74
  "type": "object",
68
75
  "description": "Severity for the declaration checks that allow it, keyed by WSP id (#2535, ws-028): error, warning, info or off. chant workspace check refuses an id it does not know, and one whose severity is fixed.",
@@ -72,6 +79,28 @@
72
79
  "additionalProperties": {
73
80
  "$ref": "#/$defs/severity"
74
81
  }
82
+ },
83
+ "changes": {
84
+ "type": "object",
85
+ "additionalProperties": false,
86
+ "description": "The forward coverage check, chant workspace check --changes <base>..<head> (#2773): each path a diff changes is mapped to the current records whose constrains cover it. severity says what a finding does: off reports none, warn (the default) reports them and passes, fail reports them and fails. ignore lists globs over files whose changes need no record, such as lockfiles and generated output. Added in schema 1 by chant 0.92.0, so a declaration that uses it sets minReader to 0.92.0 or newer.",
87
+ "properties": {
88
+ "severity": {
89
+ "enum": [
90
+ "off",
91
+ "warn",
92
+ "fail"
93
+ ],
94
+ "description": "What a change-uncovered or change-out-of-scope finding does. warn when absent."
95
+ },
96
+ "ignore": {
97
+ "type": "array",
98
+ "description": "Globs over file paths from the workspace root: * and ? within one segment, ** across segments. A changed path one matches is listed as ignored and never reported.",
99
+ "items": {
100
+ "$ref": "#/$defs/fileGlob"
101
+ }
102
+ }
103
+ }
75
104
  }
76
105
  },
77
106
  "patternProperties": {
@@ -276,6 +305,13 @@
276
305
  "$ref": "#/$defs/recordKind"
277
306
  }
278
307
  },
308
+ "diagrams": {
309
+ "type": "array",
310
+ "description": "The diagram artifacts this member holds (#2764). chant workspace ls lists every declared diagram, and chant workspace check reports one whose render is stale for its source when the entry records a sourceHash. Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
311
+ "items": {
312
+ "$ref": "#/$defs/diagram"
313
+ }
314
+ },
279
315
  "box": {
280
316
  "$ref": "#/$defs/box"
281
317
  },
@@ -366,8 +402,13 @@
366
402
  },
367
403
  "box": {
368
404
  "type": "object",
369
- "description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
405
+ "description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). A member that builds the fountain lexicon's Box declares the callback token fountain gives its persistent sandbox as the capability fountain-callback, brokered by fountain, with scope owner, or chant workspace check fails (WSP125, #2780). Its intent names the decision record that says what the box is for (#2850): chant workspace status --json reports the record's state and answer, and chant workspace check fails when no decision record has the id (WSP126) and warns when the record constrains no member or path of this workspace (WSP127, #2857). Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
370
406
  "properties": {
407
+ "intent": {
408
+ "type": "string",
409
+ "minLength": 1,
410
+ "description": "The id of the decision record that says what the box is for (#2850), a record of a declared kind named decision. A new box starts as a question: the record is proposed with a null choice until the person who answers it decides it. The record constrains a member or path of this workspace, with member:<a member's name> or a path: entry at, above or inside a member's directory. It need not be this member: the intent can constrain the app the box runs when that app is another member (#2857). When it constrains this member, chant workspace graph --intent shows it for the box's files. Added by chant 0.94.0, so a declaration that uses it sets minReader to 0.94.0 or newer."
411
+ },
371
412
  "capabilities": {
372
413
  "type": "array",
373
414
  "description": "Each capability the box reaches outside itself, named once. None when omitted.",
@@ -493,6 +534,78 @@
493
534
  },
494
535
  "additionalProperties": false
495
536
  },
537
+ "diagram": {
538
+ "type": "object",
539
+ "description": "A diagram artifact the workspace declares (#2764): a rendered image with a source, pinned to the renderer that made it. chant never runs the renderer.",
540
+ "required": [
541
+ "name",
542
+ "title",
543
+ "render",
544
+ "renderer"
545
+ ],
546
+ "properties": {
547
+ "name": {
548
+ "$ref": "#/$defs/name",
549
+ "description": "Unique across the declaration."
550
+ },
551
+ "title": {
552
+ "type": "string",
553
+ "minLength": 1,
554
+ "description": "Shown above the diagram."
555
+ },
556
+ "source": {
557
+ "oneOf": [
558
+ { "$ref": "#/$defs/path" },
559
+ { "type": "null" }
560
+ ],
561
+ "description": "The source file, from the workspace root, with / separators. Null for an SVG with no source. Default null."
562
+ },
563
+ "render": {
564
+ "$ref": "#/$defs/path",
565
+ "description": "The rendered image, from the workspace root, with / separators."
566
+ },
567
+ "renderer": {
568
+ "$ref": "#/$defs/diagramRenderer"
569
+ },
570
+ "sourceHash": {
571
+ "type": ["string", "null"],
572
+ "pattern": "^[0-9a-f]{64}$",
573
+ "description": "sha256 hex of the source's bytes when render was last produced from it. chant workspace check compares this against the source in the tree read and reports drift (WSP133) on a mismatch; without it, or without a source, the render is never checked for drift. Default null."
574
+ }
575
+ },
576
+ "patternProperties": {
577
+ "^x-": true
578
+ },
579
+ "additionalProperties": false
580
+ },
581
+ "diagramRenderer": {
582
+ "type": "object",
583
+ "description": "The renderer a diagram's render was made with. chant records the pin; it never installs or runs the renderer (ws-052).",
584
+ "required": [
585
+ "tool",
586
+ "version"
587
+ ],
588
+ "properties": {
589
+ "tool": {
590
+ "enum": ["d2", "mermaid", "graphviz"]
591
+ },
592
+ "version": {
593
+ "$ref": "#/$defs/version",
594
+ "description": "The exact release the render was made with, such as 0.9.0."
595
+ },
596
+ "args": {
597
+ "type": "array",
598
+ "description": "Passed before the input and output paths, in order, such as [\"--layout=elk\", \"--theme=0\", \"--pad=40\", \"--omit-version\"]. Empty when absent.",
599
+ "items": {
600
+ "type": "string"
601
+ }
602
+ }
603
+ },
604
+ "patternProperties": {
605
+ "^x-": true
606
+ },
607
+ "additionalProperties": false
608
+ },
496
609
  "generatedFile": {
497
610
  "type": "object",
498
611
  "description": "One generated file of a member (#2524 D14, #2541).",
@@ -641,6 +754,11 @@
641
754
  "type": "integer",
642
755
  "minimum": 1,
643
756
  "maximum": 65535
757
+ },
758
+ "fileGlob": {
759
+ "type": "string",
760
+ "description": "A glob over file paths, relative to the workspace root: * and ? within one segment, ** across segments, [...] classes and {a,b} alternatives. No leading /, no . or .. segments.",
761
+ "pattern": "^(?!\\.\\.?(/|$))[^/\\\\]+(/(?!\\.\\.?(/|$))[^/\\\\]+)*$"
644
762
  }
645
763
  }
646
764
  }
@@ -6,6 +6,7 @@ import { afterAll, describe, expect, test } from "vitest";
6
6
  import {
7
7
  compareVersions,
8
8
  DECLARATION_SCHEMA_ID,
9
+ declaredDiagrams,
9
10
  ownerOf,
10
11
  parseDeclaration,
11
12
  readDeclaration,
@@ -345,3 +346,60 @@ describe("generated files (#2541)", () => {
345
346
  expect(failure(base([{ name: "g", kind: "examples", glob: "examples/*", generated: [] }])).message).toContain('unknown field "generated"');
346
347
  });
347
348
  });
349
+
350
+ describe("diagram artifacts (#2764)", () => {
351
+ const renderer = { tool: "d2", version: "0.9.0", args: ["--layout=elk", "--theme=0", "--pad=40", "--omit-version"] };
352
+ const diagram = (extra: Record<string, unknown> = {}) => ({
353
+ name: "architecture",
354
+ title: "Studio architecture",
355
+ source: "docs/diagrams/architecture.d2",
356
+ render: "docs/diagrams/architecture.svg",
357
+ renderer,
358
+ ...extra,
359
+ });
360
+ const member = (diagrams: unknown[], extra: Record<string, unknown> = {}) => ({ name: "docs", dir: "docs", kind: "other", because: "the docs site", diagrams, ...extra });
361
+
362
+ test("a member's diagram is read with the workspace-relative source and render, and a null source is the default", () => {
363
+ const d = parse(base([member([diagram()])]));
364
+ expect(d.members[0].diagrams).toEqual([
365
+ { name: "architecture", title: "Studio architecture", source: "docs/diagrams/architecture.d2", render: "docs/diagrams/architecture.svg", renderer, sourceHash: null, member: "docs", pointer: "/members/0/diagrams/0" },
366
+ ]);
367
+ const noSource = parse(base([member([diagram({ source: undefined })])]));
368
+ expect(noSource.members[0].diagrams[0].source).toBeNull();
369
+ });
370
+
371
+ test("the workspace's own diagrams are in the top-level diagrams, with member null", () => {
372
+ const d = parse(base([{ name: "x", dir: "x", kind: "chant" }], { diagrams: [diagram({ name: "boundary" })] }));
373
+ expect(d.diagrams).toEqual([{ ...diagram({ name: "boundary" }), sourceHash: null, member: null, pointer: "/diagrams/0" }]);
374
+ expect(declaredDiagrams(d)).toEqual(d.diagrams);
375
+ });
376
+
377
+ test("declaredDiagrams lists the workspace's own first, then each member's, in file order", () => {
378
+ const d = parse(base([member([diagram()]), { name: "app", dir: "app", kind: "chant" }], { diagrams: [diagram({ name: "boundary", render: "docs/diagrams/boundary.svg" })] }));
379
+ expect(declaredDiagrams(d).map((x) => x.name)).toEqual(["boundary", "architecture"]);
380
+ });
381
+
382
+ test("a diagram name is unique across the declaration, workspace and members together", () => {
383
+ const clash = failure(base([member([diagram()])], { diagrams: [diagram()] }));
384
+ expect(clash.code).toBe("declaration-invalid");
385
+ expect(clash.message).toContain('the diagram name "architecture" is already used by the diagram at /diagrams/0');
386
+ });
387
+
388
+ test("renderer.tool is a closed list, and title, render and renderer are required", () => {
389
+ expect(failure(base([member([diagram({ renderer: { tool: "mscgen", version: "1.0.0" } })])])).code).toBe("declaration-invalid");
390
+ expect(failure(base([member([{ name: "a", source: null, render: "a.svg", renderer }])])).message).toContain('missing required field "title"');
391
+ expect(failure(base([member([{ name: "a", title: "A", source: null, renderer }])])).message).toContain('missing required field "render"');
392
+ expect(failure(base([member([{ name: "a", title: "A", source: null, render: "a.svg" }])])).message).toContain('missing required field "renderer"');
393
+ });
394
+
395
+ test("a sourceHash is a 64-character lowercase hex string, or absent", () => {
396
+ const hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
397
+ expect(parse(base([member([diagram({ sourceHash: hash })])])).members[0].diagrams[0].sourceHash).toBe(hash);
398
+ expect(failure(base([member([diagram({ sourceHash: "not-a-hash" })])])).code).toBe("declaration-invalid");
399
+ });
400
+
401
+ test("source and render stay inside the workspace, like every other path", () => {
402
+ expect(failure(base([member([diagram({ source: "/etc/passwd" })])])).code).toBe("declaration-invalid");
403
+ expect(failure(base([member([diagram({ render: "../out.svg" })])])).code).toBe("declaration-invalid");
404
+ });
405
+ });