@intentius/chant 0.102.0 → 0.104.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/dist/audit/core.d.ts +2 -0
  2. package/dist/audit/core.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/change-set.d.ts +232 -0
  5. package/dist/change-set.d.ts.map +1 -0
  6. package/dist/cli/commands/audit.d.ts +7 -0
  7. package/dist/cli/commands/audit.d.ts.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/handlers/build.d.ts.map +1 -1
  10. package/dist/cli/handlers/change-set.d.ts +11 -0
  11. package/dist/cli/handlers/change-set.d.ts.map +1 -0
  12. package/dist/cli/handlers/fan-out.d.ts +33 -0
  13. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  14. package/dist/cli/handlers/operator.d.ts +7 -0
  15. package/dist/cli/handlers/operator.d.ts.map +1 -1
  16. package/dist/cli/handlers/pr.d.ts +31 -0
  17. package/dist/cli/handlers/pr.d.ts.map +1 -0
  18. package/dist/cli/handlers/run.d.ts.map +1 -1
  19. package/dist/cli/main.d.ts.map +1 -1
  20. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  21. package/dist/cli/mcp/workspace-tools.d.ts +3 -2
  22. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  23. package/dist/cli/registry.d.ts +63 -1
  24. package/dist/cli/registry.d.ts.map +1 -1
  25. package/dist/components/auto-release.d.ts +16 -0
  26. package/dist/components/auto-release.d.ts.map +1 -1
  27. package/dist/components/capability.d.ts +53 -0
  28. package/dist/components/capability.d.ts.map +1 -1
  29. package/dist/components/changed-files.d.ts +31 -0
  30. package/dist/components/changed-files.d.ts.map +1 -0
  31. package/dist/components/cli-support.d.ts +9 -1
  32. package/dist/components/cli-support.d.ts.map +1 -1
  33. package/dist/components/component.d.ts +24 -0
  34. package/dist/components/component.d.ts.map +1 -1
  35. package/dist/components/deploy-units.d.ts.map +1 -1
  36. package/dist/components/driver.d.ts +13 -0
  37. package/dist/components/driver.d.ts.map +1 -1
  38. package/dist/components/fan-out-output.d.ts +6 -0
  39. package/dist/components/fan-out-output.d.ts.map +1 -1
  40. package/dist/components/fan-out-record.d.ts +41 -0
  41. package/dist/components/fan-out-record.d.ts.map +1 -0
  42. package/dist/components/fan-out-run.d.ts +65 -0
  43. package/dist/components/fan-out-run.d.ts.map +1 -1
  44. package/dist/components/fan-out-support.d.ts +2 -0
  45. package/dist/components/fan-out-support.d.ts.map +1 -1
  46. package/dist/components/fan-out.d.ts +6 -0
  47. package/dist/components/fan-out.d.ts.map +1 -1
  48. package/dist/components/gated-wave-pipeline.d.ts +50 -0
  49. package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
  50. package/dist/components/index.d.ts +1 -1
  51. package/dist/components/index.d.ts.map +1 -1
  52. package/dist/components/layers.d.ts +25 -0
  53. package/dist/components/layers.d.ts.map +1 -0
  54. package/dist/components/pr-pipeline.d.ts +61 -0
  55. package/dist/components/pr-pipeline.d.ts.map +1 -0
  56. package/dist/components/pr-run.d.ts +119 -0
  57. package/dist/components/pr-run.d.ts.map +1 -0
  58. package/dist/components/promote.d.ts.map +1 -1
  59. package/dist/components/verbs/shell.d.ts.map +1 -1
  60. package/dist/components/wave-plan.d.ts +49 -0
  61. package/dist/components/wave-plan.d.ts.map +1 -0
  62. package/dist/gated-waves.d.ts +147 -0
  63. package/dist/gated-waves.d.ts.map +1 -0
  64. package/dist/index.d.ts +1 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/lexicon.d.ts +48 -0
  67. package/dist/lexicon.d.ts.map +1 -1
  68. package/dist/lifecycle/gate-ledger.d.ts +7 -0
  69. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  70. package/dist/lifecycle/git.d.ts +46 -1
  71. package/dist/lifecycle/git.d.ts.map +1 -1
  72. package/dist/lint/post-synth.d.ts +18 -0
  73. package/dist/lint/post-synth.d.ts.map +1 -1
  74. package/dist/op/activities/activity-contracts.d.ts +126 -0
  75. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  76. package/dist/op/activities/change-set.d.ts +62 -0
  77. package/dist/op/activities/change-set.d.ts.map +1 -0
  78. package/dist/op/activities/decide.d.ts +2 -0
  79. package/dist/op/activities/decide.d.ts.map +1 -1
  80. package/dist/op/activities/factory.d.ts +137 -0
  81. package/dist/op/activities/factory.d.ts.map +1 -0
  82. package/dist/op/activities/index.d.ts +4 -0
  83. package/dist/op/activities/index.d.ts.map +1 -1
  84. package/dist/op/activity-contract.d.ts.map +1 -1
  85. package/dist/op/builders.d.ts +11 -0
  86. package/dist/op/builders.d.ts.map +1 -1
  87. package/dist/op/decide-backend.d.ts +5 -1
  88. package/dist/op/decide-backend.d.ts.map +1 -1
  89. package/dist/op/decide-read-inputs.d.ts +4 -2
  90. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  91. package/dist/op/factory.d.ts +83 -0
  92. package/dist/op/factory.d.ts.map +1 -0
  93. package/dist/op/index.d.ts +4 -1
  94. package/dist/op/index.d.ts.map +1 -1
  95. package/dist/op/op-ir.d.ts +2 -0
  96. package/dist/op/op-ir.d.ts.map +1 -1
  97. package/dist/op/op-verb-class.d.ts.map +1 -1
  98. package/dist/op/types.d.ts +8 -0
  99. package/dist/op/types.d.ts.map +1 -1
  100. package/dist/op/work-lease-run.d.ts.map +1 -1
  101. package/dist/plan-summary.d.ts +207 -0
  102. package/dist/plan-summary.d.ts.map +1 -0
  103. package/dist/pr-forge.d.ts +105 -0
  104. package/dist/pr-forge.d.ts.map +1 -0
  105. package/dist/pr-loop.d.ts +178 -0
  106. package/dist/pr-loop.d.ts.map +1 -0
  107. package/dist/telemetry-attribution.d.ts +31 -0
  108. package/dist/telemetry-attribution.d.ts.map +1 -1
  109. package/dist/terraform/hcl2json.d.ts +45 -0
  110. package/dist/terraform/hcl2json.d.ts.map +1 -0
  111. package/dist/terraform/parse.d.ts +4 -40
  112. package/dist/terraform/parse.d.ts.map +1 -1
  113. package/dist/vendor/js-yaml-load.d.ts +56 -0
  114. package/dist/vendor/js-yaml-load.d.ts.map +1 -0
  115. package/dist/workspace/box-cli.d.ts +12 -0
  116. package/dist/workspace/box-cli.d.ts.map +1 -0
  117. package/dist/workspace/box-factory.d.ts +27 -3
  118. package/dist/workspace/box-factory.d.ts.map +1 -1
  119. package/dist/workspace/box-listing.d.ts +85 -0
  120. package/dist/workspace/box-listing.d.ts.map +1 -0
  121. package/dist/workspace/box-publish.d.ts +147 -0
  122. package/dist/workspace/box-publish.d.ts.map +1 -0
  123. package/dist/workspace/broker-protocol.d.ts +190 -0
  124. package/dist/workspace/broker-protocol.d.ts.map +1 -0
  125. package/dist/workspace/checks.d.ts.map +1 -1
  126. package/dist/workspace/composites.d.ts +2 -2
  127. package/dist/workspace/composites.d.ts.map +1 -1
  128. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
  129. package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
  130. package/dist/workspace/conformance/broker.d.ts +202 -0
  131. package/dist/workspace/conformance/broker.d.ts.map +1 -0
  132. package/dist/workspace/conformance/index.d.ts +2 -1
  133. package/dist/workspace/conformance/index.d.ts.map +1 -1
  134. package/dist/workspace/conformance/vitest.d.ts +9 -2
  135. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  136. package/dist/workspace/conformance/writer.d.ts +108 -14
  137. package/dist/workspace/conformance/writer.d.ts.map +1 -1
  138. package/dist/workspace/decide.d.ts +126 -12
  139. package/dist/workspace/decide.d.ts.map +1 -1
  140. package/dist/workspace/decision-points.schema.json +11 -4
  141. package/dist/workspace/declaration.d.ts +65 -0
  142. package/dist/workspace/declaration.d.ts.map +1 -1
  143. package/dist/workspace/declaration.schema.json +153 -4
  144. package/dist/workspace/environments.d.ts +36 -10
  145. package/dist/workspace/environments.d.ts.map +1 -1
  146. package/dist/workspace/factory-rules.d.ts +180 -0
  147. package/dist/workspace/factory-rules.d.ts.map +1 -0
  148. package/dist/workspace/init.d.ts +21 -0
  149. package/dist/workspace/init.d.ts.map +1 -1
  150. package/dist/workspace/intent-cli.d.ts +1 -1
  151. package/dist/workspace/intent-cli.d.ts.map +1 -1
  152. package/dist/workspace/intent-record.d.ts +28 -1
  153. package/dist/workspace/intent-record.d.ts.map +1 -1
  154. package/dist/workspace/intent.d.ts +102 -5
  155. package/dist/workspace/intent.d.ts.map +1 -1
  156. package/dist/workspace/json-edit.d.ts +24 -0
  157. package/dist/workspace/json-edit.d.ts.map +1 -0
  158. package/dist/workspace/jsonc.d.ts +11 -0
  159. package/dist/workspace/jsonc.d.ts.map +1 -1
  160. package/dist/workspace/kinds.d.ts +48 -0
  161. package/dist/workspace/kinds.d.ts.map +1 -1
  162. package/dist/workspace/lexicon-symbols.d.ts +29 -0
  163. package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
  164. package/dist/workspace/lineage-upgrade.d.ts +31 -2
  165. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  166. package/dist/workspace/point-answer.schema.json +42 -2
  167. package/dist/workspace/points-cli.d.ts +10 -3
  168. package/dist/workspace/points-cli.d.ts.map +1 -1
  169. package/dist/workspace/points.d.ts +66 -5
  170. package/dist/workspace/points.d.ts.map +1 -1
  171. package/dist/workspace/reason-codes.d.ts +33 -4
  172. package/dist/workspace/reason-codes.d.ts.map +1 -1
  173. package/dist/workspace/record-sessions.d.ts +5 -7
  174. package/dist/workspace/record-sessions.d.ts.map +1 -1
  175. package/dist/workspace/records-cli.d.ts +7 -0
  176. package/dist/workspace/records-cli.d.ts.map +1 -1
  177. package/dist/workspace/records-close.d.ts +7 -3
  178. package/dist/workspace/records-close.d.ts.map +1 -1
  179. package/dist/workspace/records-write.d.ts +66 -6
  180. package/dist/workspace/records-write.d.ts.map +1 -1
  181. package/dist/workspace/records.d.ts +28 -9
  182. package/dist/workspace/records.d.ts.map +1 -1
  183. package/dist/workspace/runs-cli.d.ts +138 -6
  184. package/dist/workspace/runs-cli.d.ts.map +1 -1
  185. package/dist/workspace/runs.d.ts +132 -11
  186. package/dist/workspace/runs.d.ts.map +1 -1
  187. package/dist/workspace/session-kinds.d.ts +1 -1
  188. package/dist/workspace/session-kinds.d.ts.map +1 -1
  189. package/dist/workspace/squash.d.ts +80 -0
  190. package/dist/workspace/squash.d.ts.map +1 -0
  191. package/dist/workspace/status-gates.d.ts +2 -0
  192. package/dist/workspace/status-gates.d.ts.map +1 -1
  193. package/dist/workspace/status.d.ts +54 -0
  194. package/dist/workspace/status.d.ts.map +1 -1
  195. package/dist/workspace/symbols.d.ts +16 -5
  196. package/dist/workspace/symbols.d.ts.map +1 -1
  197. package/dist/workspace/trust/run-commits.d.ts +46 -0
  198. package/dist/workspace/trust/run-commits.d.ts.map +1 -0
  199. package/dist/workspace/trust/run-statement.d.ts +128 -0
  200. package/dist/workspace/trust/run-statement.d.ts.map +1 -0
  201. package/dist/workspace/trust/seal.d.ts +6 -1
  202. package/dist/workspace/trust/seal.d.ts.map +1 -1
  203. package/dist/workspace/trust/verify-cli.d.ts +1 -1
  204. package/dist/workspace/trust/verify.d.ts +16 -2
  205. package/dist/workspace/trust/verify.d.ts.map +1 -1
  206. package/dist/workspace/wip-cli.d.ts +21 -0
  207. package/dist/workspace/wip-cli.d.ts.map +1 -0
  208. package/dist/workspace/wip.d.ts +268 -0
  209. package/dist/workspace/wip.d.ts.map +1 -0
  210. package/dist/workspace/work-cli.d.ts.map +1 -1
  211. package/dist/workspace/work-evidence.d.ts +1 -1
  212. package/dist/workspace/work-evidence.d.ts.map +1 -1
  213. package/dist/workspace/work-size.d.ts +55 -0
  214. package/dist/workspace/work-size.d.ts.map +1 -0
  215. package/dist/workspace/workspace-kinds.schema.json +122 -0
  216. package/dist/workspace/write-lock-cli.d.ts +60 -0
  217. package/dist/workspace/write-lock-cli.d.ts.map +1 -0
  218. package/dist/workspace/write-lock.d.ts +152 -0
  219. package/dist/workspace/write-lock.d.ts.map +1 -0
  220. package/dist/workspace/write-scope.d.ts +6 -2
  221. package/dist/workspace/write-scope.d.ts.map +1 -1
  222. package/dist/yaml.d.ts +1 -1
  223. package/dist/yaml.d.ts.map +1 -1
  224. package/package.json +6 -1
  225. package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
  226. package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
  227. package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
  228. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
  229. package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
  230. package/src/__fixtures__/change-set/warden.plan.json +44 -0
  231. package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
  232. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
  233. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
  234. package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
  235. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
  236. package/src/audit/core.test.ts +15 -0
  237. package/src/audit/core.ts +8 -2
  238. package/src/audit/discover.test.ts +11 -0
  239. package/src/audit/discover.ts +7 -2
  240. package/src/change-set-bundle.test.ts +57 -0
  241. package/src/change-set.test.ts +215 -0
  242. package/src/change-set.ts +445 -0
  243. package/src/cli/commands/audit-opt-in.test.ts +54 -0
  244. package/src/cli/commands/audit.ts +29 -2
  245. package/src/cli/commands/build.test.ts +19 -0
  246. package/src/cli/commands/build.ts +3 -2
  247. package/src/cli/handlers/build.ts +13 -1
  248. package/src/cli/handlers/change-set.test.ts +56 -0
  249. package/src/cli/handlers/change-set.ts +44 -0
  250. package/src/cli/handlers/fan-out.test.ts +221 -2
  251. package/src/cli/handlers/fan-out.ts +165 -75
  252. package/src/cli/handlers/graph.ts +3 -0
  253. package/src/cli/handlers/operator.test.ts +21 -0
  254. package/src/cli/handlers/operator.ts +34 -3
  255. package/src/cli/handlers/pr.ts +373 -0
  256. package/src/cli/handlers/run.ts +1 -0
  257. package/src/cli/main.test.ts +20 -0
  258. package/src/cli/main.ts +185 -23
  259. package/src/cli/mcp/never-over-mcp.test.ts +101 -0
  260. package/src/cli/mcp/op-tools.ts +24 -2
  261. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  262. package/src/cli/mcp/workspace-tools.ts +34 -6
  263. package/src/cli/registry.ts +63 -1
  264. package/src/components/auto-release.ts +1 -1
  265. package/src/components/capability.ts +55 -0
  266. package/src/components/changed-files.ts +66 -0
  267. package/src/components/cli-support.ts +28 -5
  268. package/src/components/component.schema.json +17 -0
  269. package/src/components/component.ts +25 -0
  270. package/src/components/deploy-units.ts +3 -0
  271. package/src/components/driver.test.ts +52 -0
  272. package/src/components/driver.ts +31 -13
  273. package/src/components/fan-out-output.ts +42 -1
  274. package/src/components/fan-out-record.ts +69 -0
  275. package/src/components/fan-out-run.test.ts +17 -0
  276. package/src/components/fan-out-run.ts +304 -1
  277. package/src/components/fan-out-support.ts +3 -0
  278. package/src/components/fan-out-waves.test.ts +375 -0
  279. package/src/components/fan-out.ts +18 -17
  280. package/src/components/gated-wave-pipeline.ts +82 -0
  281. package/src/components/index.ts +1 -0
  282. package/src/components/layers.ts +30 -0
  283. package/src/components/pr-pipeline.ts +91 -0
  284. package/src/components/pr-run.test.ts +205 -0
  285. package/src/components/pr-run.ts +280 -0
  286. package/src/components/promote.ts +5 -0
  287. package/src/components/verbs/shell.test.ts +11 -0
  288. package/src/components/verbs/shell.ts +7 -3
  289. package/src/components/wave-plan.ts +124 -0
  290. package/src/discovery/sandbox/bundle.ts +1 -1
  291. package/src/gated-waves-bundle.test.ts +62 -0
  292. package/src/gated-waves.test.ts +118 -0
  293. package/src/gated-waves.ts +240 -0
  294. package/src/index.ts +1 -0
  295. package/src/lexicon.ts +51 -0
  296. package/src/lifecycle/gate-ledger.ts +7 -0
  297. package/src/lifecycle/git.ts +102 -16
  298. package/src/lint/post-synth.test.ts +19 -0
  299. package/src/lint/post-synth.ts +19 -1
  300. package/src/meta/declared-imports.test.ts +1 -1
  301. package/src/op/activities/activity-contracts.ts +84 -0
  302. package/src/op/activities/change-set.ts +114 -0
  303. package/src/op/activities/decide.ts +3 -0
  304. package/src/op/activities/factory.ts +675 -0
  305. package/src/op/activities/index.ts +6 -0
  306. package/src/op/activity-contract.test.ts +21 -1
  307. package/src/op/activity-contract.ts +19 -9
  308. package/src/op/builders.ts +23 -0
  309. package/src/op/decide-backend.ts +5 -1
  310. package/src/op/decide-read-inputs.ts +33 -5
  311. package/src/op/factory.e2e.test.ts +232 -0
  312. package/src/op/factory.ts +118 -0
  313. package/src/op/index.ts +9 -1
  314. package/src/op/op-ir.test.ts +9 -0
  315. package/src/op/op-ir.ts +4 -0
  316. package/src/op/op-verb-class.ts +3 -0
  317. package/src/op/types.ts +8 -0
  318. package/src/op/work-lease-run.ts +3 -0
  319. package/src/plan-summary.test.ts +263 -0
  320. package/src/plan-summary.ts +845 -0
  321. package/src/pr-forge.test.ts +237 -0
  322. package/src/pr-forge.ts +332 -0
  323. package/src/pr-loop-bundle.test.ts +46 -0
  324. package/src/pr-loop.test.ts +171 -0
  325. package/src/pr-loop.ts +324 -0
  326. package/src/telemetry-attribution.test.ts +23 -1
  327. package/src/telemetry-attribution.ts +51 -0
  328. package/src/terraform/hcl2json.test.ts +21 -0
  329. package/src/terraform/hcl2json.ts +81 -0
  330. package/src/terraform/parse.ts +11 -74
  331. package/src/vendor/js-yaml-load.test.ts +115 -0
  332. package/src/vendor/js-yaml-load.ts +1939 -0
  333. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  334. package/src/workspace/app-kind.test.ts +196 -0
  335. package/src/workspace/box-cli.ts +113 -0
  336. package/src/workspace/box-factory.test.ts +4 -1
  337. package/src/workspace/box-factory.ts +37 -4
  338. package/src/workspace/box-listing-write.schema.json +204 -0
  339. package/src/workspace/box-listing.test.ts +234 -0
  340. package/src/workspace/box-listing.ts +324 -0
  341. package/src/workspace/box-publish.schema.json +498 -0
  342. package/src/workspace/box-publish.test.ts +204 -0
  343. package/src/workspace/box-publish.ts +420 -0
  344. package/src/workspace/box-ship.test.ts +96 -0
  345. package/src/workspace/broker-protocol.schema.json +186 -0
  346. package/src/workspace/broker-protocol.test.ts +122 -0
  347. package/src/workspace/broker-protocol.ts +317 -0
  348. package/src/workspace/builder-tiers.test.ts +146 -0
  349. package/src/workspace/change-set.schema.json +460 -0
  350. package/src/workspace/chant-migrations.e2e.test.ts +17 -5
  351. package/src/workspace/checks/boxes.test.ts +3 -0
  352. package/src/workspace/checks.ts +10 -3
  353. package/src/workspace/component-environments.test.ts +105 -0
  354. package/src/workspace/composites.schema.json +23 -4
  355. package/src/workspace/composites.test.ts +8 -8
  356. package/src/workspace/composites.ts +13 -2
  357. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
  358. package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
  359. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
  360. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
  361. package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
  362. package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
  363. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
  364. package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
  365. package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
  366. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
  367. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
  368. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
  369. package/src/workspace/conformance/broker.test.ts +122 -0
  370. package/src/workspace/conformance/broker.ts +792 -0
  371. package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
  372. package/src/workspace/conformance/index.mjs +22 -1
  373. package/src/workspace/conformance/index.ts +5 -1
  374. package/src/workspace/conformance/vitest.ts +33 -1
  375. package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
  376. package/src/workspace/conformance/writer.test.ts +32 -2
  377. package/src/workspace/conformance/writer.ts +339 -28
  378. package/src/workspace/decide.test.ts +208 -4
  379. package/src/workspace/decide.ts +427 -73
  380. package/src/workspace/decision-points.schema.json +11 -4
  381. package/src/workspace/declaration.schema.json +153 -4
  382. package/src/workspace/declaration.ts +141 -0
  383. package/src/workspace/environments.ts +66 -14
  384. package/src/workspace/factory-rules.test.ts +62 -0
  385. package/src/workspace/factory-rules.ts +218 -0
  386. package/src/workspace/graph-cli.ts +2 -2
  387. package/src/workspace/graph.schema.json +67 -0
  388. package/src/workspace/identity.test.ts +23 -0
  389. package/src/workspace/init.ts +82 -2
  390. package/src/workspace/intent-cli.ts +26 -9
  391. package/src/workspace/intent-patch-id.test.ts +162 -0
  392. package/src/workspace/intent-record.schema.json +130 -3
  393. package/src/workspace/intent-record.ts +74 -28
  394. package/src/workspace/intent-reviews.test.ts +76 -0
  395. package/src/workspace/intent-squash.test.ts +213 -0
  396. package/src/workspace/intent-why.test.ts +5 -5
  397. package/src/workspace/intent.schema.json +234 -4
  398. package/src/workspace/intent.ts +260 -66
  399. package/src/workspace/json-edit.test.ts +121 -0
  400. package/src/workspace/json-edit.ts +142 -0
  401. package/src/workspace/jsonc.ts +20 -0
  402. package/src/workspace/kinds.ts +157 -2
  403. package/src/workspace/lexicon-symbols.test.ts +148 -0
  404. package/src/workspace/lexicon-symbols.ts +125 -0
  405. package/src/workspace/lineage-upgrade.test.ts +63 -0
  406. package/src/workspace/lineage-upgrade.ts +146 -5
  407. package/src/workspace/plan-summary.schema.json +155 -0
  408. package/src/workspace/point-answer.schema.json +42 -2
  409. package/src/workspace/points-cli.ts +42 -16
  410. package/src/workspace/points-write.schema.json +162 -4
  411. package/src/workspace/points.schema.json +157 -2
  412. package/src/workspace/points.test.ts +13 -1
  413. package/src/workspace/points.ts +118 -12
  414. package/src/workspace/pr-report.schema.json +95 -0
  415. package/src/workspace/profiles.test.ts +159 -0
  416. package/src/workspace/read-contract.test.ts +2 -2
  417. package/src/workspace/reason-codes.test.ts +13 -1
  418. package/src/workspace/reason-codes.ts +38 -4
  419. package/src/workspace/record-sessions.ts +9 -8
  420. package/src/workspace/records-amend.schema.json +78 -0
  421. package/src/workspace/records-cli.ts +20 -6
  422. package/src/workspace/records-close.schema.json +78 -0
  423. package/src/workspace/records-close.ts +31 -7
  424. package/src/workspace/records-concurrent.e2e.test.ts +135 -0
  425. package/src/workspace/records-concurrent.test.ts +145 -0
  426. package/src/workspace/records-formats.test.ts +46 -10
  427. package/src/workspace/records-new.schema.json +7 -0
  428. package/src/workspace/records-review.schema.json +78 -0
  429. package/src/workspace/records-sessions-write.test.ts +69 -0
  430. package/src/workspace/records-sessions.test.ts +22 -0
  431. package/src/workspace/records-write.ts +192 -25
  432. package/src/workspace/records.schema.json +16 -0
  433. package/src/workspace/records.ts +50 -9
  434. package/src/workspace/reference-kinds/app/package.json +10 -0
  435. package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
  436. package/src/workspace/run-statement.schema.json +622 -0
  437. package/src/workspace/runs-cli.ts +343 -14
  438. package/src/workspace/runs-statement.test.ts +239 -0
  439. package/src/workspace/runs-write.schema.json +416 -5
  440. package/src/workspace/runs.schema.json +451 -4
  441. package/src/workspace/runs.test.ts +39 -0
  442. package/src/workspace/runs.ts +296 -35
  443. package/src/workspace/session-kinds.ts +3 -3
  444. package/src/workspace/squash.ts +198 -0
  445. package/src/workspace/status-contract.test.ts +5 -2
  446. package/src/workspace/status-gates.ts +3 -1
  447. package/src/workspace/status.schema.json +126 -3
  448. package/src/workspace/status.ts +90 -1
  449. package/src/workspace/symbols.ts +20 -9
  450. package/src/workspace/trust/run-commits.ts +115 -0
  451. package/src/workspace/trust/run-statement.ts +226 -0
  452. package/src/workspace/trust/seal.ts +8 -2
  453. package/src/workspace/trust/verify-cli.ts +6 -5
  454. package/src/workspace/trust/verify.ts +39 -6
  455. package/src/workspace/wip-cli.ts +97 -0
  456. package/src/workspace/wip-write.schema.json +504 -0
  457. package/src/workspace/wip.schema.json +334 -0
  458. package/src/workspace/wip.test.ts +294 -0
  459. package/src/workspace/wip.ts +753 -0
  460. package/src/workspace/work-cli.ts +3 -0
  461. package/src/workspace/work-evidence.schema.json +3 -0
  462. package/src/workspace/work-size.test.ts +77 -0
  463. package/src/workspace/work-size.ts +93 -0
  464. package/src/workspace/workspace-kinds.schema.json +122 -0
  465. package/src/workspace/write-lock-cli.ts +131 -0
  466. package/src/workspace/write-lock.schema.json +172 -0
  467. package/src/workspace/write-lock.test.ts +189 -0
  468. package/src/workspace/write-lock.ts +474 -0
  469. package/src/workspace/write-scope.test.ts +16 -1
  470. package/src/workspace/write-scope.ts +33 -11
  471. package/src/yaml.ts +31 -21
@@ -31,7 +31,8 @@
31
31
  * record kind the declaration names (#2680), and each one's `commitJoins`
32
32
  * runs for every commit in that order. chant emits the
33
33
  * graph and hud renders it (#2524 D8, D15). Git is read through a local
34
- * `git` subprocess only: no fetch, no network.
34
+ * `git` subprocess only: no fetch, no network, except to follow a squash
35
+ * when asked (below).
35
36
  *
36
37
  * A region can name a symbol, `path#symbol`, which `symbols.ts` resolves to
37
38
  * its current lines (#3034). For a file region the document also carries
@@ -39,6 +40,21 @@
39
40
  * behind each (narrowed by the hunks a run recorded when several share a
40
41
  * commit), and the decisions governing the region, most relevant first, with
41
42
  * `explained: false` and closed gap codes when nothing accounts for it.
43
+ *
44
+ * A commit joins an agent run by its `Chant-Run` trailer or the run's own
45
+ * list of commits, and, failing both, by content (#3036): a commit whose
46
+ * `git patch-id --stable` equals one the run recorded, as after a rebase or a
47
+ * cherry-pick that dropped the trailer. Its `made-by` edge says `patch-id`,
48
+ * and a commit joined one way whose content matches another run's recorded
49
+ * commit is `intent-commit-join-conflict`.
50
+ *
51
+ * Asked to (`followSquash`, #3035), the walk follows a squash merge to its
52
+ * pull request's original commits (`squash.ts`): the squash commit node lists
53
+ * them with their trailers, joins and signatures, carries what their trailers
54
+ * and plugin joins carry, and joins their runs, marked `squash`. That is the
55
+ * one part of the walk that may reach the network, to fetch a pull request
56
+ * ref the clone lacks; a ref that can't be read is `squash-unfollowed` in
57
+ * `reasons`, and the walk keeps its answer without it.
42
58
  */
43
59
 
44
60
  import { execFileSync } from "node:child_process";
@@ -56,8 +72,10 @@ import type { DecidedIn } from "./record-decided";
56
72
  import { importKindModule, loadRecordKind, parseFrontMatter, RecordReadError, supersedesTargets, type LoadedRecordKind } from "./records";
57
73
  import { queryRecords, type RecordView } from "./records-cli";
58
74
  import { isPluginCode, type PluginCode, type ReasonCode } from "./reason-codes";
59
- import { runsForCommits, type RunCost, type RunPin, type RunUsage, type RunView } from "./runs";
60
- import { resolveSymbol } from "./symbols";
75
+ import { addSquashRuns, RUN_JOIN_ORDER, runsForCommits, type RunCost, type RunJoin, type RunPin, type RunUsage, type RunView } from "./runs";
76
+ import { followSquashes, squashLineOrigins, type Forge, type SquashFollow } from "./squash";
77
+ import { memberDirOnDisk, memberSymbolResolvers, type SymbolPluginLoader } from "./lexicon-symbols";
78
+ import { resolveSymbol, symbolResolverFor, type SymbolResolver } from "./symbols";
61
79
  import { readTrailerJoins, type CommitTrailerJoins } from "./trailer-joins";
62
80
  import { joinPath, skippedDir, type WorkspaceTree } from "./tree";
63
81
  import { activeAttestors, type ProvenanceLevel } from "./trust/attestor";
@@ -109,6 +127,8 @@ export const INTENT_FINDING_CODES = [
109
127
  "intent-work-blocked",
110
128
  /** Commits in the region are a decision's own work while the work item implementing it is still open: the code says done and the queue says not (#2683). */
111
129
  "intent-work-open-decided-code",
130
+ /** A commit joined to an agent run by its trailer or the run's record has the patch-id of a commit another run recorded; the content join is not made (#3036). */
131
+ "intent-commit-join-conflict",
112
132
  ] as const satisfies readonly ReasonCode[];
113
133
  export type IntentFindingCode = (typeof INTENT_FINDING_CODES)[number];
114
134
 
@@ -118,6 +138,8 @@ export const INTENT_REASON_CODES = [
118
138
  "intent-history-shallow",
119
139
  /** A plugin's commitJoins failed for a commit. */
120
140
  "intent-plugin-failed",
141
+ /** A squash commit's pull request ref could not be read or fetched, so its original commits are not followed (#3035). */
142
+ "squash-unfollowed",
121
143
  ] as const satisfies readonly ReasonCode[];
122
144
  export type IntentReasonCode = (typeof INTENT_REASON_CODES)[number];
123
145
 
@@ -235,6 +257,39 @@ export interface CommitNode {
235
257
  * a kind read has is linked by a `carries` edge.
236
258
  */
237
259
  joins: CommitTrailerJoins;
260
+ /**
261
+ * Set only when the walk follows squashes (#3035) and the commit squashes
262
+ * a pull request: its original commits, read from the forge's pull request
263
+ * ref. The commit carries what they carry and joins their runs.
264
+ */
265
+ squash?: SquashNode;
266
+ }
267
+
268
+ /** A squash commit's pull request, followed to its original commits (#3035). */
269
+ export interface SquashNode {
270
+ pullRequest: number;
271
+ /** The forge `origin` points at, or null for one chant doesn't know. */
272
+ forge: Forge | null;
273
+ /** The local ref the pull request's head was read from, or null. */
274
+ ref: string | null;
275
+ /** The pull request's head commit, or null. */
276
+ head: string | null;
277
+ /** Whether this read fetched the ref. */
278
+ fetched: boolean;
279
+ /** Whether the original commits were read; when false, `squash-unfollowed` in `reasons` says why. */
280
+ followed: boolean;
281
+ /** The original commits, oldest first, each with what its own trailers join and its provenance level. */
282
+ commits: SquashedCommit[];
283
+ }
284
+
285
+ export interface SquashedCommit {
286
+ sha: string;
287
+ subject: string;
288
+ author: { name: string; email: string };
289
+ date: string;
290
+ trailers: Record<string, string[]>;
291
+ signature: { level: ProvenanceLevel; reason: string; principal?: string };
292
+ joins: CommitTrailerJoins;
238
293
  }
239
294
 
240
295
  export type CommitState = "decided" | "decided-by-window" | "undecided";
@@ -338,6 +393,18 @@ export interface WorkGapSource {
338
393
  artifact?: string;
339
394
  }
340
395
 
396
+ /** A review session that led to a work item (#3154, E21). */
397
+ export interface WorkReview {
398
+ /** The session record's node-style id, `record:<kind>/<id>`. */
399
+ id: string;
400
+ recordKind: string;
401
+ record: string;
402
+ path: string;
403
+ state: string | null;
404
+ /** The comments whose follow_ups name the item, numbered from 1; empty when only the session's own follow_ups do. */
405
+ comments: number[];
406
+ }
407
+
341
408
  export interface WorkNode {
342
409
  id: string;
343
410
  kind: "work";
@@ -368,6 +435,15 @@ export interface WorkNode {
368
435
  constrains: { entry: string; granularity: Granularity }[];
369
436
  /** The item's work warnings, as records reports them, and work-done-gap-open. */
370
437
  warnings: { code: WorkWarningCode; message: string }[];
438
+ /**
439
+ * The review sessions whose comments led to this item (#3154, E21 in
440
+ * arugula-salad/hud#823): each session record of a session kind read whose
441
+ * `comments[].follow_ups` (or its own `follow_ups`) names the item as
442
+ * `<kind>:<id>`, with the 1-based numbers of those comments. A commit that
443
+ * carries the item, by its `Chant-Record: work:<id>` trailer, joins back
444
+ * to the review through it. Empty when none does. Added within contract 1.
445
+ */
446
+ reviews?: WorkReview[];
371
447
  }
372
448
 
373
449
  /**
@@ -408,7 +484,7 @@ export type IntentEdge =
408
484
  | { kind: "pins"; from: string; to: string; pinnedSha256: string | null; pinState: PinState }
409
485
  | { kind: "touched-by"; from: string; to: string; lines: LineRange[] | null }
410
486
  | { kind: "within"; from: string; to: string; state: "decided" | "decided-by-window" | "worked" }
411
- | { kind: "made-by"; from: string; to: string; joinedBy: ("trailer" | "record")[] }
487
+ | { kind: "made-by"; from: string; to: string; joinedBy: RunJoin[]; recordedAs?: string; via?: string[] }
412
488
  | { kind: "produced-by" | "serves" | "cites-evidence" | "supersedes" | "links" | "implements" | "needs" | "addressed-by" | "carries" | "worked-on"; from: string; to: string };
413
489
 
414
490
  /** Lines of the region that one commit, and the same runs, last wrote (#3034). */
@@ -422,6 +498,16 @@ export interface WhySpan {
422
498
  runs: string[];
423
499
  /** `hunks` when a run's recorded hunks chose among the commit's runs; null when every run that made the commit is listed. */
424
500
  narrowedBy: "hunks" | null;
501
+ /** How the runs in `runs` join the commit, as their `made-by` edges say, without repeats (#3036). Empty with no runs. */
502
+ joinedBy: RunJoin[];
503
+ /**
504
+ * For lines of a followed squash (#3035): the sha of the original commit
505
+ * that last wrote them, one of the commit node's `squash.commits`, by blame
506
+ * at the pull request's head; `runs` keeps that commit's runs and the
507
+ * squash's own. Empty when the file at the head is not the squash's, so
508
+ * blame there can't say. Absent for any other commit.
509
+ */
510
+ via?: string[];
425
511
  }
426
512
 
427
513
  /** Why a decision is in `why`, most relevant first (#3034). */
@@ -454,6 +540,8 @@ export interface WhyRun {
454
540
  unit: { id: string; kind: string | null; node: string | null } | null;
455
541
  /** The decision nodes it carried out: through its work item's implements, and the decision records it names. */
456
542
  decisions: string[];
543
+ /** How it joins the commits in `commits`, as their `made-by` edges say, without repeats (#3036). */
544
+ joinedBy: RunJoin[];
457
545
  }
458
546
 
459
547
  export interface WhyGap {
@@ -510,6 +598,8 @@ export interface IntentQuery {
510
598
  /** `path`, `path:line`, `path:start-end` or `path#symbol`, or a graph node id `<member>/<id>`. */
511
599
  region: string;
512
600
  at?: string;
601
+ /** Follow squash merges to their pull requests' original commits, fetching a missing pull request ref from `origin` (#3035). */
602
+ followSquash?: boolean;
513
603
  /**
514
604
  * Kind files: record kinds, plugins with `commitJoins`, or both. Relative to
515
605
  * `cwd`. Left out, every record kind the declaration names, in its order
@@ -522,6 +612,11 @@ export interface IntentQuery {
522
612
  * --member` does.
523
613
  */
524
614
  resolveNode?: (cwd: string, at: string | undefined, member: string, id: string) => Promise<{ file: string; line: number | null } | null | undefined>;
615
+ /**
616
+ * Load a lexicon plugin by name, for the symbol resolvers a member's
617
+ * lexicons contribute (#3313). The default is the CLI's `loadPlugin`.
618
+ */
619
+ loadLexicon?: SymbolPluginLoader;
525
620
  }
526
621
 
527
622
  export interface IntentResult {
@@ -850,8 +945,20 @@ async function resolveRegion(query: IntentQuery, located: LocatedWorkspace, decl
850
945
  const file = inWorkspace(query.region.slice(0, hash));
851
946
  if (file !== undefined && name !== "") {
852
947
  if (file === "." || located.tree.stat(file) !== "file") throw new IntentError("intent-region-invalid", `${file} is a directory, and a symbol needs a file`);
853
- const r = resolveSymbol(file, located.tree.read(file), name);
854
- if (!r.ok) throw new IntentError(r.reason === "unsupported" ? "intent-symbol-unsupported" : r.reason === "ambiguous" ? "intent-symbol-ambiguous" : "intent-symbol-unknown", `${r.message}${located.tree.label}`);
948
+ // A file core's resolver doesn't read may have one from its member's lexicons (#3313).
949
+ let extra: SymbolResolver[] = [];
950
+ let why = "";
951
+ if (!symbolResolverFor(file)) {
952
+ const holder = memberHolding(file, declaration.members);
953
+ const m = holder ? declaration.members.find((x) => x.name === holder) : undefined;
954
+ if (m && m.kind === "chant") {
955
+ const got = await memberSymbolResolvers(memberDirOnDisk(located.rootOnDisk, m.dir), query.loadLexicon);
956
+ extra = got.resolvers;
957
+ if (got.problems.length > 0) why = `; member ${m.name}: ${got.problems.join("; ")}`;
958
+ }
959
+ }
960
+ const r = resolveSymbol(file, located.tree.read(file), name, extra);
961
+ if (!r.ok) throw new IntentError(r.reason === "unsupported" ? "intent-symbol-unsupported" : r.reason === "ambiguous" ? "intent-symbol-ambiguous" : "intent-symbol-unknown", `${r.message}${r.reason === "unsupported" ? why : ""}${located.tree.label}`);
855
962
  return { path: file, lines: r.declaration.lines, node: null, symbol: { name, qualified: r.declaration.qualified, kind: r.declaration.kind } };
856
963
  }
857
964
  }
@@ -954,6 +1061,13 @@ async function walk(query: IntentQuery, head: Head): Promise<IntentResult> {
954
1061
  if (!touched.some((t) => t.sha === sha)) touched.push({ sha, lines: null });
955
1062
  }
956
1063
  const details = commitDetails(top, touched.map((t) => t.sha));
1064
+ // Squash merges, followed to the pull request's original commits when asked (#3035).
1065
+ const follows = query.followSquash ? followSquashes(top, [...details.values()], { fetchMissing: true }) : new Map<string, SquashFollow>();
1066
+ for (const f of follows.values()) {
1067
+ 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` });
1068
+ }
1069
+ const originalDetails = commitDetails(top, [...new Set([...follows.values()].flatMap((f) => f.commits))]);
1070
+ const originalsOf = (sha: string): IntentCommit[] => (follows.get(sha)?.commits ?? []).map((o) => originalDetails.get(o)).filter((x): x is IntentCommit => x !== undefined);
957
1071
 
958
1072
  // Each commit's provenance, judged by the policy at base as records judges a record's (#2547).
959
1073
  const policy = policyAtBase(top, resolveBase(top));
@@ -981,54 +1095,75 @@ async function walk(query: IntentQuery, head: Head): Promise<IntentResult> {
981
1095
  }
982
1096
  const joined = new Map<string, Joined>();
983
1097
  // chant's own trailers on each commit (#3149), with the items their lease tokens name.
984
- const trailerJoins = readTrailerJoins(top, [...details.values()]);
1098
+ const trailerJoins = readTrailerJoins(top, [...details.values(), ...originalDetails.values()]);
985
1099
  const pluginFindings: { commit: string; plugin: string; finding: PluginFinding }[] = [];
1100
+ const signatureOf = (sha: string): CommitNode["signature"] => {
1101
+ if (!policy.active) return { level: "unattested", reason: policy.problems.length ? policy.problems.join("; ") : `no signers file (${policy.signersPath}) at base; attestation is off` };
1102
+ const p = commitProvenance(top, policy, sha, attestors);
1103
+ return { level: p.level, reason: p.reason, ...(p.principal ? { principal: p.principal } : {}) };
1104
+ };
986
1105
  const failedPlugins = new Set<string>();
987
1106
  for (const t of touched) {
988
1107
  const c = details.get(t.sha);
989
1108
  if (!c) continue;
990
- const signature = policy.active
991
- ? (() => {
992
- const p = commitProvenance(top, policy, c.sha, attestors);
993
- return { level: p.level, reason: p.reason, ...(p.principal ? { principal: p.principal } : {}) };
994
- })()
995
- : { level: "unattested" as const, reason: policy.problems.length ? policy.problems.join("; ") : `no signers file (${policy.signersPath}) at base; attestation is off` };
1109
+ const signature = signatureOf(c.sha);
996
1110
  const pr = c.subject.match(/\(#([0-9]+)\)\s*$/);
997
1111
  const cid = `commit:${c.sha}`;
998
- add<CommitNode>({ id: cid, kind: "commit", sha: c.sha, subject: c.subject, author: c.author, date: c.date, trailers: c.trailers, pullRequest: pr ? Number(pr[1]) : null, signature, lines: t.lines, state: null, joins: trailerJoins.get(c.sha)!.joins });
1112
+ const f = follows.get(c.sha);
1113
+ const squash: SquashNode | undefined = f
1114
+ ? {
1115
+ pullRequest: f.pullRequest,
1116
+ forge: f.forge,
1117
+ ref: f.ref,
1118
+ head: f.head,
1119
+ fetched: f.fetched,
1120
+ followed: f.followed,
1121
+ commits: originalsOf(c.sha).map((o) => ({ sha: o.sha, subject: o.subject, author: o.author, date: o.date, trailers: o.trailers, signature: signatureOf(o.sha), joins: trailerJoins.get(o.sha)!.joins })),
1122
+ }
1123
+ : undefined;
1124
+ add<CommitNode>({ id: cid, kind: "commit", sha: c.sha, subject: c.subject, author: c.author, date: c.date, trailers: c.trailers, pullRequest: pr ? Number(pr[1]) : null, signature, lines: t.lines, state: null, joins: trailerJoins.get(c.sha)!.joins, ...(squash ? { squash } : {}) });
999
1125
  edges.push({ kind: "touched-by", from: rid, to: cid, lines: t.lines });
1000
1126
 
1001
- // 2. Each commit's origin, from the plugins.
1127
+ // 2. Each commit's origin, from the plugins. A followed squash also takes its original commits' (#3035).
1002
1128
  const entry: Joined = { contracts: [], authorship: [], decisions: [], carries: [] };
1003
1129
  joined.set(c.sha, entry);
1130
+ const pushEdge = (e: IntentEdge) => {
1131
+ if (!edges.some((x) => x.kind === e.kind && x.from === e.from && x.to === e.to)) edges.push(e);
1132
+ };
1133
+ const subjects: IntentCommit[] = [c, ...originalsOf(c.sha)];
1004
1134
  for (const k of kinds) {
1005
1135
  if (!k.joins) continue;
1006
- let result;
1007
- try {
1008
- result = await runCommitJoins(k.joins, c, { read: readAt, list: listAt, at: located.at }, k.name);
1009
- } catch (err) {
1010
- const message = `${k.display}: commitJoins failed for ${c.sha.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`;
1011
- if (!failedPlugins.has(message)) reasons.push({ code: "intent-plugin-failed", message });
1012
- failedPlugins.add(message);
1013
- continue;
1014
- }
1015
- const joinedNode = (kind: JoinedNode["kind"], e: JoinedEntity): string => {
1016
- const { id, ...data } = e;
1017
- return add<JoinedNode>({ id: `${kind}:${id}`, kind, ref: id, plugin: k.display, data }).id;
1018
- };
1019
- const unitId = result.unit ? joinedNode("unit", result.unit) : undefined;
1020
- const contractId = result.contract ? joinedNode("contract", result.contract) : undefined;
1021
- if (unitId) {
1022
- entry.unit = unitId;
1023
- edges.push({ kind: "produced-by", from: cid, to: unitId });
1024
- if (contractId) edges.push({ kind: "serves", from: unitId, to: contractId });
1136
+ for (const subject of subjects) {
1137
+ const original = subject !== c;
1138
+ let result;
1139
+ try {
1140
+ result = await runCommitJoins(k.joins, subject, { read: readAt, list: listAt, at: located.at }, k.name);
1141
+ } catch (err) {
1142
+ const message = `${k.display}: commitJoins failed for ${subject.sha.slice(0, 8)}: ${err instanceof Error ? err.message : String(err)}`;
1143
+ if (!failedPlugins.has(message)) reasons.push({ code: "intent-plugin-failed", message });
1144
+ failedPlugins.add(message);
1145
+ continue;
1146
+ }
1147
+ const joinedNode = (kind: JoinedNode["kind"], e: JoinedEntity): string => {
1148
+ const { id, ...data } = e;
1149
+ return add<JoinedNode>({ id: `${kind}:${id}`, kind, ref: id, plugin: k.display, data }).id;
1150
+ };
1151
+ const unitId = result.unit ? joinedNode("unit", result.unit) : undefined;
1152
+ const contractId = result.contract ? joinedNode("contract", result.contract) : undefined;
1153
+ if (unitId) {
1154
+ if (original) entry.unit ??= unitId;
1155
+ else entry.unit = unitId;
1156
+ pushEdge({ kind: "produced-by", from: cid, to: unitId });
1157
+ if (contractId) pushEdge({ kind: "serves", from: unitId, to: contractId });
1158
+ }
1159
+ if (contractId) entry.contracts.push(result.contract!.id);
1160
+ if (unitId) entry.decisions.push(...entityDecisions(result.unit), ...entityDecisions(result.contract));
1161
+ for (const f of result.findings ?? []) pluginFindings.push({ commit: cid, plugin: k.display, finding: f });
1162
+ const evidence = result.evidence === undefined ? [] : Array.isArray(result.evidence) ? result.evidence : [result.evidence];
1163
+ for (const e of evidence) pushEdge({ kind: "cites-evidence", from: unitId ?? contractId ?? cid, to: joinedNode("evidence", e) });
1164
+ // Authorship is checked against the squash's own trailers, so only its own claims count.
1165
+ if (!original) entry.authorship.push(...(result.authorship ?? []));
1025
1166
  }
1026
- if (contractId) entry.contracts.push(result.contract!.id);
1027
- if (unitId) entry.decisions.push(...entityDecisions(result.unit), ...entityDecisions(result.contract));
1028
- for (const f of result.findings ?? []) pluginFindings.push({ commit: cid, plugin: k.display, finding: f });
1029
- const evidence = result.evidence === undefined ? [] : Array.isArray(result.evidence) ? result.evidence : [result.evidence];
1030
- for (const e of evidence) edges.push({ kind: "cites-evidence", from: unitId ?? contractId ?? cid, to: joinedNode("evidence", e) });
1031
- entry.authorship.push(...(result.authorship ?? []));
1032
1167
  }
1033
1168
  }
1034
1169
 
@@ -1036,8 +1171,10 @@ async function walk(query: IntentQuery, head: Head): Promise<IntentResult> {
1036
1171
  const runJoins = await runsForCommits(
1037
1172
  top,
1038
1173
  located.rootOnDisk,
1039
- touched.map((t) => ({ sha: t.sha, run: trailerJoins.get(t.sha)?.joins.run ?? null })),
1174
+ [...new Set([...touched.map((t) => t.sha), ...originalDetails.keys()])].map((sha) => ({ sha, run: trailerJoins.get(sha)?.joins.run ?? null })),
1040
1175
  );
1176
+ // A followed squash joins its original commits' runs (#3035).
1177
+ addSquashRuns(runJoins.refs, follows);
1041
1178
  for (const t of touched) {
1042
1179
  if (!nodes.has(`commit:${t.sha}`)) continue;
1043
1180
  for (const ref of runJoins.refs.get(t.sha) ?? []) {
@@ -1063,9 +1200,15 @@ async function walk(query: IntentQuery, head: Head): Promise<IntentResult> {
1063
1200
  instruction: r?.instruction ?? null,
1064
1201
  lease: r?.lease ?? null,
1065
1202
  });
1066
- edges.push({ kind: "made-by", from: `commit:${t.sha}`, to: node.id, joinedBy: ref.joinedBy });
1203
+ edges.push({ kind: "made-by", from: `commit:${t.sha}`, to: node.id, joinedBy: ref.joinedBy, ...(ref.recordedAs ? { recordedAs: ref.recordedAs } : {}), ...(ref.via ? { via: ref.via } : {}) });
1067
1204
  }
1068
1205
  }
1206
+ // A commit a trailer or a record joins to one run, with the content of a commit another run recorded (#3036).
1207
+ for (const x of runJoins.conflicts) {
1208
+ if (!nodes.has(`commit:${x.sha}`)) continue;
1209
+ const others = x.content.map((m) => `${m.run} (${m.sha.slice(0, 8)})`).join(", ");
1210
+ find("intent-commit-join-conflict", `${x.sha.slice(0, 8)} joins ${x.joined.join(", ")} by its trailer or the run's record, and has the patch-id of a commit ${others} recorded; it is not joined to ${x.content.length === 1 ? "that run" : "those runs"}`, [`commit:${x.sha}`, ...x.joined.map((id) => `run:${id}`)]);
1211
+ }
1069
1212
 
1070
1213
  // 3. The decisions whose constrains cover the region, and their chains.
1071
1214
  const regionFull = region.path === "." ? workspacePrefix : gitPath;
@@ -1420,24 +1563,27 @@ async function walk(query: IntentQuery, head: Head): Promise<IntentResult> {
1420
1563
  j.decisions.push(...implementedIds(h).map((id) => id.slice("record:".length)));
1421
1564
  return node.id;
1422
1565
  };
1423
- for (const r of c.joins.records) {
1424
- const k = recordKinds.get(r.kind);
1425
- if (!k) continue;
1426
- if (k.records!.loaded.kind.work) {
1427
- const h = workByKey.get(`${r.kind}/${r.id}`);
1428
- if (h) r.node = carryWork(h);
1429
- continue;
1566
+ // The commit's own trailers, and a followed squash's original commits' (#3035).
1567
+ for (const joins of [c.joins, ...(c.squash?.commits ?? []).map((o) => o.joins)]) {
1568
+ for (const r of joins.records) {
1569
+ const k = recordKinds.get(r.kind);
1570
+ if (!k) continue;
1571
+ if (k.records!.loaded.kind.work) {
1572
+ const h = workByKey.get(`${r.kind}/${r.id}`);
1573
+ if (h) r.node = carryWork(h);
1574
+ continue;
1575
+ }
1576
+ const hit = decisionById.get(`${r.kind}/${r.id}`);
1577
+ if (!hit) continue;
1578
+ r.node = decisionNode(hit.kind, hit.view).id;
1579
+ carry(r.node);
1580
+ j.decisions.push(`${r.kind}/${r.id}`);
1581
+ }
1582
+ const item = joins.lease?.item;
1583
+ if (item) {
1584
+ const hits = workKinds.map((k) => workByKey.get(`${k.records!.loaded.kind.name}/${item}`)).filter((h): h is WorkHit => h !== undefined);
1585
+ if (hits.length === 1) carryWork(hits[0]);
1430
1586
  }
1431
- const hit = decisionById.get(`${r.kind}/${r.id}`);
1432
- if (!hit) continue;
1433
- r.node = decisionNode(hit.kind, hit.view).id;
1434
- carry(r.node);
1435
- j.decisions.push(`${r.kind}/${r.id}`);
1436
- }
1437
- const item = c.joins.lease?.item;
1438
- if (item) {
1439
- const hits = workKinds.map((k) => workByKey.get(`${k.records!.loaded.kind.name}/${item}`)).filter((h): h is WorkHit => h !== undefined);
1440
- if (hits.length === 1) carryWork(hits[0]);
1441
1587
  }
1442
1588
  // The runs that made the commit (#3034): its run's work item and the
1443
1589
  // records the run names are carried as a Chant-Lease and a Chant-Record are.
@@ -1651,6 +1797,35 @@ async function walk(query: IntentQuery, head: Head): Promise<IntentResult> {
1651
1797
  for (const h of by) edges.push({ kind: "addressed-by", from: f.id, to: workNode(h).id });
1652
1798
  }
1653
1799
  }
1800
+ // Reviews that led to a work item in the graph (#3154, E21): a session's comments[].follow_ups name it.
1801
+ const workNodes = [...nodes.values()].filter((n): n is WorkNode => n.kind === "work");
1802
+ if (workNodes.length > 0) {
1803
+ const byRef = new Map(workNodes.map((n) => [`${n.recordKind}:${n.record}`, n]));
1804
+ for (const k of kinds) {
1805
+ const loaded = k.records?.loaded.kind;
1806
+ if (!loaded?.session) continue;
1807
+ for (const v of k.records!.views) {
1808
+ if (v.id === null || !v.data) continue;
1809
+ const hits = new Map<WorkNode, number[]>();
1810
+ const note = (refs: unknown, comment: number | null) => {
1811
+ if (!Array.isArray(refs)) return;
1812
+ for (const ref of refs) {
1813
+ const n = typeof ref === "string" ? byRef.get(ref) : undefined;
1814
+ if (!n) continue;
1815
+ const list = hits.get(n) ?? [];
1816
+ if (comment !== null && !list.includes(comment)) list.push(comment);
1817
+ hits.set(n, list);
1818
+ }
1819
+ };
1820
+ note(v.data.follow_ups, null);
1821
+ const comments = Array.isArray(v.data.comments) ? v.data.comments : [];
1822
+ comments.forEach((c: unknown, i: number) => note((c as { follow_ups?: unknown } | null)?.follow_ups, i + 1));
1823
+ for (const [n, list] of hits) {
1824
+ (n.reviews ??= []).push({ id: `record:${loaded.name}/${v.id}`, recordKind: loaded.name, record: v.id, path: v.path, state: v.state, comments: list.sort((a, b) => a - b) });
1825
+ }
1826
+ }
1827
+ }
1828
+ }
1654
1829
  for (const f of findings) nodes.set(f.id, f);
1655
1830
  const why = explain();
1656
1831
  const all = [...nodes.values()];
@@ -1679,25 +1854,43 @@ async function walk(query: IntentQuery, head: Head): Promise<IntentResult> {
1679
1854
  /** "Why is it like this" over the region (#3034): who made its current lines, and the decisions governing it, most relevant first. */
1680
1855
  function explain(): WhyAnswer {
1681
1856
  const hunksOf = (run: string, sha: string) => runJoins.runs.get(run)?.commits.find((c) => c.sha === sha)?.hunks ?? null;
1857
+ const joinOf = (run: string, sha: string): RunJoin[] => (runJoins.refs.get(sha) ?? []).find((r) => r.id === run)?.joinedBy ?? [];
1858
+ const joinsOf = (runs: string[], shas: string[]): RunJoin[] => {
1859
+ const all = new Set(runs.flatMap((id) => shas.flatMap((sha) => joinOf(id, sha))));
1860
+ return RUN_JOIN_ORDER.filter((j) => all.has(j));
1861
+ };
1862
+ // For a followed squash (#3035), the original commit behind each of its lines, by blame at the pull request's head.
1863
+ const origins = new Map<string, Map<number, string> | undefined>();
1864
+ const originOf = (sha: string, path: string, line: number): { squash: boolean; origin: string | undefined } => {
1865
+ const f = follows.get(sha);
1866
+ if (!f?.followed || !f.head) return { squash: false, origin: undefined };
1867
+ const key = `${sha}\0${path}`;
1868
+ if (!origins.has(key)) origins.set(key, squashLineOrigins(top!, sha, f.head, path, new Set(f.commits)));
1869
+ return { squash: true, origin: origins.get(key)?.get(line) };
1870
+ };
1682
1871
  // Each current line: its commit, and the runs that wrote it.
1683
1872
  const who = blamed.map((b) => {
1684
- if (b.sha === null) return { line: b.line, sha: null, runs: [] as string[], narrowedBy: null as "hunks" | null };
1873
+ if (b.sha === null) return { line: b.line, sha: null, runs: [] as string[], narrowedBy: null as "hunks" | null, via: undefined as string[] | undefined };
1685
1874
  const sha = b.sha;
1686
- const refs = (runJoins.refs.get(sha) ?? []).map((r) => r.id);
1875
+ const { squash, origin } = originOf(sha, b.origPath, b.origLine);
1876
+ const via = squash ? (origin ? [origin] : []) : undefined;
1877
+ // On a squash line blame at the head traced to one original, a run joined only through the other originals didn't write it.
1878
+ const refs = (runJoins.refs.get(sha) ?? []).filter((r) => !origin || r.joinedBy.some((j) => j !== "squash") || (r.via ?? []).includes(origin)).map((r) => r.id);
1687
1879
  const withHunks = refs.filter((id) => hunksOf(id, sha) !== null);
1688
- if (withHunks.length === 0) return { line: b.line, sha, runs: refs, narrowedBy: null };
1880
+ if (withHunks.length === 0) return { line: b.line, sha, runs: refs, narrowedBy: null, via };
1689
1881
  const wrote = withHunks.filter((id) => hunksOf(id, sha)!.some((h) => h.path === b.origPath && h.start <= b.origLine && b.origLine <= h.end));
1690
1882
  // A run that recorded its hunks and doesn't cover the line didn't write it; a run that recorded none may have.
1691
- return { line: b.line, sha, runs: wrote.length > 0 ? wrote : refs.filter((id) => !withHunks.includes(id)), narrowedBy: "hunks" as const };
1883
+ return { line: b.line, sha, runs: wrote.length > 0 ? wrote : refs.filter((id) => !withHunks.includes(id)), narrowedBy: "hunks" as const, via };
1692
1884
  });
1693
1885
  const spans: WhySpan[] = [];
1694
1886
  for (const w of who) {
1695
1887
  const last = spans[spans.length - 1];
1696
- if (last && last.end === w.line - 1 && last.sha === w.sha && last.narrowedBy === w.narrowedBy && last.runs.join("\0") === w.runs.map((r) => `run:${r}`).join("\0")) {
1888
+ const via = w.via;
1889
+ if (last && last.end === w.line - 1 && last.sha === w.sha && last.narrowedBy === w.narrowedBy && last.runs.join("\0") === w.runs.map((r) => `run:${r}`).join("\0") && last.via?.join("\0") === via?.join("\0")) {
1697
1890
  last.end = w.line;
1698
1891
  continue;
1699
1892
  }
1700
- spans.push({ start: w.line, end: w.line, commit: w.sha ? `commit:${w.sha}` : null, sha: w.sha, runs: w.runs.map((r) => `run:${r}`), narrowedBy: w.narrowedBy });
1893
+ spans.push({ start: w.line, end: w.line, commit: w.sha ? `commit:${w.sha}` : null, sha: w.sha, runs: w.runs.map((r) => `run:${r}`), narrowedBy: w.narrowedBy, joinedBy: w.sha ? joinsOf(w.runs, [w.sha]) : [], ...(via ? { via } : {}) });
1701
1894
  }
1702
1895
  const ranges = (lines: number[]): LineRange[] => {
1703
1896
  const out: LineRange[] = [];
@@ -1745,7 +1938,8 @@ async function walk(query: IntentQuery, head: Head): Promise<IntentResult> {
1745
1938
  if (t?.kind === "decision") carried.add(t.id);
1746
1939
  if (t?.kind === "work") for (const e of edges) if (e.kind === "implements" && e.from === t.id) carried.add(e.to);
1747
1940
  }
1748
- return { run: n.id, lines, commits, unit: r?.unit ? { id: r.unit.id, kind: r.unit.kind, node: work?.id ?? null } : null, decisions: [...carried] };
1941
+ const shas = commits.map((c) => c.slice("commit:".length));
1942
+ return { run: n.id, lines, commits, unit: r?.unit ? { id: r.unit.id, kind: r.unit.kind, node: work?.id ?? null } : null, decisions: [...carried], joinedBy: joinsOf([n.run], shas) };
1749
1943
  };
1750
1944
  let runs: WhyRun[];
1751
1945
  if (fileLines !== null) {
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Editing one property of a JSON or JSONC document in place (#3308): the
3
+ * rest of the text is left as it was, comments and trailing commas included.
4
+ */
5
+
6
+ import { describe, expect, test } from "vitest";
7
+ import { removeProperty, setProperty, valueAt } from "./json-edit";
8
+ import { parseJsonText } from "./jsonc";
9
+
10
+ const read = (text: string, jsonc = false) => {
11
+ const r = parseJsonText(text, { jsonc });
12
+ if (!r.ok) throw new Error(r.message);
13
+ return r.value;
14
+ };
15
+
16
+ const JSON_TEXT = `{
17
+ "name": "w",
18
+ "members": [
19
+ { "name": "a", "box": { "services": [] } },
20
+ {
21
+ "name": "b",
22
+ "box": {
23
+ "intent": "ws-001"
24
+ }
25
+ }
26
+ ]
27
+ }
28
+ `;
29
+
30
+ const JSONC_TEXT = `{
31
+ // the workspace
32
+ "name": "w",
33
+ "members": [
34
+ {
35
+ "name": "b",
36
+ "box": {
37
+ "listing": {
38
+ "title": "Old", // kept as is
39
+ "line": "one line",
40
+ },
41
+ },
42
+ },
43
+ ],
44
+ }
45
+ `;
46
+
47
+ describe("setProperty", () => {
48
+ test("replaces a value in place and leaves every other byte", () => {
49
+ const out = setProperty(JSON_TEXT, false, "/members/1/box", "intent", "ws-002");
50
+ expect(out).toBe(JSON_TEXT.replace('"ws-001"', '"ws-002"'));
51
+ });
52
+
53
+ test("adds a key after the last one, indented like it, and an object value on its own lines", () => {
54
+ const out = setProperty(JSON_TEXT, false, "/members/1/box", "listing", { title: "B", published: false });
55
+ expect(out).toContain(` "intent": "ws-001",\n "listing": {\n "title": "B",\n "published": false\n }\n }`);
56
+ expect(read(out)).toEqual({ ...(read(JSON_TEXT) as object), members: [{ name: "a", box: { services: [] } }, { name: "b", box: { intent: "ws-001", listing: { title: "B", published: false } } }] });
57
+ });
58
+
59
+ test("keeps an object on one line on one line", () => {
60
+ const out = setProperty(JSON_TEXT, false, "/members/0/box", "listing", { title: "A" });
61
+ expect(out).toContain(`{ "name": "a", "box": { "services": [], "listing": {"title":"A"} } }`);
62
+ expect(valueAt(read(out), "/members/0/box/listing")).toEqual({ title: "A" });
63
+ });
64
+
65
+ test("fills an empty object", () => {
66
+ const out = setProperty('{\n "box": {}\n}\n', false, "/box", "listing", { title: "T" });
67
+ expect(out).toBe('{\n "box": {\n "listing": {\n "title": "T"\n }\n }\n}\n');
68
+ });
69
+
70
+ test("in JSONC, keeps comments with their lines and the trailing comma style", () => {
71
+ const out = setProperty(JSONC_TEXT, true, "/members/0/box/listing", "published", false);
72
+ expect(out).toContain(` "line": "one line",\n "published": false,\n },`);
73
+ const out2 = setProperty(JSONC_TEXT, true, "/members/0/box/listing", "title", "New");
74
+ expect(out2).toBe(JSONC_TEXT.replace('"Old"', '"New"'));
75
+ expect(out2).toContain("// kept as is");
76
+ });
77
+
78
+ test("puts a new key after a comment that ends the last property's line", () => {
79
+ const text = '{\n "a": 1, // one\n "b": 2 // two\n}\n';
80
+ const out = setProperty(text, true, "", "c", 3);
81
+ expect(out).toBe('{\n "a": 1, // one\n "b": 2, // two\n "c": 3\n}\n');
82
+ });
83
+
84
+ test("keeps CRLF line endings", () => {
85
+ const text = '{\r\n "box": {\r\n "intent": "x"\r\n }\r\n}\r\n';
86
+ const out = setProperty(text, false, "/box", "listing", { title: "T" });
87
+ expect(out).toBe('{\r\n "box": {\r\n "intent": "x",\r\n "listing": {\r\n "title": "T"\r\n }\r\n }\r\n}\r\n');
88
+ });
89
+
90
+ test("refuses when there is no object at the pointer", () => {
91
+ expect(() => setProperty(JSON_TEXT, false, "/members/5/box", "listing", {})).toThrow(/no object at \/members\/5\/box/);
92
+ });
93
+ });
94
+
95
+ describe("removeProperty", () => {
96
+ test("removes a key with the comma after it, and its line", () => {
97
+ const text = '{\n "a": 1,\n "b": 2,\n "c": 3\n}\n';
98
+ expect(removeProperty(text, false, "", "b")).toBe('{\n "a": 1,\n "c": 3\n}\n');
99
+ });
100
+
101
+ test("removes the last key with the comma before it", () => {
102
+ const text = '{\n "a": 1,\n "b": 2\n}\n';
103
+ expect(removeProperty(text, false, "", "b")).toBe('{\n "a": 1\n}\n');
104
+ });
105
+
106
+ test("removes a key on a one-line object", () => {
107
+ expect(removeProperty('{ "a": 1, "b": 2 }', false, "", "a")).toBe('{ "b": 2 }');
108
+ expect(removeProperty('{ "a": 1, "b": 2 }', false, "", "b")).toBe('{ "a": 1 }');
109
+ });
110
+
111
+ test("leaves {} when the only key goes, and the text unchanged when the key is not there", () => {
112
+ expect(removeProperty('{\n "box": {\n "listing": {\n "a": 1\n }\n }\n}', false, "/box/listing", "a")).toBe('{\n "box": {\n "listing": {}\n }\n}');
113
+ expect(removeProperty('{ "a": 1 }', false, "", "z")).toBe('{ "a": 1 }');
114
+ });
115
+
116
+ test("in JSONC, removes a key with its trailing comma and keeps the comments of the others", () => {
117
+ const out = removeProperty(JSONC_TEXT, true, "/members/0/box/listing", "line");
118
+ expect(out).toContain(`"title": "Old", // kept as is\n },`);
119
+ expect(valueAt(read(out, true), "/members/0/box/listing")).toEqual({ title: "Old" });
120
+ });
121
+ });