ADL v0.1 — Adjudon Decision Language
ADL (Adjudon Decision Language) is the typed, human-readable surface for authoring policies. It compiles to AWS Cedar (CNCF Sandbox, peer-reviewed formal semantics) for production evaluation. ADL exists because Cedar's syntax is optimized for IAM-shape problems, and AI-decision policies need a vocabulary that matches confidenceScore, outputDecision, humanOverride, and friends.
Why a separate language
| Concern | Cedar native | ADL |
|---|---|---|
| AI-decision predicates | Has to be expressed via context attributes | First-class confidenceScore < 0.7 |
| Decimal handling | Method calls (a.lessThan(b)) | Operators (a < b) — auto-translated |
| Type errors | Caught at evaluation | Caught at compile time |
| Customer authoring | Steeper learning curve | Tuned for compliance teams |
| Substrate portability | Single substrate (Cedar) | Substrate-agnostic — recompiles to OPA, Rego, or future engines |
Lifting from JSON v1
Existing JSON v1 policies (the legacy surface that ships in templates and the dashboard editor) are lifted to ADL via adlLifter.liftV1ToAdl. The lift is loss-less: every JSON v1 condition + action maps to an ADL clause. Customers can read the lifted ADL view at GET /api/policies/:id/adl (Phase 1 Track E read-only ship; Phase 2 Track A.1–A.3 ships the Monaco editor).
Predicate registry (seed, Phase 1)
| Predicate | Underlying field | Operator | Args |
|---|---|---|---|
confidence_below(threshold) | confidenceScore | < | threshold: decimal |
confidence_below_or_equal(threshold) | confidenceScore | <= | threshold: decimal |
confidence_above(threshold) | confidenceScore | > | threshold: decimal |
confidence_above_or_equal(threshold) | confidenceScore | >= | threshold: decimal |
confidence_equals(value) | confidenceScore | == | value: decimal |
score_calibrated_below(threshold) | score_calibrated | < | threshold: decimal |
status_equals(value) | status | == | value: string |
output_contains(substring) | outputDecision | contains | substring: string |
output_matches_regex(pattern) | outputDecision | matches (RE2) | pattern: string |
agent_equals(agentId) | agentId | == | agentId: string |
human_override_enabled(value) | humanOverride | == | value: bool |
New predicates ship per release; the predicate registry is versioned with the language (adjudon-adl-v0.1). Customers writing custom predicates submit a request via the dashboard; Adjudon adds the type binding + Cedar compile target. The lifter (adlLifter.liftV1ToAdl) emits a generic <field>_<operator> predicate for legacy v1 conditions not yet promoted to the curated registry.
Surface syntax (textual)
ADL v0.1 uses a predicate-call surface — every condition is a named domain function from the predicate registry, applied to typed arguments. This is closer to Cedar's evaluation shape than infix operator notation and gives the editor a clean autocomplete + arity-check experience.
name "Block low-confidence outputs"
priority 10
enabled true
when confidence_below(0.7)
and human_override_enabled(false)
or status_equals("flagged")
then flag_for_review
Operators:
| Token | Meaning |
|---|---|
and | Short-circuit AND |
or | Short-circuit OR |
not | Boolean negation (Phase 2 — no JSON v1 equivalent yet) |
(...) | Explicit grouping (flips default and > or precedence) |
Reserved words: name, priority, enabled, when, then, and, or, not, true, false.
Grammar (EBNF)
POLICY = HEADER? WHEN_CLAUSE THEN_CLAUSE
HEADER = ( "name" STRING | "priority" INTEGER | "enabled" BOOL )*
WHEN_CLAUSE = "when" EXPRESSION
EXPRESSION = OR_EXPR
OR_EXPR = AND_EXPR ( "or" AND_EXPR )*
AND_EXPR = UNARY ( "and" UNARY )*
UNARY = "not" UNARY | PRIMARY
PRIMARY = PRED_CALL | "(" EXPRESSION ")"
PRED_CALL = IDENT "(" ( ARG ( "," ARG )* )? ")"
ARG = NUMBER | STRING | BOOL
THEN_CLAUSE = "then" ACTION ( "," ACTION )*
ACTION = IDENT (* one of: block flag_for_review notify approve allow auto_approve *)
STRING = '"' ( ESC | <any-char-except-quote-or-backslash> )* '"'
| "'" ( ESC | <any-char-except-quote-or-backslash> )* "'"
ESC = "\\" ( "n" | "t" | "r" | "\"" | "'" | "\\" )
NUMBER = "-"? <digit>+ ( "." <digit>+ )?
BOOL = "true" | "false"
IDENT = <letter-or-underscore> ( <letter-or-digit-or-underscore> )*
INTEGER = "-"? <digit>+
Comments: // line and /* block */. Stripped at tokenization.
Predicate vocabulary (seed registry)
Action ladder
ADL supports the same 5 action types as JSON v1, with the same priority ordering:
block (highest priority — never overridden by lower)
flag_for_review + auto_approve (Auto-Approval Engine merger semantic)
flag_for_review
notify
auto_approve standalone
approve (lowest priority — explicit allow)
The Auto-Approval Engine merger semantic (LD-9, implemented in policyEngine.evaluatePolicies): if flag_for_review and auto_approve both fire on the same trace from any combination of policies, auto_approve wins and the trace resolves as approved (the auto-approval marker is preserved on the transcript so the audit trail shows which pattern caused the resolution). auto_approve NEVER overrides a block from any policy on the same trace — the hard-rule contract holds at the evaluator level.
Compile chain
┌──────────┐
ADL textual source ────▶ adlParser ─────────────▶ ADL IR (JSON tree)
└──────────┘ │
│ adlLowerer
▼
JSON v1 policy ◀────[lowerAdlToV1]───── ADL IR ───[policyCompiler]──▶ Cedar
│ ▲
│ liftV1ToAdl (adlLifter) │
└──────────────────────────────────┘
Three transforms make ADL bidirectional + substrate-portable:
adlParser.parseAdl(source)— textual ADL → ADL IRadlLowerer.lowerAdlToV1(ir)— ADL IR → JSON v1 (policyEngine + storage)adlLifter.liftV1ToAdl(policy)— JSON v1 → ADL IR (round-trip + read view)policyCompiler.compileV1ToCedar(policy)— JSON v1 → Cedar (production substrate)
A round-trip property test (backend/test/adlParser.unit.test.mjs) asserts that lowerAdlToV1(liftV1ToAdl(P)) is engine-equivalent to P for every random v1 policy in a 200-case fast-check suite. This is the kill-gate for any parser/lowerer change.
The IR is substrate-agnostic. Phase 3.1 ships an OPA/Rego compile target so customers who already operate OPA at scale can run Adjudon policies on their existing infrastructure (Cedar remains primary).
Compile errors
The compiler raises PolicyCompileError (backend/services/policyCompiler.js) with one of these structured codes:
| Code | Meaning |
|---|---|
POLICY_COMPILE_UNKNOWN_FIELD | Field not in the registry (FIELD_TYPES) |
POLICY_COMPILE_TYPE_MISMATCH | Operator argument type does not match field type |
POLICY_COMPILE_BAD_OPERATOR | Operator not supported for this field's type |
POLICY_COMPILE_REGEX_NOT_SUBSTRING | RE2 rejected the regex pattern |
POLICY_COMPILE_NO_FIELD | Condition object missing field property |
POLICY_COMPILE_NO_CONDITIONS | Policy has no conditions to compile |
POLICY_COMPILE_INTERNAL | Compiler invariant violated — file a bug |
POLICY_COMPILE_ERROR | Generic compile failure (rare; specifics in .message) |
The ADL parser raises its own error class (AdlParseError in backend/services/adlParser.js) for surface-level issues (ADL_PARSE_ERROR, ADL_MISSING_WHEN, ADL_MISSING_THEN, ADL_ARITY_MISMATCH, ADL_TYPE_MISMATCH, ADL_UNKNOWN_ACTION, etc.). Each error carries line + column so the dashboard editor can highlight the exact token.
Property-based equivalence
Every PR to the compiler runs a 1000-case property-based test (fast-check) that generates random ADL policies + random traces and asserts that:
- The legacy JSON v1 evaluator and the Cedar substrate produce identical match results.
- The compile chain is round-trip stable:
liftV1ToAdl(p)→compileAdlToCedar(...)matchescompileV1ToCedar(p)cell-for-cell.
This is the kill-gate for any compiler change. CI fails on a single divergence.
Versioning
ADL is versioned independently from Adjudon's API. The current version is adjudon-adl-v0.1. Breaking changes require a new major version (v0.2, v1.0) and a migration path documented in the release notes. Existing PolicyVersion records pin the ADL version they were compiled under, so a regulator replaying a 2-year-old transcript gets the exact semantics that were in force at evaluation time — even if the language has since evolved.