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