“European or internal” still has to mean “active and not excluded.”
Suppose the release rule is: send the canary to active accounts in the EU, or active
internal testers anywhere, unless support has explicitly excluded that account. The clause
“unless excluded” is a hard stop. If it is attached to only one side of an OR, an excluded internal tester can slip through.
We will use five illustrative accounts. acct-17 is active in the EU; acct-23 is an active US internal tester; acct-31 is paused in the EU; acct-42 is an active US internal tester on the exclusion list; acct-58 is an active US
account outside the internal group. These are constructed examples, not production records.
- Allow rule
- EU account OR internal tester
- Required condition
- Account must be active
- Safety boundary
- Excluded account never receives this rollout
- Question
- Which inputs satisfy the full rule, and how would we catch a precedence bug?
Give each claim a name before combining it.
For one account, define A = active, E = in the EU, I = an internal tester, and X = explicitly excluded. Each predicate evaluates to
true or false for the account. The intended decision is:
eligible = A AND (E OR I) AND NOT X
AND means both required claims hold; OR means at least one
branch is sufficient; NOT reverses one Boolean result. Parentheses show that EU
and internal are alternative ways to satisfy the allow group, after which activity and exclusion
apply to the whole group.
The expression follows standard precedence rules in many languages, but relying on memory
makes policy review harder. Parentheses are useful documentation even when the parser
would infer the same grouping. Keep predicates named after the evidence they inspect: isActive, isEuAccount, isInternalTester, and isExcluded.
| Account | A | E | I | X | A ∧ (E ∨ I) ∧ ¬X |
|---|---|---|---|---|---|
| acct-17 | true | true | false | false | true |
| acct-23 | true | false | true | false | true |
| acct-31 | false | true | false | false | false |
| acct-42 | true | false | true | true | false |
| acct-58 | true | false | false | false | false |
The same rule is a sequence of set operations.
Let U be the accounts in the rollout's candidate population. Let A contain active accounts, E contain EU accounts, I contain
internal testers, and X contain exclusions. Then the eligible set is A ∩ (E ∪ I) ∩ (U \ X).
Here, ∩ is intersection (“in both”), ∪ is union (“in either or
both”), and U \ X is the complement of exclusions relative to this particular candidate
universe. The universe matters: subtracting exclusions from all accounts in a company is not
the same as subtracting them from the candidate set being evaluated.
De Morgan's law helps rewrite compound vetoes: NOT (P OR Q) means NOT P AND NOT Q; NOT (P AND Q) means NOT P OR NOT Q. This is useful when translating a policy between a readable
statement, a query, and an alert filter. Check that the universe and unknown-value
behavior remain the same.
Five accounts under review in this example.
acct-17, acct-23, acct-31, acct-42.
Drop paused acct-31; three remain.
Drop excluded acct-42; select acct-17 and acct-23.
Reduce the failure to one record and evaluate every clause.
Suppose someone writes (A AND E AND NOT X) OR (A AND I). This looks close,
but the exclusion is scoped to the EU branch only. An excluded internal tester can still
pass the second branch. The intended rule applies activity, the allow group, and the veto
to the account as a whole: A AND (E OR I) AND NOT X. Parentheses also prevent
language-specific operator precedence from obscuring that policy during review.
Diagnose by selecting a counterexample, not by staring at a long query. For acct-42, evaluate A=true, E=false, I=true, X=true for acct-42. The
flawed expression returns true through its A AND I branch; the intended rule
returns false because NOT X is false. This single counterexample exposes the veto-scope
bug. Add a non-excluded internal account as a positive boundary case so a fix does not accidentally
remove the valid internal rollout path.
For production investigation, capture the account fields as seen by the evaluator, the policy version, and the final decision. Then compare expected and actual predicate values. A policy change can be valid while stale audience membership, cached attributes, or a different query path causes the wrong rollout.
Keep the expression readable at the point of decision.
The examples name the allow predicate, preserve the global activity and exclusion gates,
and apply the same account records in TypeScript and Go. Run the code mentally for acct-42: it passes the internal allow branch, then fails the final exclusion gate. Explicit
parentheses make the scope reviewable.
Both versions evaluate the same five records and return the same selected IDs.
export type Account = {
id: string;
active: boolean;
region: 'eu' | 'us';
};
/** A candidate receives the canary only when active and selected by at least one allow rule,
* while never appearing in the explicit exclusion set. The rules are illustrative policy. */
export function canReceiveCanary(
account: Account,
internalIds: ReadonlySet<string>,
excludedIds: ReadonlySet<string>
): boolean {
const allowed = account.region === 'eu' || internalIds.has(account.id);
return account.active && allowed && !excludedIds.has(account.id);
}
export function selectCanaryAccounts(
accounts: Account[],
internalIds: ReadonlySet<string>,
excludedIds: ReadonlySet<string>
): string[] {
return accounts
.filter((account) => canReceiveCanary(account, internalIds, excludedIds))
.map((account) => account.id);
}
const accounts: Account[] = [
{ id: 'acct-17', active: true, region: 'eu' },
{ id: 'acct-23', active: true, region: 'us' },
{ id: 'acct-31', active: false, region: 'eu' },
{ id: 'acct-42', active: true, region: 'us' },
{ id: 'acct-58', active: true, region: 'us' }
];
const internalIds = new Set(['acct-23', 'acct-42']);
const excludedIds = new Set(['acct-42']);
console.log(selectCanaryAccounts(accounts, internalIds, excludedIds));
// [ 'acct-17', 'acct-23' ]
package main
import "fmt"
type Account struct {
ID string
Active bool
Region string
}
// A candidate receives the canary only when active and selected by at least one allow rule,
// while never appearing in the explicit exclusion set. The rules are illustrative policy.
func canReceiveCanary(account Account, internalIDs, excludedIDs map[string]bool) bool {
allowed := account.Region == "eu" || internalIDs[account.ID]
return account.Active && allowed && !excludedIDs[account.ID]
}
func selectCanaryAccounts(accounts []Account, internalIDs, excludedIDs map[string]bool) []string {
selected := make([]string, 0)
for _, account := range accounts {
if canReceiveCanary(account, internalIDs, excludedIDs) {
selected = append(selected, account.ID)
}
}
return selected
}
func main() {
accounts := []Account{
{ID: "acct-17", Active: true, Region: "eu"},
{ID: "acct-23", Active: true, Region: "us"},
{ID: "acct-31", Active: false, Region: "eu"},
{ID: "acct-42", Active: true, Region: "us"},
{ID: "acct-58", Active: true, Region: "us"},
}
internalIDs := map[string]bool{"acct-23": true, "acct-42": true}
excludedIDs := map[string]bool{"acct-42": true}
fmt.Println(selectCanaryAccounts(accounts, internalIDs, excludedIDs))
// [acct-17 acct-23]
}
Transfer the method to a deny rule with two independent causes.
- Predicate V
- User has verified their email.
- Predicate C
- Client version is supported.
- Predicate S
- Account is suspended.
- Task
- Write the predicate, then test: verified and supported but suspended; verified and unsupported; unverified but supported.
Show a worked answer
The rule is V AND C AND NOT S. The suspended user fails despite satisfying
both positive checks. The unsupported client fails because C is false; the
unverified user fails because V is false.
Before shipping, clarify what happens if verification or suspension state is unavailable, whether “supported” is computed from a server-controlled version, and how quickly a suspension propagates. The Boolean rule cannot answer those data freshness questions.