Skip to content
docs
Arxo ↗

Exceptions and conflicting rules

For LLMs10 sections
← Course mapChapter 03 / 25 · Beginner

Ann is a Northbridge resident with a registered car, so the general rule says she is eligible for a parking permit. Then the office learns more: her permit was suspended; or she has outstanding fines; or a badge case carries a fraud flag.

Those are four situations where a plain grant is not the answer, and the engine gives four observably different answers. This article shows which mechanism to use for each, and what goes wrong when they are mixed up.

Northbridge is a synthetic training town: every act, office and calendar here is fictional, and nothing in this article is legal advice or a claim about real legislation.

This article assumes only those two: facts, one rule and law test from the first; the four-valued outcome of a question from the second. No advanced constructs are required.

Base rule: a resident with a registered car is eligible. The rule is defeasible, meaning its conclusion can be withdrawn when an exception applies — unlike a strict rule, which admits none.

Excerpt (package demo.northbridge.permits, packs/examples/language-demo/permits/package.law; imports, types and unrelated rules cut):

Arxo Law
rule PermitEligibility defeasible {
for a: Applicant;
when demo.northbridge.vocabulary::resident(a) and demo.northbridge.vocabulary::vehicle_registered(a);
then permit_eligible(a);
unless permit_suspended(a);
}

Read the block top to bottom: for names the applicant, when states the two qualifying facts, then draws the eligibility conclusion, and unless names the one fact that switches the whole rule off. unless is an exception written inside the rule: when permit_suspended(a) holds, this rule silently produces nothing — it does not argue against eligibility, it just stays out.

A competing rule argues the opposite — fines mean refusal — and a priority names the winner. A third route, the disability badge, has no priority against the fines rule, so that conflict stays visible.

Excerpt (same package and file; only these three blocks shown):

Arxo Law
rule FinesRefusal defeasible {
for a: Applicant;
when outstanding_fines(a);
then not permit_eligible(a);
}
rule BadgeEligibility defeasible {
for a: Applicant;
when disability_badge(a);
then permit_eligible(a);
}
priority RefusalOverEligibility {
prefer FinesRefusal over PermitEligibility;
reason lex_specialis;
}

Note what each block contributes. FinesRefusal concludes not permit_eligible(a) — the explicit negation, not silence. BadgeEligibility offers a second, independent route to eligibility from disability_badge(a). The priority block ranks only the first pair: prefer FinesRefusal over PermitEligibility, with the reason lex_specialis. No line ranks FinesRefusal against BadgeEligibility, and that gap is deliberate — the suite shows the resulting conflict.

A fourth mechanism, the defeater, blocks evidence without asserting the negation. Fraud does not prove Bob ineligible; it only stops his badge from counting in his favour.

Excerpt (same package and file; only this block shown):

Arxo Law
rule FraudBlocksBadge defeater {
for a: Applicant;
when fraud_flag(a);
defeat permit_eligible(a);
}

Note the verb: defeat permit_eligible(a) names the conclusion it blocks, but the block has no then line — there is no conclusion at all. Contrast with FinesRefusal, whose then not permit_eligible(a) records a refusal.

The suite packs/examples/language-demo/permits contains one test per mechanism. Command (run from the repository root):

Terminal
law test packs/examples/language-demo/permits

Observed result (exception and conflict lines; full run: 28 checked, 28 passed, 0 failed):

Output
ok [demo.northbridge.permits] tests/permits.lawtest / unless removes the conclusion
ok [demo.northbridge.permits] tests/permits.lawtest / denial wins by priority
ok [demo.northbridge.permits] tests/permits.lawtest / without priority the conflict persists
ok [demo.northbridge.permits] tests/permits.lawtest / defeater removes support without negation
итого: 28 проверено, 28 прошли, 0 не прошли, 0 не исполнены; код 0

Each test asks the same shape of question — truth(permit_eligible(...)) — with the facts and outcomes below (tests/permits.lawtest, expect truth_status == ...):

TestFacts on fileResult
unless removes the conclusionresident + vehicle + suspended (Ann)NEITHER
denial wins by priorityresident + vehicle + fines (Ann)FALSE_ONLY
without priority the conflict persistsbadge + fines (Bob)BOTH
defeater removes support without negationbadge + fraud flag (Bob)NEITHER

All four tests pass: each answer matched its expectation. A passing test says the mechanism behaved as declared; it does not say the office made the right call on any applicant.

The summary line is in Russian: 28 tests checked, 28 passed, 0 failed, 0 skipped, exit code 0.

The badge-route scenarios use Bob (urn:demo:northbridge:bob) because the badge rule, not the resident rule, is the positive side of that conflict; identifiers are as written in the suite. The package also passes a structural check: law engine check packs/examples/language-demo/permits prints check OK.

The task each mechanism solves:

  • defeasible marks a rule as overridable. It states the general case — residents with cars qualify — while leaving room for exceptions. Without it, every exception would have to be written as a negated precondition, and the rule would rot with each new one.
  • unless handles an exception owned by the rule itself: suspension is part of what “eligible under this rule” means. The rule withdraws its own conclusion; nothing else changes.
  • priority resolves a genuine disagreement between two rules that both fired. The office has two applicable norms — residents qualify, debtors are refused — and must record a refusal, not a shrug. The priority flips the outcome from BOTH to FALSE_ONLY.
  • defeater blocks a piece of support without taking the other side. A fraud flag taints the badge evidence, but the office has said nothing about Bob’s eligibility as such. Support is removed, so the answer is NEITHER — and, crucially, no negation is asserted.

Why these and not something simpler? A strict rule cannot be excepted at all, so it cannot express any of the four rows. A negated precondition (and not permit_suspended(a)) behaves like unless here but spreads the exception logic across every rule body instead of naming it. An explicit not permit_eligible(a) conclusion (like FinesRefusal) is right for the fines case — the office does refuse — but wrong for fraud, where no refusal was decided.

What proves each action: the priority pair is the proof. The same kind of conflict — one rule for, one rule against permit_eligible — yields BOTH where no priority applies (without priority the conflict persists) and FALSE_ONLY where RefusalOverEligibility applies (denial wins by priority). The defeater proof is the contrast between its NEITHER and a refusal’s FALSE_ONLY: the fraud test expects NEITHER — support gone, negation absent.

What the example does NOT prove: it does not prove that lex_specialis is the correct legal ground for preferring refusals — the reason is a label in a teaching package, not a holding. It does not prove anything about real parking law.

Remove the priority and the conflict returns. The suite demonstrates exactly this with the badge rule pair: BadgeEligibility (for) and FinesRefusal (against) have no priority between them, so when Bob has both a badge and outstanding fines, both rules fire and neither yields:

Output
ok [demo.northbridge.permits] tests/permits.lawtest / without priority the conflict persists

with expect truth_status == BOTH. Compare with Ann’s fines case — the same FinesRefusal against a different positive rule (PermitEligibility), but with RefusalOverEligibility in force: FALSE_ONLY. The priority is what turns this shape of conflict into a refusal: where a ranking applies, the denial wins; where none applies, the conflict persists as BOTH.

A BOTH answer is the engine telling the office that two applicable norms disagree and no ranking was given — work for the drafter, not a decision.

Writing the fines refusal as an unless clause on the eligibility rule instead of a separate rule with priority:

Sketch — the wrong shape on purpose (the extra unless line is the mistake); contrast with the real rule in permits/package.law.

Arxo Law
rule PermitEligibility defeasible {
for a: Applicant;
when demo.northbridge.vocabulary::resident(a) and demo.northbridge.vocabulary::vehicle_registered(a);
then permit_eligible(a);
unless permit_suspended(a);
unless outstanding_fines(a); // mistake: refusal modelled as exception
}

Observable consequence: with outstanding fines, the question returns NEITHER (no rule speaks) instead of FALSE_ONLY (the office refuses). Downstream, everything built on a refusal breaks: duties and positions that trigger on not permit_eligible(a) never activate, and the file records no decision, only silence.

The fix: if the office must be able to say “refused”, the refusal needs its own rule concluding the negation, plus a priority. unless can only ever produce silence.

  • A priority needs explicit grounds: prefer X over Y with a reason. Without the block, conflicting defeasible rules yield BOTH — the engine never invents a ranking.
  • A defeater is not a negation. defeat permit_eligible(a) removes support; it never produces FALSE_ONLY by itself. Do not use it where a refusal must be recorded.
  • unless belongs to one rule only: it blocks that rule’s conclusion and nothing else. It cannot resolve two rules that both fired.
  • Verified profile: engine law 0.1.0, semantics law.core/0.2, std 0.2.0. Refusal surfaces (e.g. what the engine rejects) are facts about this implementation, not claims about the language in general.

Bob holds a disability badge and has outstanding fines recorded. Two questions, answered from the suite output and the test sources — no new runs needed:

  1. What is the status of truth(permit_eligible(bob)), and which test name and expect line say so?
  2. Which single package element explains why Ann’s fines case on the resident route answers FALSE_ONLY instead, and what would Ann’s answer become if that element were deleted?

The solution lives only in the nb-03 solution.

  • Teaching package: packs/examples/language-demo/permits/package.law (rules PermitEligibility, FinesRefusal, BadgeEligibility, priority RefusalOverEligibility, defeater FraudBlocksBadge).
  • Scenarios: packs/examples/language-demo/permits/tests/permits.lawtest (unless removes the conclusion, denial wins by priority, without priority the conflict persists, defeater removes support without negation).
  • Level 1 — Northbridge use: this article (fictional permit office).
  • Level 2 — domain template: when a general entitlement meets an internal exception, use unless; when two applicable norms disagree and the denial must stand, use a separate negating rule plus priority with a stated reason; when evidence is tainted but nothing is decided against the person, use a defeater.
  • Level 3 — confirmed external formalization: scope and exclusions (ILO Convention No. 138, minimum age, art. 4) — package intl.labour.c138, corpus/laws/intl/ilo-c138/04-scope-and-exclusions.law:118-125, construct rule ... defeasible with a contrary proviso (WorkIsExcludable: excludable unless hazardous_work(w) then not excludable_work(w)). The exception lives inside the one rule it limits, and only the contrary clause establishes the negation — the same unless-vs-priority-vs-defeater separation this article teaches.
Evidence and scope of the external example

Documented in docs/research/constructs/02-rule-defeasible-unless/corpus-forms.en.md, section 1 (rated exemplary there). Verification confirms the named construct at the cited lines only, by direct source read; it claims nothing about deployment, runtime behavior, or legal correctness.

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.