# 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. ```law 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 **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 ```law 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. ```law 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 Extending issue is by one month from the application date. ```law 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 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: ```law 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 A digitization order is filled within ten business days, a large one within twenty. A general term with named exceptions is the `term` construction: ```law 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 A date does not add with a magnitude; a term is built by a step, not by arithmetic: ```text 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: ```text 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: ```text 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: ```text 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. ## Next A term computed here is a date in a fact. In [the duty tutorial](/tutorials/duty/) 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](/tutorials/time/), and that is still another axis. The exercise for this page is [/tutorials/exercise-deadlines/](/tutorials/exercise-deadlines/).