docs← Back to article

Markdown for LLMs

Periods and calendar: deadline policy, add_calendar_period, add_business_days

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

Download this articlePlain text ↗
# Periods and calendar: deadline policy, add_calendar_period, add_business_days

**In one sentence:** these constructs answer the question "when a
deadline falls". Take them when a norm names a period ("within a month",
"ten business days"): the period policy says how to count, and the
term counts; the calendar snapshot is attached only when the period runs in
business days.

Deadlines are calendar steps and business-day counts under a named period policy: a business-day count always pins the official calendar snapshot, a calendar step never does.

## 1. When to take it and when not to

| Instead of | Selection rule |
|---|---|
| `add_calendar_period` vs `add_business_days` | Period in calendar units (month, year, week, day-step) — `add_calendar_period`, no calendar snapshot is ever taken. Period in business days — `add_business_days`, the snapshot is always required, even with `no_roll` and even for `calendar_day`. The difference is in the term name; the `calendar_day` unit is allowed in both |
| Step vs measurement | "Deadline N ahead of a date" — a step (`add_calendar_period` / `add_business_days`). "How many days between dates" (penalty per day) — the `days_between` measurement: asks for no snapshot, counts no business days. Seven days as an interval is `days_between`, not a step (EU Regulation 261/2004, see `corpus-forms.md`) |
| `start_count same_day` vs `next_day` | Text counts the trigger day ("from the day") — `same_day`; text counts from the next day — `next_day`. Price of the mistake — exactly one day: 15.01 + 1 month gives 15.02 under `same_day` and 16.02 under `next_day` (checked against both policies in the second example package) |
| `month_end last_day_of_month` vs absent | A month/year step across a nonexistent date (31.01 + 1 month) — the policy is mandatory: `last_day_of_month` gives 28.02, absence — `MISSING_POLICY`, not a silent choice. The field is read only by the month/year unit |
| Calendar snapshot vs case fact | The jurisdiction's business calendar is pinned (a pinned dataset + `law.lock` line) — snapshot. Not pinned (an annual ministers' decision that does not exist) — the business-day count comes as a case fact, and comparison against the limit is a literal in the rule (halal certification, see `corpus-forms.md`): honest "we do not count", not an invented "weekends-only" calendar |

## Grammar: the deadline window as an interval (EBNF verbatim from the grammar)

A deadline window is an interval with value endpoints; a term boundary requires a named `deadline policy`, otherwise `MISSING_POLICY`. Goal-window syntax lives on the [duty](/constructs/duty/) pages; only period computation lives here:

```ebnf
interval_expression     = expression ;
interval_literal        = interval_left, interval_endpoint, ",",
                          interval_endpoint, interval_right ;
interval_left           = "[" | "(" ;
interval_right          = "]" | ")" ;
interval_endpoint       = expression | "infinity" | "-infinity" ;
```
## 2. Minimal example

Package `research.deadline.monthly`: a report filed on day `filed`, deadline — one
month by calendar step, `same_day` policy without roll.

```law
deadline policy CASE_POLICY {
    start_count same_day;
    include_end true;
    roll no_roll;
    month_end last_day_of_month;
}

rule MonthlyDue(r: Report, filed: Date) strict {
    label ru-KZ official "Срок — месяц со дня подачи";
    when filed_on(r, filed);
    then due_on(r, add_calendar_period(filed, 1 calendar_month));
}
```

Case 1: `filed_on(r1, @2026-01-15)`, context with `deadline_policy
CASE_POLICY`. Query: `evaluate truth(due_on(r1, @2026-02-15))`.
Case 2: `filed_on(r2, @2026-01-31)`; query `due_on(r2, @2026-02-28)`.

Observed engine answer (installed `law`, semantics `law.core/0.2`):

```text
law test research.deadline.monthly: мир research.deadline.monthly
  ok   [research.deadline.monthly#authored] tests/01-due.lawtest / urn:query:research-deadline-01
  ok   [research.deadline.monthly#authored] tests/02-month-end.lawtest / urn:query:research-deadline-02
итого: 2 проверено, 2 прошли, 0 не прошли, 0 не исполнены; код 0
```

`law engine check` — `check OK`, no warnings. The calendar step
takes no snapshot: the policy is read from the case environment (calendar
and policy come via the environment, not as an argument; the term arity is 2).
Sensitivity: removing `filed_on` gives `NEITHER` instead of `TRUE_ONLY`;
31 January gives 28 February only with `month_end last_day_of_month` —
without it the refusal is `MISSING_POLICY` (the two scenario tests).

Nearest wrong outcome (caught in trials): the same norm with
`start_count next_day` answers 16.02 instead of 15.02 — a one-day mistake with
no diagnostics, because both dates "look right". The second
package is devoted to it.

Second package `research.deadline.start` — same rule, two policies:

| Policy | Filing | Question | Answer |
|---|---|---|---|
| `same_day` | @2026-01-15 | `due_on(@2026-02-15)` | `TRUE_ONLY` |
| `next_day` | @2026-01-15 | `due_on(@2026-02-16)` | `TRUE_ONLY` |

## 3. Example by domain

- **Law:** French Civil Code (package `fr.code_civil`, France) — five-year limitation by step
  (`then echeance_prescription_le(c, add_calendar_period(depart, 5
  calendar_year))`, see `corpus-forms.md`); the policy is a line in
  the case context (`deadline_policy OECD_CODE_CIVIL_PRESCRIPTION`).
- **Standard/protocol:** EU Regulation 261/2004 (package `eu.transport.air_passenger_rights`, European Union) — the seven-day period
  is computed with `days_between`, not a step: no pinned source holds a calendar
  of non-business days for 27 member states, and opening an empty one would assert
  extras.
- **Religion/custom:** Indonesian halal certification (package `id.halal`, Indonesia) — business-day
  periods are NOT computed: the SKB calendar is not pinned, the count comes as a case
  fact.
- **Science/teaching case:** `research.deadline.start` — the `start_count` price
  of one day in miniature; the Turkish Constitution (package `tr.constitution`, Türkiye) — a moratorium year by step,
  not recomputed in days: a year is not measured in days without a leap-year
  decision.

## 4. How the engine answers

Table — observed runs of this directory's examples (installed
`law`, semantics `law.core/0.2`):

| Facts | Question | Answer | Why |
|---|---|---|---|
| filing 15.01, `same_day` policy | `due_on(15.02)` | `TRUE_ONLY` | step to the corresponding date |
| filing 31.01, `month_end last_day` | `due_on(28.02)` | `TRUE_ONLY` | nonexistent date — last day of the month |
| filing 15.01, `next_day` policy | `due_on(16.02)` | `TRUE_ONLY` | count from the next day |
| no filing fact | `due_on(…)` | `NEITHER` | rule did not fire |
| corpus term-window `[appointed, add_calendar_period(appointed, 60 calendar_day)]` | period by step | `TRUE_ONLY` with a policy, else `MISSING_POLICY` | a term boundary requires a named `deadline policy` (goal windows from the positions pages: 563 variable boundaries versus 3,929 literals) |

- The policy is a named package node (`deadline policy Name { … }`),
  lowering — a `deadline_policy` node with `contentHash`; the context carries only
  the reference; ambient policies and inline copies do not exist.
- Without a policy (set by neither case nor profile) — `MISSING_POLICY`: for a
  conclusion question this is a refusal status with `missing_inputs` and the rule
  address, not just an issue.
- The calendar step reads `start_count` from the policy, and the month/year
  unit additionally reads `month_end`; `roll` must be `no_roll`,
  `include_end` — `true`, otherwise `MISSING_POLICY`, not interpretation.
- "Period — a month, and a non-business day rolls to the next business one" is a
  composition: the `add_calendar_period` result feeds `add_business_days` with zero
  business days; the calendar is then pinned explicitly.
- Calendar questions do not read context axes: they take the date from the
  question itself.
- Month/year boundaries are the terms `month_start_of`/`month_end_of`,
  `year_start_of`/`year_end_of`: neither calendar nor
  policy is read; weekday — `weekday_of`.

## 5. Common mistakes

1. Wrong `start_count` — a one-day mistake with no diagnostics (caught by
   trials; `pitfalls.md`, item 1).
2. No `month_end` on a month step — `MISSING_POLICY`, not
   "last day" (`pitfalls.md`, item 2).
3. `business_day` on `add_calendar_period` (and vice versa) — `TYPE_ERROR`:
   units do not mix (`pitfalls.md`, item 3).
4. `add_business_days` without a snapshot — refusal: the snapshot is always
   required, even with `no_roll` (`pitfalls.md`, item 4).
5. A "weekends-only" snapshot instead of a real calendar — an answer to the
   dangerous side (earlier than real); honestly — a case fact (halal;
   `pitfalls.md`, item 5).
6. Policy as a third term argument — no: arity 2, environment
   (`LDC-E2116`; `pitfalls.md`, item 6).
7. `cutoff` outside the `HH:MM:SS` canon — `DEADLINE_POLICY_INVALID` without
   normalization (`pitfalls.md`, item 7).

## 6. References

- The official calendar is external context: it is read as data and pinned by digest, never computed.
- Neighbouring pages: [time](/constructs/time/) (law date and `effective`), [duty](/constructs/duty/) (goal windows), [lifecycle statuses](/constructs/lifecycle-statuses/) (duty deadlines).