# Todo The plan. Design questions are settled in `AGENTS.md`; remaining spec detail is settled at its own milestone. A done item shrinks to its title here; its full text moves to `todo-history.md`. ## Milestones Shipping order: 3h, 3i, 3j, 5 → `0.1.0`; 4b → `0.1.1`; 4, 3k → `0.2.0`; 6, 7 → `0.3.0`. The numbering is the order the work was planned in, not the order it ships. - [x] **0 — Scaffold.** - [x] **1a — The directive grammar.** - [x] **1b — Block node syntaxes.** - [x] **1c — Inline node syntaxes and marks.** - [x] **1d — Corpus start.** - [x] **1d1 — The CommonMark subset.** - [x] **1d2 — Block nodes.** - [x] **1d3 — Inline nodes and marks.** - [x] **2 — `adfToMarkdown`.** - [x] **2a — The runner and the CommonMark subset.** - [x] **2b — Block nodes.** - [x] **2c — Inline nodes and marks.** - [x] **2d — The opaque carry.** - [x] **2e — Carve-outs and combinations.** - [x] **2e1 — The carve-outs and the claimed line.** - [x] **2e2 — Mark runs and the runs a carry breaks.** - [x] **2e3 — Attribute canonicalization and the quoted value's escape.** - [x] **2e4 — The carry's fallback triggers.** - [x] **2e5 — Combined documents and the collision property.** - [x] **2f — The attributes CommonMark cannot hold.** - [ ] **3 — `markdownToAdf` (`0.1.0`).** Each sub-item lands the fixtures its own code reads, and the runner grows a parse half as they do: `parsingDirectories` beside `emittingDirectories`, a round-trip directory joining it only once every fixture in it reads back to its document, and readers for `corpus/normalization/` (setext, indented code, loose lists, `*`/`+` bullets, entity references, soft wraps — one-way, the markdown not canonical) and `corpus/errors/` (a markdown input per named error, the code in a `.error` beside it) with the first fixture each. `commonmark-subset/` cannot be the first to green — `::paragraph` and `:hardBreak{}` sit in it — so 3b through 3f answer to their own tests and the one-way fixtures they land, and 3g is where the first directory reads back. The raw-HTML element mapping is empty until milestone 6, so at `0.1.0` every raw-HTML construct in input — a block, an inline tag, a comment, a processing instruction — is a named error. Input is where unbounded nesting actually arrives, so §11's 500 binds all three of the emitter's guards here: block depth at 3c and again at 3f's container fences, inline and mark depth at 3f and 3i, a carried value's JSON at 3j, where `isJsonValue` already bounds it. - [x] **3a — The hierarchy.** - [x] **3b — The leaf blocks.** - [x] **3c — The container blocks.** - [x] **3d — Inline text.** - [x] **3e — Emphasis and links.** - [x] **3f — The directive grammar.** - [x] **3g — The node tables read backwards.** - [x] **3h — The block nodes.** - [ ] **3i — The inline nodes and the marks.** `inline-nodes/` reads back: the content slot's `text` attribute and the error a slot holding anything but one unmarked text node is; the `:text{text="…"}` whitespace spelling; the four directive marks and their nesting order, outermost first; and `:em[x]` as the error `spec/flavour.md` promises. Editor-normal's merging half lands here, `text-whitespace` being the first fixture that forces it, and 4's `toEditorNormal` is built on it. 3g's shape leaves three: `readInlineDirectiveNode` takes the name, the attributes and the slot's parsed text rather than the span, since `inline-content.ts` already imports it and parsing the slot inside it is a cycle; the four directive marks get `parse/directive-marks.ts` that `inline-content.ts` tries ahead of the node reader, as `mark-spellings.ts` sits apart from `emit/inline-directive-spelling.ts`; and the five markdown-spelled mark names in inline directive position take `unsupported-node-shape` rather than a code of their own — §8 already answers a well-formed directive the node tables refuse, and the message names the spelling to use (`*x*`), while `unknown-directive-name`'s "a later MINOR may give the name meaning" stays the wrong signal, as it was for `adf`. `corpus/errors/directive-content-slot` goes when the slot opens. The marks a spelling wraps answer the same question 3g settled for a block's form: only the nesting the emitter writes parses back. - [ ] **3j — The carry and the combinations.** `opaque-carry/` and `combinations/` read back: the `adf` fence and `:adf{json="…"}` restoring a deep-equal node, invalid JSON in either a named error, a carry inside a mark spelling another, and the three carve-outs' escapes reading as the literal text they hold. 3g refuses the `adf` fence rather than reading a `codeBlock` from it; the refusal goes when the carry reads it. `index.ts` gains `markdownToAdf` here, and the README's status line with it: this is the last parser chunk, so `parsingDirectories` becomes `emittingDirectories` and the whole corpus round-trips both ways — `0.1.0`'s proof, which 4 widens rather than replaces. - [ ] **3k — The CommonMark spec suite (`0.2.0`).** Checked in at `corpus/commonmark-spec/`, pinned to the version it ships — the one `html-blocks.ts` names for its start conditions — `corpus/README.md` gaining the kind. **Settled** (the maintainer, 2026-08-27): three checks an example must pass, the reference HTML each ships read as corpus data — which adds no format and no direction (§1). §2's canonical fixpoint: a named error, or markdown that parses and emits to itself byte for byte. That HTML's text, tags stripped and entities decoded, against the parsed document's concatenated `text`. And a count of the dozen elements the CommonMark subset covers against the marks and nodes they map to — counting distinct mark types per text node, since 3e collapses a spelling nested inside its own kind and `*(*a*)*` is two `` against one `em`. The fixpoint alone is self-consistency a parser returning the empty document passes, and the text alone one dropping every emphasis; the counts close both. The exception list stays the maintainer's, and one entry is owed already: 3h continues a list across the marker change CommonMark splits on, so an example the reference HTML gives two `