Facts and questions
One package, five kinds of question. The parking-permit rules of Northbridge grow a little: besides eligibility they now name a fee, a deadline for the town’s decision, and the duty to notify the applicant. Each question below is a scenario you can run; the package is compiled as part of publishing this page.
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;entity Office;
relation resident(a: Applicant) kind institutional;relation vehicle_registered(a: Applicant) kind institutional;relation permit_eligible(a: Applicant) kind institutional;relation application_filed(a: Applicant, on: Date) kind empirical { key(a); }relation decision_due(a: Applicant, due: Date) kind institutional;relation decision_notified(a: Applicant) kind empirical;relation permit_office(o: Office) kind institutional;Two new kinds of declaration: a constant with a typed value, and a pure function over it. Money carries its currency; a fee in a different currency would not add up with it.
const MONTHLY_RATE: Money = 10 EUR;
function permit_fee(months: Integer) -> Money = months * MONTHLY_RATE;Deadlines need a policy: does the count start on the day of the event or the next one, is the last day included, and what happens when the end falls on a non-working day. The policy is declared by the package and selected by the case — the package never assumes one.
deadline policy CALENDAR_DAYS { start_count next_day; include_end true; roll no_roll;}Three rules. The first is the eligibility rule from Your first rule. The second derives a date: the decision is due thirty calendar days after filing. The third derives not a fact but a position — a duty of the permit office towards the applicant, with a goal and a window in which the goal must be achieved.
rule PermitEligibility strict { for a: Applicant; when resident(a) and vehicle_registered(a); then permit_eligible(a);}
rule DecisionDeadline strict { for a: Applicant; for on: Date; when application_filed(a, on); then decision_due(a, add_calendar_period(on, 30 calendar_day));}
rule DecisionDuty strict { for a: Applicant; for o: Office; for on, due: Date; when application_filed(a, on) and decision_due(a, due) and permit_office(o); then duty NotifyDecision { bearer o; beneficiary a; goal achievement { condition decision_notified(a); window [on, due]; } };}A fact is an assert line in the case: a predicate applied to values.
entity_ref("…") names a thing by its identifier; dates are written
@2026-03-02; amounts are 30 EUR. Every fact has an id, so the proof
of an answer can point at it, and an origin — here case_input, a fact
submitted with the case. A fact you do not state is simply unknown: the
engine never fills in a default.
Five questions
Section titled “Five questions”Truth — is a statement established?
test "truth: is Ann eligible?" { 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;}Collect — which values satisfy a condition? Bob is a resident without a registered vehicle, so the answer is the set with Ann alone.
test "collect: who is eligible?" { 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 resident(entity_ref("urn:demo:parking:bob")) { id "bob-resident"; origin case_input; } } evaluate collect a: Applicant where permit_eligible(a); expect result_kind == COLLECTION; expect collected_count(1); expect collected(entity_ref("urn:demo:parking:ann"));}Calc — the value of a term. A calculation needs no facts, only the context.
test "calc: the fee for three months" { given { context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; } } evaluate permit_fee(3); expect value == 30 EUR;}Deadline — a date derived by a rule under a policy. The case selects
the policy in its context; filed on 2 March, counting from the next day,
thirty days end on 1 April, and the due date is 2 April.
test "deadline: the decision is due thirty days after filing" { given { context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; deadline_policy CALENDAR_DAYS; } assert application_filed(entity_ref("urn:demo:parking:ann"), @2026-03-02) { id "ann-filed"; origin case_input; } } evaluate truth(decision_due(entity_ref("urn:demo:parking:ann"), @2026-04-02)); expect truth_status == TRUE_ONLY; expect evaluation_status == COMPUTED;}Leave deadline_policy out of the context, and the same question is not
computed at all: the answer carries evaluation_status == MISSING_POLICY
instead of a guessed date.
test "deadline without a policy is not computed" { given { context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; } assert application_filed(entity_ref("urn:demo:parking:ann"), @2026-03-02) { id "ann-filed"; origin case_input; } } evaluate truth(decision_due(entity_ref("urn:demo:parking:ann"), @2026-04-02)); expect evaluation_status == MISSING_POLICY;}Positions — which duties, permissions and powers are in force in this
case, and in what state. On 10 March the office’s duty to notify Ann is
ACTIVE: the window is open and the goal is not yet achieved.
test "positions: the office owes Ann a decision" { given { context { legal_time @2026-03-10; decision_time @2026-03-10T09:00:00Z; knowledge_time @2026-03-10T09:00:00Z; timezone "UTC"; deadline_policy CALENDAR_DAYS; } assert application_filed(entity_ref("urn:demo:parking:ann"), @2026-03-02) { id "ann-filed"; origin case_input; } assert permit_office(entity_ref("urn:demo:parking:office")) { id "office"; origin case_input; } } evaluate positions(); expect position(NotifyDecision, ACTIVE); expect not position(NotifyDecision, VIOLATED);}Run it
Section titled “Run it”Save the package as parking.law and the six scenarios, preceded by the
three header lines, as tests/questions.lawtest:
law engine check parking.lawlaw engine test tests/questions.lawtest --program parking.lawExpected output:
check OK: parking.lawtest PASS: truth: is Ann eligible?test PASS: collect: who is eligible?test PASS: calc: the fee for three monthstest PASS: deadline: the decision is due thirty days after filingtest PASS: deadline without a policy is not computedtest PASS: positions: the office owes Ann a decisionTry one change
Section titled “Try one change”In the positions scenario move legal_time to @2026-05-01 (and the two
other times with it). The window closed on 2 April, but the case says
nothing about whether Ann was notified — so the duty is neither active nor
violated. Its status is UNDETERMINED, and the test fails on its first
expectation:
test FAIL: positions: the office owes Ann a decision position(urn:law:demo:parking#NotifyDecision, ACTIVE): в документе нетNow add an explicit negative fact to the same scenario:
assert not decision_notified(entity_ref("urn:demo:parking:ann")) { id "ann-not-notified"; origin case_input; }With the failure established, the duty is VIOLATED and both expectations
fail. Assert the positive fact instead (assert decision_notified(…)), and
the duty is SATISFIED. Silence, denial and confirmation are three
different inputs; the next pages show how the same distinction works for
ordinary facts.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.