@intentius/chant 0.102.0 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (464) hide show
  1. package/dist/audit/core.d.ts +2 -0
  2. package/dist/audit/core.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/change-set.d.ts +232 -0
  5. package/dist/change-set.d.ts.map +1 -0
  6. package/dist/cli/commands/audit.d.ts +7 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/handlers/build.d.ts.map +1 -1
  10. package/dist/cli/handlers/change-set.d.ts +11 -0
  11. package/dist/cli/handlers/change-set.d.ts.map +1 -0
  12. package/dist/cli/handlers/fan-out.d.ts +33 -0
  13. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  14. package/dist/cli/handlers/operator.d.ts +7 -0
  15. package/dist/cli/handlers/operator.d.ts.map +1 -1
  16. package/dist/cli/handlers/pr.d.ts +31 -0
  17. package/dist/cli/handlers/pr.d.ts.map +1 -0
  18. package/dist/cli/handlers/run.d.ts.map +1 -1
  19. package/dist/cli/main.d.ts.map +1 -1
  20. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  21. package/dist/cli/mcp/workspace-tools.d.ts +3 -2
  22. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  23. package/dist/cli/registry.d.ts +63 -1
  24. package/dist/cli/registry.d.ts.map +1 -1
  25. package/dist/components/auto-release.d.ts +16 -0
  26. package/dist/components/auto-release.d.ts.map +1 -1
  27. package/dist/components/capability.d.ts +53 -0
  28. package/dist/components/capability.d.ts.map +1 -1
  29. package/dist/components/changed-files.d.ts +31 -0
  30. package/dist/components/changed-files.d.ts.map +1 -0
  31. package/dist/components/cli-support.d.ts +9 -1
  32. package/dist/components/cli-support.d.ts.map +1 -1
  33. package/dist/components/component.d.ts +24 -0
  34. package/dist/components/component.d.ts.map +1 -1
  35. package/dist/components/deploy-units.d.ts.map +1 -1
  36. package/dist/components/driver.d.ts +13 -0
  37. package/dist/components/driver.d.ts.map +1 -1
  38. package/dist/components/fan-out-output.d.ts +6 -0
  39. package/dist/components/fan-out-output.d.ts.map +1 -1
  40. package/dist/components/fan-out-record.d.ts +41 -0
  41. package/dist/components/fan-out-record.d.ts.map +1 -0
  42. package/dist/components/fan-out-run.d.ts +65 -0
  43. package/dist/components/fan-out-run.d.ts.map +1 -1
  44. package/dist/components/fan-out-support.d.ts +2 -0
  45. package/dist/components/fan-out-support.d.ts.map +1 -1
  46. package/dist/components/fan-out.d.ts +6 -0
  47. package/dist/components/fan-out.d.ts.map +1 -1
  48. package/dist/components/gated-wave-pipeline.d.ts +50 -0
  49. package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
  50. package/dist/components/index.d.ts +1 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/layers.d.ts +25 -0
  53. package/dist/components/layers.d.ts.map +1 -0
  54. package/dist/components/pr-pipeline.d.ts +61 -0
  55. package/dist/components/pr-pipeline.d.ts.map +1 -0
  56. package/dist/components/pr-run.d.ts +119 -0
  57. package/dist/components/pr-run.d.ts.map +1 -0
  58. package/dist/components/promote.d.ts.map +1 -1
  59. package/dist/components/verbs/shell.d.ts.map +1 -1
  60. package/dist/components/wave-plan.d.ts +49 -0
  61. package/dist/components/wave-plan.d.ts.map +1 -0
  62. package/dist/gated-waves.d.ts +147 -0
  63. package/dist/gated-waves.d.ts.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/lexicon.d.ts +48 -0
  67. package/dist/lexicon.d.ts.map +1 -1
  68. package/dist/lifecycle/gate-ledger.d.ts +7 -0
  69. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  70. package/dist/lifecycle/git.d.ts +46 -1
  71. package/dist/lifecycle/git.d.ts.map +1 -1
  72. package/dist/lint/post-synth.d.ts +18 -0
  73. package/dist/lint/post-synth.d.ts.map +1 -1
  74. package/dist/op/activities/activity-contracts.d.ts +126 -0
  75. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  76. package/dist/op/activities/change-set.d.ts +62 -0
  77. package/dist/op/activities/change-set.d.ts.map +1 -0
  78. package/dist/op/activities/decide.d.ts +2 -0
  79. package/dist/op/activities/decide.d.ts.map +1 -1
  80. package/dist/op/activities/factory.d.ts +137 -0
  81. package/dist/op/activities/factory.d.ts.map +1 -0
  82. package/dist/op/activities/index.d.ts +4 -0
  83. package/dist/op/activities/index.d.ts.map +1 -1
  84. package/dist/op/activity-contract.d.ts.map +1 -1
  85. package/dist/op/builders.d.ts +11 -0
  86. package/dist/op/builders.d.ts.map +1 -1
  87. package/dist/op/decide-backend.d.ts +5 -1
  88. package/dist/op/decide-backend.d.ts.map +1 -1
  89. package/dist/op/decide-read-inputs.d.ts +4 -2
  90. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  91. package/dist/op/factory.d.ts +83 -0
  92. package/dist/op/factory.d.ts.map +1 -0
  93. package/dist/op/index.d.ts +4 -1
  94. package/dist/op/index.d.ts.map +1 -1
  95. package/dist/op/op-ir.d.ts +2 -0
  96. package/dist/op/op-ir.d.ts.map +1 -1
  97. package/dist/op/op-verb-class.d.ts.map +1 -1
  98. package/dist/op/types.d.ts +8 -0
  99. package/dist/op/types.d.ts.map +1 -1
  100. package/dist/op/work-lease-run.d.ts.map +1 -1
  101. package/dist/plan-summary.d.ts +207 -0
  102. package/dist/plan-summary.d.ts.map +1 -0
  103. package/dist/pr-forge.d.ts +105 -0
  104. package/dist/pr-forge.d.ts.map +1 -0
  105. package/dist/pr-loop.d.ts +178 -0
  106. package/dist/pr-loop.d.ts.map +1 -0
  107. package/dist/telemetry-attribution.d.ts +31 -0
  108. package/dist/telemetry-attribution.d.ts.map +1 -1
  109. package/dist/terraform/hcl2json.d.ts +45 -0
  110. package/dist/terraform/hcl2json.d.ts.map +1 -0
  111. package/dist/terraform/parse.d.ts +4 -40
  112. package/dist/terraform/parse.d.ts.map +1 -1
  113. package/dist/workspace/box-cli.d.ts +12 -0
  114. package/dist/workspace/box-cli.d.ts.map +1 -0
  115. package/dist/workspace/box-factory.d.ts +27 -3
  116. package/dist/workspace/box-factory.d.ts.map +1 -1
  117. package/dist/workspace/box-listing.d.ts +85 -0
  118. package/dist/workspace/box-listing.d.ts.map +1 -0
  119. package/dist/workspace/box-publish.d.ts +147 -0
  120. package/dist/workspace/box-publish.d.ts.map +1 -0
  121. package/dist/workspace/broker-protocol.d.ts +190 -0
  122. package/dist/workspace/broker-protocol.d.ts.map +1 -0
  123. package/dist/workspace/checks.d.ts.map +1 -1
  124. package/dist/workspace/composites.d.ts +2 -2
  125. package/dist/workspace/composites.d.ts.map +1 -1
  126. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
  127. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
  128. package/dist/workspace/conformance/broker.d.ts +202 -0
  129. package/dist/workspace/conformance/broker.d.ts.map +1 -0
  130. package/dist/workspace/conformance/index.d.ts +2 -1
  131. package/dist/workspace/conformance/index.d.ts.map +1 -1
  132. package/dist/workspace/conformance/vitest.d.ts +9 -2
  133. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  134. package/dist/workspace/conformance/writer.d.ts +108 -14
  135. package/dist/workspace/conformance/writer.d.ts.map +1 -1
  136. package/dist/workspace/decide.d.ts +126 -12
  137. package/dist/workspace/decide.d.ts.map +1 -1
  138. package/dist/workspace/decision-points.schema.json +11 -4
  139. package/dist/workspace/declaration.d.ts +65 -0
  140. package/dist/workspace/declaration.d.ts.map +1 -1
  141. package/dist/workspace/declaration.schema.json +153 -4
  142. package/dist/workspace/environments.d.ts +36 -10
  143. package/dist/workspace/environments.d.ts.map +1 -1
  144. package/dist/workspace/factory-rules.d.ts +180 -0
  145. package/dist/workspace/factory-rules.d.ts.map +1 -0
  146. package/dist/workspace/init.d.ts +21 -0
  147. package/dist/workspace/init.d.ts.map +1 -1
  148. package/dist/workspace/intent-cli.d.ts +1 -1
  149. package/dist/workspace/intent-cli.d.ts.map +1 -1
  150. package/dist/workspace/intent-record.d.ts +28 -1
  151. package/dist/workspace/intent-record.d.ts.map +1 -1
  152. package/dist/workspace/intent.d.ts +102 -5
  153. package/dist/workspace/intent.d.ts.map +1 -1
  154. package/dist/workspace/json-edit.d.ts +24 -0
  155. package/dist/workspace/json-edit.d.ts.map +1 -0
  156. package/dist/workspace/jsonc.d.ts +11 -0
  157. package/dist/workspace/jsonc.d.ts.map +1 -1
  158. package/dist/workspace/kinds.d.ts +48 -0
  159. package/dist/workspace/kinds.d.ts.map +1 -1
  160. package/dist/workspace/lexicon-symbols.d.ts +29 -0
  161. package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
  162. package/dist/workspace/lineage-upgrade.d.ts +31 -2
  163. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  164. package/dist/workspace/point-answer.schema.json +42 -2
  165. package/dist/workspace/points-cli.d.ts +10 -3
  166. package/dist/workspace/points-cli.d.ts.map +1 -1
  167. package/dist/workspace/points.d.ts +66 -5
  168. package/dist/workspace/points.d.ts.map +1 -1
  169. package/dist/workspace/reason-codes.d.ts +33 -4
  170. package/dist/workspace/reason-codes.d.ts.map +1 -1
  171. package/dist/workspace/record-sessions.d.ts +5 -7
  172. package/dist/workspace/record-sessions.d.ts.map +1 -1
  173. package/dist/workspace/records-cli.d.ts +7 -0
  174. package/dist/workspace/records-cli.d.ts.map +1 -1
  175. package/dist/workspace/records-close.d.ts +7 -3
  176. package/dist/workspace/records-close.d.ts.map +1 -1
  177. package/dist/workspace/records-write.d.ts +66 -6
  178. package/dist/workspace/records-write.d.ts.map +1 -1
  179. package/dist/workspace/records.d.ts +28 -9
  180. package/dist/workspace/records.d.ts.map +1 -1
  181. package/dist/workspace/runs-cli.d.ts +138 -6
  182. package/dist/workspace/runs-cli.d.ts.map +1 -1
  183. package/dist/workspace/runs.d.ts +132 -11
  184. package/dist/workspace/runs.d.ts.map +1 -1
  185. package/dist/workspace/session-kinds.d.ts +1 -1
  186. package/dist/workspace/session-kinds.d.ts.map +1 -1
  187. package/dist/workspace/squash.d.ts +80 -0
  188. package/dist/workspace/squash.d.ts.map +1 -0
  189. package/dist/workspace/status-gates.d.ts +2 -0
  190. package/dist/workspace/status-gates.d.ts.map +1 -1
  191. package/dist/workspace/status.d.ts +54 -0
  192. package/dist/workspace/status.d.ts.map +1 -1
  193. package/dist/workspace/symbols.d.ts +16 -5
  194. package/dist/workspace/symbols.d.ts.map +1 -1
  195. package/dist/workspace/trust/run-commits.d.ts +46 -0
  196. package/dist/workspace/trust/run-commits.d.ts.map +1 -0
  197. package/dist/workspace/trust/run-statement.d.ts +128 -0
  198. package/dist/workspace/trust/run-statement.d.ts.map +1 -0
  199. package/dist/workspace/trust/seal.d.ts +6 -1
  200. package/dist/workspace/trust/seal.d.ts.map +1 -1
  201. package/dist/workspace/trust/verify-cli.d.ts +1 -1
  202. package/dist/workspace/trust/verify.d.ts +16 -2
  203. package/dist/workspace/trust/verify.d.ts.map +1 -1
  204. package/dist/workspace/wip-cli.d.ts +21 -0
  205. package/dist/workspace/wip-cli.d.ts.map +1 -0
  206. package/dist/workspace/wip.d.ts +268 -0
  207. package/dist/workspace/wip.d.ts.map +1 -0
  208. package/dist/workspace/work-cli.d.ts.map +1 -1
  209. package/dist/workspace/work-evidence.d.ts +1 -1
  210. package/dist/workspace/work-evidence.d.ts.map +1 -1
  211. package/dist/workspace/work-size.d.ts +55 -0
  212. package/dist/workspace/work-size.d.ts.map +1 -0
  213. package/dist/workspace/workspace-kinds.schema.json +122 -0
  214. package/dist/workspace/write-lock-cli.d.ts +60 -0
  215. package/dist/workspace/write-lock-cli.d.ts.map +1 -0
  216. package/dist/workspace/write-lock.d.ts +152 -0
  217. package/dist/workspace/write-lock.d.ts.map +1 -0
  218. package/dist/workspace/write-scope.d.ts +6 -2
  219. package/dist/workspace/write-scope.d.ts.map +1 -1
  220. package/package.json +6 -1
  221. package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
  222. package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
  223. package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
  224. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
  225. package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
  226. package/src/__fixtures__/change-set/warden.plan.json +44 -0
  227. package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
  228. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
  229. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
  230. package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
  231. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
  232. package/src/audit/core.test.ts +15 -0
  233. package/src/audit/core.ts +8 -2
  234. package/src/audit/discover.test.ts +11 -0
  235. package/src/audit/discover.ts +7 -2
  236. package/src/change-set-bundle.test.ts +57 -0
  237. package/src/change-set.test.ts +215 -0
  238. package/src/change-set.ts +445 -0
  239. package/src/cli/commands/audit-opt-in.test.ts +54 -0
  240. package/src/cli/commands/audit.ts +29 -2
  241. package/src/cli/commands/build.test.ts +19 -0
  242. package/src/cli/commands/build.ts +3 -2
  243. package/src/cli/handlers/build.ts +13 -1
  244. package/src/cli/handlers/change-set.test.ts +56 -0
  245. package/src/cli/handlers/change-set.ts +44 -0
  246. package/src/cli/handlers/fan-out.test.ts +221 -2
  247. package/src/cli/handlers/fan-out.ts +165 -75
  248. package/src/cli/handlers/graph.ts +3 -0
  249. package/src/cli/handlers/operator.test.ts +21 -0
  250. package/src/cli/handlers/operator.ts +34 -3
  251. package/src/cli/handlers/pr.ts +373 -0
  252. package/src/cli/handlers/run.ts +1 -0
  253. package/src/cli/main.test.ts +20 -0
  254. package/src/cli/main.ts +185 -23
  255. package/src/cli/mcp/never-over-mcp.test.ts +101 -0
  256. package/src/cli/mcp/op-tools.ts +24 -2
  257. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  258. package/src/cli/mcp/workspace-tools.ts +34 -6
  259. package/src/cli/registry.ts +63 -1
  260. package/src/components/auto-release.ts +1 -1
  261. package/src/components/capability.ts +55 -0
  262. package/src/components/changed-files.ts +66 -0
  263. package/src/components/cli-support.ts +28 -5
  264. package/src/components/component.schema.json +17 -0
  265. package/src/components/component.ts +25 -0
  266. package/src/components/deploy-units.ts +3 -0
  267. package/src/components/driver.test.ts +52 -0
  268. package/src/components/driver.ts +31 -13
  269. package/src/components/fan-out-output.ts +42 -1
  270. package/src/components/fan-out-record.ts +69 -0
  271. package/src/components/fan-out-run.test.ts +17 -0
  272. package/src/components/fan-out-run.ts +304 -1
  273. package/src/components/fan-out-support.ts +3 -0
  274. package/src/components/fan-out-waves.test.ts +375 -0
  275. package/src/components/fan-out.ts +18 -17
  276. package/src/components/gated-wave-pipeline.ts +82 -0
  277. package/src/components/index.ts +1 -0
  278. package/src/components/layers.ts +30 -0
  279. package/src/components/pr-pipeline.ts +91 -0
  280. package/src/components/pr-run.test.ts +205 -0
  281. package/src/components/pr-run.ts +280 -0
  282. package/src/components/promote.ts +5 -0
  283. package/src/components/verbs/shell.test.ts +11 -0
  284. package/src/components/verbs/shell.ts +7 -3
  285. package/src/components/wave-plan.ts +124 -0
  286. package/src/discovery/sandbox/bundle.ts +1 -1
  287. package/src/gated-waves-bundle.test.ts +62 -0
  288. package/src/gated-waves.test.ts +118 -0
  289. package/src/gated-waves.ts +240 -0
  290. package/src/index.ts +1 -0
  291. package/src/lexicon.ts +51 -0
  292. package/src/lifecycle/gate-ledger.ts +7 -0
  293. package/src/lifecycle/git.ts +102 -16
  294. package/src/lint/post-synth.test.ts +19 -0
  295. package/src/lint/post-synth.ts +19 -1
  296. package/src/meta/declared-imports.test.ts +1 -1
  297. package/src/op/activities/activity-contracts.ts +84 -0
  298. package/src/op/activities/change-set.ts +114 -0
  299. package/src/op/activities/decide.ts +3 -0
  300. package/src/op/activities/factory.ts +675 -0
  301. package/src/op/activities/index.ts +6 -0
  302. package/src/op/activity-contract.test.ts +21 -1
  303. package/src/op/activity-contract.ts +19 -9
  304. package/src/op/builders.ts +23 -0
  305. package/src/op/decide-backend.ts +5 -1
  306. package/src/op/decide-read-inputs.ts +33 -5
  307. package/src/op/factory.e2e.test.ts +232 -0
  308. package/src/op/factory.ts +118 -0
  309. package/src/op/index.ts +9 -1
  310. package/src/op/op-ir.test.ts +9 -0
  311. package/src/op/op-ir.ts +4 -0
  312. package/src/op/op-verb-class.ts +3 -0
  313. package/src/op/types.ts +8 -0
  314. package/src/op/work-lease-run.ts +3 -0
  315. package/src/plan-summary.test.ts +263 -0
  316. package/src/plan-summary.ts +845 -0
  317. package/src/pr-forge.test.ts +237 -0
  318. package/src/pr-forge.ts +332 -0
  319. package/src/pr-loop-bundle.test.ts +46 -0
  320. package/src/pr-loop.test.ts +171 -0
  321. package/src/pr-loop.ts +324 -0
  322. package/src/telemetry-attribution.test.ts +23 -1
  323. package/src/telemetry-attribution.ts +51 -0
  324. package/src/terraform/hcl2json.test.ts +21 -0
  325. package/src/terraform/hcl2json.ts +81 -0
  326. package/src/terraform/parse.ts +11 -74
  327. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  328. package/src/workspace/app-kind.test.ts +196 -0
  329. package/src/workspace/box-cli.ts +113 -0
  330. package/src/workspace/box-factory.test.ts +4 -1
  331. package/src/workspace/box-factory.ts +37 -4
  332. package/src/workspace/box-listing-write.schema.json +204 -0
  333. package/src/workspace/box-listing.test.ts +234 -0
  334. package/src/workspace/box-listing.ts +324 -0
  335. package/src/workspace/box-publish.schema.json +498 -0
  336. package/src/workspace/box-publish.test.ts +204 -0
  337. package/src/workspace/box-publish.ts +420 -0
  338. package/src/workspace/box-ship.test.ts +96 -0
  339. package/src/workspace/broker-protocol.schema.json +186 -0
  340. package/src/workspace/broker-protocol.test.ts +122 -0
  341. package/src/workspace/broker-protocol.ts +317 -0
  342. package/src/workspace/builder-tiers.test.ts +146 -0
  343. package/src/workspace/change-set.schema.json +460 -0
  344. package/src/workspace/chant-migrations.e2e.test.ts +17 -5
  345. package/src/workspace/checks/boxes.test.ts +3 -0
  346. package/src/workspace/checks.ts +10 -3
  347. package/src/workspace/component-environments.test.ts +105 -0
  348. package/src/workspace/composites.schema.json +23 -4
  349. package/src/workspace/composites.test.ts +8 -8
  350. package/src/workspace/composites.ts +13 -2
  351. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
  352. package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
  353. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
  354. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
  355. package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  356. package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  357. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
  358. package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  359. package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  360. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
  361. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
  362. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
  363. package/src/workspace/conformance/broker.test.ts +122 -0
  364. package/src/workspace/conformance/broker.ts +792 -0
  365. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  366. package/src/workspace/conformance/index.mjs +22 -1
  367. package/src/workspace/conformance/index.ts +5 -1
  368. package/src/workspace/conformance/vitest.ts +33 -1
  369. package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
  370. package/src/workspace/conformance/writer.test.ts +32 -2
  371. package/src/workspace/conformance/writer.ts +339 -28
  372. package/src/workspace/decide.test.ts +208 -4
  373. package/src/workspace/decide.ts +427 -73
  374. package/src/workspace/decision-points.schema.json +11 -4
  375. package/src/workspace/declaration.schema.json +153 -4
  376. package/src/workspace/declaration.ts +141 -0
  377. package/src/workspace/environments.ts +66 -14
  378. package/src/workspace/factory-rules.test.ts +62 -0
  379. package/src/workspace/factory-rules.ts +218 -0
  380. package/src/workspace/graph-cli.ts +2 -2
  381. package/src/workspace/graph.schema.json +67 -0
  382. package/src/workspace/identity.test.ts +23 -0
  383. package/src/workspace/init.ts +82 -2
  384. package/src/workspace/intent-cli.ts +26 -9
  385. package/src/workspace/intent-patch-id.test.ts +162 -0
  386. package/src/workspace/intent-record.schema.json +130 -3
  387. package/src/workspace/intent-record.ts +74 -28
  388. package/src/workspace/intent-reviews.test.ts +76 -0
  389. package/src/workspace/intent-squash.test.ts +213 -0
  390. package/src/workspace/intent-why.test.ts +5 -5
  391. package/src/workspace/intent.schema.json +234 -4
  392. package/src/workspace/intent.ts +260 -66
  393. package/src/workspace/json-edit.test.ts +121 -0
  394. package/src/workspace/json-edit.ts +142 -0
  395. package/src/workspace/jsonc.ts +20 -0
  396. package/src/workspace/kinds.ts +157 -2
  397. package/src/workspace/lexicon-symbols.test.ts +148 -0
  398. package/src/workspace/lexicon-symbols.ts +125 -0
  399. package/src/workspace/lineage-upgrade.test.ts +63 -0
  400. package/src/workspace/lineage-upgrade.ts +146 -5
  401. package/src/workspace/plan-summary.schema.json +155 -0
  402. package/src/workspace/point-answer.schema.json +42 -2
  403. package/src/workspace/points-cli.ts +42 -16
  404. package/src/workspace/points-write.schema.json +162 -4
  405. package/src/workspace/points.schema.json +157 -2
  406. package/src/workspace/points.test.ts +13 -1
  407. package/src/workspace/points.ts +118 -12
  408. package/src/workspace/pr-report.schema.json +95 -0
  409. package/src/workspace/profiles.test.ts +159 -0
  410. package/src/workspace/read-contract.test.ts +2 -2
  411. package/src/workspace/reason-codes.test.ts +13 -1
  412. package/src/workspace/reason-codes.ts +38 -4
  413. package/src/workspace/record-sessions.ts +9 -8
  414. package/src/workspace/records-amend.schema.json +78 -0
  415. package/src/workspace/records-cli.ts +20 -6
  416. package/src/workspace/records-close.schema.json +78 -0
  417. package/src/workspace/records-close.ts +31 -7
  418. package/src/workspace/records-concurrent.e2e.test.ts +135 -0
  419. package/src/workspace/records-concurrent.test.ts +145 -0
  420. package/src/workspace/records-formats.test.ts +46 -10
  421. package/src/workspace/records-new.schema.json +7 -0
  422. package/src/workspace/records-review.schema.json +78 -0
  423. package/src/workspace/records-sessions-write.test.ts +69 -0
  424. package/src/workspace/records-sessions.test.ts +22 -0
  425. package/src/workspace/records-write.ts +192 -25
  426. package/src/workspace/records.schema.json +16 -0
  427. package/src/workspace/records.ts +50 -9
  428. package/src/workspace/reference-kinds/app/package.json +10 -0
  429. package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  430. package/src/workspace/run-statement.schema.json +622 -0
  431. package/src/workspace/runs-cli.ts +343 -14
  432. package/src/workspace/runs-statement.test.ts +239 -0
  433. package/src/workspace/runs-write.schema.json +416 -5
  434. package/src/workspace/runs.schema.json +451 -4
  435. package/src/workspace/runs.test.ts +39 -0
  436. package/src/workspace/runs.ts +296 -35
  437. package/src/workspace/session-kinds.ts +3 -3
  438. package/src/workspace/squash.ts +198 -0
  439. package/src/workspace/status-contract.test.ts +5 -2
  440. package/src/workspace/status-gates.ts +3 -1
  441. package/src/workspace/status.schema.json +126 -3
  442. package/src/workspace/status.ts +90 -1
  443. package/src/workspace/symbols.ts +20 -9
  444. package/src/workspace/trust/run-commits.ts +115 -0
  445. package/src/workspace/trust/run-statement.ts +226 -0
  446. package/src/workspace/trust/seal.ts +8 -2
  447. package/src/workspace/trust/verify-cli.ts +6 -5
  448. package/src/workspace/trust/verify.ts +39 -6
  449. package/src/workspace/wip-cli.ts +97 -0
  450. package/src/workspace/wip-write.schema.json +504 -0
  451. package/src/workspace/wip.schema.json +334 -0
  452. package/src/workspace/wip.test.ts +294 -0
  453. package/src/workspace/wip.ts +753 -0
  454. package/src/workspace/work-cli.ts +3 -0
  455. package/src/workspace/work-evidence.schema.json +3 -0
  456. package/src/workspace/work-size.test.ts +77 -0
  457. package/src/workspace/work-size.ts +93 -0
  458. package/src/workspace/workspace-kinds.schema.json +122 -0
  459. package/src/workspace/write-lock-cli.ts +131 -0
  460. package/src/workspace/write-lock.schema.json +172 -0
  461. package/src/workspace/write-lock.test.ts +189 -0
  462. package/src/workspace/write-lock.ts +474 -0
  463. package/src/workspace/write-scope.test.ts +16 -1
  464. package/src/workspace/write-scope.ts +33 -11
@@ -0,0 +1,204 @@
1
+ /**
2
+ * `chant workspace box publish` (#3165, ws-088): the box's declared
3
+ * publisher run through chant, on workspaces in throwaway git repositories
4
+ * whose publisher is a small script that records the request it got and
5
+ * answers as a test tells it to through the environment.
6
+ */
7
+
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { afterAll, describe, expect, test } from "vitest";
11
+ import { cleanScratch, contract, git, repo } from "./__fixtures__/contract-repo";
12
+ import { boxPublish, readAnswer, splitCommand, type BoxPublishDocument, type BoxPublishRequest } from "./box-publish";
13
+ import schema from "./box-publish.schema.json";
14
+ import statusSchema from "./status.schema.json";
15
+ import { workspaceStatus } from "./status";
16
+
17
+ afterAll(cleanScratch);
18
+
19
+ const { expectValid } = contract(schema);
20
+ const { expectValid: expectRequest } = contract({ $defs: schema.$defs, $ref: "#/$defs/request" });
21
+
22
+ /**
23
+ * The publisher: writes the request it read to .publish-request.json, then
24
+ * does what PUBLISH_MODE says. `good` commits a file with the apply record
25
+ * for an item, or one Chant-Record per record for --records.
26
+ */
27
+ const PUBLISHER = `
28
+ import { execFileSync } from "node:child_process";
29
+ import { readFileSync, writeFileSync } from "node:fs";
30
+ const req = JSON.parse(readFileSync(0, "utf8"));
31
+ writeFileSync(".publish-request.json", JSON.stringify({ ...req, env: process.env.CHANT_PUBLISH_CONTRACT }));
32
+ const mode = process.env.PUBLISH_MODE ?? "good";
33
+ const git = (...a) => execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...a], { encoding: "utf8" }).trim();
34
+ const say = (o) => process.stdout.write("working...\\n" + JSON.stringify(o) + "\\n");
35
+ if (mode === "refuse") { process.stderr.write("W-1 is not built: there is no branch chant/work/W-1\\n"); process.exit(2); }
36
+ if (mode === "fail") { process.stderr.write("the push to acme/app failed\\n"); process.exit(1); }
37
+ if (mode === "silent") { process.stdout.write("done\\n"); process.exit(0); }
38
+ if (mode === "bad-answer") { say({ ok: true, commit: "abc" }); process.exit(0); }
39
+ if (mode === "sleep") { await new Promise((r) => setTimeout(r, 5000)); }
40
+ const records = [{ kind: "decision", id: "D-001", path: "decisions/D-001.md", title: "Keep it" }];
41
+ if (req.dryRun) { say({ ok: true, records: req.action === "records" ? records : [] }); process.exit(0); }
42
+ const tip = git("rev-parse", "HEAD");
43
+ writeFileSync("applied.txt", String(Date.now()));
44
+ git("add", "applied.txt");
45
+ const trailers = req.action === "item"
46
+ ? [mode === "no-record" ? "" : "Chant-Record: work:" + req.item, "Chant-Applied-By: " + (mode === "wrong-by" ? "mallory" : req.by), "Chant-Applied-At: 2026-10-03T12:00:00Z", "Chant-Applied-Commit: " + tip].filter(Boolean)
47
+ : mode === "no-record" ? [] : records.map((r) => "Chant-Record: " + r.kind + ":" + r.id);
48
+ git("commit", "-q", "-m", "publish", ...(trailers.length ? ["-m", trailers.join("\\n")] : []));
49
+ const commit = git("rev-parse", "HEAD");
50
+ say({ ok: true, commit, records: req.action === "records" ? records : [], pullRequest: { url: "https://github.com/acme/app/pull/7", number: 7, branch: "box/" + (req.item ?? "records"), base: "main" }, "x-studio": { box: "b1" } });
51
+ `;
52
+
53
+ const quote = (s: string) => `'${s.replace(/'/g, "'\\''")}'`;
54
+ const PUBLISHER_CMD = `${quote(process.execPath)} publish.mjs`;
55
+
56
+ function declaration(box: Record<string, unknown> = { publisher: PUBLISHER_CMD }, extra: Record<string, unknown> = {}): string {
57
+ return `${JSON.stringify(
58
+ {
59
+ name: "demo",
60
+ schema: 1,
61
+ members: [
62
+ {
63
+ name: "app",
64
+ dir: "app",
65
+ kind: "other",
66
+ because: "a plain Node server",
67
+ box: { services: [{ name: "web", cmd: "node server.mjs" }], factory: { builds: ["app"], publish: { repo: "acme/app", base: "main", branchPrefix: "box/" } }, ...box },
68
+ },
69
+ { name: "docs", dir: "docs", kind: "other", because: "prose" },
70
+ ],
71
+ ...extra,
72
+ },
73
+ null,
74
+ 2,
75
+ )}\n`;
76
+ }
77
+
78
+ /** A workspace committed on main, the publisher beside the declaration. */
79
+ function workspace(decl = declaration()): string {
80
+ const root = repo({ "chant.workspace.json": decl, "publish.mjs": PUBLISHER, "app/server.mjs": "export {};\n", "docs/README.md": "# Docs\n", ".gitignore": ".publish-request.json\n" }, true);
81
+ git(root, "branch", "-M", "main");
82
+ return root;
83
+ }
84
+
85
+ function run(root: string, req: Partial<BoxPublishRequest>, mode = "good"): BoxPublishDocument {
86
+ const doc = boxPublish({ cwd: root, member: "app", env: { ...process.env, PUBLISH_MODE: mode }, ...req });
87
+ expectValid(doc);
88
+ return doc;
89
+ }
90
+
91
+ const ok = (doc: BoxPublishDocument) => {
92
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
93
+ return doc;
94
+ };
95
+ const code = (doc: BoxPublishDocument) => ("error" in doc ? doc.error.code : null);
96
+ const request = (root: string) => JSON.parse(readFileSync(join(root, ".publish-request.json"), "utf-8"));
97
+
98
+ describe("box publish runs the box's publisher and checks its apply record (#3165)", () => {
99
+ test("an item: the request on stdin, the answer read back, and the apply record from the commit's trailers", () => {
100
+ const root = workspace();
101
+ const tip = git(root, "rev-parse", "HEAD");
102
+ const doc = ok(run(root, { item: "W-1", by: "alice" }));
103
+ expect(doc).toMatchObject({ member: "app", action: "item", item: "W-1", by: "alice", dryRun: false, publisher: PUBLISHER_CMD, records: [], pushed: null, local: null });
104
+ expect(doc.commit).toBe(git(root, "rev-parse", "HEAD"));
105
+ expect(doc.applied).toEqual({ by: "alice", at: "2026-10-03T12:00:00Z", commit: tip });
106
+ expect(doc.pullRequest).toEqual({ url: "https://github.com/acme/app/pull/7", number: 7, branch: "box/W-1", base: "main", head: null });
107
+ expect(doc.answer["x-studio"]).toEqual({ box: "b1" });
108
+ const req = request(root);
109
+ expect(req.env).toBe("1");
110
+ delete req.env;
111
+ expectRequest(req);
112
+ expect(req).toMatchObject({ contract: 1, action: "item", member: "app", item: "W-1", by: "alice", head: null, dryRun: false });
113
+ expect(req.factory.publish).toEqual({ forge: "github", repo: "acme/app", base: "main", branchPrefix: "box/", head: null });
114
+ });
115
+
116
+ test("--head reaches the publisher, and a malformed one is refused before it runs", () => {
117
+ const root = workspace();
118
+ ok(run(root, { item: "W-1", by: "alice", head: "alice/app" }));
119
+ expect(request(root).head).toBe("alice/app");
120
+ expect(code(run(root, { item: "W-1", by: "alice", head: "not a repo" }))).toBe("write-usage-invalid");
121
+ });
122
+
123
+ test("a commit without the apply record is publish-unrecorded, with the publisher's answer kept", () => {
124
+ const root = workspace();
125
+ const doc = run(root, { item: "W-1", by: "alice" }, "no-record");
126
+ expect(code(doc)).toBe("publish-unrecorded");
127
+ if (!("error" in doc)) throw new Error("expected an error");
128
+ expect(doc.error.message).toContain("Chant-Record: <kind>:W-1");
129
+ expect(doc.answer?.pullRequest).toBeTruthy();
130
+ const wrong = run(root, { item: "W-1", by: "alice" }, "wrong-by");
131
+ expect(code(wrong)).toBe("publish-unrecorded");
132
+ if ("error" in wrong) expect(wrong.error.message).toContain("it names mallory");
133
+ });
134
+
135
+ test("records: a Chant-Record for each record sent, and --dry-run lists them without a commit", () => {
136
+ const root = workspace();
137
+ const listed = ok(run(root, { records: true, dryRun: true }));
138
+ expect(listed).toMatchObject({ action: "records", item: null, by: null, dryRun: true, commit: null, applied: null });
139
+ expect(listed.records).toEqual([{ kind: "decision", id: "D-001", path: "decisions/D-001.md", title: "Keep it" }]);
140
+ const sent = ok(run(root, { records: true, by: "alice" }));
141
+ expect(sent.commit).toBe(git(root, "rev-parse", "HEAD"));
142
+ expect(sent.applied).toBeNull();
143
+ expect(code(run(root, { records: true, by: "alice" }, "no-record"))).toBe("publish-unrecorded");
144
+ });
145
+
146
+ test("the publisher's exit says refused or failed, and a missing or malformed answer is publish-answer-invalid", () => {
147
+ const root = workspace();
148
+ const refused = run(root, { item: "W-1", by: "alice" }, "refuse");
149
+ expect(code(refused)).toBe("publish-refused");
150
+ if ("error" in refused) expect(refused.error.message).toBe("W-1 is not built: there is no branch chant/work/W-1");
151
+ expect(code(run(root, { item: "W-1", by: "alice" }, "fail"))).toBe("publish-failed");
152
+ expect(code(run(root, { item: "W-1", by: "alice" }, "silent"))).toBe("publish-answer-invalid");
153
+ expect(code(run(root, { item: "W-1", by: "alice" }, "bad-answer"))).toBe("publish-answer-invalid");
154
+ });
155
+
156
+ test("a publisher that runs past the timeout is stopped and reported as publish-failed", () => {
157
+ const root = workspace();
158
+ const doc = run(root, { item: "W-1", by: "alice", timeoutMs: 300 }, "sleep");
159
+ expect(code(doc)).toBe("publish-failed");
160
+ if ("error" in doc) expect(doc.error.message).toContain("no answer within 300ms");
161
+ });
162
+
163
+ test("refusals before the publisher runs: no member, no publisher, no box, no item, no --by", () => {
164
+ const root = workspace();
165
+ expect(code(run(root, { member: "ghost", item: "W-1", by: "alice" }))).toBe("publish-member-unknown");
166
+ expect(code(run(root, { member: "docs", item: "W-1", by: "alice" }))).toBe("publish-none");
167
+ expect(code(run(root, { by: "alice" }))).toBe("write-usage-invalid");
168
+ expect(code(run(root, { item: "W-1" }))).toBe("write-usage-invalid");
169
+ expect(code(run(root, { item: "W-1", records: true, by: "alice" }))).toBe("write-usage-invalid");
170
+ expect(code(run(workspace(declaration({})), { item: "W-1", by: "alice" }))).toBe("publish-none");
171
+ });
172
+
173
+ test("under identity.attribution identified, --by names a forge identity or a signer", () => {
174
+ const root = workspace(declaration(undefined, { identity: { attribution: "identified" } }));
175
+ expect(code(run(root, { item: "W-1", by: "alice" }))).toBe("principal-unidentified");
176
+ expect(ok(run(root, { item: "W-1", by: "github:alice" })).applied?.by).toBe("github:alice");
177
+ });
178
+
179
+ test("status --json prints the publisher under the member's box, and null where none is named", async () => {
180
+ const root = workspace();
181
+ const status = await workspaceStatus({ env: "dev", cwd: root });
182
+ contract(statusSchema).expectValid(status);
183
+ const members = (status as { members: { name: string; box: { publisher?: string | null } | null }[] }).members;
184
+ expect(members.find((m) => m.name === "app")?.box?.publisher).toBe(PUBLISHER_CMD);
185
+ expect(members.find((m) => m.name === "docs")?.box).toBeNull();
186
+ });
187
+ });
188
+
189
+ describe("the publisher's command and answer", () => {
190
+ test("a command splits into words as a shell splits plain words, and nothing else", () => {
191
+ expect(splitCommand("node box/ops/factory/publish.mjs")).toEqual(["node", "box/ops/factory/publish.mjs"]);
192
+ expect(splitCommand(`'/opt/my node/bin/node' "a b" c\\ d $HOME ~/x`)).toEqual(["/opt/my node/bin/node", "a b", "c d", "$HOME", "~/x"]);
193
+ expect(() => splitCommand("node 'open")).toThrow(/unclosed/);
194
+ expect(() => splitCommand(" ")).toThrow(/empty/);
195
+ });
196
+
197
+ test("an answer is checked field by field", () => {
198
+ expect(readAnswer({ ok: false })).toBe("it has no ok: true");
199
+ expect(readAnswer({ ok: true })).toEqual({ commit: null, records: [], pullRequest: null, pushed: null, local: null });
200
+ expect(readAnswer({ ok: true, pullRequest: { url: "http://x/1", number: 1, branch: "b", base: "main" } })).toMatch(/pullRequest/);
201
+ expect(readAnswer({ ok: true, records: [{ kind: "work", id: "W-1" }] })).toMatch(/records\[0\]/);
202
+ expect(readAnswer({ ok: true, pushed: { branch: "box/W-1", repo: "acme/app" }, local: "kept local" })).toMatchObject({ pushed: { branch: "box/W-1", repo: "acme/app" }, local: "kept local" });
203
+ });
204
+ });
@@ -0,0 +1,420 @@
1
+ /**
2
+ * `chant workspace box publish <member> <item> | --records` (#3165, ws-088):
3
+ * the one call a surface such as hud makes to publish a box's work.
4
+ *
5
+ * Publishing is the orchestrator's: studio's kit merges a built work item's
6
+ * branch into the box's checkout, pushes it to the box's `factory.publish`
7
+ * repo through its broker, opens the pull request, and sends the records a
8
+ * person kept uncommitted. The box block names the command that does it,
9
+ * `box.publisher`, so nothing is injected into a surface's environment.
10
+ *
11
+ * chant does none of that work. It reads the publisher from the declaration,
12
+ * applies the identity rule to `--by` (ws-080), runs the publisher from the
13
+ * workspace root with no shell and a JSON request on stdin, reads the last
14
+ * JSON object it prints, and checks the commit it names for the apply record
15
+ * of ws-075: `Chant-Applied-By` naming `--by`, `Chant-Applied-At`,
16
+ * `Chant-Applied-Commit` and a `Chant-Record` for the item, or a
17
+ * `Chant-Record` for each record sent. It prints one document,
18
+ * `box-publish.schema.json`, and never commits, pushes or calls a forge.
19
+ *
20
+ * The publisher's protocol, which `box-publish.schema.json` also states:
21
+ *
22
+ * - stdin: one JSON object, `$defs.request`.
23
+ * - exit 0 and a JSON object on the last line of stdout that holds one:
24
+ * `$defs.answer`, with `ok: true`.
25
+ * - exit 2: a refusal, nothing was published, stderr says why.
26
+ * - any other exit: a failure, stderr says why and what was done.
27
+ */
28
+
29
+ import { spawnSync } from "node:child_process";
30
+ import { factoryView, type FactoryView } from "./box-factory";
31
+ import { readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError } from "./declaration";
32
+ import { IDENTITY_CODES, IdentityError, refuseUnidentified } from "./identity";
33
+ import { commitDetails, tryGit } from "./intent";
34
+ import type { ReasonCode } from "./reason-codes";
35
+ import { readChantTrailers } from "./trailers";
36
+ import { locateWorkspace } from "./which-chant";
37
+ import { scopeSource } from "./write-scope";
38
+
39
+ /** The version of the document this command prints, and of the request it gives a publisher. */
40
+ export const BOX_PUBLISH_CONTRACT_VERSION = 1;
41
+
42
+ /** `$id` of the JSON Schema for what `box publish` prints, shipped beside this file. */
43
+ export const BOX_PUBLISH_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/box-publish/v1/box-publish.schema.json";
44
+
45
+ /** How long a publisher may run before chant stops it: a push and a pull request through a broker, with room to spare. */
46
+ export const PUBLISH_TIMEOUT_MS = 600_000;
47
+
48
+ /** Why a publish printed no result. Closed. */
49
+ export const BOX_PUBLISH_ERROR_CODES = [
50
+ ...WORKSPACE_ERROR_CODES,
51
+ "write-usage-invalid",
52
+ "publish-member-unknown",
53
+ "publish-none",
54
+ "publish-refused",
55
+ "publish-failed",
56
+ "publish-answer-invalid",
57
+ "publish-unrecorded",
58
+ ...IDENTITY_CODES,
59
+ ] as const satisfies readonly ReasonCode[];
60
+ export type BoxPublishErrorCode = (typeof BOX_PUBLISH_ERROR_CODES)[number];
61
+
62
+ /** What a publish is asked to do: one built work item, or the records kept uncommitted. */
63
+ export type PublishAction = "item" | "records";
64
+
65
+ /** A record a publisher sent, or would send with dryRun. */
66
+ export interface PublishedRecord {
67
+ kind: string;
68
+ id: string;
69
+ path: string;
70
+ title: string | null;
71
+ }
72
+
73
+ /** The pull request a publisher opened, or found already open. */
74
+ export interface PublishedPullRequest {
75
+ url: string;
76
+ number: number;
77
+ branch: string;
78
+ base: string;
79
+ /** owner/name of the fork the branch was pushed to, or null when it went to the repo itself. */
80
+ head: string | null;
81
+ }
82
+
83
+ /** What chant writes on the publisher's stdin. */
84
+ export interface PublishRequest {
85
+ contract: number;
86
+ action: PublishAction;
87
+ member: string;
88
+ item: string | null;
89
+ by: string | null;
90
+ head: string | null;
91
+ dryRun: boolean;
92
+ workspace: { root: string };
93
+ /** The workspace's factory as status --json prints it, or null when no box declares one. */
94
+ factory: FactoryView | null;
95
+ }
96
+
97
+ /** The publisher's answer, checked and with every field present. */
98
+ export interface PublishAnswer {
99
+ commit: string | null;
100
+ records: PublishedRecord[];
101
+ pullRequest: PublishedPullRequest | null;
102
+ pushed: { branch: string; repo: string } | null;
103
+ local: string | null;
104
+ }
105
+
106
+ /** What `box publish` prints. */
107
+ export type BoxPublishDocument =
108
+ | ({
109
+ $schema: string;
110
+ contract: number;
111
+ chant: string;
112
+ member: string;
113
+ action: PublishAction;
114
+ item: string | null;
115
+ by: string | null;
116
+ dryRun: boolean;
117
+ /** The publisher as the box block declares it. */
118
+ publisher: string;
119
+ /** The apply record read back from the commit's trailers, for an item; null for records and with dryRun. */
120
+ applied: { by: string; at: string; commit: string } | null;
121
+ /** The publisher's answer as it printed it, x- fields and all. */
122
+ answer: Record<string, unknown>;
123
+ } & PublishAnswer)
124
+ | {
125
+ $schema: string;
126
+ contract: number;
127
+ chant: string;
128
+ member: string | null;
129
+ action: PublishAction | null;
130
+ item: string | null;
131
+ error: { code: BoxPublishErrorCode; message: string };
132
+ /** The publisher's answer when it printed one and something about it was refused. */
133
+ answer?: Record<string, unknown>;
134
+ };
135
+
136
+ export interface BoxPublishRequest {
137
+ cwd: string;
138
+ member: string;
139
+ /** The work item to publish; absent with `records`. */
140
+ item?: string;
141
+ /** Publish the records kept uncommitted instead of an item (`--records`). */
142
+ records?: boolean;
143
+ /** Who publishes (`--by`); required unless dryRun. */
144
+ by?: string;
145
+ /** A fork to push to, as owner/name (`--head`). */
146
+ head?: string;
147
+ /** Ask the publisher what would be published, and publish nothing (`--dry-run`). */
148
+ dryRun?: boolean;
149
+ /** The agent session (`CHANT_AGENT`). */
150
+ agent?: string;
151
+ /** The environment the publisher runs with; the caller's when absent. */
152
+ env?: NodeJS.ProcessEnv;
153
+ timeoutMs?: number;
154
+ }
155
+
156
+ class PublishError extends Error {
157
+ constructor(
158
+ readonly code: BoxPublishErrorCode,
159
+ message: string,
160
+ readonly answer?: Record<string, unknown>,
161
+ ) {
162
+ super(message);
163
+ }
164
+ }
165
+
166
+ const RECORD_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
167
+ const COMMIT_ID = /^[0-9a-f]{40,64}$/;
168
+ const REPO = /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9._-]{1,100}$/;
169
+ const MAX_MESSAGE = 2000;
170
+ const isObject = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === "object" && !Array.isArray(v);
171
+
172
+ /**
173
+ * Split a command into words as a POSIX shell splits plain words: blanks
174
+ * separate words, single quotes keep everything, double quotes keep
175
+ * everything but `\\`, `\"`, `\$` and `` \` ``, and a backslash outside quotes
176
+ * escapes the next character. Nothing else a shell does (variables, `~`,
177
+ * globs, pipes, redirection) happens: those characters are part of a word.
178
+ */
179
+ export function splitCommand(text: string): string[] {
180
+ const words: string[] = [];
181
+ let word = "";
182
+ let inWord = false;
183
+ let quote: "'" | '"' | null = null;
184
+ for (let i = 0; i < text.length; i++) {
185
+ const c = text[i];
186
+ if (quote === "'") {
187
+ if (c === "'") quote = null;
188
+ else word += c;
189
+ continue;
190
+ }
191
+ if (quote === '"') {
192
+ if (c === '"') quote = null;
193
+ else if (c === "\\" && i + 1 < text.length && '\\"$`'.includes(text[i + 1])) word += text[++i];
194
+ else word += c;
195
+ continue;
196
+ }
197
+ if (c === " " || c === "\t" || c === "\n") {
198
+ if (inWord) words.push(word);
199
+ word = "";
200
+ inWord = false;
201
+ continue;
202
+ }
203
+ inWord = true;
204
+ if (c === "'" || c === '"') quote = c;
205
+ else if (c === "\\") {
206
+ if (i + 1 < text.length) word += text[++i];
207
+ } else word += c;
208
+ }
209
+ if (quote !== null) throw new Error(`it has an unclosed ${quote} quote`);
210
+ if (inWord) words.push(word);
211
+ if (words.length === 0) throw new Error("it is empty");
212
+ return words;
213
+ }
214
+
215
+ /** The last line of `stdout` that is a JSON object, or null. */
216
+ function lastJsonObject(stdout: string): Record<string, unknown> | null {
217
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
218
+ for (let i = lines.length - 1; i >= 0; i--) {
219
+ try {
220
+ const value: unknown = JSON.parse(lines[i]);
221
+ if (isObject(value)) return value;
222
+ } catch {
223
+ // Not this line.
224
+ }
225
+ }
226
+ return null;
227
+ }
228
+
229
+ const clip = (text: string) => {
230
+ const t = text.trim();
231
+ return t.length > MAX_MESSAGE ? `...${t.slice(-MAX_MESSAGE)}` : t;
232
+ };
233
+
234
+ const line = (v: unknown): v is string => typeof v === "string" && v.trim() !== "" && !/[\u0000-\u001f\u007f]/.test(v);
235
+
236
+ /** The publisher's answer, checked field by field, or a reason it is not one. */
237
+ export function readAnswer(answer: Record<string, unknown>): PublishAnswer | string {
238
+ if (answer.ok !== true) return "it has no ok: true";
239
+ const out: PublishAnswer = { commit: null, records: [], pullRequest: null, pushed: null, local: null };
240
+ if (answer.commit !== undefined && answer.commit !== null) {
241
+ if (typeof answer.commit !== "string" || !COMMIT_ID.test(answer.commit)) return `its commit ${JSON.stringify(answer.commit)} is not a full commit id`;
242
+ out.commit = answer.commit;
243
+ }
244
+ if (answer.records !== undefined) {
245
+ if (!Array.isArray(answer.records)) return "its records is not a list";
246
+ for (const [i, r] of answer.records.entries()) {
247
+ if (!isObject(r) || !line(r.kind) || !line(r.id) || !line(r.path)) return `records[${i}] is not { kind, id, path, title }`;
248
+ if (r.title !== undefined && r.title !== null && typeof r.title !== "string") return `records[${i}].title is not a string`;
249
+ out.records.push({ kind: r.kind, id: r.id, path: r.path, title: typeof r.title === "string" ? r.title : null });
250
+ }
251
+ }
252
+ if (answer.pullRequest !== undefined && answer.pullRequest !== null) {
253
+ const pr = answer.pullRequest;
254
+ let https = false;
255
+ try {
256
+ https = isObject(pr) && typeof pr.url === "string" && new URL(pr.url).protocol === "https:";
257
+ } catch {
258
+ https = false;
259
+ }
260
+ if (!isObject(pr) || !https || typeof pr.number !== "number" || !Number.isSafeInteger(pr.number) || pr.number <= 0 || !line(pr.branch) || !line(pr.base)) {
261
+ return "its pullRequest is not { url (https), number, branch, base, head }";
262
+ }
263
+ if (pr.head !== undefined && pr.head !== null && !(typeof pr.head === "string" && REPO.test(pr.head))) return "its pullRequest.head is not owner/name";
264
+ out.pullRequest = { url: pr.url as string, number: pr.number, branch: pr.branch, base: pr.base, head: typeof pr.head === "string" ? pr.head : null };
265
+ }
266
+ if (answer.pushed !== undefined && answer.pushed !== null) {
267
+ const p = answer.pushed;
268
+ if (!isObject(p) || !line(p.branch) || !line(p.repo)) return "its pushed is not { branch, repo }";
269
+ out.pushed = { branch: p.branch, repo: p.repo };
270
+ }
271
+ if (answer.local !== undefined && answer.local !== null) {
272
+ if (typeof answer.local !== "string") return "its local is not a string";
273
+ out.local = answer.local;
274
+ }
275
+ return out;
276
+ }
277
+
278
+ /** Run the publish and build the document it prints. Never throws for a refusal. */
279
+ export function boxPublish(req: BoxPublishRequest): BoxPublishDocument {
280
+ const head = { $schema: BOX_PUBLISH_SCHEMA_ID, contract: BOX_PUBLISH_CONTRACT_VERSION, chant: readerVersion() };
281
+ const action: PublishAction = req.records ? "records" : "item";
282
+ try {
283
+ return publish(req, action, head);
284
+ } catch (err) {
285
+ const fail = (code: BoxPublishErrorCode, message: string, answer?: Record<string, unknown>): BoxPublishDocument => ({
286
+ ...head,
287
+ member: req.member || null,
288
+ action,
289
+ item: req.item ?? null,
290
+ error: { code, message },
291
+ ...(answer ? { answer } : {}),
292
+ });
293
+ if (err instanceof PublishError) return fail(err.code, err.message, err.answer);
294
+ if (err instanceof IdentityError) return fail(err.code, err.message);
295
+ if (err instanceof WorkspaceReadError) return fail(err.code as BoxPublishErrorCode, err.describe());
296
+ throw err;
297
+ }
298
+ }
299
+
300
+ function publish(req: BoxPublishRequest, action: PublishAction, head: { $schema: string; contract: number; chant: string }): BoxPublishDocument {
301
+ if (!req.member) throw new PublishError("write-usage-invalid", "box publish needs the member whose box publishes: box publish <member> <item>, or box publish <member> --records");
302
+ if (action === "item") {
303
+ if (req.item === undefined) throw new PublishError("write-usage-invalid", "box publish needs the work item to publish, or --records for the records kept uncommitted");
304
+ if (!RECORD_ID.test(req.item)) throw new PublishError("write-usage-invalid", `${JSON.stringify(req.item)} is not a work item id`);
305
+ } else if (req.item !== undefined) {
306
+ throw new PublishError("write-usage-invalid", `--records publishes the records kept uncommitted and takes no work item, and was given ${req.item}`);
307
+ }
308
+ if (req.head !== undefined && !REPO.test(req.head)) throw new PublishError("write-usage-invalid", `--head names the fork to push to as owner/name, not ${JSON.stringify(req.head)}`);
309
+ const by = req.by?.trim() || null;
310
+ if (by === null && !req.dryRun) throw new PublishError("write-usage-invalid", "box publish needs --by: who publishes, which the apply record names");
311
+
312
+ const located = locateWorkspace(req.cwd);
313
+ if (!located.top) throw new WorkspaceReadError("not-a-git-repository", "box publish reads the commit a publisher made, and this directory is not in a git repository");
314
+ const declaration = readDeclaration(located.tree);
315
+ const member = declaration.members.find((m) => m.name === req.member);
316
+ if (!member) {
317
+ throw new PublishError("publish-member-unknown", `no member is named ${req.member}; the declaration's members are ${declaration.members.map((m) => m.name).join(", ") || "none"}`);
318
+ }
319
+ const publisher = member.box?.publisher ?? null;
320
+ if (publisher === null) {
321
+ throw new PublishError(
322
+ "publish-none",
323
+ member.box
324
+ ? `member ${member.name}'s box block names no publisher, so nothing publishes its work; an orchestrator such as studio names one (box.publisher, ws-088)`
325
+ : `member ${member.name} declares no box block, so it has no publisher (box.publisher, ws-088)`,
326
+ );
327
+ }
328
+ let argv: string[];
329
+ try {
330
+ argv = splitCommand(publisher);
331
+ } catch (err) {
332
+ throw new PublishError("publish-failed", `member ${member.name}'s publisher ${JSON.stringify(publisher)} can't be run: ${(err as Error).message}`);
333
+ }
334
+ if (by !== null) refuseUnidentified(scopeSource(req.cwd), [by], "--by", { agent: req.agent });
335
+
336
+ const factory = declaration.members.find((m) => m.box?.factory)?.box?.factory ?? null;
337
+ const request: PublishRequest = {
338
+ contract: BOX_PUBLISH_CONTRACT_VERSION,
339
+ action,
340
+ member: member.name,
341
+ item: req.item ?? null,
342
+ by,
343
+ head: req.head ?? null,
344
+ dryRun: req.dryRun === true,
345
+ workspace: { root: located.rootOnDisk },
346
+ factory: factoryView(factory),
347
+ };
348
+ const timeoutMs = req.timeoutMs ?? PUBLISH_TIMEOUT_MS;
349
+ const run = spawnSync(argv[0], argv.slice(1), {
350
+ cwd: located.rootOnDisk,
351
+ input: `${JSON.stringify(request)}\n`,
352
+ env: { ...(req.env ?? process.env), CHANT_PUBLISH_CONTRACT: String(BOX_PUBLISH_CONTRACT_VERSION) },
353
+ encoding: "utf-8",
354
+ timeout: timeoutMs,
355
+ maxBuffer: 16 * 1024 * 1024,
356
+ });
357
+ const stdout = run.stdout ?? "";
358
+ const stderr = run.stderr ?? "";
359
+ const printed = lastJsonObject(stdout);
360
+ if (run.error) {
361
+ const e = run.error as NodeJS.ErrnoException;
362
+ if (e.code === "ETIMEDOUT") throw new PublishError("publish-failed", `the publisher gave no answer within ${timeoutMs}ms and was stopped; what it had done by then stays done`, printed ?? undefined);
363
+ throw new PublishError("publish-failed", e.code === "ENOENT" ? `the publisher ${argv[0]} was not found` : `the publisher could not be run: ${e.message}`);
364
+ }
365
+ if (run.status !== 0) {
366
+ const said = clip(stderr) || `it exited ${run.status ?? `on ${run.signal}`} and said nothing on stderr`;
367
+ if (run.status === 2) throw new PublishError("publish-refused", said, printed ?? undefined);
368
+ throw new PublishError("publish-failed", said, printed ?? undefined);
369
+ }
370
+ if (printed === null) throw new PublishError("publish-answer-invalid", "the publisher exited 0 and printed no JSON object on stdout");
371
+ const answer = readAnswer(printed);
372
+ if (typeof answer === "string") throw new PublishError("publish-answer-invalid", `the publisher's answer is not one box-publish.schema.json allows: ${answer}`, printed);
373
+
374
+ let applied: { by: string; at: string; commit: string } | null = null;
375
+ if (!req.dryRun) {
376
+ if (answer.commit === null) throw new PublishError("publish-unrecorded", `the publisher answered with no commit, so the ${action === "item" ? "apply" : "records"} commit can't be checked for its trailers`, printed);
377
+ if (action === "records" && answer.records.length === 0) throw new PublishError("publish-answer-invalid", "the publisher sent records and named none", printed);
378
+ applied = checkRecord(located.top, answer, action, req.item ?? null, by!, printed);
379
+ }
380
+
381
+ return {
382
+ ...head,
383
+ member: member.name,
384
+ action,
385
+ item: req.item ?? null,
386
+ by,
387
+ dryRun: req.dryRun === true,
388
+ publisher,
389
+ ...answer,
390
+ applied,
391
+ answer: printed,
392
+ };
393
+ }
394
+
395
+ /**
396
+ * The apply record on the commit the publisher named (ws-075): for an item,
397
+ * Chant-Applied-By naming `by`, Chant-Applied-At, Chant-Applied-Commit and a
398
+ * Chant-Record for the item; for records, a Chant-Record for each one sent.
399
+ */
400
+ function checkRecord(top: string, answer: PublishAnswer, action: PublishAction, item: string | null, by: string, printed: Record<string, unknown>): { by: string; at: string; commit: string } | null {
401
+ const sha = answer.commit!;
402
+ if (tryGit(top, ["cat-file", "-e", `${sha}^{commit}`]) === undefined) {
403
+ throw new PublishError("publish-unrecorded", `the publisher named commit ${sha}, which this repository does not have`, printed);
404
+ }
405
+ const commit = commitDetails(top, [sha]).get(sha);
406
+ const trailers = readChantTrailers(commit?.trailers ?? {});
407
+ const missing: string[] = [];
408
+ if (action === "records") {
409
+ for (const r of answer.records) if (!trailers.records.some((t) => t.kind === r.kind && t.id === r.id)) missing.push(`Chant-Record: ${r.kind}:${r.id}`);
410
+ if (missing.length) throw new PublishError("publish-unrecorded", `commit ${sha.slice(0, 12)} sends records without naming them: it lacks ${missing.join(", ")} (ws-075)`, printed);
411
+ return null;
412
+ }
413
+ if (!trailers.records.some((t) => t.id === item)) missing.push(`Chant-Record: <kind>:${item}`);
414
+ if (trailers.applied === null) missing.push(`Chant-Applied-By: ${by}`);
415
+ else if (trailers.applied.by !== by) missing.push(`Chant-Applied-By: ${by} (it names ${trailers.applied.by})`);
416
+ if (trailers.applied?.at === null || trailers.applied?.at === undefined || Number.isNaN(Date.parse(trailers.applied.at))) missing.push("Chant-Applied-At: <ISO 8601>");
417
+ if (trailers.applied?.commit === null || trailers.applied?.commit === undefined) missing.push("Chant-Applied-Commit: <the applied branch's tip>");
418
+ if (missing.length) throw new PublishError("publish-unrecorded", `commit ${sha.slice(0, 12)} applies ${item} without its apply record: it lacks ${missing.join(", ")} (ws-075)`, printed);
419
+ return { by: trailers.applied!.by, at: trailers.applied!.at!, commit: trailers.applied!.commit! };
420
+ }