# 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](/comparisons/opa/): 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 | 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 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. ```law 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 The Rego policy closes the decision with a default: ```text default allow := false ``` In 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. ```law 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 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: ```law 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: ```law 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 The OPA documentation illustrates `not` with a rule that fires when a key is missing: ```text deny contains "missing email" if not input.email ``` Under 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: ```law 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 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: ```law 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 - **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 `priority` with a reason. - **Give up: Rego idioms with no direct form.** There is no `else` chain and no `with` override; 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](/comparisons/opa/) marks that as inferred from the documentation. ## Next steps - [OPA / Rego](/comparisons/opa/): the full comparison, the studied version and what is still open. - [Defeasible rules and unless](/constructs/rule-defeasible-unless/), [Priority](/constructs/priority/), [Negation and truth statuses](/constructs/negation-and-status/): the constructs used on this page. - [Missing and conflicting facts](/language/missing-and-conflicting/): `NEITHER`, `BOTH` and `whyNot` in a worked example. - [Guide](/guide/): getting started with Arxo.