docs← Back to article

Markdown for LLMs

Terms: calendar, policy, and the business-day step

The source Markdown for this article. Copy it into your assistant or download it as a text file.

Download this articlePlain text ↗
# 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/).