# calendar@0.1

**Status:** normative. **Inherits:** [`../conventions.md`](../conventions.md) — envelope,
preservation, safe rendering, and shared diagnostics. It has no items and does not inherit the
shared item model: its bullet entries are events, not tasks. **Schema:** [`schema.json`](schema.json).
**Fixtures:** [`fixtures/`](fixtures/). **Owns diagnostic codes:** `MDTO900`–`MDTO999`.

**Companion skill:** [Authoring and editorial guidance](https://markdownto.ai/specs/calendar.skill.md) (non-normative).

---

## 1. Purpose

`calendar@0.1` describes a **portable calendar**: level-2 headings are calendar dates and the bullet
entries beneath them are events. The source reads like an agenda in any Markdown viewer; the HTML
adds month and agenda views, and the export verb produces a standards-compatible iCalendar file.

---

## 2. Grammar

### 2.1 Envelope

```yaml
---
markdownto: calendar@0.1
title: Studio calendar
timezone: America/Los_Angeles
---
```

`timezone` is optional and defaults to `UTC`. When present it MUST name an IANA time zone
(`MDTO910`). It is emitted as `TZID` in iCalendar output; local event times never silently become
the renderer's machine time.

### 2.2 Days

`## YYYY-MM-DD` opens a **day**. At least one day is required (`MDTO900`). The date must be real
(`MDTO901`) and may occur only once (`MDTO902`). An event before the first day is `MDTO903`. One
optional leading `#` title heading is allowed; every other heading level is `MDTO911`.

### 2.3 Events

An event is an ordinary bullet list item with a readable label and inline direction fields:

```markdown
- Design review [start:: 09:00] [end:: 10:30] [location:: North studio] [notes:: Bring the revised floor plan.]
- Company retreat [all-day:: true] [location:: Mendocino]
```

Ordered-list events and task-list checkboxes are `MDTO904`: Calendar events use plain unordered
bullets and never borrow completion state from the shared task model.

| Field | Required | Meaning |
|---|---:|---|
| `[start:: HH:MM]` | timed events | Local start time on the heading date. |
| `[end:: HH:MM]` | timed events | Local end time, later than start. |
| `[all-day:: true]` | all-day events | Replaces start/end; `false` is equivalent to omission. |
| `[location:: text]` | no | Display location and iCalendar `LOCATION`. |
| `[notes:: text]` | no | Plain-text event details, revealed on selection and exported as iCalendar `DESCRIPTION`. |
| `[url:: value]` | no | An absolute `https` value becomes a link and iCalendar `URL`; anything else stays visible but inert. |

Direction fields are removed from the displayed label but preserved in `raw`. A field-only event is
`MDTO905`; a timed event missing start or end is `MDTO906`; a malformed time is `MDTO907`; end not
later than start is `MDTO908`; and `all-day` accepts only `true` or `false` (`MDTO909`). Unknown
fields remain preserved and inert.

`notes` is deliberately a one-line inline field in 0.1. Its value cannot contain a line break or
`]`, which closes the field. The HTML event sheet exposes **Edit notes**, **Save**, and **Cancel**:
Save performs the same source-preserving `notes` mutation as the CLI. A host MUST accept it only
when its canonical source is still byte-for-byte the source the event sheet edited (the CLI uses
the equivalent source-hash check), and the sheet MUST wait for that acknowledgement before claiming
success. A conflict leaves canonical Markdown untouched, keeps the proposed note visible, and tells
the author to reopen against current source; Cancel changes no byte. A standalone render is itself
a capable local host: its in-page source and regenerated Markdown download become the canonical
session artifact. A host that remounts the rendered frame after its own source changes MUST preserve
and restore any dirty note draft into the matching date and event, or explicitly confirm its
discard; destroying a draft as an iframe side effect is not allowed. Before acknowledging a
writeback, a host MUST also reparse the proposed source and verify that the addressed event carries
the requested note. Any other host that cannot accept writeback MUST leave the editor unavailable
rather than keep a second, session-only copy. The event is addressed by authored date plus its
one-based position beneath that date; the mandatory conflict check makes a concurrent reorder a
refusal rather than an edit to the wrong event.

URL safety is a rendering/export policy rather than a validity rule: an unfamiliar scheme may be
meaningful to another consumer, so the core preserves it, while this implementation emits and links
only absolute `https` values. Raw source never becomes executable markup.

### 2.4 Canonical IR and iCalendar

The IR sits at `result.calendar`, beside `result.document`. It carries the effective timezone,
ordered days, typed events (including optional notes), complete frontmatter, and source positions.
When an event has notes, `notesPosition` is the exact authored `[notes:: …]` field span; editors use
that parser-derived address so field-looking text inside Markdown links, images, or code is never
mistaken for the mutable field. A timed event carries local
`start` and `end` strings; an all-day event carries `null` for both. Calendar rendering never guesses
missing schedule.

`export` emits RFC 5545-style VCALENDAR text with deterministic UID values derived from source
identity, date, and event position. It uses `VALUE=DATE` for all-day events and `TZID` local values
for timed events. CRLF line endings and text escaping are fixed; no network or account is involved.

### 2.5 Diagnostics

| Code | Severity | Meaning |
|---|---|---|
| `MDTO900` | error | Document has no day heading. |
| `MDTO901` | error | Day heading is not a real full-date. |
| `MDTO902` | error | Day heading is duplicated. |
| `MDTO903` | error | Event appears before the first day. |
| `MDTO904` | error | Event is ordered or uses a task-list checkbox instead of a plain bullet. |
| `MDTO905` | error | Event has no display text. |
| `MDTO906` | error | Timed event is missing start or end. |
| `MDTO907` | error | Event time is not `HH:MM`. |
| `MDTO908` | error | Event end is not later than start. |
| `MDTO909` | error | `all-day` is not `true` or `false`. |
| `MDTO910` | error | `timezone` is not an IANA time zone. |
| `MDTO911` | error | Heading level is invalid. |

---

## 3. Rationale

**Why dates are headings.** A week remains skimmable as prose, a date is stable under reordering, and
month layout derives without hidden state. Headings say *when* at the day grain; fields say the
smaller schedule.

**Why local time plus an IANA zone.** Meetings are usually authored as wall-clock commitments. An
explicit IANA zone preserves daylight-saving meaning across future dates and lets `.ics` consumers
do the conversion instead of freezing today's offset into tomorrow's event.

**Why events are not shared-model items.** A calendar entry is not completed by checking it off.
Giving a checkbox cancellation or attendance semantics would redefine the shared task model; plain
bullets keep the distinction honest.

**Why iCalendar export is one-way.** The `.md` file remains canonical. Importing a changed `.ics`
would need identity and recurrence conflict rules that 0.1 deliberately does not pretend to solve.
Editing an authored event's existing `notes` field is different: it is one local, source-preserving
operation with an exact address and conflict hash, so the rendered view can offer it without making
the HTML or `.ics` file another authority.

---

## 4. Examples

```markdown
---
markdownto: calendar@0.1
title: Studio week
timezone: America/Los_Angeles
---

## 2026-08-10
- Lighting review [start:: 09:00] [end:: 09:45] [location:: North studio] [notes:: Compare the warm and neutral scenes.]
- Open house [start:: 18:00] [end:: 20:00]

## 2026-08-11
- Team offsite [all-day:: true]
```

`- Direction only [start:: 18:00]` is `MDTO906`: a timed event needs an end.

---

## 5. Verb reference

`calendar@0.1` owns the vocabulary **export, notes**.

| Verb | Signature | Source edit |
|---|---|---|
| `export` | `mdto calendar export [--out <file.ics>] [--stdout]` | None. Writes a deterministic `.ics` artifact or prints it. |
| `notes` | `mdto calendar notes <date> <event> (--set <text> \| --clear)` | Add, replace, or remove only the selected event's `[notes:: value]` field. |

`mdto calendar export` accepts `--out <file.ics>`, `--stdout`, and `--json`. The default output is
the Markdown basename with `.ics`. `mdto calendar notes` uses a one-based event number within the
authored day and accepts the shared mutation flags `--dry-run`, `--json`, `--expect`, and `--force`;
`--set` and `--clear` are mutually exclusive and exactly one is required. HTML month/agenda toggles
change only presentation and keep no canonical state; saving notes changes the Markdown source.

### Common flags

`--file <path>` · `--out <file.ics>` · `--stdout` · `--set <text>` · `--clear` · `--dry-run` ·
`--expect <hash>` · `--force` · `--json` · `--help`
