Terms: calendar, policy, and the business-day step
“Within ten business days” is neither a date nor a number. To turn these words into a day, three things must be known: which days are working, from which day to count, and what to do if the tenth day falls on a day off. The core guesses none of the three answers: the calendar is hashed data, the counting rule a declared policy, and the step a term that asks for both. Measured across the corpus: 52 packages count business days, 48 calendar families are pinned by locks.
The Archive of Veliky Ustin answers requests within ten business days, extends issue by a month, and requires visit notice three business days ahead. The archive takes working days from Kazakhstan’s official 2026 calendar.
language "law.core" version "0.2";package tutorial.archive version "0.6.0";namespace "urn:law:tutorial:archive";
entity Person;
source KZ_OFFICIAL_CALENDAR_2026 { kind law; jurisdiction KZ; number "calendar-2026";}
calendar archive_calendar_2026 { timezone "Asia/Almaty"; timezone_db "iana-tzdb@2026a"; period [@2026-01-01, @2027-01-01); source KZ_OFFICIAL_CALENDAR_2026; resource "artifact:corpus/clir/kz-official-2026.calendar.json"; format "law.calendar/0.1"; content_hash "sha256:a17b851ac08e622c05c116637ecf8d818bc7b480568f5560407d69461c4b8c21";}
deadline policy ARCHIVE_BUSINESS_DAYS { start_count next_day; include_end true; roll next_working_day;}
deadline policy ARCHIVE_CALENDAR_MONTHS { start_count same_day; include_end true; roll no_roll; month_end last_day_of_month;}
relation request_received(p: Person, on: Date) kind empirical { key(p); }relation reply_due(p: Person, due: Date) kind institutional;relation extension_requested(p: Person, on: Date) kind empirical { key(p); }relation extended_until(p: Person, until: Date) kind institutional;relation notice_sent(p: Person, on: Date) kind empirical { key(p); }relation visit_scheduled(p: Person, on: Date) kind empirical { key(p); }relation notice_timely(p: Person) kind institutional;relation digitization_requested(p: Person, on: Date) kind empirical { key(p); }relation digitization_due(p: Person, due: Date) kind institutional;relation large_order(p: Person) kind empirical;Three things without which there is no term
Section titled “Three things without which there is no term”Calendar is a snapshot: zone, zone-database version, coverage
period, source, and a dataset with the hash of its bytes. Holidays and
shifts are snapshot data, not machine settings. The runner finds the
dataset bytes via the package’s law.lock, or, for a bare program like
here, via the artifact: address upward from the test file; in both
cases it hashes the found bytes and accepts them only on a match with
content_hash. A package holds one snapshot, chosen by itself; with two
or more, the case must name it via the calendar axis, else
AMBIGUOUS_CALENDAR before any derivation.
Policy is how to count. Three fields are mandatory:
start_count — count from the event day or the next; include_end —
whether the last day counts; roll — where to move a term landing on
a non-working day. cutoff and month_end are optional. A policy is not
a norm’s property but a case input: the package declares it, and the
case selects it via the deadline_policy axis. Without it no term is
computed: MISSING_POLICY, and the norm stays silent.
Step is a term, and there are two. add_business_days takes
business_day and calendar_day and always asks the calendar, even at
no_roll. add_calendar_period takes calendar_day, calendar_week,
calendar_month, and calendar_year and never asks the calendar: a month
is a step to the same date, not a count of days. The split is not
stylistic: while the step was one, the snapshot requirement depended on
the policy, and one implementation asked the calendar where another
stayed silent, with green statics.
Ten business days
Section titled “Ten business days”rule ReplyDeadline strict { for p: Person; for on: Date; when request_received(p, on); then reply_due(p, add_business_days(on, 10 business_day));}The request arrived on Friday 6 March 2026. The next_day policy:
count from the 7th. Saturday and Sunday do not count; 8 March is
a holiday shifted to Monday the 9th. The first four business days are
10–13 March. Then 15 March, Constitution Day, a Sunday shifted to the
16th; 17–20 March make eight. Nauryz takes 21–25 March with shifts, and
the ninth and tenth days fall on 26 and 27 March.
| Date asked | Policy | reply_due |
|---|---|---|
| 27 March | business days | TRUE_ONLY |
| 26 March | business days | NEITHER |
| 27 March | not set | NEITHER, MISSING_POLICY |
The first row is the test from this page, byte for byte. Note the
deadline_policy axis in the context and the calendar axis: the second
is not mandatory here — the snapshot is one — but named for clarity.
test "запрос получен в пятницу 6 марта — ответ до 27 марта" { given { context { deadline_policy ARCHIVE_BUSINESS_DAYS; decision_time @2026-04-01T09:00:00+05:00; knowledge_time @2026-04-01T09:00:00+05:00; legal_time @2026-03-06; timezone "Asia/Almaty"; calendar archive_calendar_2026; } assert request_received(entity_ref("urn:tutorial:ivanova"), @2026-03-06) { id "assert-request"; origin case_input; } } evaluate truth(reply_due(entity_ref("urn:tutorial:ivanova"), @2026-03-27)); expect truth_status == TRUE_ONLY; expect evaluation_status == COMPUTED;}The test name reads: “Request received Friday 6 March — reply due by 27 March.”
The policy the test must declare itself with the same deadline policy
block — otherwise the reference will not resolve even at lowering
(LDC-E1332). And runtime resolves it against the program’s CLIR, so
the name in the test and the name in the package must match. The
ready-made test file for this page starts with the same two policies.
A month is another machine
Section titled “A month is another machine”Extending issue is by one month from the application date.
rule ExtensionTerm strict { for p: Person; for on: Date; when extension_requested(p, on); then extended_until(p, add_calendar_period(on, 1 calendar_month));}| Application | Policy | extended_until |
|---|---|---|
| 31 January | calendar months | 28 February, TRUE_ONLY |
| 31 January | business days | NEITHER, MISSING_POLICY |
The first row is about month_end. February has no 31st, and the core
does not choose for the act what to do about it: the policy must say
last_day_of_month or reject_nonexistent, and with month and year
units the field is mandatory. start_count decides here too: at
same_day a month from 31 January ends 28 February, at next_day —
1 March. Both classical readings are expressed by one field.
The second row is the most important. A business-day policy with roll next_working_day is rejected for a calendar step, not interpreted:
shifting to a working day is a working-calendar notion, and admitting it
here would bring the calendar back through the back door. The consequence
for authors: a case has one policy, and a case where a monthly term and
a business-day term meet in one question will not execute. Such questions
are asked separately — a norm needing both has composition: the
add_calendar_period result feeds add_business_days with zero business
days.
A backward count is a condition, not a date
Section titled “A backward count is a condition, not a date”Visit notice is due no later than three business days ahead. The
temptation to write add_business_days(visit, -3 business_day) is
great, and check will let it through. At runtime such a step answers
DEADLINE_POLICY_INVALID, and the refusal is not local: it rises to
the whole document, and neighbouring terms of the same case become
NEITHER without explanation. A norm about “no less than” judges an
already performed action, and is written as a comparison:
rule NoticeTimely strict { for p: Person; for sent, visit: Date; when notice_sent(p, sent) and visit_scheduled(p, visit) and days_between(add_business_days(sent, 3 business_day), visit) >= 0; then notice_timely(p);}| Notice | Visit | notice_timely |
|---|---|---|
| 18 March | 27 March | TRUE_ONLY |
| 18 March | 20 March | NEITHER |
days_between yields a whole number of calendar days and does not read
the calendar; it is fed the already business-day-computed boundary.
Duration in the core is only measured: the Duration and CalendarPeriod
types have no constructor, and 30 days is a magnitude incomparable with
a whole number.
A term with special cases
Section titled “A term with special cases”A digitization order is filled within ten business days, a large one
within twenty. A general term with named exceptions is the term
construction:
term DigitizationTerm { for p: Person; for on: Date; from digitization_requested(p, on); due digitization_due(p); default 10 business_day label ru official "общий срок исполнения заказа"; case Large when large_order(p): 20 business_day label ru official "крупный заказ";}The labels read: “general order-fulfilment term” and “large order”.
term has no node of its own: the compiler unfolds it into a defeasible
general-term rule, a special-case rule, an exception rule, and a priority
of the case over the default. That is why the layer view of this page
answers L1 — “a term with a defeasible default and special cases”.
The step unit is read at runtime: business_day follows the working
calendar, calendar_month a calendar step.
| Order of 6 March | Date asked | digitization_due |
|---|---|---|
| ordinary | 27 March | TRUE_ONLY |
| large | 10 April | TRUE_ONLY |
| large | 27 March | FALSE_ONLY |
The third row shows the special case does not merely win: the exception rule gives the general term a negative support, and the question “is the deadline 27 March?” for a large order is refuted, not left unanswered.
Compiler refusals
Section titled “Compiler refusals”A date does not add with a magnitude; a term is built by a step, not by arithmetic:
error LDC-E2108: «+»: несовместимые виды Date и Quantity — §58 требуетсовпадения видов для сложения и вычитанияThe diagnostic reads: “‘+’: incompatible kinds Date and Quantity — kinds must match for addition and subtraction”.
A policy without a mandatory field is a refusal at check, not
MISSING_POLICY at runtime; a repeated field too:
error LDC-E1307: политика §86 неполна: не заданы roll; на исполнении эточиталось бы как MISSING_POLICY («политики нет»)error LDC-E1207: поле политики "roll" задано дважды (§86)The diagnostics read: “policy is incomplete: roll unset; at runtime this would read as MISSING_POLICY (‘no policy’)” and “policy field ‘roll’ is set twice”.
A day count is whole, and it cannot be compared with a magnitude:
error LDC-E2108: «>=»: несравнимые виды Integer и Quantity — §57/§48требуют совпадения видов, неявных конверсий нетThe diagnostic reads: “‘>=’: incomparable kinds Integer and Quantity — kinds must match, no implicit conversions”.
A term holds exactly one general term:
error LDC-E0201: term содержит ровно одну клаузу `default` (DECISION-0101)The diagnostic reads: “‘term’ holds exactly one default clause”.
All five refusals belong to the language. The negative step cannot be
pinned the same way: check lets it through, and the lesson stays the
text above.
A term computed here is a date in a fact. In the duty
tutorial such a date became the aim window’s boundary;
now it need not be fed in the case but derived by a rule from the event
date. The norm’s own window — effective — stayed in the time
tutorial, and that is still another axis.
The exercise for this page is /tutorials/exercise-deadlines/.
Documentation for Arxo. Writings — blog.arxo.io.
Anonymous visit counts on stats.arxo.io, no cookies.