Skip to content

Policy language reference

Specifies policy-engine v0.7.3, synced verbatim from docs/LANGUAGE.md in Repyh-Labs/policy-engine — do not edit this page, regenerate it. The policy engine overview says which Orchestrator release runs this compiler.

This document specifies the policy language: source syntax, static semantics, runtime semantics, error model, bytecode, and hashing rules.

A policy is a named program pairing a parameters schema with a set of constraints. It is identified by the hash of its canonical serialization; evaluation binds the parameters to a values map and checks the constraints against the evidence.

The language is statically typed. Compilation requires an evidence schema. It embeds the minimal subshape of that schema the constraints reference into the bytecode, which evaluation uses to check the evidence. Compilation and evaluation are deterministic.

The first version keeps the language small. These are non-goals:

  • Range types.
  • Division.
  • Ordering between strings.
  • Floating-point numbers.
  • User-defined functions.
  • User-defined types.
  • General loops, recursion, or nested reductions.
  • Implicit type coercions.
  • Date arithmetic.
  • Timestamps.
  • Nested collection types such as List<List<int>> or Set<List<int>>.
  • Nested input objects such as an Object with an Object field.

A policy starts with:

  1. name <identifier>

After the header, top-level declarations may appear in any order:

  • at most one parameters schema block
  • exactly one non-empty requires block

An empty parameters {} block is allowed and is equivalent to omitting it.

Evidence fields are referenced directly as evidence.<field> and object attributes through reducer loop variables; both are typed by the evidence schema supplied at compile time.

Example:

name cart_guard
parameters {
max_total_usd_cents: int
max_item_usd_cents: int
}
requires {
sum(item.price_usd_cents * item.quantity for item in evidence.cart)
+ evidence.shipping_usd_cents <= parameters.max_total_usd_cents;
all(item.price_usd_cents <= parameters.max_item_usd_cents
for item in evidence.cart);
count_distinct(item.brand for item in evidence.cart) <= 3;
all(item.organic for item in evidence.cart
if item.category under "Food > Vegetables");
}

Supported scalar types:

  • bool
  • int
  • string
  • date

Supported collection types:

  • List<T>, where T is bool, int, string, date, or Object
  • Set<T>, where T is int, string, or date

Collection element types cannot be collection types. Object is valid only as a List element type, and only for evidence values produced at runtime; it is not a declared field type.

Parameter field types may be prefixed with optional:

parameters {
allowed_brands: optional Set<string>
}

Rules:

  • If an optional parameter is present at runtime, it has the underlying non-optional type.
  • optional may prefix only parameters.
  • No declared field may have bare type Object.
  • Parameter fields cannot contain Object, including List<Object>.
  • int is a signed 64-bit integer.
  • string values are valid UTF-8.
  • date is a calendar date with no time of day and no timezone.
  • Lists and sets are homogeneous.
  • Lists preserve order.
  • Sets are canonicalized, so duplicate elements are ignored.

Literal forms:

True
False
0
18
-5
"CH"
date(2026-01-01)
{1, 2, 3}
{"CH", "DE", "FR"}
{date(2026-01-01), date(2026-06-01)}

Field references are namespace-qualified:

parameters.max_price_cents
evidence.memory_gb

Object attributes are available through a reducer loop variable:

item.price_usd_cents

The part after parameters., evidence., or an object loop variable is a single field name, and like a loop variable it cannot be a reserved word.

Identifiers use ASCII and match [A-Za-z_][A-Za-z0-9_]*; the policy name and loop variables are identifiers. Field names — parameter fields, evidence fields, object attributes, and discriminants — may also start with a digit: they match [A-Za-z0-9_]+.

Quoted literals are delimited by " and have type string. The recognized escapes are \\, \", \n, \r, and \t. Any other backslash escape is a syntax error. Raw newline characters are not allowed inside quoted literals.

The {} literal is allowed only when its element type can be inferred from the other operand. Expressions such as {} == {} are rejected because the Set type cannot be inferred.

There are no list literals.

Objects are the evidence root and the elements of an evidence collection, the latter reached through a reducer loop variable. The language does not declare object fields in the source; an object attribute access such as item.price_usd_cents is typed by the evidence schema.

An object always exposes its common fields. A discriminated object also names a discriminant string field whose value selects a case that contributes further typed fields. There are two kinds:

  • an enum has a flat set of variants, each keyed by an opaque string tag;
  • a tree has a prefix-closed set of nodes, each keyed by a category path; a node inherits its ancestors’ fields.

A case field resolves only after a category check has narrowed the object to a case that contributes it. Common fields and the discriminant are always available.

A tree discriminant holds a category path: the empty path "" for the root, or one or more category names joined by the exact separator >, where category names are non-empty and cannot contain >.

under checks whether a tree discriminant is equal to or below a category path:

item.category under "Food > Vegetables"

Rules:

  • The left operand must be <variable>.<d> for an object loop variable, or evidence.<d>, where <d> is the discriminant of a tree object.
  • The right operand must be a string literal that is a declared node path.
  • At runtime, actual under path is true when path is "", actual == path, or actual starts with path + " > ".

Equality and membership on a discriminant are ordinary string operations that also drive narrowing:

item.category == "Food > Vegetables > Carrots"
item.category in {"Food > Vegetables", "Food > Fruit"}

Every tag or path compared against a discriminant through ==, in, not in, or under must be declared by the schema; an undeclared one is a compile-time error. At runtime, an unknown discriminant value is not by itself an error: it compares unequal to other values but may still satisfy under by path ancestry.

Arithmetic is supported only on signed 64-bit integers.

parameters.base_price_cents + parameters.shipping_budget_cents
evidence.price_cents - evidence.discount_cents
2 * item.quantity

All signed 64-bit integer arithmetic is checked. Overflow or underflow during expression evaluation or reducer accumulation is a runtime error.

Supported boolean forms:

  • Equality: ==
  • Ordering comparisons: <, <=, >, >=
  • Membership: in, not in
  • Boolean operators: not, and, or
  • Parentheses
  • Chained comparisons
  • Category checks with under

A boolean field or boolean-valued expression may appear directly in boolean position:

requires {
evidence.in_stock;
not evidence.refurbished;
}

and and or use left-to-right short-circuit evaluation.

5.3 Equality, Ordering, and Set Operations

Section titled “5.3 Equality, Ordering, and Set Operations”

Equality is allowed for any two operands with the same non-Object type.

Scalar List equality is order-sensitive. Set equality is order-insensitive. Equality is not defined for Object or for collections containing Object.

Ordering comparisons are allowed only for:

  • int
  • date

Set operations are allowed only for matching scalar-in-Set and Set-in-Set checks:

  • int in Set<int>
  • string in Set<string>
  • date in Set<date>
  • int not in Set<int>
  • string not in Set<string>
  • date not in Set<date>
  • Set<int> subset of Set<int>
  • Set<string> subset of Set<string>
  • Set<date> subset of Set<date>
  • Set<int> superset of Set<int>
  • Set<string> superset of Set<string>
  • Set<date> superset of Set<date>

These typing rules constrain the operands of each operator.

Reductions operate over fields whose type is List<T> or Set<T>.

all(expr for value in evidence.values)
all(expr for value in evidence.values if condition)
all(expr for value in evidence.values if condition_a if condition_b)

Supported reductions:

  • all: the body must be bool; returns bool
  • any: the body must be bool; returns bool
  • count: the body may be any well-typed expression; returns int
  • sum: the body must be int; returns int
  • count_distinct: the body must be a scalar primitive; returns int

Scalar primitives are bool, int, string, and date.

count also accepts a single collection in place of a comprehension, giving its length as an int:

count(evidence.cart)
count(parameters.allowed_tags)

Its argument is a parameter or evidence field reference of type List<T> or Set<T>. At runtime an evidence source that is not a list or set is a runtime error.

Rules:

  • The loop variable may be any non-reserved identifier.
  • parameters, evidence, type names, booleans, reducers, and other built-ins are reserved.
  • A reducer source is a parameter or evidence field.
  • A scalar loop variable may be used directly as an expression.
  • An object loop variable may be used through <variable>.<field>.
  • Reductions cannot be nested.
  • List reductions visit elements in list order.
  • Set reductions visit elements in canonical order: ascending for integers and dates, lexicographical for strings.
  • If a reducer has no filters, every collection element is produced.
  • If a reducer has filters, they are evaluated left-to-right.
  • The body is evaluated only for elements where every filter evaluates to True.
  • If a filter evaluates to False, later filters and the body are skipped for that element.
  • Any runtime error in an evaluated filter or produced body makes the reduction a runtime error.
  • count evaluates the body for every produced element, even though the body value is not used.
  • Bare object values are allowed only as the body of count.
  • all and any evaluate every produced body result. They do not short-circuit over the collection.
  • Boolean expressions inside a reducer body or filter still short-circuit.
  • all over an empty result is True.
  • any over an empty result is False.
  • count and count_distinct over an empty result are 0.
  • sum over an empty result is 0.

The loop variable type follows the source element type: a parameter Set<string> yields a string loop variable, and a List<Object> evidence field yields an Object loop variable. Attribute accesses on an object loop variable resolve through the evidence schema. At runtime, an evidence source that is not a list or set is a runtime error.

Examples:

all(item.price_usd_cents <= 1000 for item in evidence.cart)
any(item.category == "Food > Vegetables"
for item in evidence.cart)
all(tag in parameters.allowed_tags for tag in evidence.tags)
any(country == evidence.country for country in parameters.allowed_countries)
count(item for item in evidence.cart
if item.category under "Food") >= 1
count(item for item in evidence.cart
if item.category under "Food > Vegetables" if item.organic) >= 1
sum(item.quantity for item in evidence.cart) <= 10
count_distinct(item.brand for item in evidence.cart) <= 3

The language rejects mixed unparenthesized chains of and and or.

Allowed:

a and b and c
a or b or c
a and (b or c)
(a and b) or c

Rejected:

a and b or c
a or b and c

The language supports Python-style chained comparisons:

8 <= evidence.memory_gb <= 16
date(2026-01-01) <= parameters.latest_delivery <= date(2026-12-31)
a <= b == c < d

a <= b == c < d means (a <= b) and (b == c) and (c < d).

Rules:

  • Chaining applies only to ==, <, <=, >, >=.
  • in, not in, and under are not part of chained-comparison syntax.
  • Direction reversals such as a < b >= c are rejected.

A list element is read by a constant index after a dot:

evidence.readings.0
parameters.thresholds.2

Rules:

  • The indexed operand is a parameter or evidence field reference of type List<T> for a scalar T.
  • The index is a decimal integer in the unsigned 64-bit range. The result has type T.
  • An all-digit name directly after parameters. or evidence. is a field name, not an index; every later all-digit segment is an index.
  • At runtime, an index outside the list bounds is a runtime error.

Precedence and associativity for operators that can appear together unparenthesized, from tightest to loosest:

  1. Parentheses, literals, field references, list indexing, object references, loop variables, and reductions.
  2. * (left-associative).
  3. +, - (left-associative).
  4. Comparisons, equality, membership, under, and Set operators (non-associative, apart from chained comparisons).
  5. Homogeneous chains of and or homogeneous chains of or.

Unary not does not participate in ordinary precedence. Its operand must be an atom, a reduction, or a parenthesized expression. Use parentheses when negating an expression that contains an operator: not a, not (a == b), and not any(item.recalled for item in evidence.cart) are valid, while not a == b and not a and b are rejected. A bare not expression must also be parenthesized before it participates in a larger expression: use (not a) == b or (not a) and b. For repeated negation, write not (not a) rather than not not a.

policy := header top_decl*
header := "name" IDENT
top_decl := parameters_block
| requires_block
parameters_block := "parameters" "{" "}" | "parameters" "{" opt_field_decl+ "}"
opt_field_decl := FIELD_NAME ":" "optional"? type
type := scalar_type | list_type | set_type
scalar_type := "bool" | "int" | "string" | "date"
list_type := "List" "<" scalar_type ">"
set_type := "Set" "<" set_elem_type ">"
set_elem_type := "int" | "string" | "date"
requires_block := "requires" "{" opt_expr (";" opt_expr)* ";"? "}"
opt_expr := "optional:"? top_expr
top_expr := sub_expr | "not" (atom | reduction)
sub_expr := atom
| reduction
| sub_expr (bool_op sub_expr)+
| sub_expr (cmp_op sub_expr)+
| sub_expr (arith_op sub_expr)+
| sub_expr set_op sub_expr
| discriminant_ref "under" STRING
reduction := reducer "(" top_expr "for" IDENT "in" field_ref filter* ")"
| "count" "(" field_ref ")"
filter := "if" top_expr
reducer := "all" | "any" | "count" | "sum" | "count_distinct"
atom := "(" top_expr ")" | indexed_ref | field_ref | object_ref | loop_var | literal
indexed_ref := field_ref ("." INDEX)+
field_ref := parameters_ref | evidence_ref
parameters_ref := "parameters" "." FIELD_NAME
evidence_ref := "evidence" "." FIELD_NAME
object_ref := IDENT "." FIELD_NAME
discriminant_ref := object_ref | evidence_ref
loop_var := IDENT
cmp_op := "==" | "<" | "<=" | ">" | ">="
bool_op := "and" | "or"
set_op := "in" | "not" "in" | "subset" "of" | "superset" "of"
arith_op := "+" | "-" | "*"
literal := BOOL | INT | STRING | DATE | SET
BOOL := "True" | "False"
DATE := "date(" YYYY "-" MM "-" DD ")"
SET := "{}" | "{" set_elem ("," set_elem)* "}"
set_elem := INT | STRING | DATE

This grammar sketch omits comments, whitespace, and line-termination details.

  • Newlines separate top-level declarations and schema field declarations.
  • Constraints inside requires { ... } are separated by semicolon ;.
  • The leading IDENT of object_ref and loop_var is a reducer loop variable and cannot be parameters, evidence, or any other reserved word. This is what distinguishes object_ref from parameters_ref and evidence_ref.
  • IDENT is an identifier, FIELD_NAME a field name, and INDEX a list index.
  • # starts a line comment and runs to the end of the line.
  • Comments may appear on their own line or after code.
  • # inside a quoted literal is not a comment.
  • STRING is the quoted literal token and has type string.

Compilation parses the policy, type-checks it against the evidence schema, and lowers the result to bytecode. The evidence schema is required.

Every expression has a concrete static type:

  • parameters.<field> has the declared parameter type. An optional parameter has its underlying non-optional type.
  • evidence.<field> has the type the evidence schema gives it.
  • A loop variable has the source element type.
  • <variable>.<field> has the type the evidence schema gives it, using the object’s narrowed case for a case field.
  • A list index has the list’s scalar element type.
  • Literals have their literal type.
  • An operator’s result type follows from its type-checked operands.

A constraint expression must be bool. Every type requirement is checked at compile time; evaluation only verifies that the runtime data matches.

Syntax and structure errors:

  • The header is missing or malformed.
  • A top-level declaration is repeated when at most one is allowed.
  • The requires block is missing or empty.
  • A parameter name is duplicated within the parameters block.
  • A date literal is not a valid calendar date.
  • An integer literal is outside the signed 64-bit range.
  • A list index is outside the unsigned 64-bit range.
  • Mixed unparenthesized and and or appear in the same chain.
  • A chained comparison reverses direction.

Schema and reference errors:

  • A non-optional: constraint references an optional parameter.
  • An optional: constraint does not reference any optional parameter.
  • A parameter references a name not declared in the parameters block.
  • An evidence.<field> names a field the evidence schema does not declare.
  • An object attribute is neither a common field, the discriminant, nor a case field of the object’s narrowed case.
  • A case field is accessed without narrowing the object to a case that contributes it.
  • A tag or path compared against a discriminant is not declared by the schema.
  • A discriminated object’s discriminant, common, and case field names are not all distinct.
  • A case field is declared as an object or List<Object>.
  • A tree lists the root "" as a node, has a malformed node path, is not prefix-closed, or a node’s own field collides with an inherited one.
  • A collection type contains another collection type.
  • A parameter is declared as bare Object or as List<Object>.
  • A reduction is nested inside another reduction.
  • A reducer loop variable uses a reserved name.
  • A bare object loop variable appears anywhere except as the body of count.
  • A field is accessed on a scalar loop variable.
  • under is used on anything other than a tree discriminant, as <variable>.<d> for an object loop variable or evidence.<d>.

Type errors:

  • A constraint expression is not bool.
  • Arithmetic operands are not int.
  • Comparison operands do not have the same type.
  • Equality is used on Object or a collection containing Object.
  • in or not in operands do not form a matching scalar-in-Set check.
  • subset of or superset of operands do not form a matching Set-in-Set check.
  • and, or, or not operands are not boolean.
  • all or any has a non-boolean body.
  • sum has a non-integer body.
  • count_distinct has a non-scalar body.
  • count, all, any, sum, or count_distinct iterates over, or measures, a source that is not List<T> or Set<T>.
  • List indexing is applied to a value that is not a List<T> for a scalar T.
  • A Set literal is heterogeneous.

The static semantics above resolve every evidence and object access against the evidence schema. This section defines how those types are resolved and what the compiler records: the minimal subshape of the evidence schema the constraints reference. The inputs and outputs are defined in the Rust API specification of the policy engine (docs/API.md in Repyh-Labs/policy-engine).

The evidence schema describes the runtime shape of the evidence map. Each evidence type is one of:

  • a scalar type bool, int, string, or date;
  • List<T> or Set<T>, where T is another evidence type (sets hold scalars);
  • an object.

An object declares common fields shared by every value, and may be discriminated as an enum or a tree. The evidence root is an object of any of these kinds.

A reference resolves its static type from the evidence schema:

  • evidence.<field> resolves against the root.
  • A loop variable over a List/Set evidence field uses the element type.
  • On an object, a common field has its declared type, the discriminant has type string, and a case field has the type its narrowed case gives it.

Resolved types feed the same operator and reduction type rules that compilation applies.

The API representation can express schemas outside the source language subset. Such fields are valid schema data even when current policy source syntax cannot reference them; policies that try unsupported source syntax are rejected.

A case field’s type may be scalar, scalar List, or Set, but not Object or List<Object>. Common fields may use any evidence type. The discriminant, the common fields, and each case’s fields have distinct names matching the field-name syntax. Names that are source keywords may appear in schemas, but policy source cannot reference them.

A tree’s nodes additionally satisfy:

  • The root node is the empty category path ""; its fields are the object’s common fields, so it is not listed among the nodes.
  • A non-root node path is one or more category names joined by >; category names are non-empty and cannot contain >.
  • Node paths uniquely identify a node.
  • The set is prefix-closed: every non-root node’s parent path is also a node, the root being the parent of every depth-one node.
  • A node lists only the fields it adds and inherits its ancestors’ fields; its own field may not collide with an inherited field.

Compilation validates this contract for every discriminated object in the evidence schema, whether or not the constraints reference it.

A category check on an object’s discriminant narrows the object so its case fields resolve. Checks read the discriminant field: <item>.<d> for an object loop variable, or evidence.<d> when the evidence root is discriminated.

For an enum:

  • d == "tag" and "tag" == d narrow to that variant.
  • d in {tags} narrows to the fields common to the listed variants: a field present, with the same type, in every listed variant.

For a tree:

  • d == "path" and "path" == d narrow to exactly that node.
  • d under "path" narrows to that node, not the union of its descendants.
  • d in {paths} narrows to exactly the path of a singleton set, and to the deepest common ancestor of the listed paths otherwise.

Common rules:

  • Every tag or path compared against a discriminant must be declared by the schema; an undeclared one is a compile-time error. Tree paths must also be well-formed.
  • Narrowing applies top-down through the constraints in a requires block, left-to-right through and, through parentheses, and from a filter to later filters and the reducer body. Only the evidence root discriminant carries across constraints; a loop object exists within a single reduction.
  • Narrowing of the evidence root also applies inside a reduction: it resolves a root case-field collection used as the source, and evidence-root reads in the filters and body.
  • An optional: constraint receives the narrowing before it but does not narrow the constraints after it.
  • Narrowing established inside an or or not does not apply outside it.
  • Successive checks on the same object combine to the most specific case. Checks that cannot all hold narrow to no case at all: equalities to two different cases, an equality combined with a check strictly below its path, unrelated tree paths, or an empty in {} set. Such checks still compile and evaluate, they just cannot all hold.
  • Checks against runtime string or Set<string> values do not narrow.

Accessing a case field without narrowing to a case that contributes it, or after narrowing to no case, is a compile-time error; the discriminant itself is always accessible.

Allowed:

all(item.organic for item in evidence.cart
if item.category under "Food > Vegetables")
all(item.organic for item in evidence.cart
if item.category under "Food"
and item.category == "Food > Vegetables")
all(item.perishable for item in evidence.cart
if item.category in {"Food > Vegetables", "Food > Fruit"})

Rejected by compilation:

all(item.organic for item in evidence.cart)
all(item.organic and item.category under "Food > Vegetables"
for item in evidence.cart)
all(item.organic for item in evidence.cart
if item.category under "Food"
and item.category == "Apparel > Shoes")
all(item.organic for item in evidence.cart
if item.category in {})

When the evidence root is a tree, a leading guard narrows it for the constraints after it:

requires {
evidence.category == "Food > Vegetables";
evidence.organic;
}

Reordering these two constraints is rejected, since evidence.organic would be read without narrowing to a case that exposes it.

Compilation records, in the compiled policy, the minimal subshape of the evidence schema: only the parts the constraints reference. It is derived from the policy and the evidence schema, applying the same narrowing used elsewhere in this document. It contains only:

  • root fields the constraints reference;
  • for each referenced object, the common fields and discriminant it reads;
  • each case a case-field read narrows to, keyed by its tag or path, holding the fields read there.

A case field read under a narrowed enum variant or tree node is recorded at that case. Enum membership narrows to each listed variant, so a field read there is recorded under every one of them; tree membership narrows to the deepest common ancestor node and records there. Along a tree path, a field read under several nodes is recorded only at the shallowest, which its descendants inherit, so no node re-records an inherited field. Unreferenced ancestor nodes may be omitted from the embedded subshape. Unreferenced root fields, common fields, variants, nodes, and case fields are omitted, so schemas differing only in parts no constraint reads produce the same subshape.

An object satisfies the subshape when, for its runtime discriminant value, it exposes every field recorded at the matching case and, for a tree, at every ancestor node of it. The root’s recorded common fields therefore apply to every value.

For the policy

requires {
sum(item.quantity for item in evidence.cart) <= 10;
all(item.organic for item in evidence.cart
if item.category under "Food");
all(p.color == "red" for p in evidence.cart
if p.category == "Food > Carrots");
}

where cart items are a tree with common field quantity: int and discriminant category, the subshape for evidence.cart items records

common -> { quantity: int }
"Food" -> { organic: bool }
"Food > Carrots" -> { color: string }

At runtime, the engine first checks the parameters input. If the check succeeds, it evaluates constraints, checking each evidence and object access it reads against the evidence subshape embedded in the policy. Evaluation can produce runtime errors, such as missing fields, wrong field types, an out-of-bounds list index, or integer overflow.

Evaluation is deterministic.

Only the parameters input is checked before constraint evaluation:

  • every non-optional parameter exists in the parameters map
  • each provided parameter has the declared type
  • the parameters map has no undeclared fields

Declared type checks recurse into collection values: for a List<T> or Set<T> parameter, every element must have type T.

The evidence map is not checked upfront. Each evidence field and object attribute is checked when a constraint reads it: it must be present, and if the embedded subshape records a type for the field at that runtime case, including tree ancestor nodes, the value must match it. A missing field or mismatched recorded type is a runtime error in the reading constraint. Because the check is lazy, an evidence error on a part of a constraint that short-circuit evaluation never reaches is not raised.

If the parameters input check fails, evaluation stops and returns an error.

Constraints are evaluated independently in source order. Every constraint is evaluated even if earlier constraints evaluate to False or hit a runtime error.

Each constraint yields exactly one of:

  • the constraint evaluated to True
  • the constraint evaluated to False
  • runtime error

A non-optional constraint is satisfied if and only if its expression evaluates to True.

An optional: constraint is satisfied without evaluating its expression if any referenced optional parameter is absent from the parameters map. This applies wherever the field is referenced, including a reducer source, filter, or body.

Otherwise, an optional: constraint evaluates like any other constraint. To gate checks independently, write a separate optional: constraint for each optional parameter.

A policy is satisfied if and only if every constraint is satisfied.

Runtime errors are fail-closed: they make the current constraint non-passing. They do not stop evaluation of other constraints.

Runtime errors distinguish at least:

  • a missing evidence field or object attribute, including the discriminant
  • a wrong-typed evidence field or object attribute, including a value used in an operation that requires a different type
  • a list index outside the list bounds
  • integer overflow or underflow

The evaluator returns a structured report containing all non-passing constraints in source order.

A good failure payload includes at least:

  • constraint_index
  • the pretty-printed constraint expression
  • whether the constraint evaluated to False or hit a runtime error
  • a human-readable reason
  • optional evaluated subvalues when helpful

For runtime errors, the payload still identifies the constraint and includes the runtime error reason.

Bytecode generation is deterministic: compiling the same source with the same evidence schema always produces the same bytecode.

  • The bytecode is the same regardless of source formatting and comments.
  • x not in s compiles to the same bytecode as not (x in s).
  • A chained comparison compiles to the same bytecode as its expansion into pairwise comparisons joined by and.
  • The compiled policy can be pretty-printed back into an equivalent source form.
  • The compiled policy holds the policy name, the parameters schema, the constraints, and the minimal evidence subshape the constraints reference.
  • Parts of the evidence schema no constraint reads are not included.

The policy ID is the base58-encoded SHA-256 digest of the bytecode, so it covers the embedded evidence subshape: two evidence schemas that differ only in parts no constraint reads yield the same ID, while a change to a referenced part yields a different one.