@intentius/chant 0.91.0 → 0.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/dist/cli/commands/build.d.ts.map +1 -1
  2. package/dist/cli/commands/lint.d.ts.map +1 -1
  3. package/dist/cli/handlers/graph.d.ts.map +1 -1
  4. package/dist/cli/handlers/operator.d.ts +6 -0
  5. package/dist/cli/handlers/operator.d.ts.map +1 -1
  6. package/dist/cli/handlers/run.d.ts.map +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/mcp/workspace-tools.d.ts +2 -1
  9. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  10. package/dist/cli/plugins.d.ts.map +1 -1
  11. package/dist/cli/registry.d.ts +12 -2
  12. package/dist/cli/registry.d.ts.map +1 -1
  13. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  14. package/dist/config.d.ts +22 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/lexicon-module.d.ts +22 -0
  17. package/dist/lexicon-module.d.ts.map +1 -1
  18. package/dist/lifecycle/converge-ledger.d.ts +19 -0
  19. package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
  20. package/dist/lifecycle/run-ledger.d.ts +2 -0
  21. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  22. package/dist/lifecycle/symptoms.d.ts +51 -0
  23. package/dist/lifecycle/symptoms.d.ts.map +1 -1
  24. package/dist/lint/rules/index.d.ts +4 -2
  25. package/dist/lint/rules/index.d.ts.map +1 -1
  26. package/dist/lint/rules/op/index.d.ts +14 -3
  27. package/dist/lint/rules/op/index.d.ts.map +1 -1
  28. package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
  29. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
  30. package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
  31. package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
  32. package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
  33. package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
  34. package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
  35. package/dist/op/activities/activity-contracts.d.ts +201 -0
  36. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  37. package/dist/op/activities/apply.d.ts.map +1 -1
  38. package/dist/op/activities/change-coverage.d.ts +44 -0
  39. package/dist/op/activities/change-coverage.d.ts.map +1 -0
  40. package/dist/op/activities/converge.d.ts +24 -1
  41. package/dist/op/activities/converge.d.ts.map +1 -1
  42. package/dist/op/activities/decide.d.ts +107 -0
  43. package/dist/op/activities/decide.d.ts.map +1 -0
  44. package/dist/op/activities/index.d.ts +10 -0
  45. package/dist/op/activities/index.d.ts.map +1 -1
  46. package/dist/op/activities/shell.d.ts +32 -0
  47. package/dist/op/activities/shell.d.ts.map +1 -1
  48. package/dist/op/activities/source-release.d.ts +86 -0
  49. package/dist/op/activities/source-release.d.ts.map +1 -0
  50. package/dist/op/activities/source-rollback.d.ts +100 -0
  51. package/dist/op/activities/source-rollback.d.ts.map +1 -0
  52. package/dist/op/activities/work-evidence.d.ts +42 -0
  53. package/dist/op/activities/work-evidence.d.ts.map +1 -0
  54. package/dist/op/activity-contract.d.ts +1 -0
  55. package/dist/op/activity-contract.d.ts.map +1 -1
  56. package/dist/op/activity-profiles.d.ts +16 -0
  57. package/dist/op/activity-profiles.d.ts.map +1 -1
  58. package/dist/op/builders.d.ts +84 -2
  59. package/dist/op/builders.d.ts.map +1 -1
  60. package/dist/op/composites/converge-op.d.ts +31 -4
  61. package/dist/op/composites/converge-op.d.ts.map +1 -1
  62. package/dist/op/decide-backend.d.ts +116 -0
  63. package/dist/op/decide-backend.d.ts.map +1 -0
  64. package/dist/op/decide-config.d.ts +120 -0
  65. package/dist/op/decide-config.d.ts.map +1 -0
  66. package/dist/op/decide-read-inputs.d.ts +40 -0
  67. package/dist/op/decide-read-inputs.d.ts.map +1 -0
  68. package/dist/op/gate-wait.d.ts +43 -0
  69. package/dist/op/gate-wait.d.ts.map +1 -0
  70. package/dist/op/gate.d.ts +5 -2
  71. package/dist/op/gate.d.ts.map +1 -1
  72. package/dist/op/index.d.ts +11 -4
  73. package/dist/op/index.d.ts.map +1 -1
  74. package/dist/op/local-executor.d.ts +29 -4
  75. package/dist/op/local-executor.d.ts.map +1 -1
  76. package/dist/op/local-output.d.ts +2 -0
  77. package/dist/op/local-output.d.ts.map +1 -1
  78. package/dist/op/op-ir.d.ts +2 -0
  79. package/dist/op/op-ir.d.ts.map +1 -1
  80. package/dist/op/op-verb-class.d.ts.map +1 -1
  81. package/dist/op/operator.d.ts +97 -1
  82. package/dist/op/operator.d.ts.map +1 -1
  83. package/dist/op/runtime.d.ts +37 -5
  84. package/dist/op/runtime.d.ts.map +1 -1
  85. package/dist/op/runtimes/local.d.ts.map +1 -1
  86. package/dist/op/source-archive.d.ts +63 -0
  87. package/dist/op/source-archive.d.ts.map +1 -0
  88. package/dist/op/steward-points.d.ts +136 -0
  89. package/dist/op/steward-points.d.ts.map +1 -0
  90. package/dist/op/steward-turn.d.ts +53 -0
  91. package/dist/op/steward-turn.d.ts.map +1 -0
  92. package/dist/op/steward.d.ts +22 -1
  93. package/dist/op/steward.d.ts.map +1 -1
  94. package/dist/op/types.d.ts +7 -0
  95. package/dist/op/types.d.ts.map +1 -1
  96. package/dist/op/work-lease-run.d.ts +1 -1
  97. package/dist/op/work-lease-run.d.ts.map +1 -1
  98. package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
  99. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
  100. package/dist/workspace/answers-ledger.d.ts +76 -0
  101. package/dist/workspace/answers-ledger.d.ts.map +1 -0
  102. package/dist/workspace/changes-cli.d.ts +16 -0
  103. package/dist/workspace/changes-cli.d.ts.map +1 -0
  104. package/dist/workspace/changes.d.ts +200 -0
  105. package/dist/workspace/changes.d.ts.map +1 -0
  106. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
  107. package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
  108. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
  109. package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
  110. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
  111. package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
  112. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
  113. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
  114. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
  115. package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
  116. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
  117. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
  118. package/dist/workspace/chant-migrations.d.ts +99 -0
  119. package/dist/workspace/chant-migrations.d.ts.map +1 -0
  120. package/dist/workspace/checks/boxes.d.ts +34 -1
  121. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  122. package/dist/workspace/checks/diagrams.d.ts +31 -0
  123. package/dist/workspace/checks/diagrams.d.ts.map +1 -0
  124. package/dist/workspace/checks/records.d.ts +7 -0
  125. package/dist/workspace/checks/records.d.ts.map +1 -1
  126. package/dist/workspace/checks.d.ts +3 -0
  127. package/dist/workspace/checks.d.ts.map +1 -1
  128. package/dist/workspace/compose-graph.d.ts +2 -0
  129. package/dist/workspace/compose-graph.d.ts.map +1 -1
  130. package/dist/workspace/conformance/index.d.ts.map +1 -1
  131. package/dist/workspace/decide.d.ts +49 -4
  132. package/dist/workspace/decide.d.ts.map +1 -1
  133. package/dist/workspace/declaration.d.ts +52 -0
  134. package/dist/workspace/declaration.d.ts.map +1 -1
  135. package/dist/workspace/declaration.schema.json +114 -1
  136. package/dist/workspace/declared-kinds.d.ts +20 -0
  137. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  138. package/dist/workspace/generated-files.d.ts +7 -0
  139. package/dist/workspace/generated-files.d.ts.map +1 -1
  140. package/dist/workspace/intent.d.ts.map +1 -1
  141. package/dist/workspace/lineage-check.d.ts +3 -0
  142. package/dist/workspace/lineage-check.d.ts.map +1 -1
  143. package/dist/workspace/lineage-upgrade.d.ts +6 -1
  144. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  145. package/dist/workspace/ls.d.ts +39 -1
  146. package/dist/workspace/ls.d.ts.map +1 -1
  147. package/dist/workspace/point-answer.schema.json +95 -0
  148. package/dist/workspace/points-cli.d.ts.map +1 -1
  149. package/dist/workspace/points.d.ts +1 -1
  150. package/dist/workspace/reason-codes.d.ts +14 -2
  151. package/dist/workspace/reason-codes.d.ts.map +1 -1
  152. package/dist/workspace/records-cli.d.ts +11 -1
  153. package/dist/workspace/records-cli.d.ts.map +1 -1
  154. package/dist/workspace/records-close.d.ts +1 -1
  155. package/dist/workspace/records-close.d.ts.map +1 -1
  156. package/dist/workspace/records-write.d.ts +11 -5
  157. package/dist/workspace/records-write.d.ts.map +1 -1
  158. package/dist/workspace/records.d.ts +34 -4
  159. package/dist/workspace/records.d.ts.map +1 -1
  160. package/dist/workspace/status-gates.d.ts +7 -2
  161. package/dist/workspace/status-gates.d.ts.map +1 -1
  162. package/dist/workspace/status-stewards.d.ts +59 -1
  163. package/dist/workspace/status-stewards.d.ts.map +1 -1
  164. package/dist/workspace/status.d.ts +16 -0
  165. package/dist/workspace/status.d.ts.map +1 -1
  166. package/dist/workspace/work-cli.d.ts +113 -2
  167. package/dist/workspace/work-cli.d.ts.map +1 -1
  168. package/dist/workspace/work-evidence.d.ts +74 -0
  169. package/dist/workspace/work-evidence.d.ts.map +1 -0
  170. package/dist/workspace/work.d.ts +43 -1
  171. package/dist/workspace/work.d.ts.map +1 -1
  172. package/package.json +1 -1
  173. package/src/cli/commands/audit.test.ts +9 -1
  174. package/src/cli/commands/build.ts +3 -2
  175. package/src/cli/commands/lint.ts +7 -1
  176. package/src/cli/handlers/graph.test.ts +20 -0
  177. package/src/cli/handlers/graph.ts +16 -2
  178. package/src/cli/handlers/operator.ts +34 -2
  179. package/src/cli/handlers/run.test.ts +154 -2
  180. package/src/cli/handlers/run.ts +121 -2
  181. package/src/cli/main.test.ts +10 -0
  182. package/src/cli/main.ts +19 -0
  183. package/src/cli/mcp/workspace-tools.test.ts +35 -19
  184. package/src/cli/mcp/workspace-tools.ts +62 -4
  185. package/src/cli/plugins.ts +11 -3
  186. package/src/cli/registry.ts +12 -2
  187. package/src/codegen/pinned-upgrade.ts +2 -1
  188. package/src/codegen/release-wiring.test.ts +49 -2
  189. package/src/components/capability-plugin-loader.ts +3 -3
  190. package/src/components/cli-support.ts +2 -2
  191. package/src/config.ts +11 -0
  192. package/src/lexicon-module.ts +56 -1
  193. package/src/lexicon-package.test.ts +96 -0
  194. package/src/lifecycle/converge-ledger.ts +20 -0
  195. package/src/lifecycle/run-ledger.ts +6 -0
  196. package/src/lifecycle/symptoms.ts +95 -0
  197. package/src/lint/rules/index.ts +6 -2
  198. package/src/lint/rules/op/index.ts +17 -4
  199. package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
  200. package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
  201. package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
  202. package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
  203. package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
  204. package/src/lint/rules/sys001-no-literal-key.ts +67 -0
  205. package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
  206. package/src/op/activities/activity-contracts.ts +190 -1
  207. package/src/op/activities/apply.ts +7 -6
  208. package/src/op/activities/change-coverage.ts +60 -0
  209. package/src/op/activities/converge.test.ts +55 -1
  210. package/src/op/activities/converge.ts +208 -29
  211. package/src/op/activities/decide.live.test.ts +78 -0
  212. package/src/op/activities/decide.test.ts +300 -0
  213. package/src/op/activities/decide.ts +267 -0
  214. package/src/op/activities/index.ts +21 -0
  215. package/src/op/activities/shell.test.ts +91 -1
  216. package/src/op/activities/shell.ts +91 -2
  217. package/src/op/activities/source-release.test.ts +118 -0
  218. package/src/op/activities/source-release.ts +168 -0
  219. package/src/op/activities/source-rollback.test.ts +131 -0
  220. package/src/op/activities/source-rollback.ts +239 -0
  221. package/src/op/activities/work-evidence.ts +62 -0
  222. package/src/op/activity-contract-registry.test.ts +2 -0
  223. package/src/op/activity-contract-registry.ts +2 -2
  224. package/src/op/activity-contract.test.ts +12 -0
  225. package/src/op/activity-contract.ts +6 -1
  226. package/src/op/activity-profiles.ts +29 -0
  227. package/src/op/activity-registry.test.ts +1 -1
  228. package/src/op/activity-registry.ts +2 -2
  229. package/src/op/builders.ts +119 -5
  230. package/src/op/command-gate.test.ts +192 -0
  231. package/src/op/composites/converge-op.test.ts +27 -1
  232. package/src/op/composites/converge-op.ts +69 -15
  233. package/src/op/decide-backend.test.ts +44 -0
  234. package/src/op/decide-backend.ts +212 -0
  235. package/src/op/decide-config.test.ts +52 -0
  236. package/src/op/decide-config.ts +80 -0
  237. package/src/op/decide-read-inputs.ts +117 -0
  238. package/src/op/gate-approval.ts +2 -2
  239. package/src/op/gate-wait.ts +57 -0
  240. package/src/op/gate.ts +8 -3
  241. package/src/op/generate-pipeline.ts +2 -2
  242. package/src/op/index.ts +18 -3
  243. package/src/op/local-executor.ts +151 -15
  244. package/src/op/local-output.test.ts +33 -0
  245. package/src/op/local-output.ts +20 -2
  246. package/src/op/op-ir.test.ts +10 -0
  247. package/src/op/op-ir.ts +4 -0
  248. package/src/op/op-verb-class.ts +1 -0
  249. package/src/op/operator.ts +290 -11
  250. package/src/op/runtime.ts +31 -8
  251. package/src/op/runtimes/local.ts +3 -1
  252. package/src/op/source-archive.ts +161 -0
  253. package/src/op/steward-points.test.ts +313 -0
  254. package/src/op/steward-points.ts +194 -0
  255. package/src/op/steward-turn.ts +79 -0
  256. package/src/op/steward.test.ts +87 -3
  257. package/src/op/steward.ts +25 -1
  258. package/src/op/types.ts +7 -0
  259. package/src/op/work-lease-run.test.ts +20 -1
  260. package/src/op/work-lease-run.ts +2 -2
  261. package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
  262. package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
  263. package/src/workspace/answers-ledger.ts +118 -0
  264. package/src/workspace/changes-cli.ts +56 -0
  265. package/src/workspace/changes.schema.json +472 -0
  266. package/src/workspace/changes.test.ts +315 -0
  267. package/src/workspace/changes.ts +512 -0
  268. package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
  269. package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
  270. package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
  271. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
  272. package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
  273. package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
  274. package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
  275. package/src/workspace/chant-migrations.ts +164 -0
  276. package/src/workspace/check-contract.test.ts +4 -2
  277. package/src/workspace/check.schema.json +15 -5
  278. package/src/workspace/checks/boxes.test.ts +80 -1
  279. package/src/workspace/checks/boxes.ts +136 -1
  280. package/src/workspace/checks/diagrams.test.ts +134 -0
  281. package/src/workspace/checks/diagrams.ts +108 -0
  282. package/src/workspace/checks/records.ts +47 -0
  283. package/src/workspace/checks.ts +8 -1
  284. package/src/workspace/compose-graph.ts +2 -0
  285. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
  286. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
  287. package/src/workspace/conformance/index.ts +5 -0
  288. package/src/workspace/decide.test.ts +1 -1
  289. package/src/workspace/decide.ts +156 -23
  290. package/src/workspace/declaration.schema.json +114 -1
  291. package/src/workspace/declaration.test.ts +58 -0
  292. package/src/workspace/declaration.ts +104 -0
  293. package/src/workspace/declared-kinds.test.ts +5 -5
  294. package/src/workspace/declared-kinds.ts +46 -1
  295. package/src/workspace/generated-files.ts +14 -0
  296. package/src/workspace/graph-cli.ts +1 -1
  297. package/src/workspace/graph.schema.json +9 -1
  298. package/src/workspace/intent.schema.json +8 -2
  299. package/src/workspace/intent.ts +3 -10
  300. package/src/workspace/lineage-check.ts +16 -1
  301. package/src/workspace/lineage-upgrade-cli.ts +1 -1
  302. package/src/workspace/lineage-upgrade.test.ts +70 -0
  303. package/src/workspace/lineage-upgrade.ts +81 -10
  304. package/src/workspace/ls-contract.test.ts +57 -0
  305. package/src/workspace/ls.schema.json +46 -1
  306. package/src/workspace/ls.ts +62 -3
  307. package/src/workspace/points-cli.ts +14 -3
  308. package/src/workspace/points-write.schema.json +11 -1
  309. package/src/workspace/points.schema.json +33 -0
  310. package/src/workspace/points.test.ts +1 -1
  311. package/src/workspace/points.ts +1 -1
  312. package/src/workspace/read-contract.test.ts +1 -1
  313. package/src/workspace/reason-codes.test.ts +11 -1
  314. package/src/workspace/reason-codes.ts +18 -2
  315. package/src/workspace/records-amend.schema.json +2 -0
  316. package/src/workspace/records-cli.ts +13 -3
  317. package/src/workspace/records-close.schema.json +2 -0
  318. package/src/workspace/records-new.schema.json +2 -0
  319. package/src/workspace/records-review.schema.json +2 -0
  320. package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
  321. package/src/workspace/records-sessions-write.test.ts +2 -34
  322. package/src/workspace/records-write.e2e.test.ts +108 -0
  323. package/src/workspace/records-write.test.ts +32 -41
  324. package/src/workspace/records-write.ts +33 -15
  325. package/src/workspace/records.schema.json +36 -2
  326. package/src/workspace/records.test.ts +48 -3
  327. package/src/workspace/records.ts +109 -3
  328. package/src/workspace/status-contract.test.ts +13 -8
  329. package/src/workspace/status-gates.ts +9 -4
  330. package/src/workspace/status-stewards.ts +108 -4
  331. package/src/workspace/status.schema.json +111 -7
  332. package/src/workspace/status.ts +36 -1
  333. package/src/workspace/work-acceptance.test.ts +274 -0
  334. package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
  335. package/src/workspace/work-cli.ts +193 -10
  336. package/src/workspace/work-evidence.schema.json +212 -0
  337. package/src/workspace/work-evidence.ts +169 -0
  338. package/src/workspace/work-history.schema.json +349 -0
  339. package/src/workspace/work-history.test.ts +156 -0
  340. package/src/workspace/work.test.ts +3 -2
  341. package/src/workspace/work.ts +100 -1
  342. /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
  343. /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
  344. /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
@@ -0,0 +1,349 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://intentius.io/chant/schemas/workspace/work-history/v1/work-history.schema.json",
4
+ "title": "chant workspace work history output",
5
+ "description": "What `chant workspace work history <id> --json` prints (#2785): one work item's lease history, read from _leases/<id>.jsonl on chant/lifecycle in the ledger of the member that owns the work kind, folded into claims, one per fencing token, each with how it ended. A runner counts failed attempts from it. It reads the local branch and lease refs and never fetches. Version 1 of the read contract. 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
+ "event": {
16
+ "type": "object",
17
+ "description": "One line of the history, as written by a claim, renew or release.",
18
+ "required": [
19
+ "version",
20
+ "event",
21
+ "item",
22
+ "holder",
23
+ "by",
24
+ "token",
25
+ "acquiredAt",
26
+ "expiresAt",
27
+ "timestamp"
28
+ ],
29
+ "properties": {
30
+ "version": {
31
+ "const": 1
32
+ },
33
+ "event": {
34
+ "enum": [
35
+ "claim",
36
+ "renew",
37
+ "release"
38
+ ]
39
+ },
40
+ "item": {
41
+ "type": "string"
42
+ },
43
+ "holder": {
44
+ "type": "string",
45
+ "description": "The lease's holder."
46
+ },
47
+ "by": {
48
+ "type": "string",
49
+ "description": "Who wrote the line: the holder, or, for the release of an expired lease, whoever closed it out."
50
+ },
51
+ "token": {
52
+ "type": "string",
53
+ "description": "The fencing token: new on every claim, kept by every renew."
54
+ },
55
+ "acquiredAt": {
56
+ "type": "string"
57
+ },
58
+ "expiresAt": {
59
+ "type": "string",
60
+ "description": "The lease's expiry after this event; a release keeps the expiry it had."
61
+ },
62
+ "timestamp": {
63
+ "type": "string",
64
+ "description": "When the event happened, ISO 8601."
65
+ },
66
+ "outcome": {
67
+ "type": "string",
68
+ "description": "How the work ended, on a release. Free text such as done, not_done, gated or waiting."
69
+ },
70
+ "note": {
71
+ "type": "string"
72
+ }
73
+ }
74
+ },
75
+ "claim": {
76
+ "type": "object",
77
+ "description": "One claim of the item: one fencing token, from its claim to how it ended.",
78
+ "required": [
79
+ "token",
80
+ "holder",
81
+ "acquiredAt",
82
+ "expiresAt",
83
+ "renewals",
84
+ "ended",
85
+ "release"
86
+ ],
87
+ "properties": {
88
+ "token": {
89
+ "type": "string"
90
+ },
91
+ "holder": {
92
+ "type": "string"
93
+ },
94
+ "acquiredAt": {
95
+ "type": "string"
96
+ },
97
+ "expiresAt": {
98
+ "type": "string",
99
+ "description": "The expiry its last claim or renew set."
100
+ },
101
+ "renewals": {
102
+ "type": "integer",
103
+ "minimum": 0
104
+ },
105
+ "ended": {
106
+ "enum": [
107
+ "released",
108
+ "held",
109
+ "expired",
110
+ "lost"
111
+ ],
112
+ "description": "released: given back, with release.outcome when one was given. held: it is the live lease now. expired: it ran out, and nobody released it or claimed the item since. lost: it ran out unreleased, and a later claim took the item."
113
+ },
114
+ "release": {
115
+ "oneOf": [
116
+ {
117
+ "type": "null"
118
+ },
119
+ {
120
+ "type": "object",
121
+ "required": [
122
+ "by",
123
+ "at",
124
+ "outcome",
125
+ "note"
126
+ ],
127
+ "properties": {
128
+ "by": {
129
+ "type": "string"
130
+ },
131
+ "at": {
132
+ "type": "string"
133
+ },
134
+ "outcome": {
135
+ "type": [
136
+ "string",
137
+ "null"
138
+ ]
139
+ },
140
+ "note": {
141
+ "type": [
142
+ "string",
143
+ "null"
144
+ ]
145
+ }
146
+ }
147
+ }
148
+ ]
149
+ }
150
+ }
151
+ },
152
+ "result": {
153
+ "type": "object",
154
+ "description": "The history was read. Exit code 0. An item never claimed has no claims.",
155
+ "required": [
156
+ "$schema",
157
+ "contract",
158
+ "chant",
159
+ "item",
160
+ "kind",
161
+ "ref",
162
+ "ledger",
163
+ "lease",
164
+ "claims",
165
+ "events",
166
+ "malformed",
167
+ "summary"
168
+ ],
169
+ "properties": {
170
+ "$schema": {
171
+ "const": "https://intentius.io/chant/schemas/workspace/work-history/v1/work-history.schema.json"
172
+ },
173
+ "contract": {
174
+ "const": 1
175
+ },
176
+ "chant": {
177
+ "type": "string"
178
+ },
179
+ "item": {
180
+ "type": "string"
181
+ },
182
+ "kind": {
183
+ "type": "string",
184
+ "description": "The work kind file, from the repository root."
185
+ },
186
+ "ref": {
187
+ "type": "string",
188
+ "description": "The lease ref: refs/chant/lease/[_members/<member>/]work/<id>."
189
+ },
190
+ "ledger": {
191
+ "type": "object",
192
+ "required": [
193
+ "branch",
194
+ "path"
195
+ ],
196
+ "properties": {
197
+ "branch": {
198
+ "const": "chant/lifecycle"
199
+ },
200
+ "path": {
201
+ "type": "string",
202
+ "description": "The history file on the branch: [_members/<member>/]_leases/<id>.jsonl."
203
+ }
204
+ }
205
+ },
206
+ "lease": {
207
+ "description": "The lease ref's record now, live or expired, or null when there is none. Read from the local ref and the remote's as last fetched.",
208
+ "oneOf": [
209
+ {
210
+ "type": "null"
211
+ },
212
+ {
213
+ "type": "object",
214
+ "required": [
215
+ "holder",
216
+ "token",
217
+ "acquiredAt",
218
+ "expiresAt",
219
+ "state"
220
+ ],
221
+ "properties": {
222
+ "holder": {
223
+ "type": "string"
224
+ },
225
+ "token": {
226
+ "type": "string"
227
+ },
228
+ "acquiredAt": {
229
+ "type": "string"
230
+ },
231
+ "expiresAt": {
232
+ "type": "string"
233
+ },
234
+ "state": {
235
+ "enum": [
236
+ "active",
237
+ "expired"
238
+ ]
239
+ }
240
+ }
241
+ }
242
+ ]
243
+ },
244
+ "claims": {
245
+ "type": "array",
246
+ "items": {
247
+ "$ref": "#/$defs/claim"
248
+ },
249
+ "description": "Each claim, oldest first."
250
+ },
251
+ "events": {
252
+ "type": "array",
253
+ "items": {
254
+ "$ref": "#/$defs/event"
255
+ },
256
+ "description": "Every line of the history, oldest first."
257
+ },
258
+ "malformed": {
259
+ "type": "integer",
260
+ "minimum": 0,
261
+ "description": "Lines that are not lease events, left out."
262
+ },
263
+ "summary": {
264
+ "type": "object",
265
+ "required": [
266
+ "claims",
267
+ "released",
268
+ "held",
269
+ "expired",
270
+ "lost",
271
+ "outcomes"
272
+ ],
273
+ "properties": {
274
+ "claims": {
275
+ "type": "integer",
276
+ "minimum": 0
277
+ },
278
+ "released": {
279
+ "type": "integer",
280
+ "minimum": 0
281
+ },
282
+ "held": {
283
+ "type": "integer",
284
+ "minimum": 0
285
+ },
286
+ "expired": {
287
+ "type": "integer",
288
+ "minimum": 0
289
+ },
290
+ "lost": {
291
+ "type": "integer",
292
+ "minimum": 0
293
+ },
294
+ "outcomes": {
295
+ "type": "object",
296
+ "additionalProperties": {
297
+ "type": "integer",
298
+ "minimum": 1
299
+ },
300
+ "description": "How many released claims gave each outcome."
301
+ }
302
+ }
303
+ }
304
+ }
305
+ },
306
+ "failure": {
307
+ "type": "object",
308
+ "description": "The history could not be read. Exit code 1.",
309
+ "required": [
310
+ "$schema",
311
+ "contract",
312
+ "chant",
313
+ "error"
314
+ ],
315
+ "properties": {
316
+ "$schema": {
317
+ "const": "https://intentius.io/chant/schemas/workspace/work-history/v1/work-history.schema.json"
318
+ },
319
+ "contract": {
320
+ "const": 1
321
+ },
322
+ "chant": {
323
+ "type": "string"
324
+ },
325
+ "error": {
326
+ "type": "object",
327
+ "required": [
328
+ "code",
329
+ "message"
330
+ ],
331
+ "properties": {
332
+ "code": {
333
+ "enum": [
334
+ "kind-unreadable",
335
+ "work-kind-missing",
336
+ "work-kind-ambiguous",
337
+ "work-item-unknown",
338
+ "not-a-git-repository"
339
+ ]
340
+ },
341
+ "message": {
342
+ "type": "string"
343
+ }
344
+ }
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * #2785 — `chant workspace work history <id>`: a work item's lease history,
3
+ * each claim with how it ended, from the ledger of the member that owns the
4
+ * work kind, so a runner counts failed builds without reading git itself.
5
+ */
6
+
7
+ import { readFileSync, rmSync, writeFileSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
10
+ import { cleanScratch, contract, git, REPO, repo } from "./__fixtures__/contract-repo";
11
+ import { claimWorkLease, releaseWorkLease, renewWorkLease, type LeaseHistoryRecord } from "../lifecycle/work-lease";
12
+ import { claimsOf, formatWorkHistory, workHistory, type WorkHistoryDocument } from "./work-cli";
13
+ import historySchema from "./work-history.schema.json";
14
+
15
+ const REF = join(REPO, "reference-workspace");
16
+ const { expectValid } = contract(historySchema);
17
+
18
+ function work(id: string): string {
19
+ const data = { schema: 1, id, title: `Work ${id}`, state: "open", implements: [], needs: [], constrains: ["path:app/server.mjs"], evidence: [], opened_on: "2026-09-25", source: { kind: "workspace", member: "app" }, supersedes: [] };
20
+ return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
21
+ }
22
+
23
+ let root: string;
24
+ let kindDir: string;
25
+
26
+ beforeAll(() => {
27
+ // The work kind sits in the design member, so its leases are in that member's ledger.
28
+ root = repo({
29
+ "chant.workspace.json": JSON.stringify(
30
+ {
31
+ name: "studio",
32
+ schema: 1,
33
+ members: [
34
+ { name: "app", dir: "app", kind: "other", because: "a plain Node server" },
35
+ { name: "design", dir: "design", kind: "other", because: "records only" },
36
+ ],
37
+ records: [{ kind: "design/decisions/decision.kind.mjs" }, { kind: "design/work/work.kind.mjs" }],
38
+ },
39
+ null,
40
+ 2,
41
+ ),
42
+ "app/server.mjs": "export const port = 8080;\n",
43
+ "design/decisions/decision.kind.mjs": readFileSync(join(REF, "decisions", "decision.kind.mjs"), "utf-8"),
44
+ "design/decisions/decision.schema.json": readFileSync(join(REF, "decisions", "decision.schema.json"), "utf-8"),
45
+ "design/work/work.kind.mjs": readFileSync(join(REF, "work", "work.kind.mjs"), "utf-8"),
46
+ "design/work/work.schema.json": readFileSync(join(REF, "work", "work.schema.json"), "utf-8"),
47
+ "design/work/W-001-one.md": work("W-001"),
48
+ "design/work/W-002-two.md": work("W-002"),
49
+ });
50
+ git(root, "config", "user.name", "History Test");
51
+ git(root, "config", "user.email", "history@example.com");
52
+ git(root, "add", "-A");
53
+ git(root, "commit", "-q", "-m", "the queue");
54
+ kindDir = join(root, "design", "work");
55
+ });
56
+ afterAll(cleanScratch);
57
+
58
+ const at = (min: number) => () => new Date(Date.UTC(2020, 0, 1, 12, min));
59
+
60
+ describe("workspace work history (#2785)", () => {
61
+ test("each claim with its token and holder, and how it ended: released with an outcome, lost, expired or held", async () => {
62
+ const opts = (min: number) => ({ cwd: kindDir, now: at(min), ttlMs: 10 * 60_000 });
63
+ // A build that ended not_done.
64
+ const first = await claimWorkLease("W-001", "steward/dispatch@1", opts(0));
65
+ if (!first.ok) throw new Error(first.message);
66
+ await renewWorkLease("W-001", "steward/dispatch@1", { ...opts(3), token: first.lease.token });
67
+ await releaseWorkLease("W-001", "steward/dispatch@1", { cwd: kindDir, now: at(5), token: first.lease.token, outcome: "not_done" });
68
+ // A build whose lease ran out unreleased, and another worker then claimed the item.
69
+ const second = await claimWorkLease("W-001", "steward/dispatch@2", opts(10));
70
+ if (!second.ok) throw new Error(second.message);
71
+ const third = await claimWorkLease("W-001", "steward/dispatch@3", opts(30));
72
+ if (!third.ok) throw new Error(third.message);
73
+
74
+ const doc = await workHistory({ id: "W-001", cwd: root });
75
+ expectValid(doc);
76
+ if ("error" in doc) throw new Error(doc.error.message);
77
+ expect(doc).toMatchObject({
78
+ item: "W-001",
79
+ kind: "design/work/work.kind.mjs",
80
+ ref: "refs/chant/lease/_members/design/work/W-001",
81
+ ledger: { branch: "chant/lifecycle", path: "_members/design/_leases/W-001.jsonl" },
82
+ malformed: 0,
83
+ });
84
+ expect(doc.claims.map((c) => [c.holder, c.token, c.ended, c.renewals, c.release?.outcome ?? null])).toEqual([
85
+ ["steward/dispatch@1", first.lease.token, "released", 1, "not_done"],
86
+ ["steward/dispatch@2", second.lease.token, "lost", 0, null],
87
+ ["steward/dispatch@3", third.lease.token, "expired", 0, null],
88
+ ]);
89
+ expect(doc.events.map((e) => e.event)).toEqual(["claim", "renew", "release", "claim", "claim"]);
90
+ expect(doc.summary).toEqual({ claims: 3, released: 1, held: 0, expired: 1, lost: 1, outcomes: { not_done: 1 } });
91
+ // The history file is where git would find it, under the member's prefix.
92
+ expect(git(root, "show", `chant/lifecycle:${doc.ledger.path}`).split("\n")).toHaveLength(5);
93
+ expect(formatWorkHistory(doc)).toContain("released not_done by steward/dispatch@1");
94
+ });
95
+
96
+ test("a live claim is held, and a done release is counted by outcome", async () => {
97
+ const now = () => new Date();
98
+ const done = await claimWorkLease("W-002", "worker-a", { cwd: kindDir, now });
99
+ if (!done.ok) throw new Error(done.message);
100
+ await releaseWorkLease("W-002", "worker-a", { cwd: kindDir, now, token: done.lease.token, outcome: "done" });
101
+ const live = await claimWorkLease("W-002", "worker-b", { cwd: kindDir, now });
102
+ if (!live.ok) throw new Error(live.message);
103
+
104
+ const doc = await workHistory({ id: "W-002", cwd: kindDir, kind: join(kindDir, "work.kind.mjs") });
105
+ expectValid(doc);
106
+ if ("error" in doc) throw new Error(doc.error.message);
107
+ expect(doc.claims.map((c) => c.ended)).toEqual(["released", "held"]);
108
+ expect(doc.lease).toMatchObject({ holder: "worker-b", token: live.lease.token, state: "active" });
109
+ expect(doc.summary.outcomes).toEqual({ done: 1 });
110
+ });
111
+
112
+ test("an item never claimed has no claims; an unknown item is an error", async () => {
113
+ const W3 = join(kindDir, "W-003-three.md");
114
+ writeFileSync(W3, work("W-003"));
115
+ try {
116
+ const none = await workHistory({ id: "W-003", cwd: root });
117
+ expectValid(none);
118
+ expect(none).toMatchObject({ claims: [], events: [], lease: null, summary: { claims: 0 } });
119
+ } finally {
120
+ rmSync(W3);
121
+ }
122
+ const unknown = await workHistory({ id: "W-404", cwd: root });
123
+ expectValid(unknown);
124
+ expect(unknown).toMatchObject({ error: { code: "work-item-unknown" } });
125
+ const bad = await workHistory({ id: "../x", cwd: root });
126
+ expect((bad as Extract<WorkHistoryDocument, { error: unknown }>).error.code).toBe("work-item-unknown");
127
+ });
128
+ });
129
+
130
+ describe("claimsOf", () => {
131
+ const e = (event: LeaseHistoryRecord["event"], token: string, extra: Partial<LeaseHistoryRecord> = {}): LeaseHistoryRecord => ({
132
+ version: 1,
133
+ event,
134
+ item: "W",
135
+ holder: `h-${token}`,
136
+ by: `h-${token}`,
137
+ token,
138
+ acquiredAt: "2026-09-25T00:00:00.000Z",
139
+ expiresAt: "2026-09-25T00:10:00.000Z",
140
+ timestamp: "2026-09-25T00:00:00.000Z",
141
+ ...extra,
142
+ });
143
+
144
+ test("a release by someone closing out an expired lease keeps the holder and records who released it", () => {
145
+ const claims = claimsOf([e("claim", "a"), e("release", "a", { by: "janitor", outcome: "expired" })], null);
146
+ expect(claims).toEqual([
147
+ { token: "a", holder: "h-a", acquiredAt: "2026-09-25T00:00:00.000Z", expiresAt: "2026-09-25T00:10:00.000Z", renewals: 0, ended: "released", release: { by: "janitor", at: "2026-09-25T00:00:00.000Z", outcome: "expired", note: null } },
148
+ ]);
149
+ });
150
+
151
+ test("the live ref decides between held and expired for the newest claim", () => {
152
+ expect(claimsOf([e("claim", "a")], { token: "a", state: "active" })[0].ended).toBe("held");
153
+ expect(claimsOf([e("claim", "a")], { token: "a", state: "expired" })[0].ended).toBe("expired");
154
+ expect(claimsOf([e("claim", "a"), e("claim", "b")], { token: "b", state: "active" }).map((c) => c.ended)).toEqual(["lost", "held"]);
155
+ });
156
+ });
@@ -32,6 +32,7 @@ import { queryRecords } from "./records-cli";
32
32
  import { amendRecord, newRecord, resolveWriteKind, reviewRecord } from "./records-write";
33
33
  import recordsSchema from "./records.schema.json";
34
34
  import { WORK_WARNING_CODES } from "./work";
35
+ import { CHANGES_FINDING_CODES } from "./changes";
35
36
 
36
37
  const REF = join(REPO, "reference-workspace");
37
38
  const DECISION = (() => {
@@ -227,9 +228,9 @@ describe("work records on read (#2683)", () => {
227
228
  expect("error" in doc && doc.error.code).toBe("kind-invalid");
228
229
  });
229
230
 
230
- test("the reference work schema's finding codes are the intent graph's", () => {
231
+ test("the reference work schema's finding codes are the intent graph's and the change check's (#2773)", () => {
231
232
  const schema = JSON.parse(readFileSync(join(REF, "work", "work.schema.json"), "utf-8")) as { definitions: { sourceGap: { properties: { finding: { anyOf: [{ enum: string[] }] } } } } };
232
- expect(schema.definitions.sourceGap.properties.finding.anyOf[0].enum).toEqual([...INTENT_FINDING_CODES]);
233
+ expect(schema.definitions.sourceGap.properties.finding.anyOf[0].enum).toEqual([...INTENT_FINDING_CODES, ...CHANGES_FINDING_CODES]);
233
234
  });
234
235
  });
235
236
 
@@ -15,6 +15,8 @@
15
15
  * record is valid and not superseded;
16
16
  * - `blockedBy`: each need that is not done, with its state;
17
17
  * - `implements`: each decision it names, with that decision's state;
18
+ * - `acceptance`, for a kind with acceptance criteria (#2772): how many of the
19
+ * record's criteria passing evidence of the expected verification meets;
18
20
  *
19
21
  * and each decision `implementedBy`, the work records naming it. The
20
22
  * warnings are closed codes. `work-done-gap-open` is not raised here: it
@@ -24,7 +26,7 @@
24
26
 
25
27
  import { dirname } from "node:path";
26
28
  import type { ReasonCode } from "./reason-codes";
27
- import { loadRecordKind, readRecords, type LoadedRecordKind, type ReadRecordsOptions, type RecordEntry } from "./records";
29
+ import { loadRecordKind, normalisePrincipal, readRecords, type LoadedRecordKind, type ReadRecordsOptions, type RecordEntry } from "./records";
28
30
 
29
31
  /** Why a work record carries a warning. Closed, like the record warning codes. */
30
32
  export const WORK_WARNING_CODES = [
@@ -42,6 +44,10 @@ export const WORK_WARNING_CODES = [
42
44
  "work-closed-without-date",
43
45
  /** The record is done, and the finding it came from (`source.finding`) still fires on its region. Raised by `graph --intent`, and by `records` through a walk of that region (#2686). */
44
46
  "work-done-gap-open",
47
+ /** The record is done, and an acceptance criterion has no passing evidence of the verification it expects (#2772). `check` fails on it (WSP117). */
48
+ "work-acceptance-unmet",
49
+ /** A passing `manual` verdict on a criterion names the record's implementer, so it does not count: a manual verdict comes from someone else (#2772). */
50
+ "work-acceptance-self-verified",
45
51
  ] as const satisfies readonly ReasonCode[];
46
52
  export type WorkWarningCode = (typeof WORK_WARNING_CODES)[number];
47
53
 
@@ -61,6 +67,82 @@ export interface DecisionWork {
61
67
  implementedBy: WorkLink[];
62
68
  }
63
69
 
70
+ /** How a criterion is to be verified (#2772): by a unit, integration or end-to-end test, by the running system, or by a person. */
71
+ export const VERIFICATION_TYPES = ["unit", "integration", "e2e", "runtime", "manual"] as const;
72
+ export type VerificationType = (typeof VERIFICATION_TYPES)[number];
73
+
74
+ /** The results a piece of evidence for a criterion can carry. */
75
+ export const EVIDENCE_RESULTS = ["pass", "fail"] as const;
76
+ export type EvidenceResult = (typeof EVIDENCE_RESULTS)[number];
77
+
78
+ /** One acceptance criterion as a work record states it. */
79
+ export interface Criterion {
80
+ id: string;
81
+ text: string;
82
+ verification: VerificationType;
83
+ }
84
+
85
+ /** One criterion on read: whether passing evidence of its verification meets it. */
86
+ export interface CriterionState {
87
+ id: string;
88
+ verification: VerificationType;
89
+ met: boolean;
90
+ }
91
+
92
+ /** A work record's acceptance criteria on read (#2772): how many are met, of how many. */
93
+ export interface WorkAcceptance {
94
+ met: number;
95
+ total: number;
96
+ criteria: CriterionState[];
97
+ }
98
+
99
+ /** The acceptance criteria a record lists in `field`, leaving out entries without a string id and a known verification (the schema reports those). */
100
+ export function acceptanceCriteria(data: Record<string, unknown> | null, field: string): Criterion[] {
101
+ const v = data?.[field];
102
+ if (!Array.isArray(v)) return [];
103
+ return v.filter(
104
+ (c): c is Criterion =>
105
+ c !== null && typeof c === "object" && typeof c.id === "string" && typeof c.text === "string" && (VERIFICATION_TYPES as readonly string[]).includes(c.verification),
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Which of a record's criteria its evidence meets. Evidence meets a criterion
111
+ * when it names the criterion, its result is `pass` and its verification is
112
+ * the criterion's. A `manual` verdict also names who gave it in `by`, and
113
+ * counts only when that is not the record's implementer; one that is lands in
114
+ * `selfVerified`. `acceptance` is null when the record lists no criteria.
115
+ */
116
+ export function workAcceptance(
117
+ data: Record<string, unknown> | null,
118
+ spec: { field: string; implementer: string },
119
+ pinsField: string,
120
+ ): { acceptance: WorkAcceptance | null; selfVerified: string[] } {
121
+ if (!Array.isArray(data?.[spec.field])) return { acceptance: null, selfVerified: [] };
122
+ const criteria = acceptanceCriteria(data, spec.field);
123
+ const implementer = typeof data?.[spec.implementer] === "string" ? normalisePrincipal(data[spec.implementer] as string) : null;
124
+ const evidence = Array.isArray(data?.[pinsField]) ? (data![pinsField] as unknown[]) : [];
125
+ const selfVerified: string[] = [];
126
+ const states = criteria.map((c): CriterionState => {
127
+ let met = false;
128
+ for (const e of evidence) {
129
+ if (e === null || typeof e !== "object") continue;
130
+ const ev = e as Record<string, unknown>;
131
+ if (ev.criterion !== c.id || ev.result !== "pass" || ev.verification !== c.verification) continue;
132
+ if (c.verification === "manual") {
133
+ if (typeof ev.by !== "string") continue;
134
+ if (implementer !== null && normalisePrincipal(ev.by) === implementer) {
135
+ if (!selfVerified.includes(c.id)) selfVerified.push(c.id);
136
+ continue;
137
+ }
138
+ }
139
+ met = true;
140
+ }
141
+ return { id: c.id, verification: c.verification, met };
142
+ });
143
+ return { acceptance: { met: states.filter((c) => c.met).length, total: states.length, criteria: states }, selfVerified };
144
+ }
145
+
64
146
  /** The strings of a front-matter list, or none. */
65
147
  export function idList(data: Record<string, unknown> | null, field: string): string[] {
66
148
  const v = data?.[field];
@@ -143,6 +225,23 @@ export async function applyWork(loaded: LoadedRecordKind, entries: RecordEntry[]
143
225
  if (e.state === work.done && kind.pins && !(Array.isArray(evidence) && evidence.length > 0)) {
144
226
  e.warnings.push({ code: "work-done-unpinned", message: `${e.id ?? e.path} is ${work.done} and ${kind.pins.field} is empty or missing: nothing shows the work was done` });
145
227
  }
228
+ if (work.acceptance && kind.pins) {
229
+ const { acceptance, selfVerified } = workAcceptance(e.data, work.acceptance, kind.pins.field);
230
+ e.acceptance = acceptance;
231
+ if (selfVerified.length > 0) {
232
+ e.warnings.push({
233
+ code: "work-acceptance-self-verified",
234
+ message: `the manual verdict on ${selfVerified.join(", ")} is by ${String(e.data[work.acceptance.implementer])}, the implementer, so it does not count: a manual verdict comes from someone else`,
235
+ });
236
+ }
237
+ const unmet = acceptance?.criteria.filter((c) => !c.met) ?? [];
238
+ if (e.state === work.done && unmet.length > 0) {
239
+ e.warnings.push({
240
+ code: "work-acceptance-unmet",
241
+ message: `${e.id ?? e.path} is ${work.done}, and no passing evidence meets ${unmet.map((c) => `${c.id} (${c.verification})`).join(", ")}`,
242
+ });
243
+ }
244
+ }
146
245
  if (e.state !== null && closed.has(e.state) && typeof e.data[work.closedOn] !== "string") {
147
246
  e.warnings.push({ code: "work-closed-without-date", message: `${e.id ?? e.path} is ${e.state} and has no ${work.closedOn}` });
148
247
  }