@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,237 @@
1
+ /**
2
+ * The forge clients of the pull-request loop (#3183), driven through a
3
+ * recording `fetch`: the calls each makes on GitHub, Forgejo and GitLab, and
4
+ * what each reads back.
5
+ */
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import {
9
+ ForgeApiError,
10
+ forgeFromEnv,
11
+ forgejoForge,
12
+ forgePrincipalOf,
13
+ githubForge,
14
+ gitlabForge,
15
+ type ForgeFetch,
16
+ } from "./pr-forge";
17
+ import { forgePrincipal, parseForgeIdentity } from "./workspace/identity";
18
+
19
+ interface Call {
20
+ method: string;
21
+ url: string;
22
+ headers: Record<string, string>;
23
+ body?: unknown;
24
+ }
25
+
26
+ /** A fake forge: `routes` maps `METHOD path-without-query` to a response body (or a status). */
27
+ function fakeFetch(routes: Record<string, unknown | ((call: Call) => unknown)>): { fetch: ForgeFetch; calls: Call[] } {
28
+ const calls: Call[] = [];
29
+ const fetch: ForgeFetch = async (url, init) => {
30
+ const call: Call = { method: init.method, url, headers: init.headers, ...(init.body ? { body: JSON.parse(init.body) } : {}) };
31
+ calls.push(call);
32
+ const u = new URL(url);
33
+ const key = `${init.method} ${u.pathname}`;
34
+ const page = Number(u.searchParams.get("page") ?? "1");
35
+ let found = routes[`${key}?page=${page}`] ?? routes[key];
36
+ if (typeof found === "function") found = (found as (c: Call) => unknown)(call);
37
+ if (found === undefined) return { ok: false, status: 404, text: async () => '{"message":"Not Found"}' };
38
+ if (typeof found === "number") return { ok: false, status: found, text: async () => "" };
39
+ return { ok: true, status: 200, text: async () => JSON.stringify(found) };
40
+ };
41
+ return { fetch, calls };
42
+ }
43
+
44
+ const SHA = "a".repeat(40);
45
+
46
+ describe("github", () => {
47
+ const opts = { apiBase: "https://api.github.com", repo: "acme/infra", token: "t0k" };
48
+
49
+ test("updates the note that starts with the marker, found on a later page", async () => {
50
+ const page1 = Array.from({ length: 100 }, (_, i) => ({ id: i + 1, body: `other ${i}` }));
51
+ const { fetch, calls } = fakeFetch({
52
+ "GET /repos/acme/infra/issues/12/comments?page=1": page1,
53
+ "GET /repos/acme/infra/issues/12/comments?page=2": [{ id: 555, body: "<!-- chant-pr:prod -->\nold" }],
54
+ "PATCH /repos/acme/infra/issues/comments/555": {},
55
+ });
56
+ await githubForge({ ...opts, fetch }).upsertNote(12, "<!-- chant-pr:prod -->", "<!-- chant-pr:prod -->\nnew");
57
+ expect(calls.map((c) => `${c.method} ${c.url}`)).toEqual([
58
+ "GET https://api.github.com/repos/acme/infra/issues/12/comments?per_page=100&page=1",
59
+ "GET https://api.github.com/repos/acme/infra/issues/12/comments?per_page=100&page=2",
60
+ "PATCH https://api.github.com/repos/acme/infra/issues/comments/555",
61
+ ]);
62
+ expect(calls[2].body).toEqual({ body: "<!-- chant-pr:prod -->\nnew" });
63
+ expect(calls[0].headers.Authorization).toBe("Bearer t0k");
64
+ });
65
+
66
+ test("adds a note when none carries the marker", async () => {
67
+ const { fetch, calls } = fakeFetch({
68
+ "GET /repos/acme/infra/issues/12/comments": [{ id: 1, body: "lgtm" }],
69
+ "POST /repos/acme/infra/issues/12/comments": {},
70
+ });
71
+ await githubForge({ ...opts, fetch }).upsertNote(12, "<!-- chant-pr:prod -->", "body");
72
+ expect(calls.at(-1)).toMatchObject({ method: "POST", body: { body: "body" } });
73
+ });
74
+
75
+ test("sets a commit status with its context and link", async () => {
76
+ const { fetch, calls } = fakeFetch({ [`POST /repos/acme/infra/statuses/${SHA}`]: {} });
77
+ await githubForge({ ...opts, fetch }).setStatus(SHA, { context: "chant/plan", state: "success", description: "2 members", url: "https://ci/run/1" });
78
+ expect(calls[0].body).toEqual({ state: "success", context: "chant/plan", description: "2 members", target_url: "https://ci/run/1" });
79
+ });
80
+
81
+ test("an approver is someone whose newest deciding review approves", async () => {
82
+ const { fetch } = fakeFetch({
83
+ "GET /repos/acme/infra/pulls/12/reviews": [
84
+ { user: { login: "alice" }, state: "APPROVED" },
85
+ { user: { login: "alice" }, state: "COMMENTED" },
86
+ { user: { login: "bob" }, state: "APPROVED" },
87
+ { user: { login: "bob" }, state: "CHANGES_REQUESTED" },
88
+ { user: { login: "carol" }, state: "APPROVED" },
89
+ { user: { login: "carol" }, state: "DISMISSED" },
90
+ { user: { login: "dan" }, state: "CHANGES_REQUESTED" },
91
+ { user: { login: "dan" }, state: "APPROVED" },
92
+ ],
93
+ });
94
+ expect(await githubForge({ ...opts, fetch }).approvers(12)).toEqual(["alice", "dan"]);
95
+ });
96
+
97
+ test("finds the merged pull request whose merge commit is the pushed one", async () => {
98
+ const { fetch } = fakeFetch({
99
+ [`GET /repos/acme/infra/commits/${SHA}/pulls`]: [
100
+ { number: 3, merged_at: null },
101
+ { number: 7, merged_at: "2026-10-01T00:00:00Z", merge_commit_sha: "b".repeat(40) },
102
+ { number: 9, merged_at: "2026-10-02T00:00:00Z", merge_commit_sha: SHA },
103
+ ],
104
+ });
105
+ expect(await githubForge({ ...opts, fetch }).pullRequestFor(SHA)).toBe(9);
106
+ });
107
+
108
+ test("an error status is a ForgeApiError naming the call", async () => {
109
+ const { fetch } = fakeFetch({ [`POST /repos/acme/infra/statuses/${SHA}`]: 403 });
110
+ await expect(githubForge({ ...opts, fetch }).setStatus(SHA, { context: "c", state: "success", description: "d" })).rejects.toBeInstanceOf(ForgeApiError);
111
+ });
112
+ });
113
+
114
+ describe("forgejo", () => {
115
+ const opts = { apiBase: "https://codeberg.org/api/v1", repo: "acme/infra", token: "t0k", host: "codeberg.org" };
116
+
117
+ test("speaks GitHub's note and status calls under /api/v1, with a token header and Forgejo's page size", async () => {
118
+ const { fetch, calls } = fakeFetch({
119
+ "GET /api/v1/repos/acme/infra/issues/4/comments": [{ id: 9, body: "<!-- chant-pr:prod -->x" }],
120
+ "PATCH /api/v1/repos/acme/infra/issues/comments/9": {},
121
+ [`POST /api/v1/repos/acme/infra/statuses/${SHA}`]: {},
122
+ });
123
+ const forge = forgejoForge({ ...opts, fetch });
124
+ await forge.upsertNote(4, "<!-- chant-pr:prod -->", "y");
125
+ await forge.setStatus(SHA, { context: "chant/apply", state: "failure", description: "refused" });
126
+ expect(calls.map((c) => `${c.method} ${c.url}`)).toEqual([
127
+ "GET https://codeberg.org/api/v1/repos/acme/infra/issues/4/comments?limit=50&page=1",
128
+ "PATCH https://codeberg.org/api/v1/repos/acme/infra/issues/comments/9",
129
+ `POST https://codeberg.org/api/v1/repos/acme/infra/statuses/${SHA}`,
130
+ ]);
131
+ expect(calls[0].headers.Authorization).toBe("token t0k");
132
+ });
133
+
134
+ test("a dismissed approval no longer counts, and a comment review decides nothing", async () => {
135
+ const { fetch } = fakeFetch({
136
+ "GET /api/v1/repos/acme/infra/pulls/4/reviews": [
137
+ { user: { login: "ana" }, state: "APPROVED" },
138
+ { user: { login: "ana" }, state: "COMMENT" },
139
+ { user: { login: "ben" }, state: "APPROVED", dismissed: true },
140
+ ],
141
+ });
142
+ expect(await forgejoForge({ ...opts, fetch }).approvers(4)).toEqual(["ana"]);
143
+ });
144
+
145
+ test("finds the pull request of a commit through /commits/{sha}/pull, and none answers null", async () => {
146
+ const found = fakeFetch({ [`GET /api/v1/repos/acme/infra/commits/${SHA}/pull`]: { number: 4 } });
147
+ expect(await forgejoForge({ ...opts, fetch: found.fetch }).pullRequestFor(SHA)).toBe(4);
148
+ const none = fakeFetch({});
149
+ expect(await forgejoForge({ ...opts, fetch: none.fetch }).pullRequestFor(SHA)).toBeNull();
150
+ });
151
+
152
+ test("names reviewers on its host", () => {
153
+ expect(forgejoForge({ ...opts, fetch: fakeFetch({}).fetch }).principalOf("Ana")).toBe("forgejo@codeberg.org:ana");
154
+ });
155
+ });
156
+
157
+ describe("gitlab", () => {
158
+ const opts = { apiBase: "https://gitlab.example.com/api/v4", project: "42", token: "glpat", host: "gitlab.example.com" };
159
+
160
+ test("updates a merge request note with PUT, and adds one with POST", async () => {
161
+ const { fetch, calls } = fakeFetch({
162
+ "GET /api/v4/projects/42/merge_requests/5/notes": [{ id: 77, body: "<!-- chant-pr:prod -->old" }],
163
+ "PUT /api/v4/projects/42/merge_requests/5/notes/77": {},
164
+ });
165
+ await gitlabForge({ ...opts, fetch }).upsertNote(5, "<!-- chant-pr:prod -->", "new");
166
+ expect(calls.map((c) => `${c.method} ${new URL(c.url).pathname}`)).toEqual([
167
+ "GET /api/v4/projects/42/merge_requests/5/notes",
168
+ "PUT /api/v4/projects/42/merge_requests/5/notes/77",
169
+ ]);
170
+ expect(calls[0].headers["PRIVATE-TOKEN"]).toBe("glpat");
171
+ });
172
+
173
+ test("a status maps failure to GitLab's failed and names the context", async () => {
174
+ const { fetch, calls } = fakeFetch({ [`POST /api/v4/projects/42/statuses/${SHA}`]: {} });
175
+ await gitlabForge({ ...opts, fetch }).setStatus(SHA, { context: "chant/apply", state: "failure", description: "refused" });
176
+ expect(calls[0].body).toEqual({ state: "failed", name: "chant/apply", description: "refused" });
177
+ });
178
+
179
+ test("approvers come from the approvals endpoint, merged requests from the commit", async () => {
180
+ const { fetch } = fakeFetch({
181
+ "GET /api/v4/projects/42/merge_requests/5/approvals": { approved_by: [{ user: { username: "ana" } }, { user: { username: "ben" } }] },
182
+ [`GET /api/v4/projects/42/repository/commits/${SHA}/merge_requests`]: [
183
+ { iid: 3, state: "opened" },
184
+ { iid: 5, state: "merged", merge_commit_sha: SHA },
185
+ ],
186
+ });
187
+ const forge = gitlabForge({ ...opts, fetch });
188
+ expect(await forge.approvers(5)).toEqual(["ana", "ben"]);
189
+ expect(await forge.pullRequestFor(SHA)).toBe(5);
190
+ expect(forge.principalOf("ana")).toBe("gitlab@gitlab.example.com:ana");
191
+ });
192
+ });
193
+
194
+ describe("naming reviewers", () => {
195
+ test("agrees with the workspace's own forge identities (#3163)", () => {
196
+ for (const [kind, host, login] of [
197
+ ["github", "github.com", "Alice"],
198
+ ["github", "ghe.acme.io", "alice"],
199
+ ["gitlab", "gitlab.com", "ana"],
200
+ ["gitlab", "gitlab.acme.io", "ana"],
201
+ ["forgejo", "codeberg.org", "ben"],
202
+ ] as const) {
203
+ const principal = forgePrincipalOf(kind, host, login);
204
+ expect(principal).toBe(forgePrincipal(parseForgeIdentity(principal)!));
205
+ expect(parseForgeIdentity(principal)).toEqual({ forge: kind, host, login: login.toLowerCase() });
206
+ }
207
+ });
208
+ });
209
+
210
+ describe("forgeFromEnv", () => {
211
+ test("reads GitHub's job environment, with the run as the status link", () => {
212
+ const forge = forgeFromEnv("github", {
213
+ GITHUB_REPOSITORY: "acme/infra",
214
+ GITHUB_TOKEN: "t",
215
+ GITHUB_SERVER_URL: "https://github.com",
216
+ GITHUB_RUN_ID: "77",
217
+ });
218
+ expect(forge.kind).toBe("github");
219
+ expect(forge.host).toBe("github.com");
220
+ expect(forge.runUrl).toBe("https://github.com/acme/infra/actions/runs/77");
221
+ });
222
+
223
+ test("Forgejo needs its server and API URLs, and says which are missing", () => {
224
+ expect(() => forgeFromEnv("forgejo", { GITHUB_REPOSITORY: "acme/infra", GITHUB_TOKEN: "t" })).toThrow(
225
+ "--forge forgejo needs GITHUB_SERVER_URL, GITHUB_API_URL in the environment",
226
+ );
227
+ expect(
228
+ forgeFromEnv("forgejo", { GITHUB_REPOSITORY: "a/b", GITHUB_TOKEN: "t", GITHUB_SERVER_URL: "https://codeberg.org", GITHUB_API_URL: "https://codeberg.org/api/v1" }).host,
229
+ ).toBe("codeberg.org");
230
+ });
231
+
232
+ test("GitLab needs a token with the api scope, since a job token cannot write notes", () => {
233
+ expect(() => forgeFromEnv("gitlab", { CI_API_V4_URL: "https://gitlab.com/api/v4", CI_PROJECT_ID: "1", CI_JOB_TOKEN: "j" })).toThrow(
234
+ /CHANT_FORGE_TOKEN/,
235
+ );
236
+ });
237
+ });
@@ -0,0 +1,332 @@
1
+ /**
2
+ * What the pull-request loop (#3183) asks of a forge: keep one note on the
3
+ * pull request up to date, set a commit status per stage, list who approved
4
+ * the pull request, and find the pull request that merged a commit.
5
+ *
6
+ * GitHub and Forgejo speak the same REST shapes for the first three, at
7
+ * different base URLs (`$GITHUB_API_URL` is right on both, #2305); they part
8
+ * on the commit-to-pull-request lookup and on review states. GitLab has its
9
+ * own API for all four. Each client takes a `fetch`, so the tests drive them
10
+ * with a recording fake and no network.
11
+ *
12
+ * A reviewer is named the way #3163 names a person: `github:<login>`,
13
+ * `gitlab:<login>`, or `<forge>@<host>:<login>` off the forge's default host.
14
+ * The apply compares that string with the approval's `resolvedBy`.
15
+ *
16
+ * Like `./pr-loop.ts`, this bundles without the TypeScript toolchain (#3421):
17
+ * it imports nothing.
18
+ */
19
+
20
+ export type ForgeKind = "github" | "gitlab" | "forgejo";
21
+
22
+ export const FORGE_KINDS: readonly ForgeKind[] = ["github", "gitlab", "forgejo"];
23
+
24
+ /** The subset of the WHATWG `fetch` the clients use. */
25
+ export type ForgeFetch = (
26
+ url: string,
27
+ init: { method: string; headers: Record<string, string>; body?: string },
28
+ ) => Promise<{ ok: boolean; status: number; text(): Promise<string> }>;
29
+
30
+ export interface ForgeStatus {
31
+ /** `chant/plan` or `chant/apply`. */
32
+ context: string;
33
+ state: "pending" | "success" | "failure";
34
+ description: string;
35
+ /** The pipeline run, linked from the status. */
36
+ url?: string;
37
+ }
38
+
39
+ export interface PrForge {
40
+ kind: ForgeKind;
41
+ /** Lower-cased, such as `github.com`. */
42
+ host: string;
43
+ /** The URL of the run this process is part of, when the environment says. */
44
+ runUrl?: string;
45
+ /** How #3163 names `login` on this forge. */
46
+ principalOf(login: string): string;
47
+ /** Update the note on `pr` whose body starts with `marker`, or add one. */
48
+ upsertNote(pr: number, marker: string, body: string): Promise<void>;
49
+ setStatus(sha: string, status: ForgeStatus): Promise<void>;
50
+ /** Logins whose standing review approves `pr`. */
51
+ approvers(pr: number): Promise<string[]>;
52
+ /** The merged pull request that produced `sha`, or null. */
53
+ pullRequestFor(sha: string): Promise<number | null>;
54
+ }
55
+
56
+ /** Thrown when a forge answers a call with an error status. */
57
+ export class ForgeApiError extends Error {
58
+ constructor(
59
+ readonly method: string,
60
+ readonly url: string,
61
+ readonly status: number,
62
+ detail: string,
63
+ ) {
64
+ super(`${method} ${url} answered ${status}${detail ? `: ${detail.slice(0, 300)}` : ""}`);
65
+ this.name = "ForgeApiError";
66
+ }
67
+ }
68
+
69
+ const DEFAULT_HOST: Record<ForgeKind, string | null> = { github: "github.com", gitlab: "gitlab.com", forgejo: null };
70
+
71
+ /** `<forge>:<login>` on the forge's default host, `<forge>@<host>:<login>` elsewhere, lower-cased (#3163's form). */
72
+ export function forgePrincipalOf(kind: ForgeKind, host: string, login: string): string {
73
+ const h = host.toLowerCase();
74
+ const l = login.toLowerCase();
75
+ return h === DEFAULT_HOST[kind] ? `${kind}:${l}` : `${kind}@${h}:${l}`;
76
+ }
77
+
78
+ interface Transport {
79
+ call(method: string, path: string, body?: unknown): Promise<unknown>;
80
+ /** Every page of a list, `pageParams(page)` appended to `path`. */
81
+ list(path: string, pageParams: (page: number) => string, pageSize: number): Promise<unknown[]>;
82
+ }
83
+
84
+ function transport(base: string, headers: Record<string, string>, fetchImpl: ForgeFetch): Transport {
85
+ const root = base.replace(/\/+$/, "");
86
+ const call = async (method: string, path: string, body?: unknown): Promise<unknown> => {
87
+ const url = `${root}${path}`;
88
+ const res = await fetchImpl(url, {
89
+ method,
90
+ headers: { ...headers, ...(body !== undefined ? { "Content-Type": "application/json" } : {}) },
91
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
92
+ });
93
+ const text = await res.text();
94
+ if (!res.ok) throw new ForgeApiError(method, url, res.status, text);
95
+ return text ? JSON.parse(text) : null;
96
+ };
97
+ return {
98
+ call,
99
+ async list(path, pageParams, pageSize) {
100
+ const out: unknown[] = [];
101
+ for (let page = 1; page <= 50; page++) {
102
+ const sep = path.includes("?") ? "&" : "?";
103
+ const items = await call("GET", `${path}${sep}${pageParams(page)}`);
104
+ if (!Array.isArray(items)) break;
105
+ out.push(...items);
106
+ if (items.length < pageSize) break;
107
+ }
108
+ return out;
109
+ },
110
+ };
111
+ }
112
+
113
+ const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v);
114
+ const loginOf = (v: unknown): string | undefined => (isObject(v) && isObject(v.user) && typeof v.user.login === "string" ? v.user.login : undefined);
115
+
116
+ export interface GithubForgeOptions {
117
+ /** `$GITHUB_API_URL`: `https://api.github.com`, `<host>/api/v3`, or Forgejo's `<host>/api/v1`. */
118
+ apiBase: string;
119
+ /** `owner/name`. */
120
+ repo: string;
121
+ token: string;
122
+ /** The forge's host, for naming reviewers. Default github.com. */
123
+ host?: string;
124
+ runUrl?: string;
125
+ fetch?: ForgeFetch;
126
+ }
127
+
128
+ /** Which of the two GitHub-shaped forges a client is talking to. */
129
+ function githubShaped(kind: "github" | "forgejo", opts: GithubForgeOptions): PrForge {
130
+ const host = (opts.host ?? "github.com").toLowerCase();
131
+ const api = transport(
132
+ opts.apiBase,
133
+ kind === "github"
134
+ ? { Authorization: `Bearer ${opts.token}`, Accept: "application/vnd.github+json" }
135
+ : { Authorization: `token ${opts.token}`, Accept: "application/json" },
136
+ opts.fetch ?? (globalThis.fetch as unknown as ForgeFetch),
137
+ );
138
+ const repo = `/repos/${opts.repo}`;
139
+ // GitHub pages with per_page (up to 100); Forgejo with limit (capped at 50 by default).
140
+ const pageSize = kind === "github" ? 100 : 50;
141
+ const pageParams = (page: number) => (kind === "github" ? `per_page=100&page=${page}` : `limit=50&page=${page}`);
142
+ return {
143
+ kind,
144
+ host,
145
+ ...(opts.runUrl ? { runUrl: opts.runUrl } : {}),
146
+ principalOf: (login) => forgePrincipalOf(kind, host, login),
147
+ async upsertNote(pr, marker, body) {
148
+ const comments = await api.list(`${repo}/issues/${pr}/comments`, pageParams, pageSize);
149
+ const mine = comments.find((c) => isObject(c) && typeof c.body === "string" && c.body.startsWith(marker));
150
+ if (isObject(mine) && (typeof mine.id === "number" || typeof mine.id === "string")) {
151
+ await api.call("PATCH", `${repo}/issues/comments/${mine.id}`, { body });
152
+ } else {
153
+ await api.call("POST", `${repo}/issues/${pr}/comments`, { body });
154
+ }
155
+ },
156
+ async setStatus(sha, status) {
157
+ await api.call("POST", `${repo}/statuses/${sha}`, {
158
+ state: status.state,
159
+ context: status.context,
160
+ description: status.description,
161
+ ...(status.url ? { target_url: status.url } : {}),
162
+ });
163
+ },
164
+ async approvers(pr) {
165
+ // A reviewer's newest deciding review is their standing one. A comment
166
+ // review decides nothing; a dismissed one (GitHub's state, Forgejo's
167
+ // flag) no longer approves.
168
+ const reviews = await api.list(`${repo}/pulls/${pr}/reviews`, pageParams, pageSize);
169
+ const standing = new Map<string, string>();
170
+ for (const r of reviews) {
171
+ const login = loginOf(r);
172
+ if (!login || !isObject(r) || typeof r.state !== "string") continue;
173
+ const state = r.dismissed === true ? "DISMISSED" : r.state.toUpperCase();
174
+ if (state === "COMMENTED" || state === "COMMENT" || state === "PENDING") continue;
175
+ standing.set(login, state);
176
+ }
177
+ return [...standing].filter(([, state]) => state === "APPROVED").map(([login]) => login).sort();
178
+ },
179
+ async pullRequestFor(sha) {
180
+ if (kind === "forgejo") {
181
+ try {
182
+ const pr = await api.call("GET", `${repo}/commits/${sha}/pull`);
183
+ return isObject(pr) && typeof pr.number === "number" ? pr.number : null;
184
+ } catch (err) {
185
+ if (err instanceof ForgeApiError && err.status === 404) return null;
186
+ throw err;
187
+ }
188
+ }
189
+ const prs = await api.call("GET", `${repo}/commits/${sha}/pulls`);
190
+ if (!Array.isArray(prs)) return null;
191
+ const merged = prs.filter((p) => isObject(p) && p.merged_at);
192
+ const exact = merged.find((p) => isObject(p) && p.merge_commit_sha === sha);
193
+ const pick = (exact ?? merged[0]) as Record<string, unknown> | undefined;
194
+ return pick && typeof pick.number === "number" ? pick.number : null;
195
+ },
196
+ };
197
+ }
198
+
199
+ /** A GitHub client (github.com or GitHub Enterprise Server). */
200
+ export function githubForge(opts: GithubForgeOptions): PrForge {
201
+ return githubShaped("github", opts);
202
+ }
203
+
204
+ /** A Forgejo (or Gitea) client. `host` is required: Forgejo has no default host. */
205
+ export function forgejoForge(opts: GithubForgeOptions & { host: string }): PrForge {
206
+ return githubShaped("forgejo", opts);
207
+ }
208
+
209
+ export interface GitlabForgeOptions {
210
+ /** `$CI_API_V4_URL`. */
211
+ apiBase: string;
212
+ /** `$CI_PROJECT_ID`, or the URL-encoded path. */
213
+ project: string;
214
+ token: string;
215
+ /** Default gitlab.com. */
216
+ host?: string;
217
+ runUrl?: string;
218
+ fetch?: ForgeFetch;
219
+ }
220
+
221
+ const GITLAB_STATE: Record<ForgeStatus["state"], string> = { pending: "pending", success: "success", failure: "failed" };
222
+
223
+ /** A GitLab client. The token needs the `api` scope: a job token cannot write merge-request notes. */
224
+ export function gitlabForge(opts: GitlabForgeOptions): PrForge {
225
+ const host = (opts.host ?? "gitlab.com").toLowerCase();
226
+ const api = transport(opts.apiBase, { "PRIVATE-TOKEN": opts.token }, opts.fetch ?? (globalThis.fetch as unknown as ForgeFetch));
227
+ const project = `/projects/${encodeURIComponent(decodeURIComponent(opts.project))}`;
228
+ const pageParams = (page: number) => `per_page=100&page=${page}`;
229
+ return {
230
+ kind: "gitlab",
231
+ host,
232
+ ...(opts.runUrl ? { runUrl: opts.runUrl } : {}),
233
+ principalOf: (login) => forgePrincipalOf("gitlab", host, login),
234
+ async upsertNote(mr, marker, body) {
235
+ const notes = await api.list(`${project}/merge_requests/${mr}/notes`, pageParams, 100);
236
+ const mine = notes.find((n) => isObject(n) && typeof n.body === "string" && n.body.startsWith(marker));
237
+ if (isObject(mine) && typeof mine.id === "number") {
238
+ await api.call("PUT", `${project}/merge_requests/${mr}/notes/${mine.id}`, { body });
239
+ } else {
240
+ await api.call("POST", `${project}/merge_requests/${mr}/notes`, { body });
241
+ }
242
+ },
243
+ async setStatus(sha, status) {
244
+ await api.call("POST", `${project}/statuses/${sha}`, {
245
+ state: GITLAB_STATE[status.state],
246
+ name: status.context,
247
+ description: status.description,
248
+ ...(status.url ? { target_url: status.url } : {}),
249
+ });
250
+ },
251
+ async approvers(mr) {
252
+ const approvals = await api.call("GET", `${project}/merge_requests/${mr}/approvals`);
253
+ const by = isObject(approvals) && Array.isArray(approvals.approved_by) ? approvals.approved_by : [];
254
+ const logins = by
255
+ .map((a) => (isObject(a) && isObject(a.user) && typeof a.user.username === "string" ? a.user.username : undefined))
256
+ .filter((l): l is string => l !== undefined);
257
+ return [...new Set(logins)].sort();
258
+ },
259
+ async pullRequestFor(sha) {
260
+ const mrs = await api.call("GET", `${project}/repository/commits/${sha}/merge_requests`);
261
+ if (!Array.isArray(mrs)) return null;
262
+ const merged = mrs.find((m) => isObject(m) && m.state === "merged" && (m.merge_commit_sha === sha || m.squash_commit_sha === sha))
263
+ ?? mrs.find((m) => isObject(m) && m.state === "merged");
264
+ return isObject(merged) && typeof merged.iid === "number" ? merged.iid : null;
265
+ },
266
+ };
267
+ }
268
+
269
+ /** Thrown when the environment lacks what a forge client needs. */
270
+ export class ForgeEnvironmentError extends Error {
271
+ constructor(kind: ForgeKind, missing: string[]) {
272
+ super(`--forge ${kind} needs ${missing.join(", ")} in the environment`);
273
+ this.name = "ForgeEnvironmentError";
274
+ }
275
+ }
276
+
277
+ const hostOf = (url: string | undefined): string | undefined => {
278
+ if (!url) return undefined;
279
+ try {
280
+ return new URL(url).host.toLowerCase();
281
+ } catch {
282
+ return undefined;
283
+ }
284
+ };
285
+
286
+ /**
287
+ * A client for `kind` from the CI job's environment. The token is
288
+ * `CHANT_FORGE_TOKEN` when set, else the forge's own: `GITHUB_TOKEN` (also
289
+ * set on Forgejo Actions) or `GITLAB_TOKEN`. GitLab's `CI_JOB_TOKEN` cannot
290
+ * write notes, so a GitLab pipeline names a project access token.
291
+ */
292
+ export function forgeFromEnv(kind: ForgeKind, env: Record<string, string | undefined> = process.env, fetchImpl?: ForgeFetch): PrForge {
293
+ const f = fetchImpl ? { fetch: fetchImpl } : {};
294
+ if (kind === "gitlab") {
295
+ const token = env.CHANT_FORGE_TOKEN ?? env.GITLAB_TOKEN;
296
+ const missing = [
297
+ ...(env.CI_API_V4_URL ? [] : ["CI_API_V4_URL"]),
298
+ ...(env.CI_PROJECT_ID ? [] : ["CI_PROJECT_ID"]),
299
+ ...(token ? [] : ["CHANT_FORGE_TOKEN (a token with the api scope)"]),
300
+ ];
301
+ if (missing.length > 0) throw new ForgeEnvironmentError(kind, missing);
302
+ return gitlabForge({
303
+ apiBase: env.CI_API_V4_URL!,
304
+ project: env.CI_PROJECT_ID!,
305
+ token: token!,
306
+ host: env.CI_SERVER_HOST ?? hostOf(env.CI_SERVER_URL) ?? "gitlab.com",
307
+ ...(env.CI_PIPELINE_URL ? { runUrl: env.CI_PIPELINE_URL } : {}),
308
+ ...f,
309
+ });
310
+ }
311
+ const token = env.CHANT_FORGE_TOKEN ?? env.GITHUB_TOKEN ?? env.GH_TOKEN;
312
+ const host = hostOf(env.GITHUB_SERVER_URL);
313
+ const missing = [
314
+ ...(env.GITHUB_REPOSITORY ? [] : ["GITHUB_REPOSITORY"]),
315
+ ...(token ? [] : ["GITHUB_TOKEN or CHANT_FORGE_TOKEN"]),
316
+ ...(kind === "forgejo" && !host ? ["GITHUB_SERVER_URL"] : []),
317
+ ...(kind === "forgejo" && !env.GITHUB_API_URL ? ["GITHUB_API_URL"] : []),
318
+ ];
319
+ if (missing.length > 0) throw new ForgeEnvironmentError(kind, missing);
320
+ const runUrl =
321
+ env.GITHUB_SERVER_URL && env.GITHUB_RUN_ID
322
+ ? `${env.GITHUB_SERVER_URL.replace(/\/+$/, "")}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`
323
+ : undefined;
324
+ const opts = {
325
+ apiBase: env.GITHUB_API_URL ?? "https://api.github.com",
326
+ repo: env.GITHUB_REPOSITORY!,
327
+ token: token!,
328
+ ...(runUrl ? { runUrl } : {}),
329
+ ...f,
330
+ };
331
+ return kind === "forgejo" ? forgejoForge({ ...opts, host: host! }) : githubForge({ ...opts, host: host ?? "github.com" });
332
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The pull-request loop's report, note and forge clients bundle small
3
+ * (#3183, ruling #3421).
4
+ *
5
+ * terragucci's plan stage reads the report and renders the note, and talks
6
+ * to the forge, from one bundled file with no TypeScript toolchain. This
7
+ * bundles `pr-loop.ts` and `pr-forge.ts` the way terragucci would and reads
8
+ * esbuild's own list of what went in.
9
+ */
10
+
11
+ import { join } from "node:path";
12
+ import { build } from "esbuild";
13
+ import { describe, expect, test } from "vitest";
14
+ import { REPO } from "./workspace/__fixtures__/contract-repo";
15
+
16
+ const FORBIDDEN = [/node_modules\//, /\/src\/fold\//, /\/src\/lint\//, /\/src\/codegen\//, /\/src\/workspace\//];
17
+
18
+ describe("the pull-request loop bundle", () => {
19
+ test("holds the report, the note, the grouped summary and the forge clients, and nothing heavier", async () => {
20
+ const result = await build({
21
+ entryPoints: [join(REPO, "packages", "core", "src", "pr-loop.ts"), join(REPO, "packages", "core", "src", "pr-forge.ts")],
22
+ bundle: true,
23
+ platform: "node",
24
+ format: "esm",
25
+ write: false,
26
+ metafile: true,
27
+ outdir: "out",
28
+ logLevel: "silent",
29
+ });
30
+ const inputs = Object.keys(result.metafile.inputs);
31
+ for (const pattern of FORBIDDEN) {
32
+ expect(inputs.filter((i) => pattern.test(i)), String(pattern)).toEqual([]);
33
+ }
34
+ expect(inputs.map((i) => i.replace(/^.*?packages\//, "packages/")).sort()).toEqual([
35
+ "packages/core/src/content-digest.ts",
36
+ "packages/core/src/declarable.ts",
37
+ "packages/core/src/effect-receipt.ts",
38
+ "packages/core/src/intrinsic.ts",
39
+ "packages/core/src/lifecycle/plan-digest.ts",
40
+ "packages/core/src/plan-summary.ts",
41
+ "packages/core/src/pr-forge.ts",
42
+ "packages/core/src/pr-loop.ts",
43
+ ]);
44
+ expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(64 * 1024);
45
+ });
46
+ });