@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
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://intentius.io/chant/schemas/workspace/status/v1/status.schema.json",
4
4
  "title": "chant workspace status output",
5
- "description": "What `chant workspace status <env> --json` prints, version 1 of the read contract for releases across members (#2524 D15, D19, #2544). Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists: a new code is a new contract version. Contract version 1 is written by chant 0.81.0 and newer; a reader that needs it refuses output whose `contract` it doesn't know. Every code is in the one closed list of `reason-codes.ts`. Each member's gates, read from its gate ledger on the same branch, are in the JSON only (#2674), and so is each member's box block, from the declaration (#2726), with its isolation resolved (#2727). So are its stewards, their Ops and each Op's last run (#2731), and the work lease each Op's turn holds (#2748).",
5
+ "description": "What `chant workspace status <env> --json` prints, version 1 of the read contract for releases across members (#2524 D15, D19, #2544). Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists: a new code is a new contract version. Contract version 1 is written by chant 0.81.0 and newer; a reader that needs it refuses output whose `contract` it doesn't know. Every code is in the one closed list of `reason-codes.ts`. Each member's gates, read from its gate ledger on the same branch, are in the JSON only (#2674), and so is each member's box block, from the declaration (#2726), with its isolation resolved (#2727). So are its stewards, their Ops and each Op's last run (#2731), the work lease each Op's turn holds (#2748), and the open decision points a steward waits on (#2749).",
6
6
  "oneOf": [{ "$ref": "#/$defs/result" }, { "$ref": "#/$defs/failure" }],
7
7
  "$defs": {
8
8
  "result": {
@@ -50,6 +50,11 @@
50
50
  "type": "array",
51
51
  "items": { "$ref": "#/$defs/lease" }
52
52
  },
53
+ "acceptance": {
54
+ "description": "Added in contract 1 by #2772. Each current work item of a declared work kind whose work block names acceptance criteria, when the item lists some, by declared kind and then item, read in the working tree: how many of its criteria passing evidence meets. Empty when no declared work kind has criteria. A done item with a criterion unmet is what chant workspace check fails on (WSP117).",
55
+ "type": "array",
56
+ "items": { "$ref": "#/$defs/acceptance" }
57
+ },
53
58
  "summary": {
54
59
  "type": "object",
55
60
  "required": ["members", "released", "unreadable", "differing"],
@@ -66,6 +71,18 @@
66
71
  }
67
72
  }
68
73
  },
74
+ "acceptance": {
75
+ "type": "object",
76
+ "required": ["member", "kind", "item", "state", "met", "total"],
77
+ "properties": {
78
+ "member": { "type": ["string", "null"], "description": "The member that declares the work kind, or null for the workspace's own." },
79
+ "kind": { "type": "string", "description": "The work kind file, from the workspace root." },
80
+ "item": { "type": "string", "description": "The work item's id." },
81
+ "state": { "type": ["string", "null"], "description": "The item's state." },
82
+ "met": { "type": "integer", "minimum": 0, "description": "Criteria met by evidence naming them, with result pass and the criterion's verification; a manual verdict counts only from someone other than the implementer." },
83
+ "total": { "type": "integer", "minimum": 0, "description": "Criteria the item lists." }
84
+ }
85
+ },
69
86
  "lease": {
70
87
  "type": "object",
71
88
  "required": ["item", "member", "ref", "holder", "token", "acquiredAt", "expiresAt", "state"],
@@ -135,7 +152,7 @@
135
152
  "stewardForm": { "enum": ["local", "fountain"] },
136
153
  "steward": {
137
154
  "type": "object",
138
- "required": ["name", "file", "form", "forms", "vault", "capabilities", "lease", "ops"],
155
+ "required": ["name", "file", "form", "forms", "vault", "capabilities", "lease", "ops", "waiting"],
139
156
  "properties": {
140
157
  "name": { "type": "string", "description": "The steward's name; on Fountain, its Agent's and Teammate's." },
141
158
  "file": { "type": "string", "description": "The *.op.ts file that declares it, relative to the member's directory." },
@@ -187,7 +204,7 @@
187
204
  "type": "array",
188
205
  "items": {
189
206
  "type": "object",
190
- "required": ["name", "schedule", "env", "lastRun", "changesCheckout", "workLease"],
207
+ "required": ["name", "schedule", "env", "lastRun", "lastTick", "changesCheckout", "workLease"],
191
208
  "properties": {
192
209
  "name": { "type": "string" },
193
210
  "schedule": {
@@ -208,16 +225,77 @@
208
225
  { "type": "null" },
209
226
  {
210
227
  "type": "object",
211
- "required": ["id", "status", "started", "ended", "gate"],
228
+ "required": ["id", "status", "started", "ended", "gate", "point"],
212
229
  "properties": {
213
230
  "id": { "type": "string" },
214
- "status": { "enum": ["ok", "fail", "gated"] },
231
+ "status": { "enum": ["ok", "fail", "gated", "waiting"], "description": "waiting: an activity asked a decision point that is still open (#2749), and the run stopped there." },
215
232
  "started": { "type": "string" },
216
233
  "ended": { "type": "string" },
217
234
  "gate": {
218
235
  "oneOf": [
219
236
  { "type": "null" },
220
- { "type": "object", "required": ["name", "since"], "properties": { "name": { "type": "string" }, "since": { "type": "string" } } }
237
+ {
238
+ "type": "object",
239
+ "required": ["name", "since"],
240
+ "properties": {
241
+ "name": { "type": "string" },
242
+ "since": { "type": "string" },
243
+ "op": { "type": "string", "description": "The op the gate is recorded under: the Op's own, or the command's when a step's command stopped at a gate of its own (#2779), such as workspace-upgrade." },
244
+ "approve": { "type": "string", "description": "The command that approves the gate, run in the member's directory." }
245
+ }
246
+ }
247
+ ]
248
+ },
249
+ "point": {
250
+ "description": "The open decision point a waiting run stopped on (#2749), as the run ledger records it, or null.",
251
+ "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/stewardWait" }]
252
+ }
253
+ }
254
+ }
255
+ ]
256
+ },
257
+ "lastTick": {
258
+ "description": "For a ConvergeOp, its newest tick on the converge ledger (#2778): the rules that fired, what each did and for which resource, and what its observer step reported. null for any other Op, or before its first tick.",
259
+ "oneOf": [
260
+ { "type": "null" },
261
+ {
262
+ "type": "object",
263
+ "required": ["id", "timestamp", "log", "firedRuleIds", "outcomes", "resources"],
264
+ "properties": {
265
+ "id": { "type": ["string", "null"] },
266
+ "timestamp": { "type": "string" },
267
+ "log": { "type": "string" },
268
+ "firedRuleIds": { "type": "array", "items": { "type": "string" } },
269
+ "outcomes": {
270
+ "type": "array",
271
+ "items": {
272
+ "type": "object",
273
+ "required": ["ruleId", "action", "op", "resource", "reason"],
274
+ "properties": {
275
+ "ruleId": { "type": "string" },
276
+ "action": { "enum": ["ran", "reported", "skipped-budget", "skipped-flap", "gated"] },
277
+ "op": { "type": ["string", "null"] },
278
+ "resource": { "type": ["string", "null"], "description": "The resource the rule fired for, on a ConvergeOp with an observer step." },
279
+ "reason": { "type": ["string", "null"] }
280
+ }
281
+ }
282
+ },
283
+ "resources": {
284
+ "description": "What the observer step reported, or null for a ConvergeOp that observes a lexicon environment.",
285
+ "oneOf": [
286
+ { "type": "null" },
287
+ {
288
+ "type": "array",
289
+ "items": {
290
+ "type": "object",
291
+ "required": ["name", "status", "detail"],
292
+ "properties": {
293
+ "name": { "type": "string" },
294
+ "status": { "enum": ["in-sync", "drifted", "unknown"] },
295
+ "detail": { "type": ["string", "null"] }
296
+ }
297
+ }
298
+ }
221
299
  ]
222
300
  }
223
301
  }
@@ -255,13 +333,56 @@
255
333
  }
256
334
  }
257
335
  }
336
+ },
337
+ "waiting": {
338
+ "description": "The open decision points the steward waits on (#2749): one for each Op whose newest run is the steward's own (its ledger record names the steward) and stopped on a question: the steward's own Ops in ops order, then any other Op of the member, such as one the steward's process starts itself (studio#137), by name. The state is the question's when the run stopped; `chant workspace points --open` has its state now, and hud asks a person there. The steward never answers one, and runs an Op it lists again on the first round after it is answered.",
339
+ "type": "array",
340
+ "items": {
341
+ "allOf": [
342
+ { "$ref": "#/$defs/stewardWait" },
343
+ {
344
+ "type": "object",
345
+ "required": ["op", "run"],
346
+ "properties": { "op": { "type": "string" }, "run": { "type": "string", "description": "The waiting run's id." } }
347
+ }
348
+ ]
349
+ }
258
350
  }
259
351
  }
260
352
  },
353
+ "stewardWait": {
354
+ "type": "object",
355
+ "required": ["id", "point", "state", "path", "subject", "since"],
356
+ "properties": {
357
+ "id": { "type": "string", "description": "The answer record's id, as `points` lists the question and `points answer` names it." },
358
+ "point": { "type": "string" },
359
+ "state": { "enum": ["escalated", "proposed"] },
360
+ "path": { "type": "string", "description": "The answer record, from the repository root." },
361
+ "subject": { "type": ["string", "null"] },
362
+ "since": { "type": "string", "description": "When the run stopped on it." }
363
+ }
364
+ },
365
+ "intent": {
366
+ "type": "object",
367
+ "required": ["id", "state", "question", "choice", "answer", "decided_by", "decided_on"],
368
+ "properties": {
369
+ "id": { "type": "string", "description": "The record's id, as the box block names it." },
370
+ "state": { "type": ["string", "null"], "description": "The record's state as written, such as proposed or decided, or null when no decision record has the id." },
371
+ "question": { "type": ["string", "null"], "description": "The question the record answers." },
372
+ "choice": { "description": "The record's choice as written, such as { option, reason }, or null while the record is proposed." },
373
+ "answer": { "type": ["string", "null"], "description": "The chosen option's label, from options[]'s entry whose id is choice.option, or null while no option is chosen (#2855). Added by chant 0.94.0." },
374
+ "decided_by": { "type": ["string", "null"], "description": "Who decided it, or null while it is proposed." },
375
+ "decided_on": { "type": ["string", "null"], "description": "The day it was decided, or null while it is proposed." }
376
+ }
377
+ },
261
378
  "box": {
262
379
  "type": "object",
263
- "required": ["capabilities", "isolation"],
380
+ "required": ["capabilities", "isolation", "intent"],
264
381
  "properties": {
382
+ "intent": {
383
+ "description": "The decision record the box block names as the box's intent (#2850), or null when it names none. A box starts as a question: the record is proposed with a null choice, and decided when the person who answered it amends it. Read from the working tree, from the declared record kinds named decision. When no decision record has the id, every field but id is null and chant workspace check fails (WSP126). Added by chant 0.94.0.",
384
+ "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/intent" }]
385
+ },
265
386
  "isolation": {
266
387
  "description": "The box's isolation resolved from its identity, its host, the member's name and its slot (#2727), or null when the block declares no host. A runtime that plants or starts the box sets these values instead of choosing its own. The same for every environment.",
267
388
  "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/isolation" }]
@@ -371,7 +492,7 @@
371
492
  }
372
493
  },
373
494
  "needed": { "type": "integer", "minimum": 1, "description": "The human approvals the gate needs: its quorum's count, 1 without one." },
374
- "approve": { "type": "string", "description": "The chant approve line for the gate, run in the member's directory: chant approve <component> <gate>, with --env <env> when env is not null." }
495
+ "approve": { "type": "string", "description": "The chant approve line for the gate, run in the member's directory: chant approve <component> <gate>, with --env <env> when env is not null and --plan <planDigest> when planDigest is not null (#2832), so the line approves the plan this read showed." }
375
496
  }
376
497
  },
377
498
  "environment": {
@@ -43,7 +43,8 @@ import { readReleasePlan, type ReleasePlan } from "../lifecycle/plan-ledger";
43
43
  import { listWorkLeases, type WorkLeaseState } from "../lifecycle/work-lease";
44
44
  import { findWorkspaceRoot } from "../project-root";
45
45
  import { resolveBoxes, type ResolvedIsolation } from "./box-isolation";
46
- import { readDeclaration, readerVersion, WorkspaceReadError, type ErrorLocation, type Member } from "./declaration";
46
+ import { resolveBoxIntents, unresolvedIntent, type BoxIntent } from "./box-intent";
47
+ import { declaredRecordKinds, readDeclaration, readerVersion, WorkspaceReadError, type Declaration, type ErrorLocation, type Member } from "./declaration";
47
48
  import type { ReasonCode } from "./reason-codes";
48
49
  import { GATE_REASON_CODES, readMemberGates, type GateLedgerReader, type StatusGate, type StatusGateLedger } from "./status-gates";
49
50
  import { STEWARD_REASON_CODES, readMemberStewards, type StatusSteward, type StewardReasonCode } from "./status-stewards";
@@ -178,6 +179,13 @@ export interface StatusBox {
178
179
  capabilities: { name: string; broker: string | null; scope: string[] }[];
179
180
  /** The box's ports, state paths and cookie names, resolved from its identity, or null when the block declares no host (#2727). */
180
181
  isolation: ResolvedIsolation | null;
182
+ /**
183
+ * The decision record the block names as the box's intent (#2850): its id,
184
+ * state, question, choice, answer (the chosen option's label), decided_by
185
+ * and decided_on. Null when the block names none; every field but the id
186
+ * is null when no decision record has the id (WSP126).
187
+ */
188
+ intent: BoxIntent | null;
181
189
  }
182
190
 
183
191
  /**
@@ -190,6 +198,21 @@ export interface StatusLease extends Omit<WorkLeaseState, "ref"> {
190
198
  ref: string;
191
199
  }
192
200
 
201
+ /**
202
+ * One work item's acceptance criteria, counted (#2772): each current record
203
+ * of a declared work kind with acceptance criteria that lists some.
204
+ */
205
+ export interface StatusAcceptance {
206
+ /** The member that declares the work kind, or null for the workspace's own. */
207
+ member: string | null;
208
+ /** The work kind file, from the workspace root. */
209
+ kind: string;
210
+ item: string;
211
+ state: string | null;
212
+ met: number;
213
+ total: number;
214
+ }
215
+
193
216
  export type StatusDocument =
194
217
  | {
195
218
  $schema: string;
@@ -202,6 +225,8 @@ export type StatusDocument =
202
225
  members: StatusMember[];
203
226
  /** Every work lease in the workspace's ledgers, active and expired, by member then item (#2732). A released lease has no ref and is not listed. */
204
227
  leases: StatusLease[];
228
+ /** Each work item's acceptance criteria, met of total, by declared kind then item (#2772). Empty when no declared work kind has criteria. */
229
+ acceptance: StatusAcceptance[];
205
230
  summary: { members: number; released: number; unreadable: number; differing: number | null };
206
231
  }
207
232
  | {
@@ -360,6 +385,7 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
360
385
  const now = query.now ?? new Date().toISOString();
361
386
 
362
387
  const isolation = new Map(resolveBoxes(declaration).map((b) => [b.member.name, b.isolation]));
388
+ const intents = new Map((await resolveBoxIntents(declaration, found.dir)).map((i) => [i.member, i.record?.intent ?? unresolvedIntent(i.id)]));
363
389
  const members: StatusMember[] = [];
364
390
  for (const m of declaration.members) {
365
391
  const environments: StatusEnvironment[] = [];
@@ -382,6 +408,7 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
382
408
  : {
383
409
  capabilities: m.box.capabilities.map((c) => ({ name: c.name, broker: c.broker, scope: [...c.scope] })),
384
410
  isolation: isolation.get(m.name) ?? null,
411
+ intent: intents.get(m.name) ?? null,
385
412
  },
386
413
  stewards: stewards.stewards,
387
414
  stewardReasons: stewards.reasons,
@@ -395,6 +422,7 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
395
422
  for (const m of members) m.gateLedger.shared = m.gateLedger.layout === "flat" && flatGates > 1;
396
423
 
397
424
  const leases = await readWorkspaceLeases(declaration.members, found.dir, new Date(now));
425
+ const acceptance = await readWorkspaceAcceptance(declaration, found.dir);
398
426
 
399
427
  return {
400
428
  ...head,
@@ -404,6 +432,7 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
404
432
  workspace: { name: declaration.name, root: rootDir === "" ? "." : rootDir, file: declaration.file },
405
433
  members,
406
434
  leases,
435
+ acceptance,
407
436
  summary: {
408
437
  members: members.length,
409
438
  released: members.filter((m) => m.environments[0].releases.length > 0).length,
@@ -436,6 +465,16 @@ async function readWorkspaceLeases(members: Member[], cwd: string, now: Date): P
436
465
  return out;
437
466
  }
438
467
 
468
+ /** The acceptance counts of every declared work kind with criteria (#2772), read in the working tree. */
469
+ async function readWorkspaceAcceptance(declaration: Declaration, root: string): Promise<StatusAcceptance[]> {
470
+ if (declaredRecordKinds(declaration).length === 0) return [];
471
+ const { loadDeclaredKinds } = await import("./declared-kinds");
472
+ const kinds = await loadDeclaredKinds(declaration, workingTree(root), root, { acceptance: true });
473
+ return kinds.flatMap((k) =>
474
+ (k.acceptance ?? []).map((a) => ({ member: k.declared.member, kind: k.declared.path, item: a.item, state: a.state, met: a.met, total: a.total })),
475
+ );
476
+ }
477
+
439
478
  export async function runWorkspaceStatus(ctx: CommandContext): Promise<number> {
440
479
  const { args } = ctx;
441
480
  const env = args.extraPositional;
@@ -525,6 +564,12 @@ export function formatStatus(doc: Extract<StatusDocument, { members: unknown }>)
525
564
  for (const l of doc.leases) rows.push([l.member ? `${l.member}/${l.item}` : l.item, l.holder, l.expiresAt, l.state]);
526
565
  lines.push(...table(rows));
527
566
  }
567
+ if (doc.acceptance.length > 0) {
568
+ lines.push("");
569
+ const rows: string[][] = [["WORK ITEM", "STATE", "CRITERIA MET"]];
570
+ for (const a of doc.acceptance) rows.push([a.member ? `${a.member}/${a.item}` : a.item, a.state ?? "-", `${a.met}/${a.total}`]);
571
+ lines.push(...table(rows));
572
+ }
528
573
  const boxes = doc.members.filter((m) => m.box?.isolation);
529
574
  if (boxes.length > 0) {
530
575
  lines.push("");
@@ -0,0 +1,274 @@
1
+ /**
2
+ * Acceptance criteria on work records (#2772), on a workspace built from the
3
+ * reference workspace's decision and work kinds in a throwaway git repository.
4
+ *
5
+ * - W-001 is done, and passing evidence of each criterion's verification
6
+ * meets both of them, the manual one by bob while alice owns the item: met.
7
+ * - W-002 is done, and its only evidence is a unit test for an e2e
8
+ * criterion: unmet.
9
+ * - W-003 is done, and its manual criterion's verdict is by alice, its owner:
10
+ * the verdict does not count, so it is unmet as well.
11
+ * - W-004 is done and lists no criteria: a legacy item, read as before.
12
+ * - W-005 is in progress. A steward's Op attaches evidence to it under its
13
+ * work lease, and is refused on the manual criterion.
14
+ */
15
+
16
+ import { readFileSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
19
+ import { cleanScratch, contract, git, REPO, repo } from "./__fixtures__/contract-repo";
20
+ import { runDeclarationChecks } from "./checks";
21
+ import { listWorkspaceWithKinds } from "./ls";
22
+ import lsSchema from "./ls.schema.json";
23
+ import { queryRecords } from "./records-cli";
24
+ import recordsSchema from "./records.schema.json";
25
+ import { workspaceStatus } from "./status";
26
+ import statusSchema from "./status.schema.json";
27
+ import { attachWorkEvidence } from "./work-evidence";
28
+ import workEvidenceSchema from "./work-evidence.schema.json";
29
+ import { workEvidence } from "../op/builders";
30
+ import { workEvidence as workEvidenceActivity } from "../op/activities";
31
+ import { ACTIVITY_PROFILES } from "../op/activity-profiles";
32
+ import { OpRunFailure, runOpLocally, type OpRunResult } from "../op/local-executor";
33
+ import type { ActivityFn } from "../op/activity-registry";
34
+ import type { OpConfig } from "../op/types";
35
+ import { declareSteward } from "../op/steward";
36
+ import { claimWorkLease, releaseWorkLease } from "../lifecycle/work-lease";
37
+
38
+ const REF = join(REPO, "reference-workspace");
39
+ const ref = (path: string) => readFileSync(join(REF, path), "utf-8");
40
+
41
+ function work(id: string, fields: Record<string, unknown>): string {
42
+ const data = {
43
+ schema: 1,
44
+ id,
45
+ title: `Work ${id}`,
46
+ state: "done",
47
+ implements: [],
48
+ needs: [],
49
+ constrains: ["member:app"],
50
+ evidence: [],
51
+ owner: "alice",
52
+ opened_on: "2026-09-25",
53
+ closed_on: "2026-09-25",
54
+ source: { kind: "workspace", member: "app" },
55
+ supersedes: [],
56
+ ...fields,
57
+ };
58
+ return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
59
+ }
60
+
61
+ const link = (criterion: string, verification: string, result: string, extra: Record<string, unknown> = {}) => ({
62
+ title: `${criterion} ${verification}`,
63
+ url: `https://example.com/${criterion}`,
64
+ criterion,
65
+ verification,
66
+ result,
67
+ ...extra,
68
+ });
69
+
70
+ const KIND = "work/work.kind.mjs";
71
+ let root: string;
72
+
73
+ beforeAll(() => {
74
+ root = repo({
75
+ "chant.workspace.json": JSON.stringify(
76
+ {
77
+ name: "studio",
78
+ schema: 1,
79
+ members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }],
80
+ records: [{ kind: "decisions/decision.kind.mjs" }, { kind: KIND }],
81
+ },
82
+ null,
83
+ 2,
84
+ ),
85
+ "app/server.mjs": "export const port = 8080;\n",
86
+ "decisions/decision.kind.mjs": ref("decisions/decision.kind.mjs"),
87
+ "decisions/decision.schema.json": ref("decisions/decision.schema.json"),
88
+ "decisions/ref-001-how-the-app-is-deployed.md": ref("decisions/ref-001-how-the-app-is-deployed.md"),
89
+ [KIND]: ref(KIND),
90
+ "work/work.schema.json": ref("work/work.schema.json"),
91
+ "work/W-001-met.md": work("W-001", {
92
+ acceptance: [
93
+ { id: "AC-1", text: "The server answers on its port", verification: "unit" },
94
+ { id: "AC-2", text: "A designer finds the page matches the spec", verification: "manual" },
95
+ ],
96
+ evidence: [link("AC-1", "unit", "fail"), link("AC-1", "unit", "pass"), link("AC-2", "manual", "pass", { by: "bob" })],
97
+ }),
98
+ "work/W-002-unmet.md": work("W-002", {
99
+ acceptance: [{ id: "AC-1", text: "The page loads in a browser", verification: "e2e" }],
100
+ evidence: [link("AC-1", "unit", "pass")],
101
+ }),
102
+ "work/W-003-self.md": work("W-003", {
103
+ acceptance: [{ id: "AC-1", text: "Someone else signs off", verification: "manual" }],
104
+ evidence: [link("AC-1", "manual", "pass", { by: "Alice" })],
105
+ }),
106
+ "work/W-004-legacy.md": work("W-004", { evidence: [{ title: "The review", url: "https://example.com/review" }] }),
107
+ "work/W-005-leased.md": work("W-005", {
108
+ state: "in-progress",
109
+ closed_on: null,
110
+ acceptance: [
111
+ { id: "AC-1", text: "The server's test passes", verification: "unit" },
112
+ { id: "AC-2", text: "A person tries it", verification: "manual" },
113
+ ],
114
+ }),
115
+ });
116
+ git(root, "config", "user.name", "t");
117
+ git(root, "config", "user.email", "t@example.com");
118
+ git(root, "config", "commit.gpgsign", "false");
119
+ git(root, "add", "-A");
120
+ git(root, "commit", "-q", "-m", "the work");
121
+ });
122
+ afterAll(cleanScratch);
123
+
124
+ const records = contract(recordsSchema);
125
+
126
+ async function read() {
127
+ const doc = await queryRecords({ kind: join(root, KIND), cwd: root, workGaps: false });
128
+ records.expectValid(doc);
129
+ if ("error" in doc || !("records" in doc)) throw new Error(JSON.stringify(doc));
130
+ return doc.records;
131
+ }
132
+
133
+ const codes = (r: { warnings: { code: string }[] }) => r.warnings.map((w) => w.code);
134
+
135
+ describe("acceptance criteria on read (#2772)", () => {
136
+ test("met: each criterion has passing evidence of its verification, and the manual verdict is someone else's", async () => {
137
+ const w1 = (await read()).find((r) => r.id === "W-001")!;
138
+ expect(w1.valid).toBe(true);
139
+ expect(w1.acceptance).toEqual({
140
+ met: 2,
141
+ total: 2,
142
+ criteria: [
143
+ { id: "AC-1", verification: "unit", met: true },
144
+ { id: "AC-2", verification: "manual", met: true },
145
+ ],
146
+ });
147
+ expect(codes(w1)).toEqual([]);
148
+ });
149
+
150
+ test("unmet: evidence of another verification does not count, and the done item is warned", async () => {
151
+ const w2 = (await read()).find((r) => r.id === "W-002")!;
152
+ expect(w2.acceptance).toMatchObject({ met: 0, total: 1 });
153
+ expect(codes(w2)).toEqual(["work-acceptance-unmet"]);
154
+ expect(w2.warnings[0].message).toMatch(/AC-1 \(e2e\)/);
155
+ });
156
+
157
+ test("manual by the implementer: the verdict is refused, warned, and the criterion stays unmet", async () => {
158
+ const w3 = (await read()).find((r) => r.id === "W-003")!;
159
+ expect(w3.acceptance).toMatchObject({ met: 0, total: 1 });
160
+ expect(codes(w3)).toEqual(["work-acceptance-self-verified", "work-acceptance-unmet"]);
161
+ });
162
+
163
+ test("a legacy item without criteria is valid, reads acceptance null and gets no acceptance warning", async () => {
164
+ const w4 = (await read()).find((r) => r.id === "W-004")!;
165
+ expect(w4.valid).toBe(true);
166
+ expect(w4.acceptance).toBeNull();
167
+ expect(codes(w4)).toEqual([]);
168
+ });
169
+
170
+ test("an open item with criteria unmet is not warned: only done needs them met", async () => {
171
+ const w5 = (await read()).find((r) => r.id === "W-005")!;
172
+ expect(w5.acceptance).toMatchObject({ met: 0, total: 2 });
173
+ expect(codes(w5)).toEqual([]);
174
+ });
175
+
176
+ test("check fails with WSP117 and the reason code on each done item with a criterion unmet", async () => {
177
+ const report = await runDeclarationChecks(root);
178
+ const wsp117 = report.diagnostics.filter((d) => d.ruleId === "WSP117");
179
+ expect(wsp117.map((d) => [d.file, d.code, d.severity])).toEqual([
180
+ ["work/W-002-unmet.md", "work-acceptance-unmet", "error"],
181
+ ["work/W-003-self.md", "work-acceptance-unmet", "error"],
182
+ ]);
183
+ expect(report.ok).toBe(false);
184
+ });
185
+
186
+ test("ls --json and status --json count each item's criteria, met of total, in the read contract", async () => {
187
+ const ls = await listWorkspaceWithKinds({ cwd: root });
188
+ contract(lsSchema).expectValid(ls);
189
+ if ("error" in ls) throw new Error(ls.error.message);
190
+ const kinds = Object.fromEntries(ls.workspace.records.map((r) => [r.path, r.acceptance]));
191
+ expect(kinds["decisions/decision.kind.mjs"]).toBeNull();
192
+ expect(kinds[KIND]).toEqual([
193
+ { item: "W-001", state: "done", met: 2, total: 2 },
194
+ { item: "W-002", state: "done", met: 0, total: 1 },
195
+ { item: "W-003", state: "done", met: 0, total: 1 },
196
+ { item: "W-005", state: "in-progress", met: 0, total: 2 },
197
+ ]);
198
+
199
+ const status = await workspaceStatus({ cwd: root, env: "dev" });
200
+ contract(statusSchema).expectValid(status);
201
+ if ("error" in status) throw new Error(status.error.message);
202
+ expect(status.acceptance.map((a) => [a.member, a.kind, a.item, `${a.met}/${a.total}`])).toEqual([
203
+ [null, KIND, "W-001", "2/2"],
204
+ [null, KIND, "W-002", "0/1"],
205
+ [null, KIND, "W-003", "0/1"],
206
+ [null, KIND, "W-005", "0/2"],
207
+ ]);
208
+ });
209
+ });
210
+
211
+ describe("a steward Op under workLease attaches evidence to a criterion (#2772)", () => {
212
+ const evidenceDoc = contract(workEvidenceSchema);
213
+
214
+ async function run(config: OpConfig, holder: string): Promise<OpRunResult> {
215
+ const activities = new Map<string, ActivityFn>([["workEvidence", workEvidenceActivity as unknown as ActivityFn]]);
216
+ try {
217
+ return await runOpLocally(config, activities, ACTIVITY_PROFILES, undefined, { cwd: root, ledger: { cwd: root }, work: { holder } });
218
+ } catch (err) {
219
+ if (err instanceof OpRunFailure) return err.result;
220
+ throw err;
221
+ }
222
+ }
223
+
224
+ const op = (step: ReturnType<typeof workEvidence>): OpConfig => ({
225
+ name: "verify",
226
+ overview: "attach the test run to its criterion",
227
+ workLease: { item: "W-005", kind: KIND },
228
+ phases: [{ name: "Verify", steps: [step] }],
229
+ });
230
+
231
+ test("the run holding the lease appends passing evidence, and the criterion is met", async () => {
232
+ const config = op(workEvidence("AC-1", { result: "pass", title: "the server test", url: "https://example.com/ci/1", cwd: root }));
233
+ expect(declareSteward({ name: "box", ops: [config] }).ops).toHaveLength(1);
234
+ const result = await run(config, "box/verify@h1");
235
+ expect(result.status).toBe("ok");
236
+ expect(result.workLease).toMatchObject({ item: "W-005", released: true });
237
+ const w5 = (await read()).find((r) => r.id === "W-005")!;
238
+ expect(w5.acceptance).toMatchObject({ met: 1, total: 2 });
239
+ expect(w5.data!.evidence).toEqual([
240
+ expect.objectContaining({ url: "https://example.com/ci/1", criterion: "AC-1", verification: "unit", result: "pass", by: "box/verify@h1" }),
241
+ ]);
242
+ });
243
+
244
+ test("a manual criterion is refused: the run holding the lease is the implementer", async () => {
245
+ const result = await run(op(workEvidence("AC-2", { result: "pass", title: "tried it", url: "https://example.com/tried", cwd: root })), "box/verify@h2");
246
+ expect(result.status).toBe("fail");
247
+ expect(JSON.stringify(result.records)).toMatch(/work-acceptance-self-verified/);
248
+ expect((await read()).find((r) => r.id === "W-005")!.acceptance).toMatchObject({ met: 1, total: 2 });
249
+ });
250
+
251
+ test("without the live lease, or with another token or criterion, nothing is written", async () => {
252
+ const ask = (over: Partial<Parameters<typeof attachWorkEvidence>[0]>) =>
253
+ attachWorkEvidence({ cwd: root, item: "W-005", holder: "me", token: "t", criterion: "AC-1", result: "pass", title: "x", url: "https://example.com/x", ...over });
254
+ const unleased = await ask({});
255
+ evidenceDoc.expectValid(unleased);
256
+ expect("error" in unleased && unleased.error.code).toBe("lease-not-held");
257
+
258
+ const claimed = await claimWorkLease("W-005", "me", { cwd: join(root, "work") });
259
+ if (!claimed.ok) throw new Error(claimed.message);
260
+ try {
261
+ expect(await ask({ token: "other" })).toMatchObject({ error: { code: "lease-token-mismatch" } });
262
+ expect(await ask({ holder: "you", token: claimed.lease.token })).toMatchObject({ error: { code: "lease-held" } });
263
+ expect(await ask({ token: claimed.lease.token, criterion: "AC-9" })).toMatchObject({ error: { code: "work-criterion-unknown" } });
264
+ const ok = await ask({ token: claimed.lease.token, result: "fail" });
265
+ evidenceDoc.expectValid(ok);
266
+ expect(ok).toMatchObject({ item: "W-005", kind: KIND, path: "work/W-005-leased.md", acceptance: { met: 1, total: 2 } });
267
+ } finally {
268
+ await releaseWorkLease("W-005", "me", { cwd: join(root, "work"), token: claimed.lease.token });
269
+ }
270
+ expect(await attachWorkEvidence({ cwd: root, item: "W-004", holder: "me", token: "t", criterion: "AC-1", result: "pass", title: "x", url: "https://example.com/x" })).toMatchObject({
271
+ error: { code: "work-item-closed" },
272
+ });
273
+ });
274
+ });
@@ -158,7 +158,7 @@ describe("chant workspace work (#2732)", () => {
158
158
  expect(json(closed).error.code).toBe("work-item-closed");
159
159
  const verb = await chant(root, ["workspace", "work", "grab", "W-001", "--holder", "a"]);
160
160
  expect(verb.code).toBe(1);
161
- expect(verb.stderr).toContain("claim, renew or release");
161
+ expect(verb.stderr).toContain("claim, renew, release or history");
162
162
  const noHolder = await chant(root, ["workspace", "work", "claim", "W-001"]);
163
163
  expect(noHolder.code).toBe(1);
164
164
  expect(noHolder.stderr).toContain("--holder");