@intentius/chant 0.90.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 (402) 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/components.d.ts +8 -0
  4. package/dist/cli/handlers/components.d.ts.map +1 -1
  5. package/dist/cli/handlers/graph.d.ts.map +1 -1
  6. package/dist/cli/handlers/operator.d.ts +20 -0
  7. package/dist/cli/handlers/operator.d.ts.map +1 -1
  8. package/dist/cli/handlers/run.d.ts.map +1 -1
  9. package/dist/cli/main.d.ts.map +1 -1
  10. package/dist/cli/mcp/workspace-tools.d.ts +7 -4
  11. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  12. package/dist/cli/plugins.d.ts.map +1 -1
  13. package/dist/cli/registry.d.ts +42 -1
  14. package/dist/cli/registry.d.ts.map +1 -1
  15. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  16. package/dist/components/verbs/vuln-scan.d.ts +72 -0
  17. package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
  18. package/dist/config.d.ts +22 -0
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/lexicon-module.d.ts +22 -0
  21. package/dist/lexicon-module.d.ts.map +1 -1
  22. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  23. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  24. package/dist/lifecycle/git.d.ts +40 -5
  25. package/dist/lifecycle/git.d.ts.map +1 -1
  26. package/dist/lifecycle/lease.d.ts +72 -16
  27. package/dist/lifecycle/lease.d.ts.map +1 -1
  28. package/dist/lifecycle/member-ledger.d.ts +3 -2
  29. package/dist/lifecycle/member-ledger.d.ts.map +1 -1
  30. package/dist/lifecycle/plan-ledger.d.ts +114 -0
  31. package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
  32. package/dist/lifecycle/run-ledger.d.ts +2 -0
  33. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  34. package/dist/lifecycle/symptoms.d.ts +51 -0
  35. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  36. package/dist/lifecycle/work-lease.d.ts +140 -0
  37. package/dist/lifecycle/work-lease.d.ts.map +1 -0
  38. package/dist/lint/rules/index.d.ts +4 -2
  39. package/dist/lint/rules/index.d.ts.map +1 -1
  40. package/dist/lint/rules/op/index.d.ts +14 -3
  41. package/dist/lint/rules/op/index.d.ts.map +1 -1
  42. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  43. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  44. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  45. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  46. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  47. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  48. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  49. package/dist/op/activities/activity-contracts.d.ts +203 -2
  50. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  51. package/dist/op/activities/apply.d.ts.map +1 -1
  52. package/dist/op/activities/change-coverage.d.ts +44 -0
  53. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  54. package/dist/op/activities/converge.d.ts +24 -1
  55. package/dist/op/activities/converge.d.ts.map +1 -1
  56. package/dist/op/activities/decide.d.ts +107 -0
  57. package/dist/op/activities/decide.d.ts.map +1 -0
  58. package/dist/op/activities/index.d.ts +10 -0
  59. package/dist/op/activities/index.d.ts.map +1 -1
  60. package/dist/op/activities/shell.d.ts +32 -0
  61. package/dist/op/activities/shell.d.ts.map +1 -1
  62. package/dist/op/activities/source-release.d.ts +86 -0
  63. package/dist/op/activities/source-release.d.ts.map +1 -0
  64. package/dist/op/activities/source-rollback.d.ts +100 -0
  65. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  66. package/dist/op/activities/work-evidence.d.ts +42 -0
  67. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  68. package/dist/op/activity-contract.d.ts +1 -0
  69. package/dist/op/activity-contract.d.ts.map +1 -1
  70. package/dist/op/activity-profiles.d.ts +16 -0
  71. package/dist/op/activity-profiles.d.ts.map +1 -1
  72. package/dist/op/builders.d.ts +84 -2
  73. package/dist/op/builders.d.ts.map +1 -1
  74. package/dist/op/composites/converge-op.d.ts +31 -4
  75. package/dist/op/composites/converge-op.d.ts.map +1 -1
  76. package/dist/op/decide-backend.d.ts +116 -0
  77. package/dist/op/decide-backend.d.ts.map +1 -0
  78. package/dist/op/decide-config.d.ts +120 -0
  79. package/dist/op/decide-config.d.ts.map +1 -0
  80. package/dist/op/decide-read-inputs.d.ts +40 -0
  81. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  82. package/dist/op/discover.d.ts +25 -0
  83. package/dist/op/discover.d.ts.map +1 -1
  84. package/dist/op/gate-wait.d.ts +43 -0
  85. package/dist/op/gate-wait.d.ts.map +1 -0
  86. package/dist/op/gate.d.ts +5 -2
  87. package/dist/op/gate.d.ts.map +1 -1
  88. package/dist/op/index.d.ts +18 -5
  89. package/dist/op/index.d.ts.map +1 -1
  90. package/dist/op/lifecycle-receipt-store.d.ts +34 -0
  91. package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
  92. package/dist/op/local-executor.d.ts +48 -4
  93. package/dist/op/local-executor.d.ts.map +1 -1
  94. package/dist/op/local-output.d.ts +2 -0
  95. package/dist/op/local-output.d.ts.map +1 -1
  96. package/dist/op/op-ir.d.ts +12 -1
  97. package/dist/op/op-ir.d.ts.map +1 -1
  98. package/dist/op/op-verb-class.d.ts.map +1 -1
  99. package/dist/op/operator.d.ts +126 -1
  100. package/dist/op/operator.d.ts.map +1 -1
  101. package/dist/op/runtime.d.ts +46 -5
  102. package/dist/op/runtime.d.ts.map +1 -1
  103. package/dist/op/runtimes/local.d.ts.map +1 -1
  104. package/dist/op/source-archive.d.ts +63 -0
  105. package/dist/op/source-archive.d.ts.map +1 -0
  106. package/dist/op/step-output-ref.d.ts +2 -2
  107. package/dist/op/step-output-ref.d.ts.map +1 -1
  108. package/dist/op/steward-points.d.ts +136 -0
  109. package/dist/op/steward-points.d.ts.map +1 -0
  110. package/dist/op/steward-turn.d.ts +53 -0
  111. package/dist/op/steward-turn.d.ts.map +1 -0
  112. package/dist/op/steward.d.ts +161 -0
  113. package/dist/op/steward.d.ts.map +1 -0
  114. package/dist/op/types.d.ts +58 -0
  115. package/dist/op/types.d.ts.map +1 -1
  116. package/dist/op/work-lease-decl.d.ts +18 -0
  117. package/dist/op/work-lease-decl.d.ts.map +1 -0
  118. package/dist/op/work-lease-run.d.ts +173 -0
  119. package/dist/op/work-lease-run.d.ts.map +1 -0
  120. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  121. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  122. package/dist/workspace/answers-ledger.d.ts +76 -0
  123. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  124. package/dist/workspace/box-isolation.d.ts +99 -0
  125. package/dist/workspace/box-isolation.d.ts.map +1 -0
  126. package/dist/workspace/changes-cli.d.ts +16 -0
  127. package/dist/workspace/changes-cli.d.ts.map +1 -0
  128. package/dist/workspace/changes.d.ts +200 -0
  129. package/dist/workspace/changes.d.ts.map +1 -0
  130. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  131. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  132. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  133. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  134. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  135. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  136. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  137. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  138. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  139. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  140. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  141. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  142. package/dist/workspace/chant-migrations.d.ts +99 -0
  143. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  144. package/dist/workspace/checks/box-isolation.d.ts +18 -0
  145. package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
  146. package/dist/workspace/checks/boxes.d.ts +104 -0
  147. package/dist/workspace/checks/boxes.d.ts.map +1 -0
  148. package/dist/workspace/checks/diagrams.d.ts +31 -0
  149. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  150. package/dist/workspace/checks/records.d.ts +8 -0
  151. package/dist/workspace/checks/records.d.ts.map +1 -1
  152. package/dist/workspace/checks.d.ts +13 -1
  153. package/dist/workspace/checks.d.ts.map +1 -1
  154. package/dist/workspace/compose-graph.d.ts +2 -0
  155. package/dist/workspace/compose-graph.d.ts.map +1 -1
  156. package/dist/workspace/conformance/index.d.ts.map +1 -1
  157. package/dist/workspace/decide.d.ts +229 -0
  158. package/dist/workspace/decide.d.ts.map +1 -0
  159. package/dist/workspace/decision-points.schema.json +137 -0
  160. package/dist/workspace/declaration.d.ts +103 -0
  161. package/dist/workspace/declaration.d.ts.map +1 -1
  162. package/dist/workspace/declaration.schema.json +285 -0
  163. package/dist/workspace/declared-kinds.d.ts +32 -0
  164. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  165. package/dist/workspace/generated-files.d.ts +7 -0
  166. package/dist/workspace/generated-files.d.ts.map +1 -1
  167. package/dist/workspace/intent.d.ts.map +1 -1
  168. package/dist/workspace/lineage-check.d.ts +3 -0
  169. package/dist/workspace/lineage-check.d.ts.map +1 -1
  170. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  171. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  172. package/dist/workspace/ls.d.ts +39 -1
  173. package/dist/workspace/ls.d.ts.map +1 -1
  174. package/dist/workspace/point-answer.schema.json +95 -0
  175. package/dist/workspace/points-cli.d.ts +113 -0
  176. package/dist/workspace/points-cli.d.ts.map +1 -0
  177. package/dist/workspace/points.d.ts +320 -0
  178. package/dist/workspace/points.d.ts.map +1 -0
  179. package/dist/workspace/reason-codes.d.ts +38 -0
  180. package/dist/workspace/reason-codes.d.ts.map +1 -1
  181. package/dist/workspace/record-assets.d.ts.map +1 -1
  182. package/dist/workspace/records-cli.d.ts +23 -1
  183. package/dist/workspace/records-cli.d.ts.map +1 -1
  184. package/dist/workspace/records-close.d.ts +1 -1
  185. package/dist/workspace/records-close.d.ts.map +1 -1
  186. package/dist/workspace/records-write.d.ts +11 -5
  187. package/dist/workspace/records-write.d.ts.map +1 -1
  188. package/dist/workspace/records.d.ts +42 -6
  189. package/dist/workspace/records.d.ts.map +1 -1
  190. package/dist/workspace/status-gates.d.ts +7 -2
  191. package/dist/workspace/status-gates.d.ts.map +1 -1
  192. package/dist/workspace/status-stewards.d.ts +179 -0
  193. package/dist/workspace/status-stewards.d.ts.map +1 -0
  194. package/dist/workspace/status.d.ts +68 -1
  195. package/dist/workspace/status.d.ts.map +1 -1
  196. package/dist/workspace/work-cli.d.ts +189 -0
  197. package/dist/workspace/work-cli.d.ts.map +1 -0
  198. package/dist/workspace/work-evidence.d.ts +74 -0
  199. package/dist/workspace/work-evidence.d.ts.map +1 -0
  200. package/dist/workspace/work.d.ts +43 -1
  201. package/dist/workspace/work.d.ts.map +1 -1
  202. package/package.json +1 -1
  203. package/src/cli/commands/audit.test.ts +9 -1
  204. package/src/cli/commands/build.ts +3 -2
  205. package/src/cli/commands/lint.ts +7 -1
  206. package/src/cli/handlers/components.test.ts +93 -0
  207. package/src/cli/handlers/components.ts +44 -3
  208. package/src/cli/handlers/graph.test.ts +20 -0
  209. package/src/cli/handlers/graph.ts +16 -2
  210. package/src/cli/handlers/operator.ts +141 -4
  211. package/src/cli/handlers/run.test.ts +173 -2
  212. package/src/cli/handlers/run.ts +174 -3
  213. package/src/cli/main.test.ts +50 -0
  214. package/src/cli/main.ts +97 -2
  215. package/src/cli/mcp/workspace-tools.test.ts +48 -19
  216. package/src/cli/mcp/workspace-tools.ts +110 -8
  217. package/src/cli/plugins.ts +11 -3
  218. package/src/cli/registry.ts +42 -1
  219. package/src/codegen/pinned-upgrade.ts +2 -1
  220. package/src/codegen/release-wiring.test.ts +49 -2
  221. package/src/components/capability-plugin-loader.ts +3 -3
  222. package/src/components/cli-support.ts +2 -2
  223. package/src/components/verbs/vuln-scan.test.ts +124 -1
  224. package/src/components/verbs/vuln-scan.ts +142 -1
  225. package/src/config.ts +11 -0
  226. package/src/lexicon-module.ts +56 -1
  227. package/src/lexicon-package.test.ts +96 -0
  228. package/src/lifecycle/converge-ledger.ts +20 -0
  229. package/src/lifecycle/git.ts +65 -15
  230. package/src/lifecycle/lease.test.ts +22 -0
  231. package/src/lifecycle/lease.ts +133 -29
  232. package/src/lifecycle/member-ledger.ts +3 -2
  233. package/src/lifecycle/plan-ledger.test.ts +148 -0
  234. package/src/lifecycle/plan-ledger.ts +158 -0
  235. package/src/lifecycle/run-ledger.ts +6 -0
  236. package/src/lifecycle/symptoms.ts +95 -0
  237. package/src/lifecycle/work-lease.test.ts +236 -0
  238. package/src/lifecycle/work-lease.ts +426 -0
  239. package/src/lint/rules/index.ts +6 -2
  240. package/src/lint/rules/op/index.ts +17 -4
  241. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  242. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  243. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  244. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  245. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  246. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  247. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  248. package/src/op/activities/activity-contracts.ts +190 -1
  249. package/src/op/activities/apply.ts +7 -6
  250. package/src/op/activities/change-coverage.ts +60 -0
  251. package/src/op/activities/converge.test.ts +55 -1
  252. package/src/op/activities/converge.ts +208 -29
  253. package/src/op/activities/decide.live.test.ts +78 -0
  254. package/src/op/activities/decide.test.ts +300 -0
  255. package/src/op/activities/decide.ts +267 -0
  256. package/src/op/activities/index.ts +21 -0
  257. package/src/op/activities/shell.test.ts +91 -1
  258. package/src/op/activities/shell.ts +91 -2
  259. package/src/op/activities/source-release.test.ts +118 -0
  260. package/src/op/activities/source-release.ts +168 -0
  261. package/src/op/activities/source-rollback.test.ts +131 -0
  262. package/src/op/activities/source-rollback.ts +239 -0
  263. package/src/op/activities/work-evidence.ts +62 -0
  264. package/src/op/activity-contract-registry.test.ts +2 -0
  265. package/src/op/activity-contract-registry.ts +2 -2
  266. package/src/op/activity-contract.test.ts +12 -0
  267. package/src/op/activity-contract.ts +6 -1
  268. package/src/op/activity-profiles.ts +29 -0
  269. package/src/op/activity-registry.test.ts +1 -1
  270. package/src/op/activity-registry.ts +2 -2
  271. package/src/op/builders.ts +124 -5
  272. package/src/op/command-gate.test.ts +192 -0
  273. package/src/op/composites/converge-op.test.ts +27 -1
  274. package/src/op/composites/converge-op.ts +69 -15
  275. package/src/op/decide-backend.test.ts +44 -0
  276. package/src/op/decide-backend.ts +212 -0
  277. package/src/op/decide-config.test.ts +52 -0
  278. package/src/op/decide-config.ts +80 -0
  279. package/src/op/decide-read-inputs.ts +117 -0
  280. package/src/op/discover.ts +71 -0
  281. package/src/op/gate-approval.ts +2 -2
  282. package/src/op/gate-wait.ts +57 -0
  283. package/src/op/gate.ts +8 -3
  284. package/src/op/generate-pipeline.ts +2 -2
  285. package/src/op/index.ts +34 -6
  286. package/src/op/lifecycle-receipt-store.test.ts +60 -0
  287. package/src/op/lifecycle-receipt-store.ts +61 -0
  288. package/src/op/local-executor.ts +367 -33
  289. package/src/op/local-output.test.ts +33 -0
  290. package/src/op/local-output.ts +33 -2
  291. package/src/op/op-ir.test.ts +10 -0
  292. package/src/op/op-ir.ts +18 -0
  293. package/src/op/op-verb-class.ts +1 -0
  294. package/src/op/operator.ts +360 -10
  295. package/src/op/runtime.ts +37 -8
  296. package/src/op/runtimes/local.ts +6 -1
  297. package/src/op/source-archive.ts +161 -0
  298. package/src/op/step-output-ref.ts +6 -2
  299. package/src/op/steward-points.test.ts +313 -0
  300. package/src/op/steward-points.ts +194 -0
  301. package/src/op/steward-turn.ts +79 -0
  302. package/src/op/steward.test.ts +296 -0
  303. package/src/op/steward.ts +277 -0
  304. package/src/op/types.ts +60 -0
  305. package/src/op/work-lease-decl.ts +80 -0
  306. package/src/op/work-lease-run.test.ts +345 -0
  307. package/src/op/work-lease-run.ts +395 -0
  308. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  309. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  310. package/src/workspace/answers-ledger.ts +118 -0
  311. package/src/workspace/box-isolation.test.ts +261 -0
  312. package/src/workspace/box-isolation.ts +205 -0
  313. package/src/workspace/changes-cli.ts +56 -0
  314. package/src/workspace/changes.schema.json +472 -0
  315. package/src/workspace/changes.test.ts +315 -0
  316. package/src/workspace/changes.ts +512 -0
  317. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  318. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  319. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  320. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  321. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  322. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  323. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  324. package/src/workspace/chant-migrations.ts +164 -0
  325. package/src/workspace/check-contract.test.ts +5 -1
  326. package/src/workspace/check.schema.json +25 -7
  327. package/src/workspace/checks/box-isolation.ts +68 -0
  328. package/src/workspace/checks/boxes.test.ts +276 -0
  329. package/src/workspace/checks/boxes.ts +442 -0
  330. package/src/workspace/checks/diagrams.test.ts +134 -0
  331. package/src/workspace/checks/diagrams.ts +108 -0
  332. package/src/workspace/checks/records.ts +72 -0
  333. package/src/workspace/checks.test.ts +7 -0
  334. package/src/workspace/checks.ts +27 -3
  335. package/src/workspace/compose-graph.ts +2 -0
  336. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  337. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  338. package/src/workspace/conformance/index.ts +5 -0
  339. package/src/workspace/decide.test.ts +224 -0
  340. package/src/workspace/decide.ts +709 -0
  341. package/src/workspace/decision-points.schema.json +137 -0
  342. package/src/workspace/declaration.schema.json +285 -0
  343. package/src/workspace/declaration.test.ts +58 -0
  344. package/src/workspace/declaration.ts +241 -0
  345. package/src/workspace/declared-kinds.test.ts +5 -5
  346. package/src/workspace/declared-kinds.ts +71 -3
  347. package/src/workspace/generated-files.ts +14 -0
  348. package/src/workspace/graph-cli.ts +1 -1
  349. package/src/workspace/graph.schema.json +9 -1
  350. package/src/workspace/intent.schema.json +12 -3
  351. package/src/workspace/intent.ts +3 -10
  352. package/src/workspace/lineage-check.ts +16 -1
  353. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  354. package/src/workspace/lineage-upgrade.test.ts +70 -0
  355. package/src/workspace/lineage-upgrade.ts +81 -10
  356. package/src/workspace/ls-contract.test.ts +57 -0
  357. package/src/workspace/ls.schema.json +46 -1
  358. package/src/workspace/ls.ts +62 -3
  359. package/src/workspace/point-answer.schema.json +95 -0
  360. package/src/workspace/points-cli.ts +284 -0
  361. package/src/workspace/points-write.schema.json +499 -0
  362. package/src/workspace/points.schema.json +743 -0
  363. package/src/workspace/points.test.ts +264 -0
  364. package/src/workspace/points.ts +564 -0
  365. package/src/workspace/read-contract.test.ts +18 -1
  366. package/src/workspace/reason-codes.test.ts +24 -1
  367. package/src/workspace/reason-codes.ts +49 -0
  368. package/src/workspace/record-assets.test.ts +3 -2
  369. package/src/workspace/record-assets.ts +4 -1
  370. package/src/workspace/records-amend.schema.json +2 -0
  371. package/src/workspace/records-cli.ts +28 -5
  372. package/src/workspace/records-close.schema.json +2 -0
  373. package/src/workspace/records-contract.test.ts +3 -2
  374. package/src/workspace/records-new.schema.json +2 -0
  375. package/src/workspace/records-review.schema.json +2 -0
  376. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  377. package/src/workspace/records-sessions-write.test.ts +2 -34
  378. package/src/workspace/records-write.e2e.test.ts +108 -0
  379. package/src/workspace/records-write.test.ts +32 -41
  380. package/src/workspace/records-write.ts +33 -15
  381. package/src/workspace/records.schema.json +53 -2
  382. package/src/workspace/records.test.ts +48 -3
  383. package/src/workspace/records.ts +142 -6
  384. package/src/workspace/status-contract.test.ts +189 -6
  385. package/src/workspace/status-gates.ts +9 -4
  386. package/src/workspace/status-stewards.ts +329 -0
  387. package/src/workspace/status.schema.json +335 -5
  388. package/src/workspace/status.ts +142 -6
  389. package/src/workspace/work-acceptance.test.ts +274 -0
  390. package/src/workspace/work-cli.e2e.test.ts +180 -0
  391. package/src/workspace/work-cli.ts +429 -0
  392. package/src/workspace/work-evidence.schema.json +212 -0
  393. package/src/workspace/work-evidence.ts +169 -0
  394. package/src/workspace/work-history.schema.json +349 -0
  395. package/src/workspace/work-history.test.ts +156 -0
  396. package/src/workspace/work-lease.schema.json +233 -0
  397. package/src/workspace/work-readiness-chud.test.ts +145 -0
  398. package/src/workspace/work.test.ts +3 -2
  399. package/src/workspace/work.ts +100 -1
  400. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  401. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  402. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -0,0 +1,709 @@
1
+ /**
2
+ * Answers to decision points as records (ws-058, #2739).
3
+ *
4
+ * {@link askPoint} asks a point's deciders for one set of inputs and records
5
+ * the answer in the answer kind's records directory, and {@link answerPoint}
6
+ * records people's answer to an open question. Both write one Markdown record
7
+ * or none, never commit, and go through the checks every records write goes
8
+ * through (`records-write.ts`): the record is read back with the kind's schema
9
+ * before it is written, and no other record may become invalid.
10
+ *
11
+ * - A table's answer is `answered`.
12
+ * - A model's answer at or above its threshold is `proposed`, whatever its
13
+ * confidence, with the probabilities, confidence and threshold, until a
14
+ * person confirms it. Below the threshold the question is `escalated`.
15
+ * - A question no decider before the quorum answered is `escalated`: open for
16
+ * people, with every decider's reason and any model's answer.
17
+ *
18
+ * The same point, declaration and inputs are answered once: the record's id is
19
+ * the point's name and the first 12 hex digits of the inputs hash. Asking again
20
+ * returns the record that is there when it is proposed or answered. An
21
+ * escalated one is asked again, since a backend may answer now, and rewritten
22
+ * only when the chain no longer escalates.
23
+ *
24
+ * chant never calls a model (ws-052): the model call is the caller's
25
+ * {@link ModelAsk}, such as the decide Op activity (#2740), or a backend's
26
+ * response the caller already has (`points ask --response`).
27
+ */
28
+
29
+ import { writeFileSync } from "node:fs";
30
+ import type { ReasonCode } from "./reason-codes";
31
+ import { gitRoot } from "./record-source";
32
+ import { loadRecordKind, normalisePrincipal, RECORD_REASON_CODES, RecordReadError, type RecordEntry, type RecordWarning } from "./records";
33
+ import { declaredKindFiles } from "./records-cli";
34
+ import {
35
+ abs,
36
+ LOAD_ERROR_CODES,
37
+ open,
38
+ pick,
39
+ readAll,
40
+ RecordWriteError,
41
+ renderRecord,
42
+ resolveWriteKind,
43
+ schemaOrder,
44
+ validateWrite,
45
+ type KindView,
46
+ type Opened,
47
+ } from "./records-write";
48
+ import {
49
+ answerId,
50
+ candidates,
51
+ DeciderFailed,
52
+ inputsHash,
53
+ pointOf,
54
+ pointsFileOf,
55
+ pointVersion,
56
+ quorumOf,
57
+ readPointsThrough,
58
+ runChain,
59
+ type ChainResult,
60
+ type Escalation,
61
+ type ModelAsk,
62
+ type Point,
63
+ } from "./points";
64
+ import { WorkspaceReadError } from "./declaration";
65
+ import { policyAtBase, resolveBase } from "./trust/provenance";
66
+ import { emptyPolicy, type TrustPolicy } from "./trust/policy";
67
+ import { AGENT_ROLE } from "./records-cli";
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";
73
+
74
+ /** The version of the write documents `points ask` and `points answer` print. */
75
+ export const POINTS_WRITE_CONTRACT_VERSION = 1;
76
+ export const POINTS_WRITE_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/points-write/v1/points-write.schema.json";
77
+
78
+ /** Why `points ask` or `points answer` wrote nothing. Closed: a reader may switch on it. */
79
+ export const POINTS_WRITE_ERROR_CODES = [
80
+ ...LOAD_ERROR_CODES,
81
+ "write-usage-invalid",
82
+ "write-input-invalid",
83
+ /** No record kind with an answers block is declared, or given with --kind. */
84
+ "points-undeclared",
85
+ /** The points file the answer kind names can't be read or is not valid. */
86
+ "points-invalid",
87
+ /** No points file declares the point. */
88
+ "point-unknown",
89
+ /** The inputs are not a JSON object of the point's declared inputs. */
90
+ "point-inputs-invalid",
91
+ /** A model decider declared unreachable "fail" could not answer. */
92
+ "point-decider-failed",
93
+ /** The answer is not one of the question's candidates. */
94
+ "answer-not-candidate",
95
+ /** Too few people who count toward the point's quorum answered. */
96
+ "quorum-not-met",
97
+ "record-not-found",
98
+ "record-closed",
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",
102
+ ...RECORD_REASON_CODES,
103
+ ] as const satisfies readonly ReasonCode[];
104
+ export type PointsWriteErrorCode = (typeof POINTS_WRITE_ERROR_CODES)[number];
105
+
106
+ export class PointsWriteError extends Error {
107
+ constructor(
108
+ readonly code: PointsWriteErrorCode,
109
+ message: string,
110
+ ) {
111
+ super(message);
112
+ this.name = "PointsWriteError";
113
+ }
114
+ }
115
+
116
+ /** An answer as the read contract shows it: in `points --json` and in the documents `points ask` and `points answer` print. */
117
+ export interface QuestionView {
118
+ id: string;
119
+ /** The record file, from the repository root, with / separators. */
120
+ path: string;
121
+ point: string;
122
+ title: string;
123
+ /** What the question is about, the record's first `constrains` entry, or null. */
124
+ subject: string | null;
125
+ state: "escalated" | "proposed" | "answered";
126
+ /** Not answered yet: escalated to people, or proposed by a model and waiting for a person to confirm it. */
127
+ open: boolean;
128
+ questionType: string;
129
+ candidates: (string | boolean)[];
130
+ inputs: Record<string, unknown>;
131
+ inputsHash: string;
132
+ pointVersion: string;
133
+ /** The point's declaration is still the one the question was asked under, or null when the points file no longer declares the point. */
134
+ current: boolean | null;
135
+ answer: string | boolean | null;
136
+ decider: Record<string, unknown>;
137
+ probabilities: Record<string, number> | null;
138
+ confidence: number | null;
139
+ threshold: number | null;
140
+ /**
141
+ * Any model answer: the proposal for a proposed question, or for an
142
+ * escalated one the last model answer below its threshold. null when no
143
+ * model answered.
144
+ */
145
+ model: { answer: string | boolean | null; confidence: number | null; threshold: number | null; model: string | null; backend: string | null; observed: boolean } | null;
146
+ escalations: Escalation[];
147
+ answeredBy: string[];
148
+ askedOn: string | null;
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;
164
+ valid: boolean;
165
+ warnings: RecordWarning[];
166
+ }
167
+
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 {
191
+ const d = entry.data;
192
+ if (d === null || entry.id === null) return null;
193
+ const str = (v: unknown): string | null => (typeof v === "string" ? v : null);
194
+ const num = (v: unknown): number | null => (typeof v === "number" ? v : null);
195
+ const state = (["escalated", "proposed", "answered"] as const).find((s) => s === entry.state) ?? "escalated";
196
+ const decider = d.decider !== null && typeof d.decider === "object" && !Array.isArray(d.decider) ? (d.decider as Record<string, unknown>) : {};
197
+ const escalations = Array.isArray(d.escalations) ? (d.escalations as Escalation[]) : [];
198
+ const answer = typeof d.answer === "string" || typeof d.answer === "boolean" ? d.answer : null;
199
+ let model: QuestionView["model"] = null;
200
+ if (decider.kind === "model") {
201
+ model = { answer, confidence: num(d.confidence), threshold: num(d.threshold), model: str(decider.model), backend: str(decider.backend), observed: true };
202
+ } else {
203
+ const lean = [...escalations].reverse().find((e) => e.kind === "model" && e.answer !== undefined && e.answer !== null);
204
+ if (lean) model = { answer: lean.answer ?? null, confidence: lean.confidence ?? null, threshold: lean.threshold ?? null, model: lean.model ?? null, backend: lean.backend ?? null, observed: false };
205
+ }
206
+ const constrains = Array.isArray(d.constrains) ? d.constrains.filter((c): c is string => typeof c === "string") : [];
207
+ return {
208
+ id: entry.id,
209
+ path: entry.path,
210
+ point: str(d.point) ?? "",
211
+ title: str(d.title) ?? "",
212
+ subject: constrains[0] ?? null,
213
+ state,
214
+ open: state !== "answered",
215
+ questionType: str(d.question_type) ?? "",
216
+ candidates: Array.isArray(d.candidates) ? (d.candidates as (string | boolean)[]) : [],
217
+ inputs: d.inputs !== null && typeof d.inputs === "object" && !Array.isArray(d.inputs) ? (d.inputs as Record<string, unknown>) : {},
218
+ inputsHash: str(d.inputs_hash) ?? "",
219
+ pointVersion: str(d.point_version) ?? "",
220
+ current: point ? pointVersion(point) === d.point_version : null,
221
+ answer,
222
+ decider,
223
+ probabilities: d.probabilities !== null && typeof d.probabilities === "object" ? (d.probabilities as Record<string, number>) : null,
224
+ confidence: num(d.confidence),
225
+ threshold: num(d.threshold),
226
+ model,
227
+ escalations,
228
+ answeredBy: Array.isArray(d.answered_by) ? d.answered_by.filter((b): b is string => typeof b === "string") : [],
229
+ askedOn: str(d.asked_on),
230
+ answeredOn: str(d.answered_on),
231
+ askedBy: askedByOf(d),
232
+ ledger,
233
+ valid: entry.valid,
234
+ warnings: entry.warnings,
235
+ };
236
+ }
237
+
238
+ /** What `points ask` and `points answer` print. */
239
+ export type PointsWriteDocument =
240
+ | {
241
+ $schema: string;
242
+ contract: number;
243
+ verb: "ask" | "answer";
244
+ kind: KindView;
245
+ id: string;
246
+ path: string;
247
+ /** True when the record was already there and nothing new was written. */
248
+ reused: boolean;
249
+ /** True when a file was written. False when reused, and with --dry-run. */
250
+ written: boolean;
251
+ dryRun: boolean;
252
+ question: QuestionView;
253
+ /** With --dry-run, the text the command would write. */
254
+ text?: string;
255
+ }
256
+ | { $schema: string; contract: number; verb: "ask" | "answer"; error: { code: PointsWriteErrorCode; message: string } };
257
+
258
+ function failure(verb: "ask" | "answer", err: unknown): PointsWriteDocument {
259
+ if (err instanceof PointsWriteError || err instanceof RecordWriteError || err instanceof RecordReadError) {
260
+ return { $schema: POINTS_WRITE_SCHEMA_ID, contract: POINTS_WRITE_CONTRACT_VERSION, verb, error: { code: err.code as PointsWriteErrorCode, message: err.message } };
261
+ }
262
+ if (err instanceof WorkspaceReadError) {
263
+ return { $schema: POINTS_WRITE_SCHEMA_ID, contract: POINTS_WRITE_CONTRACT_VERSION, verb, error: { code: "points-undeclared", message: `the declaration can't be read: ${err.code}: ${err.message}` } };
264
+ }
265
+ throw err;
266
+ }
267
+
268
+ // ── Finding the answer kind and the point ───────────────────────────────────
269
+
270
+ /**
271
+ * The answer kinds a write can go through: the one `kind` names (a file, or a
272
+ * declared kind's name), or every declared kind with an answers block.
273
+ */
274
+ export async function answerKindFiles(cwd: string, kind?: string): Promise<string[]> {
275
+ if (kind !== undefined) return [resolveWriteKind(kind, cwd)];
276
+ const out: string[] = [];
277
+ for (const k of declaredKindFiles(cwd)) {
278
+ try {
279
+ if ((await loadRecordKind(k.file)).kind.answers) out.push(k.file);
280
+ } catch (err) {
281
+ // A kind that can't be loaded is check's to report (WSP115); a write names its kind with --kind.
282
+ if (!(err instanceof RecordReadError)) throw err;
283
+ }
284
+ }
285
+ return out;
286
+ }
287
+
288
+ interface OpenedPoints {
289
+ o: Opened;
290
+ points: Record<string, Point>;
291
+ pointsFile: string;
292
+ }
293
+
294
+ async function openAnswers(kindFile: string, cwd: string): Promise<OpenedPoints> {
295
+ const o = await open(kindFile, cwd);
296
+ if (!o.loaded.kind.answers) throw new PointsWriteError("points-undeclared", `the ${o.loaded.kind.name} kind has no answers block, so it holds no answers to decision points`);
297
+ const pointsFile = pointsFileOf(o.loaded, o.root);
298
+ const read = readPointsThrough(o.source, pointsFile);
299
+ if ("error" in read) throw new PointsWriteError("points-invalid", read.error);
300
+ return { o, points: read.points, pointsFile };
301
+ }
302
+
303
+ // ── points ask ───────────────────────────────────────────────────────────────
304
+
305
+ export interface AskPointOptions {
306
+ /** Where the kind resolves and the workspace is found. */
307
+ cwd: string;
308
+ /** The point's name. */
309
+ point: string;
310
+ /** The inputs, keyed by the point's declared input names. */
311
+ inputs: unknown;
312
+ /** What the question is about, such as a work item's id: written to `constrains`. */
313
+ subject?: string;
314
+ /** The answer kind file, or a declared kind's name. Without it, the declared answer kind whose points file declares the point. */
315
+ kind?: string;
316
+ /** The model call. Without it, a model decider is not asked. */
317
+ ask?: ModelAsk;
318
+ /** How the answer reached the workspace, for its source block (#2708). Defaults to cli. */
319
+ via?: SourceVia;
320
+ /** The client that asked, for its source block. */
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";
337
+ /** The date written as asked_on, and answered_on for a table's answer, YYYY-MM-DD. Defaults to today, in UTC. */
338
+ on?: string;
339
+ dryRun?: boolean;
340
+ }
341
+
342
+ const today = (): string => new Date().toISOString().slice(0, 10);
343
+ const show = (answer: string | boolean, type: string): string => (type === "noul" ? (answer ? "yes" : "no") : String(answer));
344
+
345
+ function titleFor(point: Point, subject: string | undefined, state: string, answer: string | boolean | undefined): string {
346
+ const outcome = state === "escalated" ? "open for people" : state === "proposed" ? `${show(answer!, point.question.type)}, proposed` : show(answer!, point.question.type);
347
+ return `${point.title}${subject ? ` (${subject})` : ""}: ${outcome}`;
348
+ }
349
+
350
+ function body(point: Point, title: string): string {
351
+ const q = point.question;
352
+ const describe = (c: string | boolean): string => (q.type === "score" ? "" : ((q.criteria as Record<string, string>)[String(c)] ?? ""));
353
+ const options = candidates(q).map((c) => `- ${show(c, q.type)}${describe(c) ? `: ${describe(c)}` : ""}`);
354
+ return ["", `# ${title}`, "", q.instructions.trim(), "", ...options, ""].join("\n");
355
+ }
356
+
357
+ /** The first kind whose points file declares `name`. */
358
+ async function findPoint(kinds: string[], name: string, cwd: string): Promise<OpenedPoints & { point: Point }> {
359
+ 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");
360
+ const declared: string[] = [];
361
+ for (const k of kinds) {
362
+ const opened = await openAnswers(k, cwd);
363
+ const point = pointOf(opened.points, name);
364
+ if (point) return { ...opened, point };
365
+ declared.push(...Object.keys(opened.points));
366
+ }
367
+ throw new PointsWriteError("point-unknown", `no points file declares ${JSON.stringify(name)}${declared.length ? ` (the points are ${declared.join(", ")})` : ""}`);
368
+ }
369
+
370
+ function checkInputs(name: string, point: Point, inputs: unknown): Record<string, unknown> {
371
+ if (inputs === null || typeof inputs !== "object" || Array.isArray(inputs)) throw new PointsWriteError("point-inputs-invalid", `the inputs to ${name} must be a JSON object`);
372
+ const declared = Object.keys(point.inputs);
373
+ const unknown = Object.keys(inputs).filter((k) => !declared.includes(k));
374
+ if (unknown.length > 0) {
375
+ throw new PointsWriteError("point-inputs-invalid", `${name} reads ${declared.join(", ")}, and the inputs also give ${unknown.join(", ")}`);
376
+ }
377
+ return inputs as Record<string, unknown>;
378
+ }
379
+
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
+ }
408
+ if (!dryRun) {
409
+ try {
410
+ writeFileSync(abs(o, path), text, create ? { flag: "wx" } : undefined);
411
+ } catch (err) {
412
+ if ((err as NodeJS.ErrnoException).code === "EEXIST") throw new PointsWriteError("record-id-taken", `${path} was written by another ask at the same time: ask again to read it`);
413
+ throw err;
414
+ }
415
+ }
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) };
424
+ }
425
+
426
+ function recordData(o: Opened, fields: Record<string, unknown>): Record<string, unknown> {
427
+ const clean = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
428
+ // The schema's properties order, so a record reads question, then answer, then when.
429
+ return pick(clean, schemaOrder(clean, { properties: o.loaded.schema.properties }));
430
+ }
431
+
432
+ function resultFields(result: ChainResult): Record<string, unknown> {
433
+ const escalations = result.escalations.length > 0 ? result.escalations : undefined;
434
+ if (result.status === "proposed") {
435
+ return { state: "proposed", answer: result.answer, decider: result.decider, probabilities: result.probabilities, confidence: result.confidence, threshold: result.threshold, escalations };
436
+ }
437
+ if (result.status === "answered") return { state: "answered", answer: result.answer, decider: result.decider, escalations };
438
+ return { state: "escalated", decider: result.decider, escalations };
439
+ }
440
+
441
+ /**
442
+ * Ask a point for these inputs, and record the answer: `points ask`. Returns
443
+ * the record that is there when the same point, declaration and inputs were
444
+ * answered or proposed before.
445
+ */
446
+ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocument> {
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;
452
+ const kinds = await answerKindFiles(opts.cwd, opts.kind);
453
+ const { o, point } = await findPoint(kinds, opts.point, opts.cwd);
454
+ const inputs = checkInputs(opts.point, point, opts.inputs);
455
+ const version = pointVersion(point);
456
+ const hash = inputsHash(opts.point, version, inputs);
457
+ const id = answerId(opts.point, hash);
458
+ if (toLedger && !opts.dryRun) await refreshLedger(o.loaded.file);
459
+ const { ledger, source: read, before } = await readWithLedger(o);
460
+ const existing = before.find((e) => e.id === id);
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 => ({
463
+ $schema: POINTS_WRITE_SCHEMA_ID,
464
+ contract: POINTS_WRITE_CONTRACT_VERSION,
465
+ verb: "ask",
466
+ kind: o.view,
467
+ id,
468
+ path: entry.path,
469
+ reused: extra.reused,
470
+ written: extra.written,
471
+ dryRun: !!opts.dryRun,
472
+ question: questionView(entry, point, extra.ledger !== undefined ? extra.ledger : heldAt(entry))!,
473
+ ...(extra.text !== undefined ? { text: extra.text } : {}),
474
+ });
475
+ if (existing && existing.data !== null && (existing.state === "answered" || existing.state === "proposed")) return done(existing, { reused: true, written: false });
476
+
477
+ let result: ChainResult;
478
+ try {
479
+ result = await runChain(opts.point, point, inputs, opts.ask);
480
+ } catch (err) {
481
+ if (err instanceof DeciderFailed) throw new PointsWriteError("point-decider-failed", err.message);
482
+ throw err;
483
+ }
484
+ // A standing escalation is kept while the chain still escalates.
485
+ if (existing && existing.data !== null && result.status === "escalated") return done(existing, { reused: true, written: false });
486
+
487
+ const on = opts.on ?? today();
488
+ const state = result.status;
489
+ const answer = result.status === "escalated" ? undefined : result.answer;
490
+ const title = titleFor(point, opts.subject, state, answer);
491
+ const modelId = result.status === "proposed" ? result.decider.model : undefined;
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
+ };
500
+ const data = recordData(o, {
501
+ id,
502
+ title,
503
+ point: opts.point,
504
+ point_version: version,
505
+ question_type: point.question.type,
506
+ candidates: candidates(point.question),
507
+ inputs,
508
+ inputs_hash: hash,
509
+ constrains: opts.subject !== undefined ? [opts.subject] : existing?.data?.constrains,
510
+ ...resultFields(result),
511
+ asked_on: on,
512
+ answered_on: state === "answered" ? on : undefined,
513
+ source,
514
+ });
515
+ const path = existing ? existing.path : o.dirRel === "." ? `${id}.md` : `${o.dirRel}/${id}.md`;
516
+ const text = renderRecord(data, body(point, title));
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 } : {}) });
523
+ } catch (err) {
524
+ return failure("ask", err);
525
+ }
526
+ }
527
+
528
+ function emptyEntry(path: string): RecordEntry {
529
+ return { id: null, path, state: null, valid: true, reasons: [], supersededBy: null, remediatedBy: [], data: null, assets: [], warnings: [], digest: "" };
530
+ }
531
+
532
+ // ── points answer ────────────────────────────────────────────────────────────
533
+
534
+ export interface AnswerPointOptions {
535
+ cwd: string;
536
+ /** The answer record's id. */
537
+ id: string;
538
+ /** The people's answer: one of the question's candidates. For a noul, true, false, yes or no. */
539
+ answer: string | boolean;
540
+ /** Who answered, as the caller names them. chant does not check who they are; the trust policy says who counts. */
541
+ by: string[];
542
+ kind?: string;
543
+ on?: string;
544
+ dryRun?: boolean;
545
+ }
546
+
547
+ /** The trust policy at base, for the quorum: who holds the agent role and each role a quorum names. */
548
+ function policyFor(root: string): TrustPolicy {
549
+ if (!gitRoot(root)) return emptyPolicy(null);
550
+ try {
551
+ return policyAtBase(root, resolveBase(root));
552
+ } catch {
553
+ return emptyPolicy(null);
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Count `by` toward a point's quorum: distinct people, after normalising,
559
+ * leaving out anyone holding the agent role in the trust policy at base, and,
560
+ * when the quorum names roles, anyone holding none of them.
561
+ */
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 } {
569
+ const holders = (role: string) => new Set((policy.roles[role] ?? []).map(normalisePrincipal));
570
+ const agents = holders(AGENT_ROLE);
571
+ const asker = steward !== undefined ? normalisePrincipal(steward) : undefined;
572
+ const counted: string[] = [];
573
+ const seen = new Set<string>();
574
+ const left: { name: string; why: string }[] = [];
575
+ for (const name of by) {
576
+ const p = normalisePrincipal(name);
577
+ if (p === "" || seen.has(p)) continue;
578
+ seen.add(p);
579
+ if (asker !== undefined && p === asker) {
580
+ left.push({ name, why: "is the steward that asked" });
581
+ continue;
582
+ }
583
+ if (agents.has(p)) {
584
+ left.push({ name, why: "holds the agent role" });
585
+ continue;
586
+ }
587
+ if (quorum.roles && !quorum.roles.some((r) => holders(r).has(p))) {
588
+ left.push({ name, why: `holds none of the roles ${quorum.roles.join(", ")}` });
589
+ continue;
590
+ }
591
+ counted.push(name);
592
+ }
593
+ return { counted, left, met: counted.length >= quorum.count };
594
+ }
595
+
596
+ /**
597
+ * People answer an open question: `points answer`. A proposed question the
598
+ * people answer as the model did is confirmed, and keeps the model as its
599
+ * decider; any other answer is the quorum's, and a model's proposal moves into
600
+ * the escalations. The question becomes `answered`.
601
+ */
602
+ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWriteDocument> {
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
+ }
614
+ const kinds = await answerKindFiles(opts.cwd, opts.kind);
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");
616
+ let found:
617
+ | { opened: OpenedPoints; before: RecordEntry[]; read: Awaited<ReturnType<typeof readWithLedger>>; target: RecordEntry & { data: Record<string, unknown> } }
618
+ | undefined;
619
+ for (const k of kinds) {
620
+ const opened = await openAnswers(k, opts.cwd);
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);
624
+ if (target && target.data !== null) {
625
+ found = { opened, before: read.before, read, target: target as RecordEntry & { data: Record<string, unknown> } };
626
+ break;
627
+ }
628
+ }
629
+ if (!found) throw new PointsWriteError("record-not-found", `no answer record has id ${opts.id}`);
630
+ const { opened, before, target } = found;
631
+ const ledger = found.read.ledger;
632
+ const { o } = opened;
633
+ const d = target.data;
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`);
635
+ const name = String(d.point);
636
+ const point = pointOf(opened.points, name);
637
+ if (!point) throw new PointsWriteError("point-unknown", `${opts.id} answers ${name}, which ${opened.pointsFile} no longer declares`);
638
+ const allowed = Array.isArray(d.candidates) ? (d.candidates as (string | boolean)[]) : candidates(point.question);
639
+ const type = String(d.question_type);
640
+ const value = type === "noul" && typeof opts.answer === "string" ? ({ true: true, yes: true, false: false, no: false } as Record<string, boolean>)[opts.answer.toLowerCase()] : opts.answer;
641
+ if (value === undefined || !allowed.includes(value)) {
642
+ throw new PointsWriteError("answer-not-candidate", `${opts.id} takes one of ${allowed.map((c) => JSON.stringify(c)).join(", ")}, not ${JSON.stringify(opts.answer)}`);
643
+ }
644
+ const quorum = quorumOf(point);
645
+ const tally = tallyQuorum(opts.by, quorum, policyFor(o.root), askedByOf(d)?.steward);
646
+ if (!tally.met) {
647
+ const got = tally.counted.length;
648
+ const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
649
+ throw new PointsWriteError(
650
+ "quorum-not-met",
651
+ `${name} needs ${quorum.count} ${quorum.count === 1 ? "person" : "people"}${quorum.roles ? ` holding ${quorum.roles.join(" or ")}` : ""} to answer, and ${got} ${got === 1 ? "counts" : "count"}${got ? ` (${tally.counted.join(", ")})` : ""}${left}`,
652
+ );
653
+ }
654
+ const on = opts.on ?? today();
655
+ const decider = d.decider as Record<string, unknown>;
656
+ const confirmed = target.state === "proposed" && decider.kind === "model" && d.answer === value;
657
+ const escalations = Array.isArray(d.escalations) ? [...(d.escalations as Escalation[])] : [];
658
+ let fields: Record<string, unknown>;
659
+ if (confirmed) {
660
+ fields = { ...d, state: "answered", answered_by: tally.counted, answered_on: on };
661
+ } else {
662
+ if (target.state === "proposed" && decider.kind === "model") {
663
+ escalations.push({
664
+ kind: "model",
665
+ backend: String(decider.backend),
666
+ model: String(decider.model),
667
+ answer: d.answer as string | boolean,
668
+ ...(d.probabilities ? { probabilities: d.probabilities as Record<string, number> } : {}),
669
+ ...(typeof d.confidence === "number" ? { confidence: d.confidence } : {}),
670
+ ...(typeof d.threshold === "number" ? { threshold: d.threshold } : {}),
671
+ reason: `proposed ${show(d.answer as string | boolean, type)}, and people answered ${show(value, type)}`,
672
+ });
673
+ }
674
+ const rest = Object.fromEntries(Object.entries(d).filter(([k]) => k !== "probabilities" && k !== "confidence" && k !== "threshold"));
675
+ fields = {
676
+ ...rest,
677
+ state: "answered",
678
+ answer: value,
679
+ decider: { kind: "quorum", count: quorum.count, ...(quorum.roles ? { roles: quorum.roles } : {}), by: tally.counted },
680
+ escalations: escalations.length > 0 ? escalations : undefined,
681
+ answered_by: tally.counted,
682
+ answered_on: on,
683
+ };
684
+ }
685
+ const subject = Array.isArray(d.constrains) && typeof d.constrains[0] === "string" ? (d.constrains[0] as string) : undefined;
686
+ fields.title = titleFor(point, subject, "answered", value);
687
+ const data = recordData(o, fields);
688
+ const text = renderRecord(data, body(point, String(fields.title)));
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 };
693
+ return {
694
+ $schema: POINTS_WRITE_SCHEMA_ID,
695
+ contract: POINTS_WRITE_CONTRACT_VERSION,
696
+ verb: "answer",
697
+ kind: o.view,
698
+ id: opts.id,
699
+ path: target.path,
700
+ reused: false,
701
+ written: !opts.dryRun,
702
+ dryRun: !!opts.dryRun,
703
+ question: questionView(entry, point, onLedger ? (wrote.ledger ?? ledger.byPath.get(target.path)?.ledger ?? null) : null)!,
704
+ ...(opts.dryRun ? { text } : {}),
705
+ };
706
+ } catch (err) {
707
+ return failure("answer", err);
708
+ }
709
+ }