Exceptions and priorities
Real rules have exceptions, and exceptions collide. The eligibility rule of Northbridge gets three of them: a permit is not issued while the applicant’s previous permit is suspended; an applicant with outstanding parking fines is refused; and a holder of a disability badge is eligible whether or not a vehicle is registered. What happens when a badge holder also has fines? The package below compiles, and every answer on this page is a scenario the checks run.
Open this package in the playground → — edit the rules and the scenarios of this page and run them in your browser.
The package
Section titled “The package”language "law.core" version "0.2";package demo.parking version "0.1.0";namespace "urn:law:demo:parking";
entity Applicant;
relation resident(a: Applicant) kind institutional;relation vehicle_registered(a: Applicant) kind institutional;relation permit_suspended(a: Applicant) kind institutional;relation outstanding_fines(a: Applicant) kind institutional;relation disability_badge(a: Applicant) kind institutional;relation permit_eligible(a: Applicant) kind institutional;The base rule is now defeasible: a conclusion that can be defeated. A
strict rule admits no exception at all, so a rule that is meant to have
one must say so — an unless clause on a strict rule is refused by the
check with LDC-E4110. (The older spelling default is still accepted
for defeasible, with warning LDC-E1308.) The unless clause is the
first exception — a proviso that withdraws the conclusion without
asserting the opposite.
rule PermitEligibility defeasible { for a: Applicant; when resident(a) and vehicle_registered(a); then permit_eligible(a); unless permit_suspended(a);}The second exception is a refusal: a rule whose conclusion is the
negation. Two defeasible rules with opposite conclusions are, on their
own, equal — the answer would keep both. A priority declaration says
which one wins and names the ground.
rule FinesRefusal defeasible { for a: Applicant; when outstanding_fines(a); then not permit_eligible(a);}
priority RefusalOverEligibility { prefer FinesRefusal over PermitEligibility; reason lex_specialis;}The third rule is another route to eligibility. No priority relates it to the refusal — deliberately, to show what the engine does with a conflict nobody has resolved.
rule BadgeEligibility defeasible { for a: Applicant; when disability_badge(a); then permit_eligible(a);}Four scenarios
Section titled “Four scenarios”The base case: Ann is a resident with a registered vehicle.
test "no exception applies" { given { context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; } assert resident(entity_ref("urn:demo:parking:ann")) { id "ann-resident"; origin case_input; } assert vehicle_registered(entity_ref("urn:demo:parking:ann")) { id "ann-vehicle"; origin case_input; } } evaluate truth(permit_eligible(entity_ref("urn:demo:parking:ann"))); expect truth_status == TRUE_ONLY; expect evaluation_status == COMPUTED;}The proviso: her previous permit is suspended. The unless clause blocks
the conclusion, and nothing asserts the opposite — the answer is
NEITHER, not a refusal.
test "the unless clause withdraws the conclusion" { given { context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; } assert resident(entity_ref("urn:demo:parking:ann")) { id "ann-resident"; origin case_input; } assert vehicle_registered(entity_ref("urn:demo:parking:ann")) { id "ann-vehicle"; origin case_input; } assert permit_suspended(entity_ref("urn:demo:parking:ann")) { id "ann-suspended"; origin case_input; } } evaluate truth(permit_eligible(entity_ref("urn:demo:parking:ann"))); expect truth_status == NEITHER; expect evaluation_status == COMPUTED;}The refusal: outstanding fines. Both rules fire; the priority makes the
refusal win, and the answer is FALSE_ONLY.
test "the refusal wins by priority" { given { context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; } assert resident(entity_ref("urn:demo:parking:ann")) { id "ann-resident"; origin case_input; } assert vehicle_registered(entity_ref("urn:demo:parking:ann")) { id "ann-vehicle"; origin case_input; } assert outstanding_fines(entity_ref("urn:demo:parking:ann")) { id "ann-fines"; origin case_input; } } evaluate truth(permit_eligible(entity_ref("urn:demo:parking:ann"))); expect truth_status == FALSE_ONLY; expect evaluation_status == COMPUTED;}The unresolved conflict: Bob holds a disability badge and has outstanding
fines. BadgeEligibility supports eligibility, FinesRefusal supports its
negation, and no priority ranks them. The engine keeps both sides — the
answer is BOTH.
test "two grounds, no priority: the conflict is preserved" { given { context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; } assert disability_badge(entity_ref("urn:demo:parking:bob")) { id "bob-badge"; origin case_input; } assert outstanding_fines(entity_ref("urn:demo:parking:bob")) { id "bob-fines"; origin case_input; } } evaluate truth(permit_eligible(entity_ref("urn:demo:parking:bob"))); expect truth_status == BOTH; expect evaluation_status == COMPUTED;}BOTH is an answer, not an error. It tells the author that the package
has two applicable norms and no rule for choosing between them — exactly
what a lawyer would want flagged rather than silently resolved by the
order the rules happen to be written in.
Run it
Section titled “Run it”law engine check parking.lawlaw engine test tests/exceptions.lawtest --program parking.lawExpected output:
check OK: parking.lawtest PASS: no exception appliestest PASS: the unless clause withdraws the conclusiontest PASS: the refusal wins by prioritytest PASS: two grounds, no priority: the conflict is preservedTry one change
Section titled “Try one change”Delete the priority RefusalOverEligibility { … } declaration and run the
scenarios again. The third scenario now ends in BOTH — the refusal and
the eligibility rule are equals, and the priority was the only thing that
ranked them:
test FAIL: the refusal wins by priority truth_status == FALSE_ONLY: в документе BOTHThen put the priority back and add a second one that prefers
FinesRefusal over BadgeEligibility. Bob’s answer becomes FALSE_ONLY.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.