Coming from OPA / Rego
In short: if you write Rego, the grounds of an Arxo policy will look
familiar: several bodies for one decision, a default, a negation, a deny
that wins over allow. What changes is that each of those is a named
construct with its own meaning rather than an idiom. A ground is a
defeasible rule; default allow := false becomes a closure over a named
register; not stays silent when the fact is unknown; deny-overrides is a
contrary rule plus a priority with a stated reason; and “undefined” is an
explicit answer, NEITHER, instead of a missing key. This page walks
through the admission policy of the OPA comparison:
two grounds for admission, an explicit refusal for suspended subjects, a
default refusal, and a break-glass exception. The Arxo package quoted here
passes law engine check; its scenarios and the Rego side of the
experiment have not been run yet, so the page shows how the policy is
written, not measured outcomes.
Concept map
Section titled “Concept map”| Rego | Arxo | What changes for you |
|---|---|---|
Incremental definitions: several bodies for allow (logical OR) | Several rule … defeasible with the same head, admitted(s) | Each ground is a rule with its own name, so the answer can say which ground supported it |
default allow := false | closure over a domain (subject_on_file) with a snapshot, a complete_as_of time and derive_explicit_negative true | The negative is derived only for subjects in the domain that have no positive support; grounds are not touched. You name the register you treat as complete and the time it is complete as of |
not (negation as failure) | not p(x) in a rule body | The literal needs p(x) to be established false. If p(x) is unknown, the rule stays silent instead of firing |
Deny overriding allow, written by hand with not deny, else and a default | A contrary rule with head not admitted(s) and one priority per ground it beats, each with reason explicit_exception | Precedence is a declaration with a named reason, not a pattern spread over several rules |
| Undefined: a missing attribute makes the rule not fire | NEITHER: not established; whyNot names the rule that did not fire | Absence of a fact, an explicit refusal and a conflict are three different answers |
Two values for one complete rule: eval_conflict_error, “complete rules must not produce multiple outputs” | Two contrary rules with no priority between them: the conflict is kept in the answer as BOTH | A conflict is a result you can inspect, not an evaluation error |
else chains | No else form; the same refusal is reached through a priority | Branch order carries no meaning of its own |
and / or keywords in a body | and inside when; an alternative is another rule with the same head | The policy revision that rewrites grounds with and / or leaves no trace in the Arxo package |
with to replace input in opa test | The facts of a scenario, written in its given block | A variant of a case is another scenario with different facts |
The vocabulary
Section titled “The vocabulary”In Rego, the input document is free-form JSON and a rule reads whatever
paths it needs. In Arxo the facts a policy reads are declared first, each
with a kind: empirical for observations about the world,
institutional for statuses that exist because a rule or a register
says so.
language "law.core" version "0.2";package opa.admission version "0.1.0";namespace "urn:law:stand:opa-admission";
entity Subject;
relation subject_on_file(s: Subject) kind institutional;relation is_editor(s: Subject) kind institutional;relation is_owner(s: Subject) kind institutional;relation resource_public(s: Subject) kind empirical;relation active_flag(s: Subject) kind empirical;relation suspended(s: Subject) kind institutional;relation break_glass(s: Subject) kind institutional;relation admitted(s: Subject) kind institutional;The case supplies these facts; the JSON cases of the experiment are mapped onto them by a small adapter, and the verdict document (admitted, grounds, refusal reasons, policy revision) has the same shape on both sides.
Grounds and the default
Section titled “Grounds and the default”The Rego policy closes the decision with a default:
default allow := falseIn Arxo, each ground is its own defeasible rule, and the default is a closure: a statement that the register of subjects on file is complete as of a given time, so a subject on file with no ground is explicitly not admitted.
rule EditorAllow defeasible { for s: Subject; when is_editor(s); then admitted(s);}
rule OwnerPublicAllow defeasible { for s: Subject; when is_owner(s) and resource_public(s); then admitted(s);}
closure AdmissionDefault { predicate admitted; domain subject_on_file; snapshot "urn:stand:opa-admission:cases:2026-09-30"; complete_as_of @2026-09-30T00:00:00Z; derive_explicit_negative true;}Two differences from default matter in practice. The closure applies
only inside its domain: a subject who is not on file and has no ground is
NEITHER, not refused. And the closure produces a negative only where no
rule supports admitted, so adding a ground never has to be coordinated
with the default.
Deny overrides allow
Section titled “Deny overrides allow”In Rego, allow and deny are ordinary rule names; a refusal that beats
every ground is written out with negations, alternatives and the default.
In Arxo the refusal is a rule with the opposite head, and its precedence
over each ground is declared with a reason:
rule SuspendedRefusal defeasible { for s: Subject; when suspended(s); then not admitted(s);}
priority SuspendedOverEditor { prefer SuspendedRefusal over EditorAllow; reason explicit_exception;}
priority SuspendedOverOwner { prefer SuspendedRefusal over OwnerPublicAllow; reason explicit_exception;}The policy revision that adds a break-glass exception is one more ground.
It needs the break_glass fact to fire, so cases without that fact are
unaffected by the revision:
rule BreakGlassAllow defeasible { for s: Subject; when break_glass(s); then admitted(s);}The package deliberately declares no priority between BreakGlassAllow
and SuspendedRefusal, because no case in the experiment combines them.
A subject with both facts would get BOTH: the conflict stays visible
until someone decides which rule wins and writes it down.
Negation and the unknown
Section titled “Negation and the unknown”The OPA documentation illustrates not with a rule that fires when a key
is missing:
deny contains "missing email" if not input.emailUnder negation as failure, a missing key is enough for not to succeed.
In Arxo, a negative literal in a body needs the fact to be established
false:
rule InactiveRefusal defeasible { for s: Subject; when subject_on_file(s) and not active_flag(s); then not admitted(s);}If the case says nothing about active_flag, the rule does not fire. To
make silence count as “false”, close the predicate over a register, as
AdmissionDefault does for admitted; then the negative is derived and
the literal is satisfied. Absence becomes “false” only where you have
said the list is complete.
Two values for one decision
Section titled “Two values for one decision”In Rego, two complete-rule bodies that produce different values for the same document are an evaluation error. The experiment includes a mutant policy with exactly that defect. Its Arxo counterpart is two rules with contrary heads and no priority:
rule AllowVersionA defeasible { for s: Subject; when subject_on_file(s) and is_editor(s); then admitted(s);}
rule AllowVersionB defeasible { for s: Subject; when subject_on_file(s) and is_editor(s); then not admitted(s);}Arxo does not stop here: it keeps both supports and answers BOTH. The
comparison records this pair as not directly comparable and examines it
case by case, rather than counting it as a mismatch: both sides flag a
policy that contradicts itself, by different means.
What you gain, what you give up
Section titled “What you gain, what you give up”- Gain: three answers where Rego has one silence. Not established, explicitly refused, and in conflict are separate outcomes, and an explanation names the rule that did not fire.
- Gain: declared precedence. Which refusal beats which ground, and
why, is written once as a
prioritywith a reason. - Give up: Rego idioms with no direct form. There is no
elsechain and nowithoverride; order is expressed by priority, and variants of a case are separate scenarios. - Different deployment model. OPA’s delivery machinery (signed bundles, decision logs with replay, the server, the portable module) is outside this comparison and has no counterpart on this page; the comparison is about the policy language and its answers. In the other direction, OPA has no notion of “the policy as of this date” with editions and pinned sources; the OPA comparison marks that as inferred from the documentation.
Next steps
Section titled “Next steps”- OPA / Rego: the full comparison, the studied version and what is still open.
- Defeasible rules and unless, Priority, Negation and truth statuses: the constructs used on this page.
- Missing and conflicting facts:
NEITHER,BOTHandwhyNotin a worked example. - Guide: getting started with Arxo.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.