@intentius/chant 0.102.0 → 0.104.1

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 (471) hide show
  1. package/dist/audit/core.d.ts +2 -0
  2. package/dist/audit/core.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/change-set.d.ts +232 -0
  5. package/dist/change-set.d.ts.map +1 -0
  6. package/dist/cli/commands/audit.d.ts +7 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/handlers/build.d.ts.map +1 -1
  10. package/dist/cli/handlers/change-set.d.ts +11 -0
  11. package/dist/cli/handlers/change-set.d.ts.map +1 -0
  12. package/dist/cli/handlers/fan-out.d.ts +33 -0
  13. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  14. package/dist/cli/handlers/operator.d.ts +7 -0
  15. package/dist/cli/handlers/operator.d.ts.map +1 -1
  16. package/dist/cli/handlers/pr.d.ts +31 -0
  17. package/dist/cli/handlers/pr.d.ts.map +1 -0
  18. package/dist/cli/handlers/run.d.ts.map +1 -1
  19. package/dist/cli/main.d.ts.map +1 -1
  20. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  21. package/dist/cli/mcp/workspace-tools.d.ts +3 -2
  22. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  23. package/dist/cli/registry.d.ts +63 -1
  24. package/dist/cli/registry.d.ts.map +1 -1
  25. package/dist/components/auto-release.d.ts +16 -0
  26. package/dist/components/auto-release.d.ts.map +1 -1
  27. package/dist/components/capability.d.ts +53 -0
  28. package/dist/components/capability.d.ts.map +1 -1
  29. package/dist/components/changed-files.d.ts +31 -0
  30. package/dist/components/changed-files.d.ts.map +1 -0
  31. package/dist/components/cli-support.d.ts +9 -1
  32. package/dist/components/cli-support.d.ts.map +1 -1
  33. package/dist/components/component.d.ts +24 -0
  34. package/dist/components/component.d.ts.map +1 -1
  35. package/dist/components/deploy-units.d.ts.map +1 -1
  36. package/dist/components/driver.d.ts +13 -0
  37. package/dist/components/driver.d.ts.map +1 -1
  38. package/dist/components/fan-out-output.d.ts +6 -0
  39. package/dist/components/fan-out-output.d.ts.map +1 -1
  40. package/dist/components/fan-out-record.d.ts +41 -0
  41. package/dist/components/fan-out-record.d.ts.map +1 -0
  42. package/dist/components/fan-out-run.d.ts +65 -0
  43. package/dist/components/fan-out-run.d.ts.map +1 -1
  44. package/dist/components/fan-out-support.d.ts +2 -0
  45. package/dist/components/fan-out-support.d.ts.map +1 -1
  46. package/dist/components/fan-out.d.ts +6 -0
  47. package/dist/components/fan-out.d.ts.map +1 -1
  48. package/dist/components/gated-wave-pipeline.d.ts +50 -0
  49. package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
  50. package/dist/components/index.d.ts +1 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/layers.d.ts +25 -0
  53. package/dist/components/layers.d.ts.map +1 -0
  54. package/dist/components/pr-pipeline.d.ts +61 -0
  55. package/dist/components/pr-pipeline.d.ts.map +1 -0
  56. package/dist/components/pr-run.d.ts +119 -0
  57. package/dist/components/pr-run.d.ts.map +1 -0
  58. package/dist/components/promote.d.ts.map +1 -1
  59. package/dist/components/verbs/shell.d.ts.map +1 -1
  60. package/dist/components/wave-plan.d.ts +49 -0
  61. package/dist/components/wave-plan.d.ts.map +1 -0
  62. package/dist/gated-waves.d.ts +147 -0
  63. package/dist/gated-waves.d.ts.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/lexicon.d.ts +48 -0
  67. package/dist/lexicon.d.ts.map +1 -1
  68. package/dist/lifecycle/gate-ledger.d.ts +7 -0
  69. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  70. package/dist/lifecycle/git.d.ts +46 -1
  71. package/dist/lifecycle/git.d.ts.map +1 -1
  72. package/dist/lint/post-synth.d.ts +18 -0
  73. package/dist/lint/post-synth.d.ts.map +1 -1
  74. package/dist/op/activities/activity-contracts.d.ts +126 -0
  75. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  76. package/dist/op/activities/change-set.d.ts +62 -0
  77. package/dist/op/activities/change-set.d.ts.map +1 -0
  78. package/dist/op/activities/decide.d.ts +2 -0
  79. package/dist/op/activities/decide.d.ts.map +1 -1
  80. package/dist/op/activities/factory.d.ts +137 -0
  81. package/dist/op/activities/factory.d.ts.map +1 -0
  82. package/dist/op/activities/index.d.ts +4 -0
  83. package/dist/op/activities/index.d.ts.map +1 -1
  84. package/dist/op/activity-contract.d.ts.map +1 -1
  85. package/dist/op/builders.d.ts +11 -0
  86. package/dist/op/builders.d.ts.map +1 -1
  87. package/dist/op/decide-backend.d.ts +5 -1
  88. package/dist/op/decide-backend.d.ts.map +1 -1
  89. package/dist/op/decide-read-inputs.d.ts +4 -2
  90. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  91. package/dist/op/factory.d.ts +83 -0
  92. package/dist/op/factory.d.ts.map +1 -0
  93. package/dist/op/index.d.ts +4 -1
  94. package/dist/op/index.d.ts.map +1 -1
  95. package/dist/op/op-ir.d.ts +2 -0
  96. package/dist/op/op-ir.d.ts.map +1 -1
  97. package/dist/op/op-verb-class.d.ts.map +1 -1
  98. package/dist/op/types.d.ts +8 -0
  99. package/dist/op/types.d.ts.map +1 -1
  100. package/dist/op/work-lease-run.d.ts.map +1 -1
  101. package/dist/plan-summary.d.ts +207 -0
  102. package/dist/plan-summary.d.ts.map +1 -0
  103. package/dist/pr-forge.d.ts +105 -0
  104. package/dist/pr-forge.d.ts.map +1 -0
  105. package/dist/pr-loop.d.ts +178 -0
  106. package/dist/pr-loop.d.ts.map +1 -0
  107. package/dist/telemetry-attribution.d.ts +31 -0
  108. package/dist/telemetry-attribution.d.ts.map +1 -1
  109. package/dist/terraform/hcl2json.d.ts +45 -0
  110. package/dist/terraform/hcl2json.d.ts.map +1 -0
  111. package/dist/terraform/parse.d.ts +4 -40
  112. package/dist/terraform/parse.d.ts.map +1 -1
  113. package/dist/vendor/js-yaml-load.d.ts +56 -0
  114. package/dist/vendor/js-yaml-load.d.ts.map +1 -0
  115. package/dist/workspace/box-cli.d.ts +12 -0
  116. package/dist/workspace/box-cli.d.ts.map +1 -0
  117. package/dist/workspace/box-factory.d.ts +27 -3
  118. package/dist/workspace/box-factory.d.ts.map +1 -1
  119. package/dist/workspace/box-listing.d.ts +85 -0
  120. package/dist/workspace/box-listing.d.ts.map +1 -0
  121. package/dist/workspace/box-publish.d.ts +147 -0
  122. package/dist/workspace/box-publish.d.ts.map +1 -0
  123. package/dist/workspace/broker-protocol.d.ts +190 -0
  124. package/dist/workspace/broker-protocol.d.ts.map +1 -0
  125. package/dist/workspace/checks.d.ts.map +1 -1
  126. package/dist/workspace/composites.d.ts +2 -2
  127. package/dist/workspace/composites.d.ts.map +1 -1
  128. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
  129. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
  130. package/dist/workspace/conformance/broker.d.ts +202 -0
  131. package/dist/workspace/conformance/broker.d.ts.map +1 -0
  132. package/dist/workspace/conformance/index.d.ts +2 -1
  133. package/dist/workspace/conformance/index.d.ts.map +1 -1
  134. package/dist/workspace/conformance/vitest.d.ts +9 -2
  135. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  136. package/dist/workspace/conformance/writer.d.ts +108 -14
  137. package/dist/workspace/conformance/writer.d.ts.map +1 -1
  138. package/dist/workspace/decide.d.ts +126 -12
  139. package/dist/workspace/decide.d.ts.map +1 -1
  140. package/dist/workspace/decision-points.schema.json +11 -4
  141. package/dist/workspace/declaration.d.ts +65 -0
  142. package/dist/workspace/declaration.d.ts.map +1 -1
  143. package/dist/workspace/declaration.schema.json +153 -4
  144. package/dist/workspace/environments.d.ts +36 -10
  145. package/dist/workspace/environments.d.ts.map +1 -1
  146. package/dist/workspace/factory-rules.d.ts +180 -0
  147. package/dist/workspace/factory-rules.d.ts.map +1 -0
  148. package/dist/workspace/init.d.ts +21 -0
  149. package/dist/workspace/init.d.ts.map +1 -1
  150. package/dist/workspace/intent-cli.d.ts +1 -1
  151. package/dist/workspace/intent-cli.d.ts.map +1 -1
  152. package/dist/workspace/intent-record.d.ts +28 -1
  153. package/dist/workspace/intent-record.d.ts.map +1 -1
  154. package/dist/workspace/intent.d.ts +102 -5
  155. package/dist/workspace/intent.d.ts.map +1 -1
  156. package/dist/workspace/json-edit.d.ts +24 -0
  157. package/dist/workspace/json-edit.d.ts.map +1 -0
  158. package/dist/workspace/jsonc.d.ts +11 -0
  159. package/dist/workspace/jsonc.d.ts.map +1 -1
  160. package/dist/workspace/kinds.d.ts +48 -0
  161. package/dist/workspace/kinds.d.ts.map +1 -1
  162. package/dist/workspace/lexicon-symbols.d.ts +29 -0
  163. package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
  164. package/dist/workspace/lineage-upgrade.d.ts +31 -2
  165. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  166. package/dist/workspace/point-answer.schema.json +42 -2
  167. package/dist/workspace/points-cli.d.ts +10 -3
  168. package/dist/workspace/points-cli.d.ts.map +1 -1
  169. package/dist/workspace/points.d.ts +66 -5
  170. package/dist/workspace/points.d.ts.map +1 -1
  171. package/dist/workspace/reason-codes.d.ts +33 -4
  172. package/dist/workspace/reason-codes.d.ts.map +1 -1
  173. package/dist/workspace/record-sessions.d.ts +5 -7
  174. package/dist/workspace/record-sessions.d.ts.map +1 -1
  175. package/dist/workspace/records-cli.d.ts +7 -0
  176. package/dist/workspace/records-cli.d.ts.map +1 -1
  177. package/dist/workspace/records-close.d.ts +7 -3
  178. package/dist/workspace/records-close.d.ts.map +1 -1
  179. package/dist/workspace/records-write.d.ts +66 -6
  180. package/dist/workspace/records-write.d.ts.map +1 -1
  181. package/dist/workspace/records.d.ts +28 -9
  182. package/dist/workspace/records.d.ts.map +1 -1
  183. package/dist/workspace/runs-cli.d.ts +138 -6
  184. package/dist/workspace/runs-cli.d.ts.map +1 -1
  185. package/dist/workspace/runs.d.ts +132 -11
  186. package/dist/workspace/runs.d.ts.map +1 -1
  187. package/dist/workspace/session-kinds.d.ts +1 -1
  188. package/dist/workspace/session-kinds.d.ts.map +1 -1
  189. package/dist/workspace/squash.d.ts +80 -0
  190. package/dist/workspace/squash.d.ts.map +1 -0
  191. package/dist/workspace/status-gates.d.ts +2 -0
  192. package/dist/workspace/status-gates.d.ts.map +1 -1
  193. package/dist/workspace/status.d.ts +54 -0
  194. package/dist/workspace/status.d.ts.map +1 -1
  195. package/dist/workspace/symbols.d.ts +16 -5
  196. package/dist/workspace/symbols.d.ts.map +1 -1
  197. package/dist/workspace/trust/run-commits.d.ts +46 -0
  198. package/dist/workspace/trust/run-commits.d.ts.map +1 -0
  199. package/dist/workspace/trust/run-statement.d.ts +128 -0
  200. package/dist/workspace/trust/run-statement.d.ts.map +1 -0
  201. package/dist/workspace/trust/seal.d.ts +6 -1
  202. package/dist/workspace/trust/seal.d.ts.map +1 -1
  203. package/dist/workspace/trust/verify-cli.d.ts +1 -1
  204. package/dist/workspace/trust/verify.d.ts +16 -2
  205. package/dist/workspace/trust/verify.d.ts.map +1 -1
  206. package/dist/workspace/wip-cli.d.ts +21 -0
  207. package/dist/workspace/wip-cli.d.ts.map +1 -0
  208. package/dist/workspace/wip.d.ts +268 -0
  209. package/dist/workspace/wip.d.ts.map +1 -0
  210. package/dist/workspace/work-cli.d.ts.map +1 -1
  211. package/dist/workspace/work-evidence.d.ts +1 -1
  212. package/dist/workspace/work-evidence.d.ts.map +1 -1
  213. package/dist/workspace/work-size.d.ts +55 -0
  214. package/dist/workspace/work-size.d.ts.map +1 -0
  215. package/dist/workspace/workspace-kinds.schema.json +122 -0
  216. package/dist/workspace/write-lock-cli.d.ts +60 -0
  217. package/dist/workspace/write-lock-cli.d.ts.map +1 -0
  218. package/dist/workspace/write-lock.d.ts +152 -0
  219. package/dist/workspace/write-lock.d.ts.map +1 -0
  220. package/dist/workspace/write-scope.d.ts +6 -2
  221. package/dist/workspace/write-scope.d.ts.map +1 -1
  222. package/dist/yaml.d.ts +1 -1
  223. package/dist/yaml.d.ts.map +1 -1
  224. package/package.json +6 -1
  225. package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
  226. package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
  227. package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
  228. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
  229. package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
  230. package/src/__fixtures__/change-set/warden.plan.json +44 -0
  231. package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
  232. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
  233. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
  234. package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
  235. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
  236. package/src/audit/core.test.ts +15 -0
  237. package/src/audit/core.ts +8 -2
  238. package/src/audit/discover.test.ts +11 -0
  239. package/src/audit/discover.ts +7 -2
  240. package/src/change-set-bundle.test.ts +57 -0
  241. package/src/change-set.test.ts +215 -0
  242. package/src/change-set.ts +445 -0
  243. package/src/cli/commands/audit-opt-in.test.ts +54 -0
  244. package/src/cli/commands/audit.ts +29 -2
  245. package/src/cli/commands/build.test.ts +19 -0
  246. package/src/cli/commands/build.ts +3 -2
  247. package/src/cli/handlers/build.ts +13 -1
  248. package/src/cli/handlers/change-set.test.ts +56 -0
  249. package/src/cli/handlers/change-set.ts +44 -0
  250. package/src/cli/handlers/fan-out.test.ts +221 -2
  251. package/src/cli/handlers/fan-out.ts +165 -75
  252. package/src/cli/handlers/graph.ts +3 -0
  253. package/src/cli/handlers/operator.test.ts +21 -0
  254. package/src/cli/handlers/operator.ts +34 -3
  255. package/src/cli/handlers/pr.ts +373 -0
  256. package/src/cli/handlers/run.ts +1 -0
  257. package/src/cli/main.test.ts +20 -0
  258. package/src/cli/main.ts +185 -23
  259. package/src/cli/mcp/never-over-mcp.test.ts +101 -0
  260. package/src/cli/mcp/op-tools.ts +24 -2
  261. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  262. package/src/cli/mcp/workspace-tools.ts +34 -6
  263. package/src/cli/registry.ts +63 -1
  264. package/src/components/auto-release.ts +1 -1
  265. package/src/components/capability.ts +55 -0
  266. package/src/components/changed-files.ts +66 -0
  267. package/src/components/cli-support.ts +28 -5
  268. package/src/components/component.schema.json +17 -0
  269. package/src/components/component.ts +25 -0
  270. package/src/components/deploy-units.ts +3 -0
  271. package/src/components/driver.test.ts +52 -0
  272. package/src/components/driver.ts +31 -13
  273. package/src/components/fan-out-output.ts +42 -1
  274. package/src/components/fan-out-record.ts +69 -0
  275. package/src/components/fan-out-run.test.ts +17 -0
  276. package/src/components/fan-out-run.ts +304 -1
  277. package/src/components/fan-out-support.ts +3 -0
  278. package/src/components/fan-out-waves.test.ts +375 -0
  279. package/src/components/fan-out.ts +18 -17
  280. package/src/components/gated-wave-pipeline.ts +82 -0
  281. package/src/components/index.ts +1 -0
  282. package/src/components/layers.ts +30 -0
  283. package/src/components/pr-pipeline.ts +91 -0
  284. package/src/components/pr-run.test.ts +205 -0
  285. package/src/components/pr-run.ts +280 -0
  286. package/src/components/promote.ts +5 -0
  287. package/src/components/verbs/shell.test.ts +11 -0
  288. package/src/components/verbs/shell.ts +7 -3
  289. package/src/components/wave-plan.ts +124 -0
  290. package/src/discovery/sandbox/bundle.ts +1 -1
  291. package/src/gated-waves-bundle.test.ts +62 -0
  292. package/src/gated-waves.test.ts +118 -0
  293. package/src/gated-waves.ts +240 -0
  294. package/src/index.ts +1 -0
  295. package/src/lexicon.ts +51 -0
  296. package/src/lifecycle/gate-ledger.ts +7 -0
  297. package/src/lifecycle/git.ts +102 -16
  298. package/src/lint/post-synth.test.ts +19 -0
  299. package/src/lint/post-synth.ts +19 -1
  300. package/src/meta/declared-imports.test.ts +1 -1
  301. package/src/op/activities/activity-contracts.ts +84 -0
  302. package/src/op/activities/change-set.ts +114 -0
  303. package/src/op/activities/decide.ts +3 -0
  304. package/src/op/activities/factory.ts +675 -0
  305. package/src/op/activities/index.ts +6 -0
  306. package/src/op/activity-contract.test.ts +21 -1
  307. package/src/op/activity-contract.ts +19 -9
  308. package/src/op/builders.ts +23 -0
  309. package/src/op/decide-backend.ts +5 -1
  310. package/src/op/decide-read-inputs.ts +33 -5
  311. package/src/op/factory.e2e.test.ts +232 -0
  312. package/src/op/factory.ts +118 -0
  313. package/src/op/index.ts +9 -1
  314. package/src/op/op-ir.test.ts +9 -0
  315. package/src/op/op-ir.ts +4 -0
  316. package/src/op/op-verb-class.ts +3 -0
  317. package/src/op/types.ts +8 -0
  318. package/src/op/work-lease-run.ts +3 -0
  319. package/src/plan-summary.test.ts +263 -0
  320. package/src/plan-summary.ts +845 -0
  321. package/src/pr-forge.test.ts +237 -0
  322. package/src/pr-forge.ts +332 -0
  323. package/src/pr-loop-bundle.test.ts +46 -0
  324. package/src/pr-loop.test.ts +171 -0
  325. package/src/pr-loop.ts +324 -0
  326. package/src/telemetry-attribution.test.ts +23 -1
  327. package/src/telemetry-attribution.ts +51 -0
  328. package/src/terraform/hcl2json.test.ts +21 -0
  329. package/src/terraform/hcl2json.ts +81 -0
  330. package/src/terraform/parse.ts +11 -74
  331. package/src/vendor/js-yaml-load.test.ts +115 -0
  332. package/src/vendor/js-yaml-load.ts +1939 -0
  333. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  334. package/src/workspace/app-kind.test.ts +196 -0
  335. package/src/workspace/box-cli.ts +113 -0
  336. package/src/workspace/box-factory.test.ts +4 -1
  337. package/src/workspace/box-factory.ts +37 -4
  338. package/src/workspace/box-listing-write.schema.json +204 -0
  339. package/src/workspace/box-listing.test.ts +234 -0
  340. package/src/workspace/box-listing.ts +324 -0
  341. package/src/workspace/box-publish.schema.json +498 -0
  342. package/src/workspace/box-publish.test.ts +204 -0
  343. package/src/workspace/box-publish.ts +420 -0
  344. package/src/workspace/box-ship.test.ts +96 -0
  345. package/src/workspace/broker-protocol.schema.json +186 -0
  346. package/src/workspace/broker-protocol.test.ts +122 -0
  347. package/src/workspace/broker-protocol.ts +317 -0
  348. package/src/workspace/builder-tiers.test.ts +146 -0
  349. package/src/workspace/change-set.schema.json +460 -0
  350. package/src/workspace/chant-migrations.e2e.test.ts +17 -5
  351. package/src/workspace/checks/boxes.test.ts +3 -0
  352. package/src/workspace/checks.ts +10 -3
  353. package/src/workspace/component-environments.test.ts +105 -0
  354. package/src/workspace/composites.schema.json +23 -4
  355. package/src/workspace/composites.test.ts +8 -8
  356. package/src/workspace/composites.ts +13 -2
  357. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
  358. package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
  359. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
  360. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
  361. package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  362. package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  363. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
  364. package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  365. package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  366. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
  367. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
  368. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
  369. package/src/workspace/conformance/broker.test.ts +122 -0
  370. package/src/workspace/conformance/broker.ts +792 -0
  371. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  372. package/src/workspace/conformance/index.mjs +22 -1
  373. package/src/workspace/conformance/index.ts +5 -1
  374. package/src/workspace/conformance/vitest.ts +33 -1
  375. package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
  376. package/src/workspace/conformance/writer.test.ts +32 -2
  377. package/src/workspace/conformance/writer.ts +339 -28
  378. package/src/workspace/decide.test.ts +208 -4
  379. package/src/workspace/decide.ts +427 -73
  380. package/src/workspace/decision-points.schema.json +11 -4
  381. package/src/workspace/declaration.schema.json +153 -4
  382. package/src/workspace/declaration.ts +141 -0
  383. package/src/workspace/environments.ts +66 -14
  384. package/src/workspace/factory-rules.test.ts +62 -0
  385. package/src/workspace/factory-rules.ts +218 -0
  386. package/src/workspace/graph-cli.ts +2 -2
  387. package/src/workspace/graph.schema.json +67 -0
  388. package/src/workspace/identity.test.ts +23 -0
  389. package/src/workspace/init.ts +82 -2
  390. package/src/workspace/intent-cli.ts +26 -9
  391. package/src/workspace/intent-patch-id.test.ts +162 -0
  392. package/src/workspace/intent-record.schema.json +130 -3
  393. package/src/workspace/intent-record.ts +74 -28
  394. package/src/workspace/intent-reviews.test.ts +76 -0
  395. package/src/workspace/intent-squash.test.ts +213 -0
  396. package/src/workspace/intent-why.test.ts +5 -5
  397. package/src/workspace/intent.schema.json +234 -4
  398. package/src/workspace/intent.ts +260 -66
  399. package/src/workspace/json-edit.test.ts +121 -0
  400. package/src/workspace/json-edit.ts +142 -0
  401. package/src/workspace/jsonc.ts +20 -0
  402. package/src/workspace/kinds.ts +157 -2
  403. package/src/workspace/lexicon-symbols.test.ts +148 -0
  404. package/src/workspace/lexicon-symbols.ts +125 -0
  405. package/src/workspace/lineage-upgrade.test.ts +63 -0
  406. package/src/workspace/lineage-upgrade.ts +146 -5
  407. package/src/workspace/plan-summary.schema.json +155 -0
  408. package/src/workspace/point-answer.schema.json +42 -2
  409. package/src/workspace/points-cli.ts +42 -16
  410. package/src/workspace/points-write.schema.json +162 -4
  411. package/src/workspace/points.schema.json +157 -2
  412. package/src/workspace/points.test.ts +13 -1
  413. package/src/workspace/points.ts +118 -12
  414. package/src/workspace/pr-report.schema.json +95 -0
  415. package/src/workspace/profiles.test.ts +159 -0
  416. package/src/workspace/read-contract.test.ts +2 -2
  417. package/src/workspace/reason-codes.test.ts +13 -1
  418. package/src/workspace/reason-codes.ts +38 -4
  419. package/src/workspace/record-sessions.ts +9 -8
  420. package/src/workspace/records-amend.schema.json +78 -0
  421. package/src/workspace/records-cli.ts +20 -6
  422. package/src/workspace/records-close.schema.json +78 -0
  423. package/src/workspace/records-close.ts +31 -7
  424. package/src/workspace/records-concurrent.e2e.test.ts +135 -0
  425. package/src/workspace/records-concurrent.test.ts +145 -0
  426. package/src/workspace/records-formats.test.ts +46 -10
  427. package/src/workspace/records-new.schema.json +7 -0
  428. package/src/workspace/records-review.schema.json +78 -0
  429. package/src/workspace/records-sessions-write.test.ts +69 -0
  430. package/src/workspace/records-sessions.test.ts +22 -0
  431. package/src/workspace/records-write.ts +192 -25
  432. package/src/workspace/records.schema.json +16 -0
  433. package/src/workspace/records.ts +50 -9
  434. package/src/workspace/reference-kinds/app/package.json +10 -0
  435. package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  436. package/src/workspace/run-statement.schema.json +622 -0
  437. package/src/workspace/runs-cli.ts +343 -14
  438. package/src/workspace/runs-statement.test.ts +239 -0
  439. package/src/workspace/runs-write.schema.json +416 -5
  440. package/src/workspace/runs.schema.json +451 -4
  441. package/src/workspace/runs.test.ts +39 -0
  442. package/src/workspace/runs.ts +296 -35
  443. package/src/workspace/session-kinds.ts +3 -3
  444. package/src/workspace/squash.ts +198 -0
  445. package/src/workspace/status-contract.test.ts +5 -2
  446. package/src/workspace/status-gates.ts +3 -1
  447. package/src/workspace/status.schema.json +126 -3
  448. package/src/workspace/status.ts +90 -1
  449. package/src/workspace/symbols.ts +20 -9
  450. package/src/workspace/trust/run-commits.ts +115 -0
  451. package/src/workspace/trust/run-statement.ts +226 -0
  452. package/src/workspace/trust/seal.ts +8 -2
  453. package/src/workspace/trust/verify-cli.ts +6 -5
  454. package/src/workspace/trust/verify.ts +39 -6
  455. package/src/workspace/wip-cli.ts +97 -0
  456. package/src/workspace/wip-write.schema.json +504 -0
  457. package/src/workspace/wip.schema.json +334 -0
  458. package/src/workspace/wip.test.ts +294 -0
  459. package/src/workspace/wip.ts +753 -0
  460. package/src/workspace/work-cli.ts +3 -0
  461. package/src/workspace/work-evidence.schema.json +3 -0
  462. package/src/workspace/work-size.test.ts +77 -0
  463. package/src/workspace/work-size.ts +93 -0
  464. package/src/workspace/workspace-kinds.schema.json +122 -0
  465. package/src/workspace/write-lock-cli.ts +131 -0
  466. package/src/workspace/write-lock.schema.json +172 -0
  467. package/src/workspace/write-lock.test.ts +189 -0
  468. package/src/workspace/write-lock.ts +474 -0
  469. package/src/workspace/write-scope.test.ts +16 -1
  470. package/src/workspace/write-scope.ts +33 -11
  471. package/src/yaml.ts +31 -21
@@ -19,12 +19,27 @@
19
19
  * as well, after checking both files, so the two lists never differ.
20
20
  * `records close` is in `records-close.ts`.
21
21
  *
22
+ * Each write holds the working tree's write lock (`write-lock.ts`, #3173,
23
+ * ws-089) from its first read to its last write, so writes to one working
24
+ * tree never interleave. `amend`, `review` and `close` take `--expect
25
+ * <digest>`: the record's digest the caller last read. When the record has
26
+ * moved on since, the write is refused with record-conflict, naming the
27
+ * digest it has now and the last chant write of it, and the caller re-reads
28
+ * and tries again.
29
+ *
22
30
  * `new --sign` and `amend --sign` seal the record's author (#2688), and
23
31
  * `review --sign` seals a verdict (#2687): see `trust/seal.ts`. A record of
24
32
  * a kind that declares `seal` gets its whole-file seal when `new` or `amend`
25
33
  * writes it into a closed state (#2546, ws-063).
34
+ *
35
+ * `new` also writes a record of a content-addressed JSON kind (`format:
36
+ * "json"`, `idFrom: "sha256"`, ws-053), such as the reference workspace's
37
+ * evidence (ws-082): the fields as JSON, in a file named for the SHA-256 of
38
+ * its bytes. Such a record never changes, so amend, review and close refuse
39
+ * the kind.
26
40
  */
27
41
 
42
+ import { createHash } from "node:crypto";
28
43
  import { readFileSync, statSync, writeFileSync } from "node:fs";
29
44
  import { dirname, join, posix, relative, resolve } from "node:path";
30
45
  import type { CommandContext } from "../cli/registry";
@@ -35,6 +50,7 @@ import {
35
50
  loadRecordKind,
36
51
  parseFrontMatter,
37
52
  readRecords,
53
+ readSessionSubjects,
38
54
  RECORD_REASON_CODES,
39
55
  RecordReadError,
40
56
  RECORD_SEAL_FIELD,
@@ -53,6 +69,7 @@ import { workingTree } from "./tree";
53
69
  import { AGENT_ENV, refuseRecordWrite, WRITE_SCOPE_CODES, WriteScopeError, type ScopeSource } from "./write-scope";
54
70
  import { IDENTITY_CODES, IdentityError, refuseUnidentified } from "./identity";
55
71
  import type { WriteVerb } from "./declaration";
72
+ import { lastWriteReader, noteWrites, withWriteLock, writeFileAtomic, WriteLockError, WRITE_LOCK_CODES, type LastWrite, type WriteLockWho } from "./write-lock";
56
73
 
57
74
  // ── Contract ─────────────────────────────────────────────────────────────────
58
75
 
@@ -78,6 +95,7 @@ export const NEW_ERROR_CODES = [
78
95
  "source-harvest-not-proposed",
79
96
  "record-state-not-initial",
80
97
  "ratify-quorum-not-met",
98
+ ...WRITE_LOCK_CODES,
81
99
  ...WRITE_SCOPE_CODES,
82
100
  ...IDENTITY_CODES,
83
101
  ...RECORD_REASON_CODES,
@@ -94,6 +112,8 @@ export const AMEND_ERROR_CODES = [
94
112
  "amend-supersede-instead",
95
113
  "record-sign-failed",
96
114
  "ratify-quorum-not-met",
115
+ "record-conflict",
116
+ ...WRITE_LOCK_CODES,
97
117
  ...WRITE_SCOPE_CODES,
98
118
  ...IDENTITY_CODES,
99
119
  ...RECORD_REASON_CODES,
@@ -110,6 +130,8 @@ export const REVIEW_ERROR_CODES = [
110
130
  "review-sign-failed",
111
131
  "session-unknown",
112
132
  "session-not-open",
133
+ "record-conflict",
134
+ ...WRITE_LOCK_CODES,
113
135
  ...WRITE_SCOPE_CODES,
114
136
  ...IDENTITY_CODES,
115
137
  ...RECORD_REASON_CODES,
@@ -127,12 +149,28 @@ export class RecordWriteError extends Error {
127
149
  constructor(
128
150
  readonly code: WriteErrorCode,
129
151
  message: string,
152
+ /** With record-conflict: the record as it is now. */
153
+ readonly conflict?: RecordConflict,
130
154
  ) {
131
155
  super(message);
132
156
  this.name = "RecordWriteError";
133
157
  }
134
158
  }
135
159
 
160
+ /**
161
+ * What a record-conflict refusal says about the record (#3173): the digest
162
+ * the caller expected, the one it has now, and the last chant write of it
163
+ * when the journal knows it. The caller re-reads the record and writes again
164
+ * with `--expect` set to `digest`.
165
+ */
166
+ export interface RecordConflict {
167
+ id: string;
168
+ path: string;
169
+ expected: string;
170
+ digest: string;
171
+ lastWrite: LastWrite | null;
172
+ }
173
+
136
174
  export interface KindView {
137
175
  name: string;
138
176
  schema: string;
@@ -151,6 +189,12 @@ export interface WriteResult {
151
189
  dryRun: boolean;
152
190
  /** The written record's warnings, as `records` would report them. */
153
191
  warnings: RecordWarning[];
192
+ /**
193
+ * The record's digest after the write, as `records --json` reports it
194
+ * (#3173): what the caller passes as `--expect` to its next write of the
195
+ * record.
196
+ */
197
+ digest: string;
154
198
  /** With --dry-run, the whole text the command would write. */
155
199
  text?: string;
156
200
  }
@@ -159,6 +203,8 @@ export interface WriteFailure<C> {
159
203
  $schema: string;
160
204
  contract: number;
161
205
  error: { code: C; message: string };
206
+ /** With record-conflict (#3173): the record as it is now. */
207
+ conflict?: RecordConflict;
162
208
  }
163
209
 
164
210
  /** A record's author seal as `new` and `amend` write it with `--sign` (#2688). */
@@ -339,15 +385,26 @@ export interface Opened {
339
385
  dirRel: string;
340
386
  }
341
387
 
342
- export async function open(kind: string, cwd: string): Promise<Opened> {
388
+ /** A kind `records new` writes as one JSON file named for the SHA-256 of its bytes (ws-053, ws-082). */
389
+ export function isContentAddressed(kind: LoadedRecordKind["kind"]): boolean {
390
+ return kind.format === "json" && kind.idFrom === "sha256";
391
+ }
392
+
393
+ export async function open(kind: string, cwd: string, opts: { contentAddressed?: boolean } = {}): Promise<Opened> {
343
394
  const real = realpathOr(cwd);
344
395
  const root = gitRoot(real) ?? real;
345
396
  const loaded = await loadRecordKind(kind, real);
346
- // The writers write Markdown front matter with an id field. A JSON or
347
- // content-addressed kind (ws-053) is read by records, and written by its own tool.
348
- if (loaded.kind.format !== "markdown-front-matter" || loaded.kind.idField === undefined) {
397
+ // The writers write Markdown front matter with an id field. `records new`
398
+ // also writes a content-addressed JSON kind (ws-082), whose record never
399
+ // changes once written, so amend, review and close never open one. A JSON
400
+ // kind with an id field is read by records, and written by its own tool.
401
+ const writable = (loaded.kind.format === "markdown-front-matter" && loaded.kind.idField !== undefined) || (!!opts.contentAddressed && isContentAddressed(loaded.kind));
402
+ if (!writable) {
349
403
  const what = loaded.kind.format !== "markdown-front-matter" ? `format ${loaded.kind.format}` : `ids from ${loaded.kind.idFrom}`;
350
- throw new RecordWriteError("write-usage-invalid", `the ${loaded.kind.name} kind has ${what}, and records new, amend and review write only Markdown front matter records with an idField`);
404
+ const verbs = isContentAddressed(loaded.kind)
405
+ ? "a content-addressed record never changes once records new writes it, so amend, review and close never write one"
406
+ : "records new, amend and review write only Markdown front matter records with an idField, and records new a JSON record named for the sha256 of its bytes";
407
+ throw new RecordWriteError("write-usage-invalid", `the ${loaded.kind.name} kind has ${what}: ${verbs}`);
351
408
  }
352
409
  const dirRel = relative(root, loaded.dir).split("\\").join("/") || ".";
353
410
  return {
@@ -392,11 +449,7 @@ function refuseUnidentifiedAuthors(source: ScopeSource, kind: Opened["loaded"]["
392
449
  */
393
450
  export async function readAll(o: Opened, source: RecordSource): Promise<RecordEntry[]> {
394
451
  const assets = workingTree(o.workspaceRoot === "." ? o.root : join(o.root, ...o.workspaceRoot.split("/")));
395
- let subjects: { records: RecordEntry[]; reviews: string } | undefined;
396
- if (o.loaded.kind.session) {
397
- const subjectKind = await loadRecordKind(resolve(dirname(o.loaded.file), o.loaded.kind.session.subjects.kind), o.root);
398
- subjects = { records: (await readRecords(subjectKind, { root: o.root, source })).records, reviews: subjectKind.kind.reviews?.field ?? "reviews" };
399
- }
452
+ const subjects = await readSessionSubjects(o.loaded, o.root, source);
400
453
  return (await readRecords(o.loaded, { root: o.root, source, assets, workspaceRoot: o.workspaceRoot, ...(subjects ? { subjects } : {}) })).records;
401
454
  }
402
455
 
@@ -470,6 +523,35 @@ async function refuseRatifyBelowQuorum(o: Opened, written: RecordEntry, id: stri
470
523
  );
471
524
  }
472
525
 
526
+ /**
527
+ * Refuse a write to `target` when the caller expected another digest
528
+ * (#3173): someone wrote the record since the caller read it. Called holding
529
+ * the write lock, after the records are read, so the digest compared is the
530
+ * one the write would build on.
531
+ */
532
+ export function refuseStale(o: Opened, target: RecordEntry, id: string, expect: string | undefined): void {
533
+ if (expect === undefined || expect === target.digest) return;
534
+ const lastWrite = lastWriteReader(o.root)(target.path, o.source.read(target.path));
535
+ const who = lastWrite ? ` It was last written by chant workspace ${lastWrite.verb}${lastWrite.by ? ` for ${lastWrite.by}` : ""}${lastWrite.agent ? ` in agent session ${lastWrite.agent}` : ""} at ${lastWrite.at}.` : "";
536
+ throw new RecordWriteError(
537
+ "record-conflict",
538
+ `${id} has changed since the digest given with --expect (${expect}): its digest is now ${target.digest}.${who} Read it again with chant workspace records --json, and write again with --expect ${target.digest} if the change still stands`,
539
+ { id, path: target.path, expected: expect, digest: target.digest, lastWrite },
540
+ );
541
+ }
542
+
543
+ /** Refuse a --expect that is not a digest, before anything is read. */
544
+ function refuseExpectShape(expect: string | undefined): void {
545
+ if (expect !== undefined && !/^[0-9a-f]{64}$/.test(expect)) {
546
+ throw new RecordWriteError("write-usage-invalid", `--expect takes a record's digest, 64 lowercase hex digits as records --json prints it, not ${JSON.stringify(expect)}`);
547
+ }
548
+ }
549
+
550
+ /** Who a write names, for the lock's owner file and the journal. */
551
+ export function writeWho(verb: string, opts: { by?: string; agent?: string }): WriteLockWho {
552
+ return { verb, by: opts.by ?? null, agent: opts.agent ?? null };
553
+ }
554
+
473
555
  export function findRecord(entries: RecordEntry[], id: string, kind: string): RecordEntry & { data: Record<string, unknown> } {
474
556
  const hits = entries.filter((e) => e.id === id);
475
557
  if (hits.length === 0) throw new RecordWriteError("record-not-found", `no ${kind} record has id ${id}`);
@@ -648,8 +730,9 @@ function today(): string {
648
730
  }
649
731
 
650
732
  export function failure<C>(schema: string, err: unknown): WriteFailure<C> {
651
- if (err instanceof RecordWriteError || err instanceof RecordReadError || err instanceof WriteScopeError || err instanceof IdentityError) {
652
- return { $schema: schema, contract: RECORDS_WRITE_CONTRACT_VERSION, error: { code: err.code as C, message: err.message } };
733
+ if (err instanceof RecordWriteError || err instanceof RecordReadError || err instanceof WriteScopeError || err instanceof IdentityError || err instanceof WriteLockError) {
734
+ const conflict = err instanceof RecordWriteError ? err.conflict : undefined;
735
+ return { $schema: schema, contract: RECORDS_WRITE_CONTRACT_VERSION, error: { code: err.code as C, message: err.message }, ...(conflict ? { conflict } : {}) };
653
736
  }
654
737
  throw err;
655
738
  }
@@ -786,19 +869,28 @@ function applyChannel(o: Opened, fields: Record<string, unknown>, opts: ChannelO
786
869
  return out;
787
870
  }
788
871
 
789
- /** `records new`: write one new record from validated fields, sealed by its author with `sign`. */
872
+ /** `records new`: write one new record from validated fields, sealed by its author with `sign`. Holds the write lock (#3173). */
790
873
  export async function newRecord(opts: NewRecordOptions & ChannelOptions): Promise<NewDocument> {
874
+ try {
875
+ return await withWriteLock(opts.cwd, writeWho("records new", opts), opts.dryRun, () => newRecordLocked(opts));
876
+ } catch (err) {
877
+ return failure<NewErrorCode>(RECORDS_NEW_SCHEMA_ID, err);
878
+ }
879
+ }
880
+
881
+ async function newRecordLocked(opts: NewRecordOptions & ChannelOptions): Promise<NewDocument> {
791
882
  try {
792
883
  if (opts.prefix !== undefined && !/^[A-Za-z][A-Za-z0-9]*$/.test(opts.prefix)) {
793
884
  throw new RecordWriteError("write-usage-invalid", `--prefix takes letters and digits, starting with a letter, not ${JSON.stringify(opts.prefix)}`);
794
885
  }
795
- const o = await open(opts.kind, opts.cwd);
886
+ const o = await open(opts.kind, opts.cwd, { contentAddressed: true });
796
887
  const scope = refuseOutOfScope(o, "new", opts.cwd, opts);
797
888
  const fields = applyChannel(o, parseFields(opts.fields, "--from"), opts, true, "--from");
798
889
  const { kind, schema } = o.loaded;
799
890
  refuseUnidentifiedAuthors(scope, kind, fields, "--from", opts.agent);
800
891
  refuseSealField(o, fields, "--from");
801
892
  refuseRevisionFields(kind, fields, {}, "--from");
893
+ if (isContentAddressed(kind)) return await newContentAddressed(o, fields, opts);
802
894
  const idField = kind.idField!;
803
895
  const before = await readAll(o, o.source);
804
896
  const given = fields[idField];
@@ -830,7 +922,10 @@ export async function newRecord(opts: NewRecordOptions & ChannelOptions): Promis
830
922
  const written = await validatedEntry(o, before, path, text);
831
923
  await refuseRatifyBelowQuorum(o, written, id);
832
924
  const warnings = written.warnings;
833
- if (!opts.dryRun) writeFileSync(abs(o, path), text, { flag: "wx" });
925
+ if (!opts.dryRun) {
926
+ writeFileSync(abs(o, path), text, { flag: "wx" });
927
+ noteWrites(o.root, [{ path, text }], writeWho("records new", opts));
928
+ }
834
929
  return {
835
930
  $schema: RECORDS_NEW_SCHEMA_ID,
836
931
  contract: RECORDS_WRITE_CONTRACT_VERSION,
@@ -840,6 +935,7 @@ export async function newRecord(opts: NewRecordOptions & ChannelOptions): Promis
840
935
  ...(seal ? { seal } : {}),
841
936
  dryRun: !!opts.dryRun,
842
937
  warnings,
938
+ digest: written.digest,
843
939
  ...(opts.dryRun ? { text } : {}),
844
940
  };
845
941
  } catch (err) {
@@ -847,6 +943,43 @@ export async function newRecord(opts: NewRecordOptions & ChannelOptions): Promis
847
943
  }
848
944
  }
849
945
 
946
+ /**
947
+ * `records new` on a content-addressed JSON kind (ws-053, ws-082), such as
948
+ * the reference workspace's evidence: the fields, in the schema's order, as
949
+ * JSON with two-space indents and a final newline, in a file named for the
950
+ * SHA-256 of those bytes. The id is that hash, so the fields give no id, and
951
+ * the record never changes afterwards.
952
+ */
953
+ async function newContentAddressed(o: Opened, fields: Record<string, unknown>, opts: NewRecordOptions): Promise<NewDocument> {
954
+ const { kind, schema } = o.loaded;
955
+ if (opts.prefix !== undefined) throw new RecordWriteError("write-usage-invalid", `the ${kind.name} kind's ids are the sha256 of each record's bytes, so --prefix allocates nothing`);
956
+ if (opts.sign !== undefined) throw new RecordWriteError("write-usage-invalid", `the ${kind.name} kind names no author to seal: its records are named for the sha256 of their bytes`);
957
+ const data = pick(fields, schemaOrder(fields, schema));
958
+ const text = `${JSON.stringify(data, null, 2)}\n`;
959
+ const id = createHash("sha256").update(text, "utf-8").digest("hex");
960
+ const name = `${id}.json`;
961
+ if (!new RegExp(kind.location.match).test(name)) throw new RecordWriteError("record-path-unmatched", `the kind's location.match ${kind.location.match} does not match ${name}`);
962
+ const path = o.dirRel === "." ? name : `${o.dirRel}/${name}`;
963
+ if (o.source.list(o.dirRel)?.includes(name)) throw new RecordWriteError("record-id-taken", `${path} already exists, with the same bytes: the record is already written`);
964
+ const before = await readAll(o, o.source);
965
+ const written = await validatedEntry(o, before, path, text);
966
+ if (!opts.dryRun) {
967
+ writeFileSync(abs(o, path), text, { flag: "wx" });
968
+ noteWrites(o.root, [{ path, text }], writeWho("records new", opts));
969
+ }
970
+ return {
971
+ $schema: RECORDS_NEW_SCHEMA_ID,
972
+ contract: RECORDS_WRITE_CONTRACT_VERSION,
973
+ kind: o.view,
974
+ path,
975
+ id,
976
+ dryRun: !!opts.dryRun,
977
+ warnings: written.warnings,
978
+ digest: written.digest,
979
+ ...(opts.dryRun ? { text } : {}),
980
+ };
981
+ }
982
+
850
983
  // ── records amend ────────────────────────────────────────────────────────────
851
984
 
852
985
  export interface AmendRecordOptions {
@@ -860,6 +993,8 @@ export interface AmendRecordOptions {
860
993
  sign?: string | true;
861
994
  /** The agent session the write is made in (#2548). */
862
995
  agent?: string;
996
+ /** The record's digest the caller last read (#3173): the write is refused with record-conflict when the record has another. */
997
+ expect?: string;
863
998
  }
864
999
 
865
1000
  /**
@@ -877,6 +1012,15 @@ export interface AmendRecordOptions {
877
1012
  * `--sign` with nothing to change seals the record as it is.
878
1013
  */
879
1014
  export async function amendRecord(opts: AmendRecordOptions & ChannelOptions): Promise<AmendDocument> {
1015
+ try {
1016
+ refuseExpectShape(opts.expect);
1017
+ return await withWriteLock(opts.cwd, writeWho("records amend", opts), opts.dryRun, () => amendRecordLocked(opts));
1018
+ } catch (err) {
1019
+ return failure<AmendErrorCode>(RECORDS_AMEND_SCHEMA_ID, err);
1020
+ }
1021
+ }
1022
+
1023
+ async function amendRecordLocked(opts: AmendRecordOptions & ChannelOptions): Promise<AmendDocument> {
880
1024
  try {
881
1025
  const o = await open(opts.kind, opts.cwd);
882
1026
  const scope = refuseOutOfScope(o, "amend", opts.cwd, opts);
@@ -886,6 +1030,7 @@ export async function amendRecord(opts: AmendRecordOptions & ChannelOptions): Pr
886
1030
  refuseSealField(o, given, "--set");
887
1031
  const before = await readAll(o, o.source);
888
1032
  const target = findRecord(before, opts.id, kind.name);
1033
+ refuseStale(o, target, opts.id, opts.expect);
889
1034
  const old = target.data;
890
1035
  refuseRevisionFields(kind, given, old, "--set");
891
1036
  const isClosed = target.state !== null && (kind.closedStates ?? []).includes(target.state);
@@ -951,12 +1096,17 @@ export async function amendRecord(opts: AmendRecordOptions & ChannelOptions): Pr
951
1096
  if (stableJson(old[kind.seal.field]) !== stableJson(closedSeal.data[kind.seal.field])) changed.push(kind.seal.field);
952
1097
  }
953
1098
  let warnings = target.warnings;
1099
+ let digest = target.digest;
954
1100
  if (changed.length > 0) {
955
1101
  const written = await validatedEntry(o, before, target.path, text);
956
1102
  if (kind.stateField !== undefined && changed.includes(kind.stateField)) await refuseRatifyBelowQuorum(o, written, opts.id);
957
1103
  warnings = written.warnings;
1104
+ digest = written.digest;
1105
+ }
1106
+ if (!opts.dryRun && changed.length > 0) {
1107
+ writeFileAtomic(abs(o, target.path), text);
1108
+ noteWrites(o.root, [{ path: target.path, text }], writeWho("records amend", opts));
958
1109
  }
959
- if (!opts.dryRun && changed.length > 0) writeFileSync(abs(o, target.path), text);
960
1110
  return {
961
1111
  $schema: RECORDS_AMEND_SCHEMA_ID,
962
1112
  contract: RECORDS_WRITE_CONTRACT_VERSION,
@@ -968,6 +1118,7 @@ export async function amendRecord(opts: AmendRecordOptions & ChannelOptions): Pr
968
1118
  ...(sealDropped ? { sealDropped } : {}),
969
1119
  dryRun: !!opts.dryRun,
970
1120
  warnings,
1121
+ digest,
971
1122
  ...(opts.dryRun ? { text } : {}),
972
1123
  };
973
1124
  } catch (err) {
@@ -997,6 +1148,8 @@ export interface ReviewRecordOptions {
997
1148
  sign?: string | true;
998
1149
  /** The agent session the write is made in (#2548). */
999
1150
  agent?: string;
1151
+ /** The record's digest the reviewer judged (#3173): the verdict is refused with record-conflict when the record has moved on. */
1152
+ expect?: string;
1000
1153
  }
1001
1154
 
1002
1155
  /**
@@ -1006,6 +1159,15 @@ export interface ReviewRecordOptions {
1006
1159
  * id, the digest, the verdict, the reviewer and the date (`trust/seal.ts`).
1007
1160
  */
1008
1161
  export async function reviewRecord(opts: ReviewRecordOptions): Promise<ReviewDocument> {
1162
+ try {
1163
+ refuseExpectShape(opts.expect);
1164
+ return await withWriteLock(opts.cwd, writeWho("records review", opts), opts.dryRun, () => reviewRecordLocked(opts));
1165
+ } catch (err) {
1166
+ return failure<ReviewErrorCode>(RECORDS_REVIEW_SCHEMA_ID, err);
1167
+ }
1168
+ }
1169
+
1170
+ async function reviewRecordLocked(opts: ReviewRecordOptions): Promise<ReviewDocument> {
1009
1171
  try {
1010
1172
  if (!(VERDICTS as readonly string[]).includes(opts.verdict)) {
1011
1173
  throw new RecordWriteError("write-usage-invalid", `--verdict takes ${VERDICTS.join(", ")}, not ${JSON.stringify(opts.verdict)}`);
@@ -1021,6 +1183,7 @@ export async function reviewRecord(opts: ReviewRecordOptions): Promise<ReviewDoc
1021
1183
  const field = kind.reviews.field;
1022
1184
  const before = await readAll(o, o.source);
1023
1185
  const target = findRecord(before, opts.id, kind.name);
1186
+ refuseStale(o, target, opts.id, opts.expect);
1024
1187
  if (target.state !== null && (kind.closedStates ?? []).includes(target.state)) {
1025
1188
  throw new RecordWriteError("record-closed", `${opts.id} is ${target.state}, a closed state, so it takes no more reviews`);
1026
1189
  }
@@ -1047,7 +1210,8 @@ export async function reviewRecord(opts: ReviewRecordOptions): Promise<ReviewDoc
1047
1210
  const list = [...reviews, review];
1048
1211
  const text = replaceFields(current, { [field]: list }, { ...target.data, [field]: list });
1049
1212
  if (text === undefined) throw new RecordWriteError("record-unparseable", `${target.path}: the ${field} block can't be rewritten in place without changing the rest of the file`);
1050
- const warnings = await validateWrite(o, before, target.path, text);
1213
+ const writtenEntry = await validatedEntry(o, before, target.path, text);
1214
+ const warnings = writtenEntry.warnings;
1051
1215
  // The same verdict on the session's own list (#2693), validated with the review in place, so the two lists never differ.
1052
1216
  let session: { path: string; text: string; out: ReviewSession } | undefined;
1053
1217
  if (inSession) {
@@ -1063,8 +1227,9 @@ export async function reviewRecord(opts: ReviewRecordOptions): Promise<ReviewDoc
1063
1227
  session = { path: st.path, text: sText, out: { id: opts.session!, path: st.path, verdict, ...(opts.dryRun ? { text: sText } : {}) } };
1064
1228
  }
1065
1229
  if (!opts.dryRun) {
1066
- writeFileSync(abs(o, target.path), text);
1067
- if (session) writeFileSync(abs(o, session.path), session.text);
1230
+ writeFileAtomic(abs(o, target.path), text);
1231
+ if (session) writeFileAtomic(abs(o, session.path), session.text);
1232
+ noteWrites(o.root, [{ path: target.path, text }, ...(session ? [{ path: session.path, text: session.text }] : [])], writeWho("records review", opts));
1068
1233
  }
1069
1234
  return {
1070
1235
  $schema: RECORDS_REVIEW_SCHEMA_ID,
@@ -1076,6 +1241,7 @@ export async function reviewRecord(opts: ReviewRecordOptions): Promise<ReviewDoc
1076
1241
  ...(session ? { session: session.out } : {}),
1077
1242
  dryRun: !!opts.dryRun,
1078
1243
  warnings,
1244
+ digest: writtenEntry.digest,
1079
1245
  ...(opts.dryRun ? { text } : {}),
1080
1246
  };
1081
1247
  } catch (err) {
@@ -1130,9 +1296,9 @@ async function seal(sign: string | true, cwd: string, v: { record: string; diges
1130
1296
 
1131
1297
  export const WRITE_USAGE = [
1132
1298
  "chant workspace records new [<kind file or declared kind>] --from <file|-> [--prefix <prefix>] [--by <name>] [--sign [<key file>]] [--dry-run]",
1133
- "chant workspace records amend <id> [--kind <kind file>] --set <file|-> [--sign [<key file>]] [--dry-run]",
1134
- "chant workspace records review <id> [--kind <kind file>] --verdict agree|dissent|abstain --by <principal> [--note <text>] [--session <id>] [--sign [<key file>]] [--dry-run]",
1135
- "chant workspace records close <session id> [--kind <session kind file>] [--dry-run]",
1299
+ "chant workspace records amend <id> [--kind <kind file>] --set <file|-> [--expect <digest>] [--sign [<key file>]] [--dry-run]",
1300
+ "chant workspace records review <id> [--kind <kind file>] --verdict agree|dissent|abstain --by <principal> [--note <text>] [--session <id>] [--expect <digest>] [--sign [<key file>]] [--dry-run]",
1301
+ "chant workspace records close <session id> [--kind <session kind file>] [--expect <digest>] [--dry-run]",
1136
1302
  ].join("\n");
1137
1303
 
1138
1304
  function usage(schema: string, message: string): WriteFailure<"write-usage-invalid"> {
@@ -1226,12 +1392,13 @@ export async function runRecordsWrite(ctx: CommandContext): Promise<number> {
1226
1392
  if (!id) return print(usage(RECORDS_CLOSE_SCHEMA_ID, "close needs the session's id"));
1227
1393
  const kind = args.kind !== undefined ? resolveWriteKind(args.kind, cwd) : await declaredSessionKind(RECORDS_CLOSE_SCHEMA_ID, cwd);
1228
1394
  if (typeof kind !== "string") return print(kind);
1229
- return print(await closeRecord({ kind, id, dryRun: args.dryRun, cwd, agent }));
1395
+ return print(await closeRecord({ kind, id, dryRun: args.dryRun, cwd, agent, expect: args.expect }));
1230
1396
  }
1231
1397
  if (verb === "new") {
1232
1398
  const named = args.extraPositional2 ?? args.kind;
1233
1399
  const kind = named !== undefined ? resolveWriteKind(named, cwd) : declaredWriteKind(RECORDS_NEW_SCHEMA_ID, cwd, "new needs the kind file");
1234
1400
  if (typeof kind !== "string") return print(kind);
1401
+ if (args.expect !== undefined) return print(usage(RECORDS_NEW_SCHEMA_ID, "--expect is not taken by new: a new record has no digest yet, and its id is allocated under the write lock"));
1235
1402
  const input = readInput(RECORDS_NEW_SCHEMA_ID, "--from", args.migrateFrom, cwd);
1236
1403
  if (typeof input !== "string") return print(input);
1237
1404
  return print(await newRecord({ kind, fields: input, prefix: args.prefix, by: args.by, sign: args.sign, dryRun: args.dryRun, cwd, agent }));
@@ -1245,11 +1412,11 @@ export async function runRecordsWrite(ctx: CommandContext): Promise<number> {
1245
1412
  if (args.by !== undefined) return print(usage(schema, "--by is not taken by amend: set decided_by, or a kind's proposedBy field, with --set"));
1246
1413
  const input = readInput(schema, "--set", args.set, cwd);
1247
1414
  if (typeof input !== "string") return print(input);
1248
- return print(await amendRecord({ kind, id, fields: input, sign: args.sign, dryRun: args.dryRun, cwd, agent }));
1415
+ return print(await amendRecord({ kind, id, fields: input, sign: args.sign, dryRun: args.dryRun, cwd, agent, expect: args.expect }));
1249
1416
  }
1250
1417
  if (args.verdict === undefined) return print(usage(schema, "--verdict agree|dissent|abstain is required"));
1251
1418
  if (args.by === undefined) return print(usage(schema, "--by <principal> is required"));
1252
1419
  return print(
1253
- await reviewRecord({ kind, id, verdict: args.verdict, by: args.by, note: args.note, session: args.session, sign: args.sign, dryRun: args.dryRun, cwd, agent }),
1420
+ await reviewRecord({ kind, id, verdict: args.verdict, by: args.by, note: args.note, session: args.session, sign: args.sign, dryRun: args.dryRun, cwd, agent, expect: args.expect }),
1254
1421
  );
1255
1422
  }
@@ -302,6 +302,22 @@
302
302
  "enum": ["committed", "modified", "new"],
303
303
  "description": "Added in contract 1 by #3160, on every record of a read of the working tree in a git repository; absent under --at and outside git. committed when the file is as HEAD holds it, modified when it differs from HEAD, new when HEAD doesn't hold it, staged or not. modified and new are the records a person has kept and not yet committed or applied."
304
304
  },
305
+ "lastWrite": {
306
+ "description": "Added in contract 1 by #3173, beside worktree. The last chant write of the record's file in this working tree (records new, amend, review or close, points ask or answer), from the write journal in the git directory, while the file still holds the text that write left; null when the journal doesn't know it, such as after a commit was checked out or the file was edited by hand. A UI shows it with digest, so a person sees who changed a record they are looking at. It is a cache, never a fact: the journal can be lost.",
307
+ "oneOf": [
308
+ { "type": "null" },
309
+ {
310
+ "type": "object",
311
+ "required": ["verb", "by", "agent", "at"],
312
+ "properties": {
313
+ "verb": { "type": "string", "description": "The write command, such as records amend or points answer." },
314
+ "by": { "type": ["string", "null"], "description": "The principal the write named (--by, or the people who answered), or null when it named none." },
315
+ "agent": { "type": ["string", "null"], "description": "The agent session the write was made in (CHANT_AGENT), or null." },
316
+ "at": { "type": "string", "description": "When, as an ISO 8601 time." }
317
+ }
318
+ }
319
+ ]
320
+ },
305
321
  "contract": {
306
322
  "description": "Added in contract 1 by #3147, on every parsed record of a work kind whose work block names a contract kind. The contract record the item builds, by the id in the work block's contract field, with that record's state now; state is null when no record of the contract kind has the id. Null when the item names no contract.",
307
323
  "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/workLink" }]
@@ -381,9 +381,11 @@ export const recordKindSchema = z
381
381
  * A review-session kind (#2673, #2650 C10): the front-matter list of the
382
382
  * verdicts a session produced, the field that seals a closed session, and
383
383
  * the records its verdicts name, as the kind file that locates them
384
- * (relative to this kind file's directory). The entries of that kind's
385
- * reviews list (its `reviews.field`, or `reviews`) name a session in
386
- * `session`. Optional.
384
+ * (relative to this kind file's directory), or as `kinds`, several kind
385
+ * files, when a session judges records of more than one kind, such as
386
+ * decisions and contracts (#3148, ws-082). The entries of each such
387
+ * kind's reviews list (its `reviews.field`, or `reviews`) name a session
388
+ * in `session`. Optional.
387
389
  *
388
390
  * `openedRev`, `closedRev` and `closedOn` (#2693) name the fields that
389
391
  * hold the commit a session opened at, the commit it closed at, and when
@@ -395,7 +397,13 @@ export const recordKindSchema = z
395
397
  .object({
396
398
  verdicts: z.string().min(1),
397
399
  seal: z.string().min(1),
398
- subjects: z.object({ kind: z.string().min(1) }).strict(),
400
+ subjects: z.union([
401
+ z.object({ kind: z.string().min(1) }).strict(),
402
+ z
403
+ .object({ kinds: z.array(z.string().min(1)).min(1) })
404
+ .strict()
405
+ .refine((x) => new Set(x.kinds).size === x.kinds.length, { message: "session.subjects.kinds lists each kind file once", path: ["kinds"] }),
406
+ ]),
399
407
  openedRev: z.string().min(1).optional(),
400
408
  closedRev: z.string().min(1).optional(),
401
409
  closedOn: z.string().min(1).optional(),
@@ -448,7 +456,12 @@ export const recordKindSchema = z
448
456
  acceptance: z.object({ field: z.string().min(1), implementer: z.string().min(1) }).strict().optional(),
449
457
  contract: z.object({ field: z.string().min(1), kind: z.string().min(1) }).strict().optional(),
450
458
  tier: z
451
- .object({ field: z.string().min(1), tiers: z.array(z.string().min(1)).min(1) })
459
+ .object({
460
+ field: z.string().min(1),
461
+ tiers: z.array(z.string().min(1)).min(1),
462
+ // What each tier may hold, for the slice-tier point's fits_<tier> inputs (#3150, work-size.ts).
463
+ limits: z.record(z.string().min(1), z.object({ criteria: z.number().int().min(0).optional(), files: z.number().int().min(0).optional(), words: z.number().int().min(0).optional() }).strict()).optional(),
464
+ })
452
465
  .strict()
453
466
  .refine((t) => new Set(t.tiers).size === t.tiers.length, { message: "work.tier.tiers lists each tier once", path: ["tiers"] })
454
467
  .optional(),
@@ -561,6 +574,34 @@ export const ANSWER_STATES = ["escalated", "proposed", "answered"] as const;
561
574
 
562
575
  export type RecordKind = z.infer<typeof recordKindSchema>;
563
576
 
577
+ /** The kind files a session kind's verdicts name records of, relative to the session kind file: `subjects.kind`, or each of `subjects.kinds` (ws-082). */
578
+ export function sessionSubjectKinds(session: NonNullable<RecordKind["session"]>): string[] {
579
+ return "kinds" in session.subjects ? session.subjects.kinds : [session.subjects.kind];
580
+ }
581
+
582
+ /** The records of one of a session kind's subject kinds, and the name of their reviews list. */
583
+ export interface SessionSubjects {
584
+ /** The subject kind file, as the session kind names it. */
585
+ kind: string;
586
+ records: RecordEntry[];
587
+ reviews: string;
588
+ }
589
+
590
+ /**
591
+ * A session kind's subject records, each subject kind read from `source`
592
+ * (#2673, ws-082), for {@link readRecords}' `subjects`. Undefined for a kind
593
+ * that is not a session kind.
594
+ */
595
+ export async function readSessionSubjects(loaded: LoadedRecordKind, root: string, source: RecordSource): Promise<SessionSubjects[] | undefined> {
596
+ if (!loaded.kind.session) return undefined;
597
+ const out: SessionSubjects[] = [];
598
+ for (const file of sessionSubjectKinds(loaded.kind.session)) {
599
+ const subjectKind = await loadRecordKind(resolve(dirname(loaded.file), file), root);
600
+ out.push({ kind: file, records: (await readRecords(subjectKind, { root, source })).records, reviews: subjectKind.kind.reviews?.field ?? "reviews" });
601
+ }
602
+ return out;
603
+ }
604
+
564
605
  /** A kind as loaded: its data, where it came from, and its schema. */
565
606
  export interface LoadedRecordKind {
566
607
  kind: RecordKind;
@@ -1195,11 +1236,11 @@ export interface ReadRecordsOptions {
1195
1236
  */
1196
1237
  history?: RecordHistory;
1197
1238
  /**
1198
- * For a session kind: the records its `session.subjects.kind` locates, read
1199
- * from the same tree (#2673). Without them no verdict is checked and no
1200
- * session is cited.
1239
+ * For a session kind: the records each of its subject kinds locates, read
1240
+ * from the same tree (#2673, ws-082). Without them no verdict is checked
1241
+ * and no session is cited.
1201
1242
  */
1202
- subjects?: { records: RecordEntry[]; reviews: string };
1243
+ subjects?: SessionSubjects[];
1203
1244
  /**
1204
1245
  * The workspace root, from `root` with / separators ("." for `root`
1205
1246
  * itself): where a content-addressed record's own path is reported from
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "chant-reference-app-kind",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "The app member kind (INTENTIUS/chant#3151): a data-only kinds file a workspace pins by path. chant reads it as data and never runs it.",
6
+ "files": ["workspace-kinds.json"],
7
+ "exports": {
8
+ "./workspace-kinds": "./workspace-kinds.json"
9
+ }
10
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "$comment": "The app member kind (#3151, ws-093). A workspace pins a copy of this directory by path, as the reference workspace does at kinds/app, and chant reads it as data. The fields are the conventions an orchestrator relies on to run, migrate, test and check an app: the scripts in its package.json, the environment variables it reads, and the path it answers health on. The canonical copy ships in @intentius/chant at src/workspace/reference-kinds/app.",
3
+ "schema": 1,
4
+ "kinds": [
5
+ {
6
+ "name": "app",
7
+ "description": "an app: a Node package with a start script in the package.json directly in its directory",
8
+ "precedence": 100,
9
+ "probe": {
10
+ "anyJsonKey": {
11
+ "in": ["package.json"],
12
+ "pointers": ["/scripts/start"]
13
+ }
14
+ },
15
+ "outputs": ["source", "url"],
16
+ "fields": {
17
+ "scripts": {
18
+ "type": "object",
19
+ "description": "Which package.json script plays each part, run with npm run from the member's directory",
20
+ "properties": {
21
+ "start": { "type": "string", "description": "Runs a release of the app; it listens on the port env.port names", "default": "start" },
22
+ "dev": { "type": "string", "description": "Runs the app in development", "default": "dev" },
23
+ "test": { "type": "string", "description": "The app's own tests, run before a release", "default": "test" },
24
+ "migrate": { "type": "string", "description": "Applies the app's migrations to the data directory env.data names; a missing script means there are none", "default": "migrate" }
25
+ }
26
+ },
27
+ "env": {
28
+ "type": "object",
29
+ "description": "The environment variables the app reads, by name",
30
+ "properties": {
31
+ "port": { "type": "string", "description": "The port the app listens on", "default": "PORT", "pattern": "^[A-Z_][A-Z0-9_]*$" },
32
+ "data": { "type": "string", "description": "The directory the app keeps its data in", "default": "APP_DATA", "pattern": "^[A-Z_][A-Z0-9_]*$" },
33
+ "revision": { "type": "string", "description": "The revision the running app reports, such as the release's commit", "default": "APP_REVISION", "pattern": "^[A-Z_][A-Z0-9_]*$" }
34
+ }
35
+ },
36
+ "health": {
37
+ "type": "string",
38
+ "description": "The path the running app answers on with a status below 500 once it is up",
39
+ "default": "/health",
40
+ "pattern": "^/"
41
+ }
42
+ }
43
+ }
44
+ ]
45
+ }