@intentius/chant 0.124.39 → 0.124.41

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 (337) hide show
  1. package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
  2. package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
  3. package/dist/__fixtures__/oidc-emulator.js +89 -0
  4. package/dist/ci-handover.d.ts +117 -0
  5. package/dist/ci-handover.d.ts.map +1 -0
  6. package/dist/ci-handover.js +217 -0
  7. package/dist/ci-identity.d.ts +190 -0
  8. package/dist/ci-identity.d.ts.map +1 -0
  9. package/dist/ci-identity.js +219 -0
  10. package/dist/cli/component-endpoint.d.ts +20 -0
  11. package/dist/cli/component-endpoint.d.ts.map +1 -0
  12. package/dist/cli/component-endpoint.js +36 -0
  13. package/dist/cli/handlers/approve.d.ts +2 -1
  14. package/dist/cli/handlers/approve.d.ts.map +1 -1
  15. package/dist/cli/handlers/approve.js +20 -6
  16. package/dist/cli/handlers/build.d.ts.map +1 -1
  17. package/dist/cli/handlers/build.js +15 -1
  18. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  19. package/dist/cli/handlers/fan-out.js +26 -4
  20. package/dist/cli/handlers/gate-resume.d.ts +3 -0
  21. package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
  22. package/dist/cli/handlers/gate-resume.js +15 -2
  23. package/dist/cli/handlers/lock.d.ts +8 -0
  24. package/dist/cli/handlers/lock.d.ts.map +1 -0
  25. package/dist/cli/handlers/lock.js +197 -0
  26. package/dist/cli/handlers/pr-env.d.ts +34 -0
  27. package/dist/cli/handlers/pr-env.d.ts.map +1 -0
  28. package/dist/cli/handlers/pr-env.js +436 -0
  29. package/dist/cli/handlers/pr.d.ts +74 -0
  30. package/dist/cli/handlers/pr.d.ts.map +1 -1
  31. package/dist/cli/handlers/pr.js +514 -58
  32. package/dist/cli/handlers/promote.d.ts.map +1 -1
  33. package/dist/cli/handlers/promote.js +16 -2
  34. package/dist/cli/handlers/query.d.ts +6 -0
  35. package/dist/cli/handlers/query.d.ts.map +1 -0
  36. package/dist/cli/handlers/query.js +80 -0
  37. package/dist/cli/handlers/run.d.ts +4 -1
  38. package/dist/cli/handlers/run.d.ts.map +1 -1
  39. package/dist/cli/handlers/run.js +17 -6
  40. package/dist/cli/main.d.ts.map +1 -1
  41. package/dist/cli/main.js +41 -2
  42. package/dist/cli/parse-args.d.ts.map +1 -1
  43. package/dist/cli/parse-args.js +31 -2
  44. package/dist/cli/registry.d.ts +12 -2
  45. package/dist/cli/registry.d.ts.map +1 -1
  46. package/dist/components/auto-release.d.ts +35 -3
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +29 -2
  49. package/dist/components/cli-support.d.ts +2 -0
  50. package/dist/components/cli-support.d.ts.map +1 -1
  51. package/dist/components/cli-support.js +2 -0
  52. package/dist/components/deploy-units.d.ts.map +1 -1
  53. package/dist/components/deploy-units.js +5 -0
  54. package/dist/components/fan-out-run.d.ts +28 -0
  55. package/dist/components/fan-out-run.d.ts.map +1 -1
  56. package/dist/components/fan-out-run.js +27 -3
  57. package/dist/components/pr-environment.d.ts +298 -0
  58. package/dist/components/pr-environment.d.ts.map +1 -0
  59. package/dist/components/pr-environment.js +406 -0
  60. package/dist/components/pr-hooks.d.ts +66 -0
  61. package/dist/components/pr-hooks.d.ts.map +1 -0
  62. package/dist/components/pr-hooks.js +119 -0
  63. package/dist/components/pr-pipeline.d.ts +72 -2
  64. package/dist/components/pr-pipeline.d.ts.map +1 -1
  65. package/dist/components/pr-pipeline.js +96 -5
  66. package/dist/components/pr-policy.d.ts +109 -0
  67. package/dist/components/pr-policy.d.ts.map +1 -0
  68. package/dist/components/pr-policy.js +150 -0
  69. package/dist/components/pr-run.d.ts.map +1 -1
  70. package/dist/components/pr-run.js +5 -3
  71. package/dist/components/promote.d.ts.map +1 -1
  72. package/dist/components/promote.js +39 -16
  73. package/dist/gate-decision.d.ts +15 -2
  74. package/dist/gate-decision.d.ts.map +1 -1
  75. package/dist/gate-decision.js +17 -2
  76. package/dist/gate-policy.d.ts +140 -0
  77. package/dist/gate-policy.d.ts.map +1 -0
  78. package/dist/gate-policy.js +242 -0
  79. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -1
  80. package/dist/lifecycle/__fixtures__/publish-fakes.js +6 -1
  81. package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
  82. package/dist/lifecycle/gate-checkpoint.js +1 -1
  83. package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
  84. package/dist/lifecycle/gate-seal.d.ts.map +1 -0
  85. package/dist/lifecycle/publish-declaration.d.ts.map +1 -1
  86. package/dist/lifecycle/publish-declaration.js +4 -3
  87. package/dist/lifecycle/publish-npm.d.ts +2 -2
  88. package/dist/lifecycle/publish-npm.d.ts.map +1 -1
  89. package/dist/lifecycle/publish-npm.js +31 -8
  90. package/dist/lifecycle/query.d.ts +68 -0
  91. package/dist/lifecycle/query.d.ts.map +1 -0
  92. package/dist/lifecycle/query.js +246 -0
  93. package/dist/lifecycle/release-ledger.d.ts +31 -0
  94. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  95. package/dist/lifecycle/release-ledger.js +19 -1
  96. package/dist/lifecycle/run-ledger.d.ts +13 -0
  97. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  98. package/dist/lifecycle/run-ledger.js +16 -0
  99. package/dist/lifecycle/status.d.ts +5 -1
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/lifecycle/status.js +10 -5
  102. package/dist/lifecycle/target-lock.d.ts +97 -0
  103. package/dist/lifecycle/target-lock.d.ts.map +1 -0
  104. package/dist/lifecycle/target-lock.js +286 -0
  105. package/dist/live-endpoint.d.ts +18 -0
  106. package/dist/live-endpoint.d.ts.map +1 -1
  107. package/dist/live-endpoint.js +27 -0
  108. package/dist/op/activities/activity-contracts.d.ts +30 -0
  109. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  110. package/dist/op/activities/activity-contracts.js +26 -0
  111. package/dist/op/activities/index.d.ts +2 -0
  112. package/dist/op/activities/index.d.ts.map +1 -1
  113. package/dist/op/activities/index.js +4 -0
  114. package/dist/op/activities/review-agent.d.ts +137 -0
  115. package/dist/op/activities/review-agent.d.ts.map +1 -0
  116. package/dist/op/activities/review-agent.js +266 -0
  117. package/dist/op/builders.d.ts +9 -0
  118. package/dist/op/builders.d.ts.map +1 -1
  119. package/dist/op/builders.js +11 -0
  120. package/dist/op/cron.d.ts +8 -0
  121. package/dist/op/cron.d.ts.map +1 -1
  122. package/dist/op/cron.js +51 -0
  123. package/dist/op/gate-approval.d.ts +20 -13
  124. package/dist/op/gate-approval.d.ts.map +1 -1
  125. package/dist/op/gate-approval.js +13 -18
  126. package/dist/op/gate-quorum.d.ts +131 -0
  127. package/dist/op/gate-quorum.d.ts.map +1 -0
  128. package/dist/op/gate-quorum.js +183 -0
  129. package/dist/op/gate-resume.d.ts +4 -2
  130. package/dist/op/gate-resume.d.ts.map +1 -1
  131. package/dist/op/gate-resume.js +5 -4
  132. package/dist/op/gate-review.js +1 -1
  133. package/dist/op/gate.d.ts +32 -4
  134. package/dist/op/gate.d.ts.map +1 -1
  135. package/dist/op/gate.js +40 -24
  136. package/dist/op/index.d.ts +6 -4
  137. package/dist/op/index.d.ts.map +1 -1
  138. package/dist/op/index.js +4 -3
  139. package/dist/op/local-executor.d.ts.map +1 -1
  140. package/dist/op/local-executor.js +9 -3
  141. package/dist/op/op-waves-run.d.ts +62 -1
  142. package/dist/op/op-waves-run.d.ts.map +1 -1
  143. package/dist/op/op-waves-run.js +190 -11
  144. package/dist/op/op-waves.d.ts +55 -3
  145. package/dist/op/op-waves.d.ts.map +1 -1
  146. package/dist/op/op-waves.js +63 -8
  147. package/dist/op/schedule-overdue.d.ts +57 -0
  148. package/dist/op/schedule-overdue.d.ts.map +1 -0
  149. package/dist/op/schedule-overdue.js +138 -0
  150. package/dist/override-gate.d.ts +2 -2
  151. package/dist/override-gate.d.ts.map +1 -1
  152. package/dist/override-gate.js +1 -1
  153. package/dist/pr-forge.d.ts +36 -0
  154. package/dist/pr-forge.d.ts.map +1 -1
  155. package/dist/pr-forge.js +82 -0
  156. package/dist/pr-loop.d.ts +90 -1
  157. package/dist/pr-loop.d.ts.map +1 -1
  158. package/dist/pr-loop.js +77 -3
  159. package/dist/pr-post.d.ts +72 -0
  160. package/dist/pr-post.d.ts.map +1 -0
  161. package/dist/pr-post.js +134 -0
  162. package/dist/wave-blocked.d.ts +79 -0
  163. package/dist/wave-blocked.d.ts.map +1 -0
  164. package/dist/wave-blocked.js +245 -0
  165. package/dist/wave-engine.d.ts +6 -0
  166. package/dist/wave-engine.d.ts.map +1 -1
  167. package/dist/wave-engine.js +2 -0
  168. package/dist/wave-runtime.mjs +239 -239
  169. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
  170. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
  171. package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
  172. package/dist/workspace/ci-branches.d.ts +51 -0
  173. package/dist/workspace/ci-branches.d.ts.map +1 -0
  174. package/dist/workspace/ci-branches.js +69 -0
  175. package/dist/workspace/ci-cloud-identity.d.ts +11 -0
  176. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
  177. package/dist/workspace/ci-cloud-identity.js +24 -0
  178. package/dist/workspace/ci-locks.d.ts +12 -0
  179. package/dist/workspace/ci-locks.d.ts.map +1 -0
  180. package/dist/workspace/ci-locks.js +6 -0
  181. package/dist/workspace/ci-run.d.ts +24 -1
  182. package/dist/workspace/ci-run.d.ts.map +1 -1
  183. package/dist/workspace/ci-run.js +85 -6
  184. package/dist/workspace/declaration.d.ts +63 -0
  185. package/dist/workspace/declaration.d.ts.map +1 -1
  186. package/dist/workspace/declaration.js +72 -1
  187. package/dist/workspace/declaration.schema.json +147 -0
  188. package/dist/workspace/identity-gate-rule.d.ts +1 -1
  189. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  190. package/dist/workspace/identity-gate-rule.js +1 -1
  191. package/dist/workspace/identity.d.ts +1 -1
  192. package/dist/workspace/identity.d.ts.map +1 -1
  193. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  194. package/dist/workspace/lineage-upgrade.js +8 -5
  195. package/dist/workspace/member-pipeline.d.ts +6 -0
  196. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  197. package/dist/workspace/member-pipeline.js +23 -0
  198. package/dist/workspace/pr-report.schema.json +73 -2
  199. package/dist/workspace/security.d.ts.map +1 -1
  200. package/dist/workspace/security.js +4 -0
  201. package/dist/workspace/status-gates.d.ts +3 -2
  202. package/dist/workspace/status-gates.d.ts.map +1 -1
  203. package/dist/workspace/status-gates.js +8 -7
  204. package/dist/workspace/status.d.ts +3 -1
  205. package/dist/workspace/status.d.ts.map +1 -1
  206. package/dist/workspace/status.js +7 -3
  207. package/dist/workspace/status.schema.json +1 -0
  208. package/dist/workspace/trust/seal.d.ts +2 -2
  209. package/dist/workspace/trust/seal.d.ts.map +1 -1
  210. package/dist/workspace/trust/seal.js +2 -2
  211. package/package.json +1 -1
  212. package/src/__fixtures__/oidc-emulator.ts +118 -0
  213. package/src/ci-handover.test.ts +209 -0
  214. package/src/ci-handover.ts +237 -0
  215. package/src/ci-identity.test.ts +201 -0
  216. package/src/ci-identity.ts +310 -0
  217. package/src/cli/component-endpoint.ts +43 -0
  218. package/src/cli/handlers/approve.ts +18 -5
  219. package/src/cli/handlers/build.ts +15 -1
  220. package/src/cli/handlers/fan-out.test.ts +10 -0
  221. package/src/cli/handlers/fan-out.ts +29 -4
  222. package/src/cli/handlers/gate-resume.ts +16 -2
  223. package/src/cli/handlers/lock.test.ts +98 -0
  224. package/src/cli/handlers/lock.ts +202 -0
  225. package/src/cli/handlers/operator.test.ts +3 -1
  226. package/src/cli/handlers/pr-endpoint.test.ts +169 -0
  227. package/src/cli/handlers/pr-env.ts +462 -0
  228. package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
  229. package/src/cli/handlers/pr.test.ts +184 -0
  230. package/src/cli/handlers/pr.ts +508 -64
  231. package/src/cli/handlers/promote.ts +25 -2
  232. package/src/cli/handlers/query.ts +78 -0
  233. package/src/cli/handlers/run.test.ts +2 -1
  234. package/src/cli/handlers/run.ts +16 -5
  235. package/src/cli/main.ts +41 -2
  236. package/src/cli/parse-args.ts +23 -2
  237. package/src/cli/registry.ts +12 -2
  238. package/src/components/auto-release-infra.test.ts +131 -0
  239. package/src/components/auto-release.test.ts +40 -1
  240. package/src/components/auto-release.ts +50 -4
  241. package/src/components/cli-support.ts +4 -0
  242. package/src/components/deploy-units.ts +5 -0
  243. package/src/components/fan-out-run.ts +39 -2
  244. package/src/components/fan-out-waves.test.ts +45 -1
  245. package/src/components/pr-environment.test.ts +249 -0
  246. package/src/components/pr-environment.ts +538 -0
  247. package/src/components/pr-hooks.test.ts +118 -0
  248. package/src/components/pr-hooks.ts +157 -0
  249. package/src/components/pr-pipeline.ts +130 -5
  250. package/src/components/pr-policy.test.ts +145 -0
  251. package/src/components/pr-policy.ts +211 -0
  252. package/src/components/pr-run.ts +5 -2
  253. package/src/components/promote.test.ts +39 -0
  254. package/src/components/promote.ts +38 -14
  255. package/src/gate-decision-bundle.test.ts +4 -3
  256. package/src/gate-decision.ts +39 -2
  257. package/src/gate-policy-bundle.test.ts +28 -0
  258. package/src/gate-policy.test.ts +162 -0
  259. package/src/gate-policy.ts +298 -0
  260. package/src/lifecycle/__fixtures__/publish-fakes.ts +6 -1
  261. package/src/lifecycle/gate-checkpoint.ts +1 -1
  262. package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
  263. package/src/lifecycle/publish-declaration.ts +4 -3
  264. package/src/lifecycle/publish-npm.ts +33 -8
  265. package/src/lifecycle/publish-targets.test.ts +36 -3
  266. package/src/lifecycle/query.test.ts +91 -0
  267. package/src/lifecycle/query.ts +315 -0
  268. package/src/lifecycle/release-ledger.ts +42 -1
  269. package/src/lifecycle/run-ledger.ts +18 -0
  270. package/src/lifecycle/status.test.ts +19 -0
  271. package/src/lifecycle/status.ts +14 -6
  272. package/src/lifecycle/target-lock.test.ts +195 -0
  273. package/src/lifecycle/target-lock.ts +347 -0
  274. package/src/live-endpoint.ts +31 -0
  275. package/src/op/activities/activity-contracts.ts +31 -0
  276. package/src/op/activities/index.ts +6 -0
  277. package/src/op/activities/review-agent.test.ts +240 -0
  278. package/src/op/activities/review-agent.ts +355 -0
  279. package/src/op/builders.ts +13 -0
  280. package/src/op/cron.ts +44 -0
  281. package/src/op/gate-alias.test.ts +2 -2
  282. package/src/op/gate-approval.test.ts +30 -9
  283. package/src/op/gate-approval.ts +23 -26
  284. package/src/op/gate-quorum.test.ts +139 -0
  285. package/src/op/gate-quorum.ts +248 -0
  286. package/src/op/gate-resume.ts +6 -3
  287. package/src/op/gate-review.ts +1 -1
  288. package/src/op/gate.ts +64 -23
  289. package/src/op/index.ts +13 -1
  290. package/src/op/local-executor.ts +9 -2
  291. package/src/op/op-waves-hook-targets.test.ts +126 -0
  292. package/src/op/op-waves-policy-hold.test.ts +66 -0
  293. package/src/op/op-waves-quorum.test.ts +101 -0
  294. package/src/op/op-waves-run.ts +233 -11
  295. package/src/op/op-waves.ts +83 -10
  296. package/src/op/schedule-overdue.test.ts +220 -0
  297. package/src/op/schedule-overdue.ts +161 -0
  298. package/src/override-gate-bundle.test.ts +1 -1
  299. package/src/override-gate.ts +2 -2
  300. package/src/pr-forge-bundle.test.ts +2 -2
  301. package/src/pr-forge.test.ts +33 -0
  302. package/src/pr-forge.ts +114 -0
  303. package/src/pr-loop-bundle.test.ts +2 -2
  304. package/src/pr-loop.test.ts +58 -0
  305. package/src/pr-loop.ts +146 -4
  306. package/src/pr-post.test.ts +141 -0
  307. package/src/pr-post.ts +159 -0
  308. package/src/wave-blocked-bundle.test.ts +25 -0
  309. package/src/wave-blocked.test.ts +146 -0
  310. package/src/wave-blocked.ts +271 -0
  311. package/src/wave-engine.ts +8 -0
  312. package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
  313. package/src/workspace/ci-branches.test.ts +62 -0
  314. package/src/workspace/ci-branches.ts +79 -0
  315. package/src/workspace/ci-cloud-identity.test.ts +53 -0
  316. package/src/workspace/ci-cloud-identity.ts +23 -0
  317. package/src/workspace/ci-locks.ts +16 -0
  318. package/src/workspace/ci-run-index.test.ts +2 -1
  319. package/src/workspace/ci-run.test.ts +133 -8
  320. package/src/workspace/ci-run.ts +92 -7
  321. package/src/workspace/declaration.schema.json +147 -0
  322. package/src/workspace/declaration.test.ts +76 -0
  323. package/src/workspace/declaration.ts +134 -2
  324. package/src/workspace/identity-gate-rule.ts +1 -1
  325. package/src/workspace/identity.ts +1 -1
  326. package/src/workspace/lineage-upgrade.ts +11 -8
  327. package/src/workspace/member-pipeline.test.ts +29 -0
  328. package/src/workspace/member-pipeline.ts +30 -0
  329. package/src/workspace/pr-report.schema.json +73 -2
  330. package/src/workspace/security.ts +4 -0
  331. package/src/workspace/status-gates.ts +9 -6
  332. package/src/workspace/status.schema.json +1 -0
  333. package/src/workspace/status.ts +11 -4
  334. package/src/workspace/trust/gate-seal.test.ts +1 -1
  335. package/src/workspace/trust/seal.ts +2 -2
  336. package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
  337. /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Gate-time policy on the pull-request path (#4294): `ci.policy` in the
3
+ * workspace declaration, run by `chant components pr-plan` and `pr-apply`
4
+ * over the change-set document the gate binds (`CapabilityPlan.changeSet`,
5
+ * composed by `planPrSet`).
6
+ *
7
+ * The settings and the policy directory are both read at the base the job
8
+ * measures the change from, so a pull request cannot loosen, edit or delete
9
+ * the policy that judges it. The engine and its result shapes are
10
+ * `../gate-policy.ts`.
11
+ *
12
+ * - deny: pr-apply applies nothing unless an override counts
13
+ * (`../override-gate.ts`, subject the gate's op, such as `pr-12`). The
14
+ * first refusal records the denial on `_gates/policy-override.jsonl`, so
15
+ * `chant approve policy-override <op> --rule <id> --reason <text>` can
16
+ * answer it. The gate's own approval is still needed. The override is
17
+ * spent just before anything applies.
18
+ * - hold: the plan needs a person's approval of its own digest. pr-apply
19
+ * always asks for one, so a hold changes one path: a green commit's plan
20
+ * no longer applies under the approvals of the merges it carries.
21
+ * - error: the policy did not run or gave no readable answer. pr-apply
22
+ * applies nothing.
23
+ */
24
+
25
+ import { readDeclaration, WorkspaceReadError } from "../workspace/declaration";
26
+ import { locateWorkspace } from "../workspace/which-chant";
27
+ import type { WorkspaceTree } from "../workspace/tree";
28
+ import {
29
+ checkGatePolicy,
30
+ DEFAULT_POLICY_PATH,
31
+ exportGatePolicy,
32
+ type GatePolicyExec,
33
+ type GatePolicySettings,
34
+ type GatePolicyVerdict,
35
+ } from "../gate-policy";
36
+ import type { ChangeSetDocument } from "../change-set";
37
+ import type { GateLedgerPort } from "../op/gate";
38
+ import type { PrPolicyReport } from "../pr-loop";
39
+
40
+ /** `ci.policy` as read at base. */
41
+ export interface PrLoopPolicy {
42
+ /** Absent when the declaration at base sets none. */
43
+ settings?: GatePolicySettings;
44
+ /** The commit it was read at. */
45
+ base: string;
46
+ /** The tree of the declaration's directory at base, where the policy directory is read. */
47
+ tree?: WorkspaceTree;
48
+ /** A line for the log when no policy runs because the base has no declaration, or could not be read. */
49
+ note?: string;
50
+ }
51
+
52
+ /** Whether the working tree's declaration names a `ci.policy`. */
53
+ function checkoutNamesPolicy(cwd: string): boolean {
54
+ try {
55
+ return readDeclaration(locateWorkspace(cwd).tree).ci?.policy !== undefined;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * The policy a pull-request job in `cwd` runs, from the declaration at
63
+ * `base`. Throws when the declaration at base cannot be read and the
64
+ * working tree's names a policy: running none would let the change waive it.
65
+ */
66
+ export function prLoopPolicyAt(cwd: string, base: string): PrLoopPolicy {
67
+ try {
68
+ const located = locateWorkspace(cwd, base);
69
+ const settings = readDeclaration(located.tree).ci?.policy;
70
+ return { base, ...(settings ? { settings, tree: located.tree } : {}) };
71
+ } catch (err) {
72
+ const missing = err instanceof WorkspaceReadError && (err.code === "declaration-missing" || err.code === "not-a-git-repository");
73
+ const why = err instanceof Error ? err.message : String(err);
74
+ if (checkoutNamesPolicy(cwd)) {
75
+ if (missing) return { base, note: `ci.policy: the base ${base.slice(0, 12)} has no workspace declaration, so the policy this change declares runs once it merges` };
76
+ throw new Error(`ci.policy is read from the workspace declaration at ${base.slice(0, 12)}, and it could not be read (${why}), so the policy cannot be run`);
77
+ }
78
+ return missing ? { base } : { base, note: `ci.policy: the declaration at ${base.slice(0, 12)} could not be read (${why}); this checkout names no policy, so none runs` };
79
+ }
80
+ }
81
+
82
+ /** What the engine reads as `input`. */
83
+ export interface PrPolicyInput {
84
+ version: 1;
85
+ stage: "plan" | "apply";
86
+ /** The gate's op, `pr-<n>`. */
87
+ op: string;
88
+ gate: string;
89
+ env: string;
90
+ pr: number | null;
91
+ base: string;
92
+ head: string;
93
+ /** The change-set document the gate binds. Its `entries` are every change the plan proposes. */
94
+ changeSet: ChangeSetDocument;
95
+ }
96
+
97
+ /** Run the policy over `input`. Undefined when the base sets no policy. */
98
+ export function runPrPolicy(read: PrLoopPolicy, input: PrPolicyInput, exec?: GatePolicyExec): GatePolicyVerdict | undefined {
99
+ if (!read.settings || !read.tree) return undefined;
100
+ const engine = read.settings.engine ?? "conftest";
101
+ const path = read.settings.path ?? DEFAULT_POLICY_PATH;
102
+ const exported = exportGatePolicy(read.tree, path);
103
+ if ("error" in exported) return { result: "error", engine, denials: [], rules: [], holds: [], warnings: [], error: exported.error };
104
+ try {
105
+ return checkGatePolicy({ settings: read.settings, input, policyDir: exported.dir, cwd: process.cwd(), ...(exec ? { exec } : {}) });
106
+ } finally {
107
+ exported.cleanup();
108
+ }
109
+ }
110
+
111
+ /** The verdict as the report keeps it. */
112
+ export function prPolicyReport(verdict: GatePolicyVerdict, base: string): PrPolicyReport {
113
+ return {
114
+ result: verdict.result,
115
+ engine: verdict.engine,
116
+ base,
117
+ denials: verdict.denials,
118
+ ...(verdict.rules.length > 0 ? { rules: verdict.rules } : {}),
119
+ holds: verdict.holds,
120
+ warnings: verdict.warnings,
121
+ ...(verdict.error !== undefined ? { error: verdict.error } : {}),
122
+ };
123
+ }
124
+
125
+ export interface PrPolicyOverrideInput {
126
+ port: GateLedgerPort;
127
+ /** The override's subject: the gate's op. */
128
+ subject: string;
129
+ /** The digest the gate binds: the change-set document's. */
130
+ digest: string;
131
+ rules: readonly string[];
132
+ /** The commit the overriders and signers are read at. */
133
+ base: string;
134
+ cwd: string;
135
+ /** Count only sealed overrides, as a sealed gate counts only sealed approvals. */
136
+ sealed: boolean;
137
+ /** Record the denial when no override counts and none stands. The plan stage records nothing. */
138
+ record: boolean;
139
+ description?: string;
140
+ now?: string;
141
+ }
142
+
143
+ /** Where an override of a denial stands. */
144
+ export type PrPolicyOverride =
145
+ | { status: "overridden"; override: NonNullable<PrPolicyReport["override"]> }
146
+ | { status: "none"; refusals: string[]; command: string; overriders: string[]; recorded: boolean; moved?: { by: string } };
147
+
148
+ /**
149
+ * Decide a denied plan against the override ledger, as an Op waves job
150
+ * decides a denied run (`settleDenials` in `../op/op-waves-run.ts`).
151
+ */
152
+ export async function decidePrPolicyOverride(input: PrPolicyOverrideInput): Promise<PrPolicyOverride> {
153
+ const og = await import("../override-gate");
154
+ const overriders = og.overridersAt(input.cwd, input.base).principals;
155
+ const signers = input.sealed ? (await import("../gate-decision")).gateSignersAt(input.cwd, input.base) : undefined;
156
+ const ledger = await input.port.read(og.OVERRIDE_OP);
157
+ const applied = ((await input.port.readApplied?.(og.OVERRIDE_OP)) ?? []) as unknown as import("../override-gate").AppliedOverrideRecord[];
158
+ const now = input.now ?? new Date().toISOString();
159
+ const verdict = og.decideOverride({
160
+ ledger,
161
+ applied,
162
+ subject: input.subject,
163
+ planDigest: input.digest,
164
+ rules: input.rules,
165
+ overriders,
166
+ mode: input.sealed ? "sealed" : "ledger",
167
+ ...(signers ? { signers } : {}),
168
+ now,
169
+ });
170
+ if (verdict.status === "overridden") {
171
+ const o = verdict.override;
172
+ return { status: "overridden", override: { by: o.by, at: o.at, reason: o.reason, digest: o.digest } };
173
+ }
174
+ let recorded = false;
175
+ if (input.record && !verdict.standing) {
176
+ const { version: _v, kind: _k, ...fact } = og.overridePending({
177
+ subject: input.subject,
178
+ planDigest: input.digest,
179
+ rules: input.rules,
180
+ timestamp: now,
181
+ expiresAt: new Date(new Date(now).getTime() + 48 * 3600 * 1000).toISOString(),
182
+ ...(input.description ? { description: input.description } : {}),
183
+ });
184
+ await input.port.appendPending(fact);
185
+ recorded = true;
186
+ }
187
+ return {
188
+ status: "none",
189
+ refusals: verdict.refusals,
190
+ command: og.overrideCommand(input.subject, input.rules, input.sealed),
191
+ overriders,
192
+ recorded,
193
+ ...(verdict.status === "moved" ? { moved: { by: verdict.by } } : {}),
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Spend the override the apply runs under: append its applied record before
199
+ * anything applies. Throws when it could not be written or did not reach the
200
+ * remote, and the caller applies nothing.
201
+ */
202
+ export async function spendPrPolicyOverride(port: GateLedgerPort, subject: string, override: NonNullable<PrPolicyReport["override"]>, at: { now?: string; commit?: string } = {}): Promise<void> {
203
+ const og = await import("../override-gate");
204
+ if (!port.appendApplied) throw new Error("this gate ledger cannot record an applied override");
205
+ const record = og.appliedOverride({ subject, digest: override.digest, at: override.at, by: override.by }, { timestamp: at.now ?? new Date().toISOString(), ...(at.commit ? { commit: at.commit } : {}) });
206
+ const pushed = await port.appendApplied(record as unknown as Parameters<NonNullable<GateLedgerPort["appendApplied"]>>[0]);
207
+ const { isLifecycleLocalNote } = await import("../lifecycle/local-note");
208
+ if (!pushed.pushed && !isLifecycleLocalNote(pushed.pushWarning)) {
209
+ throw new Error(`the applied record did not reach the remote the next run reads: ${pushed.pushWarning ?? "the push did not land"}`);
210
+ }
211
+ }
@@ -514,7 +514,7 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
514
514
 
515
515
  const completed = new Set(run.completed);
516
516
  const held = new Map((run.held ?? []).map((h) => [h.component, h]));
517
- const blocked = new Set(run.blocked.map((b) => b.component));
517
+ const blocked = new Map(run.blocked.map((b) => [b.component, b.blockedBy]));
518
518
  const errorOf = (component: string): string | undefined => {
519
519
  const result = [...run.results].reverse().find((r) => r.component === component);
520
520
  if (result?.status === "gated") return `stopped at its own gate ${result.gate?.gate ?? ""}`.trim();
@@ -529,7 +529,10 @@ export async function applyPrSet(options: PrApplyOptions): Promise<PrApplyResult
529
529
  const h = held.get(m.component);
530
530
  if (h) return { ...asPlanned, status: "waiting", held: h.heldBy ? `${h.heldBy} is held: ${h.reason}` : h.reason };
531
531
  if (fresh?.status === "plan-failed") return { ...asPlanned, status: "plan-failed", ...(fresh.error ? { error: fresh.error } : {}) };
532
- if (blocked.has(m.component)) return { ...asPlanned, status: "blocked" };
532
+ if (blocked.has(m.component)) {
533
+ const by = blocked.get(m.component);
534
+ return { ...asPlanned, status: "blocked", ...(by ? { blockedBy: by } : {}) };
535
+ }
533
536
  const error = errorOf(m.component);
534
537
  return { ...asPlanned, status: "failed", ...(error ? { error } : {}) };
535
538
  });
@@ -108,6 +108,45 @@ describe("planning", () => {
108
108
  });
109
109
  });
110
110
 
111
+ describe("records of an apply, not an artifact (#4274)", () => {
112
+ const apply = (component: string, timestamp: string, digestKind: "plan" | "deploy-input" = "plan"): ReleaseRecord => ({
113
+ ...rec(component, "staging", `jcs1-sha256:${timestamp.slice(0, 10)}`, timestamp),
114
+ digestKind,
115
+ });
116
+ const records = [
117
+ rec("api", "staging", "sha256:api", "2026-01-02T00:00:00Z"),
118
+ apply("infra", "2026-01-02T00:00:00Z"),
119
+ apply("infra", "2026-01-03T00:00:00Z", "deploy-input"),
120
+ ];
121
+
122
+ test("promoting every component leaves an infrastructure component out and says why", () => {
123
+ const plan = planPromotion({ from: "staging", to: "prod", sourceRecords: records, declared: ["api", "infra"] }) as PromotionPlan;
124
+ expect(plan.items.map((i) => i.component)).toEqual(["api"]);
125
+ expect(plan.notPromoted).toEqual([{ component: "infra", reason: expect.stringMatching(/only applies are recorded/) }]);
126
+ });
127
+
128
+ test("a newer apply never hides the artifact release before it", () => {
129
+ const mixed = [rec("api", "staging", "sha256:api", "2026-01-01T00:00:00Z"), apply("api", "2026-01-05T00:00:00Z")];
130
+ const picked = selectRelease(mixed, "api", "staging");
131
+ expect("record" in picked && picked.record.digest).toBe("sha256:api");
132
+ });
133
+
134
+ test("promote, rollback and redeploy of a component with only applies are refused by name", () => {
135
+ const promote = planPromotion({ from: "staging", to: "prod", sourceRecords: records, declared: ["api", "infra"], component: "infra" });
136
+ expect("error" in promote && promote.error).toMatch(/"infra" has only applies recorded in "staging"/);
137
+ const rollback = planRollback({ env: "staging", records, declared: ["infra"], component: "infra" });
138
+ expect("error" in rollback && rollback.error).toMatch(/only applies recorded/);
139
+ const redeploy = planRedeploy({ env: "staging", records, declared: ["infra"], component: "infra" });
140
+ expect("error" in redeploy && redeploy.error).toMatch(/only applies recorded/);
141
+ });
142
+
143
+ test("a rollback or redeploy carries a templates digest's kind forward", () => {
144
+ const source: ReleaseRecord = { ...rec("infra", "staging", "sha256:tpl", "2026-01-02T00:00:00Z"), digestKind: "templates" };
145
+ const run = { runId: "r2", actor: "ci", timestamp: "2026-01-04T00:00:00Z" };
146
+ expect(rollbackRecord({ component: "infra", digest: "sha256:tpl", source }, "staging", run)).toMatchObject({ digestKind: "templates" });
147
+ });
148
+ });
149
+
111
150
  describe("pinning each component to a digest (#2602)", () => {
112
151
  // api's pinned release is older than its latest one: another pipeline
113
152
  // recorded sha256:newer while this one ran.
@@ -39,7 +39,7 @@
39
39
  */
40
40
 
41
41
  import { parseWiringRef, type WiringRef } from "../lint/rules/comp/support";
42
- import { latestPerComponent, type ReleaseRecord, type ReleaseRecordInput, type ReleaseAdmission, type RunOrigin } from "../lifecycle/release-ledger";
42
+ import { isApplyRecord, latestPerComponent, type ReleaseRecord, type ReleaseRecordInput, type ReleaseAdmission, type RunOrigin } from "../lifecycle/release-ledger";
43
43
  import { CapabilityRegistry, type Capability } from "./capability";
44
44
  import {
45
45
  runInterpretDriver,
@@ -109,19 +109,35 @@ export function selectRelease(
109
109
  env: string,
110
110
  digest?: string,
111
111
  ): { record: ReleaseRecord } | { error: string } {
112
- const own = records.filter((r) => r.component === component);
113
- if (own.length === 0) return { error: `no release of "${component}" is recorded in "${env}"` };
114
- if (digest === undefined) return { record: latestPerComponent(own).get(component)! };
115
- const matching = own.filter((r) => r.digest === digest);
112
+ const own = deployableRecords(records, component, env);
113
+ if ("error" in own) return own;
114
+ if (digest === undefined) return { record: latestPerComponent(own.records).get(component)! };
115
+ const matching = own.records.filter((r) => r.digest === digest);
116
116
  if (matching.length === 0) {
117
117
  return {
118
118
  error: `digest ${digest} is not recorded for "${component}" in "${env}"; ` +
119
- `its recorded digests are ${[...new Set(own.map((r) => r.digest))].join(", ")}`,
119
+ `its recorded digests are ${[...new Set(own.records.map((r) => r.digest))].join(", ")}`,
120
120
  };
121
121
  }
122
122
  return { record: latestPerComponent(matching).get(component)! };
123
123
  }
124
124
 
125
+ /**
126
+ * The records of `component` a promote, rollback or redeploy can deploy from
127
+ * (#4274): every one but those that only say it applied ({@link isApplyRecord}),
128
+ * whose digest is a plan or deploy-input digest and names no artifact.
129
+ */
130
+ function deployableRecords(records: ReleaseRecord[], component: string, env: string): { records: ReleaseRecord[] } | { error: string } {
131
+ const own = records.filter((r) => r.component === component);
132
+ if (own.length === 0) return { error: `no release of "${component}" is recorded in "${env}"` };
133
+ const deployable = own.filter((r) => !isApplyRecord(r));
134
+ if (deployable.length === 0) return { error: appliesOnly(component, env) };
135
+ return { records: deployable };
136
+ }
137
+
138
+ const appliesOnly = (component: string, env: string): string =>
139
+ `"${component}" has only applies recorded in "${env}", each by a plan or deploy-input digest, so no artifact of it can be deployed again`;
140
+
125
141
  /**
126
142
  * Read `--digest <component>=<digest>` values (#2602) into a component-to-
127
143
  * digest map. A value with no `=` is left to the single-component form
@@ -198,13 +214,17 @@ export function planPromotion(input: {
198
214
  return { from, to, items: [{ component, digest: picked.record.digest, source: picked.record }], notPromoted: [] };
199
215
  }
200
216
 
201
- const latest = latestPerComponent(sourceRecords);
217
+ const latest = latestPerComponent(sourceRecords.filter((r) => !isApplyRecord(r)));
202
218
  const items: PromotionItem[] = [];
203
219
  const notPromoted: PromotionPlan["notPromoted"] = [];
204
220
  for (const [name, record] of [...latest].sort(([a], [b]) => a.localeCompare(b))) {
205
221
  if (declared.includes(name)) items.push({ component: name, digest: record.digest, source: record });
206
222
  else notPromoted.push({ component: name, reason: "recorded but not declared in this checkout" });
207
223
  }
224
+ // #4274: a component that only applied (infrastructure, no artifact) has nothing to promote.
225
+ for (const name of [...new Set(sourceRecords.filter((r) => isApplyRecord(r) && !latest.has(r.component)).map((r) => r.component))].sort()) {
226
+ notPromoted.push({ component: name, reason: "only applies are recorded, by a plan or deploy-input digest; there is no artifact to promote" });
227
+ }
208
228
  if (items.length === 0) {
209
229
  return { error: `nothing to promote: no component declared here has a release recorded in "${from}"` };
210
230
  }
@@ -228,9 +248,10 @@ export function planRollback(input: {
228
248
  if (!declared.includes(component)) {
229
249
  return { error: `component "${component}" is not declared in this checkout` };
230
250
  }
231
- const own = records.filter((r) => r.component === component);
232
- const current = latestPerComponent(own).get(component);
233
- if (!current) return { error: `no release of "${component}" is recorded in "${env}"` };
251
+ const deployable = deployableRecords(records, component, env);
252
+ if ("error" in deployable) return deployable;
253
+ const own = deployable.records;
254
+ const current = latestPerComponent(own).get(component)!;
234
255
 
235
256
  let target: ReleaseRecord;
236
257
  if (digest !== undefined) {
@@ -284,7 +305,7 @@ export function planReleaseRollback(input: {
284
305
  notPromoted.push({ component, reason: `in release ${name} but not declared in this checkout` });
285
306
  continue;
286
307
  }
287
- const ran = records.filter((r) => r.component === component && r.releaseDigest === release.releaseDigest);
308
+ const ran = records.filter((r) => r.component === component && r.releaseDigest === release.releaseDigest && !isApplyRecord(r));
288
309
  if (ran.length === 0) {
289
310
  neverRan.push(component);
290
311
  continue;
@@ -299,7 +320,7 @@ export function planReleaseRollback(input: {
299
320
  };
300
321
  }
301
322
  if (items.length === 0) return { error: `release ${name} has no component declared in this checkout` };
302
- const current = latestPerComponent(records);
323
+ const current = latestPerComponent(records.filter((r) => !isApplyRecord(r)));
303
324
  const already = items.every((i) => {
304
325
  const now = current.get(i.component);
305
326
  return now !== undefined && now.releaseDigest === release.releaseDigest && now.digest === i.digest && !input.unsettled?.has(i.component);
@@ -329,8 +350,9 @@ export function planRedeploy(input: {
329
350
  if (!declared.includes(component)) {
330
351
  return { error: `component "${component}" is not declared in this checkout` };
331
352
  }
332
- const current = latestPerComponent(records.filter((r) => r.component === component)).get(component);
333
- if (!current) return { error: `no release of "${component}" is recorded in "${env}"` };
353
+ const deployable = deployableRecords(records, component, env);
354
+ if ("error" in deployable) return deployable;
355
+ const current = latestPerComponent(deployable.records).get(component)!;
334
356
  if (digest !== undefined && digest !== current.digest) {
335
357
  return {
336
358
  error: `${digest} is not the current release of "${component}" in "${env}" (that is ${current.digest}); ` +
@@ -699,6 +721,8 @@ function sharedRecord(item: PromotionItem, env: string, run: DeployRunInfo): Rel
699
721
  ...(run.admission ? { admission: run.admission } : {}),
700
722
  ...(source.manifestDigest ? { manifestDigest: source.manifestDigest } : {}),
701
723
  ...(source.inputDigest ? { inputDigest: source.inputDigest } : {}),
724
+ // #4274: a release's templates digest stays one when it is deployed again.
725
+ ...(source.digestKind ? { digestKind: source.digestKind } : {}),
702
726
  };
703
727
  }
704
728
 
@@ -41,20 +41,21 @@ describe("the gate-decision bundle", () => {
41
41
  "packages/core/src/intrinsic.ts",
42
42
  "packages/core/src/lifecycle/gate-applied.ts",
43
43
  "packages/core/src/lifecycle/gate-ledger-select.ts",
44
+ "packages/core/src/lifecycle/gate-seal.ts",
44
45
  "packages/core/src/lifecycle/plan-digest.ts",
45
46
  "packages/core/src/notify-verify.ts",
46
47
  "packages/core/src/notify.ts",
47
48
  "packages/core/src/op/gate-point.ts",
49
+ "packages/core/src/op/gate-quorum.ts",
48
50
  "packages/core/src/op/op-waves.ts",
49
51
  "packages/core/src/wave-layout.ts",
50
52
  "packages/core/src/workspace/class-data.ts",
51
53
  "packages/core/src/workspace/identity-gate-rule.ts",
52
54
  "packages/core/src/workspace/jsonc.ts",
53
- "packages/core/src/workspace/trust/gate-seal.ts",
54
55
  "packages/core/src/workspace/trust/trust-files.ts",
55
56
  ]);
56
- // About 43 KB unminified today, 7 KB of it the pinned classes read as data (#3888).
57
+ // About 49 KB unminified today: 7 KB of it the pinned classes read as data (#3888), 6 KB the approval rules (#4290).
57
58
  const bytes = result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0);
58
- expect(bytes).toBeLessThan(48 * 1024);
59
+ expect(bytes).toBeLessThan(54 * 1024);
59
60
  });
60
61
  });
@@ -27,6 +27,11 @@
27
27
  * counts for nothing, as terragucci's wave gate has it: the caller hands in
28
28
  * the lines of `_gates/<op>/applied.jsonl` ({@link parseAppliedGateLines}).
29
29
  *
30
+ * Approval rules (#4290), a quorum as a list of `{ targets, count, roles,
31
+ * notAuthor }`, are judged with `./op/gate-quorum.ts`, re-exported here:
32
+ * {@link trustRolesAt} and {@link gateSignersAt} read what
33
+ * `trustQuorumJudge` judges by, at base.
34
+ *
30
35
  * The module reads the ledger lines and the review it is handed; reading
31
36
  * them is the caller's. It imports no TypeScript, esbuild, zod, ajv, lint,
32
37
  * fold or codegen module, no package's code, and none of chant's git ledger
@@ -46,7 +51,7 @@ import {
46
51
  } from "./lifecycle/gate-ledger-select";
47
52
  import { samePlanDigest } from "./lifecycle/plan-digest";
48
53
  import { isApprovalSpent, type AppliedGateRecord } from "./lifecycle/gate-applied";
49
- import { gateSealRefusal, type GateSigners, type SealedGateApproval } from "./workspace/trust/gate-seal";
54
+ import { gateSealRefusal, type GateSigners, type SealedGateApproval } from "./lifecycle/gate-seal";
50
55
  import {
51
56
  DEFAULT_SIGNERS_PATH,
52
57
  TRUST_CONFIG_PATH,
@@ -79,7 +84,7 @@ export {
79
84
  type GateSigners,
80
85
  type SealedGateApproval,
81
86
  type SshSignatureCheck,
82
- } from "./workspace/trust/gate-seal";
87
+ } from "./lifecycle/gate-seal";
83
88
  export {
84
89
  DEFAULT_SIGNERS_PATH,
85
90
  TRUST_CONFIG_PATH,
@@ -105,6 +110,28 @@ export {
105
110
  type AppliedGateRecord,
106
111
  } from "./lifecycle/gate-applied";
107
112
  export type { MergedReview } from "./op/gate-review";
113
+ export {
114
+ quorumNeed,
115
+ quorumProblems,
116
+ quorumRuleLine,
117
+ quorumRuleProgress,
118
+ quorumRules,
119
+ quorumRulesFor,
120
+ quorumWaiting,
121
+ rolesOf,
122
+ ruleNeedsProof,
123
+ samePrincipal,
124
+ targetGlobMatch,
125
+ trustQuorumJudge,
126
+ UNPROVEN_JUDGE,
127
+ type GateQuorumJudge,
128
+ type GateQuorumRule,
129
+ type GateQuorumRuleProgress,
130
+ type GateQuorumSpec,
131
+ type QuorumApprover,
132
+ type QuorumJudgement,
133
+ type TrustQuorumInput,
134
+ } from "./op/gate-quorum";
108
135
 
109
136
  // ── reading at base ──────────────────────────────────────────────────────
110
137
 
@@ -172,6 +199,16 @@ export function gateSignersAt(repo: string, base: string): GateSigners {
172
199
  return trustAt(repo, base).signers;
173
200
  }
174
201
 
202
+ /**
203
+ * The role grants of `.chant/trust.json` at `base` (#4290): role name to the
204
+ * principals that hold it, as chant reads them. A missing file gives none,
205
+ * and so does one that is not valid, which turns chant's trust off.
206
+ * {@link trustQuorumJudge} takes them with {@link gateSignersAt}'s signers.
207
+ */
208
+ export function trustRolesAt(repo: string, base: string): Record<string, string[]> {
209
+ return trustAt(repo, base).roles;
210
+ }
211
+
175
212
  /** A gate the declaration at base names under `identity.gates`, and the principal class its signer must be in, if any. */
176
213
  export interface IdentityGate {
177
214
  gate: string;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The gate policy bundles alone (#4294), as the override gate does: a
3
+ * product runs the engine and reads its answer from one bundled file with
4
+ * no TypeScript toolchain. This bundles `@intentius/chant/gate-policy` and
5
+ * reads esbuild's list of what went in.
6
+ */
7
+
8
+ import { join } from "node:path";
9
+ import { build } from "esbuild";
10
+ import { describe, expect, test } from "vitest";
11
+ import { REPO } from "./workspace/__fixtures__/contract-repo";
12
+
13
+ describe("the gate-policy bundle", () => {
14
+ test("holds the policy module and nothing else", async () => {
15
+ const result = await build({
16
+ entryPoints: [join(REPO, "packages", "core", "src", "gate-policy.ts")],
17
+ bundle: true,
18
+ platform: "node",
19
+ format: "esm",
20
+ write: false,
21
+ metafile: true,
22
+ outdir: "out",
23
+ logLevel: "silent",
24
+ });
25
+ expect(Object.keys(result.metafile.inputs).map((i) => i.replace(/^.*?packages\//, "packages/"))).toEqual(["packages/core/src/gate-policy.ts"]);
26
+ expect(result.outputFiles.reduce((n, f) => n + f.contents.byteLength, 0)).toBeLessThan(16 * 1024);
27
+ });
28
+ });