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.
# 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).