This document describes the public API of the ERDL reference implementation
(@openoba/erdl). It is the implementation’s interface — the language itself
is defined by the specification (erdl-language-spec-v2.3.md /
erdl-language-spec-v2.3.en.md), which is the single source of truth for
format and semantics.
npm install @openoba/erdl
type Decision =
| 'ALLOW' | 'DENY' | 'CORRECT' | 'NOTIFY'
| 'EMERGENCY_HALT' | 'ROLLBACK' | 'QUARANTINE'
| 'REQUEST_HUMAN' | 'ESCALATE' | 'DELEGATE' | 'DEFER'
| 'WORKFLOW' | 'GUIDE'
| 'WORKFLOW_WAITING' | 'WORKFLOW_PROGRESS' // WORKFLOW substates
type OverrideLevel = 'critical' | 'high' | 'normal' | 'low'
type RingLevel = 0 | 1 | 2 | 3
type RuleTier = 0 | 1 | 2 | 3 | 4 | 5 // tier 0-2 safety baseline, >=3 business scope
type RuleCategory =
| 'coding' | 'engineering' | 'security' | 'writing' | 'design'
| 'performance' | 'testing' | 'compliance' | 'accessibility'
| 'observability' | 'custom'
type ConditionOperator =
| 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'
| 'in' | 'not_in'
| 'contains' | 'not_contains' | 'match' | 'starts_with' | 'ends_with'
| 'not_starts_with' | 'not_ends_with'
| 'exists' | 'not_exists'
| 'length_gt' | 'length_gte' | 'length_lt' | 'length_lte' | 'length_eq'
| 'between' | 'not_between'
| 'count_gt' | 'count_gte' | 'count_lt' | 'count_lte'
interface ErdlMetadata {
name: string
description?: string
category?: RuleCategory
/** Fallback decision when no rule matches. */
decision?: Decision
tags?: string[]
}
interface ErdlDocument {
protocol: string // "erdl/v2"
version: string
metadata: ErdlMetadata
state?: StateDeclaration[] // §6a state space (optional)
transitions?: TransitionRule[] // §6a transition rules (optional)
rules: RuleDefinition[]
}
function parseErdlDocument(yamlText: string): ErdlDocument
function loadErdlFile(filePath: string): ErdlDocument
parseErdlDocument parses an ERDL YAML string into RuleDefinition[].
Throws on an unsupported protocol, a missing version, a missing
metadata.name, a rule missing name/then, or an invalid when string
(only "true" is legal). The §5.4 decision-table form compiles each row into
one rule (row order = priority; empty rows become catch-all literal-true).
The §6a state/transitions blocks are validated at load time (structure,
same-variable conflict, state/event reference checks, transition-guard
whitelist) and returned in the document.loadErdlFile reads a file and delegates to parseErdlDocument.import { loadErdlFile } from '@openoba/erdl'
const { rules, metadata } = loadErdlFile('refund.erdl.yaml')
interface RuleDefinition {
id: string
name: string
description: string
category: RuleCategory
conditions: RuleCondition[]
conditionLogic?: 'AND' | 'OR'
action: RuleAction
priority: number
enabled: boolean
override?: OverrideLevel
/** Rule tier 0-5; E12 folds evaluation errors by tier (tier<=2 fail-close). */
tier?: RuleTier
version?: number
legal_basis?: string | null
source_text?: string | null
unless?: { logic?: 'AND' | 'OR'; conditions: RuleCondition[] }
workflow?: WorkflowDefinition
scopeLevel?: 1 | 2 | 3 | 4 | 5
hitCount?: number
}
interface RuleCondition {
field?: string
operator?: ConditionOperator
value?: unknown
within?: string
rate?: string
pattern?: string
keywords?: string[]
expr?: unknown
}
interface RuleAction {
decision: Decision
instruction?: string
reason?: string
explanation?: string | { zh: string; en: string }
alternative?: string | { zh: string; en: string }
ring?: RingLevel
correction?: string
}
interface RuleMatch {
ruleId: string
ruleName: string
decision: Decision
instruction?: string
reason?: string
explanation?: string | { zh: string; en: string }
alternative?: string | { zh: string; en: string }
ring?: RingLevel
correction?: string
priority: number
}
interface EvaluationResult {
decision: Decision
matchedRules: RuleMatch[]
unlessExemptions?: RuleMatch[]
primaryInstruction?: string
primaryReason?: string
primaryExplanation?: string | { zh: string; en: string }
primaryAlternative?: string | { zh: string; en: string }
primaryCorrection?: string
totalEvaluated: number
totalMatched: number
temporalState?: TemporalStateEntry[]
/** Window-count snapshots of stateful operators (within/rate), recorded into the DO temporal_state. */
temporalState?: TemporalStateEntry[]
/** §6a state snapshot read during evaluation ({ values, state_version, transitions_head }). */
stateSnapshot?: StateSnapshot
/** Matched rules' canonical tree snapshots + hashes (E6 evidence). */
canonicalTrees?: Array<{ ruleId: string; tree: unknown; hash: string }>
/** Non-fatal evaluation warnings (E3). */
evalWarnings?: Array<{ kind: string; message: string; nodeType?: string }>
/** Whether an evaluation error occurred (E3/E12). */
errored?: boolean
/** Injected evaluation moment, ISO UTC (E9). */
asOf?: string
}
class Evaluator {
evaluate(
rules: RuleDefinition[],
context: Record<string, unknown>,
options?: { asOf?: Date | string; fallbackDecision?: Decision; strict?: boolean; fieldContracts?: Record<string, { type?: string; default_value?: unknown; optional?: boolean }>; stateMachine?: StateMachine },
): EvaluationResult
}
import { loadErdlFile, Evaluator } from '@openoba/erdl'
const { rules, metadata } = loadErdlFile('refund.erdl.yaml')
const result = new Evaluator().evaluate(
rules,
{ tool: { name: 'issue_refund', args: { amount: 8000 } } },
{ fallbackDecision: metadata.decision },
)
console.log(result.decision) // 'REQUEST_HUMAN'
interface StateDeclaration {
name: string
values: string[]
initial?: string
display_name?: { zh: string; en: string }
}
interface TransitionRule {
on: string
name?: string
audit_as?: string
reason?: string
enabled?: boolean
when?: unknown
gloss?: string
set?: Record<string, string>
}
interface StateEvent {
event_id: string
on: string
at?: string
actor?: string
payload?: Record<string, unknown>
}
interface StateSnapshot {
values: Record<string, string>
state_version: number
transitions_head: string
}
class StateMachine {
constructor(state: StateDeclaration[], transitions: TransitionRule[], docTreeHash: string, options?: { protocol?: string; clock?: Clock })
injectEvent(event: StateEvent): InjectEventResult
getValue(name: string): string | undefined
snapshot(readVars: string[]): StateSnapshot
getStateVersion(): number
getTransitionsHead(): string
getChain(): readonly AuditRecord[]
}
injectEvent processes one event eagerly (FIFO, event_id de-duplicated).
A guard EvaluationError fails closed (no set, a transition_error record);
all guards passing commits one atomic transition (a transition record,
state_version +1). Duplicate event_id or no matching transition → silently
dropped.AuditRecord is one of genesis / transition / transition_error, each
serially anchored by previous_hash.import { StateMachine, Evaluator } from '@openoba/erdl'
const sm = new StateMachine(
[{ name: 'authorization', values: ['authorized', 'revoked'], initial: 'revoked' }],
[
{ on: 'authorize', audit_as: 'DELEGATE', set: { authorization: 'authorized' } },
{ on: 'revoke', audit_as: 'DELEGATE', set: { authorization: 'revoked' } },
],
docTreeHash,
)
sm.injectEvent({ event_id: 'e1', on: 'authorize', actor: 'root-P' })
const result = new Evaluator().evaluate(rules, fact, { stateMachine: sm })
result.stateSnapshot // { values: { authorization: 'authorized' }, state_version: 1, transitions_head }
type GlossLang = 'zh' | 'en'
type FieldNameMap = Record<string, string>
function renderNode(node: ExprNode, lang: GlossLang, fieldNames?: FieldNameMap): string
function renderGloss(root: ExprNode, decision: string, lang?: GlossLang, fieldNames?: FieldNameMap): string
function renderDecisionTableGloss(table: object, lang?: GlossLang, fieldNames?: FieldNameMap): string
gloss is the deterministic natural-language projection of an expression tree
(spec §5.5). renderGloss renders the full “when … then …” sentence.
function jsonWhenToExpr(when: Record<string, unknown>): ExprNode | null
function ruleWhenToExpr(rule: RuleDefinition): ExprNode | null
function normalizeOperator(op: string | undefined): string | null
jsonWhenToExpr compiles a when object (Simple / Expression / decision-table
shape) into the 34-node expression tree (spec §5.3).
interface ValidationError {
field: string
code: string
message: string
level?: 'error' | 'warning' | 'info'
}
interface ValidationResult {
valid: boolean
errors: ValidationError[]
}
class RuleValidator {
validate(input: TemplateInput): ValidationResult
}
const ruleValidator: RuleValidator
class RuleYamlSerializer {
constructor(rulesDir: string)
toSpec5Yaml(rule: object): string // -> Sec. 2.1 YAML string
toSpec5Document(rule: object): object // -> structured document
writeRuleFile(rule: object): string // -> writes <category>/<name>.erdl.yaml
removeRuleFile(name: string, category: string): void
syncAll(rules: object[]): number
}
Non-normative tooling: the template engine is an engine-bundled convenience for generating valid §2.1 YAML from structured parameters. It is not part of SPEC conformance — an implementation MAY omit it without affecting conformance (SPEC §11).
type TemplateId = 'toolInList' | 'toolInAndMatch' | 'toolEqAndCmd' | 'toolEq'
| 'fieldCompare' | 'fieldInList' | 'fieldInAndCompare' | /* ... */ string
class TemplateEngine {
getTemplates(): TemplateDef[]
getTemplate(id: TemplateId): TemplateDef | undefined
generate(input: TemplateInput): TemplateOutput
}
const templateEngine: TemplateEngine
const TEMPLATES: TemplateDef[]
The template engine produces deterministic §2.1 YAML from typed parameters.
const CONDITION_OPERATORS: readonly ConditionOperator[] // the 28 condition operators
const CONDITION_MODIFIERS: readonly ['within', 'rate'] // the 2 modifiers
const ALL_OPERATORS: readonly string[] // 28 + 2 = 30
const OP_COMPARE / OP_LIST / OP_STRING / OP_LENGTH / OP_RANGE / OP_COUNT / ...
The expression-tree node types (the 34-node kernel: ExprNode, CompareNode,
ArithNode, QuantifierNode, …) are re-exported from the @openoba/erdl
root (via index.ts); there is no subpath export such as
@openoba/erdl/expr-tree.