@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,131 @@
1
+ /**
2
+ * Approval rules (#4290): a gate's quorum as a list of rules, each with the
3
+ * targets it covers, how many approvers it needs, the roles that count, and
4
+ * whether one of them must not be the change's author.
5
+ *
6
+ * quorum: [
7
+ * { targets: ["prod-*"], count: 2, roles: ["platform"], notAuthor: true },
8
+ * { count: 1 },
9
+ * ]
10
+ *
11
+ * A gate takes every rule whose `targets` glob matches one of its targets
12
+ * (a rule with no `targets` covers every target), and passes once each rule
13
+ * it takes is met by distinct human approvers of its plan. A gate that takes
14
+ * no rule needs one human approval, the rule every gate had before.
15
+ *
16
+ * Roles come from `.chant/trust.json` at base (`roles`: role name to the
17
+ * principals that hold it), never from the approver's own claim. An
18
+ * approver counts toward `roles`, and as someone who is not the author, only
19
+ * when the approval proves who they are: a seal that verifies against the
20
+ * signers file at base, or under `pr-review` the forge review of the merged
21
+ * pull request. The judge of a gate decides that ({@link GateQuorumJudge});
22
+ * with none, no approval proves anything, so a rule naming roles or
23
+ * `notAuthor` is never met.
24
+ *
25
+ * This module is pure: no git, no file reads, no package, so
26
+ * `@intentius/chant/gate-decision` bundles it. The judge is built by the
27
+ * caller from the trust policy it read at base ({@link trustQuorumJudge}).
28
+ */
29
+ import { type GateSigners } from "../lifecycle/gate-seal.js";
30
+ import type { GateResolutionRecord } from "../lifecycle/gate-ledger.js";
31
+ /** One approval rule. */
32
+ export interface GateQuorumRule {
33
+ /** Globs over the gate's targets (an Op wave's run targets, an Op gate's environment). Absent, the rule covers every target. */
34
+ targets?: string[];
35
+ /** Distinct human approvers the rule needs. At least 1. */
36
+ count: number;
37
+ /** When set, only an approver `.chant/trust.json` at base gives one of these roles counts, and only with a proven approval. */
38
+ roles?: string[];
39
+ /** When true, at least one counted approver is proven and is not the change's author. */
40
+ notAuthor?: boolean;
41
+ }
42
+ /** A gate's quorum: one rule (the form before #4290), or a list of them. */
43
+ export type GateQuorumSpec = GateQuorumRule | readonly GateQuorumRule[];
44
+ /** What a judge says about one approval. */
45
+ export interface QuorumJudgement {
46
+ /** The approval proves who `resolvedBy` is: a verified seal, or the forge's review. */
47
+ proven: boolean;
48
+ /** The roles `.chant/trust.json` at base gives the approver. Empty unless `proven`. */
49
+ roles: string[];
50
+ /** Whether the approver is the change's author. Undefined when nobody knows who the author is. */
51
+ author?: boolean;
52
+ }
53
+ /** Judges one approval for the rules. */
54
+ export type GateQuorumJudge = (approval: GateResolutionRecord) => QuorumJudgement;
55
+ /** The judge with nothing to judge by: no approval proves anyone, and nobody's author is known. */
56
+ export declare const UNPROVEN_JUDGE: GateQuorumJudge;
57
+ /** How far one rule is met. */
58
+ export interface GateQuorumRuleProgress {
59
+ targets?: string[];
60
+ count: number;
61
+ roles?: string[];
62
+ notAuthor?: boolean;
63
+ /** The approvers it counts, oldest first. */
64
+ approvers: string[];
65
+ met: boolean;
66
+ /** Why the author could not be told apart, when `notAuthor` is set and nobody knows the author. */
67
+ authorUnknown?: boolean;
68
+ }
69
+ /** The rules of a quorum, as a list. Absent, none. */
70
+ export declare function quorumRules(quorum: GateQuorumSpec | undefined): GateQuorumRule[];
71
+ /**
72
+ * Whether `target` matches `glob`: `*` within a `/` segment, `**` across
73
+ * segments (and `a/**\/b` across none), `?` one character. The same reading
74
+ * as the terraform lexicon's unit globs, so a Terragrunt root's glob means
75
+ * one thing in both.
76
+ */
77
+ export declare function targetGlobMatch(glob: string, target: string): boolean;
78
+ /** The rules a gate whose targets are `targets` takes: those with no `targets`, and those a glob of matches one. */
79
+ export declare function quorumRulesFor(rules: readonly GateQuorumRule[], targets: readonly string[]): GateQuorumRule[];
80
+ /** The most approvers any rule needs, 1 with none. */
81
+ export declare function quorumNeed(rules: readonly GateQuorumRule[]): number;
82
+ /**
83
+ * Everything wrong with a quorum, one line each. `where` names it
84
+ * (`approval.quorum`). With `notAuthor: false`, a rule naming `notAuthor` is
85
+ * refused, since that caller has no author to judge by.
86
+ */
87
+ export declare function quorumProblems(value: unknown, where: string, opts: {
88
+ notAuthor: boolean;
89
+ }): string[];
90
+ /** Whether two principals name the same person, compared trimmed and lower-cased. */
91
+ export declare const samePrincipal: (a: string, b: string) => boolean;
92
+ /** The roles `roles` (`.chant/trust.json`'s role grants) gives `who`, sorted. */
93
+ export declare function rolesOf(who: string, roles: Readonly<Record<string, readonly string[]>>): string[];
94
+ /** One approver as a rule counts them. */
95
+ export interface QuorumApprover {
96
+ who: string;
97
+ at: string;
98
+ judged: QuorumJudgement;
99
+ }
100
+ /** How far `rule` is met by `approvers`: each counted once, by name, oldest first. */
101
+ export declare function quorumRuleProgress(rule: GateQuorumRule, approvers: readonly QuorumApprover[]): GateQuorumRuleProgress;
102
+ /**
103
+ * Where a rule stands, in one line: `prod-*: 1 of 2 approvals (github:alice),
104
+ * needs 1 more from platform`.
105
+ */
106
+ export declare function quorumRuleLine(p: GateQuorumRuleProgress): string;
107
+ /** The rules that are not met, one line each joined by `; `. */
108
+ export declare function quorumWaiting(progress: readonly GateQuorumRuleProgress[]): string;
109
+ /** Whether a rule needs a proven approver: it names roles, or `notAuthor`. */
110
+ export declare const ruleNeedsProof: (rule: GateQuorumRule) => boolean;
111
+ /** What {@link trustQuorumJudge} judges by. */
112
+ export interface TrustQuorumInput {
113
+ /** The signers at base: an approval whose seal verifies for its approver against them is proven. */
114
+ signers: GateSigners;
115
+ /** `.chant/trust.json`'s role grants at base. */
116
+ roles: Readonly<Record<string, readonly string[]>>;
117
+ /** The change's authors, as principals. Absent, nobody knows who they are. */
118
+ authors?: readonly string[];
119
+ /**
120
+ * Under `pr-review`, the merged pull request's review, read from the forge
121
+ * in this run: a resolution that names this review's head and one of its
122
+ * approvers as `resolvedBy` is proven.
123
+ */
124
+ review?: {
125
+ head: string;
126
+ approvers: readonly string[];
127
+ };
128
+ }
129
+ /** The judge a gate's rules are met by: proof from a seal or the live review, roles from trust.json at base. */
130
+ export declare function trustQuorumJudge(input: TrustQuorumInput): GateQuorumJudge;
131
+ //# sourceMappingURL=gate-quorum.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-quorum.d.ts","sourceRoot":"","sources":["../../src/op/gate-quorum.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Approval rules (#4290): a gate's quorum as a list of rules, each with the
3
+ * targets it covers, how many approvers it needs, the roles that count, and
4
+ * whether one of them must not be the change's author.
5
+ *
6
+ * quorum: [
7
+ * { targets: ["prod-*"], count: 2, roles: ["platform"], notAuthor: true },
8
+ * { count: 1 },
9
+ * ]
10
+ *
11
+ * A gate takes every rule whose `targets` glob matches one of its targets
12
+ * (a rule with no `targets` covers every target), and passes once each rule
13
+ * it takes is met by distinct human approvers of its plan. A gate that takes
14
+ * no rule needs one human approval, the rule every gate had before.
15
+ *
16
+ * Roles come from `.chant/trust.json` at base (`roles`: role name to the
17
+ * principals that hold it), never from the approver's own claim. An
18
+ * approver counts toward `roles`, and as someone who is not the author, only
19
+ * when the approval proves who they are: a seal that verifies against the
20
+ * signers file at base, or under `pr-review` the forge review of the merged
21
+ * pull request. The judge of a gate decides that ({@link GateQuorumJudge});
22
+ * with none, no approval proves anything, so a rule naming roles or
23
+ * `notAuthor` is never met.
24
+ *
25
+ * This module is pure: no git, no file reads, no package, so
26
+ * `@intentius/chant/gate-decision` bundles it. The judge is built by the
27
+ * caller from the trust policy it read at base ({@link trustQuorumJudge}).
28
+ */
29
+ import { gateSealRefusal } from "../lifecycle/gate-seal.js";
30
+ /** The judge with nothing to judge by: no approval proves anyone, and nobody's author is known. */
31
+ export const UNPROVEN_JUDGE = () => ({ proven: false, roles: [] });
32
+ /** The rules of a quorum, as a list. Absent, none. */
33
+ export function quorumRules(quorum) {
34
+ if (quorum === undefined)
35
+ return [];
36
+ return Array.isArray(quorum) ? [...quorum] : [quorum];
37
+ }
38
+ /**
39
+ * Whether `target` matches `glob`: `*` within a `/` segment, `**` across
40
+ * segments (and `a/**\/b` across none), `?` one character. The same reading
41
+ * as the terraform lexicon's unit globs, so a Terragrunt root's glob means
42
+ * one thing in both.
43
+ */
44
+ export function targetGlobMatch(glob, target) {
45
+ const g = glob.replace(/\\/g, "/").replace(/\/+$/, "");
46
+ let re = "";
47
+ for (let i = 0; i < g.length; i++) {
48
+ const c = g[i];
49
+ if (c === "*" && g[i + 1] === "*") {
50
+ if (g[i + 2] === "/") {
51
+ re += "(?:.*/)?";
52
+ i += 2;
53
+ }
54
+ else {
55
+ re += ".*";
56
+ i += 1;
57
+ }
58
+ }
59
+ else if (c === "*")
60
+ re += "[^/]*";
61
+ else if (c === "?")
62
+ re += "[^/]";
63
+ else
64
+ re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
65
+ }
66
+ return new RegExp(`^${re}$`).test(target.replace(/\\/g, "/").replace(/\/+$/, ""));
67
+ }
68
+ /** The rules a gate whose targets are `targets` takes: those with no `targets`, and those a glob of matches one. */
69
+ export function quorumRulesFor(rules, targets) {
70
+ return rules.filter((r) => r.targets === undefined || r.targets.some((g) => targets.some((t) => targetGlobMatch(g, t))));
71
+ }
72
+ /** The most approvers any rule needs, 1 with none. */
73
+ export function quorumNeed(rules) {
74
+ return rules.reduce((n, r) => Math.max(n, r.count), 1);
75
+ }
76
+ /**
77
+ * Everything wrong with a quorum, one line each. `where` names it
78
+ * (`approval.quorum`). With `notAuthor: false`, a rule naming `notAuthor` is
79
+ * refused, since that caller has no author to judge by.
80
+ */
81
+ export function quorumProblems(value, where, opts) {
82
+ const list = Array.isArray(value) ? value : [value];
83
+ const problems = [];
84
+ if (Array.isArray(value) && value.length === 0)
85
+ problems.push(`\`${where}\` must name at least one rule`);
86
+ list.forEach((rule, i) => {
87
+ const at = Array.isArray(value) ? `${where}[${i}]` : where;
88
+ if (typeof rule !== "object" || rule === null || Array.isArray(rule)) {
89
+ problems.push(`\`${at}\` must be an object`);
90
+ return;
91
+ }
92
+ const q = rule;
93
+ if (typeof q.count !== "number" || !Number.isInteger(q.count) || q.count < 1)
94
+ problems.push(`\`${at}.count\` must be an integer of at least 1`);
95
+ if (q.roles !== undefined && (!Array.isArray(q.roles) || q.roles.length === 0 || q.roles.some((r) => typeof r !== "string" || r === ""))) {
96
+ problems.push(`\`${at}.roles\` must be a non-empty list of role names`);
97
+ }
98
+ if (q.targets !== undefined && (!Array.isArray(q.targets) || q.targets.length === 0 || q.targets.some((t) => typeof t !== "string" || t === ""))) {
99
+ problems.push(`\`${at}.targets\` must be a non-empty list of globs`);
100
+ }
101
+ if (q.notAuthor !== undefined && typeof q.notAuthor !== "boolean")
102
+ problems.push(`\`${at}.notAuthor\` must be true or false`);
103
+ if (q.notAuthor === true && !opts.notAuthor) {
104
+ problems.push(`\`${at}.notAuthor\` is not read here: an Op gate has no author to judge by. An Op waves spec's \`approvals.quorum\` reads it from the merged pull request`);
105
+ }
106
+ const known = new Set(["targets", "count", "roles", "notAuthor"]);
107
+ for (const key of Object.keys(q))
108
+ if (!known.has(key))
109
+ problems.push(`\`${at}.${key}\` is not a quorum field; use targets, count, roles or notAuthor`);
110
+ });
111
+ return problems;
112
+ }
113
+ const norm = (s) => s.normalize("NFKC").trim().toLowerCase();
114
+ /** Whether two principals name the same person, compared trimmed and lower-cased. */
115
+ export const samePrincipal = (a, b) => norm(a) === norm(b);
116
+ /** The roles `roles` (`.chant/trust.json`'s role grants) gives `who`, sorted. */
117
+ export function rolesOf(who, roles) {
118
+ return Object.entries(roles)
119
+ .filter(([, principals]) => principals.some((p) => samePrincipal(p, who)))
120
+ .map(([role]) => role)
121
+ .sort();
122
+ }
123
+ /** How far `rule` is met by `approvers`: each counted once, by name, oldest first. */
124
+ export function quorumRuleProgress(rule, approvers) {
125
+ const seen = new Set();
126
+ const counted = [];
127
+ for (const a of [...approvers].sort((x, y) => Date.parse(x.at) - Date.parse(y.at))) {
128
+ if (seen.has(norm(a.who)))
129
+ continue;
130
+ if (rule.roles?.length && !(a.judged.proven && a.judged.roles.some((r) => rule.roles.includes(r))))
131
+ continue;
132
+ seen.add(norm(a.who));
133
+ counted.push(a);
134
+ }
135
+ const notAuthor = !rule.notAuthor || counted.some((a) => a.judged.proven && a.judged.author === false);
136
+ const authorUnknown = rule.notAuthor === true && !notAuthor && counted.some((a) => a.judged.proven && a.judged.author === undefined);
137
+ return {
138
+ ...(rule.targets ? { targets: [...rule.targets] } : {}),
139
+ count: rule.count,
140
+ ...(rule.roles?.length ? { roles: [...rule.roles] } : {}),
141
+ ...(rule.notAuthor ? { notAuthor: true } : {}),
142
+ approvers: counted.map((a) => a.who),
143
+ met: counted.length >= rule.count && notAuthor,
144
+ ...(authorUnknown ? { authorUnknown: true } : {}),
145
+ };
146
+ }
147
+ /**
148
+ * Where a rule stands, in one line: `prod-*: 1 of 2 approvals (github:alice),
149
+ * needs 1 more from platform`.
150
+ */
151
+ export function quorumRuleLine(p) {
152
+ const scope = p.targets ? p.targets.join(", ") : "every target";
153
+ const have = `${p.approvers.length} of ${p.count} approval${p.count === 1 ? "" : "s"}${p.approvers.length ? ` (${p.approvers.join(", ")})` : ""}`;
154
+ if (p.met)
155
+ return `${scope}: ${have}, met`;
156
+ const more = p.count - p.approvers.length;
157
+ const needs = [];
158
+ if (more > 0)
159
+ needs.push(`${more} more${p.roles?.length ? ` from ${p.roles.join(" or ")}` : ""}`);
160
+ if (p.notAuthor)
161
+ needs.push(more > 0 ? "one of them not the author" : "an approver who is not the author");
162
+ const unknown = p.authorUnknown ? " (the change's author is not known, so no approver can show they are someone else)" : "";
163
+ return `${scope}: ${have}, needs ${needs.join(", ")}${unknown}`;
164
+ }
165
+ /** The rules that are not met, one line each joined by `; `. */
166
+ export function quorumWaiting(progress) {
167
+ return progress.filter((p) => !p.met).map(quorumRuleLine).join("; ");
168
+ }
169
+ /** Whether a rule needs a proven approver: it names roles, or `notAuthor`. */
170
+ export const ruleNeedsProof = (rule) => (rule.roles?.length ?? 0) > 0 || rule.notAuthor === true;
171
+ /** The judge a gate's rules are met by: proof from a seal or the live review, roles from trust.json at base. */
172
+ export function trustQuorumJudge(input) {
173
+ return (r) => {
174
+ const sealed = r.seal !== undefined && r.seal !== null && gateSealRefusal(input.signers, r) === null;
175
+ const reviewed = input.review !== undefined && r.review?.head === input.review.head && input.review.approvers.some((a) => samePrincipal(a, r.resolvedBy));
176
+ const proven = sealed || reviewed;
177
+ return {
178
+ proven,
179
+ roles: proven ? rolesOf(r.resolvedBy, input.roles) : [],
180
+ ...(input.authors !== undefined ? { author: input.authors.some((a) => samePrincipal(a, r.resolvedBy)) } : {}),
181
+ };
182
+ };
183
+ }
@@ -8,6 +8,7 @@
8
8
  * `./gate`'s quorum rules.
9
9
  */
10
10
  import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-ledger.js";
11
+ import type { GateQuorumJudge } from "./gate-quorum.js";
11
12
  export { isGateRunLocator, resolveGateRunLocator, resumeGateRun, resumeTokenFromEnv, type GateResumeOutcome, type GateRunLocator, type ResumeGateRunOptions, } from "../gate-resume-run.js";
12
13
  /**
13
14
  * The resolution that answers `standing`, read the way a run decides the gate
@@ -15,7 +16,8 @@ export { isGateRunLocator, resolveGateRunLocator, resumeGateRun, resumeTokenFrom
15
16
  * gate ledger's pending facts, which place the human quorum's anchor (#3206);
16
17
  * without them it is `standing` itself. A workspace's
17
18
  * `identity.gates` rule is not applied here: the resumed job applies it, and
18
- * waits again when the approval does not count.
19
+ * waits again when the approval does not count. `judge` proves approvers
20
+ * for approval rules that name roles (#4290); absent, none is proven.
19
21
  */
20
- export declare function resolutionAnswering(resolutions: readonly GateResolutionRecord[], standing: PendingGateRecord, pending?: readonly PendingGateRecord[]): GateResolutionRecord | undefined;
22
+ export declare function resolutionAnswering(resolutions: readonly GateResolutionRecord[], standing: PendingGateRecord, pending?: readonly PendingGateRecord[], judge?: GateQuorumJudge): GateResolutionRecord | undefined;
21
23
  //# sourceMappingURL=gate-resume.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gate-resume.d.ts","sourceRoot":"","sources":["../../src/op/gate-resume.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAIxF,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,aAAa,EACb,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,oBAAoB,EAAE,EAC5C,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,GAAE,SAAS,iBAAiB,EAAe,GACjD,oBAAoB,GAAG,SAAS,CASlC"}
1
+ {"version":3,"file":"gate-resume.d.ts","sourceRoot":"","sources":["../../src/op/gate-resume.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAGxF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAErD,OAAO,EACL,gBAAgB,EAChB,qBAAqB,EACrB,aAAa,EACb,kBAAkB,EAClB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,oBAAoB,GAC1B,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,oBAAoB,EAAE,EAC5C,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,GAAE,SAAS,iBAAiB,EAAe,EAClD,KAAK,CAAC,EAAE,eAAe,GACtB,oBAAoB,GAAG,SAAS,CASlC"}
@@ -16,16 +16,17 @@ export { isGateRunLocator, resolveGateRunLocator, resumeGateRun, resumeTokenFrom
16
16
  * gate ledger's pending facts, which place the human quorum's anchor (#3206);
17
17
  * without them it is `standing` itself. A workspace's
18
18
  * `identity.gates` rule is not applied here: the resumed job applies it, and
19
- * waits again when the approval does not count.
19
+ * waits again when the approval does not count. `judge` proves approvers
20
+ * for approval rules that name roles (#4290); absent, none is proven.
20
21
  */
21
- export function resolutionAnswering(resolutions, standing, pending = [standing]) {
22
+ export function resolutionAnswering(resolutions, standing, pending = [standing], judge) {
22
23
  const env = standing.environment;
23
24
  const mine = resolutions.filter((r) => env === undefined || r.environment === env);
24
25
  if (standing.approval) {
25
- const tally = tallyGateApprovals(mine, standing.gate, standing.timestamp, standing.planDigest, standing.approval, humanQuorumSince(pending, standing));
26
+ const tally = tallyGateApprovals(mine, standing.gate, standing.timestamp, standing.planDigest, standing.approval, humanQuorumSince(pending, standing), judge);
26
27
  if (tally.permit)
27
28
  return tally.permit;
28
- return tally.counted.length >= tally.need ? tally.counted[tally.counted.length - 1] : undefined;
29
+ return tally.met ? tally.counted[tally.counted.length - 1] : undefined;
29
30
  }
30
31
  return latestResolutionForPlan(mine, standing.gate, standing.timestamp, standing.planDigest).resolution;
31
32
  }
@@ -29,7 +29,7 @@
29
29
  * back. The author's own review never counts.
30
30
  */
31
31
  import { readFileSync } from "node:fs";
32
- import { gateSealRefusal } from "../workspace/trust/gate-seal.js";
32
+ import { gateSealRefusal } from "../lifecycle/gate-seal.js";
33
33
  import { readTrustPolicy } from "../workspace/trust/policy.js";
34
34
  import { gitRevisionSource } from "../workspace/record-source.js";
35
35
  import { forgePrincipalOf, ForgeApiError } from "../pr-forge.js";
package/dist/op/gate.d.ts CHANGED
@@ -34,6 +34,7 @@
34
34
  import { type GateResolutionInput, type GateResolutionRecord, type PendingGateInput, type PendingGateRecord } from "../lifecycle/gate-ledger.js";
35
35
  import { type AppliedGateRecord } from "../lifecycle/gate-applied.js";
36
36
  import { type GateApprover, type ResolvedGateApproval } from "./gate-approval.js";
37
+ import { type GateQuorumJudge, type GateQuorumRuleProgress } from "./gate-quorum.js";
37
38
  import { type GateCheckpoint } from "../lifecycle/gate-checkpoint.js";
38
39
  import type { ResolvedGateAlias } from "./gate-alias.js";
39
40
  /**
@@ -84,6 +85,16 @@ export interface GateLedgerPort {
84
85
  approvalRule?(gate: string, at?: {
85
86
  base?: string;
86
87
  }): Promise<GateApprovalRule | null>;
88
+ /**
89
+ * The judge a gate's approval rules are met by (#4290): which approvals
90
+ * prove their approver (a seal verified against the signers at base) and
91
+ * the roles `.chant/trust.json` at base gives them. `at.base` as for
92
+ * {@link GateLedgerPort.approvalRule}. Absent, no approval proves anyone,
93
+ * so a rule naming roles is never met.
94
+ */
95
+ quorumJudge?(gate: string, at?: {
96
+ base?: string;
97
+ }): Promise<GateQuorumJudge>;
87
98
  /** The approvals a wave applied under (#3784), from `_gates/<op>/applied.jsonl`. Absent on a port that never spends approvals. */
88
99
  readApplied?(op: string): Promise<AppliedGateRecord[]>;
89
100
  /** Record that a wave is about to apply under an approval (#3784), which spends it. */
@@ -177,6 +188,13 @@ export interface GateCheckInput {
177
188
  * it so `chant approve` can evaluate the policy.
178
189
  */
179
190
  approval?: ResolvedGateApproval;
191
+ /**
192
+ * Judges approvals for `approval.quorum`'s rules (#4290). Absent, the
193
+ * port's {@link GateLedgerPort.quorumJudge} at its default base, else no
194
+ * approval proves anyone. A wave passes one that knows the change's author
195
+ * and the merged pull request's review.
196
+ */
197
+ judge?: GateQuorumJudge;
180
198
  /** ISO-8601 "now" — supplied by the caller, so the decision is deterministic under test. */
181
199
  now?: string;
182
200
  /**
@@ -264,6 +282,8 @@ export interface GateQuorumProgress {
264
282
  /** Approvers whose approval counts toward the quorum, oldest first. */
265
283
  approvers: string[];
266
284
  need: number;
285
+ /** Where each approval rule the gate takes stands (#4290). Absent when it takes none. */
286
+ rules?: GateQuorumRuleProgress[];
267
287
  }
268
288
  /** Either the gate is answered, or it is a standing fact. */
269
289
  export type GateCheck = {
@@ -319,8 +339,12 @@ export declare function approverOf(record: GateResolutionRecord): GateApprover;
319
339
  export interface GateTally {
320
340
  /** Distinct human approvers of this plan who count toward the quorum, one record each (their newest), oldest first. */
321
341
  counted: GateResolutionRecord[];
322
- /** The quorum's count, 1 when the gate declares none. */
342
+ /** The most approvers a rule needs, 1 when the gate declares none. */
323
343
  need: number;
344
+ /** Every rule is met (#4290); with no rules, one human approved. */
345
+ met: boolean;
346
+ /** Where each rule stands (#4290). Absent when the gate takes no rule. */
347
+ rules?: GateQuorumRuleProgress[];
324
348
  /** The newest approval whose recorded permit passes the gate on its own. Only in `enforce` mode, only for a permit recorded under `enforce`, and only under the gate's current policy version and resolved context. */
325
349
  permit?: GateResolutionRecord;
326
350
  /** The newest approval of this gate for a different plan, when the gate binds one. */
@@ -337,8 +361,12 @@ export interface GateTally {
337
361
  * pending fact re-recorded for a changed mode or context keeps a partly
338
362
  * filled quorum (#3206). It defaults to `sinceIso`.
339
363
  *
340
- * Of those, only a human's counts toward the quorum, once per `resolvedBy`,
341
- * and only with one of the quorum's roles when it names any. An agent counts
364
+ * Of those, only a human's counts toward the quorum, once per `resolvedBy`.
365
+ * The quorum is a list of rules (#4290, `./gate-quorum.ts`), already the ones
366
+ * the gate takes: each is met by distinct humans, counting toward `roles`
367
+ * only an approver `judge` proves and gives one of them (roles from
368
+ * `.chant/trust.json` at base, never the approver's claim), and the gate
369
+ * passes when every rule is met. With no rule, one human passes it. An agent counts
342
370
  * only through a recorded `allow` in `enforce` mode, evaluated under the
343
371
  * policy version the gate declares now, recorded while the gate was in
344
372
  * `enforce` (#2512), and evaluated under the context the gate resolves now
@@ -346,7 +374,7 @@ export interface GateTally {
346
374
  * after the gate switches to `enforce`, and a `deny` never removes a human's approval: a policy can add
347
375
  * a way through the gate but cannot take one away.
348
376
  */
349
- export declare function tallyGateApprovals(records: GateResolutionRecord[], gate: string, sinceIso: string, planDigest: string | undefined, approval: ResolvedGateApproval, humansSinceIso?: string): GateTally;
377
+ export declare function tallyGateApprovals(records: GateResolutionRecord[], gate: string, sinceIso: string, planDigest: string | undefined, approval: ResolvedGateApproval, humansSinceIso?: string, judge?: GateQuorumJudge): GateTally;
350
378
  /**
351
379
  * Where a gate's human quorum starts counting (#3206): the oldest pending fact
352
380
  * in the unbroken run of facts, newest first from `standing`, recorded for
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/op/gate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../../src/op/gate.ts"],"names":[],"mappings":"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"}
package/dist/op/gate.js CHANGED
@@ -37,6 +37,7 @@ import { isModelAuthored } from "../lifecycle/gate-origin.js";
37
37
  import { appliedGatePath, isApprovalSpent, parseAppliedGateLines } from "../lifecycle/gate-applied.js";
38
38
  import { sortedJsonReplacer } from "../utils.js";
39
39
  import { gateContextDigest } from "./gate-approval.js";
40
+ import { quorumNeed, quorumRuleProgress, quorumRules, ruleNeedsProof, samePrincipal, trustQuorumJudge, UNPROVEN_JUDGE, } from "./gate-quorum.js";
40
41
  import { pushLifecycleReport, readBlobBySha, readBlobFromPath, readPathSha, requireLifecycleLedger, writeBlobToPath, RefCASConflictError } from "../lifecycle/git.js";
41
42
  import { parseDuration } from "./duration.js";
42
43
  import { ciRunIdFromEnv, resolveGateRunLocator } from "./gate-run-locator.js";
@@ -74,6 +75,12 @@ export function gitGateLedgerPort(opts) {
74
75
  const { gateAdmission } = await import("../workspace/identity.js");
75
76
  return gateAdmission(opts?.cwd ?? process.cwd(), gate, at?.base);
76
77
  },
78
+ async quorumJudge(_gate, at) {
79
+ // #4290: the signers and role grants at base, never the approver's claim.
80
+ const { scopeSource } = await import("../workspace/write-scope.js");
81
+ const { policy } = scopeSource(opts?.cwd ?? process.cwd(), at?.base);
82
+ return trustQuorumJudge({ signers: policy, roles: policy.roles });
83
+ },
77
84
  async appendResolution(input) {
78
85
  const { record } = await appendGateResolution(input, opts);
79
86
  return { record, ...(await pushLifecycleReport(opts)) };
@@ -209,8 +216,12 @@ export function approverOf(record) {
209
216
  * pending fact re-recorded for a changed mode or context keeps a partly
210
217
  * filled quorum (#3206). It defaults to `sinceIso`.
211
218
  *
212
- * Of those, only a human's counts toward the quorum, once per `resolvedBy`,
213
- * and only with one of the quorum's roles when it names any. An agent counts
219
+ * Of those, only a human's counts toward the quorum, once per `resolvedBy`.
220
+ * The quorum is a list of rules (#4290, `./gate-quorum.ts`), already the ones
221
+ * the gate takes: each is met by distinct humans, counting toward `roles`
222
+ * only an approver `judge` proves and gives one of them (roles from
223
+ * `.chant/trust.json` at base, never the approver's claim), and the gate
224
+ * passes when every rule is met. With no rule, one human passes it. An agent counts
214
225
  * only through a recorded `allow` in `enforce` mode, evaluated under the
215
226
  * policy version the gate declares now, recorded while the gate was in
216
227
  * `enforce` (#2512), and evaluated under the context the gate resolves now
@@ -218,12 +229,13 @@ export function approverOf(record) {
218
229
  * after the gate switches to `enforce`, and a `deny` never removes a human's approval: a policy can add
219
230
  * a way through the gate but cannot take one away.
220
231
  */
221
- export function tallyGateApprovals(records, gate, sinceIso, planDigest, approval, humansSinceIso = sinceIso) {
232
+ export function tallyGateApprovals(records, gate, sinceIso, planDigest, approval, humansSinceIso = sinceIso, judge = UNPROVEN_JUDGE) {
222
233
  const since = new Date(sinceIso).getTime();
223
234
  const humansSince = Math.min(since, new Date(humansSinceIso).getTime());
224
235
  const at = (r) => new Date(r.timestamp).getTime();
225
- const roles = approval.quorum?.roles;
236
+ const rules = quorumRules(approval.quorum);
226
237
  const context = gateContextDigest(approval.context);
238
+ // Each human once: their newest approval, or their newest proven one when they have one.
227
239
  const byActor = new Map();
228
240
  let permit;
229
241
  let mismatched;
@@ -250,22 +262,22 @@ export function tallyGateApprovals(records, gate, sinceIso, planDigest, approval
250
262
  decision.contextDigest === context && (!permit || at(r) >= at(permit))) {
251
263
  permit = r;
252
264
  }
253
- const approver = approverOf(r);
254
- if (approver.kind !== "human")
255
- continue;
256
- if (roles && !(approver.roles ?? []).some((role) => roles.includes(role)))
265
+ if (approverOf(r).kind !== "human")
257
266
  continue;
258
- const prior = byActor.get(r.resolvedBy);
259
- if (!prior || at(r) >= at(prior))
260
- byActor.set(r.resolvedBy, r);
267
+ const key = r.resolvedBy.normalize("NFKC").trim().toLowerCase();
268
+ const proven = rules.some(ruleNeedsProof) && judge(r).proven;
269
+ const prior = byActor.get(key);
270
+ if (!prior || (proven && !prior.proven) || (proven === prior.proven && at(r) >= at(prior.record)))
271
+ byActor.set(key, { record: r, proven });
261
272
  }
262
- const counted = [...byActor.values()].sort((a, b) => at(a) - at(b));
263
- return {
264
- counted,
265
- need: approval.quorum?.count ?? 1,
266
- ...(permit ? { permit } : {}),
267
- ...(mismatched ? { mismatched } : {}),
268
- };
273
+ const humans = [...byActor.values()].map((v) => v.record).sort((a, b) => at(a) - at(b));
274
+ const extra = { ...(permit ? { permit } : {}), ...(mismatched ? { mismatched } : {}) };
275
+ if (rules.length === 0)
276
+ return { counted: humans, need: 1, met: humans.length >= 1, ...extra };
277
+ const approvers = humans.map((r) => ({ who: r.resolvedBy, at: r.timestamp, judged: judge(r) }));
278
+ const progress = rules.map((rule) => quorumRuleProgress(rule, approvers));
279
+ const counted = humans.filter((r) => progress.some((p) => p.approvers.some((a) => samePrincipal(a, r.resolvedBy))));
280
+ return { counted, need: quorumNeed(rules), met: progress.every((p) => p.met), rules: progress, ...extra };
269
281
  }
270
282
  /**
271
283
  * Where a gate's human quorum starts counting (#3206): the oldest pending fact
@@ -503,11 +515,15 @@ export async function evaluateGate(port, input) {
503
515
  standing.approval?.policy?.version === input.approval.policy?.version
504
516
  ? humanQuorumSince(pending, standing)
505
517
  : since;
506
- const tally = tallyGateApprovals(resolutions, input.gate, since, input.planDigest, input.approval, humansSince);
518
+ // #4290: who an approval proves, and their roles at base, for rules that read them.
519
+ const judge = quorumRules(input.approval.quorum).some(ruleNeedsProof)
520
+ ? (input.judge ?? (await port.quorumJudge?.(input.gate)) ?? UNPROVEN_JUDGE)
521
+ : UNPROVEN_JUDGE;
522
+ const tally = tallyGateApprovals(resolutions, input.gate, since, input.planDigest, input.approval, humansSince, judge);
507
523
  if (tally.permit) {
508
524
  return { satisfied: true, resolution: tally.permit, via: "policy", approvals: tally.counted };
509
525
  }
510
- if (tally.counted.length >= tally.need) {
526
+ if (tally.met) {
511
527
  return {
512
528
  satisfied: true,
513
529
  resolution: tally.counted[tally.counted.length - 1],
@@ -516,19 +532,19 @@ export async function evaluateGate(port, input) {
516
532
  };
517
533
  }
518
534
  mismatched = tally.mismatched;
519
- quorum = { approvers: tally.counted.map((r) => r.resolvedBy), need: tally.need };
535
+ quorum = { approvers: tally.counted.map((r) => r.resolvedBy), need: tally.need, ...(tally.rules ? { rules: tally.rules } : {}) };
520
536
  // #3949: old and new approvals add up toward one quorum, each approver once.
521
537
  const counted = new Set(tally.counted.map((r) => r.resolvedBy));
522
538
  const extra = fromAliases.filter((r) => !counted.has(r.resolvedBy));
523
539
  if (extra.length > 0) {
524
540
  const asCarried = new Map(extra.map((r) => [{ version: 1, ...carriedApproval(r, input.op, input.gate, now) }, r]));
525
- const combined = tallyGateApprovals([...resolutions, ...asCarried.keys()], input.gate, since, input.planDigest, input.approval, humansSince);
526
- if (combined.counted.length >= combined.need) {
541
+ const combined = tallyGateApprovals([...resolutions, ...asCarried.keys()], input.gate, since, input.planDigest, input.approval, humansSince, judge);
542
+ if (combined.met) {
527
543
  const written = await carry(combined.counted.filter((r) => asCarried.has(r)).map((r) => asCarried.get(r)));
528
544
  const approvals = [...combined.counted.filter((r) => !asCarried.has(r)), ...written];
529
545
  return { satisfied: true, resolution: written[written.length - 1], via: "alias", approvals, carried: written };
530
546
  }
531
- quorum = { approvers: combined.counted.map((r) => r.resolvedBy), need: combined.need };
547
+ quorum = { approvers: combined.counted.map((r) => r.resolvedBy), need: combined.need, ...(combined.rules ? { rules: combined.rules } : {}) };
532
548
  }
533
549
  }
534
550
  else {