@intentius/chant 0.124.40 → 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 (281) 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/handlers/approve.d.ts +2 -1
  11. package/dist/cli/handlers/approve.d.ts.map +1 -1
  12. package/dist/cli/handlers/approve.js +20 -6
  13. package/dist/cli/handlers/build.d.ts.map +1 -1
  14. package/dist/cli/handlers/build.js +15 -1
  15. package/dist/cli/handlers/fan-out.js +16 -3
  16. package/dist/cli/handlers/gate-resume.d.ts +3 -0
  17. package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
  18. package/dist/cli/handlers/gate-resume.js +15 -2
  19. package/dist/cli/handlers/lock.d.ts +8 -0
  20. package/dist/cli/handlers/lock.d.ts.map +1 -0
  21. package/dist/cli/handlers/lock.js +197 -0
  22. package/dist/cli/handlers/pr-env.d.ts +34 -0
  23. package/dist/cli/handlers/pr-env.d.ts.map +1 -0
  24. package/dist/cli/handlers/pr-env.js +436 -0
  25. package/dist/cli/handlers/pr.d.ts +74 -0
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +503 -53
  28. package/dist/cli/handlers/promote.d.ts.map +1 -1
  29. package/dist/cli/handlers/promote.js +9 -0
  30. package/dist/cli/handlers/query.d.ts +6 -0
  31. package/dist/cli/handlers/query.d.ts.map +1 -0
  32. package/dist/cli/handlers/query.js +80 -0
  33. package/dist/cli/main.d.ts.map +1 -1
  34. package/dist/cli/main.js +41 -2
  35. package/dist/cli/parse-args.d.ts.map +1 -1
  36. package/dist/cli/parse-args.js +31 -2
  37. package/dist/cli/registry.d.ts +12 -2
  38. package/dist/cli/registry.d.ts.map +1 -1
  39. package/dist/components/fan-out-run.d.ts +28 -0
  40. package/dist/components/fan-out-run.d.ts.map +1 -1
  41. package/dist/components/fan-out-run.js +27 -3
  42. package/dist/components/pr-environment.d.ts +298 -0
  43. package/dist/components/pr-environment.d.ts.map +1 -0
  44. package/dist/components/pr-environment.js +406 -0
  45. package/dist/components/pr-hooks.d.ts +66 -0
  46. package/dist/components/pr-hooks.d.ts.map +1 -0
  47. package/dist/components/pr-hooks.js +119 -0
  48. package/dist/components/pr-pipeline.d.ts +72 -2
  49. package/dist/components/pr-pipeline.d.ts.map +1 -1
  50. package/dist/components/pr-pipeline.js +96 -5
  51. package/dist/components/pr-policy.d.ts +109 -0
  52. package/dist/components/pr-policy.d.ts.map +1 -0
  53. package/dist/components/pr-policy.js +150 -0
  54. package/dist/components/pr-run.d.ts.map +1 -1
  55. package/dist/components/pr-run.js +5 -3
  56. package/dist/gate-decision.d.ts +15 -2
  57. package/dist/gate-decision.d.ts.map +1 -1
  58. package/dist/gate-decision.js +17 -2
  59. package/dist/gate-policy.d.ts +140 -0
  60. package/dist/gate-policy.d.ts.map +1 -0
  61. package/dist/gate-policy.js +242 -0
  62. package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
  63. package/dist/lifecycle/gate-checkpoint.js +1 -1
  64. package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
  65. package/dist/lifecycle/gate-seal.d.ts.map +1 -0
  66. package/dist/lifecycle/query.d.ts +68 -0
  67. package/dist/lifecycle/query.d.ts.map +1 -0
  68. package/dist/lifecycle/query.js +246 -0
  69. package/dist/lifecycle/run-ledger.d.ts +13 -0
  70. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  71. package/dist/lifecycle/run-ledger.js +16 -0
  72. package/dist/lifecycle/target-lock.d.ts +97 -0
  73. package/dist/lifecycle/target-lock.d.ts.map +1 -0
  74. package/dist/lifecycle/target-lock.js +286 -0
  75. package/dist/op/activities/activity-contracts.d.ts +30 -0
  76. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  77. package/dist/op/activities/activity-contracts.js +26 -0
  78. package/dist/op/activities/index.d.ts +2 -0
  79. package/dist/op/activities/index.d.ts.map +1 -1
  80. package/dist/op/activities/index.js +4 -0
  81. package/dist/op/activities/review-agent.d.ts +137 -0
  82. package/dist/op/activities/review-agent.d.ts.map +1 -0
  83. package/dist/op/activities/review-agent.js +266 -0
  84. package/dist/op/builders.d.ts +9 -0
  85. package/dist/op/builders.d.ts.map +1 -1
  86. package/dist/op/builders.js +11 -0
  87. package/dist/op/cron.d.ts +8 -0
  88. package/dist/op/cron.d.ts.map +1 -1
  89. package/dist/op/cron.js +51 -0
  90. package/dist/op/gate-approval.d.ts +20 -13
  91. package/dist/op/gate-approval.d.ts.map +1 -1
  92. package/dist/op/gate-approval.js +13 -18
  93. package/dist/op/gate-quorum.d.ts +131 -0
  94. package/dist/op/gate-quorum.d.ts.map +1 -0
  95. package/dist/op/gate-quorum.js +183 -0
  96. package/dist/op/gate-resume.d.ts +4 -2
  97. package/dist/op/gate-resume.d.ts.map +1 -1
  98. package/dist/op/gate-resume.js +5 -4
  99. package/dist/op/gate-review.js +1 -1
  100. package/dist/op/gate.d.ts +32 -4
  101. package/dist/op/gate.d.ts.map +1 -1
  102. package/dist/op/gate.js +40 -24
  103. package/dist/op/index.d.ts +6 -4
  104. package/dist/op/index.d.ts.map +1 -1
  105. package/dist/op/index.js +4 -3
  106. package/dist/op/local-executor.d.ts.map +1 -1
  107. package/dist/op/local-executor.js +9 -3
  108. package/dist/op/op-waves-run.d.ts +62 -1
  109. package/dist/op/op-waves-run.d.ts.map +1 -1
  110. package/dist/op/op-waves-run.js +190 -11
  111. package/dist/op/op-waves.d.ts +55 -3
  112. package/dist/op/op-waves.d.ts.map +1 -1
  113. package/dist/op/op-waves.js +63 -8
  114. package/dist/op/schedule-overdue.d.ts +57 -0
  115. package/dist/op/schedule-overdue.d.ts.map +1 -0
  116. package/dist/op/schedule-overdue.js +138 -0
  117. package/dist/override-gate.d.ts +2 -2
  118. package/dist/override-gate.d.ts.map +1 -1
  119. package/dist/override-gate.js +1 -1
  120. package/dist/pr-forge.d.ts +36 -0
  121. package/dist/pr-forge.d.ts.map +1 -1
  122. package/dist/pr-forge.js +82 -0
  123. package/dist/pr-loop.d.ts +90 -1
  124. package/dist/pr-loop.d.ts.map +1 -1
  125. package/dist/pr-loop.js +77 -3
  126. package/dist/pr-post.d.ts +72 -0
  127. package/dist/pr-post.d.ts.map +1 -0
  128. package/dist/pr-post.js +134 -0
  129. package/dist/wave-blocked.d.ts +79 -0
  130. package/dist/wave-blocked.d.ts.map +1 -0
  131. package/dist/wave-blocked.js +245 -0
  132. package/dist/wave-engine.d.ts +6 -0
  133. package/dist/wave-engine.d.ts.map +1 -1
  134. package/dist/wave-engine.js +2 -0
  135. package/dist/wave-runtime.mjs +242 -242
  136. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
  137. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
  138. package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
  139. package/dist/workspace/ci-branches.d.ts +51 -0
  140. package/dist/workspace/ci-branches.d.ts.map +1 -0
  141. package/dist/workspace/ci-branches.js +69 -0
  142. package/dist/workspace/ci-cloud-identity.d.ts +11 -0
  143. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
  144. package/dist/workspace/ci-cloud-identity.js +24 -0
  145. package/dist/workspace/ci-locks.d.ts +12 -0
  146. package/dist/workspace/ci-locks.d.ts.map +1 -0
  147. package/dist/workspace/ci-locks.js +6 -0
  148. package/dist/workspace/ci-run.d.ts +17 -1
  149. package/dist/workspace/ci-run.d.ts.map +1 -1
  150. package/dist/workspace/ci-run.js +64 -3
  151. package/dist/workspace/declaration.d.ts +63 -0
  152. package/dist/workspace/declaration.d.ts.map +1 -1
  153. package/dist/workspace/declaration.js +72 -1
  154. package/dist/workspace/declaration.schema.json +147 -0
  155. package/dist/workspace/identity-gate-rule.d.ts +1 -1
  156. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  157. package/dist/workspace/identity-gate-rule.js +1 -1
  158. package/dist/workspace/identity.d.ts +1 -1
  159. package/dist/workspace/identity.d.ts.map +1 -1
  160. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  161. package/dist/workspace/lineage-upgrade.js +8 -5
  162. package/dist/workspace/member-pipeline.d.ts +6 -0
  163. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  164. package/dist/workspace/member-pipeline.js +23 -0
  165. package/dist/workspace/pr-report.schema.json +73 -2
  166. package/dist/workspace/security.d.ts.map +1 -1
  167. package/dist/workspace/security.js +4 -0
  168. package/dist/workspace/status-gates.d.ts +3 -2
  169. package/dist/workspace/status-gates.d.ts.map +1 -1
  170. package/dist/workspace/status-gates.js +8 -7
  171. package/dist/workspace/status.d.ts.map +1 -1
  172. package/dist/workspace/status.js +4 -1
  173. package/dist/workspace/trust/seal.d.ts +2 -2
  174. package/dist/workspace/trust/seal.d.ts.map +1 -1
  175. package/dist/workspace/trust/seal.js +2 -2
  176. package/package.json +1 -1
  177. package/src/__fixtures__/oidc-emulator.ts +118 -0
  178. package/src/ci-handover.test.ts +209 -0
  179. package/src/ci-handover.ts +237 -0
  180. package/src/ci-identity.test.ts +201 -0
  181. package/src/ci-identity.ts +310 -0
  182. package/src/cli/handlers/approve.ts +18 -5
  183. package/src/cli/handlers/build.ts +15 -1
  184. package/src/cli/handlers/fan-out.ts +17 -4
  185. package/src/cli/handlers/gate-resume.ts +16 -2
  186. package/src/cli/handlers/lock.test.ts +98 -0
  187. package/src/cli/handlers/lock.ts +202 -0
  188. package/src/cli/handlers/operator.test.ts +3 -1
  189. package/src/cli/handlers/pr-env.ts +462 -0
  190. package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
  191. package/src/cli/handlers/pr.test.ts +184 -0
  192. package/src/cli/handlers/pr.ts +496 -59
  193. package/src/cli/handlers/promote.ts +9 -0
  194. package/src/cli/handlers/query.ts +78 -0
  195. package/src/cli/main.ts +41 -2
  196. package/src/cli/parse-args.ts +23 -2
  197. package/src/cli/registry.ts +12 -2
  198. package/src/components/fan-out-run.ts +39 -2
  199. package/src/components/fan-out-waves.test.ts +45 -1
  200. package/src/components/pr-environment.test.ts +249 -0
  201. package/src/components/pr-environment.ts +538 -0
  202. package/src/components/pr-hooks.test.ts +118 -0
  203. package/src/components/pr-hooks.ts +157 -0
  204. package/src/components/pr-pipeline.ts +130 -5
  205. package/src/components/pr-policy.test.ts +145 -0
  206. package/src/components/pr-policy.ts +211 -0
  207. package/src/components/pr-run.ts +5 -2
  208. package/src/gate-decision-bundle.test.ts +4 -3
  209. package/src/gate-decision.ts +39 -2
  210. package/src/gate-policy-bundle.test.ts +28 -0
  211. package/src/gate-policy.test.ts +162 -0
  212. package/src/gate-policy.ts +298 -0
  213. package/src/lifecycle/gate-checkpoint.ts +1 -1
  214. package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
  215. package/src/lifecycle/query.test.ts +91 -0
  216. package/src/lifecycle/query.ts +315 -0
  217. package/src/lifecycle/run-ledger.ts +18 -0
  218. package/src/lifecycle/target-lock.test.ts +195 -0
  219. package/src/lifecycle/target-lock.ts +347 -0
  220. package/src/op/activities/activity-contracts.ts +31 -0
  221. package/src/op/activities/index.ts +6 -0
  222. package/src/op/activities/review-agent.test.ts +240 -0
  223. package/src/op/activities/review-agent.ts +355 -0
  224. package/src/op/builders.ts +13 -0
  225. package/src/op/cron.ts +44 -0
  226. package/src/op/gate-alias.test.ts +2 -2
  227. package/src/op/gate-approval.test.ts +30 -9
  228. package/src/op/gate-approval.ts +23 -26
  229. package/src/op/gate-quorum.test.ts +139 -0
  230. package/src/op/gate-quorum.ts +248 -0
  231. package/src/op/gate-resume.ts +6 -3
  232. package/src/op/gate-review.ts +1 -1
  233. package/src/op/gate.ts +64 -23
  234. package/src/op/index.ts +13 -1
  235. package/src/op/local-executor.ts +9 -2
  236. package/src/op/op-waves-hook-targets.test.ts +126 -0
  237. package/src/op/op-waves-policy-hold.test.ts +66 -0
  238. package/src/op/op-waves-quorum.test.ts +101 -0
  239. package/src/op/op-waves-run.ts +233 -11
  240. package/src/op/op-waves.ts +83 -10
  241. package/src/op/schedule-overdue.test.ts +220 -0
  242. package/src/op/schedule-overdue.ts +161 -0
  243. package/src/override-gate-bundle.test.ts +1 -1
  244. package/src/override-gate.ts +2 -2
  245. package/src/pr-forge-bundle.test.ts +2 -2
  246. package/src/pr-forge.test.ts +33 -0
  247. package/src/pr-forge.ts +114 -0
  248. package/src/pr-loop-bundle.test.ts +2 -2
  249. package/src/pr-loop.test.ts +58 -0
  250. package/src/pr-loop.ts +146 -4
  251. package/src/pr-post.test.ts +141 -0
  252. package/src/pr-post.ts +159 -0
  253. package/src/wave-blocked-bundle.test.ts +25 -0
  254. package/src/wave-blocked.test.ts +146 -0
  255. package/src/wave-blocked.ts +271 -0
  256. package/src/wave-engine.ts +8 -0
  257. package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
  258. package/src/workspace/ci-branches.test.ts +62 -0
  259. package/src/workspace/ci-branches.ts +79 -0
  260. package/src/workspace/ci-cloud-identity.test.ts +53 -0
  261. package/src/workspace/ci-cloud-identity.ts +23 -0
  262. package/src/workspace/ci-locks.ts +16 -0
  263. package/src/workspace/ci-run-index.test.ts +2 -1
  264. package/src/workspace/ci-run.test.ts +107 -8
  265. package/src/workspace/ci-run.ts +72 -4
  266. package/src/workspace/declaration.schema.json +147 -0
  267. package/src/workspace/declaration.test.ts +76 -0
  268. package/src/workspace/declaration.ts +134 -2
  269. package/src/workspace/identity-gate-rule.ts +1 -1
  270. package/src/workspace/identity.ts +1 -1
  271. package/src/workspace/lineage-upgrade.ts +11 -8
  272. package/src/workspace/member-pipeline.test.ts +29 -0
  273. package/src/workspace/member-pipeline.ts +30 -0
  274. package/src/workspace/pr-report.schema.json +73 -2
  275. package/src/workspace/security.ts +4 -0
  276. package/src/workspace/status-gates.ts +9 -6
  277. package/src/workspace/status.ts +4 -1
  278. package/src/workspace/trust/gate-seal.test.ts +1 -1
  279. package/src/workspace/trust/seal.ts +2 -2
  280. package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
  281. /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
  });
@@ -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
+ });
@@ -0,0 +1,162 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ assertGatePolicySettings,
6
+ checkGatePolicy,
7
+ conftestFindings,
8
+ exportGatePolicy,
9
+ gatePolicyArgv,
10
+ opaFindings,
11
+ type GatePolicyExec,
12
+ type GatePolicyTree,
13
+ } from "./gate-policy";
14
+
15
+ /** conftest's JSON for one input: a deny with its query, an older deny without one, and a warning. */
16
+ const CONFTEST_DENY = JSON.stringify([
17
+ {
18
+ filename: "input.json",
19
+ namespace: "main",
20
+ successes: 2,
21
+ failures: [
22
+ { msg: "bucket logs is public", metadata: { query: "data.main.deny_public_bucket" } },
23
+ { msg: { msg: "no owner tag" } },
24
+ ],
25
+ warnings: [{ msg: "the plan deletes 3 resources" }],
26
+ },
27
+ ]);
28
+
29
+ const opa = (value: unknown): string => JSON.stringify({ result: [{ expressions: [{ value, text: "data.main", location: { row: 1, col: 1 } }] }] });
30
+
31
+ describe("reading the engine's answer (#4294)", () => {
32
+ test("conftest: failures deny by their query, warnings are advice", () => {
33
+ expect(conftestFindings(CONFTEST_DENY)).toEqual({
34
+ denials: [
35
+ { rule: "main.deny_public_bucket", message: "bucket logs is public" },
36
+ { rule: "main.deny", message: "no owner tag" },
37
+ ],
38
+ holds: [],
39
+ warnings: ["the plan deletes 3 resources"],
40
+ });
41
+ expect(conftestFindings(JSON.stringify([{ filename: "input.json", namespace: "main", successes: 1 }]))).toEqual({ denials: [], holds: [], warnings: [] });
42
+ expect(conftestFindings("not json")).toBeUndefined();
43
+ expect(conftestFindings(JSON.stringify({ result: [] }))).toBeUndefined();
44
+ });
45
+
46
+ test("opa: deny, violation, hold and warn rules by name; a child package one level down; allow_unattended is no rule", () => {
47
+ const found = opaFindings(
48
+ opa({
49
+ deny: ["public bucket"],
50
+ violation_size: "too many deletes",
51
+ hold_iam: ["an IAM role changes"],
52
+ hold: true,
53
+ warn: ["consider tags"],
54
+ allow_unattended: true,
55
+ allow_unattended_docs: ["docs only"],
56
+ other: 3,
57
+ aws: { deny_region: ["us-west-1 is not allowed"] },
58
+ }),
59
+ );
60
+ expect(found).toEqual({
61
+ denials: [
62
+ { rule: "main.aws.deny_region", message: "us-west-1 is not allowed" },
63
+ { rule: "main.deny", message: "public bucket" },
64
+ { rule: "main.violation_size", message: "too many deletes" },
65
+ ],
66
+ holds: [
67
+ { rule: "main.hold", message: "hold" },
68
+ { rule: "main.hold_iam", message: "an IAM role changes" },
69
+ ],
70
+ warnings: ["consider tags"],
71
+ });
72
+ });
73
+
74
+ test("opa: an undefined query matches nothing, an array is a deny set, errors and other shapes are no answer", () => {
75
+ expect(opaFindings(JSON.stringify({}))).toEqual({ denials: [], holds: [], warnings: [] });
76
+ expect(opaFindings(opa(["a", { msg: "b" }]), "policy.deny")).toEqual({ denials: [{ rule: "policy.deny", message: "a" }, { rule: "policy.deny", message: "b" }], holds: [], warnings: [] });
77
+ expect(opaFindings(JSON.stringify({ errors: [{ message: "rego_parse_error" }] }))).toBeUndefined();
78
+ expect(opaFindings(opa(7))).toBeUndefined();
79
+ expect(opaFindings("{")).toBeUndefined();
80
+ });
81
+ });
82
+
83
+ describe("running the engine (#4294)", () => {
84
+ const fake = (out: { status: number | null; stdout?: string; stderr?: string }, seen: string[][] = []): GatePolicyExec => (argv) => {
85
+ seen.push(argv);
86
+ return { status: out.status, stdout: out.stdout ?? "", stderr: out.stderr ?? "" };
87
+ };
88
+
89
+ test("the engines' own argv, and a command's placeholders filled", () => {
90
+ expect(gatePolicyArgv({}, { policy: "/p", input: "/i.json" })).toEqual(["conftest", "test", "--no-color", "--output", "json", "--policy", "/p", "--all-namespaces", "/i.json"]);
91
+ expect(gatePolicyArgv({ engine: "conftest", namespace: "infra" }, { policy: "/p", input: "/i.json" })).toContain("--namespace");
92
+ expect(gatePolicyArgv({ engine: "opa" }, { policy: "/p", input: "/i.json" })).toEqual(["opa", "eval", "--format", "json", "--data", "/p", "--input", "/i.json", "data.main"]);
93
+ expect(gatePolicyArgv({ engine: "opa", command: ["./bin/check", "--rules={policy}", "{input}"] }, { policy: "/p", input: "/i.json" })).toEqual(["./bin/check", "--rules=/p", "/i.json"]);
94
+ });
95
+
96
+ test("the input is written to the file the engine reads, and a denial is a verdict with its sorted rules", () => {
97
+ let input: unknown;
98
+ const exec: GatePolicyExec = (argv) => {
99
+ input = JSON.parse(readFileSync(argv[argv.length - 1]!, "utf-8"));
100
+ return { status: 1, stdout: CONFTEST_DENY, stderr: "" };
101
+ };
102
+ const verdict = checkGatePolicy({ settings: {}, input: { changeSet: { entries: [] } }, policyDir: "/p", exec });
103
+ expect(input).toEqual({ changeSet: { entries: [] } });
104
+ expect(verdict).toMatchObject({ result: "denied", engine: "conftest", rules: ["main.deny", "main.deny_public_bucket"], warnings: ["the plan deletes 3 resources"] });
105
+ });
106
+
107
+ test("a hold with no denial is held; nothing is passed", () => {
108
+ expect(checkGatePolicy({ settings: { engine: "opa" }, input: {}, policyDir: "/p", exec: fake({ status: 0, stdout: opa({ hold_iam: ["iam"] }) }) })).toMatchObject({ result: "held", holds: [{ rule: "main.hold_iam" }] });
109
+ expect(checkGatePolicy({ settings: { engine: "opa" }, input: {}, policyDir: "/p", exec: fake({ status: 0, stdout: opa({ deny: [], warn: [] }) }) })).toMatchObject({ result: "passed", denials: [], holds: [] });
110
+ expect(checkGatePolicy({ settings: { engine: "opa" }, input: {}, policyDir: "/p", exec: fake({ status: 0, stdout: opa({ deny: ["x"], hold: true }) }) }).result).toBe("denied");
111
+ });
112
+
113
+ test("an engine that does not start, prints no result or fails without a denial is an error", () => {
114
+ expect(checkGatePolicy({ settings: {}, input: {}, policyDir: "/p", exec: fake({ status: null, stderr: "spawn conftest ENOENT" }) })).toMatchObject({ result: "error", error: expect.stringContaining("did not run") });
115
+ expect(checkGatePolicy({ settings: {}, input: {}, policyDir: "/p", exec: fake({ status: 2, stdout: "Error: rego_parse_error" }) })).toMatchObject({ result: "error", error: expect.stringContaining("gave no conftest result") });
116
+ expect(checkGatePolicy({ settings: {}, input: {}, policyDir: "/p", exec: fake({ status: 2, stdout: "[]", stderr: "could not load policy" }) })).toMatchObject({ result: "error", error: expect.stringContaining("exited 2") });
117
+ });
118
+ });
119
+
120
+ describe("the policy at base (#4294)", () => {
121
+ const files: Record<string, string> = { "policy/main.rego": "package main\n", "policy/lib/util.rego": "package lib\n", "src/app.ts": "" };
122
+ const tree: GatePolicyTree = {
123
+ stat: (p) => (files[p] !== undefined ? "file" : Object.keys(files).some((f) => f.startsWith(`${p}/`)) ? "dir" : undefined),
124
+ list: (p) => {
125
+ const names = new Map<string, "file" | "dir">();
126
+ for (const f of Object.keys(files).filter((f) => f.startsWith(`${p}/`))) {
127
+ const rest = f.slice(p.length + 1).split("/");
128
+ names.set(rest[0]!, rest.length > 1 ? "dir" : "file");
129
+ }
130
+ return names.size ? [...names].map(([name, type]) => ({ name, type })) : undefined;
131
+ },
132
+ read: (p) => files[p]!,
133
+ };
134
+
135
+ test("the directory is copied out of the tree, and removed again", () => {
136
+ const out = exportGatePolicy(tree, "./policy/");
137
+ if ("error" in out) throw new Error(out.error);
138
+ expect(readFileSync(join(out.dir, "main.rego"), "utf-8")).toBe("package main\n");
139
+ expect(readFileSync(join(out.dir, "lib", "util.rego"), "utf-8")).toBe("package lib\n");
140
+ expect(existsSync(join(out.dir, "app.ts"))).toBe(false);
141
+ out.cleanup();
142
+ expect(existsSync(out.dir)).toBe(false);
143
+ });
144
+
145
+ test("a directory the base does not have is an error", () => {
146
+ expect(exportGatePolicy(tree, "rules")).toEqual({ error: "the policy directory rules does not exist at base" });
147
+ });
148
+ });
149
+
150
+ describe("ci.policy settings (#4294)", () => {
151
+ test("the four settings are read, and an unattended outcome is refused", () => {
152
+ expect(() => assertGatePolicySettings("ci.policy", { engine: "opa", path: "policy", namespace: "main", command: ["opa", "eval", "{input}"] })).not.toThrow();
153
+ expect(() => assertGatePolicySettings("ci.policy", {})).not.toThrow();
154
+ expect(() => assertGatePolicySettings("ci.policy", { engine: "sentinel" })).toThrow(/conftest or opa/);
155
+ expect(() => assertGatePolicySettings("ci.policy", { path: "../elsewhere" })).toThrow(/inside the workspace/);
156
+ expect(() => assertGatePolicySettings("ci.policy", { command: ["check"] })).toThrow(/\{input\}/);
157
+ expect(() => assertGatePolicySettings("ci.policy", { outcomes: ["deny", "allow_unattended"] })).toThrow(/no unattended outcome/);
158
+ expect(() => assertGatePolicySettings("ci.policy", { allow_unattended: true })).toThrow(/no unattended outcome/);
159
+ expect(() => assertGatePolicySettings("ci.policy", { rules: [] })).toThrow(/not a setting/);
160
+ expect(() => assertGatePolicySettings("ci.policy", [])).toThrow(/must be/);
161
+ });
162
+ });