Specification · v1 Working Draft
anvil·md

Specification  ·  Section 11 of 14

Parser contract

The parser is total. What every malformed input degrades to, and why derived ids must come from content.

The parser is total: it has no throw path. An LLM will eventually emit a half-finished fence mid-stream, and a thrown parse error inside a page renderer takes down far more than the block.

Malformed inputBehaviour
unknown @kindrender as a warned @note, keep raw text
unknown leading sigiltreat as prose, append to the prompt
line before any @implicit @note
duplicate id in one docsecond gets a suffix, warn
missing idderive from content (see below)
unknown field typefall back to text, warn
unclosed ~~~close at end of fence
unclosed fence (streaming)render what parsed, mark partial, suppress interaction
@gallery row with no img=placeholder for that row only
> 12 optionsrender all of them plus a warning. Never truncate.
out-of-range steps / dial defaultclamp, warn
non-string inputcoerce, never throw

Warnings are surfaced in the rendered block, not swallowed. An agent that writes something the parser did not like should be able to see that.

Derived ids

A block with no id= gets one derived from a hash of its kind and normalised body. This must be stable across re-renders and reloads, so it can only depend on content — never on array position, in a list that streaming might reorder. A positional id lands the stamp on the wrong block.