erdl-landing

ERDL™ API Reference

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.

Install

npm install @openoba/erdl

Core Types

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'

Document Loading

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
import { loadErdlFile } from '@openoba/erdl'
const { rules, metadata } = loadErdlFile('refund.erdl.yaml')

Evaluation

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'

State machine (§6a state blocks & transitions)

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[]
}
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 }

Readability (gloss)

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.

Expression-Tree Compilation

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).

Validation

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

Serialization

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
}

Templates

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.

Operator Constants

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.