Skip to content

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.

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.

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);
}

Save the package as parking.law and the six scenarios, preceded by the three header lines, as tests/questions.lawtest:

Terminal window
law engine check parking.law
law engine test tests/questions.lawtest --program parking.law

Expected output:

check OK: parking.law
test PASS: truth: is Ann eligible?
test PASS: collect: who is eligible?
test PASS: calc: the fee for three months
test PASS: deadline: the decision is due thirty days after filing
test PASS: deadline without a policy is not computed
test PASS: positions: the office owes Ann a decision

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.