(Entity-Rule Definition Language · 实体规则定义语言)
Status: v2.1 · Final Date: 2026-09-03 Version semantics: this document (the ERDL language specification) is version v2.1; the top-level
protocol: "erdl/v2"(protocol identifier, fixed value) andversion: "2.1.0"(rule-format version) are independent version identifiers, not to be conflated with the document version. 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 fact of governance. Conformance language: MUST / MUST NOT / SHOULD / SHOULD NOT / MAY in this document are interpreted per [RFC 2119].
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 two kinds of declarations: Entity defines data structure; Rule defines a when → then decision. Entity defines the structure of an object; Rule prescribes the consequence of a condition; together they form an executable, reviewable, and verifiable rule expression.
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.
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, why it was blocked, and who approved it can all be traced and verified, preventing post hoc explanations from becoming the sole basis.
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.
The semantic carrier is the kernel, not the syntax. Semantics = tree = hash. The three coincide in canonical form.
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.
An ERDL document (*.erdl.yaml) consists of four top-level fields, whose order MUST be fixed:
protocol: "erdl/v2" # protocol identifier, fixed value
version: "2.1.0" # rule-format version
metadata: { ... } # document-level metadata (see §2.2)
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 |
rules |
array | MUST | Rule list; elements defined in §4 |
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 |
protocol: "erdl/v2";protocol major version, new constraints SHOULD be non-breaking for existing rules (a Warning at load, not an Error); a cross-major-version change (e.g. erdl/v1 → erdl/v2) is a breaking change and is not covered by the backward-compatibility promise. Exception: when:"true" combined with a blocking then is rejected at load in any version (Error).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 |
| ③ Compile | Normalize the three writing forms | valid document → expression tree (canonical_tree) | E7, §8.2 |
| ④ Evaluate | Judge the tree node-by-node against the fact | expression tree + fact → decision | §7 |
| ⑤ Emit | Produce evaluation evidence | decision → hashable, recomputable result | E6, §8 |
*.erdl.yaml and parse it per the §2.3 format conventions (YAML and JSON are equivalent, losslessly interchangeable).when/expr mutual exclusion, etc.; violations are rejected at load.within/rate is injected under control via temporal_state.The full contracts for the input fact and the evaluation output are in §7.0.
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.
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.
The rules[] sub-field order MUST be fixed: name → description → category → priority → override → ring → enabled → when → 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 |
enabled |
boolean | MAY | Rule enable flag (default true); false skips the rule during evaluation |
when |
object | MUST | Trigger condition (see §5) |
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 |
protocol: "erdl/v2"
version: "2.1.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"
then: ALLOW
message: "read_file call allowed"
unless: null
when Condition Expressionwhen 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.
| 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).
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).
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) |
Semantic conventions (apply to all operators):
"100" gt 50 is always false;"2" gt "10" is true); cross-type returns false;[min,max] supports only numbers; non-numeric returns false;matches → match, neq → ne. Aliases are not new operators (still the 30-operator set); the canonical form MUST use the canonical name, and aliases never enter the tree or the hash. Not implementing aliases is still conformant.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) |
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(...)).
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, rate keys on field+operator+value+rate); ③ record timing in the “under-limit” branch.
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.
Expression writing example:
when:
expr:
lt:
- div:
- sub: [{ field: "tool.args.price" }, { field: "context.cost" }]
- field: "tool.args.price"
- 0.15
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: 100
- when: [["gte", 5000]]
then: "ESCALATE"
priority: 90
- when: [] # default row (unconditional, fallback)
then: "ALLOW"
priority: 1
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.
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
Five invariants (all MUST):
| # | Invariant |
|---|---|
| G1 | gloss = render(tree): deterministically generated by a frozen rendering template |
| G2 | every rule MUST carry gloss; lint checks gloss == render(tree); hand-writing forbidden |
| G3 | gloss forbids raw field paths and MUST use the Entity’s display_name (bilingual) |
| 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 |
gloss rendering templates (per node, bilingual; {A}/{B}/{C} are recursive render results of sub-expressions):
| Node | English template |
|---|---|
field |
{field} |
var |
{variable} |
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} is in {B} |
contains |
{A} contains {B} |
match |
{A} matches regex {B} |
starts_with |
{A} starts with {B} |
ends_with |
{A} ends with {B} |
exists |
{A} exists |
length |
the length of {A} |
between |
{A} is between {B} and {C} |
all |
every item in {A} satisfies: {B} |
any |
some item in {A} satisfies: {B} |
none |
no item in {A} satisfies: {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 |
the epoch milliseconds of {A} |
date_add |
{A} plus {B} duration |
date_part |
the {part} of {A} |
month_last_day |
the last day of the month of {A} |
aggregate(count) |
the count of {A} |
aggregate(sum) |
the sum of {A} |
aggregate(avg) |
the average of {A} |
aggregate(min) |
the minimum of {A} |
aggregate(max) |
the maximum of {A} |
existsboolean-field special case: when the field name matchesis_*/has_*(boolean-field convention),existsrenders as{A} is trueinstead of{A} exists— boolean fields are true when present, avoiding awkward phrasing (e.g. “has been notified exists”).
then Decision TypesThe 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) |
| 13 | GUIDE | guide |
Evaluation = the pure-function process (E1) by which the expression tree (the compiled product of rules) judges the input fact node by node. This section defines the evaluation input contract, the algorithm steps, and the output contract, to align implementers and users.
The evaluation input is a fact object carrying the current state of the rule’s subject entities, namespaced by Entity (§3):
fact:
tool: # Entity: tool
name: "issue_refund"
args: { amount: 8000, order_id: "O1024" }
context: # free-form context fields (referenced by rules as context.*)
country: "CN"
role: "operator"
# other Entities: agent / task / workflow / human / guardian (provided as needed)
tool.name, context.amount, tool.args.amount) resolve by key path on the fact object (§3);as_of (the evaluation moment, UTC) and temporal_state (the within/rate sliding-window state) are injected by the engine and are controlled external inputs (E1);Input: rule set rules[] + fact object fact
Output: the decision result (see 7.0.3)
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
b. the compiled when expression tree judges fact node-by-node (true / false / error)
c. first-match-wins within each ring (a match short-circuits that ring)
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)
EMERGENCY_HALT short-circuits on match; DENY does not short-circuit — evaluation continues to judge whether an override ALLOW covers it.The evaluation result MUST contain the following fields:
| 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 evaluated |
total_matched |
the total number of rules matched |
temporal_state |
the within/rate sliding-window state snapshot (omitted when nothing matched) |
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).
priority ascending (smaller value = higher precedence);override marker go first;override enumeration: critical > high > normal > low (default normal);override is allowed only in the DENY → ALLOW direction (it MUST NOT override to a less-safe state).| # | Constraint |
|---|---|
| E1 | Evaluation is a pure function: no side effects, no implicit external state, no clock reads; the state injection of within/rate (temporal_state) and as_of are controlled external inputs |
| E2 | Fixed-point decimal scale=14 + half-even + string serialization; intermediate computation uses high-precision bounded rationals, rounding only at output nodes |
| E3 | Evaluation errors are recorded as eval_warnings; folding direction follows E12 by tier |
| E4 | Resource limits (graded): Grade A arithmetic depth≤2 / tree depth≤6 / nodes≤64 / array≤10000 / per-rule≤50ms / no nested quantifiers / regex steps≤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 false (anti-vacuous-truth) |
| 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: tier≤2 and Guard contexts default to fail-close, tier 3–5 folds 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.
The following semantics MUST be explicitly annotated in the document and vectors, to avoid semantic misunderstanding against standard implementations:
(a) Null propagation (E11): Agent context is highly dynamic; missing fields are the norm. Evaluation MUST use safe failure under three-valued logic:
| Scenario | Behavior |
|---|---|
| Equality/numeric comparison on a missing field | returns false (not NPE) |
== null / != null check |
returns true / false normally |
| Type-mismatched comparison | returns false (no implicit conversion) |
| Arithmetic on a missing field | returns false (condition) or EvaluationError (arithmetic expression) |
(b) Quantifier empty-array safe folding (E8): under standard quantifier semantics all(empty)=true (vacuous truth). This specification deliberately deviates: all/any/none(empty) all fold to false — preventing “nothing to check yet judged as allowed” — and record the safe fold in the audit record. Third-party implementations MUST adopt this folding semantics.
(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).
(d) Regex ReDoS protection: the match node MUST satisfy: ① single-match step limit ≤10000; ② input length limit; ③ prefer a deterministic engine (RE2-class) or a safe syntax subset.
(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) |
false |
safe-failure fold (avoid division by zero) |
min(empty) |
false |
safe-failure fold (standard +Infinity, disabled) |
max(empty) |
false |
safe-failure fold (standard −Infinity, disabled) |
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.
(f) Time-node UTC semantics (E9): all time nodes evaluate uniformly in UTC, guaranteeing byte-for-byte consistency across implementations and time zones:
YYYY-MM-DD) parses as UTC; date-time parses per ISO 8601 with timezone, and without a timezone suffix as UTC;date_part): always takes UTC components;date_add, month_last_day): UTC calendar arithmetic;days_between): UTC millisecond difference ÷ 86400000, floor;toISOString).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.
when Minimum-Completeness Constraintswhen: "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 |
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.
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 | numbers are represented as fixed-point decimal strings (scale=14 + half-even); 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.
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.
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.
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 fact 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.
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.
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. 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.
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.1.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
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 fact object:
fact:
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.
See §4.2 (Simple rule), §5.3 (Expression rule), and §5.4 (Decision Table).
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.
Five-step verification: load vector input → generate expression tree → recompute evaluation result → compare with the answer → judge consistency.
Third-party Runner verification flow (from zero to conformance):
| 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.
| 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).
See §6.
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 |
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.
| 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 |
| 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 |
| 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) |
| temporal_state | the within/rate sliding-window state (stateful operators) |
| as_of | the evaluation moment injected by the engine (UTC, E9) |
| fact object | the evaluation input carrying the current state of entities (§7.0.1) |
| 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 against step explosion (§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) |
| Version | Date | Changes |
|---|---|---|
| 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 |
© 2026 Shenzhen Miaojing Technology Co., Ltd. · MIT License