erdl-landing

ERDL Specification v2.3

(Entity-Rule Definition Language · 实体规则定义语言)

Status: v2.3 · beta Date: 2026-10-08 Version semantics: this document (the ERDL language specification) is version v2.3; the top-level protocol: "erdl/v2" (protocol identifier, fixed value) and version: "2.2.0" (rule-format version) are independent version identifiers, not to be conflated with the document version (this v2.3 change does not alter the rule format, so version stays 2.2.0). Version strategy: Breaking changes are limited to document major versions: a rule-format breaking change is limited to a version major (e.g. 2.2.0 → 3.0.0); a DO hash-preimage schema breaking change (e.g. v2.3 adding context) is explicitly identified by eval_profile.spec_version inside the DO (§8.2a.1b), by which verifiers select the recomputation schema — and does not violate the rule-format non-breaking promise of §2.3. Author: Tang Qixin(唐启鑫) Trademark: ERDL™ is a trademark of Shenzhen Miaojing Technology Co., Ltd. Positioning: ERDL (Entity-Rule Definition Language) is a declarative rule definition format, carried in YAML/JSON, for precisely expressing entity structures and behavior rules. This specification is independent and neutral — it defines only the format itself, depending on no particular implementation or upper-layer framework; its deterministic evaluation and canonical form support byte-for-byte cross-implementation verification. In ERDL, rules decide everything: rules are the carrier of semantics, the boundary of execution, the evidence of audit, and the context of governance. Conformance language: MUST / MUST NOT / SHOULD / SHOULD NOT / MAY in this document are interpreted per [RFC 2119]. The Chinese edition annotates normative wording bilingually — e.g. 「必须(MUST)」「不得(MUST NOT)」 — where the parenthesized RFC 2119 keyword is the normative authority; unannotated Chinese modal words (「不得」「禁止」「必须」「须」) are stylistic, and their normative level follows the RFC 2119 keyword of the corresponding sentence in this English edition.


1. Introduction

1.1 What ERDL Is

ERDL (Entity-Rule Definition Language) is a declarative rule definition format carried in YAML/JSON, for precisely expressing entity structures and behavior rules. It contains a rule declaration (Rule, defining a when → then decision) and a preset entity namespace (Entity, e.g. agent/tool/task — a semantic namespace convention for field references, not a top-level declaration; see §3). Entity defines the structure of an object; Rule prescribes the consequence of a condition; together they form an executable, reviewable, and verifiable rule expression.

1.2 Design Philosophy

ERDL is a “multi-party semantic layer” — it is not just a rule format, but a semantic convention layer shared by human, LLM, system, and audit:

Party ERDL’s role
Human (business/domain expert) the precise translation of natural-language rules, readable and reviewable
LLM (general model) structured input that removes ambiguity and supports deterministic evaluation
System (rule engine) standardized rule description; fn delegation controls the call boundary
Audit (regulation/compliance) a traceable rule record clearer than code + natural language

In this semantic layer, rules decide everything: humans express intent, the LLM translates semantics, the system executes decisions, and audit reviews the evidence. ERDL’s deterministic semantic layer gives an LLM clear direction: a user describes a rule in natural language; after ERDL precisely translates it, the LLM deterministically executes it per structured semantics — the conversational interface is the unified entry point. (Informative claim, with premise): this specification’s determinism guarantee covers only the post-translation evaluation layer (§7 E1–E12); the natural-language → ERDL translation itself depends on the LLM and is a non-deterministic process, outside the byte-for-byte “same input ⇒ same output” commitment.

1.3 Value at the AI Governance Level

The core challenge of AI governance is not whether a model can answer, but how probabilistic outputs satisfy the deterministic boundary required by regulation, auditing, and accountability. ERDL encodes declarative rules in YAML/JSON, codifying entity structures, behavioral constraints, and disposition actions into a verifiable deterministic rule layer, and turns “how an AI should act” from a training assumption into an inspectable object.

From trust to verification: Traditional governance relies on alignment assessments or post hoc explanations, making it difficult to prove which constraints a specific decision followed. ERDL expresses constraints as when → then rules. Each evaluation is bound to a canonical_tree snapshot and an outcome hash, enabling independent recomputation and byte-for-byte verification across implementations. Governance stakeholders no longer merely trust that a model has been “properly trained”; they can verify whether an action matched a rule, which rule it matched, and why that result was produced.

Accountable evidence chain: ERDL evaluation results are hashable and recomputable. Rule versions, input snapshots, decision outputs, and hashes can be anchored together to form audit records. Auditing is not a restatement of logs, but a reproducible proof process — why an action was allowed and why it was blocked can be traced and verified, preventing post hoc explanations from becoming the sole basis. “Who approved it” is verifiable only when the actor identity enters the chain with authentication evidence (§6a.5.4 event-authentication evidence) — a bare actor string is not a verifiable approval proof (see §6a.5.4), so the traceability of “who approved” is premised on authentication evidence, not an unconditional format property.

Mechanized human–machine accountability boundary: ERDL can use decision types such as REQUEST_HUMAN and ESCALATE to require human adjudication for high-risk operations, sensitive entity changes, irreversible actions, and similar cases. Final human decision-making authority no longer depends on procedural slogans; it is encoded as executable, testable, and auditable rule paths.

Compliance as code and cross-organizational mutual recognition: Regulations, platform policies, and internal red lines can be translated into ERDL rules and continuously validated through test vectors. Different organizations, implementations, or third-party auditors can independently recompute outcomes from the same rules and canonical_tree, establishing a “trust but verify” governance paradigm and turning compliance from declarative documentation into governance facts that are executable, inspectable, and mutually recognizable.

1.4 Design Goals

  1. Determinism: for the same rule and input, any conforming implementation MUST produce byte-for-byte identical evaluation results and hashes;
  2. Readability: any rule can be read back as natural language (gloss) that a human can instantly understand and review;
  3. Auditability: the rule’s evaluation process is independently recomputable and traceable;
  4. Cross-implementation verifiability: semantics converge to a single kernel, and conformance is proven by byte-for-byte comparison against test vectors.

1.5 Core Commitment

The semantic carrier is the kernel, not the syntax. Same canonical tree ⇒ same hash (one-directional commitment). Same (rule set, context, evaluation options, state) ⇒ same decision and DO. Equivalent semantics do not guarantee the same tree: e.g. a AND b and b AND a are semantically equivalent but differ in tree structure and hash. Determinism guarantees “same input ⇒ same output”, not “equivalent input ⇒ same tree” — the latter does not hold and is not part of this commitment.

Any scheme that takes “operator syntax” as its semantic carrier is forced to expand operators linearly as new requirements appear, so its cost never converges. This specification therefore converges semantics onto a single kernel (the expression tree), and treats the multiple writing forms as deterministic projections of that kernel — they are not independent languages, but different views of the same semantics. Rules decide everything: a rule’s validity depends not on its writing entry point or implementation form, but on canonicalized semantics that are unique, recomputable, hashable, and byte-for-byte verifiable.


2. Document Structure

2.1 Top-Level Format

An ERDL document (*.erdl.yaml) consists of six top-level fields — four MUST and two MAY — whose order MUST be fixed:

protocol: "erdl/v2"       # protocol identifier, fixed value
version: "2.2.0"          # rule-format version
metadata: { ... }         # document-level metadata (see §2.2)
state: [ ... ]            # state space declaration (optional, see §6a)
transitions: [ ... ]      # state transition rules (optional, see §6a)
rules: [ ... ]            # rule list (see §4)
Top-level field Type Required Description
protocol string MUST Protocol identifier, fixed value "erdl/v2"
version string MUST Rule-format version (semantic versioning)
metadata object MUST Document-level metadata
state array MAY State space declaration (§6a)
transitions array MAY State transition rules (§6a)
rules array MUST Rule list; elements defined in §4

2.2 metadata

metadata:
  name: "my-first-rule-set"
  description: "Allow file read operations"
  category: coding
  decision: ALLOW            # fallback decision when no rule matches
  tags: [example]
Field Type Description
name string Rule set name
description string Rule set description
category string Rule set category (coding/security/compliance…)
decision string Fallback decision (default verdict when no rule matches; see §6)
tags array Tags

2.3 Format Conventions

2.4 Parsing and Evaluation Overview

An ERDL document moves from file to decision result through a fixed five-step pipeline. Understanding this pipeline is understanding how ERDL is “parsed” and “evaluated”:

Step Action Input → Output Basis
① Load Read the rule document *.erdl.yaml → structured object §2.1–§2.3
② Validate Load-time type checking structured object → valid document (reject invalid) E5, §6a.2/§6a.4/§6a.7
③ Compile Normalize the three writing forms valid document → expression tree (canonical_tree) E7, §8.2
④ Evaluate Judge the tree node-by-node against the context expression tree + context → decision §7
⑤ Emit Produce evaluation evidence decision → hashable, recomputable result E6, §8

The full contracts for the input context and the evaluation output are in §7.0.


3. Entity Definition

An Entity is the subject a rule acts upon (passed via context). ERDL predefines the following Entity types:

Entity type Description
agent A single Agent instance
tool A tool invoked by the Agent
task A task executed by the Agent
workflow A multi-Agent orchestration process
human A human approver
guardian Supervisor

Field references in rules (e.g. tool.name, context.amount) use Entities as their semantic namespace. Field paths are load-bearing: a field name is frozen once published ([FREEZE-1]), and aliases MUST be normalized to the canonical name first.

Field-path grammar (MUST): a field reference path is a dot-separated segment sequence (a.b.c), each segment a snake_case identifier; $ is the root reference, $.path a root dot path; state.*/event.* are controlled namespaces (§6a.3/§6a.7). Path resolution follows the context object’s key path segment-by-segment (§7.0.1); array subscripts and dotted keys MUST use the deterministic dot-split (implementations MUST fix their path grammar in the document, avoiding divergence on dotted keys / array access).


4. Rule Definition

Rule is the core unit of ERDL: Rule = Metadata + When (condition) + Then (action) + Audit (audit). When compiles to an expression tree (§5), Then is a decision type (§6), and Audit records the audit information of the rule’s evaluation.

4.1 Field Definitions

The rules[] sub-field order MUST be fixed: name → description → category → priority → override → ring → tier → enabled → when → gloss → then → message → instruction → correction → unless → explanation → alternative → legal_basis → source_text.

Field Type Required Description
name string MUST Unique rule identifier, format [CAT]-[NNN]-description
description string MUST Human-readable description
category string MAY Rule-level category; defaults to metadata.category (see §2.2), allows mixed categories within one document
priority integer MUST Smaller number = higher precedence (see §7.1)
override string SHOULD Override level: critical > high > normal > low (default normal)
ring integer SHOULD Execution ring: 0 kernel / 1 recovery / 2 approval / 3 advisory (default 3 advisory)
tier integer MAY Rule tier 0–5 (0–2 safety baseline MUST use Simple, ≥3 business scope may use Expression); tier governs the writing form only, not the evaluation-error fold (see E12); absent = undeclared (never locked, see §7.1a)
enabled boolean MAY Rule enable flag (default true); false skips the rule during evaluation
when object / string MUST Trigger condition (see §5); the string form is only the catch-all literal "true" (unconditional match, compiled to a literal true node, §7.0.2 determination); a decision-table default row when: [] is the §5.4 rows[] row-level sub-field (compiled to a literal true), not a third form of this top-level field
gloss string MUST Natural-language readable projection rendered by the engine from the when tree (§5.5); lint checks gloss == render(tree), hand-writing forbidden; does not enter the hash (G4)
then string MUST Decision type (see §6)
message string SHOULD Decision message (blocking then MUST be non-empty)
instruction string MAY Advisory instruction (for the ALLOW + instruction case)
correction string MAY Correction text (CORRECT decision; source of the evaluation output primary_correction, see §7.0.3)
unless object/null MAY Exemption condition block (optional)
explanation string / object MAY Bilingual explanation (why the rule exists and what harm it prevents)
alternative string / object MAY Suggested alternative action when blocked
legal_basis string MAY Legal basis (citation of the regulation clause)
source_text string MAY Excerpt of the original regulation text

4.2 Complete Example

protocol: "erdl/v2"
version: "2.2.0"
metadata:
  name: "my-first-rule-set"
  description: "Allow file read operations"
  category: coding
  decision: ALLOW
  tags: [example]
rules:
  - name: "COD-001-allow-read"
    description: "Allow all read_file tool calls"
    priority: 10
    override: high
    ring: 3
    when:
      logic: AND
      conditions:
        - field: "tool.name"
          operator: eq
          value: "read_file"
    gloss: "tool.name equals read_file"  # engine-generated (G2), lint-enforced
    then: ALLOW
    message: "read_file call allowed"
    unless: null

5. The when Condition Expression

when is the rule’s embedded condition expression. ERDL provides three writing forms (Simple / Expression / Decision Table), any of which compiles and normalizes to the same semantic kernel (the expression tree) and can be rendered back into any form; there is additionally a readable projection gloss (§5.5) deterministically generated from the tree as natural language.

5.1 Writing Forms (Projections)

Projection Carries Applicable tier Description
A · Simple 30 operators 0–2 (MUST) Safety baseline, the most common writing form
B · Expression full 34-node tree ≥3 Business panorama: logic/quantifiers/arithmetic/time/aggregation
C · Decision Table matrix — Preferred by business/finance staff; compiles to the same kernel

The three forms share the same semantics, differing only in expressiveness and tier authorization. tier is a rule hierarchy level (0–5), ordered low-to-high by the rule’s constraint strength and scope — in this document, tier 0–2 is the safety baseline (MUST use Simple), and tier ≥3 is the business panorama (Expression may be used). when and expr MUST NOT coexist (E5).

5.2 Projection A: Simple (30 Operators)

Simple is the preserved, existing set of semantic units — 30 operators = 28 conditions + 2 modifiers, unchanged. It corresponds to system safety rules (tier 0–2).

5.2.1 Set definition

Family Count Operators
Comparison 6 eq · ne · gt · gte · lt · lte
List 2 in · not_in
String 5 contains · not_contains · match · starts_with · ends_with
Boundary negation 2 not_starts_with · not_ends_with
Existence 2 exists · not_exists
Length 5 length_gt · length_gte · length_lt · length_lte · length_eq
Range 2 between · not_between
Count 4 count_gt · count_gte · count_lt · count_lte
Modifier 2 within (time window) · rate (rate limit)

5.2.2 Semantic conventions

5.2.3 Authoritative compile mapping

Authoritative compile mapping: all 30 operators have a definite compile target, with none dangling — 13 direct nodes (eq/ne/gt/gte/lt/lte·in·contains/starts_with/ends_with/match·exists·between), 6 not-derived (not_in/not_contains/not_starts_with/not_ends_with/not_exists/not_between), 9 length/count compositions (length_* 5 + count_* 4), 2 time modifiers (within/rate).

# Simple operator Compile target Expression tree form
1-6 eq ne gt gte lt lte direct node comparison node
7 in direct node set node in
8 not_in not + exists guard exists(field) AND not(in(...))
9-12 contains starts_with ends_with match direct node string node
13 not_contains not + exists guard exists(field) AND not(contains(...))
14-15 not_starts_with not_ends_with not + exists guard exists(field) AND not(...)
16 exists direct node existence node (value non-null and non-undefined; empty string "", 0, false all count as “existing” — “existing” ≠ “non-empty string”)
17 not_exists not composition not(exists(...))
18-22 length_gt/gte/lt/lte/eq composition + exists guard exists(field) AND length(field) compare n
23 between direct node range node (numeric only)
24 not_between not + exists guard exists(field) AND not(between(...))
25-28 count_gt/gte/lt/lte composition + exists guard exists(field) AND aggregate(count(...)) compare n
29 within time modifier time window (as_of injected by engine)
30 rate time modifier + aggregate rate limit (temporal_state)

5.2.4 exists guard (E11 compile-layer guarantee)

exists guard (compile-layer guarantee of E11 null propagation): the not_* operators (except not_exists) and the length_*/count_* compositions MUST compile to exists(field) AND <derived expression>, not a bare not(positive operator) or a bare length/count(...) comparison. Reason: a positive operator returns false for a missing field and length(missing) returns 0; a direct not flip or numeric comparison would break null propagation (fail-open). not_exists is the sole exception — its semantics are “perceive field missing”, so it stays a bare not(exists(...)).

5.2.5 Stateful operators (within/rate)

Stateful operators (within/rate): within and rate are the only two stateful operators, whose evaluation depends on sliding-window counts across decisions. This state is not stored in the expression tree node but is maintained by a separate Guard state manager, entering the audit record as the temporal_state field — the expression tree itself remains a pure function (E1 holds), while the state source is auditable and recomputable.

Stateful operator truth semantics (MUST): count reaches threshold → condition holds (trigger); below threshold → record this event and return false (allow).

Operator Threshold First/under-limit After threshold
rate: "N/window" N first N times: record + false from the (N+1)-th time: true
within: "window" 1 first: record + false from the 2nd time in window: true (dedup)

Supporting constraints (all MUST): ① post-counting (count only when the positive condition holds); ② count isolation key (within keys on field+operator+value+scope, rate keys on field+operator+value+rate+scope; scope is the counting-subject scope — which field to group by (e.g. user.id/tool.name), avoiding a global counter shared across distinct subjects; default is global, ungrouped); ③ record timing in the “under-limit” branch.

5.3 Projection B: Expression (34-Node Tree)

5.3.1 Node set (34 nodes, 10 groups)

Expression opens the kernel’s full expressive power for complex business rules (tier ≥3). The semantic kernel is a typed expression tree of 34 nodes (in 10 groups), frozen at [FREEZE-2]:

Group Nodes Semantic capability
Value field · var · literal reference fields, context variables, constants (var only $/$.path; MUST NOT read clock or randomness)
Logic and · or · not composition
Comparison eq · ne · gt · gte · lt · lte operands may be field, variable, literal, or arithmetic subtree
Set in scalar ∈ set
String contains · match · starts_with · ends_with pattern matching; match uses safe regex
Existence/measure exists · length · between existence, length (Unicode code points), closed interval
Quantifier all · any · none per-element array judgment; empty array always false
Arithmetic add · sub · mul · div · round fixed-point deterministic arithmetic
Time days_between · epoch_ms · date_add · date_part · month_last_day date difference, timestamp, date arithmetic, component extraction, last day of month
Aggregate aggregate (count/sum/avg/min/max) array aggregation

Node total: Value 3 + Logic 3 + Comparison 6 + Set 1 + String 4 + Existence/measure 3 + Quantifier 3 + Arithmetic 5 + Time 5 + Aggregate 1 = 34. The 6 comparison operators, 4 string operators, 5 arithmetic operators, 3 quantifier kinds, and 5 aggregate functions are carried by parameterized node types in implementations (e.g. compare{op}, string{op}, arith{op}, quantifier{kind}, aggregate{fn}), so the “34 semantic nodes” map to fewer type literals in code — this is the relationship between semantic nodes and type projections, not a count contradiction.

String-normalization extension nodes (extension profile): beyond the frozen 34-node kernel, the kernel extension profile provides string-normalization nodes (unary string_transform, 3 operators): casefold (Unicode simple case folding; deterministic, locale-independent), trim (strip leading/trailing Unicode whitespace), and path_normalize (POSIX lexical path normalization: unify \// separators, collapse repeated separators, lexically resolve ./.., preserve a leading /). All three are pure string transforms (string → string); a non-string input returns null with a type_mismatch warning (safe fold). They are not part of the frozen 34-node kernel (same positioning as the fn node), but have a fixed S-expression shape (§8.2.1) entering the hash.

External-list node (extension profile): in_set{ref,digest} (the external-list variant of in, unary in_set) — membership judgment over large lists (e.g. AML country/sanction lists >256 items), not part of the frozen 34-node kernel. ref is the list reference identifier, digest is the versioned hash of the list content (sha256:hex(JCS(values))); the list body is injected by the evaluation context (resolveSet(ref)), does not enter the expression tree, and is not subject to in’s 256 inline cap; a digest mismatch or unregistered ref fails closed (not_ruleable). Membership semantics are identical to in (string NFC comparison, see §7.3(a)), with a fixed S-expression shape (§8.2.1) entering the hash.

5.3.2 Expression writing example

Expression writing example:

when:
  expr:
    lt:
      - div:
          - sub: [{ field: "tool.args.price" }, { field: "context.cost" }]
          - field: "tool.args.price"
      - 0.15

5.4 Projection C: Decision Table (Matrix Form)

5.4.1 Structure and example

The decision table faces business and finance staff, expressing multi-condition combinations in row-column structure, compiled to the same kernel:

kind: decision_table
columns:
  - field: "context.amount"
    label: "application amount"
rows:
  - when: [["gte", 10000]]
    then: "REQUEST_HUMAN"
    priority: 1
  - when: [["gte", 5000]]
    then: "ESCALATE"
    priority: 2
  - when: []                       # default row (unconditional, fallback)
    then: "ALLOW"
    priority: 3

5.4.2 Compile rules (E7)

Compile rules (E7): ① each row’s when condition group compiles to logical AND in field-column order, and each condition unit compiles to a comparison node; ② row order is precedence (first match from top, consistent with priority; the two MUST NOT conflict); ③ the default row when: [] compiles to literal true; ④ then MUST belong to the §6 decision type enumeration; ⑤ the compiled tree is identical to a hand-written Simple/Expression tree.

A decision table is syntactic sugar for rules[] (MUST): the table expands row-by-row into rules — one rule per row (then maps to each rule’s action.decision), with priority/ring/override/gloss/message attributed per row; the expanded rule set is semantically equivalent to a hand-written rules[] (E7). Row order is precedence, consistent with priority; when priority is absent it is auto-numbered by row order (1, 2, 3, …), and the default row (when: []) MUST be last with the largest priority.

5.5 Projection D: gloss (Natural-Language Readable Projection)

gloss is natural-language text deterministically generated from the tree:

gloss: "when (sale price minus cost) divided by sale price is less than 15%, human approval is required"   # engine-generated, lint-enforced

5.5.1 Five invariants (G1–G5)

Five invariants (all MUST):

# Invariant
G1 gloss = render(tree): deterministically generated by a frozen rendering template
G2 every rule and every transition rule MUST carry gloss; lint checks gloss == render(tree); hand-writing forbidden
G3 gloss MUST use the Entity’s display_name when a field contract exists (bilingual); falls back to the raw field path when no contract exists (except event.* payload keys, see §5.5) — so the lint check gloss == render(tree) remains reproducible without a contract
G4 gloss is a render product (does not enter the hash); displayed via live render(tree)
G5 Simple rules also generate gloss (rendered after compiling to a tree) — the reading layer is uniform

5.5.2 Rendering templates (per node)

gloss rendering templates (per node, English as canonical; {A}/{B}/{C} are recursive render results of sub-expressions):

Node English template
field {field}
var $ or path
literal {value}
and {A} and {B}
or {A} or {B}
not not ({A})
eq {A} equals {B}
ne {A} does not equal {B}
gt {A} is greater than {B}
gte {A} is greater than or equal to {B}
lt {A} is less than {B}
lte {A} is less than or equal to {B}
in {A} in {B}
contains {A} contains {B}
match {A} matches {B}
starts_with {A} starts with {B}
ends_with {A} ends with {B}
exists {A} exists
length length of {A}
between {A} is in the inclusive range {B} to {C}
all all elements in {A} satisfy "{B}"
any at least one element in {A} satisfy "{B}"
none no elements in {A} satisfy "{B}"
add {A} plus {B}
sub {A} minus {B}
mul {A} times {B}
div {A} divided by {B}
round {A} rounded
days_between days between {A} and {B}
epoch_ms epoch ms of {A}
date_add {A} plus {B} {unit}
date_part {part} of {A}
month_last_day the last day of the month of {A}
aggregate(count) count of {A}
aggregate(sum) sum of {A}
aggregate(avg) average of {A}
aggregate(min) minimum of {A}
aggregate(max) maximum of {A}

5.5.3 Special rendering rules

state.* gloss rendering (G3): a field node whose path’s first segment is state renders the state.<name> display_name (en, G3, §6a.1), falling back to the variable name; the exists boolean special case does not apply to state fields (state enum values are non-boolean; exists(state.x) is always “variable declared and always has a value”, independent of the enum value).

event.* gloss rendering (G3): a field node whose path’s first segment is event (only in transitions[].when, §6a.7) renders its four reserved fields as fixed English readable names: event.event_id → event id, event.on → event name, event.actor → event actor, event.at → event time; a payload key event.<key> has no display_name and renders as the bare key name.

exists rendering (MUST): exists uniformly renders as {A} is present — MUST NOT special-case is_*/has_* fields as “is true”: the semantics of exists is “the field is present” (§5.2.3, false also counts as present), and rendering “is true” would mislead exists(has_consent) (value false) as “has_consent is true”.

Gloss rendering details (must be reproduced exactly across implementations):


6. then Decision Types

The value of then MUST belong to the following 13 decision types:

# Decision type Semantics
1 ALLOW allow
2 DENY block
3 CORRECT correct deviation
4 NOTIFY notify
5 REQUEST_HUMAN request human adjudication
6 ESCALATE escalate
7 DELEGATE delegate
8 DEFER defer
9 EMERGENCY_HALT emergency stop
10 ROLLBACK roll back
11 QUARANTINE quarantine
12 WORKFLOW workflow (state machine; substates WORKFLOW_WAITING / WORKFLOW_PROGRESS; note: distinct from the §6a authority state machine)
13 GUIDE guide

Why 13 decision types (design rationale):

Traditional IT rule engines and access control are binary — ALLOW or DENY. They assume the decider is “the system”, which only needs to answer “allow or deny”.

But in the AI era, the thing being decided on is an understanding but not fully reliable LLM. Faced with an LLM’s output, a simple “deny” is giving up — it discards all the work the LLM has done and forfeits the chance to “correct once and keep going”. This specification’s design principle is: deterministic rules exist not to reject the LLM, but to maximize the LLM’s value while keeping the safety floor.

The 13 decision types unfold that principle into five groups:

Group Decision types Meaning
Allow vs. block ALLOW / DENY the binary floor — allow when clearly safe, block when clearly out of bounds
Guide, don’t abandon CORRECT / GUIDE when the LLM output deviates, correct or guide it back, rather than discarding the whole output
Human-in-the-loop REQUEST_HUMAN / ESCALATE / DELEGATE / DEFER when uncertain, bring in human adjudication, escalation, delegation, or deferral — hand “what the machine can’t resolve” to “people or process”
Safety fallback EMERGENCY_HALT / ROLLBACK / QUARANTINE intervene decisively on danger — halt, roll back committed side effects, quarantine suspicious objects
Process NOTIFY / WORKFLOW notify (record without blocking), workflow (enter a multi-step state machine)

In one line: traditional IT asks “allow or deny”; ERDL asks “how to let the LLM do better under control”. DENY is the last resort, not the only resort.


6a. State Blocks and State Transitions (Controlled State Source)

Session-level state — delegated authority, revocation, approval flows — needs to be declared explicitly. This section defines the state space (state) and state transition rules (transitions), generalizing the §5.2 within/rate “tree-external counter state” into named authority state. The state body is held by the engine (outside the expression-tree kernel); the expression-tree kernel (34 nodes) gains no new nodes — state is read-only injected via state.*.

Hard boundary: the state layer is a finite-state machine (FSM), not a Turing machine. The state space MUST be finite (enums + resource caps); arbitrary-length history, recursion, and pushdown stacks are rejected. Anything beyond FSM expressiveness (event sourcing) does not enter the engine.

Layering boundary (single instance): a state variable is document-instance single-key — one state variable holds a single value within the current ERDL document instance. Per-entity / per-authorization-relation multi-instance state (e.g. the P→A grant and the A→B grant each need their own state) is carried by the organization layer instantiating one document per relation — outside this section’s scope (layering: the organization layer consumes ERDL primitives; this state block is a single-instance FSM primitive).

6a.1 State Space Declaration (state)

state:
  - name: authorization          # state variable name, unique within the document
    values: [authorized, revoked]  # enum values (2–4 strings, unique within the list)
    initial: authorized          # initial state (MUST belong to values)
    display_name: { zh: 授权状态, en: authorization state }  # optional, gloss-readable name (G3)
Field Type Required Description
name string MUST State variable name: non-empty ASCII identifier ([A-Za-z_][A-Za-z0-9_]*), MUST NOT contain . (avoids field dot-path parsing collision; ASCII-only makes key order identical under UTF-8 code-point and UTF-16 code-unit orderings), MUST NOT be the reserved word state, unique within the document; state.<name> namespace
values array MUST Enum value list: 2–4 non-empty ASCII-identifier strings, MUST NOT contain ., values MUST be unique; strings MUST be NFC-normalized (E10) before comparison
initial string MUST Initial state, MUST be one of values
display_name object MAY Bilingual readable name { zh, en } (same as Entity convention, G3); gloss rendering of state.<name> uses the en value, falling back to name

A state variable is initialized to initial at document load and always has a value — there is no “runtime missing” state. To express “authorization not yet established” (unavailable), the author MUST declare an explicit sentinel value (e.g. unestablished) in values and set it as initial, rather than relying on null propagation.

6a.2 State Transition Rules (transitions)

A transition rule is the FSM’s F (transition function) — deterministic, triggered only by an event:

The transitions[] sub-field order MUST be fixed: on → name → audit_as → reason → enabled → when → gloss → set (name is an optional field, MAY, same format as §4.1; gloss is a render product, MUST, see the gloss clause below).

transitions:
  - on: revoke                  # triggering event name (the sole controlled-injection entry)
    name: "TR-001-revoke"       # optional, rule identifier (format per §4.1)
    audit_as: DELEGATE          # audit carrier only (does not participate in evaluation / short-circuit)
    reason: revoke              # transition semantic tag (distinguishes delegation vs revocation)
    enabled: true               # enable flag (default true)
    when: { ... }               # optional guard (expression tree; reads only state.* and event.*, not free context)
    set:
      authorization: revoked    # state transition: state.authorization ← revoked
Field Type Required Description
on string MUST Triggering event name (controlled injection, see §6a.3); MUST match [a-z][a-z0-9_]{0,31} (event names enter audit and hash; free Unicode adds normalization burden)
name string MAY Transition rule identifier (format per §4.1’s [CAT]-[NNN]-description); when absent, error attribution uses (on, reason, definition ordinal)
audit_as string MAY Audit classification label (audit-only, see §6a.5): defaults to NOTIFY; value narrowed to {ALLOW, NOTIFY, DELEGATE, ESCALATE, REQUEST_HUMAN} (borrows the then vocabulary, but its semantics is audit classification, not an evaluation decision — this field does not participate in §7.0.2 evaluation and triggers no short-circuit), MUST NOT be a restrictive type (DENY/EMERGENCY_HALT etc.)
reason string SHOULD Transition semantic tag (e.g. revoke); MUST match [a-z][a-z0-9_]{0,31}, SHOULD be unique within the document (lint)
enabled boolean MAY Enable flag (default true); when false the transition rule does not participate in event handling (gray-release / emergency disable without editing the document)
when object MAY Guard condition (compiled to expression tree; reads only state.* and event.*, MUST NOT read free context, see §6a.7)
gloss string MUST Natural-language readable projection rendered by the engine from the when tree (§5.5); lint checks gloss == render(tree), hand-writing forbidden; does not enter the hash (G4)
set object MUST State transition mapping; keys are state variable names, values MUST be one of that variable’s values

transitions gloss (G2, MUST): like rules[].when, the transitions[].when guard expression tree is rendered to gloss by the engine (§5.5), with lint verifying gloss == render(tree) and forbidding hand-writing; state.*/event.* rendering is per §5.5. A transition rule’s gloss serves audit readability only (not in the hash, same as G4) and does not change evaluation semantics.

6a.2.1 Transition semantics

6a.2.2 Guard constraints

6a.2.3 Same-variable conflict check (decidable, sound)

6a.2.4 Consistency and load-time validation

6a.3 State Controlled Injection (Security Foundation)

6a.4 Resource Caps (Anti-Bloat)

The state space MUST be finite; exceeding the caps rejects loading (Error):

Dimension Cap Basis
State variable count (state array length) ≤ 4 authority / freshness / approval / task — four common state kinds
Single-variable values enum size 2–4 revocation 2 states (valid/revoked); authority/approval/task at most 3–4 states
State combination space (∏ values length) ≤ 4⁴ = 256 anti combinatorial explosion
Transition rule count (transitions array length) / document ≤ 32 bounds the pairwise mutual-exclusion check (§6a.2) to O(n²)
Distinct on event-name count ≤ 16 finite event-name space, anti unbounded growth
Event payload ≤ 8 keys / depth ≤ 2 / single value ≤ 256B controlled payload, anti large payload (§6a.7)

Delegation chain depth (the hop count of per-entity multi-instance) is an organization-layer constraint, not a single-instance FSM state-space dimension, and is outside this section’s resource caps (§6a layering boundary).

6a.5 State Transition Audit Closure

State outside the kernel ≠ state un-auditable. Audit closes over three rings plus a cryptographic link ring, all required:

Ring Audits Mechanism
① Transition chain every step of authorized→revoked transition event = transition audit record (audit_as mapped to the §6a.2 narrowed set), previous_hash serial anchoring
② Snapshot the state value read at evaluation state_snapshot enters the DO, in the hash preimage (§7.0.3)
③ Validity only transition rules may change state, in a legal direction engine validates the transition (fail-closed); undeclared transitions do not execute

6a.5.1 state_snapshot structure

state_snapshot structure (provenance anchoring, MUST): state_snapshot is:

{ values: { <state-variable-name>: <value> },   # the state variables actually read in this evaluation (on-demand, not the full state)
  state_version: <uint>,                        # starts at 0 (i.e. genesis); +1 per successfully committed event transaction
  transitions_head: <hash> }                    # hash of the most recent state-changing audit record; genesis hash initially

All three enter the DO hash preimage (§7.0.3). state_version and transitions_head anchor the snapshot to the transition chain:

6a.5.2 Replay verification

6a.5.3 state_snapshot serialization normalization

Two implementations differing in any of key order / encoding / field order would compute different DO hashes, violating the “semantics = tree = hash” core promise — hence all three MUST be normalized.

6a.5.4 Event injection authentication

Event injection authentication (MUST): event injection MUST be engine-authenticated — the actor identity (§6a.7) enters the transition audit record; an unauthenticated event MUST be rejected (fail-closed). No arbitrary caller may inject revoke/authorize events.

Event authentication evidence (SHOULD): an authenticated event SHOULD carry verifiable authentication evidence — a signature or proof digest (e.g. a JWS kid + digest), written into the transition audit record; when the actor identity enters the chain, its authentication evidence SHOULD enter the chain too, so “who approved it” is independently verifiable (closing the hole that actor as a mere string is forgeable). Note: the current chain records only the actor string — no evidence field exists in the §8.2a.2 record field order — so “who approved” is not yet independently verifiable.

audit_as is not proof of human approval: audit_as is only an audit label and carries no approval proof; an attacker can inject a forged event with actor: human-1. The only auditable form of human approval = the authenticated identity layer injecting the event as a human identity (actor enters the chain) — audit_as: REQUEST_HUMAN does not mean “this transition is itself a human approval”.

6a.5.5 The three audit-record kinds

Successful transition record (first-class on-chain record, MUST): a transition that successfully commits set produces a successful transition record, with the format:

{ type: "transition", event_id, on, actor, at, audit_as, set, state_version, previous_hash }

transition_error record (first-class on-chain record, MUST): a transition_error (on EvaluationError, §6a.2) is a first-class on-chain record with the format:

{ type: "transition_error", event_id, on, actor, at, audit_as, error, errored: true, previous_hash }

Guard warnings not recorded (MUST): a transition guard’s type_mismatch warning only affects the evaluation fold (§7.3(a), errored=false) and is not recorded — neither success nor error transition records carry a warnings field; the guard’s audit focus is the transition outcome (whether set commits), not evaluation-process warnings.

Rejected-event disposition (MUST, on-chain / off-chain separation): unauthenticated, duplicate-event_id, and over-resource-cap events MUST NOT enter the hash chain; they are recorded in off-chain audit storage (actor if known, event name, event_id, rejection reason, content sanitized before persisting) — on-chain holds only trusted records, off-chain holds intrusion traces. This ensures: the auditor sees the full trusted state-transition history on-chain, and sees injection attempts / replays / over-limit attacks off-chain, without cross-contamination.

Genesis record (MUST): at document load the engine MUST generate a genesis audit record for initial (initial snapshot + document canonical-tree hash). This is the starting point of transition chain ① — a document with initial=authorized can answer on-chain “why it was authorized to begin with”.

Two load entries (MUST): ① create (first instantiation of an instance) — generates genesis (carrying instance_id, see below), writing a new chain; ② restore (recovering from an existing chain) — MUST verify the latest authoritative head (§6a.9) and fail-closed on failure (reject the load, MUST NOT reset state to initial). Implementations MUST distinguish the two, MUST NOT treat restore as create — otherwise a restart resets state to initial, and if initial is an authorized state the revocation is bypassed.

Genesis preimage byte-level definition (MUST):

rule_set_hash binding (MUST): doc_tree_hash does not include rules, so state machines with the same name and structure but different rules can share the same chain — the meaning of a state value (e.g. revoked) depends on the rules consuming it. Therefore the DO-layer rule_set_hash (§8.2a.1a, the full rule-set semantics hash) and doc_tree_hash jointly anchor “state machine + rules”: recomputation/verification MUST have both consistent, otherwise the chain/decision is judged not to belong to the current (state-machine, rules) combination. metadata.name is not a security boundary — the same name does not mean the same rules; identity binding is by hash.

doc_tree_hash preimage field order and fixed key set (MUST, pinned field by field):

name        → metadata.name (document name, identity anchor)
state       → [ { name, values, initial } ] (state-space declarations; excludes display_name)
transitions → [ { on, name, audit_as, reason, enabled, when, set } ] (transition rules; excludes gloss)

Here initial’s keys are ordered by state-variable-name RFC 8785 JCS key order (UTF-16 code-unit ascending); the previous_hash: null key is not omitted (fixed key set). The three kinds’ full field order and fixed key set are in §8.2a.

initial (genesis record) and state_snapshot.values (§7.0.3) are both “state-variable → value” mappings, but belong to two different preimages — the genesis record and the DO evaluation result — with their own fixed field names: initial expresses the initial state, values expresses the current state read at evaluation; implementers MUST serialize under each respective field name and MUST NOT conflate them.

6a.5.6 Rejected-event disposition and concurrency

Normative guidance (SHOULD): documents carrying authorization semantics SHOULD set initial to the most conservative sentinel (unestablished/revoked); an explicit bootstrap authorization must go through a transition event with an actor, leaving on-chain provenance, rather than a groundless initial=authorized.

Concurrency semantics (per document instance MUST serialize): event handling and evaluate() on the same document instance MUST be mutually exclusive; the state_snapshot that evaluate() sees is a consistent snapshot at evaluation start, no intermediate state allowed.

Ring ③ is what the snapshot model (state materialized externally) cannot do, and is the core of an explicit state machine: the engine verifies “how the state legally arrived here” instead of accepting external state wholesale.

6a.6 Relationship to within/rate

within/rate are an extremely narrow special case of this FSM (state=counter, transition=+1, output=over-limit). State blocks generalize them without changing their existing semantics. The two coexist: within/rate remain injected via temporal_state; state blocks are injected via state.*. State blocks and within/rate counters do not share a namespace: state blocks use state.<name> (driven by the §6a.1 state declaration), within/rate use field+operator+value (§5.2 count-isolation key) — the two key-isolation rules are independent and do not collide.

6a.7 Event and Transition Evaluation Context (Controlled Injection)

Transition-rule guard evaluation is likewise brought into the “controlled injection” model (E1) and does not read free context. The event object, guard context, and compilation discipline MUST be as follows:

6a.7.1 Event object structure

Event object, structure MUST be:

event:
  event_id: "evt-2026-09-12-0001"   # unique within the document; replay-deduplication basis
  on: revoke            # event name (matches transitions.on)
  at: "2026-09-12T10:00:00Z"  # engine-injected UTC moment (the transition evaluation's as_of)
  actor: "agent-A"      # authenticated event-source identifier
  payload: { ... }      # restricted payload (see below)
Field Type Description
event_id string Unique within the document; replay-deduplication basis (the same event_id is processed only once)
on string Event name, matching transitions.on
at string Engine-injected UTC moment (the transition evaluation’s as_of); MUST NOT be supplied externally (E9 no-wall-clock applies equally)
actor string Authenticated event-source identifier (provided by the identity layer, not self-asserted by payload); string MUST be NFC-normalized (E10, enters the transition-audit-record hash preimage)
payload object Restricted payload: ≤8 keys, depth ≤2, scalar leaf values, single value ≤256B; keys MUST NOT contain . and MUST NOT be the four reserved field names (event_id/on/actor/at)

6a.7.2 event.* resolution mechanism

event.* resolution mechanism (MUST):

6a.7.3 Transition evaluation context (the input to when)

Transition evaluation context (the input to when): the when guard’s evaluation context MUST be only the following two kinds, and MUST NOT read free context:

  1. Pre-transition state snapshot state.* (the state before any of this event’s transitions take effect, §6a.2);
  2. Event object event.* (event.event_id/event.on/event.actor/event.at + payload keys event.<key>; event.at is the engine-injected UTC moment, i.e. the transition evaluation’s as_of).

A guard reading any field outside state.* and event.* (free context) MUST be rejected at load (Error, see §6a.2 load-time validation full set).

6a.7.4 Compilation and evaluation discipline

Compilation and evaluation discipline: transition evaluation MUST go through the same single compilation pipeline as rule evaluation (E7); its warning / errored conventions are fully consistent with §7.3.

6a.7.5 Time and freshness (no time trigger)

Time and freshness (no time trigger, MUST): the state machine has no time trigger — transitions are event-driven only, state never expires on its own. When a “freshness” state (e.g. fresh/stale) is needed, choose one of two modes:

  1. External sweeper injection: the organization layer periodically injects an on: expire event, driving the fresh → stale transition (the engine keeps no timer);
  2. Guard time comparison: when compares expiry moments using event.at / epoch_ms, deciding expiry at event arrival (no background timer).

Example (authorization expiry, mode 2):

state:
  - name: authorization
    values: [authorized, revoked]
    initial: revoked
transitions:
  - on: exercise
    audit_as: ESCALATE
    reason: auth_expired
    when:
      gt:
        - epoch_ms: { field: "event.at" }
        - epoch_ms: { field: "event.expires" }
    set: { authorization: revoked }

The engine does not “auto-expire over time” — time exists only as a controlled event attribute (event.at) or as a guard time-comparison input, consistent with E9 (no wall clock, controlled as_of injection).

Expiry/entitlement carrying (MUST): scalar constraints such as expiry and quota MUST be carried by an authorization-root-signed credential field (written into the chain, committed with the authorize event), MUST NOT be carried by the caller-payload event.expires — a caller can omit or extend expires, making expiry meaningless. When a guard references event.expires, its value MUST come from the authenticated credential (§6a.5.4 event authentication), not free payload. “Inject an exercise event before every evaluate” is a boundary obligation (integration requirement, §6a.8): the engine does not advance time on its own, and expiry is triggered by the boundary injecting an exercise event before each evaluation.


6a.8 Enforcement-Boundary Check/Act Atomicity (Integration Requirement)

§6a’s authority state is consumed by an enforcement boundary (the Action Guard / tool-call guard, §9.1) that gates security-sensitive side effects. evaluate() is a pure function (E1): it returns a decision plus a state_snapshot = { values, state_version, transitions_head } (§7.0.3), but it does not itself commit the gated side effect — that commit happens in the enforcement boundary, a distinct component, after evaluate() returns and releases the instance lock.

This leaves a check/act window: evaluate() may return ALLOW against state_version = N, a revoke event may then commit state_version = N+1 before the effect lands, and the effect would still execute under a now-superseded authorization lineage.

Enforcement-boundary re-validation (MUST): for a security-sensitive side effect whose authorization depends on §6a state, the enforcement boundary MUST ensure that no state-change event affecting the authorization lineage commits between the state version used for the authorization decision and the commit of the protected effect. A conforming boundary satisfies this by one of:

  1. Atomic re-validation: immediately before committing the effect, re-read the document’s current { state_version, transitions_head } under the instance lock (§6a.5) and compare it with the decision’s state_snapshot; on any mismatch, fail closed (do not execute — treat as unavailable/stale authority, AV-05/AV-10/AV-14 semantics); or
  2. Equivalent closure of the synchronous boundary: hold the relevant lock (or an equivalent serialization guarantee) across the effect commit so no transition event can interleave between check and act.

Layering (engine vs. boundary): the engine MUST expose the re-validation primitive — the current state_version/transitions_head readable under the instance lock — but it does not execute the side effect and does not hold the lock across the effect commit on the boundary’s behalf (E1: evaluation is pure; the commit is outside the engine). Check/act atomicity is therefore an integration obligation the enforcement boundary discharges by re-validating against the engine’s snapshot anchor, not an engine-side side-effect-execution guarantee.

Adversarial conformance vector (V-STATE): authorized@N → evaluate(ALLOW@N) → revoke@N+1 (before effect commit) → attempt the effect. Expected: the effect MUST NOT execute under the stale ALLOW; the boundary re-validates and fails closed (or otherwise closes the boundary). This is the stateful continuation of AV-05 / AV-10 at the execution boundary.

6a.9 Latest-Authoritative-Head Freshness (Anti-Rollback, Integration Requirement)

§6a.5’s replay verification proves that a snapshot is consistent with a valid transition-chain prefix (integrity/provenance), but it does not prove that the prefix is the current latest authoritative prefix (freshness). The two MUST be distinguished.

Rollback attack (adversarial scenario): authorized@N / HN → revoke commits @N+1 / HN+1 → a restart/recovery/replica restoration presents a valid historical prefix ending at {N, HN} → replay verification of that prefix succeeds (it was not forged or modified, only superseded) → evaluation sees authorization = authorized → the enforcement boundary re-validates against the same restored instance and again observes {N, HN}. Result: a previously revoked authorization becomes exercisable again without violating the existing hash-chain replay checks.

Latest-authoritative-head freshness (MUST): for a security-sensitive side effect whose authorization depends on §6a state, the enforcement/recovery boundary MUST ensure that the accepted { state_version, transitions_head } is the latest authoritative state for the same document instance — not a historical prefix superseded by a later authoritative state. A conforming boundary satisfies this by one of:

  1. Monotonic external anchor: establish the latest authoritative head via a monotonically increasing external epoch (or a durable latest-head anchor, a signed/versioned checkpoint, a consensus-backed state version); or
  2. Equivalent anti-rollback mechanism: any implementation-neutral equivalent mechanism that prevents a superseded historical prefix from being accepted as current.

If the freshness of the latest authoritative state cannot be established, the authority exercise MUST fail closed (treat as unavailable/stale authority, AV-05/AV-10/AV-14 semantics).

Layering (engine vs. boundary): hash-chain integrity is guaranteed by the engine (§6a.5); latest-authoritative-head freshness across restart/recovery/replica boundaries is an integration obligation the enforcement/recovery boundary discharges via an external anchor — the engine maintains no cross-instance persistent epoch and does not decide recovery policy on the boundary’s behalf (E1: evaluation is pure). The durable freshness anchor is provided by the organization/deployment layer; and the fail-closed property MUST be preserved: if the enforcement boundary cannot establish that the restored { state_version, transitions_head } is sufficiently fresh relative to the authoritative persistence state, authority-bearing effects MUST NOT proceed — successful replay/integrity verification is never sufficient evidence that authority is still current.

Adversarial conformance vector (V-STATE): authorized@N / HN → revoke commits @N+1 / HN+1 → restore a valid historical prefix (ending at @N / HN) → replay verification succeeds → evaluate a protected action → attempt the effect. Expected: the protected effect MUST NOT execute using the superseded authority state; the system MUST establish that {N, HN} is still the latest authoritative state, discover that it has been superseded, or fail closed when freshness cannot be established.

6a.10 Authorization-Root Provenance for Establishing/Re-establishing Authority (Integration Requirement)

§6a.2 models authorize/revoke uniformly as event-triggered state transitions (the FSM’s F function), with event injection authenticated by actor (§6a.5.4). But actor authentication only proves “who triggered the event”, it does not prove “whether the triggerer is entitled to establish that authority”. Without constraining the provenance of establishing/re-establishing authority, revocation degenerates into a reversible local state bit — a revoked subject can flip revoked → authorized by merely triggering an authorize/re-authorize transition without proving where the renewed authority came from.

Authorization-root provenance (MUST): for a state variable carrying authorization semantics, any transition that makes the authority exercisable (i.e. a set that moves the variable into an “exercisable” value such as authorized) MUST carry authorization-root provenance — the triggering event’s actor MUST be attributable to a principal/authority entitled to establish/re-establish that authority (the authorization root), not the authorized subject itself. After revocation, a revoked → authorized re-authorization MUST be supported by a new valid authorization basis; a local state transition alone (without authorization-root provenance) MUST NOT make the authority exercisable again. A descendant or previously-authorized subject MUST NOT restore its own revoked authority by merely triggering an authorize/re-authorize state transition.

Layering (engine vs. boundary): the engine exposes an identifiable marker for “authority-establishing/re-establishing” transitions (the reason semantic marker, e.g. reason: authorize) and records actor into the transition audit chain (§6a.5.4, already present); determining authorization-root eligibility (who is entitled to establish that authority) is an integration obligation of the enforcement boundary / organization layer — the §6a single-instance FSM does not model the P→A→B authority chain (§6a.1 layering boundary); who is entitled to authorize is adjudicated by the organization layer. Before committing an authorize/re-authorize event the enforcement boundary MUST verify the actor’s authorization-root eligibility; when the authorization root cannot be established, it MUST fail closed (treated as unavailable/unauthorized, AV-05 / AV-10 / AV-14 semantics).

Interface to organization-layer invariants: this section’s authorization-root provenance is the single-instance-FSM-layer primitive support for the organization layer’s delegated-authority invariants (authority non-amplification INV-01, narrow-only constraint inheritance INV-03, transitive revocation INV-04) — the organization layer consumes the “establishing/re-establishing authority must be attributable to the authorization root” primitive to guarantee the chain-level INV invariants. Their full definitions live in §6b, beyond this section’s single-instance FSM scope (§6a.1 layering boundary).

6a.11 Boundary minimal API (interface signatures)

§6a.8–§6a.10 state integration obligations as MUST; their conformance depends on the enforcement/recovery boundary being able to read the state anchors the engine exposes. To remove the “integration obligations without interface definitions” dangling, this specification pins the minimal revalidation API the engine MUST expose (interface signatures, language-neutral; the host-language type mapping is implementation-defined, but field semantics MUST match the table below):

Primitive Signature Semantics
get_head () → { state_version: uint, transitions_head: hash } returns the current document instance’s latest authoritative head (the comparison anchor for §6a.9 freshness and §6a.8 revalidation); read under the instance lock
get_value (name: string) → string \| undefined returns a state variable’s current value (the underlying primitive of §6a.3 state.<name> read-only resolution)
get_chain () → AuditRecord[] returns the transition-chain audit records (genesis / transition / transition_error, §6a.5 three kinds)
snapshot (read_vars: string[]) → { values, state_version, transitions_head } on-demand state snapshot (§6a.5.1 / §7.0.3, entering the DO hash preimage)
inject_event (event: Event) → { record: AuditRecord \| null, disposition: committed \| noop \| rejected, error? } injects an event (§6a.2.1 atomic processing; receipt committed / noop / rejected)

6b. Delegated-Authority Security Model (Organization Behavior Layer)

Attribution: The delegated-authority security invariants (INV-01–INV-05) and associated adversarial conformance vectors (AV-01–AV-14) were proposed by Ravindra Annam and subsequently refined and developed through technical review and collaboration with OpenOBA.

§6a defines the single-instance FSM (the state machine of a single authorization relationship); this section defines the security invariants of the delegation chain (multiple authorization relationships composed along “authorization root → intermediate node → authorized subject”) — constraining “how authority propagates along the delegation chain”, the normative semantics of the organization behavior layer. Layering: §6a provides “verifiable adjudication of authorization state”, this section guarantees “the delegation chain’s security invariants”; per-relationship multi-instance state is carried by the organization layer instantiating one document per relationship (§6a.1 layering boundary). In this section “delegation” means delegation of authority (propagating authority along the authorization chain), distinct from the §6 DELEGATE decision type (human-in-the-loop: handing “what the machine cannot handle” to a human or process).

6b.1 Umbrella: Delegation Must Never Manufacture Authority (MUST)

All delegation, assignment, re-delegation, transitive delegation, privilege brokering, downstream constraint change, and revocation MUST NOT let effective authority exceed or escape the originating authority chain:

effective_authority(subject) ⊆ authority(chain) — effective authority is a subset of the originating authority chain; no operation may amplify it.

6b.2 Five Delegated-Authority Invariants (INV-01~05)

Each invariant = property + violation shape + normative assertion.

INV-01 Authority Non-Amplification

INV-02 Provenance Continuity

INV-03 Narrow-Only Constraint Inheritance

INV-04 Transitive Revocation

INV-05 Capability Boundary Axis

6b.3 Revocation Freshness (Mechanism-Neutral)

This section generalizes §6a.9 (latest-authoritative-head freshness) to the delegation-chain layer: §6a.9 constrains single-instance-FSM state-head freshness, this section constrains the freshness of a delegation-chain ancestor’s revocation state.

Before exercising authority that depends on a revocable ancestor, the enforcement boundary MUST establish that revocation state satisfies the configured freshness requirement; absence of visible revocation MUST NOT by itself establish continued validity; when freshness cannot be established, fail closed. Mechanism-neutral: monotonic epoch / lease / version vector / signed status object / online introspection / equivalent mechanisms.

6b.4 Adversarial Vector Family (AV-01~14)

Convergence criterion = decision + matched_invariant + first_invalid_boundary. Full vector table in the independent conformance suite (vectors/ + conformance/CONFORMANCE.md).

7. Evaluation Semantics

7.0 Evaluation Overview

Evaluation = the pure-function process (E1) by which the expression tree (the compiled product of rules) judges the input context node by node. This section defines the evaluation input contract, the algorithm steps, and the output contract, to align implementers and users.

7.0.1 Input Contract (Context Object)

The evaluation input is a context object carrying the current state of the rule’s subject entities, namespaced by Entity (§3):

context:
  tool:                 # Entity: tool
    name: "issue_refund"
    args: { amount: 8000, order_id: "O1024" }
  country: "CN"         # free-form fields directly at the context top level (aligned with the RFC-002 `context` field)
  role: "operator"
  # other Entities: agent / task / workflow / human / guardian (provided as needed)

7.0.1a Field Contract (EntityFieldContract)

A field contract declares a context field’s type and semantics, used for: ① LLM prompt generation (constrain field names, no inventing); ② gloss rendering (display_name, G3); ③ runtime type checking under strict mode.

Field Type Description
field string field name (snake_case, enters the kernel/hash)
display_name object bilingual display name { zh, en } (enters gloss, G3; render-layer only, not entering kernel/hash)
type string field type: number / boolean / string / string[] / date
optional boolean (optional, default true) whether the field may be absent; false means required, and a missing required field is an evaluation error (fail-closed, E12)
description string field semantics
default_value any (optional) default value when the field is absent
definition_period string (optional) definition period: DAY / MONTH / YEAR / ETERNITY (aligned with OpenFisca Variable.definition_period); informational field — used only for parameter-evolution hints, not entering the kernel/hash, not participating in evaluation
provenance string (optional, informative) fact-source type (§7.0.1b: system_observed / llm_asserted / user_input / config / derived); informational field — declares the fact source for governance and rule review, not entering the kernel/hash, not participating in evaluation (normative red line in §7.0.1b)

Under strict mode, a contracted field whose value type does not match type records a type_mismatch warning (same as §7.3(a) strict mode); a contracted field with a default_value and absent is evaluated by the default (not E11 null propagation); a contracted field with optional: false and absent and no default_value is an evaluation error (errored=true, E12 fail-closed).

7.0.1b Fact-source Typology (informative)

Corresponding to §6’s systematic treatment of the conclusion space via the 13 decision types, the premise space of the context object is classified by source as follows. This subsection is an informative classification, deliberately asymmetric with the output-side closed enumeration: decision types enter the fold and the hash and must stay closed and frozen (the existing §6 closed-enumeration requirement); fact sources evolve openly (new integration forms keep appearing), do not enter evaluation semantics or the hash, and may be freely extended:

Source type Description Typical example
system_observed system-observed: values directly observed by the host system context.amount taken from a payment-system response
llm_asserted LLM-generated assertion: tool names, arguments, intent descriptions output by the model — possibly hallucinated; an “assertion to be verified”, not yet a “fact” tool.name, tool.args.*
user_input direct input by a human user an amount submitted by the user in a form/dialogue
config configuration snapshot: static parameters fixed at deployment thresholds, allowlists
derived derived value: computed from the above sources cumulative amount, aggregate statistics

ERDL’s fact-neutrality stance (design note): the engine does not vouch for the truth of facts — for an llm_asserted field, ERDL guarantees “deterministic evaluation + complete preimage (recomputable) for the given input”, not “the input itself is trustworthy”. Fact-trustworthiness governance is borne by three layers: ① the field contract (§7.0.1a) declares the source via the informational provenance field; ② rule authors explicitly consume source differences (rules over llm_asserted fields written more strictly than over system_observed — expressed by rules, not by implicit engine down-weighting); ③ the controlled injection of state.*/event.* and event authentication (§6a.3/§6a.5.4) provide source assurance for the authority lineage. Implementations MUST NOT admit provenance into evaluation semantics or hash preimages — mixing trust assessment into deterministic evaluation would break the closed-kernel positioning of “semantics = tree = hash” (E1/E6).

7.0.2 Evaluation Algorithm

Input: rule set rules[] + context object context (state-machine events already processed first, see step 0)
Output: the decision result (see 7.0.3)

0. Events happen first (happens-before declaration, no extra action): events are already processed at arrival (eager, §6a.2).
   evaluate() acquires the instance lock (§6a.5); its state.* input is the state after all arrived events have committed.
1. Sort: by priority ascending (smaller = higher priority)
2. Group: execute rings in order 0 to 3 (0 kernel → 1 recovery → 2 approval → 3 advice)
3. Within each ring, evaluate each rule in order:
   a. the unless exemption is judged before when — on exemption, record and skip the rule (unless and when share the same evaluation context: context + state.* read-only injection)
   b. the compiled when expression tree judges context node-by-node (true / false / error)
   c. a match does not short-circuit (only `EMERGENCY_HALT` is the exception, noted below): only `EMERGENCY_HALT` short-circuits on match; other decisions (including DENY/ROLLBACK/QUARANTINE/WORKFLOW) continue (an override ALLOW may cover)
   d. override: only the DENY → ALLOW direction, never to a less-safe state (§7.1)
4. Fallback: no rule matched → metadata.decision (fallback decision, §2.2)
5. Summarize: produce decision + matched_rules + evidence (canonical_tree / hash / eval_trace / state_snapshot)

7.0.3 Output Contract (Evaluation Result)

The evaluation result MUST contain the following fields:

Absence encoding (MUST, §8.2a): nullable object-type fields (primary_instruction, primary_reason, primary_explanation, primary_correction, temporal_state, state_snapshot) encode as null when valueless, key MUST NOT be omitted; list-type fields (unless_exemptions, eval_warnings, canonical_trees) encode their empty state as [] (key not omitted), and matched_rules is always an array — keeping the DO hash-preimage structure constant. Exception: errored is a boolean flag (E3), always valued (EvaluationError → true, otherwise false); it does not participate in absence encoding. The full empty-state encoding boundary is in §8.2a.

Field Description
decision the final decision (one of the §6 enum, or the fallback decision)
matched_rules the matched rules (in evaluation order)
unless_exemptions rules exempted via unless (recorded separately, not counted in matched_rules)
primary_instruction the primary instruction (ALLOW + instruction scenario)
primary_reason the primary reason (DENY and other blocking scenarios)
primary_explanation the primary explanation (may be bilingual)
primary_correction the correction text (CORRECT decision; sourced from the rule field correction, see §4.1)
total_evaluated the total number of rules whose unless/when evaluation was actually entered (rules skipped by catch-all inertness are NOT counted)
total_matched the total number of rules matched
temporal_state the within/rate sliding-window state snapshot (encoded as null when nothing matched, key not omitted, §8.2a)
state_snapshot the state.* values read during evaluation: { values, state_version, transitions_head } (encoded as null when no state was read, key not omitted, §8.2a); enters the DO hash preimage (§6a.5)
canonical_trees the matched rules’ canonical tree snapshots (tree = canonical-tree JSON) and hashes (sha256: prefix), E6 evidence
rule_set_hash the hash of the full rule-set semantics (including the fallback decision; §8.2a.1a), closing the gap that canonical_trees covers only matched rules’ when trees, so a third party can verify “no other rule should have matched”
eval_warnings non-fatal warnings collected during evaluation (E3)
indeterminate_rules the list of rule names whose when evaluated to unknown (S3 top-level unknown folding; empty state encodes [], key not omitted, §8.2a)
errored whether an evaluation error occurred (E3); Guard contexts fail-close, covering all tiers (E12)
as_of the evaluation moment injected by the engine (ISO UTC, E9)

The evaluation evidence (canonical_tree snapshot, result hash, eval_trace) are independently recomputable derived products (§8.2, E6) — canonical_tree enters the hash, eval_trace does not (§8.3).

7.1 Precedence and Conflict Resolution

  1. Sort by priority ascending (smaller value = higher precedence);
  2. At equal priority, sort by override level (critical > high > normal > low);
  3. override enumeration: critical > high > normal > low (default normal);
  4. Equal priority and equal override: definition order;
  5. override is allowed only in the DENY → ALLOW direction (it MUST NOT override to a less-safe state); an override ALLOW (critical/high) covers a restrictive rule r only if level(o) > level(r) and ring(o) ≤ ring(r) — an outer ring (higher ring number) MUST NOT cover an inner ring (lower ring number); the tightening direction (DENY / ROLLBACK / QUARANTINE covering ALLOW) is the default consequence of “MUST NOT override to a less-safe state” — it does not compare ring and needs no override (an override on a tightening decision is inert and does not affect tightening) — an override may only move freely toward a more-safe state (tightening), while moving toward a less-safe state (relaxing) requires explicit override authorization;
  6. An empty-condition rule (catch-all / fallback) MUST NOT rewrite the decision established by an explicit-condition rule: a rule whose when is the literal true (matches unconditionally), whether its then is DENY or ALLOW and whether or not it carries override, MUST NOT override the decision established by any explicit-condition (non-literal-true when) rule. A fallback rule takes effect only when no explicit-condition rule matches (synonymous with the decision-table “default row” in §5.4). Rationale: a fallback rule carries the weak, general intent of “all other cases”, while an explicit-condition rule carries the strong, specific intent of “this particular case”; letting the fallback rewrite an explicit decision is an “override to a less-safe state” and violates the safety monotonicity of item 5.

7.1a Decision Merge (fold)

When multiple rules match, the final decision is determined by a “decision-strength partial order + fold” (not by first-match or an undefined order). The strength order of the 13 decision types (smaller number = stronger; the final decision takes the strongest):

Strength Decision type Category
0 EMERGENCY_HALT Terminal (short-circuit on hit)
1 DENY / ROLLBACK / QUARANTINE Restrictive (tightening)
2 REQUEST_HUMAN / WORKFLOW Human-in-the-loop
3 ESCALATE Human-in-the-loop
4 DELEGATE Human-in-the-loop
5 DEFER Human-in-the-loop
6 CORRECT Guidance
7 GUIDE Guidance
8 ALLOW Allow

NOTIFY does not participate in the main decision: NOTIFY is a “side action” (record, not block). When matched it is recorded in matched_rules and the notification list, and it does not change the final decision or participate in the fold.

Fold rules (set-based, permutation-invariant, MUST):

Decision merge is set-based (collect all hits first, then decide once), not a sequential fold — a sequential fold makes “can an override ALLOW cover a DENY” depend on evaluation order (whoever has smaller priority/ring is evaluated first), breaking permutation invariance. Let:

Fold pseudocode (MUST):

resolve(hits, indeterminate_rules, on_indeterminate):
  if any hit is EMERGENCY_HALT: return it              # EMERGENCY_HALT is terminal (strength 0, short-circuits on hit)
  R = hits where restrictive(decision)
  O = hits where decision == ALLOW and override in {critical, high}
  locked(r) = tier(r) <= 2                          # tier 0-2 interception lock (MUST NOT be covered)
  uncovered = R where locked(r) or not exists(o in O: level(o) > level(r) and ring(o) <= ring(r))
  if uncovered non-empty: return strongest(uncovered)      # restrictive all strength 1, take smallest ring
  nonRestrictive = hits where not restrictive(decision) and decision != NOTIFY
  if indeterminate_rules non-empty and context == Guard: nonRestrictive += on_indeterminate   # S3: unknown → synthetic hit (default REQUEST_HUMAN)
  if nonRestrictive non-empty: return strongest(nonRestrictive)  # by strength partial order
  return undefined                                          # falls through to fallback

restrictive(d) = d in {DENY, ROLLBACK, QUARANTINE}
level(rule) = override level (critical > high > normal > low; absent = normal)
strongest(S) = smallest strength order; tie → earliest definition order

The strength order guarantees “safety monotonicity”: the tightening (restrictive) direction is free, the relaxing (allow/guidance) direction requires override authorization, and the same direction converges by intervention strength — any two implementations thereby produce the same final decision without relying on first-match order.

7.2 Evaluation Constraints (E1–E12, all MUST)

# Constraint
E1 Evaluation is a pure function: no side effects, no implicit external state, no clock reads; the counting of within/rate, the authority state snapshot (state.*, §6a) and as_of are controlled external inputs — the evaluation phase reads only the pre-state, except that within/rate counting is a controlled side effect: the within/rate record is written during evaluation (when the counter is below threshold, i.e. the allowed operation), so evaluate() is not idempotent for these two operators — a repeated evaluation of the same input counts again; the state body is held by the engine, the expression tree reads only snapshots
E2 Fixed-point decimal scale=14 + half-even string serialization (evaluation scope: output precision, not canonical encoding); intermediate computation uses high-precision bounded rationals, rounding only at output nodes
E3 Evaluation errors are recorded as eval_warnings with errored=true; folding direction follows E12 by tier
E4 Resource limits (graded): Grade A arithmetic depth≤2 / tree depth≤6 / nodes≤64 / array≤10000 / no nested quantifiers / regex input length≤10000; Grade B tree depth≤10 / nodes≤256 / arithmetic depth≤4, quantifier nesting≤2; Grade C not applicable
E5 Type checking at load; when and expr MUST NOT coexist
E6 Tree as evidence: canonical_tree (a tree snapshot) serves as evaluation evidence and enters the hash; eval_trace is a recomputable derived product, not entering the hash
E7 Simple and Expression compile to the same evaluation core; a second evaluator is forbidden
E8 Quantifier safe folding: empty array → all/any/none all unknown (not false; not(unknown)=unknown does not fail open)
E9 No wall-clock reads; as_of is injected by the engine and recorded in the audit record
E10 String NFC normalization
E11 undefined sentinel semantics (null propagation, see §7.3)
E12 Evaluation error handling: Guard contexts (safety-boundary evaluation; the reference evaluate() is a Guard context) default to fail-close — all tiers fold to the blocking side (DENY); non-Guard contexts (simulation/analysis) fail-close tier≤2 and fold tier 3–5 to false

The kernel explicitly excludes: string concatenation, regex replacement, bitwise operations, date formatting, recursive references, and user-defined nodes — to keep evaluation closed and verifiable.

Operational suggestion (non-evaluation semantics, non-MUST): a wall-clock per-rule time cap (e.g. ≤50ms) is a deployment-layer DoS-guard operational suggestion — it does not enter the evaluation result, does not enter the hash, and is not part of the deterministic semantics; the reference implementation substitutes deterministic node/depth limits (E4’s graded structural limits) for wall-clock timing, consistent with E1 (pure function) and E9 (no wall-clock reads).

7.3 Deterministic Semantics (Cross-Implementation Divergence Protection)

The following semantics MUST be explicitly annotated in the document and vectors, to avoid semantic misunderstanding against standard implementations:

7.3(a) Null propagation and three-valued logic (E11)

Agent contexts are highly dynamic and missing fields are the norm. Evaluation uses Kleene three-valued logic (true / false / unknown), avoiding the fail-open of “missing/type-mismatch folds to false + not flips it”:

Scenario Behavior
equality/numeric comparison on a missing field (undefined/null) returns unknown (records a type_mismatch warning; not an NPE)
== null / != null check returns true / false normally (senses field presence)
type-mismatched comparison returns unknown (records a type_mismatch warning; no implicit conversion; errored=false)
arithmetic on a missing field propagates unknown (a missing/null operand makes the whole arithmetic unknown, errored=false); only division by zero, overflow, and illegal types (string/object into arithmetic) are EvaluationError (errored=true)
unknown operand of a logic node (and/or/not) Kleene: not(unknown)=unknown; and any false→false, all true→true, otherwise unknown; or any true→true, all false→false, otherwise unknown

Top-level unknown folding (MUST): a rule whose when evaluates to unknown does not match (a rule matches only when when === true) — unknown neither allows nor blocks, and does not trigger then; a Guard context configures a top-level fallback via metadata.on_indeterminate (default REQUEST_HUMAN; tier 0–2 MAY configure DENY) and records it in the audit.

not and unknown (fail-open guard, MUST): not(unknown) = unknown — a missing/type-mismatched operand remains unknown after not, never flipping to true (closing the “missing field → comparison false → not true → fail-open” hole).

Warning asymmetry (cross-implementation MUST reproduce exactly): type-mismatch/missing comparisons uniformly record a type_mismatch warning (errored=false); in (non-array right), string nodes (contains/match/starts_with/ends_with), length (non-str/array), aggregate (non-array/non-numeric elements), and quantifiers (all/any/none over a non-array) also record a type_mismatch warning. Third-party implementations MUST reproduce this warning set exactly.

Strict mode (evaluation option, default off): in lenient mode a comparison-node type mismatch silently yields unknown (as above). With strict mode on, a comparison node (eq/ne/gt/gte/lt/lte/between) type mismatch records a type_mismatch warning (errored still false) — making LLM parameter type errors (e.g. a number written as the string "100" gt 50) visible in the audit. Strict mode only changes the “comparison-node type mismatch” warning behavior, not the three-valued folding semantics; lenient mode MUST stay as-is (backward compatible).

7.3(b) Quantifier safe folding (E8)

under standard quantifier semantics all(empty)=true (vacuous truth). This specification deliberately deviates: all/any/none(empty) all fold to unknown (not false) — preventing both “nothing to check yet judged as allowed” (vacuous truth) and not(all(empty)) flipping to true (fail-open), and record the safe fold in the audit record. An over that is not an array (missing/scalar/object) is a type_mismatch warning: all/any/none fold to unknown with errored: false. Third-party implementations MUST adopt this folding semantics.

7.3(c) Fixed-point intermediate precision (E2)

intermediate computation uses high-precision bounded rationals (e.g. 128-bit integer numerator/denominator); only output nodes round to scale=14 + half-even string serialization (IEEE 754-2019 ROUND_HALF_EVEN). Conformance compares the scale-14 fixed-point value (numerically equal), not the string spelling: trailing zeros are insignificant ("35" ≡ "35.0"). This “string serialization” is the evaluation scope (output precision) and does not enter the canonical_tree hash; the canonical encoding scope is §8.2 (a typed decimal-string object, not JCS number).

7.3(d) Regex ReDoS protection

the match node MUST satisfy: ① input length cap (≤10000 chars); ② linear-time matching — prefer a deterministic engine (RE2-class, no backtracking), or be constrained by the safe syntax subset; ③ regex syntax subset (EBNF below). The safe syntax subset MUST be restricted to regular languages: backreferences (\1–\9, \k<name>) and lookaround ((?=) / (?!) lookahead, (?<=) / (?<!) lookbehind) are forbidden — such non-regular constructs depend on backtracking order, cannot be made byte-deterministic, and cannot be expressed by the SMT verifier (erdl-formal). Inline case flags ((?i)) are not provided (matching is always case-sensitive, §5.2). A regex that violates these limits (nested quantifiers, backreferences, lookaround, or an input-length violation) folds to unknown with a regex_re_dos warning and errored: false — it is not an E3 EvaluationError; folding to unknown (not false) keeps not(match(...)) from flipping (avoiding fail-open, §7.3(a)). MUST NOT use “regex steps” as the cap metric — a RE2-class engine has no backtracking step count and cannot reproduce it cross-implementation; substitute an input-length cap + linear-time engine (deterministic, portable).

Safe syntax subset EBNF (MUST):

regex         := alternation
alternation   := concatenation ( "|" concatenation )*
concatenation := repetition+
repetition    := atom quantifier?
quantifier    := "?" | "*" | "+" | "{" n ( "," m )? "}"
atom          := literal | "." | char_class | group | anchor
group         := "(" ( "?:" | "?<name>" )? alternation ")"
char_class    := "[" "^"? char_item+ "]"
anchor        := "^" | "$"
literal       := non-meta character | backslash-escaped character

Forbidden (non-regular constructs, MUST NOT): backreferences (\1–\9, \k<name>), lookaround ((?=) / (?!) lookahead, (?<=) / (?<!) lookbehind), inline flags ((?i) etc.).

7.3(e) aggregate empty-array safe folding

Function Empty-array result Basis
count(empty) 0 standard counting semantics
sum(empty) 0 empty-sum identity
avg(empty) unknown safe fold (avoid division by zero; unknown keeps not(avg(empty)) from flipping)
min(empty) unknown safe fold (standard +Infinity, disabled; unknown does not fail open)
max(empty) unknown safe fold (standard −Infinity, disabled; unknown does not fail open)

The over of aggregate MUST be an array; a non-array (missing/scalar/object) returns null + type_mismatch warning (folded to false). count(missing) and count(empty array) differ: the former is type_mismatch, the latter is 0.

7.3(f) Time-node UTC semantics (E9)

all time nodes evaluate uniformly in UTC, guaranteeing byte-for-byte consistency across implementations and time zones:

Business local time zone is converted by the engine to a UTC instant when injecting as_of; the evaluator computes as a UTC pure function.

7.3(g) Resource-limit violations (E4) and load-time exclusivity (E5) are constraint-verification results, not evaluation results

an E4 structural resource-limit violation (nodes / tree-depth / arithmetic-depth / array / quantifier-nesting over the grade limit) throws — the engine returns value: null with value_type: "null" and threw: true (not an E3 EvaluationError; errored stays false). A regex ReDoS violation (§7.3(d)) folds to false + regex_re_dos (not a throw). An E5 load-time exclusivity violation records value: true (= violation detected). The E12 fold and errored rules above apply to evaluation vectors only.

7.4 when Minimum-Completeness Constraints

when: "true" means “applies to all operations” and is allowed only for advisory rules:

Rule Level
when: "true" MUST NOT pair with then: DENY MUST NOT
when: "true" MUST NOT pair with then: EMERGENCY_HALT MUST NOT
when: "true" MUST NOT pair with then: CORRECT MUST NOT
when: "true" MUST NOT pair with then: REQUEST_HUMAN MUST NOT
when: "true" MAY pair with then: ALLOW + instruction MAY
when: "true" MAY pair with then: NOTIFY MAY
Safety rules (category=security) MUST contain at least 1 condition MUST
Tool-interception rules SHOULD contain a tool.name condition SHOULD
File-operation rules SHOULD contain tool.args.path SHOULD
Command-operation rules SHOULD contain tool.args.command SHOULD

8. Serialization and Canonicalization

8.1 Serialization

An ERDL document is carried in YAML and can be losslessly converted to JSON. The canonical tree (canonical_tree) is serialized as a JSON object.

8.2 Canonical Tree (Canonical Form)

The expression tree is the single benchmark object for evaluation, hashing, and recomputation. For its hash to be byte-for-byte identical across implementations, the tree MUST have a unique canonical form:

Canonicalization rule Description
Fixed node order child nodes are arranged in canonical order (strict left→right), independent of source writing order
Field names load-bearing field reference paths are load-bearing — frozen once published ([FREEZE-1]); aliases MUST be normalized first
Literal canonicalization the canonical encoding scope of number literals is a typed decimal-string object {"n":"0.15"} (a number 0.15 encodes as {"n":"0.15"}, while the string "0.15" stays the bare value "0.15" — no collision), not JCS IEEE 754 number serialization — avoiding the loss/collision of integers beyond 2^53 and decimals beyond ~17 significant digits under IEEE754; strings NFC-normalized
var canonicalization only $ / $.path, with path segments as definite byte sequences
Metadata stripping comments, source line numbers, formatting, authors, and other non-semantic metadata never enter the canonical tree

The object of tree hashing is the canonical tree, not any particular implementation’s memory representation or serialized text. Two structurally equivalent trees (differing only in field writing order, whitespace, or variable naming) produce exactly the same byte sequence and hash after canonicalization.

Versioned domain-separation prefix for tree hashing (MUST): tree hash = sha256("erdl-tree-v3:" + canonical bytes) — erdl-tree-v3: is a versioned domain-separation prefix (an S2 breaking change), preventing old/new encoding-scope tree hashes from ever being conflated (a tree with no number literal would hash identically across the change but the prefix differs).

8.2.1 Canonical-tree encoding (node JSON shape)

The canonical tree is serialized as an S-expression (JSON shape); each node is a single-key object (MUST have exactly one key). Node shapes (MUST, pinned node-by-node):

Node S-expression shape Note
literal bare value (string / boolean / null); a number literal encodes as the typed object { n: "<decimal string>" } numbers typed ({n} key is a reserved key, not a node name — eliminates number/string collision), strings NFC (E10); an array literal (e.g. the right operand of in) is a bare array
field { field: "path" } field reference (snake_case, FREEZE-1)
var { var: "path" } path is only $ or $.path (root or dot path)
and / or { and: [children…] } / { or: [children…] } logic (n-ary, see child ordering below)
not { not: child } single child
eq / ne / gt / gte / lt / lte { eq: [left, right] } comparison (binary)
in { in: [left, right] } right operand is an array
contains / match / starts_with / ends_with { contains: [left, right] } string (binary)
exists { exists: child } presence
length { length: child } length
between { between: [value, min, max] } ternary
all / any / none { all: { binding, over, predicate } } quantifier
add / sub / mul / div / round { add: [children…] } arithmetic (n-ary)
days_between { days_between: [from, to] } time difference
epoch_ms { epoch_ms: child } timestamp
date_add { date_add: { unit, base, amount } } date arithmetic
date_part { date_part: { unit, arg } } component extraction
month_last_day { month_last_day: child } end of month
count / sum / avg / min / max { count: child } aggregation
fn { fn: { name, args } } function delegation
casefold / trim / path_normalize { casefold: child } string normalization (unary, extension profile)
in_set { in_set: { ref, digest, value } } external-list membership (extension profile)

Child ordering (MUST): the child arrays of every node keep definition order (left→right) and are not sorted — sorting would change semantics (e.g. and/or evaluation order relates to the §7.3(a) warning asymmetry), so it is forbidden. The keys inside a node object (e.g. date_add’s unit/base/amount, the quantifier’s binding/over/predicate, fn’s name/args) are sorted by JCS key order (§8.2 encoding scope).

Precedence clause (MUST): when the prose and a vector conflict, the prose prevails (vectors are consistency tests of the reference implementation, not the normative authority); if a vector exposes semantics the prose does not cover, the conclusion MUST be written back into the prose (§10.3.3 “implementable from the spec alone”).

8.2a DO Hash Preimage (field order + fixed key set + absence encoding)

The DO hash preimage of an evaluation result — its field order, key set, and absence encoding — MUST be defined as follows, otherwise two implementations will necessarily compute different hashes (v2.2 added a structured field to the DO, state_snapshot, whose local key ordering is defined but whose overall preimage is not — a global anchoring gap):

8.2a.1 Evaluation-result DO field order and fixed key set

Fixed field order (MUST):

context → decision → matched_rules → unless_exemptions → primary_instruction → primary_reason
→ primary_explanation → primary_correction → total_evaluated → total_matched
→ temporal_state → state_snapshot → canonical_trees → rule_set_hash → eval_profile → eval_warnings → indeterminate_rules → errored → as_of

Versioned domain-separation prefix of the DO hash (MUST): the language-layer evaluation-result DO hash = sha256("erdl-eval-do-v3:" + JCS(preimage) bytes) — erdl-eval-do-v3: is a versioned domain-separation prefix, the same mechanism as the tree-hash prefix erdl-tree-v3: (§8.2): hashes before and after a DO-preimage schema change (e.g. the S1/S4 closure) cannot be conflated, nor can the DO hash and the tree hash.

context enters the DO (MUST): context is the evaluation input’s context object (§7.0.1, called context in RFC-002), entering the DO hash preimage — so that “this decision, made against this input” is independently recomputable, rather than only the output side (“decision → matched rules → tree”). context sits first in the field order, semantically forming the “input → decision” closed loop; a DO missing the input context cannot answer “what input was this decision made against”.

Adding the context field is a breaking change (MUST be explicitly labeled): adding context changes the DO hash preimage’s field order and key set (invalidating all old-schema DO hashes), a v2.3 breaking change — verifiers MUST select the recomputation schema via eval_profile.spec_version (§8.2a.1b). The specific position of context in the field order (first/middle/last) does not affect semantics, as long as the complete input context enters the preimage; this specification does not treat “position” as a semantic constraint. Note the layering: this section’s “evaluation-result DO” is the language-layer hash preimage of the evaluation result (context first); the RFC-002 governance-layer decision-object (v1.5) is a different hash preimage where the context object’s field name is context (one of the CORE 14, not first) — the two are two names for the same context object at different layers, but belong to two distinct DO hash preimages and MUST NOT be conflated: a language-layer breaking change does not alter the RFC-002 decision-object field order, and vice versa.

Verification approach (language-layer DO): the language-layer evaluation-result DO’s cross-implementation determinism is assured by (a) the formal pinned field order / key set / absence encoding of §8.2a.1 (normative formal definition, not a prose approximation) and (b) engine self-verification (evaluation-object.spec.ts). It does not require a dedicated cross-implementation vector family — the RFC-002 governance-layer decision-object (v1.5) is the cross-implementation vector layer; the language layer is pinned formally and self-verified.

Merkle commitment of context (optional profile, MUST support): context may contain personal information and secrets, and entering it fully into the DO conflicts verifiability with compliant deletion. Optionally commit context via a Merkle commitment — each field value is a salted-hash leaf, the DO stores only the Merkle root (context_hash), and recomputation discloses fields on demand with proofs. Whether or not committed, desensitization MUST precede evaluation (the value used for evaluation equals the recorded value; MUST NOT record the raw text first and then evaluate).

Fixed key set (MUST): a valueless key is encoded as null, keys MUST NOT be omitted (keeping the preimage structure constant); arrays in occurrence order; strings NFC (E10); numbers typed {"n":"<decimal>"} (§8.2 encoding scope). Empty-state encoding (MUST): list-type fields (matched_rules, unless_exemptions, eval_warnings, canonical_trees) encode their empty state as [] (key not omitted); only nullable object-type fields (primary_instruction/primary_reason/primary_explanation/primary_correction, temporal_state, state_snapshot) encode as null when valueless — arrays are always arrays, objects may be null, a unique boundary.

DO sub-structure key sets and key order (MUST, pinned per field): sub-structure entries also have fixed key sets and key orders (S1) — absence encodes null, keys not omitted; list-type empty state []:

Sub-structure Entry key order (fixed) Notes
matched_rules[] / unless_exemptions[] rule_id → rule_name → decision → priority → ring → instruction → reason → correction → explanation → alternative → corrected_args canonical entries of matched/exempted rules; optional fields encode null when absent; explanation/alternative encode as {zh, en} (NFC) when bilingual; corrected_args is the parameter-correction object of a CORRECT decision (snake_case keys), null when absent
canonical_trees[] rule_id → tree → hash E6 tree evidence: tree is the canonical-tree S-expression (§8.2.1); hash is the tree hash (sha256: prefix, domain-separation prefix in §8.2)
eval_warnings[] code → node_type code is a closed enumeration (E3): type_mismatch / division_by_zero / quantifier_empty / aggregate_empty / regex_re_dos / array_over_limit / invalid_date / not_ruleable; the free-text message does not enter the preimage (diagnostic wording is not cross-implementation constant and would break byte-identity); node_type encodes null when absent
temporal_state[] rule_id → operator → field → window_ms → count → limit within/rate window-count snapshot (the output side of the S6 counting-key elements); operator ∈ {within, rate}; limit is carried only by rate entries (the N of "N/1m"), within entries have no limit key
indeterminate_rules[] (string array) the list of rule names whose when evaluated to unknown (S3), empty state []

8.2a.1a Rule-set hash (rule_set_hash)

rule_set_hash is the hash of the full rule-set semantics, closing the gap that canonical_trees covers only matched rules’ when trees — unmatched rules, then/priority/ring/override/enabled/unless all enter rule_set_hash, so a third party can verify “no other rule should have matched”.

Rule canonical object (MUST): each rule is canonicalized to:

{ name, when_tree, unless_tree, then, priority, override, ring, tier, enabled, instruction, reason, correction, explanation, alternative }

rule_set_hash computation (MUST):

rule_set_hash = sha256(JCS({ fallback_decision, on_indeterminate, rules: [rule canonical objects…] }))

8.2a.1b Evaluation options (eval_profile)

eval_profile records the evaluation options — strict mode, context, field contracts, spec version. These options change the decision or eval_warnings, yet were absent from the DO, so a third party could not recompute. eval_profile enters the DO preimage:

{ strict, context, contract_hash, spec_version }

8.2a.2 Transition-chain audit-record preimage (three kinds)

Transition-chain audit-record preimage field order (MUST, three kinds): the transition chain is composed of three kinds of audit records — successful transition (type: "transition"), transition error (type: "transition_error"), and genesis (type: "genesis"). The three have different field sets; their respective field order and fixed key set MUST be as follows (the type field enters the preimage to distinguish kinds):

Successful transition record (transition):

type → event_id → on → actor → at → audit_as → fired → reason → set → state_version → previous_hash

Transition error record (transition_error):

type → event_id → on → actor → at → audit_as → fired → reason → error → errored → previous_hash

Genesis record (genesis):

type → instance_id → protocol → doc_tree_hash → initial → at → previous_hash

Fixed key set and absence encoding (MUST): each kind’s key set is the field order listed above (missing fields are not padded across kinds); the keys of set/initial are ordered by state-variable-name RFC 8785 JCS key order (UTF-16 code-unit ascending); strings NFC (E10); numbers JCS (§8.2 encoding scope); previous_hash absent (genesis only) is encoded as null and the key is not omitted. fired = the transition rule names fired by this transaction (name, defaulting to on), multiple joined by , into a string (M7); reason = the transition’s semantic identifier (the reason field of §6a.2), taking the transaction’s first non-empty value, encoding null when absent (M7).

8.3 Relationship between the Canonical Tree and gloss

This “hash the tree, verify gloss equals the tree” mechanism lets gloss obtain cryptographic anchoring of the tree without needing byte-for-byte identity or entering the hash.


9. How to Integrate ERDL

The integration goal of ERDL is to extract critical decisions from model inference, framework code, or informal conventions, and turn them into loadable, executable, and verifiable rule assets. ERDL is typically delivered as YAML/JSON rule documents, makes decisions through when → then, and outputs hashable evaluation evidence that can be recomputed byte for byte. The following three integration paths correspond to three typical engineering integration points.

9.1 Scenario 1: AI Agent (Behavioral Constraint Layer / Action Guard)

Role: In an AI Agent pipeline, ERDL is the deterministic gate between LLM intent and system execution — the Agent may generate actions, but whether an action is permitted must be determined by rule evaluation.

Simulation: A customer-service Agent receives a user request for a refund of 8,000 yuan. The LLM converts the intent into the tool call issue_refund(amount=8000, order_id=O1024). Before the call actually reaches the payment system, Action Guard packages the tool name, arguments, and session context into a context object and submits it to ERDL for evaluation. In the rule set, R1 is written as when tool.name == "issue_refund" and tool.args.amount > 5000 → REQUEST_HUMAN, while R2 is written as when tool.name == "issue_refund" → ALLOW. Because R1 matches first, the system returns REQUEST_HUMAN. The Agent stops calling the payment tool, generates a human approval task instead, and returns a “manual review required” message to the user.

Integration points: First, rules are evaluated independently of the model, so prompts no longer carry the safety boundary. Second, match records, input digests, canonical_tree, and result hashes are written together to audit logs, making every interception replayable. Third, rule changes require only updating the ERDL document, without rewriting the Agent framework, tool implementations, or model prompts.

9.2 Scenario 2: MCP (Protocol Distribution / Cross-Implementation Interoperability)

Role: In the MCP ecosystem, ERDL rules are exposed as standard tools through an ERDL MCP Server. Any MCP-compatible Agent can invoke the same rule set, enabling compliance distribution and mutual recognition across implementations.

Simulation: An enterprise deploys its fund-compliance rules as an ERDL MCP Server and exposes the guard_check tool. A third-party Agent prepares to execute a high-value refund but does not possess the enterprise rules. Before execution, it calls guard_check through MCP, passing an action description (action=issue_refund, amount=8000, channel=payment). The ERDL MCP Server loads the rule set, performs evaluation, and returns decision=REQUEST_HUMAN, matched_rule=R1, hash=0x9f.... Based on the returned value, the third-party Agent stops direct execution, enters a human approval workflow, and records the decision evidence in its own execution log.

Integration points: The key here is “Rules-as-a-Service.” Rules are no longer hard-coded in a specific Agent framework; they are distributed through a standard protocol. Clients from different vendors, programming languages, and runtime environments can consume the same decision logic. Because the response includes rule-match and hash information, callers can archive decision evidence for later auditing or independent recomputation even without storing the original rule text. When rules are upgraded, only the MCP Server side needs to be updated; clients do not need to refactor their execution chains.

9.3 Scenario 3: Rule Engine (Rule Definition Language / Deterministic Evaluation)

Role: Inside a rule engine, ERDL is the rule definition language itself, responsible for expressing business policies declaratively and providing deterministic evaluation semantics.

Simulation: An anti-money-laundering engine needs to express “large transaction involving a high-risk country → block.” The business team writes the ERDL rule when context.amount > 10000 and context.country in [high-risk country list] → DENY. Here the high-risk country list (>256 items; FATF/OFAC-style sanction lists often reach thousands) is referenced externally via the in_set{ref,digest} node — the list’s versioned hash enters the rule while the list body is injected by the engine context, not subject to in’s 256 inline cap (§5.2.2). After loading the YAML, the engine does not treat it as ordinary configuration; it compiles it into an expression tree: comparison nodes handle amount thresholds, set nodes determine country membership, and logical nodes perform the AND operation. The engine then executes according to the E1–E12 evaluation semantics and outputs DENY. The rule can also generate canonical_tree and a hash value; another implementation can recompute the same result simply by loading the same rule and the same input.

Integration points: The core value here is “verifiable determinism.” ERDL not only makes rules readable, but also allows engine implementations to be constrained by test vectors. V-ENGINE-class test vectors can prove that different engines, platforms, and versions produce byte-for-byte identical results for the same rule. As a result, a rule engine is no longer merely an internal black box inside a business system; it becomes an executor that can be independently verified by third parties. Regulators, auditors, or platform operators can determine whether the engine computed correctly based on inputs, rule hashes, and evaluation evidence.


10. Examples and Conformance Verification

10.1 Quick Start

A minimal ERDL document plus one evaluation, walking the full “write → load → evaluate → get result” chain (pipeline in §2.4).

Step 1 · Write the rule (refund.erdl.yaml):

protocol: "erdl/v2"
version: "2.2.0"
metadata:
  name: "refund-guard"
  description: "Refund amount control"
  category: coding
  decision: ALLOW
rules:
  - name: "SEC-001-refund-limit"
    description: "Refunds over 5000 require human approval"
    priority: 10
    when:
      logic: AND
      conditions:
        - field: "tool.name"
          operator: eq
          value: "issue_refund"
        - field: "tool.args.amount"
          operator: gt
          value: 5000
    gloss: "tool.name equals issue_refund and tool.args.amount is greater than 5000"  # engine-generated (G2)
    then: REQUEST_HUMAN
    message: "Refund amount over 5000, human approval required"

Step 2 · Load + validate + compile: parse the YAML, validate it, then compile when into an expression tree (§2.4 steps ①②③).

Step 3 · Evaluate: given the context object:

context:
  tool:
    name: "issue_refund"
    args: { amount: 8000 }

Rule SEC-001 matches (tool.name == "issue_refund" and amount > 5000).

Step 4 · Result:

decision: REQUEST_HUMAN
matched_rules: ["SEC-001-refund-limit"]
primary_reason: "Refund amount over 5000, human approval required"
total_evaluated: 1
total_matched: 1

If the input is changed to amount: 100, the rule does not match, and the metadata.decision fallback applies → decision: ALLOW.

10.2 Complete Examples

See §4.2 (Simple rule), §5.3 (Expression rule), and §5.4 (Decision Table).

10.2.1 State-block complete example (§6a)

State-block complete example (§6a):

protocol: "erdl/v2"
version: "2.2.0"
metadata:
  name: "delegated-refund-authority"
  description: "Revocation and freshness of delegated refund authority"
  category: security
  decision: DENY
state:
  - name: authorization
    values: [authorized, revoked]
    initial: revoked
    display_name: { zh: 授权状态, en: authorization state }
transitions:
  - on: bootstrap
    audit_as: DELEGATE
    reason: authorize
    when: { eq: [{ field: "state.authorization" }, "revoked"] }
    gloss: "authorization state equals revoked"  # engine-generated (G2), lint-enforced
    set: { authorization: authorized }
  - on: revoke
    audit_as: DELEGATE
    reason: revoke
    gloss: "true"                                # unconditional transition: literal true
    set: { authorization: revoked }
rules:
  - name: "SEC-001-revoked-denies-exercise"
    description: "Block protected operations when authorization is revoked"
    priority: 10
    ring: 0
    when:
      logic: AND
      conditions:
        - field: "state.authorization"
          operator: eq
          value: revoked
    then: DENY
    message: "authorization revoked"

Note: the state block is already updated by events (transitions) before rules evaluation; state.* is controlled injection (§6a.3), not a context field.

10.2.2 Evaluation output example (with state_snapshot)

Evaluation output example (with state_snapshot, a V-STATE prototype): after the above example undergoes two events — bootstrap (authorize) and revoke (revoke) — the rule evaluation outputs:

# Event sequence (processed before rule evaluation, eager):
#   bootstrap → set authorization: authorized   (state_version 0 → 1)
#   revoke    → set authorization: revoked      (state_version 1 → 2)

decision: DENY                                  # matches SEC-001, authorization=revoked
matched_rules: ["SEC-001-revoked-denies-exercise"]
primary_reason: "authorization revoked"
total_evaluated: 1
total_matched: 1
temporal_state: null                            # no within/rate match
state_snapshot:                                 # §6a.5, enters the DO hash preimage
  values: { authorization: revoked }            # state read at evaluation (on-demand, not full)
  state_version: 2                              # 2 committed transactions (bootstrap + revoke)
  transitions_head: "sha256:…"                  # hash of the revoke record
canonical_trees: [ { rule_id: "SEC-001-…", tree: …, hash: "sha256:…" } ]
eval_warnings: []                               # list-type field empty state is []
errored: false
as_of: "2026-09-12T10:00:00Z"

10.3 Conformance Verification

10.3.1 Vector coverage

The semantics of this specification MUST be proven by independently recomputable test vectors. The expression-layer vectors (V-ENGINE / V-GLOSS / V-PROJ) cover: 34 nodes × 4 scenarios (normal/boundary/exception/empty), E1–E12 semantics, the Simple 30-operator compile mapping, and gloss rendering templates; the state-layer vectors (V-STATE) cover all MUST semantics of §6a: event-object validation (event_id/on/actor/at/payload restricted load), same-variable conflict check (0)–(4) positive/negative cases and same-event audit_as consistency, single-event multi-rule atomicity (stop at the first EvaluationError, commit all at once on full pass), guard-error fail-closed with transition_error chain position (no set applied / no version increment / no head movement), state_version/transitions_head replay verification, duplicate event_id idempotent drop, unmatched-event silence, load failure for rules referencing event.* / undeclared state.*, catch-all vs explicit-rule two-pass interaction, and enforcement-boundary check/act re-validation (authorized@N → ALLOW@N → revoke@N+1 before effect commit, fail-closed, §6a.8), latest-authoritative-head freshness (authorized@N/HN → revoke@N+1/HN+1 → restore historical prefix → replay succeeds → reject effect, anti-rollback, §6a.9). The resolution-layer vectors (V-RESOLVE, R01–R13) cover the §7.1 ring/override/catch-all resolution semantics (including the tightening-direction boundary R08/R13).

10.3.2 Five-step verification

Five-step verification: load vector input → generate expression tree → recompute evaluation result → compare with the answer → judge consistency.

10.3.3 Third-party Runner verification flow (from zero to conformance)

Third-party Runner verification flow (from zero to conformance):

  1. Read this specification;
  2. Implement an independent verifier in your own chosen tech stack (without importing any existing implementation code);
  3. Load the test vectors and compare byte-for-byte;
  4. Confirm your implementation satisfies the Runner contract;
  5. Submit results to the implementation registry for third-party audit re-verification.

11. Conformance

An implementation MUST prove conformance via independently recomputable test vectors (§10); the sole criterion for “conforms to the specification” is that, for the same (rule set, context, evaluation options, state) input, it produces a DO hash byte-for-byte identical to the vector answer. (Here “DO” denotes the RFC-002 governance-layer decision object — the cross-implementation vector layer; the language-layer evaluation-result DO of §8.2a.1 is pinned formally and self-verified by the engine, and does not carry a separate vector family.) Conformance has two levels:

12. Security Considerations

13. Privacy Considerations

14. Extension Registration

This specification’s extension points (new node types, new decision types, new writing forms, new profiles) MUST follow the registration mechanism:

15. Trademark & Patent Policy


Appendix A · 34-Node Reference Table

Group Nodes Count
Value field · var · literal 3
Logic and · or · not 3
Comparison eq · ne · gt · gte · lt · lte 6
Set in 1
String contains · match · starts_with · ends_with 4
Existence/measure exists · length · between 3
Quantifier all · any · none 3
Arithmetic add · sub · mul · div · round 5
Time days_between · epoch_ms · date_add · date_part · month_last_day 5
Aggregate aggregate (count/sum/avg/min/max) 1

Total 34 nodes.

Appendix B · Simple 30-Operator Reference Table

Family Operators Count
Comparison eq · ne · gt · gte · lt · lte 6
List in · not_in 2
String contains · not_contains · match · starts_with · ends_with 5
Boundary negation not_starts_with · not_ends_with 2
Existence exists · not_exists 2
Length length_gt · length_gte · length_lt · length_lte · length_eq 5
Range between · not_between 2
Count count_gt · count_gte · count_lt · count_lte 4
Modifier within · rate 2

Total 30 operators (28 condition operators + 2 condition modifiers).

Appendix C · Decision Type Enumeration (13 types)

See §6.

Appendix D · Function Delegation and Rule Grading

For scenarios explicitly excluded by the kernel but genuinely needed, function delegation (FnRegistry) is provided as a controlled fallback:

Constraint Description
Registration a function MUST be registered before it can be referenced; unregistered calls are rejected
Sandboxed execution restricted environment under resource quota and timeout
Determinism exemption declaration functions on the Guard evaluation path MUST declare and guarantee determinism
Auditability every call is recorded in the audit record for offline verification

fn node reading state (§6a): an fn node is not part of the 34-node frozen kernel (Appendix D controlled fallback, Grade C) — the fn’s arguments may reference state.*/event.* (controlled state is injected via a field node and passed in as an argument); but the fn itself MUST remain deterministic (per the determinism-exemption declaration above), and MUST NOT write state (state is updatable only by transitions[].set, §6a.3).

Rule grading (Grade):

Grade Expression form Audit SLA
A pure Simple (30 operators) highest, plain-text recomputable
B Expression tree high, eval_trace MUST
C with function delegation layered, Grade C MUST NOT pose as plain-text recomputable

Rules with function delegation (Grade C) MUST explicitly mark “contains non-recomputable function delegation” in gloss; the delegated function’s call input + output hash MUST enter the result hash’s preimage.

Function-delegation calling convention and registry (MUST, resolving the “no calling convention / no registry format” gap):

Item Convention
Function signature name(params) -> returnType — name is a snake_case identifier, params comma-separated parameter names, returnType the return type; the signature is the function’s unique registry key
Registry entry { signature, sandbox, timeout, deterministic, ... } — sandbox ∈ pure/network/filesystem, deterministic declares Guard-path evaluability, timeout the timeout cap
Hash preimage a delegated call’s hash preimage MUST include: fn_id (= signature), version (function version), args_hash (JCS hash of the arguments), result_hash (result hash) — all four, so a Grade C call can be verified offline for “which version, what input, what output”

Grading and tier constraint (MUST): Grade C (function delegation) MUST NOT be used at tier 0–2 (safety baseline) — safety-sensitive rules at tier 0–2 MUST use the pure Simple kernel (Grade A) and MUST NOT bypass the closed evaluation kernel via fn delegation; Grade C is limited to tier ≥3 business-panorama rules.

Appendix E · Glossary

Term One-line definition
Entity a rule subject type (agent/tool/task/workflow/human/guardian), the namespace for field references (§3)
Rule a when → then decision unit
DO (Decision Object) the cryptographic audit record of a single evaluation (hashable, independently verifiable); distinct from the Evaluation Result (the runtime output of §7.0.3) — the DO is the canonical serialized form of the evaluation result (§8.2a field order + JCS RFC 8785 + SHA-256), while the Evaluation Result is the engine’s runtime return
genesis the chain-start audit record generated for initial at state-block load (initial snapshot + document canonical-tree hash, §6a.5)
transition_error a first-class on-chain record produced by a transition guard EvaluationError (does not apply set, does not increment version, does not move head, §6a.5)
event (event object) the transition trigger input { event_id, on, at, actor, payload } (§6a.7)
actor the authenticated event-source identifier (provided by the identity layer, enters the transition audit record, §6a.7)
when a rule’s trigger condition (compiled to an expression tree)
then the decision type after a rule matches (§6)
tier rule level 0–5, low to high for constraint strength; tier 0–2 uses Simple, ≥3 may use Expression
ring execution ring 0–3 (kernel/recovery/approval/advice); evaluation runs in ring order
Guard context a safety-boundary evaluation context (the reference evaluate()); evaluation errors fail-close (E12), covering all tiers
non-Guard context a simulation/analysis evaluation outside the safety boundary; tier 3-5 evaluation errors fold to false (E12)
override override level critical > high > normal > low; only the DENY → ALLOW direction is allowed
expression tree the evaluation semantic kernel (34 nodes, 10 groups); all three writing forms compile to it
canonical_tree the canonical tree, the sole basis for hashing and recomputation (§8.2)
gloss the natural-language readable projection deterministically generated from the tree (§5.5)
eval_trace the node-level evaluation trace (recomputable derived product, does not enter the hash, E6)
eval_warnings non-fatal warnings during evaluation (E3)
errored whether evaluation errored (E3): EvaluationError (division by zero / invalid date / arity / type-mismatched arithmetic [string or object into arithmetic]) → true (even though E12 folds to false); type-mismatched comparison, null propagation, and missing-field arithmetic → false (the latter folds to unknown, not an error)
temporal_state the temporal state: within/rate sliding-window state (stateful operators, counter semantics) — distinct from “authority state” (state block)
state block the authority state: the named state machine declared by §6a (the state top-level block + transitions rules) — distinct from the “temporal state” temporal_state; the two do not share a namespace
state variable a named state declared by the state block (§6a), state.<name> namespace, updatable only by transition rules
state space the full set of state variables and their enum values declared by a document (finite, capped)
state transition an event-triggered deterministic state change state.<name> ← value (the FSM’s F function, §6a.2)
controlled injection engine-held inputs (as_of/temporal_state/state.*) not writable externally, updatable only by engine mechanisms (§6a.3, E1)
state_snapshot the state snapshot at evaluation, entering the DO hash preimage (§6a.5)
enforcement boundary the component (Action Guard / tool-call guard) that consumes the §6a decision and commits the gated side effect (§6a.8)
check/act atomicity the §6a.8 obligation that no authorization-lineage state change commits between the authorization decision and the gated side effect’s commit
latest authoritative head the current latest authoritative state anchor {state_version, transitions_head} for a document instance; its freshness across restart/recovery/replica boundaries requires an external anchor (§6a.9)
durable freshness anchor the persistent anchor provided by the organization/deployment layer that establishes latest-authoritative-head freshness across restart/recovery/replica boundaries (monotonic epoch / durable anchor / signed checkpoint / consensus backing); the enforcement boundary uses it to determine whether restored state is sufficiently fresh (§6a.9)
authorization basis where an authority comes from — a root grant or an independently verified re-authorization decision object that establishes/re-establishes authority for a subject; distinct from the authorization root (the principal entitled to establish it) and the authority chain (the lineage).
authorization root the principal/authority entitled to establish/re-establish an authority; the actor of a transition that makes authority exercisable MUST be attributable to it (§6a.10)
delegation chain the composition of multiple authorization relationships along “authorization root → intermediate node → authorized subject” (§6b)
effective authority the authority a subject can actually exercise; MUST ⊆ the originating authority chain (§6b)
authority chain the complete authorization lineage from the authorization root to the authorized subject; effective authority MUST be a subset of it (§6b)
delegated-authority invariants the five delegation-chain security invariants INV-01~05 (non-amplification / provenance continuity / narrow-only inheritance / transitive revocation / capability boundary, §6b)
transition validity the engine validates transitions: only declared ones execute, values belong to the enum, undeclared transitions do not execute (fail-closed)
as_of the evaluation moment injected by the engine (UTC, E9)
context object the evaluation input carrying the current state of entities (§7.0.1)
evaluation context (eval_profile.context) one of the evaluation options (guard/analysis, the E12 folding direction, §8.2a.1b) — a different concept from the context object context (§7.0.1); the key name alone is shared
fallback decision the metadata.decision fallback verdict when no rule matches (§2.2)
NFC Unicode Normalization Form C (string normalization, E10)
ReDoS regular-expression denial of service; the match node MUST guard via input-length cap + linear-time engine (§7.3(d))
half-even banker’s rounding (ROUND_HALF_EVEN), the E2 fixed-point output rounding
null propagation the safe-failure semantics of returning false uniformly for missing fields (E11)
evaluation scope the E2 fixed-point output precision (scale=14 + half-even string serialization); does not enter the canonical_tree hash
encoding scope the §8.2 canonical serialization of number literals (a typed object {"n":"<decimal string>"}, not JCS number); enters the hash

This specification’s relationship to existing rule / authorization standards (design trade-off comparison, non-normative):

Standard Domain Relationship to ERDL
CEL (Common Expression Language, Google) general expression evaluation both are deterministic expression languages; CEL targets runtime evaluation, ERDL targets rule declaration + cryptographic audit (canonical_tree + DO hash, byte-for-byte cross-implementation verification), with evaluation as only one stage
DMN (Decision Model and Notation, OMG) business decision modeling both are declarative decisions; DMN targets business modeling and execution engines, ERDL emphasizes a deterministic kernel + independently recomputable hash evidence chain (§8), with an explicitly closed expression-tree kernel (30 operators + controlled extension profiles)
XACML (eXtensible Access Control Markup Language, OASIS) access-control policy both are declarative authorization policies; XACML targets a policy decision point (PDP) architecture, ERDL targets byte-for-byte-consistent evaluation semantics and hashable audit (§8.2a), without binding to a specific deployment topology
UCAN (User Controlled Authorization Networks) decentralized capability authorization both express authorization chains; UCAN carries capabilities via delegation chains, and ERDL’s §6b delegated-authority security model (INV-01~05) complements it — UCAN expresses “who grants what”, ERDL provides “verifiable adjudication of authorization state” (§6a FSM + audit chain)
OAuth 2.0 Token Exchange (RFC 8693) token delegation both involve delegation / impersonation; RFC 8693 defines the token-exchange protocol, ERDL defines no transport protocol, only deterministic semantics of authorization decisions and state — usable as an adjudication layer above RFC 8693 for “is this exchange allowed”

Design trade-offs (key points): ERDL’s key trade-off is separating the deterministic kernel from the writing forms — multiple writing forms (Simple / Expression / decision table) compile to a single expression tree (E7), thereby gaining byte-for-byte-consistent evaluation results and hashes across implementations (§8), a property that CEL/DMN/XACML do not make a primary goal; the cost is expressiveness constrained by the closed kernel (30 operators + controlled extension profiles), with out-of-kernel needs routed to function delegation (Appendix D, Grade C).

Revision History

Version Date Changes
v2.3 2026-10-06 §7.0.1b adds the fact-source typology (informative, five kinds: system_observed/llm_asserted/user_input/config/derived) + the fact-neutrality design note (provenance MUST NOT enter evaluation semantics or hash preimages); §7.0.1a field contract adds the informational provenance field; §8.2a.1b and Appendix E add the context double-meaning disambiguation note (context object vs evaluation context; renaming the key to evaluation_context is listed as a v2.4 candidate closure item)
v2.3 2026-10-06 Body text aligned with the review-closure implementation (closure decisions written into the body): §8.2a.1 adds the DO sub-structure key-set/key-order table (S1) and the DO-hash domain-separation prefix erdl-eval-do-v3:; §8.2a.1a — the rule canonical object adds the five rule-text fields (M5), the rule_set_hash preimage adds on_indeterminate (S3), and the sort key is unified to (ring, priority, override, definition order); §8.2a.1b — eval_profile drops engine_id (S4, transport-envelope only); §8.2a.2 — transition records add fired/reason (M7); §7.0.3 — the output contract adds the indeterminate_rules row; §7.1a — adds the tier 0–2 interception lock (locked); §10.2 example ruleId → rule_id
v2.3 2026-10-06 Review closure (S1–S6 + M1/M2/M5/M7/M10, breaking): S2 — the canonical encoding of number literals becomes a typed object {"n":"<decimal string>"} (eliminating number/string collisions); the tree hash gains a versioned domain-separation prefix erdl-tree-v3:; S1 — adds DO sub-structure key-set/key-order definitions + snake_case naming unification + the new indeterminate_rules; S4 — engine_id moves out of the DO hash preimage (envelope only); S3 — adds metadata.on_indeterminate (default REQUEST_HUMAN; tier 0–2 may configure DENY); unknown no longer fails open; S5 — decision merge becomes a set-based fold (permutation-invariant) + an outer ring MUST NOT cover an inner ring (ring(o) ≤ ring(r)) + WORKFLOW loses terminal status (only EMERGENCY_HALT short-circuits) + tier 0–2 interception locked; S6 — condition-level scope becomes writable + the counting key takes in rule name/window/scope values; M1/M2 — missing-field arithmetic and empty aggregation fold to unknown (not false/error); M5 — rule text (instruction/reason/correction/explanation/alternative) enters rule_set_hash; M7 — transition audit records add fired/reason fields; M10 — resource-limit violations are recorded in eval_warnings
v2.3 2026-10-06 The fact field name is unified to context across the board (consistent with the engine parameter name and the RFC-002 governance-layer field name); §7.0.1 removes the context.context nesting (free fields attach directly at the top level)
v2.3 2026-10-04 §6a engine implementation (reference implementation landing): adds state-definition.ts (load-time validation: state/transitions structure, same-variable conflict, state/event reference checks, guard whitelist) + state-machine.ts (event-injected FSM: eager FIFO, event_id de-dupe, atomic guard evaluation, genesis/transition/transition_error audit chain, on-demand state_snapshot); Evaluator gains stateMachine option + state.* controlled read + EvaluationResult.stateSnapshot
v2.3 2026-10-04 §7.0.1a adds the field contract (EntityFieldContract) + §7.3(a) adds strict mode — declaring field types + comparison-node type mismatch records a warning under strict mode, fixing the audit findings “fail-open” and “silent false”
v2.3 2026-10-04 §8.2a.1 evaluation-result DO field order adds context (the input context object, called context in RFC-002) — fixes the normative gap of “DO hash preimage missing the input context”, making “this decision, made against this input” independently recomputable; context sits first in the field order, semantically the “input → decision” closed loop (breaking: DO hash-preimage field-order change)
v2.2 2026-09-28 §7.1 wording clarifications + override-absent sort alignment: item 2 states “sort by override level (critical > high > normal > low)”; item 6 unifies “when is the literal true”; override absent sorts as “default normal” (erdl-formal absent rank 4 → 2) — closes erdl-vectors#4 SPEC-REVIEW A/B/C
v2.2 2026-09-27 §7.1 item 5 adds the tightening-direction clarification and fixes override-on-DENY semantics: DENY / ROLLBACK / QUARANTINE covering ALLOW (tightening) is the default consequence of “MUST NOT override to a less-safe state”, does not compare ring, and needs no override (an override on a tightening decision is inert); override acts only in the relaxing direction (DENY → ALLOW) — resolves the erdl-vectors PR#5 R08 interpretation ambiguity
v2.3 2026-10-07 Removed AV-15 (re-authorization provenance, §6a.10) and AV-16 (multi-root basis-scoped revocation, §6b.4) together with §6b.4 — multi-root composition and re-authorization provenance are out of current scope; the delegated-authority adversarial family converges to AV-01~14
v2.2 2026-09-17 Landed conformance vectors AV-15 (re-authorization provenance, §6a.10) and AV-16 (multi-root basis-scoped revocation, §6b.4) — each a single vector with attack (→DENY) / legal (→ALLOW) sides; §6a.10/§6b.4 V-STATE cases annotated with their vector ids; §6b.5 adversarial vector family aligned from “AV-01~14 + AV-15/16” to “AV-01~16” (fixes the “two vectors” wording: AV-15/16 are not two independent DENY/ALLOW vectors but one vector each with both sides)
v2.2 2026-09-16 §6b.4 new: basis-scoped revocation (multi-root composition) — a subject’s effective authority is the union over its currently-valid authorization bases; revoke(basis-X) removes exactly basis-X’s derivable authority (no less — full transitive closure of its downstream derivation; no more — other bases’ contribution survives); MUST NOT reduce a subject’s authority to a global per-subject revoked/authorized bit (forbids over-revocation and under-revocation); a surviving basis MUST NOT preserve authority unique to a revoked lineage; refines INV-04’s “downstream subtree” to be basis-relative; glossary adds authorization basis
v2.2 2026-09-15 §6 decision types gain a design rationale: 13 types exist to maximize LLM value in the AI era, not simply allow/deny; five groups (allow-block / guide / human-in-the-loop / safety fallback / process)
v2.2 2026-09-15 §6a.9 new: latest-authoritative-head freshness (anti-rollback, integration requirement) — successful replay verification does not establish currentness (distinguish integrity/provenance from freshness); before authorizing a security-sensitive side effect the enforcement/recovery boundary MUST establish that {state_version, transitions_head} is the latest authoritative head (not superseded), implementation-neutral (monotonic epoch / durable anchor / signed checkpoint / consensus); fail closed when freshness cannot be established; V-STATE adds the authorized@N/HN → revoke@N+1/HN+1 → restore historical prefix → replay succeeds → reject effect anti-rollback vector
v2.2 2026-09-15 §6a.9 layering clarification (Finding 2 sign-off): the durable freshness anchor is provided by the organization/deployment layer; the fail-closed property is preserved — if the enforcement boundary cannot establish that the restored {state_version, transitions_head} is sufficiently fresh relative to the authoritative persistence state, authority-bearing effects MUST NOT proceed; successful replay/integrity verification is never sufficient evidence that authority is still current
v2.2 2026-09-15 §6a.10 new: authorization-root provenance for establishing/re-establishing authority (integration requirement) — a transition that makes authority exercisable MUST carry authorization-root provenance (actor attributable to a principal entitled to establish it); re-authorization after revocation MUST have a new valid authorization basis; a descendant MUST NOT self-restore revoked authority; authorization-root eligibility is a boundary/organization-layer obligation (fail closed when unestablishable); V-STATE adds root-establish→revoke→non-root-re-authorize→attempt (DENY) and root-reestablish→ALLOW vectors
v2.2 2026-09-15 §6b new: delegated-authority security model (organization behavior layer) — umbrella “delegation must never manufacture authority”; five invariants INV-01~05 (non-amplification / provenance continuity / narrow-only inheritance / transitive revocation / capability boundary), each = property + violation shape + normative assertion; mechanism-neutral revocation freshness; adversarial vector family AV-01~14 + AV-15/16
v2.2 2026-09-14 §6a.8 new: enforcement-boundary check/act atomicity (integration requirement) — for §6a-dependent security-sensitive side effects, the boundary MUST re-validate or close the synchronous boundary so no authorization-lineage state change commits between decision and effect; the engine exposes the re-validation primitive, the boundary discharges the obligation (E1 purity preserved); V-STATE adds the authorized@N → ALLOW@N → revoke@N+1 → attempt-effect fail-closed vector
v2.2 2026-09-12 New §6a state blocks and state transitions (controlled state source): state/transitions optional top-level fields; controlled state injection (state.* reuses the field node, no new nodes); resource caps (≤4 variables/2–4 enums/≤256 combinations/≤32 transition rules/≤16 event names/≤8-key payload)
v2.2 2026-09-12 State-transition audit closure: transition chain + snapshot + validity + provenance anchoring; state_snapshot extended to {values,state_version,transitions_head}, keys code-point-ascending by state-variable name + string NFC normalization
v2.2 2026-09-12 Same-variable conflict decidable mutual-exclusion check ((0)-(4) sound constraints: unconditional-unique + top-level-conjunct-only proof basis, reject rather than silently accept)
v2.2 2026-09-12 §6a.7 event and transition evaluation context: event object event_id/on/at/actor/payload; guards read only state.* + event.*, not free context
v2.2 2026-09-12 Event-handling atomicity (evaluate guards one by one in definition order → stop at the first EvaluationError committing no set, fail-closed → commit all sets at once on full pass; order within one event must not affect the result); event injection authentication (actor enters the audit record, unauthenticated events rejected); concurrency serialization (event handling and evaluate are mutually exclusive); genesis record (initial generates an initial snapshot + canonical-tree hash)
v2.2 2026-09-12 Load-time validation full set (any expression position referencing an undeclared state., a field exactly "state", or a transitions.when referencing free context are all rejected); state scoping (only a first-segment-`state` path enters the controlled namespace, context.state.* still resolves as context but lint warns)
v2.2 2026-09-12 decision renamed audit_as (audit carrier only, does not participate in evaluation/short-circuit, narrowed to {ALLOW,NOTIFY,DELEGATE,ESCALATE,REQUEST_HUMAN}); transitions gains enabled (default true) and reason constraint ([a-z][a-z0-9_]{0,31} + document-unique); state gains display_name (bilingual, gloss uses en falling back to name)
v2.2 2026-09-12 transitions.when node whitelist (Simple conditions + time nodes; no quantifiers/arithmetic/aggregates/fn/within/rate); state machine has no time trigger (freshness via external sweeper or guard time comparison)
v2.2 2026-09-12 §7.0.2 evaluation algorithm gains an events-happen-first declaration (step 0) and catch-all lazy two-pass, fixes the WORKFLOW cross-reference (state machine split: §6 workflow / §6a authority); §7.0.3 adds the state_snapshot output field (enters the hash preimage); E1 extends the authority-state snapshot as a controlled external input; glossary adds state variable / state space / state transition / controlled injection / state_snapshot / transition validity
v2.1 2026-09-12 §8.2 pins the canonical encoding of number literals to JCS (RFC 8785) IEEE 754 number serialization (aligned with the reference implementation); distinguishes the evaluation convention (E2 fixed-point) from the encoding convention (§8.2 canonical serialization); E12 clarifies Guard-context semantics (a Guard context fail-closes for all tiers; a non-Guard context fail-closes tier≤2 and folds tier 3–5 to false); glossary adds non-Guard context / evaluation scope / encoding scope; §7.3(a) spells out the missing-field arithmetic split (comparison node→false, arith node→EvaluationError); §7.0.2/§7.0.3 aligned with E12
v2.1 2026-09-10 §7.3(c) clarifies conformance compares the scale-14 fixed-point value numerically (trailing-zero insensitive: "35" ≡ "35.0"), not the string spelling — the decimal-string form is an encoding, not the comparison unit; §7.3(a) extends the warning asymmetry to logic nodes (and/or over a non-boolean operand fold silently) and quantifiers (all/any/none over a non-array operand record type_mismatch); §7.3(b) clarifies quantifier non-array over; §7.3(d) clarifies the ReDoS fold (false + regex_re_dos, errored: false); §7.3(g) new: E4 structural resource-limit violations throw (value: null + threw: true), E5 exclusivity records value: true; §5.5 adds gloss rendering details (not(eq) normalization, quoted string/list literals, parenthesized arithmetic); §7.3(a) clarifies the errored reading: in/string/length/aggregate record a type_mismatch warning but errored: false (a warning only, not an E3 EvaluationError)
v2.1 2026-09-09 §7.3(a) annotates the warning asymmetry (comparison/between fold silently with no warning; in/string/length/aggregate record type_mismatch); §5.5 aligns gloss template wording to the renderer (in/between/length/match/epoch_ms/date_part/date_add/aggregate/quantifier/var); §5.5 pins gloss rendering to English canonical (G3 display_name takes the English value; Chinese template is a presentation-only optional projection); §7.2 E3 / §7.3(a) / Appendix E add the errored evaluation-error flag: EvaluationError (division by zero / invalid date / arity / type-mismatched arithmetic) → errored=true (even though E12 folds to false); type-mismatched comparison and null propagation → errored=false (not an error)
v2.1 2026-09-05 §7.1 adds item 6: an empty-condition rule (catch-all/fallback) MUST NOT rewrite the decision established by an explicit-condition rule (in either direction); the fallback takes effect only when no explicit rule matches; §7.3(f) clarifies date-time input parsing is whole-second precision (fractional seconds not supported), aligned across implementations
v2.1 2026-09-04 §7.3(d) clarifies the safe syntax subset as a regular language: backreferences (\1–\9, \k<name>) and lookaround ((?=)/(?!)/(?<=)/(?<!)) are forbidden; inline case flags are not provided (matching is always case-sensitive)
v2.1 2026-09-03 §4.1 adds three optional fields — category (rule-level override), enabled (enable flag), correction (CORRECT fix text) — completing the field table and fixed order; §7.0.3 adds the primary_correction source cross-reference. Protocol erdl/v2 unchanged; rule-format version 2.0.0 → 2.1.0 (additive optional fields, non-breaking)
v2.0 2026-08-30 Finalized

Normative References


Acknowledgments


© 2026 Shenzhen Miaojing Technology Co., Ltd. · MIT License