( rule: RequestPolicyRule, users: User[], namedRules: Map<UUID, NamedRule>, visited: Set<UUID> = new Set(), )
| 103 | * Unresolvable cases (cycle, missing NamedRule) return Infinity to avoid false positives. |
| 104 | */ |
| 105 | export const minVotesForRule = ( |
| 106 | rule: RequestPolicyRule, |
| 107 | users: User[], |
| 108 | namedRules: Map<UUID, NamedRule>, |
| 109 | visited: Set<UUID> = new Set(), |
| 110 | ): number => { |
| 111 | if ('AutoApproved' in rule) return 0; |
| 112 | if ('AllowListed' in rule || 'AllowListedByMetadata' in rule) return 0; |
| 113 | if ('Quorum' in rule) { |
| 114 | const resolved = resolveApprovers(rule.Quorum.approvers, users); |
| 115 | return Math.min(rule.Quorum.min_approved, resolved.length); |
| 116 | } |
| 117 | if ('QuorumPercentage' in rule) { |
| 118 | const resolved = resolveApprovers(rule.QuorumPercentage.approvers, users); |
| 119 | const scaled = rule.QuorumPercentage.min_approved * resolved.length; |
| 120 | return scaled === 0 ? 0 : Math.ceil(scaled / 100); |
| 121 | } |
| 122 | if ('AnyOf' in rule) { |
| 123 | return rule.AnyOf.reduce( |
| 124 | (acc, child) => Math.min(acc, minVotesForRule(child, users, namedRules, visited)), |
| 125 | Number.POSITIVE_INFINITY, |
| 126 | ); |
| 127 | } |
| 128 | if ('AllOf' in rule) { |
| 129 | return rule.AllOf.reduce( |
| 130 | (acc, child) => Math.max(acc, minVotesForRule(child, users, namedRules, visited)), |
| 131 | 0, |
| 132 | ); |
| 133 | } |
| 134 | if ('Not' in rule) return Number.POSITIVE_INFINITY; |
| 135 | if ('NamedRule' in rule) { |
| 136 | if (visited.has(rule.NamedRule)) return Number.POSITIVE_INFINITY; |
| 137 | const named = namedRules.get(rule.NamedRule); |
| 138 | if (!named) return Number.POSITIVE_INFINITY; |
| 139 | return minVotesForRule(named.rule, users, namedRules, new Set([...visited, rule.NamedRule])); |
| 140 | } |
| 141 | return Number.POSITIVE_INFINITY; |
| 142 | }; |
no test coverage detected