# Markdown To — shared conventions

**Status:** normative. **Applies to:** every Markdown To specification.

This document defines the rules that all Markdown To specs share: the base grammar, the
frontmatter envelope, item identity, inline metadata, tags, preservation guarantees, the
diagnostic code scheme, and the conformance-fixture contract. A spec document (`specs/<name>/SPEC.md`)
defines only what is particular to that spec and inherits everything here.

The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD NOT**, and **MAY** are to be
interpreted as described in RFC 2119.

---

## 0. Governing principle

> **A conforming file must be a joy to hand-author.**

Every rule below is subordinate to that. Where a design choice traded machine convenience against
human comfort, human comfort won: there is no HTML-comment metadata, no required IDs, no generated
preamble, no field a person would not willingly type. Where syntax was needed, we adopted vocabulary
that already exists in the Obsidian/Dataview ecosystem rather than inventing our own — humans may
already know it, agents certainly do, and conforming files open meaningfully in Obsidian for free.

Two consequences that implementers must internalize:

1. **A file a person wrote by hand, with no tooling ever having touched it, is fully conforming.**
   Tooling adds nothing on read. It adds the minimum on write.
2. **Nothing in a conforming file exists for the machine's benefit alone.** If a construct would only
   ever be produced by a tool and never typed by a person, it does not belong in a Markdown To spec.

A complete conforming file, for reference — this is the whole format on one screen:

```markdown
---
markdownto: kanban@0.1
title: Markdown To launch
---

## Backlog

- [ ] Define the format schema [priority:: high]
- [ ] Publish conformance fixtures #infra

## Doing

- [ ] Build the patch engine ^t3

## Done

- [x] Choose the portable envelope
```

Exactly one card carries an identifier, because exactly one card has been touched by tooling.

---

## 1. Base grammar

**Decision: GitHub Flavored Markdown (GFM) is the normative base grammar for every Markdown To spec.**

GFM is a strict superset of CommonMark; its task lists and tables are load-bearing (task lists *are*
the item model). A conforming file MUST be a valid GFM document. Implementations MUST parse with a
GFM-compliant parser and MUST NOT depend on parser-specific extensions beyond GFM plus the additions
defined in this document (block identifiers, inline fields, tags).

**Decision: exactly one spec per file.**

The frontmatter envelope names exactly one spec, and the whole file is interpreted under that spec.
Files do not compose (no "kanban section inside a todo file"): composition makes conformance
untestable, makes diagnostics ambiguous, and makes the patch engine's job undecidable. A project that
needs two specs uses two files.

---

## 2. The envelope

A conforming file MUST begin with a YAML frontmatter block: a line containing exactly `---` as the
**first line of the file**, a YAML mapping, and a closing `---` line.

```markdown
---
markdownto: kanban@0.1
title: Markdown To launch
---
```

### 2.1 Required key

- `markdownto` — the spec identifier, a string of the form `<name>@<major>.<minor>`.
  - `<name>` matches `^[a-z][a-z0-9-]*$`.
  - `<major>` and `<minor>` are non-negative integers without leading zeros.
  - The value MUST be a scalar string. A sequence or mapping is malformed (`MDTO004`) — this is how
    "one spec per file" is enforced mechanically.

### 2.2 Reserved keys

These keys have the same meaning in every spec and MUST NOT be redefined by a spec:

| Key | Type | Meaning |
|---|---|---|
| `markdownto` | string | Spec identifier. Required. |
| `title` | string | Human title for the document. Optional. |
| `id` | string | Stable document identifier, for tools that track files across renames. Optional. |

A spec MAY define additional keys — `kanban` defines `columns` and `done-column`, `narrate` defines
`voice`, `pace`, and `pronunciations`, `backlog` defines `bands`, `countdown` defines `target`,
`precision`, and `completion-message`, `pdf` defines its page and publication settings, and
`calendar` defines `timezone`, while `flashcards` and `timeline` add no frontmatter keys; each
`podcast` defines `speakers`, `style`, `language`, and `pronunciations`; each
spec's own §2.1 is the
authority on its list. All other keys are **unknown keys** and are governed by §6: parsed, preserved,
and written back verbatim. This is what lets a Markdown To envelope coexist with other systems'
frontmatter in one block — e.g. an agentsFS page carrying `description:` and `agentsfs_role:`
alongside `markdownto:` loses nothing on round-trip.

`guided-narration@0.1` additionally defines `source` (string): an HTTPS URL or a relative Markdown path.

The publication/application specs additionally reserve `theme`, `aspect`, `background`, `color`, `accent`, `font`, and `font-size` (slides),
`author`, `language`, `identifier`, `cover`, and `modified` (epub), and `pass` (quiz).
These keys have meaning only in their declaring spec. Form and matrix add no frontmatter keys.

### 2.3 Versioning

- `<major>` changes on any breaking change. An implementation MUST reject a file whose major version
  it does not implement (`MDTO006`).
- `<minor>` changes only additively: a file valid under `name@X.N` MUST remain valid under `name@X.M`
  for `M > N`. An implementation that reads a file declaring a **newer** minor version than it
  implements MUST accept it, MUST preserve constructs it does not understand, and SHOULD report
  `MDTO008` (warning).
- The file declares the version it was authored against. Implementations MUST NOT rewrite it as a
  side effect of an unrelated edit.

### 2.4 Filenames

`.todo.md`, `.kanban.md`, `.narrate.md`, `.backlog.md`, `.countdown.md`, `.gantt.md`, `.pdf.md`, and
`.calendar.md`, `.flashcards.md`, and `.timeline.md` — one per file spec — are the conventional
suffixes and aid discovery. They are
advisory only: **the envelope, never the filename, is authoritative.** A conforming file may be
named anything, which is why `backlog@0.1` also defines a fallback for finding one by its envelope
(backlog §5).

---

## 3. Item identity — lazy IDs

Most Markdown To specs address a repeated unit (a todo item, a kanban card). Identity works the same
way in all of them.

**No item carries an ID until something needs one.** An untouched item is addressed by its content
and position. A hand-written file therefore contains zero identifier noise — this is the single most
visible expression of §0.

When tooling first needs to reference or mutate an item durably, it **pins** an Obsidian-style block
identifier at the end of the item's line:

```markdown
- [ ] Build the patch engine ^t3
```

### 3.1 Grammar

- An identifier is `^` followed by `[A-Za-z0-9][A-Za-z0-9-]{0,63}`.
- It MUST be the **final token of the line on which the item's checkbox appears**, separated from the
  preceding text by at least one space.
- It MUST be unique within the file.
- A token at end of line matching `\^\S+` that does not match the identifier grammar is an error
  (`MDTO010`), not literal text: it is unambiguously an attempted identifier and silently treating it
  as prose would let tooling mint a colliding ID later.
- A token matching the identifier grammar that appears anywhere other than end of line is **literal
  text**, and SHOULD raise `MDTO012` (warning) because the author probably meant an identifier.

### 3.2 Rules for tooling

- IDs are **stable once pinned**. Tooling MUST NOT renumber, normalize, or garbage-collect them.
- Tooling MUST NOT pin an ID on an item it did not touch.
- Addressing: pinned items are addressed by ID; unpinned items by content hash plus position. A verb
  that must return a durable handle pins an ID as part of its edit and says so.
- Suggested minting scheme (not normative): a short spec-local prefix plus an integer, e.g. `^t1`,
  `^t2`. Tooling MUST verify uniqueness before writing.
- A spec MAY recommend a different *style* for the identifiers its authors write by hand:
  `backlog@0.1` recommends kebab slugs (backlog §2.8), because its blockers are hand-written
  references and `[[#^offline-cache]]` reads where `[[#^t7]]` only resolves. A style recommendation
  never narrows the grammar above and never carries a diagnostic.

---

## 4. Inline fields

Typed metadata uses **Dataview-style inline fields**:

```markdown
- [ ] Ship fixtures [priority:: high] [due:: 2026-08-20]
```

### 4.1 Grammar

- A field is `[`, a key, `::`, a value, `]`.
- Key: `^[A-Za-z][A-Za-z0-9_-]*$`, matched **case-insensitively** and normalized to lowercase in the IR.
- Value: any run of characters up to the closing `]`, with leading and trailing whitespace trimmed.
  Nested `]` is not supported in 0.1; a value that needs one uses a code span, which is not scanned
  (§4.5).
- Whitespace is permitted around the key and after `::` (`[due ::  2026-08-20]` is well-formed).
- The empty value (`[owner:: ]`) is an error (`MDTO021`). Omit the field instead.
- A key MUST NOT appear twice on the same item (`MDTO023`). Two values with no defined precedence is
  ambiguity, and Markdown To does not guess.
- Fields MAY appear anywhere in the item's text. Placing them at the end is conventional and SHOULD
  be what tooling produces.

### 4.2 Near-miss detection

A bracketed span whose contents match `^\s*[A-Za-z][A-Za-z0-9_-]*\s*:{1,3}` but which is not a
well-formed field (single colon, triple colon, missing close) is reported as `MDTO020` (warning) and
treated as literal text. Hand-authors write `[due: 2026-08-20]` constantly; a silent no-op would be
the worst possible outcome.

### 4.3 Shared field vocabulary

Specs that use the shared item model (`todo`, `kanban`, `backlog`, `gantt`) recognize these keys with
these types (`backlog` additionally recognizes `reason`, free text; `gantt` adds its schedule fields):

| Key | Type | Notes |
|---|---|---|
| `priority` | `low` \| `medium` \| `high` | Case-insensitive; normalized to lowercase. |
| `due` | `YYYY-MM-DD` | RFC 3339 `full-date`. MUST be a real calendar date. No time component in 0.1. |
| `owner` | string | Free text. A leading `@` is conventional and is preserved verbatim. |

A value that fails its type is `MDTO022`. **The field is not dropped** — it stays in the source and in
the IR's preserved field map, so a repair never loses the author's data.

### 4.4 Unknown fields

Any other key is an **unknown field**: not an error, not a warning. It is parsed, carried in the IR as
a string, and written back verbatim (§6). Specs extend by promoting an unknown field to a known one,
which is an additive minor version change.

### 4.5 Where fields are not recognized

Inside inline code spans and fenced/indented code blocks, **nothing** is scanned — no fields, no
tags, no identifiers. Fenced code is data. This is what makes it possible to write documentation
about Markdown To *in* a Markdown To file.

---

## 5. Tags and wikilinks

- A tag is `#` followed by `[A-Za-z0-9_/-]+`, preceded by the start of the text run or by whitespace.
- A tag MUST contain at least one non-digit character. `#1234` in "Fix issue #1234" is therefore
  literal text, not a tag — this rule exists because issue references are extremely common in
  hand-written task lists.
- `/` denotes hierarchy (`#home/errands`), matching Obsidian.
- A `#` at the start of a line followed by a space is an ATX heading and never a tag.
- Tags are stored in the IR **without** the leading `#`: the sigil marks the tag, it is not part of
  the tag's text. (This has behavioural weight — e.g. narrate reads the word, never the `#`.)
- Tags remain part of the item's rendered text (§7.2). They read as part of the sentence; fields do not.

Document-level configuration lives in frontmatter, never in tags or inline fields.

### 5.1 Wikilinks

`[[Name]]` and `[[Name#^blockid]]` are the shared cross-reference syntax, adopted from the Obsidian
ecosystem like block IDs (§3) and inline fields (§4):

- `[[Name]]` references a document by **filename without extension**, path-independent — reorganizing
  files never breaks links. `[[#^blockid]]` references an item in the same file; `[[Name#^blockid]]`
  references an item in another file. Disambiguate duplicate names with a path suffix: `[[work/Notes]]`.
- Wikilinks are **inert in the core**: they carry no validation obligation and no behaviour. They are
  recognized — surfaced by the parser as references with source positions, alongside the document IR
  — so that spec-owned relations and link-aware tools can build on them. A spec that assigns meaning
  to wikilinks defines that meaning, including whether an unresolvable target is a diagnostic.
  `backlog@0.1` is the spec that has done so: its `— blocked by [[#^slug]]` clause is the worked
  example (backlog §2.5). In `todo@0.1` and `kanban@0.1` a wikilink is text that round-trips and
  nothing more; `narrate@0.1` reads the name aloud and not the brackets (narrate §2.3), which is a
  rendering rule, not a relation.
- Like tags, wikilinks remain part of the item's rendered text, and nothing is scanned inside code
  spans or fences (§4.5).

---

## 6. Preservation and round-tripping

These are the guarantees that make the file, not the tool, the source of truth. They are numbered, and
other documents cite them as **§6.1**–**§6.5** by that number.

1. **Identity round-trip.** For any conforming file, `parse` → `serialize` MUST reproduce the input
   byte for byte. An implementation that cannot do this is not conforming, whatever else it does.
2. **Unknown data is preserved.** Unknown frontmatter keys, unknown inline fields, unrecognized tags,
   prose, blockquotes, tables, images, code fences, HTML blocks, and blank-line placement all survive
   a parse/serialize cycle unchanged.
3. **Minimal diffs.** A semantic mutation MUST produce the smallest source edit that achieves it.
   Unrelated lines MUST NOT be reformatted, re-wrapped, re-indented, or reordered. Markdown To tools
   do not normalize files.
4. **Invalid data is preserved too.** A value that fails validation (`MDTO022`) is reported, not
   deleted. Diagnostics are advice; the file remains the author's.
5. **Conflicts are surfaced, not merged.** A tool holding a file open MUST detect that the source
   changed underneath it (source hash) and surface the conflict rather than overwrite.

---

## 7. The canonical IR

Each spec ships a JSON Schema (draft 2020-12) for its canonical intermediate representation at
`specs/<name>/schema.json`. Renderers, verbs, and downstream tools consume the IR; they never
re-parse source and never guess.

### 7.1 Source positions

Every IR node carries a `position` object with `start` and `end` points (`line`, `column`, optional
`offset`), matching mdast conventions. `line` and `column` are 1-based. Positions are what make
line-addressed diagnostics and minimal patches possible, so they are REQUIRED, not optional.

### 7.2 The shared item model in the IR

This is how the schemas enforce the cross-spec guarantee §12 states.

`todo@0.1` defines the item model in `specs/todo/schema.json` under `$defs/item`. `kanban@0.1` does
not restate it: `specs/kanban/schema.json` `$ref`s that definition directly, so a card and a todo item
are the same object by construction rather than by intention. A todo list is a one-column board; the
schemas say so.

A spec that *extends* the item model (as `backlog@0.1` does with states and blockers, and
`gantt@0.1` with dates, progress, and dependencies) cannot `allOf`
against `$defs/item`, because the item is a closed object (`additionalProperties: false`) and its
`children` are the base type. Such a spec `$ref`s the item's *property* schemas individually instead,
so the shared field types are still enforced through the references rather than copied.

An item carries: `checked`, `text` (normalized, fields and identifier removed, tags retained), `raw`
(verbatim source after the checkbox), optional `id`, `tags`, the typed known fields `priority` / `due`
/ `owner`, a `fields` map holding every field including unknown ones, `children` (nested items), and
`position`.

`gantt@0.1` follows the same property-reference pattern and replaces recursive `children` with its
extended task type, exactly as backlog does. The publication/event formats still `$ref` the shared
`position` definition, but that does not make their blocks or events items.

---

## 8. Determinism and safety

- Compilation and serialization are **deterministic**. The same input produces the same output, on
  every machine, forever.
- **No LLM is involved in conforming compilation.** A renderer never infers what a heading "means".
- **No code execution.** Fenced code is data. Nothing in a conforming file can cause a tool to run it.
- **No network access** during validation, rendering, or mutation. Links are text.
- Rendering a conforming file MUST be safe on untrusted input: raw HTML in the source is escaped or
  stripped by the HTML renderer, never passed through.

---

## 9. Diagnostics

### 9.1 Shape

A diagnostic is an object with at least:

```json
{ "code": "MDTO022", "severity": "error", "line": 7, "message": "…" }
```

- `code` — `MDTO` followed by three digits. Stable forever (§9.2).
- `severity` — `error`, `warning`, or `info`. **Only `error` makes a file non-conforming.**
- `line` — 1-based line number in the source file, counting the opening `---` of the frontmatter as
  line 1.
- `message` — human-readable, present tense, says what is wrong. Implementations MAY word it
  differently; the wording is not part of the contract (§10).

A diagnostic about a specific construct reports the line that construct starts on. A diagnostic about
the **document as a whole** reports the line of the `markdownto` key if that key exists, and line 1
otherwise. A diagnostic about a frontmatter key reports that key's own line; a diagnostic about one
entry inside a frontmatter collection (a mapping or sequence value) reports that entry's own line,
not the collection key's.

Implementations MAY add fields (`column`, `endLine`, `hint`, `path`). Tools MUST NOT require them.
This implementation emits `column` wherever the construct a message names has a known span on the
reported line — a 1-based column pointing at that construct's first character (the key, the `[` of a
field, the `^`, the state character inside a checkbox) — and omits it entirely for a diagnostic about
the document as a whole.

### 9.2 Code ranges

| Range | Owner |
|---|---|
| `MDTO001`–`MDTO099` | Shared: envelope, item identity, inline fields, item structure |
| `MDTO100`–`MDTO199` | `todo` |
| `MDTO200`–`MDTO299` | `kanban` |
| `MDTO300`–`MDTO399` | `narrate` |
| `MDTO400`–`MDTO499` | `backlog` |
| `MDTO500`–`MDTO599` | `backlog-workspace` |
| `MDTO600`–`MDTO699` | `countdown` |
| `MDTO700`–`MDTO799` | `gantt` |
| `MDTO800`–`MDTO899` | `pdf` |
| `MDTO900`–`MDTO999` | `calendar` |
| `MDTO1000`–`MDTO1099` | `flashcards` |
| `MDTO1100`–`MDTO1199` | `timeline` |
| `MDTO1200`–`MDTO1299` | `podcast` |
| `MDTO1300`–`MDTO1399` | `guided-narration` |
| `MDTO1400`–`MDTO1499` | `slides` |
| `MDTO1500`–`MDTO1599` | `form` |
| `MDTO1600`–`MDTO1699` | `epub` |
| `MDTO1700`–`MDTO1799` | `matrix` |
| `MDTO1800`–`MDTO1899` | `quiz` |
| `MDTO1900`+ | Allocated to future specs, one hundred per spec, in registration order |

**Codes are never reused, renumbered, or repurposed.** A retired code is retired permanently. Adding a
code is an additive (minor) change; changing what a code means is breaking.

**A code added to a construct a published version already accepted may be a `warning`, never an
`error`.** §2.3 requires a file valid under `name@X.N` to remain valid under `name@X.M`, and only an
`error` decides validity (§9.1) — so a new error against a construct an earlier revision let through
would retroactively un-conform files that were written correctly against the spec as published. The
honest options are a warning now or a major version later. This does not constrain a code shipped
*with* the construct it checks: a construct that did not exist has no files to invalidate. The worked
example is `MDTO210`: `kanban@0.1` shipped `done-column:` explicitly unvalidated, so the resolution of
that open question could say the key names no column, and could not say the file is broken.

### 9.3 Shared codes

**Envelope (001–009)**

| Code | Severity | Meaning |
|---|---|---|
| `MDTO001` | error | Missing frontmatter envelope: the file does not begin with a `---` fenced YAML block. |
| `MDTO002` | error | Frontmatter is not parseable YAML, or is not a mapping. |
| `MDTO003` | error | Frontmatter is missing the required `markdownto` key. |
| `MDTO004` | error | Malformed spec identifier: expected the string form `name@major.minor`. |
| `MDTO005` | error | Unknown spec name. |
| `MDTO006` | error | Unsupported spec major version. |
| `MDTO007` | error | Frontmatter key has the wrong type. |
| `MDTO008` | warning | File declares a newer minor version than this implementation; unrecognized constructs are preserved but not validated. |

**Item identity (010–019)**

| Code | Severity | Meaning |
|---|---|---|
| `MDTO010` | error | Malformed block identifier. |
| `MDTO011` | error | Duplicate block identifier. |
| `MDTO012` | warning | Block identifier is not the final token of the line; it is being treated as literal text. |

**Inline fields (020–029)**

| Code | Severity | Meaning |
|---|---|---|
| `MDTO020` | warning | Bracketed span looks like an inline field but is malformed; it is being treated as literal text. |
| `MDTO021` | error | Inline field has an empty value. |
| `MDTO022` | error | Invalid value for a known typed field. |
| `MDTO023` | error | Duplicate inline field key on one item. |

**Item structure (030–039)**

| Code | Severity | Meaning |
|---|---|---|
| `MDTO030` | error | List item is not a task item: it has no `[ ]` / `[x]` checkbox. |
| `MDTO031` | error | Unrecognized checkbox state; the file's spec defines which states are legal (`todo`/`kanban`: `[ ]`, `[x]`, `[X]`; `backlog` adds `[/]`, `[-]`). |
| `MDTO032` | error | Task checkbox on an ordered list item; items must be bullet list items. |
| `MDTO033` | error | Item has no text: it consists only of metadata, or of nothing. |

`MDTO031` covers the near-miss case deliberately: a bullet whose text begins with `[` + one character
+ `]` + space is treated as an **intended** checkbox. `- [/] Draft the spec` reports `MDTO031`
("unrecognized state `/`"), not `MDTO030` ("not a task item"), because the second message would send a
repairing agent in the wrong direction.

**`MDTO031` is spec-relative: which markers are legal is a property of the file's declared spec, not
of this code.** Extended state characters are a known extension point rather than an accident, and
`backlog@0.1` has taken it — `[/]` and `[-]` are legal there and are `MDTO031` in `todo@0.1` and
`kanban@0.1`, from the same code, with only the message naming the enum. A spec that extends the
state set MUST declare its full enum explicitly (as backlog §2.4 does), and every tool — including
tools implementing specs *without* the extension — MUST preserve an unrecognized marker character
verbatim on round-trip rather than coercing it to open or checked (§6.4 applied to state markers).
**If you are repairing an `MDTO031`, read the file's own spec for the legal enum before you map the
marker; do not assume todo's two.**

### 9.4 Recovery

Validation is a single pass that reports everything it can find, rather than stopping at the first
problem — an agent repairing a file should get the whole list. Two rules make that deterministic:

- **Envelope errors halt validation.** If the envelope is missing, unparseable, or names a spec or
  major version the implementation cannot handle (`MDTO001`–`MDTO006`), no spec is known, so no
  further checking is possible and exactly one diagnostic is reported.
- **Everything else recovers locally.** A construct that fails validation is reported, kept in the
  IR, and does not suppress checks on its neighbours. Each spec states what its own invalid
  constructs recover to.

Spec-specific codes are documented in each spec's own SPEC.md.

---

## 10. Conformance fixtures

Every spec publishes conformance fixtures from day one. They are the operational definition of
"adheres to the format" — an independent implementation is conforming if and only if it agrees with
them.

```
specs/<name>/fixtures/
  valid/<case>.md
  valid/<case>.ir.json              reference IR — informative, §10.5
  invalid/<case>.md
  invalid/<case>.diagnostics.json   expected diagnostics — normative, §10.2
```

### 10.1 The contract

- **Valid fixtures.** An implementation MUST parse each one without producing any `error`-severity
  diagnostic, and MUST round-trip it byte for byte (§6.1). Warnings and info are permitted.
- **Invalid fixtures.** Every `<case>.md` has a sibling `<case>.diagnostics.json`. An implementation
  MUST produce every listed diagnostic, matching on `code`, `severity`, and `line`. It MUST NOT
  produce any `error`-severity diagnostic that is not listed. Additional `warning`/`info` diagnostics
  are permitted; `message` text is informative and need not match.
- **Recovery.** An invalid fixture is still parsed as far as possible: validation reports all
  diagnostics it can find in one pass rather than stopping at the first. This is why several fixtures
  carry more than one diagnostic.

### 10.2 The diagnostics file

```json
{
  "note": "What is wrong with this fixture, in one or two sentences.",
  "diagnostics": [
    { "code": "MDTO022", "severity": "error", "line": 7, "message": "…" }
  ]
}
```

Diagnostics are listed in ascending `line` order, then ascending `code` order.

### 10.3 Codes without fixtures

A code is deliberately unfixtured when what it reports depends on an implementation-local registry
rather than on the file alone: `MDTO005` (unknown spec name), `MDTO008` (file declares a newer minor
version than the implementation), and `MDTO306` (voice not in the implementation's catalogue).
Implementations test these against their own registry. Every other code has at least one fixture.

### 10.4 Fixture style

Valid fixtures are **also documentation**, so they must look like something a person would happily
write by hand: real content, no `foo`/`bar`, no metadata a human would not type, no ID on an item that
tooling has not touched. A valid fixture that reads like machine output is a bug in the fixture.

### 10.5 The reference IR snapshot

Each valid fixture ships a sibling `<case>.ir.json`: the canonical IR (§7) for that file, checked
against the spec's own `schema.json`. It is published so that every worked example of the *source*
has a worked example of the *IR* beside it, which is worth more to someone writing a parser than the
schema read alone.

It is **informative, and derived.** Conformance is §10.1 — no `error` diagnostic and a byte-for-byte
round-trip — never byte-equality with a snapshot; an independent implementation is not obliged to
publish one, and differences in a field this document does not specify are not conformance failures.
Because it is derived, **it is never hand-edited: editing a valid fixture's body means regenerating
its snapshot in the same change** (this repository does that with `MDTO_UPDATE_IR=1 pnpm test` — see
[`README.md`](README.md)). A hand-patched snapshot is the one way to make the suite agree with a
parser that is wrong.

---

## 11. Every spec documents itself for agents

Each spec ships an agent-facing spec document at `specs/<name>/SPEC.md`, versioned with the spec and
consultable at any time — `mdto spec <name>` prints it, and it lives at a stable URL (plus `llms.txt`)
so agents never have to scrape a human docs site. Required shape, exactly:

1. **Purpose** — what this format is for, in two sentences.
2. **Grammar** — the exact structures and what each means.
3. **Rationale** — *why* it is structured this way. Agents repair and extend files better when they
   understand intent, and won't "fix" things in the wrong direction.
4. **Examples** — valid files, plus invalid files with the exact diagnostic each produces.
5. **Verb reference** — this spec's own CLI vocabulary, and the precise source edit each verb performs.

The document is part of the spec's definition of done, alongside the schema and the fixtures.

**Verb vocabularies are spec-owned.** There is no universal mutation vocabulary. `todo`, `kanban`,
`backlog`, and `gantt` overlap in shape because they share an item model (§12), not because a platform contract
requires it — and each still spends its verbs on what its own domain does often, which is why `kanban`
has no `undone` and `backlog` has no verb that reopens a task to `[ ]`. `narrate`'s verbs share nothing
with any of them, and are read-only besides. Universal commands exist only where the operation is
cross-spec by nature: `validate`, `render`, `spec`, `inspect`, and `convert` — and of these only
`convert` writes, always to a new file, never through a spec's own vocabulary. `countdown`, `pdf`,
`flashcards`, and `timeline` also demonstrate that read-only vocabularies need not involve a
provider: they report state or create portable artifacts beside the canonical Markdown. Calendar
mostly does the same, while its narrowly addressed `notes` verb writes one event field through the
same mandatory conflict check as every other mutation. Adapters that produce
conforming files from other tools' formats (an import) may live under a spec's CLI namespace for
discoverability, but they are not part of that spec's verb vocabulary and its SPEC.md does not owe
them a row.

---

## 12. The shared item model

`todo@0.1`, `kanban@0.1`, `backlog@0.1`, and `gantt@0.1` do not each define an item. **There is one
item model, and the four of them use it.** What differs is what a `##` heading means — a section in
`todo`, a column in `kanban`, a band in `backlog`, a workstream in `gantt` — and which domain fields
extend the base item. This section states that as a contract; §7.2 is where the schemas make it
mechanical. `narrate@0.1`, `countdown@0.1`, and `pdf@0.1` have no items; `calendar@0.1` has its own
plain-bullet event model rather than pretending an appointment is a task; `flashcards@0.1` and
`timeline@0.1` each use source-faithful content blocks around their own repeated unit.

### 12.1 What the item model is

One GFM task list item on a **bullet** list, plus the task items nested under it, carrying:

- a **checkbox marker** — `[ ]`, `[x]`, `[X]` in `todo` and `kanban`; the legal set is a property of
  the file's declared spec and is an extension point a spec may take, not an accident (§9.3);
- **text** — what is left once the inline fields and the trailing identifier are removed, with tags
  and wikilinks retained, because they read as part of the sentence and fields do not (§5, §7.2);
- **inline fields** — `priority`, `due`, and `owner` typed as in §4.3, and every other key carried as
  an unknown field and written back verbatim (§4.4);
- **tags** (§5) and **wikilinks** (§5.1);
- an optional **lazy `^id`**, present only on an item some tool needed a durable handle for (§3);
- **children** — nested task items, unbounded, with no state derived between a parent and a child in
  0.1.

Because the model is shared, so are its failures: `MDTO030`–`MDTO033` are the item-structure codes in
every spec that has items, and the same broken item reports the same code in each of them (§9.3).

### 12.2 Who defines it

**`todo@0.1` owns the definition.** Its own §2.3–2.4 is the normative grammar and
`specs/todo/schema.json` `$defs/item` is the normative object. Changing the item model is
therefore changing `todo@0.1`, with `todo@0.1`'s versioning consequences (§2.3).

This document owns the item's **parts** — identifiers (§3), inline fields (§4), tags and wikilinks
(§5) — and the **cross-spec guarantee** stated here. It does not define the item, and no spec other
than `todo` does.

### 12.3 How the other specs adopt it

- **`kanban@0.1` takes it whole.** A card *is* an item (kanban §2.3), and its schema restates nothing:
  `specs/kanban/schema.json` `$defs/card` is
  `"$ref": "https://markdownto.ai/specs/todo/0.1/schema.json#/$defs/item"`. The marker enum stays the
  two `todo` defines — on a board the column a card sits in carries the workflow state, and the
  checkbox stays the done flag it already is (kanban §2.4) — and the one re-reading is of nesting: a
  card's children are its checklist, not cards.
- **`backlog@0.1` extends it.** A task is an item plus `state`, which widens the marker enum to four
  (`[ ]`, `[/]`, `[x]`, `[-]` — backlog §2.4), plus `blockers`, projected from the trailing
  `— blocked by …` clause (backlog §2.5), plus the promoted `reason` field. `checked` keeps its
  meaning exactly — true for `[x]` / `[X]` and nothing else — so a consumer written against the shared
  model still reads a backlog task. The mechanism is the one §7.2 describes: `$defs/item` is a closed
  object whose `children` are the base type, so `specs/backlog/schema.json` `$defs/task` `$ref`s
  todo's item *properties* one at a time —
  `https://markdownto.ai/specs/todo/0.1/schema.json#/$defs/item/properties/checked`, and the same path
  ending `/text`, `/raw`, `/id`, `/tags`, `/priority`, `/due`, `/owner`, `/fields`.

A spec that adopts the item model MAY re-read what a construct means for its domain (kanban's
checklist, backlog's decomposition) and MAY extend it with properties of its own. It MUST NOT redefine
or drop one it inherits: a format that would have to contradict the item model is a format with its
own item, and none of these three is that.

### 12.4 Why it is a guarantee

- **It is what lets the specs stay separate.** One format covering all three readings would need a
  mode flag in its envelope, and `##` would mean whatever the flag said — which is what §1's "exactly
  one spec per file" refuses. Three specs keep validation strict, verbs unambiguous, and files
  hand-authorable; sharing the item beneath the heading is what makes that separation cheap instead of
  duplicative.
- **It is what makes one of these files readable as another.** "Same data, many renderings" belongs in
  the renderer, not in the grammar: a backlog shown as a board, or converted into one, re-reads the
  heading axis over items that already agree. Nothing about the items is translated or inferred.
- **It is what an agent gets for free.** An agent that has learned to write one of these specs' items
  has learned all three — same marker, same `[key:: value]`, same `#tag`, same lazy `^id`, same
  nesting. Only the heading axis and that spec's own deltas are left to read.

---

## 13. Multi-file operations

§6.5 says a tool must surface a conflict rather than overwrite, and it is written for one file: one
source hash, one atomic rename, all-or-nothing. A spec whose unit is a directory has verbs that write
several files at once, and "all-or-nothing" is not a promise POSIX lets anyone keep across them. This
section is the contract for those operations. It is cited as **§13.1**–**§13.7**.

It binds any tool performing one operation over more than one file, whatever spec it belongs to.
`backlog-workspace@0.1` is the first to satisfy it; a spec whose unit is a single file never needs
it, and nothing here changes a single-file verb.

### 13.1 The manifest

Before it writes anything, a multi-file operation MUST build a **manifest**: an ordered list of every
file it will read, create, rewrite, or delete, each carrying

- the **action** — `read`, `create`, `write`, or `delete`;
- the **expected source hash** (§6.5's hash) of every file that already exists.

Each workspace-relative path appears **exactly once**. A duplicate path is two competing claims
about one file, not a longer manifest, and MUST be refused before the §13.2 transaction preflight
begins or any file is written. The manifest is complete before the first write. An operation that discovers, part-way through, a
further file it must write has a bug, not a longer manifest: the whole value of declaring one is that
the set of affected files is knowable before any of them changes.

### 13.2 Fail before any write

Every existing file in the manifest MUST be read and hashed first, and every hash MUST match the
expectation. **Any mismatch aborts the whole operation before a single byte is written**, and the
refusal names the file whose hash moved. The same abort applies when the tree is not what the
manifest describes: a `create` whose target exists, or a `write` or `delete` whose target is missing.

A caller supplies expectations from an earlier read the way §6.5 allows for one file — `mdto` spells
it `--expect <path>=<hash>`, repeatable — and a tool computes the rest from the files it has just
read. An expectation names an **existing** manifest file; a `create` row has no source hash, so a
tool MUST refuse an expectation for it rather than accept a guard it cannot check. `--force` may
override a *validation* refusal; it MUST NOT override this check.

### 13.3 Declared order, and where content goes first

Writes apply in manifest order, each through the same temp-plus-rename a single-file write uses, so
that no individual file is ever observed half-written. The order is part of the operation's contract
and MUST be deterministic.

Where an order is observable, the rule is: **write the content before the reference that points at
it.** Create the note, then rewrite the spine line that links it. An operation interrupted between
the two then leaves a file nothing points at — recoverable, and visible to a reader — rather than a
reference to a file that does not exist.

### 13.4 Rollback is attempted; partiality is reported

If a write fails part-way through the sequence, the tool MUST attempt to restore every file it has
already written, from the bytes it read in §13.2, and MUST then report the outcome **per file**:
restored, or left changed.

Where the host filesystem exposes permission modes, a create-and-delete move MUST give the created
file the source file's permission mode, and rollback that recreates a removed file MUST restore that
mode along with its bytes. A private source must not become public merely because it moved or was
restored.

**A tool MUST NOT claim atomicity here.** There is no multi-file rename, so attempt-and-report is the
honest contract and an unqualified "failed" would be a lie whenever a rollback itself failed. Two
outcomes are distinguishable, and a tool with an exit-code table MUST distinguish them: *nothing was
written* (the §13.2 refusal, and a successful rollback) and *at least one file was left changed*.
`mdto` spells them 23 and 24; 10 remains the single-file conflict of §6.5.

A tool MAY implement journal-then-apply instead — a write-ahead record a later invocation can finish
or undo. That is strictly stronger and is permitted, provided it still reports per-file outcomes in
this shape. Because it narrows the failure window and changes no observable contract, it may arrive
in a **minor** version of any spec built on this section.

### 13.5 Dry run

`--dry-run` prints the manifest and the diff of every file the operation would change, and writes
nothing. It MUST still perform the §13.2 hash check, so that a dry run of an operation that would
refuse says so rather than describing an edit that could not be applied.

### 13.6 `--json`

Additive to the `mdtoJson: 1` shape, never a rewrite of it. A multi-file operation's payload carries
`files`: an array **in manifest order**, one entry per file, each with the path, the action it was
given, whether it changed, its hash after the operation, and — on a failure — whether it was
restored. The singular `file`, `hash` and `edits` keep describing the operation's **primary** file:
the spine, for a workspace. A reader that knows only the single-file shape therefore still reads
something true.

### 13.7 What this section does not change

- §6.1–§6.4 hold per file, unchanged: identity round-trip, preservation of unknown and invalid data,
  and the minimal-diff rule apply to each file the operation touches.
- A file opened only to hash is not a file the operation may reformat.
- Single-file verbs are untouched. A multi-file operation is a set of single-file edits with one gate
  in front of them — not a new editing model, and not a licence to widen what any one edit does.
