Exercise 16. An extension moves the term, not cancels the duty
An exercise for the duty page. On the page an extension removes the document-return duty, which is imprecise: an extension grants a new term, and the duty lives until it.
Setup. Replace the “extension granted” fact with a dated “extended until” fact, and write a second duty: return the document by the new date. The overdue fine must count from the new date. Expectations: with an issue due 31 March and an extension to 30 April, a 5 May non-return yields a fine, a 15 April non-return does not; on 10 April the duty to return by the extended term is active, the original one is not.
Hint. The unless clause must bind the extension date itself. The
fine reads two dates, and the comparison goes against the later one.
Below is the solution. Try it yourself first.
Solution
Section titled “Solution”language "law.core" version "0.2";package tutorial.archive version "0.5.1";namespace "urn:law:tutorial:archive";
entity Person;
relation document_lent(p: Person, issued: Date, due: Date) kind empirical { key(p); }relation document_returned(p: Person) kind empirical;relation extension_granted_until(p: Person, until: Date) kind empirical { key(p); }relation non_return_recorded(p: Person, on: Date) kind empirical { key(p); }relation extension_granted(p: Person) kind institutional;relation late_fee_due(p: Person) kind institutional;The original duty is removed by the extension, as on the page, but
unless now reads the date:
rule ReturnRule defeasible { for p: Person; for issued, due, until: Date; when document_lent(p, issued, due); then duty Return { bearer p; goal achievement { condition document_returned(p); window [issued, due]; } }; unless document_lent(p, issued, due) and extension_granted_until(p, until);}The second duty, with a window to the new date:
rule ReturnAfterExtension strict { for p: Person; for issued, due, until: Date; when document_lent(p, issued, due) and extension_granted_until(p, until); then duty ReturnExtended { bearer p; goal achievement { condition document_returned(p); window [issued, until]; } };}
rule NonReturnRecorded strict { for p: Person; for on: Date; when non_return_recorded(p, on); then not document_returned(p);}The fine is two norms, one per term. Without an extension it counts from the original term; with one from the new, and the first norm stays silent under an extension. For the first norm to ask “no extension”, the dated fact folds into a dateless flag:
rule ExtensionExists strict { for p: Person; for until: Date; when extension_granted_until(p, until); then extension_granted(p);}
rule LateReturnFee strict { for p: Person; for issued, due, on: Date; when document_lent(p, issued, due) and non_return_recorded(p, on) and on > due and not_known(extension_granted(p)); then late_fee_due(p);}
rule LateReturnFeeAfterExtension strict { for p: Person; for issued, due, until, on: Date; when document_lent(p, issued, due) and extension_granted_until(p, until) and non_return_recorded(p, on) and on > until; then late_fee_due(p);}The first test, on the fine past the extended term:
test "невозврат после продлённого срока — штраф" { given { context { decision_time @2026-06-01T09:00:00+05:00; knowledge_time @2026-06-01T09:00:00+05:00; legal_time @2026-05-10; timezone "Asia/Almaty"; } assert document_lent(entity_ref("urn:tutorial:ivanova"), @2026-03-01, @2026-03-31) { id "assert-lent"; origin case_input; } assert extension_granted_until(entity_ref("urn:tutorial:ivanova"), @2026-04-30) { id "assert-extension"; origin case_input; } assert non_return_recorded(entity_ref("urn:tutorial:ivanova"), @2026-05-05) { id "assert-non-return"; origin case_input; } } evaluate truth(late_fee_due(entity_ref("urn:tutorial:ivanova"))); expect truth_status == TRUE_ONLY; expect evaluation_status == COMPUTED;}The test name reads: “Non-return past the extended term — a fine.”
Three tests in tests/46-exercise-duty.lawtest: the fine past the new
term, positions on 10 April (ReturnExtended active, Return gone),
and a pre-new-term non-return without a fine. The positions test is shown
not on the page but in the file: the position(...) observation does not
compile into the package.
Why the extension_granted flag. Asking
not_known(extension_granted_until(p, until)) will not do: until is
bound by nothing in this norm, and with due substituted the question
would become “no extension to exactly the old term”, which an extension
to any other date would not defeat. This page’s first version was written
exactly so, and the third test caught it: the fine accrued on a 15 April
non-return, inside the extended term.
What the exercise teaches. An extension is not a cancellation but a new duty with a new window, and the fine must follow the window. Positions show this more directly than facts: on 10 April one duty is active, the other removed.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.