@intentius/chant 0.102.0 → 0.104.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 (464) hide show
  1. package/dist/audit/core.d.ts +2 -0
  2. package/dist/audit/core.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/change-set.d.ts +232 -0
  5. package/dist/change-set.d.ts.map +1 -0
  6. package/dist/cli/commands/audit.d.ts +7 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/handlers/build.d.ts.map +1 -1
  10. package/dist/cli/handlers/change-set.d.ts +11 -0
  11. package/dist/cli/handlers/change-set.d.ts.map +1 -0
  12. package/dist/cli/handlers/fan-out.d.ts +33 -0
  13. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  14. package/dist/cli/handlers/operator.d.ts +7 -0
  15. package/dist/cli/handlers/operator.d.ts.map +1 -1
  16. package/dist/cli/handlers/pr.d.ts +31 -0
  17. package/dist/cli/handlers/pr.d.ts.map +1 -0
  18. package/dist/cli/handlers/run.d.ts.map +1 -1
  19. package/dist/cli/main.d.ts.map +1 -1
  20. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  21. package/dist/cli/mcp/workspace-tools.d.ts +3 -2
  22. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  23. package/dist/cli/registry.d.ts +63 -1
  24. package/dist/cli/registry.d.ts.map +1 -1
  25. package/dist/components/auto-release.d.ts +16 -0
  26. package/dist/components/auto-release.d.ts.map +1 -1
  27. package/dist/components/capability.d.ts +53 -0
  28. package/dist/components/capability.d.ts.map +1 -1
  29. package/dist/components/changed-files.d.ts +31 -0
  30. package/dist/components/changed-files.d.ts.map +1 -0
  31. package/dist/components/cli-support.d.ts +9 -1
  32. package/dist/components/cli-support.d.ts.map +1 -1
  33. package/dist/components/component.d.ts +24 -0
  34. package/dist/components/component.d.ts.map +1 -1
  35. package/dist/components/deploy-units.d.ts.map +1 -1
  36. package/dist/components/driver.d.ts +13 -0
  37. package/dist/components/driver.d.ts.map +1 -1
  38. package/dist/components/fan-out-output.d.ts +6 -0
  39. package/dist/components/fan-out-output.d.ts.map +1 -1
  40. package/dist/components/fan-out-record.d.ts +41 -0
  41. package/dist/components/fan-out-record.d.ts.map +1 -0
  42. package/dist/components/fan-out-run.d.ts +65 -0
  43. package/dist/components/fan-out-run.d.ts.map +1 -1
  44. package/dist/components/fan-out-support.d.ts +2 -0
  45. package/dist/components/fan-out-support.d.ts.map +1 -1
  46. package/dist/components/fan-out.d.ts +6 -0
  47. package/dist/components/fan-out.d.ts.map +1 -1
  48. package/dist/components/gated-wave-pipeline.d.ts +50 -0
  49. package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
  50. package/dist/components/index.d.ts +1 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/layers.d.ts +25 -0
  53. package/dist/components/layers.d.ts.map +1 -0
  54. package/dist/components/pr-pipeline.d.ts +61 -0
  55. package/dist/components/pr-pipeline.d.ts.map +1 -0
  56. package/dist/components/pr-run.d.ts +119 -0
  57. package/dist/components/pr-run.d.ts.map +1 -0
  58. package/dist/components/promote.d.ts.map +1 -1
  59. package/dist/components/verbs/shell.d.ts.map +1 -1
  60. package/dist/components/wave-plan.d.ts +49 -0
  61. package/dist/components/wave-plan.d.ts.map +1 -0
  62. package/dist/gated-waves.d.ts +147 -0
  63. package/dist/gated-waves.d.ts.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/lexicon.d.ts +48 -0
  67. package/dist/lexicon.d.ts.map +1 -1
  68. package/dist/lifecycle/gate-ledger.d.ts +7 -0
  69. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  70. package/dist/lifecycle/git.d.ts +46 -1
  71. package/dist/lifecycle/git.d.ts.map +1 -1
  72. package/dist/lint/post-synth.d.ts +18 -0
  73. package/dist/lint/post-synth.d.ts.map +1 -1
  74. package/dist/op/activities/activity-contracts.d.ts +126 -0
  75. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  76. package/dist/op/activities/change-set.d.ts +62 -0
  77. package/dist/op/activities/change-set.d.ts.map +1 -0
  78. package/dist/op/activities/decide.d.ts +2 -0
  79. package/dist/op/activities/decide.d.ts.map +1 -1
  80. package/dist/op/activities/factory.d.ts +137 -0
  81. package/dist/op/activities/factory.d.ts.map +1 -0
  82. package/dist/op/activities/index.d.ts +4 -0
  83. package/dist/op/activities/index.d.ts.map +1 -1
  84. package/dist/op/activity-contract.d.ts.map +1 -1
  85. package/dist/op/builders.d.ts +11 -0
  86. package/dist/op/builders.d.ts.map +1 -1
  87. package/dist/op/decide-backend.d.ts +5 -1
  88. package/dist/op/decide-backend.d.ts.map +1 -1
  89. package/dist/op/decide-read-inputs.d.ts +4 -2
  90. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  91. package/dist/op/factory.d.ts +83 -0
  92. package/dist/op/factory.d.ts.map +1 -0
  93. package/dist/op/index.d.ts +4 -1
  94. package/dist/op/index.d.ts.map +1 -1
  95. package/dist/op/op-ir.d.ts +2 -0
  96. package/dist/op/op-ir.d.ts.map +1 -1
  97. package/dist/op/op-verb-class.d.ts.map +1 -1
  98. package/dist/op/types.d.ts +8 -0
  99. package/dist/op/types.d.ts.map +1 -1
  100. package/dist/op/work-lease-run.d.ts.map +1 -1
  101. package/dist/plan-summary.d.ts +207 -0
  102. package/dist/plan-summary.d.ts.map +1 -0
  103. package/dist/pr-forge.d.ts +105 -0
  104. package/dist/pr-forge.d.ts.map +1 -0
  105. package/dist/pr-loop.d.ts +178 -0
  106. package/dist/pr-loop.d.ts.map +1 -0
  107. package/dist/telemetry-attribution.d.ts +31 -0
  108. package/dist/telemetry-attribution.d.ts.map +1 -1
  109. package/dist/terraform/hcl2json.d.ts +45 -0
  110. package/dist/terraform/hcl2json.d.ts.map +1 -0
  111. package/dist/terraform/parse.d.ts +4 -40
  112. package/dist/terraform/parse.d.ts.map +1 -1
  113. package/dist/workspace/box-cli.d.ts +12 -0
  114. package/dist/workspace/box-cli.d.ts.map +1 -0
  115. package/dist/workspace/box-factory.d.ts +27 -3
  116. package/dist/workspace/box-factory.d.ts.map +1 -1
  117. package/dist/workspace/box-listing.d.ts +85 -0
  118. package/dist/workspace/box-listing.d.ts.map +1 -0
  119. package/dist/workspace/box-publish.d.ts +147 -0
  120. package/dist/workspace/box-publish.d.ts.map +1 -0
  121. package/dist/workspace/broker-protocol.d.ts +190 -0
  122. package/dist/workspace/broker-protocol.d.ts.map +1 -0
  123. package/dist/workspace/checks.d.ts.map +1 -1
  124. package/dist/workspace/composites.d.ts +2 -2
  125. package/dist/workspace/composites.d.ts.map +1 -1
  126. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
  127. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
  128. package/dist/workspace/conformance/broker.d.ts +202 -0
  129. package/dist/workspace/conformance/broker.d.ts.map +1 -0
  130. package/dist/workspace/conformance/index.d.ts +2 -1
  131. package/dist/workspace/conformance/index.d.ts.map +1 -1
  132. package/dist/workspace/conformance/vitest.d.ts +9 -2
  133. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  134. package/dist/workspace/conformance/writer.d.ts +108 -14
  135. package/dist/workspace/conformance/writer.d.ts.map +1 -1
  136. package/dist/workspace/decide.d.ts +126 -12
  137. package/dist/workspace/decide.d.ts.map +1 -1
  138. package/dist/workspace/decision-points.schema.json +11 -4
  139. package/dist/workspace/declaration.d.ts +65 -0
  140. package/dist/workspace/declaration.d.ts.map +1 -1
  141. package/dist/workspace/declaration.schema.json +153 -4
  142. package/dist/workspace/environments.d.ts +36 -10
  143. package/dist/workspace/environments.d.ts.map +1 -1
  144. package/dist/workspace/factory-rules.d.ts +180 -0
  145. package/dist/workspace/factory-rules.d.ts.map +1 -0
  146. package/dist/workspace/init.d.ts +21 -0
  147. package/dist/workspace/init.d.ts.map +1 -1
  148. package/dist/workspace/intent-cli.d.ts +1 -1
  149. package/dist/workspace/intent-cli.d.ts.map +1 -1
  150. package/dist/workspace/intent-record.d.ts +28 -1
  151. package/dist/workspace/intent-record.d.ts.map +1 -1
  152. package/dist/workspace/intent.d.ts +102 -5
  153. package/dist/workspace/intent.d.ts.map +1 -1
  154. package/dist/workspace/json-edit.d.ts +24 -0
  155. package/dist/workspace/json-edit.d.ts.map +1 -0
  156. package/dist/workspace/jsonc.d.ts +11 -0
  157. package/dist/workspace/jsonc.d.ts.map +1 -1
  158. package/dist/workspace/kinds.d.ts +48 -0
  159. package/dist/workspace/kinds.d.ts.map +1 -1
  160. package/dist/workspace/lexicon-symbols.d.ts +29 -0
  161. package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
  162. package/dist/workspace/lineage-upgrade.d.ts +31 -2
  163. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  164. package/dist/workspace/point-answer.schema.json +42 -2
  165. package/dist/workspace/points-cli.d.ts +10 -3
  166. package/dist/workspace/points-cli.d.ts.map +1 -1
  167. package/dist/workspace/points.d.ts +66 -5
  168. package/dist/workspace/points.d.ts.map +1 -1
  169. package/dist/workspace/reason-codes.d.ts +33 -4
  170. package/dist/workspace/reason-codes.d.ts.map +1 -1
  171. package/dist/workspace/record-sessions.d.ts +5 -7
  172. package/dist/workspace/record-sessions.d.ts.map +1 -1
  173. package/dist/workspace/records-cli.d.ts +7 -0
  174. package/dist/workspace/records-cli.d.ts.map +1 -1
  175. package/dist/workspace/records-close.d.ts +7 -3
  176. package/dist/workspace/records-close.d.ts.map +1 -1
  177. package/dist/workspace/records-write.d.ts +66 -6
  178. package/dist/workspace/records-write.d.ts.map +1 -1
  179. package/dist/workspace/records.d.ts +28 -9
  180. package/dist/workspace/records.d.ts.map +1 -1
  181. package/dist/workspace/runs-cli.d.ts +138 -6
  182. package/dist/workspace/runs-cli.d.ts.map +1 -1
  183. package/dist/workspace/runs.d.ts +132 -11
  184. package/dist/workspace/runs.d.ts.map +1 -1
  185. package/dist/workspace/session-kinds.d.ts +1 -1
  186. package/dist/workspace/session-kinds.d.ts.map +1 -1
  187. package/dist/workspace/squash.d.ts +80 -0
  188. package/dist/workspace/squash.d.ts.map +1 -0
  189. package/dist/workspace/status-gates.d.ts +2 -0
  190. package/dist/workspace/status-gates.d.ts.map +1 -1
  191. package/dist/workspace/status.d.ts +54 -0
  192. package/dist/workspace/status.d.ts.map +1 -1
  193. package/dist/workspace/symbols.d.ts +16 -5
  194. package/dist/workspace/symbols.d.ts.map +1 -1
  195. package/dist/workspace/trust/run-commits.d.ts +46 -0
  196. package/dist/workspace/trust/run-commits.d.ts.map +1 -0
  197. package/dist/workspace/trust/run-statement.d.ts +128 -0
  198. package/dist/workspace/trust/run-statement.d.ts.map +1 -0
  199. package/dist/workspace/trust/seal.d.ts +6 -1
  200. package/dist/workspace/trust/seal.d.ts.map +1 -1
  201. package/dist/workspace/trust/verify-cli.d.ts +1 -1
  202. package/dist/workspace/trust/verify.d.ts +16 -2
  203. package/dist/workspace/trust/verify.d.ts.map +1 -1
  204. package/dist/workspace/wip-cli.d.ts +21 -0
  205. package/dist/workspace/wip-cli.d.ts.map +1 -0
  206. package/dist/workspace/wip.d.ts +268 -0
  207. package/dist/workspace/wip.d.ts.map +1 -0
  208. package/dist/workspace/work-cli.d.ts.map +1 -1
  209. package/dist/workspace/work-evidence.d.ts +1 -1
  210. package/dist/workspace/work-evidence.d.ts.map +1 -1
  211. package/dist/workspace/work-size.d.ts +55 -0
  212. package/dist/workspace/work-size.d.ts.map +1 -0
  213. package/dist/workspace/workspace-kinds.schema.json +122 -0
  214. package/dist/workspace/write-lock-cli.d.ts +60 -0
  215. package/dist/workspace/write-lock-cli.d.ts.map +1 -0
  216. package/dist/workspace/write-lock.d.ts +152 -0
  217. package/dist/workspace/write-lock.d.ts.map +1 -0
  218. package/dist/workspace/write-scope.d.ts +6 -2
  219. package/dist/workspace/write-scope.d.ts.map +1 -1
  220. package/package.json +6 -1
  221. package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
  222. package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
  223. package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
  224. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
  225. package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
  226. package/src/__fixtures__/change-set/warden.plan.json +44 -0
  227. package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
  228. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
  229. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
  230. package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
  231. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
  232. package/src/audit/core.test.ts +15 -0
  233. package/src/audit/core.ts +8 -2
  234. package/src/audit/discover.test.ts +11 -0
  235. package/src/audit/discover.ts +7 -2
  236. package/src/change-set-bundle.test.ts +57 -0
  237. package/src/change-set.test.ts +215 -0
  238. package/src/change-set.ts +445 -0
  239. package/src/cli/commands/audit-opt-in.test.ts +54 -0
  240. package/src/cli/commands/audit.ts +29 -2
  241. package/src/cli/commands/build.test.ts +19 -0
  242. package/src/cli/commands/build.ts +3 -2
  243. package/src/cli/handlers/build.ts +13 -1
  244. package/src/cli/handlers/change-set.test.ts +56 -0
  245. package/src/cli/handlers/change-set.ts +44 -0
  246. package/src/cli/handlers/fan-out.test.ts +221 -2
  247. package/src/cli/handlers/fan-out.ts +165 -75
  248. package/src/cli/handlers/graph.ts +3 -0
  249. package/src/cli/handlers/operator.test.ts +21 -0
  250. package/src/cli/handlers/operator.ts +34 -3
  251. package/src/cli/handlers/pr.ts +373 -0
  252. package/src/cli/handlers/run.ts +1 -0
  253. package/src/cli/main.test.ts +20 -0
  254. package/src/cli/main.ts +185 -23
  255. package/src/cli/mcp/never-over-mcp.test.ts +101 -0
  256. package/src/cli/mcp/op-tools.ts +24 -2
  257. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  258. package/src/cli/mcp/workspace-tools.ts +34 -6
  259. package/src/cli/registry.ts +63 -1
  260. package/src/components/auto-release.ts +1 -1
  261. package/src/components/capability.ts +55 -0
  262. package/src/components/changed-files.ts +66 -0
  263. package/src/components/cli-support.ts +28 -5
  264. package/src/components/component.schema.json +17 -0
  265. package/src/components/component.ts +25 -0
  266. package/src/components/deploy-units.ts +3 -0
  267. package/src/components/driver.test.ts +52 -0
  268. package/src/components/driver.ts +31 -13
  269. package/src/components/fan-out-output.ts +42 -1
  270. package/src/components/fan-out-record.ts +69 -0
  271. package/src/components/fan-out-run.test.ts +17 -0
  272. package/src/components/fan-out-run.ts +304 -1
  273. package/src/components/fan-out-support.ts +3 -0
  274. package/src/components/fan-out-waves.test.ts +375 -0
  275. package/src/components/fan-out.ts +18 -17
  276. package/src/components/gated-wave-pipeline.ts +82 -0
  277. package/src/components/index.ts +1 -0
  278. package/src/components/layers.ts +30 -0
  279. package/src/components/pr-pipeline.ts +91 -0
  280. package/src/components/pr-run.test.ts +205 -0
  281. package/src/components/pr-run.ts +280 -0
  282. package/src/components/promote.ts +5 -0
  283. package/src/components/verbs/shell.test.ts +11 -0
  284. package/src/components/verbs/shell.ts +7 -3
  285. package/src/components/wave-plan.ts +124 -0
  286. package/src/discovery/sandbox/bundle.ts +1 -1
  287. package/src/gated-waves-bundle.test.ts +62 -0
  288. package/src/gated-waves.test.ts +118 -0
  289. package/src/gated-waves.ts +240 -0
  290. package/src/index.ts +1 -0
  291. package/src/lexicon.ts +51 -0
  292. package/src/lifecycle/gate-ledger.ts +7 -0
  293. package/src/lifecycle/git.ts +102 -16
  294. package/src/lint/post-synth.test.ts +19 -0
  295. package/src/lint/post-synth.ts +19 -1
  296. package/src/meta/declared-imports.test.ts +1 -1
  297. package/src/op/activities/activity-contracts.ts +84 -0
  298. package/src/op/activities/change-set.ts +114 -0
  299. package/src/op/activities/decide.ts +3 -0
  300. package/src/op/activities/factory.ts +675 -0
  301. package/src/op/activities/index.ts +6 -0
  302. package/src/op/activity-contract.test.ts +21 -1
  303. package/src/op/activity-contract.ts +19 -9
  304. package/src/op/builders.ts +23 -0
  305. package/src/op/decide-backend.ts +5 -1
  306. package/src/op/decide-read-inputs.ts +33 -5
  307. package/src/op/factory.e2e.test.ts +232 -0
  308. package/src/op/factory.ts +118 -0
  309. package/src/op/index.ts +9 -1
  310. package/src/op/op-ir.test.ts +9 -0
  311. package/src/op/op-ir.ts +4 -0
  312. package/src/op/op-verb-class.ts +3 -0
  313. package/src/op/types.ts +8 -0
  314. package/src/op/work-lease-run.ts +3 -0
  315. package/src/plan-summary.test.ts +263 -0
  316. package/src/plan-summary.ts +845 -0
  317. package/src/pr-forge.test.ts +237 -0
  318. package/src/pr-forge.ts +332 -0
  319. package/src/pr-loop-bundle.test.ts +46 -0
  320. package/src/pr-loop.test.ts +171 -0
  321. package/src/pr-loop.ts +324 -0
  322. package/src/telemetry-attribution.test.ts +23 -1
  323. package/src/telemetry-attribution.ts +51 -0
  324. package/src/terraform/hcl2json.test.ts +21 -0
  325. package/src/terraform/hcl2json.ts +81 -0
  326. package/src/terraform/parse.ts +11 -74
  327. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  328. package/src/workspace/app-kind.test.ts +196 -0
  329. package/src/workspace/box-cli.ts +113 -0
  330. package/src/workspace/box-factory.test.ts +4 -1
  331. package/src/workspace/box-factory.ts +37 -4
  332. package/src/workspace/box-listing-write.schema.json +204 -0
  333. package/src/workspace/box-listing.test.ts +234 -0
  334. package/src/workspace/box-listing.ts +324 -0
  335. package/src/workspace/box-publish.schema.json +498 -0
  336. package/src/workspace/box-publish.test.ts +204 -0
  337. package/src/workspace/box-publish.ts +420 -0
  338. package/src/workspace/box-ship.test.ts +96 -0
  339. package/src/workspace/broker-protocol.schema.json +186 -0
  340. package/src/workspace/broker-protocol.test.ts +122 -0
  341. package/src/workspace/broker-protocol.ts +317 -0
  342. package/src/workspace/builder-tiers.test.ts +146 -0
  343. package/src/workspace/change-set.schema.json +460 -0
  344. package/src/workspace/chant-migrations.e2e.test.ts +17 -5
  345. package/src/workspace/checks/boxes.test.ts +3 -0
  346. package/src/workspace/checks.ts +10 -3
  347. package/src/workspace/component-environments.test.ts +105 -0
  348. package/src/workspace/composites.schema.json +23 -4
  349. package/src/workspace/composites.test.ts +8 -8
  350. package/src/workspace/composites.ts +13 -2
  351. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
  352. package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
  353. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
  354. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
  355. package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  356. package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  357. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
  358. package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  359. package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  360. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
  361. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
  362. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
  363. package/src/workspace/conformance/broker.test.ts +122 -0
  364. package/src/workspace/conformance/broker.ts +792 -0
  365. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  366. package/src/workspace/conformance/index.mjs +22 -1
  367. package/src/workspace/conformance/index.ts +5 -1
  368. package/src/workspace/conformance/vitest.ts +33 -1
  369. package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
  370. package/src/workspace/conformance/writer.test.ts +32 -2
  371. package/src/workspace/conformance/writer.ts +339 -28
  372. package/src/workspace/decide.test.ts +208 -4
  373. package/src/workspace/decide.ts +427 -73
  374. package/src/workspace/decision-points.schema.json +11 -4
  375. package/src/workspace/declaration.schema.json +153 -4
  376. package/src/workspace/declaration.ts +141 -0
  377. package/src/workspace/environments.ts +66 -14
  378. package/src/workspace/factory-rules.test.ts +62 -0
  379. package/src/workspace/factory-rules.ts +218 -0
  380. package/src/workspace/graph-cli.ts +2 -2
  381. package/src/workspace/graph.schema.json +67 -0
  382. package/src/workspace/identity.test.ts +23 -0
  383. package/src/workspace/init.ts +82 -2
  384. package/src/workspace/intent-cli.ts +26 -9
  385. package/src/workspace/intent-patch-id.test.ts +162 -0
  386. package/src/workspace/intent-record.schema.json +130 -3
  387. package/src/workspace/intent-record.ts +74 -28
  388. package/src/workspace/intent-reviews.test.ts +76 -0
  389. package/src/workspace/intent-squash.test.ts +213 -0
  390. package/src/workspace/intent-why.test.ts +5 -5
  391. package/src/workspace/intent.schema.json +234 -4
  392. package/src/workspace/intent.ts +260 -66
  393. package/src/workspace/json-edit.test.ts +121 -0
  394. package/src/workspace/json-edit.ts +142 -0
  395. package/src/workspace/jsonc.ts +20 -0
  396. package/src/workspace/kinds.ts +157 -2
  397. package/src/workspace/lexicon-symbols.test.ts +148 -0
  398. package/src/workspace/lexicon-symbols.ts +125 -0
  399. package/src/workspace/lineage-upgrade.test.ts +63 -0
  400. package/src/workspace/lineage-upgrade.ts +146 -5
  401. package/src/workspace/plan-summary.schema.json +155 -0
  402. package/src/workspace/point-answer.schema.json +42 -2
  403. package/src/workspace/points-cli.ts +42 -16
  404. package/src/workspace/points-write.schema.json +162 -4
  405. package/src/workspace/points.schema.json +157 -2
  406. package/src/workspace/points.test.ts +13 -1
  407. package/src/workspace/points.ts +118 -12
  408. package/src/workspace/pr-report.schema.json +95 -0
  409. package/src/workspace/profiles.test.ts +159 -0
  410. package/src/workspace/read-contract.test.ts +2 -2
  411. package/src/workspace/reason-codes.test.ts +13 -1
  412. package/src/workspace/reason-codes.ts +38 -4
  413. package/src/workspace/record-sessions.ts +9 -8
  414. package/src/workspace/records-amend.schema.json +78 -0
  415. package/src/workspace/records-cli.ts +20 -6
  416. package/src/workspace/records-close.schema.json +78 -0
  417. package/src/workspace/records-close.ts +31 -7
  418. package/src/workspace/records-concurrent.e2e.test.ts +135 -0
  419. package/src/workspace/records-concurrent.test.ts +145 -0
  420. package/src/workspace/records-formats.test.ts +46 -10
  421. package/src/workspace/records-new.schema.json +7 -0
  422. package/src/workspace/records-review.schema.json +78 -0
  423. package/src/workspace/records-sessions-write.test.ts +69 -0
  424. package/src/workspace/records-sessions.test.ts +22 -0
  425. package/src/workspace/records-write.ts +192 -25
  426. package/src/workspace/records.schema.json +16 -0
  427. package/src/workspace/records.ts +50 -9
  428. package/src/workspace/reference-kinds/app/package.json +10 -0
  429. package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  430. package/src/workspace/run-statement.schema.json +622 -0
  431. package/src/workspace/runs-cli.ts +343 -14
  432. package/src/workspace/runs-statement.test.ts +239 -0
  433. package/src/workspace/runs-write.schema.json +416 -5
  434. package/src/workspace/runs.schema.json +451 -4
  435. package/src/workspace/runs.test.ts +39 -0
  436. package/src/workspace/runs.ts +296 -35
  437. package/src/workspace/session-kinds.ts +3 -3
  438. package/src/workspace/squash.ts +198 -0
  439. package/src/workspace/status-contract.test.ts +5 -2
  440. package/src/workspace/status-gates.ts +3 -1
  441. package/src/workspace/status.schema.json +126 -3
  442. package/src/workspace/status.ts +90 -1
  443. package/src/workspace/symbols.ts +20 -9
  444. package/src/workspace/trust/run-commits.ts +115 -0
  445. package/src/workspace/trust/run-statement.ts +226 -0
  446. package/src/workspace/trust/seal.ts +8 -2
  447. package/src/workspace/trust/verify-cli.ts +6 -5
  448. package/src/workspace/trust/verify.ts +39 -6
  449. package/src/workspace/wip-cli.ts +97 -0
  450. package/src/workspace/wip-write.schema.json +504 -0
  451. package/src/workspace/wip.schema.json +334 -0
  452. package/src/workspace/wip.test.ts +294 -0
  453. package/src/workspace/wip.ts +753 -0
  454. package/src/workspace/work-cli.ts +3 -0
  455. package/src/workspace/work-evidence.schema.json +3 -0
  456. package/src/workspace/work-size.test.ts +77 -0
  457. package/src/workspace/work-size.ts +93 -0
  458. package/src/workspace/workspace-kinds.schema.json +122 -0
  459. package/src/workspace/write-lock-cli.ts +131 -0
  460. package/src/workspace/write-lock.schema.json +172 -0
  461. package/src/workspace/write-lock.test.ts +189 -0
  462. package/src/workspace/write-lock.ts +474 -0
  463. package/src/workspace/write-scope.test.ts +16 -1
  464. package/src/workspace/write-scope.ts +33 -11
@@ -2,9 +2,10 @@
2
2
  * Answers to decision points as records (ws-058, #2739).
3
3
  *
4
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
5
+ * the answer in the answer kind's records directory, {@link answerPoint}
6
+ * records people's answer to an open question, with an optional note, and
7
+ * {@link retractAnswer} takes an answer back (#3351). Each writes one Markdown
8
+ * record or none, never commits, and goes through the checks every records write goes
8
9
  * through (`records-write.ts`): the record is read back with the kind's schema
9
10
  * before it is written, and no other record may become invalid.
10
11
  *
@@ -21,12 +22,28 @@
21
22
  * escalated one is asked again, since a backend may answer now, and rewritten
22
23
  * only when the chain no longer escalates.
23
24
  *
25
+ * An ad-hoc point (#3403) has no candidates of its own: each ask gives the
26
+ * question's text and candidates, the record keeps them as `asked`, and they
27
+ * are part of the inputs hash, so a question asked with other options is
28
+ * another question. People's answer is checked against them.
29
+ *
30
+ * An answer never changes in place. People may retract it (ws-084): the
31
+ * question is escalated to them again, and the answer, who gave it, when, and
32
+ * its note move into the record's `retractions` with who retracted it, when
33
+ * and why. Asking a retracted question again returns it as it stands, since
34
+ * people took it back from the deciders; people answer it again.
35
+ *
36
+ * Each write holds the working tree's write lock (#3173, ws-089) from the first read
37
+ * to the write, so two answers to one question are never both taken from the
38
+ * same reading of it.
39
+ *
24
40
  * chant never calls a model (ws-052): the model call is the caller's
25
41
  * {@link ModelAsk}, such as the decide Op activity (#2740), or a backend's
26
42
  * response the caller already has (`points ask --response`).
27
43
  */
28
44
 
29
45
  import { writeFileSync } from "node:fs";
46
+ import { noteWrites, withWriteLock, WriteLockError, WRITE_LOCK_CODES, type WriteLockWho } from "./write-lock";
30
47
  import type { ReasonCode } from "./reason-codes";
31
48
  import { gitRoot } from "./record-source";
32
49
  import { loadRecordKind, normalisePrincipal, RECORD_REASON_CODES, RecordReadError, type RecordEntry, type RecordWarning } from "./records";
@@ -47,15 +64,19 @@ import {
47
64
  } from "./records-write";
48
65
  import {
49
66
  answerId,
67
+ askedOf,
68
+ askedQuestion,
50
69
  candidates,
51
70
  DeciderFailed,
52
71
  inputsHash,
72
+ parseAsked,
53
73
  pointOf,
54
74
  pointsFileOf,
55
75
  pointVersion,
56
76
  quorumOf,
57
77
  readPointsThrough,
58
78
  runChain,
79
+ type Asked,
59
80
  type ChainResult,
60
81
  type Escalation,
61
82
  type ModelAsk,
@@ -73,11 +94,11 @@ import { scopeSource } from "./write-scope";
73
94
  import { RefCASConflictError } from "../lifecycle/git";
74
95
  import { readLedgerAnswers, refreshLedger, withLedgerAnswers, writeLedgerAnswer, type LedgerAnswers } from "./answers-ledger";
75
96
 
76
- /** The version of the write documents `points ask` and `points answer` print. */
97
+ /** The version of the write documents `points ask`, `points answer` and `points retract` print. */
77
98
  export const POINTS_WRITE_CONTRACT_VERSION = 1;
78
99
  export const POINTS_WRITE_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/points-write/v1/points-write.schema.json";
79
100
 
80
- /** Why `points ask` or `points answer` wrote nothing. Closed: a reader may switch on it. */
101
+ /** Why `points ask`, `points answer` or `points retract` wrote nothing. Closed: a reader may switch on it. */
81
102
  export const POINTS_WRITE_ERROR_CODES = [
82
103
  ...LOAD_ERROR_CODES,
83
104
  "write-usage-invalid",
@@ -103,6 +124,13 @@ export const POINTS_WRITE_ERROR_CODES = [
103
124
  "answer-in-steward-turn",
104
125
  /** An answerer is named by a bare name, and identity.attribution at base is identified (#3163). */
105
126
  "principal-unidentified",
127
+ /** `points retract` names a question that has no answer: escalated or proposed (#3351). */
128
+ "answer-not-answered",
129
+ /** The answer kind's schema copy has no `note`, `retractions`, `relayed_by` or `asked` field for what the write was given (#3351, #3402, #3403). */
130
+ "answer-field-unsupported",
131
+ /** An ad-hoc point was asked without its question and candidates, a declared point with them, or they don't fit the point's question type (#3403). */
132
+ "point-candidates-invalid",
133
+ ...WRITE_LOCK_CODES,
106
134
  ...RECORD_REASON_CODES,
107
135
  ] as const satisfies readonly ReasonCode[];
108
136
  export type PointsWriteErrorCode = (typeof POINTS_WRITE_ERROR_CODES)[number];
@@ -141,16 +169,34 @@ export interface QuestionView {
141
169
  probabilities: Record<string, number> | null;
142
170
  confidence: number | null;
143
171
  threshold: number | null;
172
+ /**
173
+ * The model's explanation of the recorded answer (#3345): a proposal's, or a
174
+ * confirmed proposal's. null when a model gave none, or did not decide.
175
+ */
176
+ reason: string | null;
144
177
  /**
145
178
  * Any model answer: the proposal for a proposed question, or for an
146
179
  * escalated one the last model answer below its threshold. null when no
147
- * model answered.
180
+ * model answered. `reason` is the model's explanation of that answer, or
181
+ * null when it gave none (#3345).
148
182
  */
149
- model: { answer: string | boolean | null; confidence: number | null; threshold: number | null; model: string | null; backend: string | null; observed: boolean } | null;
183
+ model: { answer: string | boolean | null; confidence: number | null; threshold: number | null; model: string | null; backend: string | null; observed: boolean; reason: string | null } | null;
150
184
  escalations: Escalation[];
151
185
  answeredBy: string[];
152
186
  askedOn: string | null;
153
187
  answeredOn: string | null;
188
+ /** What the people who answered wrote with their answer (`points answer --note`, #3351), or null. */
189
+ note: string | null;
190
+ /** Who relayed the answer to chant for the people who gave it (`points answer --relayed-by`, #3402), or null. */
191
+ relayedBy: string | null;
192
+ /** Answers people took back, oldest first (`points retract`, #3351). Empty when none was. */
193
+ retractions: RetractionView[];
194
+ /**
195
+ * An ad-hoc point's question as its ask gave it (#3403): the question's text
196
+ * and its criteria, what each candidate means. null for a declared point's
197
+ * question, whose text and criteria are the point's.
198
+ */
199
+ asked: Asked | null;
154
200
  /**
155
201
  * The steward whose turn asked the question (#2749), and the run it was in,
156
202
  * or null when no steward asked it. The steward waits on the question and
@@ -169,6 +215,43 @@ export interface QuestionView {
169
215
  warnings: RecordWarning[];
170
216
  }
171
217
 
218
+ /** One retracted answer, as a {@link QuestionView} lists it: the answer as it was, and who took it back, when and why. */
219
+ export interface RetractionView {
220
+ answer: string | boolean | null;
221
+ decider: Record<string, unknown>;
222
+ answeredBy: string[];
223
+ answeredOn: string | null;
224
+ /** The note the answer carried, or null. */
225
+ answerNote: string | null;
226
+ /** Who relayed the answer (#3402), or null. */
227
+ answerRelayedBy: string | null;
228
+ /** Who retracted it. */
229
+ by: string[];
230
+ on: string | null;
231
+ /** Why it was retracted (`points retract --note`), or null. */
232
+ note: string | null;
233
+ }
234
+
235
+ /** The record's `retractions`, as views. */
236
+ function retractionsOf(d: Record<string, unknown>): RetractionView[] {
237
+ if (!Array.isArray(d.retractions)) return [];
238
+ const str = (v: unknown): string | null => (typeof v === "string" ? v : null);
239
+ const strs = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []);
240
+ return d.retractions
241
+ .filter((r): r is Record<string, unknown> => r !== null && typeof r === "object" && !Array.isArray(r))
242
+ .map((r) => ({
243
+ answer: typeof r.answer === "string" || typeof r.answer === "boolean" ? r.answer : null,
244
+ decider: r.decider !== null && typeof r.decider === "object" && !Array.isArray(r.decider) ? (r.decider as Record<string, unknown>) : {},
245
+ answeredBy: strs(r.answered_by),
246
+ answeredOn: str(r.answered_on),
247
+ answerNote: str(r.answer_note),
248
+ answerRelayedBy: str(r.answer_relayed_by),
249
+ by: strs(r.by),
250
+ on: str(r.on),
251
+ note: str(r.note),
252
+ }));
253
+ }
254
+
172
255
  /** The harness a steward's question names in its source block (#2749). */
173
256
  export const STEWARD_HARNESS = "chant-steward";
174
257
 
@@ -202,10 +285,20 @@ export function questionView(entry: RecordEntry, point: Point | undefined, ledge
202
285
  const answer = typeof d.answer === "string" || typeof d.answer === "boolean" ? d.answer : null;
203
286
  let model: QuestionView["model"] = null;
204
287
  if (decider.kind === "model") {
205
- model = { answer, confidence: num(d.confidence), threshold: num(d.threshold), model: str(decider.model), backend: str(decider.backend), observed: true };
288
+ model = { answer, confidence: num(d.confidence), threshold: num(d.threshold), model: str(decider.model), backend: str(decider.backend), observed: true, reason: str(d.reason) };
206
289
  } else {
207
290
  const lean = [...escalations].reverse().find((e) => e.kind === "model" && e.answer !== undefined && e.answer !== null);
208
- 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 };
291
+ if (lean) {
292
+ model = {
293
+ answer: lean.answer ?? null,
294
+ confidence: lean.confidence ?? null,
295
+ threshold: lean.threshold ?? null,
296
+ model: lean.model ?? null,
297
+ backend: lean.backend ?? null,
298
+ observed: false,
299
+ reason: typeof lean.model_reason === "string" ? lean.model_reason : null,
300
+ };
301
+ }
209
302
  }
210
303
  const constrains = Array.isArray(d.constrains) ? d.constrains.filter((c): c is string => typeof c === "string") : [];
211
304
  return {
@@ -227,11 +320,16 @@ export function questionView(entry: RecordEntry, point: Point | undefined, ledge
227
320
  probabilities: d.probabilities !== null && typeof d.probabilities === "object" ? (d.probabilities as Record<string, number>) : null,
228
321
  confidence: num(d.confidence),
229
322
  threshold: num(d.threshold),
323
+ reason: decider.kind === "model" ? str(d.reason) : null,
230
324
  model,
231
325
  escalations,
232
326
  answeredBy: Array.isArray(d.answered_by) ? d.answered_by.filter((b): b is string => typeof b === "string") : [],
233
327
  askedOn: str(d.asked_on),
234
328
  answeredOn: str(d.answered_on),
329
+ note: str(d.note),
330
+ relayedBy: str(d.relayed_by),
331
+ retractions: retractionsOf(d),
332
+ asked: askedOf(d) ?? null,
235
333
  askedBy: askedByOf(d),
236
334
  ledger,
237
335
  valid: entry.valid,
@@ -239,12 +337,15 @@ export function questionView(entry: RecordEntry, point: Point | undefined, ledge
239
337
  };
240
338
  }
241
339
 
242
- /** What `points ask` and `points answer` print. */
340
+ /** The write verbs of `points`. */
341
+ export type PointsWriteVerb = "ask" | "answer" | "retract";
342
+
343
+ /** What `points ask`, `points answer` and `points retract` print. */
243
344
  export type PointsWriteDocument =
244
345
  | {
245
346
  $schema: string;
246
347
  contract: number;
247
- verb: "ask" | "answer";
348
+ verb: PointsWriteVerb;
248
349
  kind: KindView;
249
350
  id: string;
250
351
  path: string;
@@ -257,10 +358,10 @@ export type PointsWriteDocument =
257
358
  /** With --dry-run, the text the command would write. */
258
359
  text?: string;
259
360
  }
260
- | { $schema: string; contract: number; verb: "ask" | "answer"; error: { code: PointsWriteErrorCode; message: string } };
361
+ | { $schema: string; contract: number; verb: PointsWriteVerb; error: { code: PointsWriteErrorCode; message: string } };
261
362
 
262
- function failure(verb: "ask" | "answer", err: unknown): PointsWriteDocument {
263
- if (err instanceof PointsWriteError || err instanceof RecordWriteError || err instanceof RecordReadError || err instanceof IdentityError) {
363
+ function failure(verb: PointsWriteVerb, err: unknown): PointsWriteDocument {
364
+ if (err instanceof PointsWriteError || err instanceof RecordWriteError || err instanceof RecordReadError || err instanceof IdentityError || err instanceof WriteLockError) {
264
365
  return { $schema: POINTS_WRITE_SCHEMA_ID, contract: POINTS_WRITE_CONTRACT_VERSION, verb, error: { code: err.code as PointsWriteErrorCode, message: err.message } };
265
366
  }
266
367
  if (err instanceof WorkspaceReadError) {
@@ -313,6 +414,12 @@ export interface AskPointOptions {
313
414
  point: string;
314
415
  /** The inputs, keyed by the point's declared input names. */
315
416
  inputs: unknown;
417
+ /**
418
+ * An ad-hoc point's question and candidates (#3403), as `{ question,
419
+ * criteria }` in the point's question type's shape. Required for an ad-hoc
420
+ * point and refused for a declared one.
421
+ */
422
+ candidates?: unknown;
316
423
  /** What the question is about, such as a work item's id: written to `constrains`. */
317
424
  subject?: string;
318
425
  /** The answer kind file, or a declared kind's name. Without it, the declared answer kind whose points file declares the point. */
@@ -346,14 +453,15 @@ export interface AskPointOptions {
346
453
  const today = (): string => new Date().toISOString().slice(0, 10);
347
454
  const show = (answer: string | boolean, type: string): string => (type === "noul" ? (answer ? "yes" : "no") : String(answer));
348
455
 
349
- function titleFor(point: Point, subject: string | undefined, state: string, answer: string | boolean | undefined): string {
456
+ /** A record's title: the point's title, or an ad-hoc question's own text (#3403), what it is about, and where it stands. */
457
+ function titleFor(point: Point, subject: string | undefined, state: string, answer: string | boolean | undefined, asked?: Asked): string {
350
458
  const outcome = state === "escalated" ? "open for people" : state === "proposed" ? `${show(answer!, point.question.type)}, proposed` : show(answer!, point.question.type);
351
- return `${point.title}${subject ? ` (${subject})` : ""}: ${outcome}`;
459
+ return `${asked ? asked.question : point.title}${subject ? ` (${subject})` : ""}: ${outcome}`;
352
460
  }
353
461
 
354
- function body(point: Point, title: string): string {
355
- const q = point.question;
356
- const describe = (c: string | boolean): string => (q.type === "score" ? "" : ((q.criteria as Record<string, string>)[String(c)] ?? ""));
462
+ function body(point: Point, title: string, asked?: Asked): string {
463
+ const q = askedQuestion(point, asked);
464
+ const describe = (c: string | boolean): string => (q.type === "score" ? "" : ((q.criteria as Record<string, string> | undefined)?.[String(c)] ?? ""));
357
465
  const options = candidates(q).map((c) => `- ${show(c, q.type)}${describe(c) ? `: ${describe(c)}` : ""}`);
358
466
  return ["", `# ${title}`, "", q.instructions.trim(), "", ...options, ""].join("\n");
359
467
  }
@@ -371,6 +479,26 @@ async function findPoint(kinds: string[], name: string, cwd: string): Promise<Op
371
479
  throw new PointsWriteError("point-unknown", `no points file declares ${JSON.stringify(name)}${declared.length ? ` (the points are ${declared.join(", ")})` : ""}`);
372
480
  }
373
481
 
482
+ /**
483
+ * An ad-hoc point's question and candidates from the ask (#3403), or
484
+ * undefined for a declared point. Refused when an ad-hoc point has none, a
485
+ * declared point is given them, they don't fit the question type, or the
486
+ * answer kind's schema copy has no `asked` field to keep them in.
487
+ */
488
+ function checkAsked(name: string, point: Point, given: unknown, loaded: { kind: { name: string }; schema: Record<string, unknown> }): Asked | undefined {
489
+ if (!point.adhoc) {
490
+ if (given !== undefined) throw new PointsWriteError("point-candidates-invalid", `${name} declares its candidates, so the ask gives none: --candidates is for an ad-hoc point`);
491
+ return undefined;
492
+ }
493
+ if (given === undefined) throw new PointsWriteError("point-candidates-invalid", `${name} is an ad-hoc point, so each ask gives the question and its candidates: --candidates <file|-|json>`);
494
+ const parsed = parseAsked(point, given);
495
+ if ("problems" in parsed) throw new PointsWriteError("point-candidates-invalid", `the candidates for ${name}, a ${point.question.type} question: ${parsed.problems.join("; ")}`);
496
+ if (!answerFields(loaded.schema).asked) {
497
+ throw new PointsWriteError("answer-field-unsupported", `the ${loaded.kind.name} kind's schema has no asked field, so an ad-hoc question's text and candidates can't be kept: copy point-answer.schema.json from @intentius/chant anew`);
498
+ }
499
+ return parsed.asked;
500
+ }
501
+
374
502
  function checkInputs(name: string, point: Point, inputs: unknown): Record<string, unknown> {
375
503
  if (inputs === null || typeof inputs !== "object" || Array.isArray(inputs)) throw new PointsWriteError("point-inputs-invalid", `the inputs to ${name} must be a JSON object`);
376
504
  const declared = Object.keys(point.inputs);
@@ -389,6 +517,8 @@ interface WriteTarget {
389
517
  /** Write to the ledger rather than the tree. */
390
518
  onLedger: boolean;
391
519
  message: string;
520
+ /** Who writes, for the write journal (#3173). */
521
+ who: WriteLockWho;
392
522
  }
393
523
 
394
524
  /**
@@ -412,6 +542,7 @@ async function write(o: Opened, before: RecordEntry[], path: string, text: strin
412
542
  if (!dryRun) {
413
543
  try {
414
544
  writeFileSync(abs(o, path), text, create ? { flag: "wx" } : undefined);
545
+ noteWrites(o.root, [{ path, text }], target.who);
415
546
  } catch (err) {
416
547
  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`);
417
548
  throw err;
@@ -433,10 +564,28 @@ function recordData(o: Opened, fields: Record<string, unknown>): Record<string,
433
564
  return pick(clean, schemaOrder(clean, { properties: o.loaded.schema.properties }));
434
565
  }
435
566
 
436
- function resultFields(result: ChainResult): Record<string, unknown> {
437
- const escalations = result.escalations.length > 0 ? result.escalations : undefined;
567
+ /**
568
+ * Whether an answer kind's schema copy takes the model's reason (#3345): at
569
+ * the top level, and in an escalation entry as `model_reason`. A workspace
570
+ * whose copy of point-answer.schema.json predates them keeps no reason rather
571
+ * than failing the write; copying the schema anew turns them on.
572
+ */
573
+ export function reasonFields(schema: Record<string, unknown>): { top: boolean; escalation: boolean } {
574
+ const has = (o: unknown, key: string): boolean => o !== null && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, key);
575
+ const defs = (schema.definitions ?? schema.$defs) as Record<string, { properties?: unknown }> | undefined;
576
+ return { top: has(schema.properties, "reason"), escalation: has(defs?.escalation?.properties, "model_reason") };
577
+ }
578
+
579
+ /** Escalations without `model_reason` when the kind's schema does not take it. */
580
+ function keepReasons(escalations: Escalation[], takes: { escalation: boolean }): Escalation[] {
581
+ return takes.escalation ? escalations : escalations.map(({ model_reason: _, ...e }) => e);
582
+ }
583
+
584
+ function resultFields(result: ChainResult, takes: { top: boolean; escalation: boolean }): Record<string, unknown> {
585
+ const escalations = result.escalations.length > 0 ? keepReasons(result.escalations, takes) : undefined;
438
586
  if (result.status === "proposed") {
439
- return { state: "proposed", answer: result.answer, decider: result.decider, probabilities: result.probabilities, confidence: result.confidence, threshold: result.threshold, escalations };
587
+ const reason = takes.top ? result.reason : undefined;
588
+ return { state: "proposed", answer: result.answer, decider: result.decider, probabilities: result.probabilities, confidence: result.confidence, threshold: result.threshold, reason, escalations };
440
589
  }
441
590
  if (result.status === "answered") return { state: "answered", answer: result.answer, decider: result.decider, escalations };
442
591
  return { state: "escalated", decider: result.decider, escalations };
@@ -448,6 +597,14 @@ function resultFields(result: ChainResult): Record<string, unknown> {
448
597
  * answered or proposed before.
449
598
  */
450
599
  export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocument> {
600
+ try {
601
+ return await withWriteLock(opts.cwd, { verb: "points ask", by: opts.steward?.name ?? null }, opts.dryRun, () => askPointLocked(opts));
602
+ } catch (err) {
603
+ return failure("ask", err);
604
+ }
605
+ }
606
+
607
+ async function askPointLocked(opts: AskPointOptions): Promise<PointsWriteDocument> {
451
608
  try {
452
609
  // A steward's turn (#2749): named by the caller, or this process's, as when an Op shells out to `points ask`.
453
610
  const turn = opts.steward ? undefined : currentStewardTurn();
@@ -456,8 +613,9 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
456
613
  const kinds = await answerKindFiles(opts.cwd, opts.kind);
457
614
  const { o, point } = await findPoint(kinds, opts.point, opts.cwd);
458
615
  const inputs = checkInputs(opts.point, point, opts.inputs);
616
+ const asked = checkAsked(opts.point, point, opts.candidates, o.loaded);
459
617
  const version = pointVersion(point);
460
- const hash = inputsHash(opts.point, version, inputs);
618
+ const hash = inputsHash(opts.point, version, inputs, asked);
461
619
  const id = answerId(opts.point, hash);
462
620
  if (toLedger && !opts.dryRun) await refreshLedger(o.loaded.file);
463
621
  const { ledger, source: read, before } = await readWithLedger(o);
@@ -477,10 +635,12 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
477
635
  ...(extra.text !== undefined ? { text: extra.text } : {}),
478
636
  });
479
637
  if (existing && existing.data !== null && (existing.state === "answered" || existing.state === "proposed")) return done(existing, { reused: true, written: false });
638
+ // People took a retracted question back from the deciders (ws-084): they answer it again, and asking returns it as it stands.
639
+ if (existing && existing.data !== null && Array.isArray(existing.data.retractions) && existing.data.retractions.length > 0) return done(existing, { reused: true, written: false });
480
640
 
481
641
  let result: ChainResult;
482
642
  try {
483
- result = await runChain(opts.point, point, inputs, opts.ask);
643
+ result = await runChain(opts.point, { ...point, question: askedQuestion(point, asked) }, inputs, opts.ask);
484
644
  } catch (err) {
485
645
  if (err instanceof DeciderFailed) throw new PointsWriteError("point-decider-failed", err.message);
486
646
  throw err;
@@ -491,7 +651,7 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
491
651
  const on = opts.on ?? today();
492
652
  const state = result.status;
493
653
  const answer = result.status === "escalated" ? undefined : result.answer;
494
- const title = titleFor(point, opts.subject, state, answer);
654
+ const title = titleFor(point, opts.subject, state, answer, asked);
495
655
  const modelId = result.status === "proposed" ? result.decider.model : undefined;
496
656
  const client = steward ? { name: steward.name } : opts.client;
497
657
  const source = {
@@ -507,21 +667,22 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
507
667
  point: opts.point,
508
668
  point_version: version,
509
669
  question_type: point.question.type,
510
- candidates: candidates(point.question),
670
+ candidates: candidates(askedQuestion(point, asked)),
671
+ asked,
511
672
  inputs,
512
673
  inputs_hash: hash,
513
674
  constrains: opts.subject !== undefined ? [opts.subject] : existing?.data?.constrains,
514
- ...resultFields(result),
675
+ ...resultFields(result, reasonFields(o.loaded.schema)),
515
676
  asked_on: on,
516
677
  answered_on: state === "answered" ? on : undefined,
517
678
  source,
518
679
  });
519
680
  const path = existing ? existing.path : o.dirRel === "." ? `${id}.md` : `${o.dirRel}/${id}.md`;
520
- const text = renderRecord(data, body(point, title));
681
+ const text = renderRecord(data, body(point, title, asked));
521
682
  // A steward's question goes on the ledger, and one already held there stays there (#2786).
522
683
  const onLedger = toLedger || ledger.byPath.has(path);
523
684
  const message = `Decision point ${opts.point}: ${state}${opts.subject ? ` (${opts.subject})` : ""}${steward ? `, asked by ${steward.name}` : ""}`;
524
- const wrote = await write(o, before, path, text, !existing, !!opts.dryRun, { source: read, ledger, onLedger, message });
685
+ const wrote = await write(o, before, path, text, !existing, !!opts.dryRun, { source: read, ledger, onLedger, message, who: { verb: "points ask", by: steward?.name ?? null } });
525
686
  const entry: RecordEntry = { ...(existing ?? emptyEntry(path)), id, path, state, data, valid: true, reasons: [], warnings: wrote.warnings };
526
687
  return done(entry, { reused: false, written: !opts.dryRun, ledger: wrote.ledger, ...(opts.dryRun ? { text } : {}) });
527
688
  } catch (err) {
@@ -543,11 +704,38 @@ export interface AnswerPointOptions {
543
704
  answer: string | boolean;
544
705
  /** Who answered, as the caller names them. chant does not check who they are; the trust policy says who counts. */
545
706
  by: string[];
707
+ /** What the people who answered say with it, recorded as the record's `note` (#3351). */
708
+ note?: string;
709
+ /**
710
+ * Who carried the answer to chant for the people in `by` (#3402), such as a
711
+ * follower relaying a person's answer through hud, recorded as the record's
712
+ * `relayed_by`. The relay does not count toward the quorum.
713
+ */
714
+ relayedBy?: string;
546
715
  kind?: string;
547
716
  on?: string;
548
717
  dryRun?: boolean;
549
718
  }
550
719
 
720
+ /**
721
+ * Whether an answer kind's schema copy takes the fields of #3351 (`note` on an
722
+ * answer, and `retractions`), of #3402 (`relayed_by`) and of #3403 (`asked`).
723
+ * A copy of point-answer.schema.json from before them refuses a note, a
724
+ * retraction, a relay or an ad-hoc ask with `answer-field-unsupported`, rather
725
+ * than dropping what a person wrote or the question an agent asked.
726
+ */
727
+ export function answerFields(schema: Record<string, unknown>): { note: boolean; retractions: boolean; relayedBy: boolean; asked: boolean } {
728
+ const props = schema.properties as Record<string, unknown> | undefined;
729
+ const has = (key: string): boolean => props !== undefined && props !== null && typeof props === "object" && Object.prototype.hasOwnProperty.call(props, key);
730
+ return { note: has("note"), retractions: has("retractions"), relayedBy: has("relayed_by"), asked: has("asked") };
731
+ }
732
+
733
+ /** A note as given, trimmed, or undefined when it is empty. */
734
+ const noteOf = (note: string | undefined): string | undefined => {
735
+ const t = note?.trim();
736
+ return t ? t : undefined;
737
+ };
738
+
551
739
  /** The trust policy at base, for the quorum: who holds the agent role and each role a quorum names. */
552
740
  function policyFor(root: string): TrustPolicy {
553
741
  if (!gitRoot(root)) return emptyPolicy(null);
@@ -597,73 +785,114 @@ export function tallyQuorum(
597
785
  return { counted, left, met: counted.length >= quorum.count };
598
786
  }
599
787
 
788
+ /**
789
+ * A steward waits on a question and never answers or retracts one (#2749), as
790
+ * it never clears a gate: the write has to come from a person, through hud or
791
+ * a shell, not from the steward's own turn or a process it started.
792
+ */
793
+ function refuseStewardTurn(id: string, does: "answers" | "retracts"): void {
794
+ const turn = currentStewardTurn();
795
+ if (turn) {
796
+ throw new PointsWriteError(
797
+ "answer-in-steward-turn",
798
+ `this is the steward ${turn.steward}'s turn, and a steward never ${does} a decision point: a person ${does === "answers" ? "answers" : "retracts the answer to"} ${id} through hud or \`chant workspace points ${does === "answers" ? "answer" : "retract"}\` at a shell`,
799
+ );
800
+ }
801
+ }
802
+
803
+ interface FoundQuestion {
804
+ opened: OpenedPoints;
805
+ before: RecordEntry[];
806
+ read: Awaited<ReturnType<typeof readWithLedger>>;
807
+ target: RecordEntry & { data: Record<string, unknown> };
808
+ }
809
+
810
+ /** The answer record with this id, in the answer kind `kind` names or the first declared one that has it, read with the ledger's answers laid over the tree. */
811
+ async function findQuestion(cwd: string, kind: string | undefined, id: string): Promise<FoundQuestion> {
812
+ const kinds = await answerKindFiles(cwd, kind);
813
+ 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");
814
+ for (const k of kinds) {
815
+ const opened = await openAnswers(k, cwd);
816
+ // A question a steward asked is on the ledger (#2786), and people's answer goes back there.
817
+ const read = await readWithLedger(opened.o);
818
+ const target = read.before.find((e) => e.id === id);
819
+ if (target && target.data !== null) return { opened, before: read.before, read, target: target as FoundQuestion["target"] };
820
+ }
821
+ throw new PointsWriteError("record-not-found", `no answer record has id ${id}`);
822
+ }
823
+
824
+ /** Count `by` toward the point's quorum, or refuse with `quorum-not-met`. */
825
+ function meetQuorum(name: string, by: string[], quorum: { count: number; roles?: string[] }, root: string, steward: string | undefined, verb: "answer" | "retract"): { counted: string[] } {
826
+ const tally = tallyQuorum(by, quorum, policyFor(root), steward);
827
+ if (!tally.met) {
828
+ const got = tally.counted.length;
829
+ const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
830
+ throw new PointsWriteError(
831
+ "quorum-not-met",
832
+ `${name} needs ${quorum.count} ${quorum.count === 1 ? "person" : "people"}${quorum.roles ? ` holding ${quorum.roles.join(" or ")}` : ""} to ${verb === "answer" ? "answer" : "retract an answer"}, and ${got} ${got === 1 ? "counts" : "count"}${got ? ` (${tally.counted.join(", ")})` : ""}${left}`,
833
+ );
834
+ }
835
+ return tally;
836
+ }
837
+
600
838
  /**
601
839
  * People answer an open question: `points answer`. A proposed question the
602
840
  * people answer as the model did is confirmed, and keeps the model as its
603
841
  * decider; any other answer is the quorum's, and a model's proposal moves into
604
- * the escalations. The question becomes `answered`.
842
+ * the escalations. The question becomes `answered`, with the people's note
843
+ * when they give one (#3351).
605
844
  */
606
845
  export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWriteDocument> {
607
846
  try {
608
- // A steward waits on a question and never answers one (#2749), as it never
609
- // clears a gate: the answer has to come from a person, through hud or a
610
- // shell, not from the steward's own turn or a process it started.
611
- const turn = currentStewardTurn();
612
- if (turn) {
613
- throw new PointsWriteError(
614
- "answer-in-steward-turn",
615
- `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`,
616
- );
617
- }
847
+ return await withWriteLock(opts.cwd, { verb: "points answer", by: (opts.by ?? []).join(", ") || null }, opts.dryRun, () => answerPointLocked(opts));
848
+ } catch (err) {
849
+ return failure("answer", err);
850
+ }
851
+ }
852
+
853
+ async function answerPointLocked(opts: AnswerPointOptions): Promise<PointsWriteDocument> {
854
+ try {
855
+ refuseStewardTurn(opts.id, "answers");
618
856
  // #3163: under identity.attribution "identified" at base, each answerer is a forge identity or a signer.
619
857
  refuseUnidentified(scopeSource(opts.cwd), opts.by, "--by");
620
- const kinds = await answerKindFiles(opts.cwd, opts.kind);
621
- 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");
622
- let found:
623
- | { opened: OpenedPoints; before: RecordEntry[]; read: Awaited<ReturnType<typeof readWithLedger>>; target: RecordEntry & { data: Record<string, unknown> } }
624
- | undefined;
625
- for (const k of kinds) {
626
- const opened = await openAnswers(k, opts.cwd);
627
- // A question a steward asked is on the ledger (#2786), and people's answer goes back there.
628
- const read = await readWithLedger(opened.o);
629
- const target = read.before.find((e) => e.id === opts.id);
630
- if (target && target.data !== null) {
631
- found = { opened, before: read.before, read, target: target as RecordEntry & { data: Record<string, unknown> } };
632
- break;
633
- }
634
- }
635
- if (!found) throw new PointsWriteError("record-not-found", `no answer record has id ${opts.id}`);
858
+ // #3402: and so is whoever relayed the answer.
859
+ const relayedBy = noteOf(opts.relayedBy);
860
+ if (relayedBy !== undefined) refuseUnidentified(scopeSource(opts.cwd), [relayedBy], "--relayed-by");
861
+ const found = await findQuestion(opts.cwd, opts.kind, opts.id);
636
862
  const { opened, before, target } = found;
637
863
  const ledger = found.read.ledger;
638
864
  const { o } = opened;
639
865
  const d = target.data;
640
- 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`);
866
+ if (target.state === "answered") {
867
+ throw new PointsWriteError("record-closed", `${opts.id} is answered, and an answer never changes in place: retract it with \`points retract ${opts.id}\` and answer again, ask again with other inputs, or change the point, which asks the question anew`);
868
+ }
869
+ const note = noteOf(opts.note);
870
+ if (note !== undefined && !answerFields(o.loaded.schema).note) {
871
+ throw new PointsWriteError("answer-field-unsupported", `the ${o.loaded.kind.name} kind's schema has no note field, so ${opts.id} can't keep the note: copy point-answer.schema.json from @intentius/chant anew`);
872
+ }
873
+ if (relayedBy !== undefined && !answerFields(o.loaded.schema).relayedBy) {
874
+ throw new PointsWriteError("answer-field-unsupported", `the ${o.loaded.kind.name} kind's schema has no relayed_by field, so ${opts.id} can't record who relayed the answer: copy point-answer.schema.json from @intentius/chant anew`);
875
+ }
641
876
  const name = String(d.point);
642
877
  const point = pointOf(opened.points, name);
643
878
  if (!point) throw new PointsWriteError("point-unknown", `${opts.id} answers ${name}, which ${opened.pointsFile} no longer declares`);
644
- const allowed = Array.isArray(d.candidates) ? (d.candidates as (string | boolean)[]) : candidates(point.question);
879
+ // An ad-hoc question is answered from the candidates it was asked with (#3403).
880
+ const asked = askedOf(d);
881
+ const allowed = asked ? candidates(askedQuestion(point, asked)) : Array.isArray(d.candidates) ? (d.candidates as (string | boolean)[]) : candidates(point.question);
645
882
  const type = String(d.question_type);
646
883
  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;
647
884
  if (value === undefined || !allowed.includes(value)) {
648
885
  throw new PointsWriteError("answer-not-candidate", `${opts.id} takes one of ${allowed.map((c) => JSON.stringify(c)).join(", ")}, not ${JSON.stringify(opts.answer)}`);
649
886
  }
650
887
  const quorum = quorumOf(point);
651
- const tally = tallyQuorum(opts.by, quorum, policyFor(o.root), askedByOf(d)?.steward);
652
- if (!tally.met) {
653
- const got = tally.counted.length;
654
- const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
655
- throw new PointsWriteError(
656
- "quorum-not-met",
657
- `${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}`,
658
- );
659
- }
888
+ const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "answer");
660
889
  const on = opts.on ?? today();
661
890
  const decider = d.decider as Record<string, unknown>;
662
891
  const confirmed = target.state === "proposed" && decider.kind === "model" && d.answer === value;
663
892
  const escalations = Array.isArray(d.escalations) ? [...(d.escalations as Escalation[])] : [];
664
893
  let fields: Record<string, unknown>;
665
894
  if (confirmed) {
666
- fields = { ...d, state: "answered", answered_by: tally.counted, answered_on: on };
895
+ fields = { ...d, state: "answered", answered_by: tally.counted, answered_on: on, note, relayed_by: relayedBy };
667
896
  } else {
668
897
  if (target.state === "proposed" && decider.kind === "model") {
669
898
  escalations.push({
@@ -675,9 +904,11 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
675
904
  ...(typeof d.confidence === "number" ? { confidence: d.confidence } : {}),
676
905
  ...(typeof d.threshold === "number" ? { threshold: d.threshold } : {}),
677
906
  reason: `proposed ${show(d.answer as string | boolean, type)}, and people answered ${show(value, type)}`,
907
+ // The model's own reason moves with its proposal (#3345).
908
+ ...(typeof d.reason === "string" && reasonFields(o.loaded.schema).escalation ? { model_reason: d.reason } : {}),
678
909
  });
679
910
  }
680
- const rest = Object.fromEntries(Object.entries(d).filter(([k]) => k !== "probabilities" && k !== "confidence" && k !== "threshold"));
911
+ const rest = Object.fromEntries(Object.entries(d).filter(([k]) => k !== "probabilities" && k !== "confidence" && k !== "threshold" && k !== "reason"));
681
912
  fields = {
682
913
  ...rest,
683
914
  state: "answered",
@@ -686,15 +917,17 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
686
917
  escalations: escalations.length > 0 ? escalations : undefined,
687
918
  answered_by: tally.counted,
688
919
  answered_on: on,
920
+ note,
921
+ relayed_by: relayedBy,
689
922
  };
690
923
  }
691
924
  const subject = Array.isArray(d.constrains) && typeof d.constrains[0] === "string" ? (d.constrains[0] as string) : undefined;
692
- fields.title = titleFor(point, subject, "answered", value);
925
+ fields.title = titleFor(point, subject, "answered", value, asked);
693
926
  const data = recordData(o, fields);
694
- const text = renderRecord(data, body(point, String(fields.title)));
927
+ const text = renderRecord(data, body(point, String(fields.title), asked));
695
928
  const onLedger = ledger.byPath.has(target.path);
696
- const message = `Decision point ${name}: answered ${show(value, type)} by ${tally.counted.join(", ")}`;
697
- const wrote = await write(o, before, target.path, text, false, !!opts.dryRun, { source: found.read.source, ledger, onLedger, message });
929
+ const message = `Decision point ${name}: answered ${show(value, type)} by ${tally.counted.join(", ")}${relayedBy ? `, relayed by ${relayedBy}` : ""}`;
930
+ const wrote = await write(o, before, target.path, text, false, !!opts.dryRun, { source: found.read.source, ledger, onLedger, message, who: { verb: "points answer", by: tally.counted.join(", ") } });
698
931
  const entry: RecordEntry = { ...target, state: "answered", data, valid: true, reasons: [], warnings: wrote.warnings };
699
932
  return {
700
933
  $schema: POINTS_WRITE_SCHEMA_ID,
@@ -713,3 +946,124 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
713
946
  return failure("answer", err);
714
947
  }
715
948
  }
949
+
950
+ // ── points retract ───────────────────────────────────────────────────────────
951
+
952
+ export interface RetractAnswerOptions {
953
+ cwd: string;
954
+ /** The answer record's id. */
955
+ id: string;
956
+ /** Who takes the answer back. They count toward the point's quorum as answerers do. */
957
+ by: string[];
958
+ /** Why, recorded on the retraction. */
959
+ note?: string;
960
+ kind?: string;
961
+ on?: string;
962
+ dryRun?: boolean;
963
+ }
964
+
965
+ /**
966
+ * People take an answer back: `points retract` (#3351, ws-084). The answer
967
+ * never changes in place. The question is escalated to the point's quorum
968
+ * again, open for people, and the answer as it was (its value, decider, who
969
+ * gave it, when, and its note) moves into the record's `retractions`, with who
970
+ * retracted it, when and why. A model's confirmed proposal also moves into the
971
+ * escalations, as when people answer otherwise. Retracting needs the point's
972
+ * quorum, counted as an answer is, and is refused in a steward's turn. People
973
+ * answer the question again with `points answer`.
974
+ */
975
+ export async function retractAnswer(opts: RetractAnswerOptions): Promise<PointsWriteDocument> {
976
+ try {
977
+ return await withWriteLock(opts.cwd, { verb: "points retract", by: (opts.by ?? []).join(", ") || null }, opts.dryRun, () => retractAnswerLocked(opts));
978
+ } catch (err) {
979
+ return failure("retract", err);
980
+ }
981
+ }
982
+
983
+ async function retractAnswerLocked(opts: RetractAnswerOptions): Promise<PointsWriteDocument> {
984
+ try {
985
+ refuseStewardTurn(opts.id, "retracts");
986
+ refuseUnidentified(scopeSource(opts.cwd), opts.by, "--by");
987
+ const found = await findQuestion(opts.cwd, opts.kind, opts.id);
988
+ const { opened, before, target } = found;
989
+ const ledger = found.read.ledger;
990
+ const { o } = opened;
991
+ const d = target.data;
992
+ if (target.state !== "answered") {
993
+ throw new PointsWriteError("answer-not-answered", `${opts.id} is ${target.state ?? "not answered"}, so there is no answer to retract: answer it with \`points answer ${opts.id}\``);
994
+ }
995
+ if (!answerFields(o.loaded.schema).retractions) {
996
+ throw new PointsWriteError("answer-field-unsupported", `the ${o.loaded.kind.name} kind's schema has no retractions field, so ${opts.id} can't keep the answer it would retract: copy point-answer.schema.json from @intentius/chant anew`);
997
+ }
998
+ const name = String(d.point);
999
+ const point = pointOf(opened.points, name);
1000
+ if (!point) throw new PointsWriteError("point-unknown", `${opts.id} answers ${name}, which ${opened.pointsFile} no longer declares, so no quorum can take the answer back: ask the point as it is declared now`);
1001
+ const quorum = quorumOf(point);
1002
+ const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "retract");
1003
+ const on = opts.on ?? today();
1004
+ const type = String(d.question_type);
1005
+ const decider = d.decider !== null && typeof d.decider === "object" && !Array.isArray(d.decider) ? (d.decider as Record<string, unknown>) : { kind: "quorum" };
1006
+ const answer = d.answer as string | boolean;
1007
+ const note = noteOf(opts.note);
1008
+ const retraction = {
1009
+ answer,
1010
+ decider,
1011
+ answered_by: Array.isArray(d.answered_by) && d.answered_by.length > 0 ? d.answered_by : undefined,
1012
+ answered_on: typeof d.answered_on === "string" ? d.answered_on : undefined,
1013
+ answer_note: typeof d.note === "string" ? d.note : undefined,
1014
+ answer_relayed_by: typeof d.relayed_by === "string" ? d.relayed_by : undefined,
1015
+ by: tally.counted,
1016
+ on,
1017
+ note,
1018
+ };
1019
+ const escalations = Array.isArray(d.escalations) ? [...(d.escalations as Escalation[])] : [];
1020
+ if (decider.kind === "model") {
1021
+ // The confirmed proposal goes back among the deciders that did not settle the question.
1022
+ escalations.push({
1023
+ kind: "model",
1024
+ backend: String(decider.backend),
1025
+ model: String(decider.model),
1026
+ answer,
1027
+ ...(d.probabilities ? { probabilities: d.probabilities as Record<string, number> } : {}),
1028
+ ...(typeof d.confidence === "number" ? { confidence: d.confidence } : {}),
1029
+ ...(typeof d.threshold === "number" ? { threshold: d.threshold } : {}),
1030
+ reason: `proposed ${show(answer, type)}, which people confirmed and then retracted`,
1031
+ ...(typeof d.reason === "string" && reasonFields(o.loaded.schema).escalation ? { model_reason: d.reason } : {}),
1032
+ });
1033
+ }
1034
+ const gone = new Set(["answer", "answered_by", "answered_on", "note", "relayed_by", "probabilities", "confidence", "threshold", "reason"]);
1035
+ const rest = Object.fromEntries(Object.entries(d).filter(([k]) => !gone.has(k)));
1036
+ const prior = Array.isArray(d.retractions) ? (d.retractions as unknown[]) : [];
1037
+ const subject = Array.isArray(d.constrains) && typeof d.constrains[0] === "string" ? (d.constrains[0] as string) : undefined;
1038
+ const asked = askedOf(d);
1039
+ const title = titleFor(point, subject, "escalated", undefined, asked);
1040
+ const data = recordData(o, {
1041
+ ...rest,
1042
+ title,
1043
+ state: "escalated",
1044
+ decider: { kind: "quorum", count: quorum.count, ...(quorum.roles ? { roles: quorum.roles } : {}) },
1045
+ escalations: escalations.length > 0 ? escalations : undefined,
1046
+ retractions: [...prior, Object.fromEntries(Object.entries(retraction).filter(([, v]) => v !== undefined))],
1047
+ });
1048
+ const text = renderRecord(data, body(point, title, asked));
1049
+ const onLedger = ledger.byPath.has(target.path);
1050
+ const message = `Decision point ${name}: ${show(answer, type)} retracted by ${tally.counted.join(", ")}`;
1051
+ const wrote = await write(o, before, target.path, text, false, !!opts.dryRun, { source: found.read.source, ledger, onLedger, message, who: { verb: "points retract", by: tally.counted.join(", ") } });
1052
+ const entry: RecordEntry = { ...target, state: "escalated", data, valid: true, reasons: [], warnings: wrote.warnings };
1053
+ return {
1054
+ $schema: POINTS_WRITE_SCHEMA_ID,
1055
+ contract: POINTS_WRITE_CONTRACT_VERSION,
1056
+ verb: "retract",
1057
+ kind: o.view,
1058
+ id: opts.id,
1059
+ path: target.path,
1060
+ reused: false,
1061
+ written: !opts.dryRun,
1062
+ dryRun: !!opts.dryRun,
1063
+ question: questionView(entry, point, onLedger ? (wrote.ledger ?? ledger.byPath.get(target.path)?.ledger ?? null) : null)!,
1064
+ ...(opts.dryRun ? { text } : {}),
1065
+ };
1066
+ } catch (err) {
1067
+ return failure("retract", err);
1068
+ }
1069
+ }