Exceptions and conflicting rules
Situation
Section titled “Situation”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.
Prerequisites
Section titled “Prerequisites”- nb-01: First permit: facts, a rule and a question — writing a fact and a rule, running the first test.
- nb-02: Why a missing fact is not a refusal —
the four truth statuses (
TRUE_ONLY,FALSE_ONLY,BOTH,NEITHER) and explicit negation.
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.
Minimal example
Section titled “Minimal example”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):
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):
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):
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.
Command and result
Section titled “Command and result”The suite packs/examples/language-demo/permits contains one test per
mechanism. Command (run from the repository root):
law test packs/examples/language-demo/permitsObserved result (exception and conflict lines; full run: 28 checked, 28 passed, 0 failed):
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 не исполнены; код 0Each test asks the same shape of question —
truth(permit_eligible(...)) — with the facts and outcomes below
(tests/permits.lawtest, expect truth_status == ...):
| Test | Facts on file | Result |
|---|---|---|
unless removes the conclusion | resident + vehicle + suspended (Ann) | NEITHER |
denial wins by priority | resident + vehicle + fines (Ann) | FALSE_ONLY |
without priority the conflict persists | badge + fines (Bob) | BOTH |
defeater removes support without negation | badge + 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.
Why this construct
Section titled “Why this construct”The task each mechanism solves:
defeasiblemarks 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.unlesshandles 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.priorityresolves 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 fromBOTHtoFALSE_ONLY.defeaterblocks 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 isNEITHER— 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.
Changed condition
Section titled “Changed condition”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:
ok [demo.northbridge.permits] tests/permits.lawtest / without priority the conflict persistswith 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.
Typical mistake
Section titled “Typical mistake”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.
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.
Limits
Section titled “Limits”- A
priorityneeds explicit grounds:prefer X over Ywith areason. Without the block, conflicting defeasible rules yieldBOTH— the engine never invents a ranking. - A
defeateris not a negation.defeat permit_eligible(a)removes support; it never producesFALSE_ONLYby itself. Do not use it where a refusal must be recorded. unlessbelongs 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, semanticslaw.core/0.2, std0.2.0. Refusal surfaces (e.g. what the engine rejects) are facts about this implementation, not claims about the language in general.
Exercise
Section titled “Exercise”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:
- What is the status of
truth(permit_eligible(bob)), and which test name andexpectline say so? - Which single package element explains why Ann’s fines case on the
resident route answers
FALSE_ONLYinstead, and what would Ann’s answer become if that element were deleted?
The solution lives only in the nb-03 solution.
Sources
Section titled “Sources”- Teaching package:
packs/examples/language-demo/permits/package.law(rulesPermitEligibility,FinesRefusal,BadgeEligibility, priorityRefusalOverEligibility, defeaterFraudBlocksBadge). - 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 plusprioritywith a stated reason; when evidence is tainted but nothing is decided against the person, use adefeater. - 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, constructrule ... defeasiblewith a contrary proviso (WorkIsExcludable: excludableunless 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 sameunless-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.