@intentius/chant 0.91.0 → 0.94.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/graph.d.ts.map +1 -1
  4. package/dist/cli/handlers/operator.d.ts +6 -0
  5. package/dist/cli/handlers/operator.d.ts.map +1 -1
  6. package/dist/cli/handlers/run.d.ts.map +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/mcp/workspace-plugins.d.ts +6 -6
  9. package/dist/cli/mcp/workspace-tools.d.ts +2 -1
  10. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  11. package/dist/cli/plugins.d.ts.map +1 -1
  12. package/dist/cli/registry.d.ts +12 -2
  13. package/dist/cli/registry.d.ts.map +1 -1
  14. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  15. package/dist/config.d.ts +22 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/lexicon-module.d.ts +22 -0
  18. package/dist/lexicon-module.d.ts.map +1 -1
  19. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  20. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  21. package/dist/lifecycle/run-ledger.d.ts +2 -0
  22. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  23. package/dist/lifecycle/symptoms.d.ts +51 -0
  24. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  25. package/dist/lint/rules/index.d.ts +4 -2
  26. package/dist/lint/rules/index.d.ts.map +1 -1
  27. package/dist/lint/rules/op/index.d.ts +14 -3
  28. package/dist/lint/rules/op/index.d.ts.map +1 -1
  29. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  31. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  33. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  35. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  36. package/dist/op/activities/activity-contracts.d.ts +201 -0
  37. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  38. package/dist/op/activities/apply.d.ts.map +1 -1
  39. package/dist/op/activities/change-coverage.d.ts +44 -0
  40. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  41. package/dist/op/activities/converge.d.ts +24 -1
  42. package/dist/op/activities/converge.d.ts.map +1 -1
  43. package/dist/op/activities/decide.d.ts +107 -0
  44. package/dist/op/activities/decide.d.ts.map +1 -0
  45. package/dist/op/activities/index.d.ts +10 -0
  46. package/dist/op/activities/index.d.ts.map +1 -1
  47. package/dist/op/activities/shell.d.ts +32 -0
  48. package/dist/op/activities/shell.d.ts.map +1 -1
  49. package/dist/op/activities/source-release.d.ts +86 -0
  50. package/dist/op/activities/source-release.d.ts.map +1 -0
  51. package/dist/op/activities/source-rollback.d.ts +100 -0
  52. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  53. package/dist/op/activities/work-evidence.d.ts +42 -0
  54. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  55. package/dist/op/activity-contract.d.ts +1 -0
  56. package/dist/op/activity-contract.d.ts.map +1 -1
  57. package/dist/op/activity-profiles.d.ts +16 -0
  58. package/dist/op/activity-profiles.d.ts.map +1 -1
  59. package/dist/op/builders.d.ts +84 -2
  60. package/dist/op/builders.d.ts.map +1 -1
  61. package/dist/op/composites/converge-op.d.ts +31 -4
  62. package/dist/op/composites/converge-op.d.ts.map +1 -1
  63. package/dist/op/decide-backend.d.ts +116 -0
  64. package/dist/op/decide-backend.d.ts.map +1 -0
  65. package/dist/op/decide-config.d.ts +120 -0
  66. package/dist/op/decide-config.d.ts.map +1 -0
  67. package/dist/op/decide-read-inputs.d.ts +40 -0
  68. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  69. package/dist/op/gate-wait.d.ts +43 -0
  70. package/dist/op/gate-wait.d.ts.map +1 -0
  71. package/dist/op/gate.d.ts +5 -2
  72. package/dist/op/gate.d.ts.map +1 -1
  73. package/dist/op/index.d.ts +11 -4
  74. package/dist/op/index.d.ts.map +1 -1
  75. package/dist/op/local-executor.d.ts +29 -4
  76. package/dist/op/local-executor.d.ts.map +1 -1
  77. package/dist/op/local-output.d.ts +2 -0
  78. package/dist/op/local-output.d.ts.map +1 -1
  79. package/dist/op/op-ir.d.ts +2 -0
  80. package/dist/op/op-ir.d.ts.map +1 -1
  81. package/dist/op/op-verb-class.d.ts.map +1 -1
  82. package/dist/op/operator.d.ts +97 -1
  83. package/dist/op/operator.d.ts.map +1 -1
  84. package/dist/op/runtime.d.ts +37 -5
  85. package/dist/op/runtime.d.ts.map +1 -1
  86. package/dist/op/runtimes/local.d.ts.map +1 -1
  87. package/dist/op/source-archive.d.ts +63 -0
  88. package/dist/op/source-archive.d.ts.map +1 -0
  89. package/dist/op/steward-points.d.ts +136 -0
  90. package/dist/op/steward-points.d.ts.map +1 -0
  91. package/dist/op/steward-turn.d.ts +53 -0
  92. package/dist/op/steward-turn.d.ts.map +1 -0
  93. package/dist/op/steward.d.ts +22 -1
  94. package/dist/op/steward.d.ts.map +1 -1
  95. package/dist/op/types.d.ts +7 -0
  96. package/dist/op/types.d.ts.map +1 -1
  97. package/dist/op/work-lease-run.d.ts +1 -1
  98. package/dist/op/work-lease-run.d.ts.map +1 -1
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  100. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  101. package/dist/workspace/answers-ledger.d.ts +76 -0
  102. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  103. package/dist/workspace/box-intent.d.ts +85 -0
  104. package/dist/workspace/box-intent.d.ts.map +1 -0
  105. package/dist/workspace/changes-cli.d.ts +16 -0
  106. package/dist/workspace/changes-cli.d.ts.map +1 -0
  107. package/dist/workspace/changes.d.ts +200 -0
  108. package/dist/workspace/changes.d.ts.map +1 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  118. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  119. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  120. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  121. package/dist/workspace/chant-migrations.d.ts +104 -0
  122. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  123. package/dist/workspace/checks/boxes.d.ts +47 -1
  124. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  125. package/dist/workspace/checks/diagrams.d.ts +31 -0
  126. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  127. package/dist/workspace/checks/records.d.ts +7 -0
  128. package/dist/workspace/checks/records.d.ts.map +1 -1
  129. package/dist/workspace/checks.d.ts +7 -0
  130. package/dist/workspace/checks.d.ts.map +1 -1
  131. package/dist/workspace/compose-graph.d.ts +2 -0
  132. package/dist/workspace/compose-graph.d.ts.map +1 -1
  133. package/dist/workspace/conformance/index.d.ts.map +1 -1
  134. package/dist/workspace/decide.d.ts +49 -4
  135. package/dist/workspace/decide.d.ts.map +1 -1
  136. package/dist/workspace/decision-points.schema.json +3 -3
  137. package/dist/workspace/declaration.d.ts +58 -0
  138. package/dist/workspace/declaration.d.ts.map +1 -1
  139. package/dist/workspace/declaration.schema.json +119 -1
  140. package/dist/workspace/declared-kinds.d.ts +20 -0
  141. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  142. package/dist/workspace/generated-files.d.ts +7 -0
  143. package/dist/workspace/generated-files.d.ts.map +1 -1
  144. package/dist/workspace/intent-joins.d.ts +5 -5
  145. package/dist/workspace/intent.d.ts.map +1 -1
  146. package/dist/workspace/lineage-check.d.ts +3 -0
  147. package/dist/workspace/lineage-check.d.ts.map +1 -1
  148. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  149. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  150. package/dist/workspace/ls.d.ts +39 -1
  151. package/dist/workspace/ls.d.ts.map +1 -1
  152. package/dist/workspace/point-answer.schema.json +95 -0
  153. package/dist/workspace/points-cli.d.ts +2 -0
  154. package/dist/workspace/points-cli.d.ts.map +1 -1
  155. package/dist/workspace/points.d.ts +4 -6
  156. package/dist/workspace/points.d.ts.map +1 -1
  157. package/dist/workspace/reason-codes.d.ts +16 -2
  158. package/dist/workspace/reason-codes.d.ts.map +1 -1
  159. package/dist/workspace/records-cli.d.ts +11 -1
  160. package/dist/workspace/records-cli.d.ts.map +1 -1
  161. package/dist/workspace/records-close.d.ts +1 -1
  162. package/dist/workspace/records-close.d.ts.map +1 -1
  163. package/dist/workspace/records-write.d.ts +11 -5
  164. package/dist/workspace/records-write.d.ts.map +1 -1
  165. package/dist/workspace/records.d.ts +34 -4
  166. package/dist/workspace/records.d.ts.map +1 -1
  167. package/dist/workspace/status-gates.d.ts +7 -2
  168. package/dist/workspace/status-gates.d.ts.map +1 -1
  169. package/dist/workspace/status-stewards.d.ts +65 -1
  170. package/dist/workspace/status-stewards.d.ts.map +1 -1
  171. package/dist/workspace/status.d.ts +24 -0
  172. package/dist/workspace/status.d.ts.map +1 -1
  173. package/dist/workspace/work-cli.d.ts +113 -2
  174. package/dist/workspace/work-cli.d.ts.map +1 -1
  175. package/dist/workspace/work-evidence.d.ts +74 -0
  176. package/dist/workspace/work-evidence.d.ts.map +1 -0
  177. package/dist/workspace/work.d.ts +43 -1
  178. package/dist/workspace/work.d.ts.map +1 -1
  179. package/package.json +1 -1
  180. package/src/cli/commands/audit.test.ts +9 -1
  181. package/src/cli/commands/build.ts +3 -2
  182. package/src/cli/commands/lint.ts +7 -1
  183. package/src/cli/handlers/graph.test.ts +20 -0
  184. package/src/cli/handlers/graph.ts +16 -2
  185. package/src/cli/handlers/operator.ts +34 -2
  186. package/src/cli/handlers/run.test.ts +154 -2
  187. package/src/cli/handlers/run.ts +121 -2
  188. package/src/cli/main.test.ts +10 -0
  189. package/src/cli/main.ts +23 -3
  190. package/src/cli/mcp/workspace-plugins.ts +6 -6
  191. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  192. package/src/cli/mcp/workspace-tools.ts +62 -4
  193. package/src/cli/plugins.ts +11 -3
  194. package/src/cli/registry.ts +12 -2
  195. package/src/cli/serve-mcp-workspace.test.ts +6 -6
  196. package/src/codegen/pinned-upgrade.ts +2 -1
  197. package/src/codegen/release-wiring.test.ts +49 -2
  198. package/src/components/capability-plugin-loader.ts +3 -3
  199. package/src/components/cli-support.ts +2 -2
  200. package/src/config.ts +11 -0
  201. package/src/lexicon-module.ts +56 -1
  202. package/src/lexicon-package.test.ts +96 -0
  203. package/src/lifecycle/converge-ledger.ts +20 -0
  204. package/src/lifecycle/run-ledger.ts +6 -0
  205. package/src/lifecycle/symptoms.ts +95 -0
  206. package/src/lint/rules/index.ts +6 -2
  207. package/src/lint/rules/op/index.ts +17 -4
  208. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  209. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  210. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  211. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  212. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  213. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  214. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  215. package/src/op/activities/activity-contracts.ts +190 -1
  216. package/src/op/activities/apply.ts +7 -6
  217. package/src/op/activities/change-coverage.ts +60 -0
  218. package/src/op/activities/converge.test.ts +55 -1
  219. package/src/op/activities/converge.ts +208 -29
  220. package/src/op/activities/decide.live.test.ts +78 -0
  221. package/src/op/activities/decide.test.ts +308 -0
  222. package/src/op/activities/decide.ts +267 -0
  223. package/src/op/activities/index.ts +21 -0
  224. package/src/op/activities/shell.test.ts +91 -1
  225. package/src/op/activities/shell.ts +91 -2
  226. package/src/op/activities/source-release.test.ts +118 -0
  227. package/src/op/activities/source-release.ts +168 -0
  228. package/src/op/activities/source-rollback.test.ts +131 -0
  229. package/src/op/activities/source-rollback.ts +239 -0
  230. package/src/op/activities/work-evidence.ts +62 -0
  231. package/src/op/activity-contract-registry.test.ts +2 -0
  232. package/src/op/activity-contract-registry.ts +2 -2
  233. package/src/op/activity-contract.test.ts +12 -0
  234. package/src/op/activity-contract.ts +6 -1
  235. package/src/op/activity-profiles.ts +29 -0
  236. package/src/op/activity-registry.test.ts +1 -1
  237. package/src/op/activity-registry.ts +2 -2
  238. package/src/op/builders.ts +119 -5
  239. package/src/op/command-gate.test.ts +192 -0
  240. package/src/op/composites/converge-op.test.ts +27 -1
  241. package/src/op/composites/converge-op.ts +69 -15
  242. package/src/op/decide-backend.test.ts +44 -0
  243. package/src/op/decide-backend.ts +212 -0
  244. package/src/op/decide-config.test.ts +52 -0
  245. package/src/op/decide-config.ts +80 -0
  246. package/src/op/decide-read-inputs.ts +117 -0
  247. package/src/op/gate-approval.ts +2 -2
  248. package/src/op/gate-wait.ts +57 -0
  249. package/src/op/gate.ts +8 -3
  250. package/src/op/generate-pipeline.ts +2 -2
  251. package/src/op/index.ts +18 -3
  252. package/src/op/local-executor.ts +151 -15
  253. package/src/op/local-output.test.ts +33 -0
  254. package/src/op/local-output.ts +20 -2
  255. package/src/op/op-ir.test.ts +10 -0
  256. package/src/op/op-ir.ts +4 -0
  257. package/src/op/op-verb-class.ts +1 -0
  258. package/src/op/operator.ts +290 -11
  259. package/src/op/runtime.ts +31 -8
  260. package/src/op/runtimes/local.ts +3 -1
  261. package/src/op/source-archive.ts +161 -0
  262. package/src/op/steward-points.test.ts +364 -0
  263. package/src/op/steward-points.ts +194 -0
  264. package/src/op/steward-turn.ts +79 -0
  265. package/src/op/steward.test.ts +87 -3
  266. package/src/op/steward.ts +25 -1
  267. package/src/op/types.ts +7 -0
  268. package/src/op/work-lease-run.test.ts +20 -1
  269. package/src/op/work-lease-run.ts +2 -2
  270. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  271. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  272. package/src/workspace/answers-ledger.ts +118 -0
  273. package/src/workspace/box-intent.test.ts +205 -0
  274. package/src/workspace/box-intent.ts +159 -0
  275. package/src/workspace/changes-cli.ts +56 -0
  276. package/src/workspace/changes.schema.json +472 -0
  277. package/src/workspace/changes.test.ts +315 -0
  278. package/src/workspace/changes.ts +512 -0
  279. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  280. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  281. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  282. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  283. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  284. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  285. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  286. package/src/workspace/chant-migrations.ts +169 -0
  287. package/src/workspace/check-contract.test.ts +4 -2
  288. package/src/workspace/check.schema.json +19 -5
  289. package/src/workspace/checks/boxes.test.ts +82 -2
  290. package/src/workspace/checks/boxes.ts +202 -1
  291. package/src/workspace/checks/diagrams.test.ts +134 -0
  292. package/src/workspace/checks/diagrams.ts +108 -0
  293. package/src/workspace/checks/records.ts +47 -0
  294. package/src/workspace/checks.ts +20 -2
  295. package/src/workspace/compose-graph.ts +2 -0
  296. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  297. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  298. package/src/workspace/conformance/index.ts +5 -0
  299. package/src/workspace/decide.test.ts +1 -1
  300. package/src/workspace/decide.ts +156 -23
  301. package/src/workspace/decision-points.schema.json +3 -3
  302. package/src/workspace/declaration.schema.json +119 -1
  303. package/src/workspace/declaration.test.ts +58 -0
  304. package/src/workspace/declaration.ts +112 -0
  305. package/src/workspace/declared-kinds.test.ts +31 -5
  306. package/src/workspace/declared-kinds.ts +46 -1
  307. package/src/workspace/generated-files.ts +14 -0
  308. package/src/workspace/graph-cli.ts +1 -1
  309. package/src/workspace/graph.schema.json +9 -1
  310. package/src/workspace/intent-joins.test.ts +5 -5
  311. package/src/workspace/intent-joins.ts +5 -5
  312. package/src/workspace/intent.schema.json +8 -2
  313. package/src/workspace/intent.ts +3 -10
  314. package/src/workspace/lineage-check.ts +16 -1
  315. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  316. package/src/workspace/lineage-upgrade.test.ts +70 -0
  317. package/src/workspace/lineage-upgrade.ts +81 -10
  318. package/src/workspace/ls-contract.test.ts +57 -0
  319. package/src/workspace/ls.schema.json +46 -1
  320. package/src/workspace/ls.ts +62 -3
  321. package/src/workspace/points-cli.ts +17 -3
  322. package/src/workspace/points-write.schema.json +11 -1
  323. package/src/workspace/points.schema.json +51 -0
  324. package/src/workspace/points.test.ts +16 -15
  325. package/src/workspace/points.ts +5 -17
  326. package/src/workspace/read-contract.test.ts +9 -1
  327. package/src/workspace/reason-codes.test.ts +18 -8
  328. package/src/workspace/reason-codes.ts +21 -2
  329. package/src/workspace/records-amend.schema.json +2 -0
  330. package/src/workspace/records-cli.ts +27 -4
  331. package/src/workspace/records-close.schema.json +2 -0
  332. package/src/workspace/records-formats.test.ts +15 -15
  333. package/src/workspace/records-new.schema.json +2 -0
  334. package/src/workspace/records-review.schema.json +2 -0
  335. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  336. package/src/workspace/records-sessions-write.test.ts +2 -34
  337. package/src/workspace/records-since.test.ts +8 -7
  338. package/src/workspace/records-write.e2e.test.ts +108 -0
  339. package/src/workspace/records-write.test.ts +32 -41
  340. package/src/workspace/records-write.ts +33 -15
  341. package/src/workspace/records.schema.json +36 -2
  342. package/src/workspace/records.test.ts +49 -4
  343. package/src/workspace/records.ts +109 -3
  344. package/src/workspace/status-contract.test.ts +14 -8
  345. package/src/workspace/status-gates.ts +9 -4
  346. package/src/workspace/status-stewards.ts +155 -5
  347. package/src/workspace/status.schema.json +129 -8
  348. package/src/workspace/status.ts +46 -1
  349. package/src/workspace/work-acceptance.test.ts +274 -0
  350. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  351. package/src/workspace/work-cli.ts +193 -10
  352. package/src/workspace/work-evidence.schema.json +212 -0
  353. package/src/workspace/work-evidence.ts +169 -0
  354. package/src/workspace/work-history.schema.json +349 -0
  355. package/src/workspace/work-history.test.ts +156 -0
  356. package/src/workspace/{work-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
  357. package/src/workspace/work.test.ts +20 -2
  358. package/src/workspace/work.ts +100 -1
  359. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  360. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  361. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * `chant workspace work claim|renew|release <id> --holder <name>` (#2732,
3
- * ws-055): the work lease, who is working on which work item.
3
+ * ws-055): the work lease, who is working on which work item. And `chant
4
+ * workspace work history <id>` (#2785): the item's lease history, each claim
5
+ * and how it ended, a read-contract output (`work-history.schema.json`).
4
6
  *
5
7
  * The lease itself is `../lifecycle/work-lease.ts`, the operator lease under
6
8
  * the key `work/<id>`. This file finds the work item first: in the work kind
@@ -24,6 +26,9 @@ import type { CommandContext } from "../cli/registry";
24
26
  import { parseDuration } from "../op/duration";
25
27
  import {
26
28
  claimWorkLease,
29
+ leaseHistoryPath,
30
+ listWorkLeases,
31
+ readLeaseHistory,
27
32
  releaseWorkLease,
28
33
  renewWorkLease,
29
34
  workLeaseRef,
@@ -32,11 +37,13 @@ import {
32
37
  type WorkLeaseRefusal,
33
38
  type WorkLeaseResult,
34
39
  type LeaseHistoryWrite,
40
+ type LeaseHistoryRecord,
41
+ type WorkLeaseState,
35
42
  } from "../lifecycle/work-lease";
36
43
  import { resolveMemberLedger } from "../lifecycle/member-ledger";
37
- import { WorkspaceReadError } from "./declaration";
44
+ import { readerVersion, WorkspaceReadError } from "./declaration";
38
45
  import { declaredKindFiles, readRecordsFor } from "./records-cli";
39
- import { loadRecordKind, RecordReadError, type LoadedRecordKind } from "./records";
46
+ import { loadRecordKind, RecordReadError, type LoadedRecordKind, type RecordEntry } from "./records";
40
47
  import { gitRoot } from "./record-source";
41
48
  import type { ReasonCode } from "./reason-codes";
42
49
 
@@ -48,7 +55,10 @@ export const WORK_LEASE_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/w
48
55
 
49
56
  export { WORK_LEASE_REFUSALS } from "../lifecycle/work-lease";
50
57
 
51
- const USAGE = "chant workspace work claim|renew|release <id> --holder <name> [--kind <kind file>] [--ttl <seconds|duration>] [--token <token>] [--outcome <text>] [--note <text>] [--json]";
58
+ const USAGE = [
59
+ "chant workspace work claim|renew|release <id> --holder <name> [--kind <kind file>] [--ttl <seconds|duration>] [--token <token>] [--outcome <text>] [--note <text>] [--json]",
60
+ "chant workspace work history <id> [--kind <kind file>] [--json]",
61
+ ].join("\n");
52
62
 
53
63
  /** Exit code when the lease was refused rather than the command failing. */
54
64
  export const EXIT_LEASE_REFUSED = 2;
@@ -94,7 +104,8 @@ export type WorkLeaseDocument = DocHead &
94
104
  | { error: { code: WorkErrorCode; message: string } }
95
105
  );
96
106
 
97
- class WorkError extends Error {
107
+ /** Why a work command could not run, with its code. */
108
+ export class WorkError extends Error {
98
109
  constructor(
99
110
  readonly code: WorkErrorCode,
100
111
  message: string,
@@ -103,8 +114,19 @@ class WorkError extends Error {
103
114
  }
104
115
  }
105
116
 
106
- /** The work kind holding `id`: the one `kind` names, or the one declared work kind with a record `id`. */
107
- async function findWorkItem(id: string, cwd: string, kind: string | undefined): Promise<{ loaded: LoadedRecordKind; state: string | null; root: string }> {
117
+ /** A work item found by id: its kind, its state, its record, and the repository root its path is from. */
118
+ export interface FoundWorkItem {
119
+ loaded: LoadedRecordKind;
120
+ state: string | null;
121
+ record: RecordEntry;
122
+ root: string;
123
+ }
124
+
125
+ /**
126
+ * The work kind holding `id`: the one `kind` names, or the one declared work
127
+ * kind with a record `id`. Throws a {@link WorkError}.
128
+ */
129
+ export async function findWorkItem(id: string, cwd: string, kind: string | undefined): Promise<FoundWorkItem> {
108
130
  let candidates: string[];
109
131
  if (kind !== undefined) {
110
132
  candidates = [kind];
@@ -116,7 +138,7 @@ async function findWorkItem(id: string, cwd: string, kind: string | undefined):
116
138
  throw new WorkError("work-kind-missing", `${err.code}: ${err.describe()}; without --kind, the declaration names the work kind`);
117
139
  }
118
140
  }
119
- const found: { loaded: LoadedRecordKind; state: string | null; root: string }[] = [];
141
+ const found: FoundWorkItem[] = [];
120
142
  let workKinds = 0;
121
143
  for (const file of candidates) {
122
144
  let loaded: LoadedRecordKind;
@@ -140,7 +162,7 @@ async function findWorkItem(id: string, cwd: string, kind: string | undefined):
140
162
  throw err;
141
163
  }
142
164
  const record = read.result.records.find((r) => r.id === id);
143
- if (record) found.push({ loaded, state: record.state, root: read.root });
165
+ if (record) found.push({ loaded, state: record.state, record, root: read.root });
144
166
  }
145
167
  if (workKinds === 0) throw new WorkError("work-kind-missing", "the declaration names no work kind; pass --kind <kind file>");
146
168
  if (found.length === 0) throw new WorkError("work-item-unknown", `no work record has the id ${id}`);
@@ -199,16 +221,177 @@ function strip(l: WorkLease): Omit<WorkLease, "item"> {
199
221
  return { holder: l.holder, token: l.token, acquiredAt: l.acquiredAt, expiresAt: l.expiresAt };
200
222
  }
201
223
 
224
+ // ── work history (#2785) ────────────────────────────────────────────────────
225
+
226
+ /** The version of the `work history` output this chant writes: read contract 1. */
227
+ export const WORK_HISTORY_CONTRACT_VERSION = 1;
228
+
229
+ /** `$id` of the JSON Schema for `work history --json`, shipped beside this file. */
230
+ export const WORK_HISTORY_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/work-history/v1/work-history.schema.json";
231
+
232
+ /** Why `work history` read nothing. Closed. */
233
+ export const WORK_HISTORY_ERROR_CODES = [
234
+ "kind-unreadable",
235
+ "work-kind-missing",
236
+ "work-kind-ambiguous",
237
+ "work-item-unknown",
238
+ "not-a-git-repository",
239
+ ] as const satisfies readonly ReasonCode[];
240
+ export type WorkHistoryErrorCode = (typeof WORK_HISTORY_ERROR_CODES)[number];
241
+
242
+ /**
243
+ * How a claim ended. `released`: its holder, or someone closing out an
244
+ * expired lease, gave it back, with an outcome when one was given. `held`: it
245
+ * is the live lease now. `expired`: it ran out and nobody released it or
246
+ * claimed the item since. `lost`: it ran out unreleased and another claim
247
+ * took the item.
248
+ */
249
+ export type ClaimEnd = "released" | "held" | "expired" | "lost";
250
+
251
+ /** One claim of a work item: one fencing token, from its claim to how it ended. */
252
+ export interface LeaseClaim {
253
+ token: string;
254
+ holder: string;
255
+ acquiredAt: string;
256
+ /** The expiry its last claim or renew set. */
257
+ expiresAt: string;
258
+ renewals: number;
259
+ ended: ClaimEnd;
260
+ /** The release, when it was released. */
261
+ release: { by: string; at: string; outcome: string | null; note: string | null } | null;
262
+ }
263
+
264
+ /** What `work history --json` prints. */
265
+ export type WorkHistoryDocument =
266
+ | {
267
+ $schema: string;
268
+ contract: number;
269
+ chant: string;
270
+ item: string;
271
+ /** The work kind file, from the repository root. */
272
+ kind: string;
273
+ /** The lease ref. */
274
+ ref: string;
275
+ /** The history file on the branch, under the member's prefix when the kind's member has one. */
276
+ ledger: { branch: string; path: string };
277
+ /** The lease ref's record now, live or expired, or null when there is none. Read from local refs, without fetching. */
278
+ lease: (Omit<WorkLease, "item"> & { state: "active" | "expired" }) | null;
279
+ /** Each claim, oldest first. */
280
+ claims: LeaseClaim[];
281
+ /** Every line of the history, oldest first. */
282
+ events: LeaseHistoryRecord[];
283
+ /** Lines of the history that are not lease events, left out. */
284
+ malformed: number;
285
+ summary: { claims: number; released: number; held: number; expired: number; lost: number; outcomes: Record<string, number> };
286
+ }
287
+ | { $schema: string; contract: number; chant: string; error: { code: WorkHistoryErrorCode; message: string } };
288
+
289
+ /** Fold a history into its claims: one per token, in the order they were first seen. */
290
+ export function claimsOf(events: LeaseHistoryRecord[], live: Pick<WorkLeaseState, "token" | "state"> | null): LeaseClaim[] {
291
+ const byToken = new Map<string, LeaseClaim>();
292
+ const order: string[] = [];
293
+ for (const e of events) {
294
+ let c = byToken.get(e.token);
295
+ if (!c) {
296
+ c = { token: e.token, holder: e.holder, acquiredAt: e.acquiredAt, expiresAt: e.expiresAt, renewals: 0, ended: "expired", release: null };
297
+ byToken.set(e.token, c);
298
+ order.push(e.token);
299
+ }
300
+ if (e.event === "renew") c.renewals++;
301
+ if (e.event !== "release") c.expiresAt = e.expiresAt;
302
+ if (e.event === "release") c.release = { by: e.by, at: e.timestamp, outcome: e.outcome ?? null, note: e.note ?? null };
303
+ }
304
+ return order.map((token, i) => {
305
+ const c = byToken.get(token)!;
306
+ if (c.release) c.ended = "released";
307
+ else if (live && live.token === token) c.ended = live.state === "active" ? "held" : "expired";
308
+ else if (i < order.length - 1) c.ended = "lost";
309
+ else c.ended = "expired";
310
+ return c;
311
+ });
312
+ }
313
+
314
+ /**
315
+ * Read work item `id`'s lease history (#2785): every claim, renew and release
316
+ * on `_leases/<id>.jsonl` in the ledger of the member owning the work kind,
317
+ * folded into claims. Read from the local `chant/lifecycle` branch and lease
318
+ * refs; it never fetches, like the other read-contract reads.
319
+ */
320
+ export async function workHistory(req: { id: string; cwd: string; kind?: string }): Promise<WorkHistoryDocument> {
321
+ const head = { $schema: WORK_HISTORY_OUTPUT_SCHEMA_ID, contract: WORK_HISTORY_CONTRACT_VERSION, chant: readerVersion() };
322
+ try {
323
+ if (!WORK_ITEM_ID_PATTERN.test(req.id) || req.id.includes("..")) throw new WorkError("work-item-unknown", `${JSON.stringify(req.id)} is not a work item id`);
324
+ if (!gitRoot(req.cwd)) throw new WorkError("not-a-git-repository", "a work lease's history is on the chant/lifecycle branch, and this directory is not in a git repository");
325
+ const { loaded, root } = await findWorkItem(req.id, req.cwd, req.kind);
326
+ const leaseCwd = dirname(loaded.file);
327
+ const { prefix } = await resolveMemberLedger(leaseCwd);
328
+ const { records: events, malformed } = await readLeaseHistory(req.id, { cwd: leaseCwd });
329
+ const live = (await listWorkLeases({ cwd: leaseCwd, memberPrefix: prefix })).find((l) => l.item === req.id) ?? null;
330
+ const claims = claimsOf(events, live);
331
+ const count = (end: ClaimEnd) => claims.filter((c) => c.ended === end).length;
332
+ const outcomes: Record<string, number> = {};
333
+ for (const c of claims) if (c.release?.outcome) outcomes[c.release.outcome] = (outcomes[c.release.outcome] ?? 0) + 1;
334
+ return {
335
+ ...head,
336
+ item: req.id,
337
+ kind: relative(root, loaded.file).split("\\").join("/"),
338
+ ref: workLeaseRef(req.id, prefix),
339
+ ledger: { branch: "chant/lifecycle", path: leaseHistoryPath(req.id, prefix) },
340
+ lease: live ? { holder: live.holder, token: live.token, acquiredAt: live.acquiredAt, expiresAt: live.expiresAt, state: live.state } : null,
341
+ claims,
342
+ events,
343
+ malformed,
344
+ summary: { claims: claims.length, released: count("released"), held: count("held"), expired: count("expired"), lost: count("lost"), outcomes },
345
+ };
346
+ } catch (err) {
347
+ if (err instanceof WorkError) return { ...head, error: { code: err.code as WorkHistoryErrorCode, message: err.message } };
348
+ throw err;
349
+ }
350
+ }
351
+
352
+ /** The history as lines for a person. */
353
+ export function formatWorkHistory(doc: Extract<WorkHistoryDocument, { claims: unknown }>): string {
354
+ const lines = [`${doc.item}: ${doc.ledger.branch}:${doc.ledger.path}`];
355
+ for (const c of doc.claims) {
356
+ const how = c.release ? `released${c.release.outcome ? ` ${c.release.outcome}` : ""} by ${c.release.by} at ${c.release.at}` : c.ended === "held" ? `held until ${c.expiresAt}` : `${c.ended}, never released (expired ${c.expiresAt})`;
357
+ lines.push(` ${c.acquiredAt} ${c.holder} token ${c.token} ${c.renewals} renewal${c.renewals === 1 ? "" : "s"} ${how}`);
358
+ }
359
+ if (doc.claims.length === 0) lines.push(" never claimed");
360
+ if (doc.malformed > 0) lines.push(` ${doc.malformed} malformed line${doc.malformed === 1 ? "" : "s"} left out`);
361
+ return lines.join("\n");
362
+ }
363
+
364
+ async function runWorkHistory(ctx: CommandContext): Promise<number> {
365
+ const { args } = ctx;
366
+ const id = args.extraPositional2;
367
+ if (!id) {
368
+ console.error(formatError({ message: "history needs a work item id", hint: USAGE }));
369
+ return 1;
370
+ }
371
+ for (const [flag, v] of [["--holder", args.holder], ["--ttl", args.ttl], ["--token", args.token], ["--outcome", args.outcome], ["--note", args.note]] as const) {
372
+ if (v !== undefined) {
373
+ console.error(formatError({ message: `history is a read and takes no ${flag}`, hint: USAGE }));
374
+ return 1;
375
+ }
376
+ }
377
+ const doc = await workHistory({ id, cwd: process.cwd(), kind: args.kind });
378
+ if (args.json) console.log(JSON.stringify(doc, null, 2));
379
+ else if ("error" in doc) console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
380
+ else console.log(formatWorkHistory(doc));
381
+ return "error" in doc ? 1 : 0;
382
+ }
383
+
202
384
  export async function runWorkspaceWork(ctx: CommandContext): Promise<number> {
203
385
  const { args } = ctx;
204
386
  const verb = args.extraPositional;
387
+ if (verb === "history") return runWorkHistory(ctx);
205
388
  const fail = (message: string): number => {
206
389
  if (args.json) console.log(JSON.stringify({ $schema: WORK_LEASE_OUTPUT_SCHEMA_ID, contract: WORK_LEASE_CONTRACT_VERSION, error: { code: "write-usage-invalid", message } }, null, 2));
207
390
  else console.error(formatError({ message, hint: USAGE }));
208
391
  return 1;
209
392
  };
210
393
  if (verb !== "claim" && verb !== "renew" && verb !== "release") {
211
- return fail(verb ? `chant workspace work takes claim, renew or release, not ${verb}` : "chant workspace work needs claim, renew or release");
394
+ return fail(verb ? `chant workspace work takes claim, renew, release or history, not ${verb}` : "chant workspace work needs claim, renew, release or history");
212
395
  }
213
396
  const id = args.extraPositional2;
214
397
  if (!id) return fail(`${verb} needs a work item id`);
@@ -0,0 +1,212 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://intentius.io/chant/schemas/workspace/work-evidence/v1/work-evidence.schema.json",
4
+ "title": "workEvidence result",
5
+ "description": "What attachWorkEvidence, which the workEvidence Op activity runs, returns (#2772): the evidence entry it appended to a work item's acceptance criterion, under the run's work lease, or the reason nothing was written. Version 1. The activity returns the result and fails its step with the refusal's code. Readers ignore fields they do not know; a field is only ever added within a version. The error codes are a closed list, each in the one closed list of `reason-codes.ts`.",
6
+ "oneOf": [
7
+ {
8
+ "$ref": "#/$defs/result"
9
+ },
10
+ {
11
+ "$ref": "#/$defs/failure"
12
+ }
13
+ ],
14
+ "$defs": {
15
+ "result": {
16
+ "description": "The entry was appended to the record's pins field, through records amend.",
17
+ "type": "object",
18
+ "required": [
19
+ "$schema",
20
+ "contract",
21
+ "item",
22
+ "kind",
23
+ "path",
24
+ "evidence",
25
+ "acceptance"
26
+ ],
27
+ "properties": {
28
+ "$schema": {
29
+ "const": "https://intentius.io/chant/schemas/workspace/work-evidence/v1/work-evidence.schema.json"
30
+ },
31
+ "contract": {
32
+ "const": 1
33
+ },
34
+ "item": {
35
+ "type": "string",
36
+ "description": "The work item's id."
37
+ },
38
+ "kind": {
39
+ "type": "string",
40
+ "description": "The work kind file, from the repository root."
41
+ },
42
+ "path": {
43
+ "type": "string",
44
+ "description": "The work record, from the repository root."
45
+ },
46
+ "evidence": {
47
+ "type": "object",
48
+ "description": "The entry appended: a title, a url or a path pinned by sha256, as_of, and the criterion, its verification, the result and by, the lease holder.",
49
+ "required": [
50
+ "title",
51
+ "criterion",
52
+ "verification",
53
+ "result",
54
+ "by"
55
+ ],
56
+ "properties": {
57
+ "title": {
58
+ "type": "string"
59
+ },
60
+ "url": {
61
+ "type": "string"
62
+ },
63
+ "path": {
64
+ "type": "string"
65
+ },
66
+ "sha256": {
67
+ "type": "string",
68
+ "pattern": "^[0-9a-f]{64}$"
69
+ },
70
+ "as_of": {
71
+ "type": "string"
72
+ },
73
+ "criterion": {
74
+ "type": "string"
75
+ },
76
+ "verification": {
77
+ "enum": [
78
+ "unit",
79
+ "integration",
80
+ "e2e",
81
+ "runtime"
82
+ ],
83
+ "description": "The criterion's verification. Never manual: a run under the lease is the implementer."
84
+ },
85
+ "result": {
86
+ "enum": [
87
+ "pass",
88
+ "fail"
89
+ ]
90
+ },
91
+ "by": {
92
+ "type": "string",
93
+ "description": "The lease holder that attached it."
94
+ }
95
+ }
96
+ },
97
+ "acceptance": {
98
+ "type": "object",
99
+ "description": "The record's criteria with the entry counted, as records --json reports them.",
100
+ "required": [
101
+ "met",
102
+ "total",
103
+ "criteria"
104
+ ],
105
+ "properties": {
106
+ "met": {
107
+ "type": "integer",
108
+ "minimum": 0
109
+ },
110
+ "total": {
111
+ "type": "integer",
112
+ "minimum": 0
113
+ },
114
+ "criteria": {
115
+ "type": "array",
116
+ "items": {
117
+ "type": "object",
118
+ "required": [
119
+ "id",
120
+ "verification",
121
+ "met"
122
+ ],
123
+ "properties": {
124
+ "id": {
125
+ "type": "string"
126
+ },
127
+ "verification": {
128
+ "enum": [
129
+ "unit",
130
+ "integration",
131
+ "e2e",
132
+ "runtime",
133
+ "manual"
134
+ ]
135
+ },
136
+ "met": {
137
+ "type": "boolean"
138
+ }
139
+ }
140
+ }
141
+ }
142
+ }
143
+ },
144
+ "error": false
145
+ }
146
+ },
147
+ "failure": {
148
+ "description": "Nothing was written: the input, the item, the criterion or the lease refused it, or records amend did.",
149
+ "type": "object",
150
+ "required": [
151
+ "$schema",
152
+ "contract",
153
+ "error"
154
+ ],
155
+ "properties": {
156
+ "$schema": {
157
+ "const": "https://intentius.io/chant/schemas/workspace/work-evidence/v1/work-evidence.schema.json"
158
+ },
159
+ "contract": {
160
+ "const": 1
161
+ },
162
+ "error": {
163
+ "type": "object",
164
+ "required": [
165
+ "code",
166
+ "message"
167
+ ],
168
+ "properties": {
169
+ "code": {
170
+ "enum": [
171
+ "kind-unreadable",
172
+ "kind-invalid",
173
+ "schema-unreadable",
174
+ "schema-id-mismatch",
175
+ "schema-invalid",
176
+ "location-missing",
177
+ "write-usage-invalid",
178
+ "write-input-invalid",
179
+ "record-not-found",
180
+ "amend-id-immutable",
181
+ "record-closed",
182
+ "amend-supersede-instead",
183
+ "record-sign-failed",
184
+ "record-unparseable",
185
+ "record-schema-invalid",
186
+ "record-id-duplicate",
187
+ "record-supersedes-unknown",
188
+ "record-supersedes-conflict",
189
+ "session-seal-mismatch",
190
+ "session-verdict-unknown-record",
191
+ "work-kind-missing",
192
+ "work-kind-ambiguous",
193
+ "work-item-unknown",
194
+ "work-item-closed",
195
+ "work-criterion-unknown",
196
+ "work-acceptance-self-verified",
197
+ "lease-held",
198
+ "lease-not-held",
199
+ "lease-token-mismatch",
200
+ "not-a-git-repository"
201
+ ]
202
+ },
203
+ "message": {
204
+ "type": "string"
205
+ }
206
+ }
207
+ },
208
+ "evidence": false
209
+ }
210
+ }
211
+ }
212
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Evidence attached to a work item's acceptance criterion by the run that
3
+ * holds its lease (#2772).
4
+ *
5
+ * A work kind with acceptance criteria (`work.acceptance` in the kind, read in
6
+ * `./work.ts`) lists what done means on the record: each criterion has an id,
7
+ * its text and the verification it expects. Evidence in the kind's pins field
8
+ * meets a criterion when it names it, carries `result: pass` and has the
9
+ * criterion's verification. {@link attachWorkEvidence} appends one such entry
10
+ * for a run under the item's work lease (an Op with `workLease`, #2748, such
11
+ * as a steward's), through `records amend`, so the record is written the way
12
+ * a person's amendment writes it.
13
+ *
14
+ * It writes only for the lease's holder, under the token the run was given:
15
+ * a run whose lease expired or went to someone else is refused, as a renewal
16
+ * would be. It refuses a criterion the record doesn't list, and a `manual`
17
+ * one: the run holding the lease is the item's implementer, and a manual
18
+ * verdict comes from someone else. A person records that verdict with
19
+ * `records amend`, naming themselves in `by`.
20
+ */
21
+
22
+ import { relative, sep } from "node:path";
23
+ import { activeWorkLeases } from "../lifecycle/work-lease";
24
+ import type { ReasonCode } from "./reason-codes";
25
+ import { pinFile } from "./records-cli";
26
+ import { AMEND_ERROR_CODES, amendRecord } from "./records-write";
27
+ import { acceptanceCriteria, EVIDENCE_RESULTS, workAcceptance, type EvidenceResult, type VerificationType, type WorkAcceptance } from "./work";
28
+ import { findWorkItem, WorkError } from "./work-cli";
29
+
30
+ /** Why evidence was not attached. Closed. Nothing is written with any of them. */
31
+ export const WORK_EVIDENCE_ERROR_CODES = [
32
+ ...AMEND_ERROR_CODES,
33
+ "work-kind-missing",
34
+ "work-kind-ambiguous",
35
+ "work-item-unknown",
36
+ "work-item-closed",
37
+ "work-criterion-unknown",
38
+ "work-acceptance-self-verified",
39
+ "lease-held",
40
+ "lease-not-held",
41
+ "lease-token-mismatch",
42
+ "not-a-git-repository",
43
+ ] as const satisfies readonly ReasonCode[];
44
+ export type WorkEvidenceErrorCode = (typeof WORK_EVIDENCE_ERROR_CODES)[number];
45
+
46
+ export interface AttachWorkEvidenceRequest {
47
+ /** Where the workspace is found, and what `path` and `kind` resolve against. */
48
+ cwd: string;
49
+ /** The work item. */
50
+ item: string;
51
+ /** The work kind file, when the declaration names more than one with the item. */
52
+ kind?: string;
53
+ /** The lease the run holds: its holder and fencing token. */
54
+ holder: string;
55
+ token: string;
56
+ /** The criterion the evidence is for, by id. */
57
+ criterion: string;
58
+ result: EvidenceResult;
59
+ title: string;
60
+ /** A public link, or */
61
+ url?: string;
62
+ /** a workspace file, pinned by the hash of its bytes now. */
63
+ path?: string;
64
+ /** When the evidence was taken, as an ISO 8601 timestamp. Now by default. */
65
+ asOf?: string;
66
+ }
67
+
68
+ /** The version of the attach document this chant writes. */
69
+ export const WORK_EVIDENCE_CONTRACT_VERSION = 1;
70
+
71
+ /** `$id` of the JSON Schema for the attach document, shipped beside this file. */
72
+ export const WORK_EVIDENCE_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/work-evidence/v1/work-evidence.schema.json";
73
+
74
+ interface DocHead {
75
+ $schema: string;
76
+ contract: number;
77
+ }
78
+
79
+ /** What {@link attachWorkEvidence} returns: the entry written, or why nothing was. */
80
+ export type WorkEvidenceDocument = DocHead &
81
+ (
82
+ | {
83
+ item: string;
84
+ /** The work kind file, from the repository root. */
85
+ kind: string;
86
+ /** The record, from the repository root. */
87
+ path: string;
88
+ /** The entry appended to the kind's pins field. */
89
+ evidence: Record<string, unknown>;
90
+ /** The record's criteria with the new entry counted. */
91
+ acceptance: WorkAcceptance;
92
+ }
93
+ | { error: { code: WorkEvidenceErrorCode; message: string } }
94
+ );
95
+
96
+ class EvidenceError extends Error {
97
+ constructor(
98
+ readonly code: WorkEvidenceErrorCode,
99
+ message: string,
100
+ ) {
101
+ super(message);
102
+ }
103
+ }
104
+
105
+ /** Append one piece of evidence for a criterion to a leased work item. Never throws for a refusal. */
106
+ export async function attachWorkEvidence(req: AttachWorkEvidenceRequest): Promise<WorkEvidenceDocument> {
107
+ const head = { $schema: WORK_EVIDENCE_OUTPUT_SCHEMA_ID, contract: WORK_EVIDENCE_CONTRACT_VERSION };
108
+ try {
109
+ if (typeof req.criterion !== "string" || req.criterion === "") throw new EvidenceError("write-usage-invalid", "the evidence names no criterion");
110
+ if (!(EVIDENCE_RESULTS as readonly string[]).includes(req.result)) {
111
+ throw new EvidenceError("write-usage-invalid", `result is pass or fail, not ${JSON.stringify(req.result)}`);
112
+ }
113
+ if (typeof req.title !== "string" || req.title.trim() === "") throw new EvidenceError("write-usage-invalid", "the evidence has no title");
114
+ if ((req.url === undefined) === (req.path === undefined)) throw new EvidenceError("write-usage-invalid", "the evidence is a url or a path, exactly one of them");
115
+ if (req.url !== undefined && !req.url.startsWith("https://")) throw new EvidenceError("write-input-invalid", `evidence links are https://, not ${JSON.stringify(req.url)}`);
116
+
117
+ const found = await findWorkItem(req.item, req.cwd, req.kind);
118
+ const { kind } = found.loaded;
119
+ const data = found.record.data;
120
+ if (found.state !== null && (kind.closedStates ?? []).includes(found.state)) {
121
+ throw new EvidenceError("work-item-closed", `${req.item} is ${found.state}, a closed state, so no evidence is added to it`);
122
+ }
123
+
124
+ // The lease, as the local refs hold it: the run claimed it here, so they are current.
125
+ const lease = (await activeWorkLeases(found.loaded.file)).get(req.item);
126
+ if (!lease) throw new EvidenceError("lease-not-held", `nobody holds a live lease on ${req.item}: it expired, was released or was never claimed`);
127
+ if (lease.holder !== req.holder) throw new EvidenceError("lease-held", `${lease.holder} holds the lease on ${req.item}, not ${req.holder}`);
128
+ if (lease.token !== req.token) throw new EvidenceError("lease-token-mismatch", `the lease on ${req.item} carries another token than the one given: it changed hands`);
129
+
130
+ const spec = kind.work?.acceptance;
131
+ if (!spec || !kind.pins) throw new EvidenceError("work-criterion-unknown", `the ${kind.name} kind has no acceptance criteria, so evidence can't name ${req.criterion}`);
132
+ const criterion = acceptanceCriteria(data, spec.field).find((c) => c.id === req.criterion);
133
+ if (!criterion) throw new EvidenceError("work-criterion-unknown", `${req.item} lists no acceptance criterion ${req.criterion}`);
134
+ if (criterion.verification === "manual") {
135
+ throw new EvidenceError(
136
+ "work-acceptance-self-verified",
137
+ `${req.criterion} is verified by a person, and ${req.holder} holds ${req.item}'s lease, so it is the implementer: a manual verdict comes from someone else, with records amend`,
138
+ );
139
+ }
140
+
141
+ let pin: Record<string, unknown>;
142
+ if (req.path !== undefined) {
143
+ const pinned = pinFile(req.path, req.cwd);
144
+ if ("error" in pinned) throw new EvidenceError("write-input-invalid", pinned.error);
145
+ pin = { path: pinned.path, sha256: pinned.sha256 };
146
+ } else {
147
+ pin = { url: req.url };
148
+ }
149
+ const entry: Record<string, unknown> = {
150
+ title: req.title,
151
+ ...pin,
152
+ as_of: req.asOf ?? new Date().toISOString(),
153
+ criterion: criterion.id,
154
+ verification: criterion.verification as VerificationType,
155
+ result: req.result,
156
+ by: req.holder,
157
+ };
158
+ const before = Array.isArray(data?.[kind.pins.field]) ? (data![kind.pins.field] as unknown[]) : [];
159
+ const evidence = [...before, entry];
160
+ const amended = await amendRecord({ kind: found.loaded.file, id: req.item, fields: JSON.stringify({ [kind.pins.field]: evidence }), cwd: req.cwd });
161
+ if ("error" in amended) throw new EvidenceError(amended.error.code, amended.error.message);
162
+ const { acceptance } = workAcceptance({ ...data, [kind.pins.field]: evidence }, spec, kind.pins.field);
163
+ const kindPath = relative(found.root, found.loaded.file).split(sep).join("/");
164
+ return { ...head, item: req.item, kind: kindPath, path: amended.path, evidence: entry, acceptance: acceptance! };
165
+ } catch (err) {
166
+ if (err instanceof EvidenceError || err instanceof WorkError) return { ...head, error: { code: err.code, message: err.message } };
167
+ throw err;
168
+ }
169
+ }