@@ -33,13 +33,19 @@ graph TD
3333 RootRule --> ID[id: string]
3434 RootRule --> Description[description: string]
3535 RootRule --> Variables[variables: List of CEL Expressions]
36- RootRule --> Match[match: List of Matches]
36+ RootRule --> Match[match: List of First Match Choices]
37+ RootRule --> Aggregate[aggregate: List of Aggregate Choices]
3738
3839 Match --> MatchItem[Match Choice]
3940 MatchItem --> Condition[condition: CEL Bool Expression]
4041 MatchItem --> Explanation[explanation: CEL String Expression]
4142 MatchItem --> Output[output: CEL Expression]
4243 MatchItem --> SubRule[rule: RuleBlock]
44+
45+ Aggregate --> AggregateItem[Aggregate Choice]
46+ AggregateItem --> AggCondition[condition: CEL Bool Expression]
47+ AggregateItem --> Emit[emit: CEL Expression]
48+ AggregateItem --> AggSubRule[rule: RuleBlock]
4349```
4450
4551---
@@ -53,17 +59,17 @@ graph TD
5359* ** Lazy Scoped Variables** : Local variables are defined using CEL expressions
5460 and are evaluated on-demand (lazily) and memoized (cached) to prevent
5561 redundant computation.
56- * ** Flexible Evaluation Semantics** : Currently, rules are evaluated in a strict
57- top-down ` FIRST_MATCH ` sequence (the first condition to evaluate to ` true `
58- determines the outcome, eliminating backtracking). The specification is
59- designed to extend to other evaluation modes in the future, such as
60- ` LAST_MATCH ` , ` ALL_MATCH ` , and ` AGGREGATE ` policies .
62+ * ** Flexible Evaluation Semantics** : Rules support multiple evaluation
63+ strategies: top-down ` FIRST_MATCH ` sequence (` match ` ), where the first
64+ condition to evaluate to ` true ` determines the single outcome, and
65+ ` AGGREGATE ` sequence ( ` aggregate ` ), where all choices with matching
66+ conditions evaluate and accumulate their emitted values into a list .
6167* ** Strong Composition and Type Checking** : The policy compiler statically
6268 validates that all possible output paths evaluate to the ** exact same type** ,
6369 avoiding dynamic runtime type mismatches.
64- * ** Structured Defaults and Optionals** : If no match conditions are met,
65- policies cleanly return ` optional.none() ` , allowing callers to seamlessly
66- handle unmatched states .
70+ * ** Structured Defaults and Optionals** : If no conditions are met in a ` match `
71+ policy, it cleanly returns ` optional.none() ` , while an unmatched ` aggregate `
72+ policy returns an empty list ` [] ` .
6773
6874---
6975
@@ -77,8 +83,10 @@ combined and ordered according to the policy evaluation semantic.
7783
7884A policy source document supports the following top-level keys:
7985
80- - ` name ` * (string, required)* : A system-specific unique identifier for the policy.
81- - ` description ` * (string, optional)* : A human-readable description of what the policy does.
86+ - ` name ` * (string, required)* : A system-specific unique identifier for the
87+ policy.
88+ - ` description ` * (string, optional)* : A human-readable description of what the
89+ policy does.
8290- ` imports ` * (list[ object] , optional)* : A list of type name aliases to simplify
8391 object and protobuf references within the expressions.
8492- ` rule ` * (object, required)* : The entry point for the policy execution.
@@ -98,7 +106,12 @@ A `rule` block supports the following fields:
98106- ` id ` * (string, optional)* : A unique identifier for the rule.
99107- ` description ` * (string, optional)* : A user-friendly description of the rule.
100108- ` variables ` * (list, optional)* : Ordered local variable declarations.
101- - ` match ` * (list, required)* : The sequential choices to evaluate.
109+ - ** Evaluation Semantics** : A rule must specify exactly one evaluation mode:
110+ - ` match ` * (list)* : The default evaluation mode. Sequential choices
111+ evaluated using ` FIRST_MATCH ` semantics (evaluates top-down until a
112+ condition is met).
113+ - ` aggregate ` * (list)* : Choices evaluated using ` AGGREGATE ` semantics
114+ (evaluates all matching choices and collects emitted values into a list).
102115
103116---
104117
@@ -132,8 +145,8 @@ within a CEL expression.
132145# ## Match Choices (`match`)
133146
134147A `match` block contains a sequence of conditional logic and outcomes evaluated
135- in a top-down, first-match sequence. A match block must contain at least one
136- output path.
148+ in a top-down, first-match sequence (`FIRST_MATCH`) . A match block must contain
149+ at least one output path.
137150
138151Each match item contains :
139152
@@ -148,15 +161,39 @@ Each match item contains:
148161
149162---
150163
164+ # ## Aggregate Choices (`aggregate`)
165+
166+ An `aggregate` block contains a list of choices evaluated under `AGGREGATE`
167+ semantics, collecting all matched outcomes into a list. Unlike `match`,
168+ evaluation does not short-circuit at the first matching condition; all
169+ matching items evaluate and accumulate their results.
170+
171+ Each aggregate choice item contains :
172+
173+ - `condition` *(string, optional)* : A CEL expression evaluating to `bool`. If
174+ omitted, it defaults to `true`. Conditions must not evaluate to a static
175+ constant `false`.
176+ - **Outcome**: Each aggregate choice item must define exactly one of:
177+ * `emit` *(string)*: A CEL expression defining a value to append to the
178+ accumulated result list if matched.
179+ * `rule` *(object)*: A nested `rule` block (such as a nested `match` block)
180+ to evaluate further if matched.
181+
182+ > [!NOTE]
183+ > `aggregate` rules cannot be nested directly or indirectly inside another
184+ > `aggregate` rule. However, `aggregate` rules can be nested within `match`
185+ > rules, and `match` rules can be nested within `aggregate` choices.
186+
187+ ---
188+
151189# ## Conditions and Return Types
152190
153191A `condition` expression must type-check to a `bool` return type. When a
154- ` condition` predicate evaluates to `true`, either the corresponding `output`
155- expression is evaluated and returned , or the nested `rule` block is evaluated.
192+ ` condition` predicate evaluates to `true`, the corresponding outcome
193+ (`output`, `emit` , or nested `rule`) is evaluated.
156194
157- # ### Optional Return Types
158- The overall return type of a policy is dynamically determined by its evaluation
159- completeness :
195+ # ### Return Types for `match` Rules (Optional & Plain Types)
196+ For `match` rules, the return type is determined by evaluation completeness :
160197
161198- **Exhaustive/Unconditional Return**: If the policy guarantees that a match
162199 path is always met (e.g., the final match has no `condition` or is
@@ -169,7 +206,32 @@ completeness:
169206 result in a matched output, `optional.none()` is returned as the overall
170207 policy result.
171208
172- For more details on CEL optionals, refer to the [CEL optional proposal](https://github.com/google/cel-spec/wiki/proposal-246).
209+ For more details on CEL optionals, refer to the
210+ [CEL optional proposal](https://github.com/google/cel-spec/wiki/proposal-246).
211+
212+ # ### Return Types for `aggregate` Rules (List Types)
213+ For `aggregate` rules, matching outcomes are collected into a list :
214+
215+ - **Aggregated Return**: If the emitted items in an `aggregate` rule evaluate to
216+ type `T`, the overall return type of the rule is `list(T)`
217+ (e.g., `list(string)`).
218+ - **Empty Result**: If no conditions within an `aggregate` block evaluate to
219+ ` true` , the policy returns an empty list `[]`.
220+ - **Nested Optional Pruning**: If a nested sub-rule (such as a nested `match`
221+ block) under an `aggregate` choice yields `optional.none()` (because no
222+ match branch was met), that `optional.none()` is pruned (omitted) from the
223+ aggregated list.
224+ - **Nested List Values**: If an `emit` or nested `output` explicitly yields a
225+ list value `list(T)` (e.g., `emit : " ['tag1', 'tag2']" ` ), each emitted list is
226+ appended as an element of the result list, yielding ` list(list(T))`
227+ (e.g., `[['tag1', 'tag2']]`).
228+
229+ For conformance test examples, see :
230+
231+ <!-- disableFinding(LINE_OVER_80) -->
232+ - ` aggregate` ([policy](conformance/testdata/aggregate/policy.yaml), [tests](conformance/testdata/aggregate/tests.yaml))
233+ - ` aggregate_explicit_list_output` ([policy](conformance/testdata/aggregate_explicit_list_output/policy.yaml), [tests](conformance/testdata/aggregate_explicit_list_output/tests.yaml))
234+ - ` aggregate_nested_explicit_list_double_wrapping` ([policy](conformance/testdata/aggregate_nested_explicit_list_double_wrapping/policy.yaml), [tests](conformance/testdata/aggregate_nested_explicit_list_double_wrapping/tests.yaml))
173235
174236---
175237
@@ -288,40 +350,54 @@ repository houses a comprehensive conformance test suite.
288350# ## Test Anatomy
289351Each test category includes three key components :
290352
353+ <!-- disableFinding(LINE_OVER_80) -->
291354| File Name | Format | Purpose |
292355| :--- | :--- | :--- |
293- | `config.yaml` / `config.textproto` | YAML or Protobuf | Configures the CEL environment, declares input variables (`variables`), specifies types, and registers stdlib extensions (like `strings`). See [context_pb/config.textproto ](conformance/testdata/context_pb/config.textproto). |
294- | `policy.yaml` | YAML | The actual CEL policy file being tested. See [nested_rule/ policy.yaml](conformance/testdata/nested_rule /policy.yaml). |
295- | `tests.yaml` / `tests.textproto` | YAML or Protobuf | Test cases containing input values (`input`) and the expected evaluation outcomes (`output`), or expected compilation error sets. See [nested_rule/tests.yaml ](conformance/testdata/nested_rule/tests.yaml). |
356+ | `config.yaml` / `config.textproto` | YAML or Protobuf | Configures the CEL environment, declares input variables (`variables`), specifies types, and registers stdlib extensions (like `strings`). See [context_pb](conformance/testdata/context_pb/config.textproto). |
357+ | `policy.yaml` | YAML | The actual CEL policy file being tested. See [nested_rule](conformance/testdata/nested_rule/ policy.yaml) and [aggregate ](conformance/testdata/aggregate /policy.yaml). |
358+ | `tests.yaml` / `tests.textproto` | YAML or Protobuf | Test cases containing input values (`input`) and the expected evaluation outcomes (`output`), or expected compilation error sets. See [nested_rule](conformance/testdata/nested_rule/tests.yaml). |
296359
297360---
298361
299362# # Static Analysis & Compilation Guarantees
300363
301364Conforming CEL Policy compilers must implement strict compile-time static
302- analysis. To verify this, the test suite in [compile_errors/](conformance/testdata/compile_errors) defines negative test cases that must fail compilation with appropriate error sets.
365+ analysis. To verify this, the test suite in
366+ [compile_errors/](conformance/testdata/compile_errors) defines negative test
367+ cases that must fail compilation with appropriate error sets.
303368
304369The suite covers the following compile-time checks :
305370
306- 1. **Type Agreement (Incompatible Outputs)** : The compiler must statically
307- verify that all possible match branches in a policy evaluate to the **exact
308- same type**. Mixing outcome types (e.g., one branch returning `bool` and
309- another returning `map`) is a compile-time error. See
310- [compile_errors/compose_conflicting_output/policy.yaml](conformance/testdata/compose_conflicting_output/policy.yaml).
311- 2. **Unreachable Code (Dead Condition Detection)** : The compiler must detect and
312- reject policies with dead-code branches. For example, if an unconditional
313- match choice (one without a `condition` or where `condition : " true" ` )
314- precedes other choices in a block, subsequent choices are unreachable and
315- will trigger a compilation failure. See
316- [compile_errors/unreachable/policy.yaml](conformance/testdata/compile_errors/unreachable/policy.yaml).
371+ 1. **Type Agreement (Incompatible Outputs & Emits)** : The compiler must
372+ statically verify that all possible outcome branches in a policy evaluate
373+ to the **exact same type**. Mixing outcome types in `match` outputs or
374+ ` aggregate` emits (e.g., one branch emitting `string` and another emitting
375+ ` int` ) is a compile-time error. See
376+ [compose_conflicting_output](conformance/testdata/compile_errors/compose_conflicting_output/policy.yaml)
377+ and
378+ [aggregate_heterogeneous_outputs](conformance/testdata/compile_errors/aggregate_heterogeneous_outputs/policy.yaml).
379+ 2. **Unreachable Code and Invalid Conditions** : The compiler must detect and
380+ reject policies with unreachable branches or conditions that are statically
381+ ` false` .
382+ - In `match` rules, if an unconditional choice (where `condition` is omitted
383+ or `condition : " true" ` ) precedes other choices in a block, subsequent
384+ choices are unreachable. See
385+ [unreachable](conformance/testdata/compile_errors/unreachable/policy.yaml).
386+ - In ` aggregate` rules, conditions that evaluate to a static constant
387+ `false` (e.g., `condition : " false" ` ) are rejected at compile time. See
388+ [aggregate_false_condition](conformance/testdata/compile_errors/aggregate_false_condition/policy.yaml).
3173893. **Scope and Reference Validation**: The compiler must validate that all
318390 referenced variables, inputs, and imported Protobuf types are properly
319391 declared in the scope. It also ensures variable names are unique (no
320392 duplicates) and prevents forward or self-referential variable dependencies.
321393 See
322- [compile_errors/ undeclared_reference/policy.yaml ](conformance/testdata/compile_errors/undeclared_reference/policy.yaml)
394+ [undeclared_reference](conformance/testdata/compile_errors/undeclared_reference/policy.yaml)
323395 and
324- [compile_errors/duplicate_variable/policy.yaml](conformance/testdata/compile_errors/duplicate_variable/policy.yaml).
396+ [duplicate_variable](conformance/testdata/compile_errors/duplicate_variable/policy.yaml).
397+ 4. **Semantics Nesting Restrictions**: ` aggregate` rules cannot be nested
398+ inside another `aggregate` rule or its sub-rules
399+ (`nested aggregate rules are not allowed`). See
400+ [aggregate_nested_mixed_semantics](conformance/testdata/compile_errors/aggregate_nested_mixed_semantics/policy.yaml).
325401
326402---
327403
0 commit comments