@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,162 @@
1
+ /**
2
+ * Joining a commit to its agent run by patch-id when the trailer is gone (#3036).
3
+ *
4
+ * - app/server.ts; s-001 (path:app/server.ts) is decided and W-001 implements it.
5
+ * - On a side branch, run A (on W-001) records c1, which carries no trailer.
6
+ * main cherry-picks it as p1: a new sha, the same patch-id.
7
+ * - Run B records b1 on the side branch; main then commits the same change
8
+ * as p2 with `Chant-Run: run-c`: a trailer join, and content that matches
9
+ * another run's recorded commit.
10
+ * - Run D records d1 and d2 on the side branch; main squashes them into one
11
+ * commit, sq, whose patch-id is neither's.
12
+ */
13
+
14
+ import { readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
17
+ import { cleanScratch, contract, git, REPO, repo, writeFiles } from "./__fixtures__/contract-repo";
18
+ import { intentGraph, type IntentDocument } from "./intent";
19
+ import { formatIntent } from "./intent-cli";
20
+ import { intentRecord } from "./intent-record";
21
+ import intentRecordSchema from "./intent-record.schema.json";
22
+ import intentSchema from "./intent.schema.json";
23
+ import { parseFrontMatter } from "./records";
24
+ import { runsWrite, workspaceRuns } from "./runs-cli";
25
+ import runsSchema from "./runs.schema.json";
26
+
27
+ const REF = join(REPO, "reference-workspace");
28
+ const BASE = (() => {
29
+ const fm = parseFrontMatter(readFileSync(join(REF, "decisions", "ref-001-how-the-app-is-deployed.md"), "utf-8"));
30
+ if (!fm.ok) throw new Error(fm.message);
31
+ return fm.value;
32
+ })();
33
+ const decision = (id: string, fields: Record<string, unknown>) => `---\n${JSON.stringify({ ...BASE, id, title: `Decision ${id}`, state: "decided", supersedes: [], evidence: [], reviews: [], ...fields }, null, 2)}\n---\n\n# ${id}\n`;
34
+ const work = (id: string, fields: Record<string, unknown>) =>
35
+ `---\n${JSON.stringify({ schema: 1, id, title: `Work ${id}`, state: "open", implements: [], needs: [], constrains: [], evidence: [], opened_on: "2026-10-01", supersedes: [], ...fields }, null, 2)}\n---\n\n# ${id}\n\nWhat the work is.\n`;
36
+
37
+ const LINES = ["// The app server.", "export const a = 0;", "export const b = 0;", "export const c = 0;", "export const d = 0;", "export const e = 0;"];
38
+ const text = (lines: string[]) => `${lines.join("\n")}\n`;
39
+ const edit = (lines: string[], at: number, to: string) => lines.map((l, i) => (i === at - 1 ? to : l));
40
+
41
+ type Doc = Exclude<IntentDocument, { error: unknown }>;
42
+
43
+ let root: string;
44
+ const sha: Record<string, string> = {};
45
+
46
+ function commit(message: string[]): string {
47
+ git(root, "add", "-A");
48
+ git(root, "commit", "-q", ...message.flatMap((m) => ["-m", m]));
49
+ return git(root, "rev-parse", "HEAD");
50
+ }
51
+ async function record(fields: object): Promise<void> {
52
+ const doc = await runsWrite({ verb: "record", fields: JSON.stringify(fields), cwd: root });
53
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
54
+ }
55
+ async function walk(region: string): Promise<Doc> {
56
+ const { doc } = await intentGraph({ cwd: root, region });
57
+ contract(intentSchema).expectValid(doc);
58
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
59
+ return doc;
60
+ }
61
+
62
+ beforeAll(async () => {
63
+ root = repo({
64
+ "chant.workspace.json": JSON.stringify({ name: "patch-id", schema: 1, records: [{ kind: "decisions/decision.kind.mjs" }, { kind: "work/work.kind.mjs" }], members: [{ name: "app", dir: "app", kind: "other", because: "the app" }] }),
65
+ "decisions/decision.kind.mjs": readFileSync(join(REF, "decisions", "decision.kind.mjs"), "utf-8"),
66
+ "decisions/decision.schema.json": readFileSync(join(REF, "decisions", "decision.schema.json"), "utf-8"),
67
+ "work/work.kind.mjs": readFileSync(join(REF, "work", "work.kind.mjs"), "utf-8"),
68
+ "work/work.schema.json": readFileSync(join(REF, "work", "work.schema.json"), "utf-8"),
69
+ "app/server.ts": text(LINES),
70
+ "decisions/s-001-server.md": decision("s-001", { constrains: ["path:app/server.ts"] }),
71
+ "work/W-001-server.md": work("W-001", { implements: ["s-001"] }),
72
+ });
73
+ sha.c0 = commit(["the workspace"]);
74
+ const main = git(root, "rev-parse", "--abbrev-ref", "HEAD");
75
+
76
+ // The side branch, where the runs made their commits.
77
+ git(root, "checkout", "-q", "-b", "side");
78
+ writeFiles(root, { "app/server.ts": text(edit(LINES, 2, "export const a = 1;")) });
79
+ sha.c1 = commit(["a is 1"]);
80
+ writeFiles(root, { "app/server.ts": text(edit(edit(LINES, 2, "export const a = 1;"), 3, "export const b = 1;")) });
81
+ sha.b1 = commit(["b is 1"]);
82
+ git(root, "checkout", "-q", "-b", "squashed", sha.c0);
83
+ writeFiles(root, { "app/server.ts": text(edit(LINES, 5, "export const d = 1;")) });
84
+ sha.d1 = commit(["d is 1"]);
85
+ writeFiles(root, { "app/server.ts": text(edit(edit(LINES, 5, "export const d = 1;"), 6, "export const e = 1;")) });
86
+ sha.d2 = commit(["e is 1"]);
87
+ git(root, "checkout", "-q", main);
88
+
89
+ await record({ id: "run-a", harness: "claude-code", model: "claude-opus-5-5", unit: "W-001", startedAt: "2026-10-01T10:00:00Z", commits: [{ sha: sha.c1, hunks: [{ path: "app/server.ts", start: 2, end: 2 }] }] });
90
+ await record({ id: "run-b", harness: "claude-code", startedAt: "2026-10-01T11:00:00Z", commits: [sha.b1] });
91
+ await record({ id: "run-c", harness: "other", startedAt: "2026-10-01T11:30:00Z" });
92
+ await record({ id: "run-d", harness: "claude-code", startedAt: "2026-10-01T12:00:00Z", commits: [sha.d1, sha.d2] });
93
+
94
+ // main: a cherry-pick that keeps no trailer, a commit whose trailer names another run, and a squash.
95
+ git(root, "cherry-pick", sha.c1);
96
+ sha.p1 = git(root, "rev-parse", "HEAD");
97
+ writeFiles(root, { "app/server.ts": text(edit(edit(LINES, 2, "export const a = 1;"), 3, "export const b = 1;")) });
98
+ sha.p2 = commit(["b is 1, again", "Chant-Run: run-c"]);
99
+ git(root, "merge", "--squash", "-q", "squashed");
100
+ sha.sq = commit(["d and e (#7)"]);
101
+ }, 60_000);
102
+ afterAll(cleanScratch);
103
+
104
+ describe("graph --intent joins by patch-id when the trailer is gone", () => {
105
+ test("a cherry-pick of a recorded commit joins its run by content", async () => {
106
+ const doc = await walk("app/server.ts:2");
107
+ expect(sha.p1).not.toBe(sha.c1);
108
+ expect(doc.edges.filter((e) => e.kind === "made-by" && e.from === `commit:${sha.p1}`)).toEqual([{ kind: "made-by", from: `commit:${sha.p1}`, to: "run:run-a", joinedBy: ["patch-id"], recordedAs: sha.c1 }]);
109
+ // The run's work item is carried as by any join, so the commit is s-001's own work.
110
+ expect(doc.edges).toContainEqual({ kind: "carries", from: `commit:${sha.p1}`, to: "record:work/W-001" });
111
+ expect(doc.edges).toContainEqual({ kind: "within", from: `commit:${sha.p1}`, to: "record:decision/s-001", state: "decided" });
112
+ // The why block marks the span and the run as joined by content. The recorded hunks are
113
+ // line numbers in the recorded commit, so a content join is not narrowed by them.
114
+ expect(doc.why.blame).toEqual([{ start: 2, end: 2, commit: `commit:${sha.p1}`, sha: sha.p1, runs: ["run:run-a"], narrowedBy: null, joinedBy: ["patch-id"] }]);
115
+ expect(doc.why.runs).toEqual([{ run: "run:run-a", lines: 1, commits: [`commit:${sha.p1}`], unit: { id: "W-001", kind: null, node: "record:work/W-001" }, decisions: ["record:decision/s-001"], joinedBy: ["patch-id"] }]);
116
+ expect(doc.why.decisions[0]).toMatchObject({ decision: "record:decision/s-001", relevance: "carried", lines: 1 });
117
+ expect(formatIntent(doc)).toContain(`run-a: claude-code/claude-opus-5-5, unpriced; joined by content, as ${sha.c1.slice(0, 8)}`);
118
+ expect(formatIntent(doc)).toContain("(joined by content)");
119
+ });
120
+
121
+ test("a trailer join is never overridden, and content that matches another run is a finding", async () => {
122
+ const doc = await walk("app/server.ts:3");
123
+ expect(doc.edges.filter((e) => e.kind === "made-by" && e.from === `commit:${sha.p2}`)).toEqual([{ kind: "made-by", from: `commit:${sha.p2}`, to: "run:run-c", joinedBy: ["trailer"] }]);
124
+ const conflict = doc.nodes.find((n) => n.kind === "finding" && n.code === "intent-commit-join-conflict");
125
+ expect(conflict).toMatchObject({ concerns: [`commit:${sha.p2}`, "run:run-c"] });
126
+ expect(conflict && "message" in conflict ? conflict.message : "").toContain(`run-b (${sha.b1.slice(0, 8)})`);
127
+ });
128
+
129
+ test("a squash of several commits has a patch-id of its own and doesn't join by content", async () => {
130
+ const doc = await walk("app/server.ts:5-6");
131
+ expect(doc.edges.filter((e) => e.kind === "made-by" && e.from === `commit:${sha.sq}`)).toEqual([]);
132
+ expect(doc.why.gaps.map((g) => g.code)).toContain("intent-why-no-run");
133
+ });
134
+
135
+ test("graph --intent --record lists the content join on the commit", async () => {
136
+ const { doc } = await intentRecord({ cwd: root, record: "s-001" });
137
+ contract(intentRecordSchema).expectValid(doc);
138
+ if ("error" in doc) throw new Error(doc.error.message);
139
+ expect(doc.commits.find((c) => c.sha === sha.p1)?.runs).toEqual([
140
+ { id: "run-a", recorded: true, state: "ended", harness: "claude-code", model: "claude-opus-5-5", provider: null, by: null, agent: null, unit: "W-001", joinedBy: ["patch-id"], recordedAs: sha.c1 },
141
+ ]);
142
+ });
143
+ });
144
+
145
+ describe("runs --json joins by patch-id", () => {
146
+ test("a run lists the rewritten commit after the one it recorded, with no hunks", async () => {
147
+ const doc = await workspaceRuns({ cwd: root });
148
+ contract(runsSchema).expectValid(doc);
149
+ if ("error" in doc) throw new Error(doc.error.message);
150
+ const byId = new Map(doc.runs.map((r) => [r.id, r]));
151
+ const a = byId.get("run-a")!;
152
+ expect(a.commits).toEqual([
153
+ { sha: sha.c1, patchId: expect.stringMatching(/^[0-9a-f]{40}$/), joinedBy: ["record"], hunks: [{ path: "app/server.ts", start: 2, end: 2 }] },
154
+ { sha: sha.p1, patchId: a.commits[0].patchId, joinedBy: ["patch-id"], hunks: null, recordedAs: sha.c1 },
155
+ ]);
156
+ // p2 carries Chant-Run: run-c, so it joins run C by trailer and run B not at all.
157
+ expect(byId.get("run-b")!.commits.map((c) => c.sha)).toEqual([sha.b1]);
158
+ expect(byId.get("run-c")!.commits).toEqual([{ sha: sha.p2, patchId: expect.any(String), joinedBy: ["trailer"], hunks: null }]);
159
+ // The squash joins neither of run D's commits.
160
+ expect(byId.get("run-d")!.commits.map((c) => c.sha)).toEqual([sha.d1, sha.d2]);
161
+ });
162
+ });
@@ -412,6 +412,9 @@
412
412
  "$ref": "#/$defs/runRef"
413
413
  },
414
414
  "description": "The agent runs that made the commit (#3033). Added within version 1, so an older document may lack it."
415
+ },
416
+ "squash": {
417
+ "$ref": "#/$defs/squash"
415
418
  }
416
419
  }
417
420
  },
@@ -473,7 +476,8 @@
473
476
  "code": {
474
477
  "enum": [
475
478
  "intent-history-shallow",
476
- "intent-plugin-failed"
479
+ "intent-plugin-failed",
480
+ "squash-unfollowed"
477
481
  ]
478
482
  },
479
483
  "message": {
@@ -726,10 +730,133 @@
726
730
  "items": {
727
731
  "enum": [
728
732
  "trailer",
729
- "record"
733
+ "record",
734
+ "patch-id",
735
+ "squash"
730
736
  ]
731
737
  },
732
- "description": "trailer: the commit carries Chant-Run with the run's id. record: the run's end lists the commit."
738
+ "description": "trailer: the commit carries Chant-Run with the run's id. record: the run's end lists the commit. patch-id: neither holds, and the commit's git patch-id --stable equals a commit's the run's end recorded, as after a rebase or a cherry-pick that dropped the trailer (#3036); never alongside the others. Added within version 1. squash: the commit squashes a pull request whose original commits join the run one of those ways, and the read was asked to follow squashes (#3035); via names them. Added within version 1."
739
+ },
740
+ "recordedAs": {
741
+ "type": "string",
742
+ "pattern": "^[0-9a-f]{40,64}$",
743
+ "description": "For a patch-id join (#3036): the commit the run's end recorded with the same patch-id, which this one rewrites. Added within version 1."
744
+ },
745
+ "via": {
746
+ "type": "array",
747
+ "items": {
748
+ "type": "string",
749
+ "pattern": "^[0-9a-f]{40,64}$"
750
+ },
751
+ "description": "For a squash join (#3035): the pull request's original commits through which the commit joins the run, oldest first. Added within version 1."
752
+ }
753
+ }
754
+ },
755
+ "squash": {
756
+ "type": "object",
757
+ "description": "Present only when the read follows squashes (--follow-squash, #3035) and the commit squashes a pull request: its subject ends with (#<n>), it has one parent, and the pull request's head is not in its history. The commit carries what the original commits' trailers carry and joins their runs. Added within version 1.",
758
+ "required": [
759
+ "pullRequest",
760
+ "forge",
761
+ "ref",
762
+ "head",
763
+ "fetched",
764
+ "followed",
765
+ "commits"
766
+ ],
767
+ "properties": {
768
+ "pullRequest": {
769
+ "type": "integer",
770
+ "minimum": 1
771
+ },
772
+ "forge": {
773
+ "enum": [
774
+ "github",
775
+ "forgejo",
776
+ null
777
+ ],
778
+ "description": "The forge origin points at, from its URL, or null for one chant doesn't know or no origin. Both keep the pull request's head at refs/pull/<n>/head."
779
+ },
780
+ "ref": {
781
+ "type": [
782
+ "string",
783
+ "null"
784
+ ],
785
+ "description": "The local ref the head was read from: refs/chant/pull/<n>/head where a read fetched it, refs/pull/<n>/head, or refs/remotes/origin/pr/<n>. Null when none could be read."
786
+ },
787
+ "head": {
788
+ "oneOf": [
789
+ {
790
+ "type": "string",
791
+ "pattern": "^[0-9a-f]{40,64}$"
792
+ },
793
+ {
794
+ "type": "null"
795
+ }
796
+ ],
797
+ "description": "The pull request's head commit, or null."
798
+ },
799
+ "fetched": {
800
+ "type": "boolean",
801
+ "description": "Whether this read fetched the ref from origin."
802
+ },
803
+ "followed": {
804
+ "type": "boolean",
805
+ "description": "Whether the original commits were read. When false, squash-unfollowed in reasons says why, and commits is empty."
806
+ },
807
+ "commits": {
808
+ "type": "array",
809
+ "items": {
810
+ "type": "object",
811
+ "required": [
812
+ "sha",
813
+ "subject",
814
+ "author",
815
+ "date",
816
+ "trailers",
817
+ "joins"
818
+ ],
819
+ "properties": {
820
+ "sha": {
821
+ "type": "string",
822
+ "pattern": "^[0-9a-f]{40,64}$"
823
+ },
824
+ "subject": {
825
+ "type": "string"
826
+ },
827
+ "author": {
828
+ "type": "object",
829
+ "required": [
830
+ "name",
831
+ "email"
832
+ ],
833
+ "properties": {
834
+ "name": {
835
+ "type": "string"
836
+ },
837
+ "email": {
838
+ "type": "string"
839
+ }
840
+ }
841
+ },
842
+ "date": {
843
+ "type": "string"
844
+ },
845
+ "trailers": {
846
+ "type": "object",
847
+ "additionalProperties": {
848
+ "type": "array",
849
+ "items": {
850
+ "type": "string"
851
+ }
852
+ }
853
+ },
854
+ "joins": {
855
+ "$ref": "#/$defs/trailerJoins"
856
+ }
857
+ }
858
+ },
859
+ "description": "The original commits, oldest first: the head's commits the squash's parent doesn't have, merges left out."
733
860
  }
734
861
  }
735
862
  }
@@ -29,12 +29,16 @@
29
29
  * `workedBy` and `alsoWithin` list every work item and other decision that
30
30
  * matched, whatever the bucket. `issue` and `contract` entries are listed and
31
31
  * not walked: they name no path. Git is read through a local `git`
32
- * subprocess only: no fetch, no network.
32
+ * subprocess only: no fetch, no network, unless the walk is asked to follow
33
+ * squashes (#3035). Then a squash commit lists its pull request's original
34
+ * commits, joins their runs and counts as the record's own work when one of
35
+ * them carries it, and a pull request ref the clone lacks is fetched from
36
+ * `origin`.
33
37
  */
34
38
 
35
39
  import { declaredRecordKinds, readDeclaration, readerVersion, WorkspaceReadError } from "./declaration";
36
40
  import { declaredKindFile } from "./declared-kinds";
37
- import { entityDecisions, runCommitJoins } from "./intent-joins";
41
+ import { entityDecisions, runCommitJoins, type IntentCommit } from "./intent-joins";
38
42
  import {
39
43
  addingCommit,
40
44
  closingCommit,
@@ -53,7 +57,8 @@ import { constraintCovers, isWorkspacePath, memberHolding } from "./record-asset
53
57
  import type { DecidedIn } from "./record-decided";
54
58
  import { RecordReadError } from "./records";
55
59
  import type { RecordView } from "./records-cli";
56
- import { runsForCommits, type RunRef } from "./runs";
60
+ import { addSquashRuns, runsForCommits, type RunRef } from "./runs";
61
+ import { followSquashes, type Forge, type SquashFollow } from "./squash";
57
62
  import { readTrailerJoins, type CommitTrailerJoins } from "./trailer-joins";
58
63
  import { joinPath } from "./tree";
59
64
  import { locateWorkspace } from "./which-chant";
@@ -98,6 +103,16 @@ export interface RecordCommit {
98
103
  joins: CommitTrailerJoins;
99
104
  /** The agent runs that made the commit (#3033): the one its Chant-Run names, and any whose record lists it. */
100
105
  runs: RunRef[];
106
+ /** Only when the walk follows squashes (#3035) and the commit squashes a pull request: its original commits and what their trailers join. */
107
+ squash?: {
108
+ pullRequest: number;
109
+ forge: Forge | null;
110
+ ref: string | null;
111
+ head: string | null;
112
+ fetched: boolean;
113
+ followed: boolean;
114
+ commits: { sha: string; subject: string; author: { name: string; email: string }; date: string; trailers: Record<string, string[]>; joins: CommitTrailerJoins }[];
115
+ };
101
116
  /** The record's constrains entries whose history listed the commit. */
102
117
  entries: string[];
103
118
  /** The files the commit changed in the record's region, from the workspace root. Empty when git lists none, as for a merge that changed nothing against its first parent there. */
@@ -146,6 +161,8 @@ export interface IntentRecordQuery {
146
161
  record: string;
147
162
  at?: string;
148
163
  kinds?: string[];
164
+ /** Follow squash merges to their pull requests' original commits, fetching a missing pull request ref from `origin` (#3035). */
165
+ followSquash?: boolean;
149
166
  }
150
167
 
151
168
  export interface IntentRecordResult {
@@ -248,6 +265,13 @@ async function walk(query: IntentRecordQuery, head: Head): Promise<IntentRecordR
248
265
  const own = decisionWindow(hit.kind, view);
249
266
  const shas = [...listed.keys()].filter((sha) => inWindow(own, sha));
250
267
  const details = commitDetails(top, shas);
268
+ // Squash merges, followed to the pull request's original commits when asked (#3035).
269
+ const follows = query.followSquash ? followSquashes(top, [...details.values()], { fetchMissing: true }) : new Map<string, SquashFollow>();
270
+ for (const f of follows.values()) {
271
+ if (f.problem) reasons.push({ code: "squash-unfollowed", message: `${f.sha.slice(0, 8)} squashes pull request #${f.pullRequest}, and ${f.problem}, so its original commits are not followed` });
272
+ }
273
+ const originalDetails = commitDetails(top, [...new Set([...follows.values()].flatMap((f) => f.commits))]);
274
+ const originalsOf = (sha: string): IntentCommit[] => (follows.get(sha)?.commits ?? []).map((o) => originalDetails.get(o)).filter((x): x is IntentCommit => x !== undefined);
251
275
  // Newest first by commit date, as git log lists them.
252
276
  const order = (shas.length > 0 ? tryGit(top, ["rev-list", "--no-walk=sorted", "--stdin"], `${shas.join("\n")}\n`) : "")?.split("\n").map((s) => s.trim()).filter(Boolean) ?? shas;
253
277
  shas.sort((a, b) => order.indexOf(a) - order.indexOf(b));
@@ -307,14 +331,15 @@ async function walk(query: IntentRecordQuery, head: Head): Promise<IntentRecordR
307
331
  const constrainedContracts = constrains.filter((c) => c.granularity === "contract").map((c) => c.entry);
308
332
  const commits: RecordCommit[] = [];
309
333
  // chant's own trailers (#3149): the record named outright, or a work item that implements it.
310
- const trailerJoins = readTrailerJoins(top, [...details.values()]);
334
+ const trailerJoins = readTrailerJoins(top, [...details.values(), ...originalDetails.values()]);
311
335
  const recordKinds = new Map(kinds.filter((k) => k.records).map((k) => [k.records!.loaded.kind.name, k]));
312
336
  const workKinds = kinds.filter((k) => k.records?.loaded.kind.work);
313
337
  const runJoins = await runsForCommits(
314
338
  top,
315
339
  located.rootOnDisk,
316
- shas.map((sha) => ({ sha, run: trailerJoins.get(sha)?.joins.run ?? null })),
340
+ [...new Set([...shas, ...originalDetails.keys()])].map((sha) => ({ sha, run: trailerJoins.get(sha)?.joins.run ?? null })),
317
341
  );
342
+ addSquashRuns(runJoins.refs, follows);
318
343
  const workImplements = (k: LoadedKind, id: string): boolean | undefined => {
319
344
  const v = k.records!.views.find((x) => x.id === id);
320
345
  return v ? idList(v.data, k.records!.loaded.kind.work!.implements).includes(view.id!) : undefined;
@@ -325,34 +350,42 @@ async function walk(query: IntentRecordQuery, head: Head): Promise<IntentRecordR
325
350
  let unit: string | null = null;
326
351
  let isOwn = false;
327
352
  const joins = trailerJoins.get(sha)!.joins;
328
- for (const r of joins.records) {
329
- const k = recordKinds.get(r.kind);
330
- if (!k || !k.records!.views.some((v) => v.id === r.id)) continue;
331
- r.node = `record:${r.kind}/${r.id}`;
332
- if (k.records!.loaded.kind.work ? workImplements(k, r.id) : r.kind === kind.name && r.id === view.id) isOwn = true;
333
- }
334
- const item = joins.lease?.item;
335
- if (item) {
336
- const holding = workKinds.filter((k) => k.records!.views.some((v) => v.id === item));
337
- if (holding.length === 1 && workImplements(holding[0], item)) isOwn = true;
353
+ const originals = originalsOf(sha);
354
+ // The commit's own trailers, and a followed squash's original commits' (#3035).
355
+ for (const j of [joins, ...originals.map((o) => trailerJoins.get(o.sha)!.joins)]) {
356
+ for (const r of j.records) {
357
+ const k = recordKinds.get(r.kind);
358
+ if (!k || !k.records!.views.some((v) => v.id === r.id)) continue;
359
+ r.node = `record:${r.kind}/${r.id}`;
360
+ if (k.records!.loaded.kind.work ? workImplements(k, r.id) : r.kind === kind.name && r.id === view.id) isOwn = true;
361
+ }
362
+ const item = j.lease?.item;
363
+ if (item) {
364
+ const holding = workKinds.filter((k) => k.records!.views.some((v) => v.id === item));
365
+ if (holding.length === 1 && workImplements(holding[0], item)) isOwn = true;
366
+ }
338
367
  }
368
+ const subjects: IntentCommit[] = [c, ...originals];
339
369
  for (const k of kinds) {
340
370
  if (!k.joins) continue;
341
- let result;
342
- try {
343
- result = await runCommitJoins(k.joins, c, { read: readAt, list: listAt, at: located.at }, k.name);
344
- } catch (err) {
345
- const message = `${k.display}: commitJoins failed for ${sha.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`;
346
- if (!failedPlugins.has(message)) reasons.push({ code: "intent-plugin-failed", message });
347
- failedPlugins.add(message);
348
- continue;
371
+ for (const subject of subjects) {
372
+ let result;
373
+ try {
374
+ result = await runCommitJoins(k.joins, subject, { read: readAt, list: listAt, at: located.at }, k.name);
375
+ } catch (err) {
376
+ const message = `${k.display}: commitJoins failed for ${subject.sha.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`;
377
+ if (!failedPlugins.has(message)) reasons.push({ code: "intent-plugin-failed", message });
378
+ failedPlugins.add(message);
379
+ continue;
380
+ }
381
+ if (!result.unit) continue;
382
+ unit ??= result.unit.id;
383
+ const named = [...entityDecisions(result.unit), ...entityDecisions(result.contract)];
384
+ if (named.some((x) => x === view.id || x === `${kind.name}/${view.id}`)) isOwn = true;
385
+ if (result.contract && constrainedContracts.includes(result.contract.id)) isOwn = true;
349
386
  }
350
- if (!result.unit) continue;
351
- unit ??= result.unit.id;
352
- const named = [...entityDecisions(result.unit), ...entityDecisions(result.contract)];
353
- if (named.some((x) => x === view.id || x === `${kind.name}/${view.id}`)) isOwn = true;
354
- if (result.contract && constrainedContracts.includes(result.contract.id)) isOwn = true;
355
387
  }
388
+ const f = follows.get(sha);
356
389
  const changed = (files.get(sha) ?? []).map((f) => joinPath(workspacePrefix, f));
357
390
  const covers = (paths: string[]) => paths.some((p) => changed.some((f) => constraintCovers(p, f)));
358
391
  const workedBy = works.filter((w) => inWindow(w.window, sha) && (w.implementsRecord || covers(w.paths))).map((w) => w.ref);
@@ -368,6 +401,19 @@ async function walk(query: IntentRecordQuery, head: Head): Promise<IntentRecordR
368
401
  unit,
369
402
  joins,
370
403
  runs: runJoins.refs.get(sha) ?? [],
404
+ ...(f
405
+ ? {
406
+ squash: {
407
+ pullRequest: f.pullRequest,
408
+ forge: f.forge,
409
+ ref: f.ref,
410
+ head: f.head,
411
+ fetched: f.fetched,
412
+ followed: f.followed,
413
+ commits: originals.map((o) => ({ sha: o.sha, subject: o.subject, author: o.author, date: o.date, trailers: o.trailers, joins: trailerJoins.get(o.sha)!.joins })),
414
+ },
415
+ }
416
+ : {}),
371
417
  entries: listed.get(sha)!,
372
418
  files: files.get(sha) ?? [],
373
419
  bucket: isOwn ? "own" : workedBy.length > 0 ? "worked" : alsoWithin.length > 0 ? "within-other" : "unexplained",
@@ -0,0 +1,76 @@
1
+ /**
2
+ * From a commit back to the review that asked for it (#3154, E21 in
3
+ * arugula-salad/hud#823): a review session's comments[].follow_ups name a
4
+ * work item, a builder's commit carries that item with `Chant-Record:
5
+ * work:<id>`, and `graph --intent` lists the session on the work node.
6
+ *
7
+ * The workspace is a copy of the reference workspace, where W-001
8
+ * constrains design/screens/home.json, with one open review session added.
9
+ */
10
+
11
+ import { cpSync, writeFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { afterAll, describe, expect, test } from "vitest";
14
+ import { cleanScratch, contract, git, REPO, scratchDir } from "./__fixtures__/contract-repo";
15
+ import { intentGraph, type CommitNode, type WorkNode } from "./intent";
16
+ import intentSchema from "./intent.schema.json";
17
+
18
+ afterAll(cleanScratch);
19
+
20
+ const intent = contract(intentSchema);
21
+
22
+ const SESSION = `---
23
+ ${JSON.stringify(
24
+ {
25
+ schema: 1,
26
+ id: "S-0002",
27
+ title: "Review of the home page",
28
+ state: "open",
29
+ agenda: [{ text: "The home page as built" }],
30
+ attendance: [{ principal: "alice", class: "person" }],
31
+ opened: "2026-10-03T10:00:00Z",
32
+ closed: null,
33
+ verdicts: [],
34
+ comments: [
35
+ { text: "The header is cut off." },
36
+ { text: "The hero image should follow the spec.", follow_ups: ["work:W-001"] },
37
+ { text: "Same for the footer.", follow_ups: ["work:W-001", "work:W-404"] },
38
+ ],
39
+ },
40
+ null,
41
+ 2,
42
+ )}
43
+ ---
44
+
45
+ # Review of the home page
46
+ `;
47
+
48
+ function workspace(): string {
49
+ const root = scratchDir("chant-intent-reviews-");
50
+ cpSync(join(REPO, "reference-workspace"), root, { recursive: true, filter: (src) => !/[\\/](node_modules|dist)$/.test(src) });
51
+ writeFileSync(join(root, "design", "sessions", "S-0002-review-of-the-home-page.md"), SESSION);
52
+ const g = (...a: string[]) => git(root, ...a);
53
+ g("init", "-q");
54
+ g("add", "-A");
55
+ g("commit", "-q", "-m", "the workspace");
56
+ g("branch", "-M", "main");
57
+ writeFileSync(join(root, "design", "screens", "home.json"), `${JSON.stringify({ regions: ["header", "hero", "footer"] }, null, 2)}\n`);
58
+ g("add", "-A");
59
+ g("commit", "-q", "-m", "the hero follows the spec", "-m", "Chant-Record: work:W-001");
60
+ return root;
61
+ }
62
+
63
+ describe("graph --intent joins a work item back to the review that asked for it (#3154, E21)", () => {
64
+ test("the work node lists the session and the comments whose follow_ups name it, and the builder's commit carries the item", async () => {
65
+ const root = workspace();
66
+ const head = git(root, "rev-parse", "HEAD");
67
+ const { doc } = await intentGraph({ cwd: root, region: "design/screens/home.json" });
68
+ intent.expectValid(doc);
69
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
70
+ const w = doc.nodes.find((n) => n.id === "record:work/W-001") as WorkNode;
71
+ expect(w.reviews).toEqual([{ id: "record:session/S-0002", recordKind: "session", record: "S-0002", path: "design/sessions/S-0002-review-of-the-home-page.md", state: "open", comments: [2, 3] }]);
72
+ const c = doc.nodes.find((n) => n.id === `commit:${head}`) as CommitNode;
73
+ expect(c).toBeDefined();
74
+ expect(doc.edges).toContainEqual({ kind: "carries", from: c.id, to: w.id });
75
+ });
76
+ });