# Decisions ## Plain markdown is a flavour of the grammar 2026-09-27, the maintainer. Goal 2. Valid while the plain flavour's spellings are ones the markdown grammar can read and write. The lossy pair is the plain flavour: the markdown grammar's reader and writer with the flavour set, its spellings — alerts, callouts, task markers, `==` — read and written there, so a marker line and a backslash reach them intact; what the flavour cannot spell reduces ADF→ADF ahead of the writer. ## The round-trip is the product 2026-08-23, real payloads 2026-09-15, the maintainer. Goal 1. Valid while a consumer saves back through the lossless pair. `markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must deep-equal `doc` — anything less silently destroys content an editor could not represent, in a document it did not author. When losslessness and readability conflict, losslessness wins. Round-trip equality is a property tested over a checked-in corpus (`corpus/README.md`), not a claim made in prose. Its real payloads are invented content written in Atlassian's editor on the maintainer's test site, so none is sanitized and a mention keeps the test user's real account id. ## Markdown in is a canonical fixpoint 2026-08-23, the maintainer. Goals 1 and 4. Valid while markdown input may be written by hand. The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way back yields the library's canonical spelling, and that spelling round-trips byte-identically — where there is a way back. CommonMark spells some things the flavour has no escape for — a paragraph opening with a code span whose backticks read back as a fence — so a parse succeeding does not imply a spellable document; `corpus/commonmark-spec/exceptions.json` names those. ## Equality is deep 2026-08-24, deep 2026-10-03, the maintainer. Goal 1. Valid while a pipeline or a bot can build a shape the editor would not. "Equals" is deep equality over the document's JSON values: `assert.deepStrictEqual` on plain objects as `JSON.parse` builds them, since ADF is JSON. Every key and value in `doc` counts, including two adjacent text nodes, an empty `attrs`, `content` or `marks`, and `-0`. Neither side is normalized. CommonMark's spelling stays wherever a document holds none of those shapes. The plain reader builds what is written, as `markdownToAdf` does. Only the plain writer is lossy: its reduction reads and writes editor-normal ADF — adjacent text nodes of identical marks and no attributes merged, `-0` as `0`, and an empty `attrs`, `content` or `marks` the absent key, but the doc's `content` — so two documents the editor holds equal write the same plain markdown. ## `!adf:textBreak{}` parts text CommonMark would join 2026-10-03, the maintainer. Goal 1. Valid while CommonMark reads adjacent text as one run. Two adjacent text nodes CommonMark would read back as one are parted by the reserved inline leaf `!adf:textBreak{}`, mirroring `!adf:listBreak`: a leaf building no node keeps both nodes and asks nothing of the text around it. A code span holds no directive, so the spans close and reopen around the leaf. The grammar: `spec/flavour.md` §Inline nodes, **Adjacent text nodes**. ## An empty key spells `empty` 2026-10-03, a writer panel and the maintainer. Goals 1 and 5. Valid while no attribute value spells an empty object or array. An `attrs`, `content` or `marks` key holding an empty object or array is the reserved key with the bare value `empty`, so an empty pair stays the node holding no content key. A writer panel chose the spelling, 5 of 7. The grammar: `spec/flavour.md` §Directives, **Attributes**, and §Marks. ## `-0` is spelled `-0` 2026-10-03, the maintainer. Goal 1. Valid while JSON's own serialization writes `-0` as `0`. `-0` is spelled `-0` wherever the flavour writes a number or a JSON value, since JSON's grammar reads it back as `-0` and no list marker spells the sign. The grammar: `spec/flavour.md` §Block nodes and §The CommonMark blocks. ## Empty markdown is a document of no blocks 2026-10-03, a writer panel and the maintainer. Goals 1 and 5. Valid while ADF's schema requires `content` on `doc`. Markdown holding no block reads as `{ content: [], type: 'doc', version: 1 }`, the document `spec/adf-schema/full.json` requires. A document holding no `content` key is `!adf:doc {content=none}` as its only block, and a named error anywhere else. The writer panel split 4 for `none` and 3 for `absent`, and the maintainer chose `none`; all seven rejected a bare `!adf:doc`. ## Unknown nodes ride the carry 2026-08-23, extended to misplaced known nodes 2026-08-26 and to code block children 2026-10-03, the maintainer. Goal 1. Valid while ADF holds nodes, or node positions, this library does not spell. An unknown ADF node is carried opaquely — raw JSON rides a dedicated syntax in both formats and restores to a deep-equal node. The round-trip holds for documents newer than the library. So does a known node no section spells where it stands: a markdown serializer spells a node by type without checking its position, and refusing loses a document ADF itself keeps in an `unsupportedBlock`. A `codeBlock` holding a child no fence holds — anything but a text node carrying no marks, `attrs` or `content` — rides the carry whole. ## The carry fence names the node type 2026-10-03, the maintainer. Goals 1 and 5. Valid while a code fence's info string reads back verbatim. The block carry is a code fence whose info string `adf:` names the node's type, its body the node's JSON without `type`: ```` ```adf:blockCard ````. A type no info string carries back — by the rule a code language follows — leaves the info string `adf:` and keeps `type` in the body. Every info string opening `adf:` is reserved, so a `codeBlock` whose language opens so takes the `language` attribute, and `carry` is an ordinary language. A body holding `type` under a named type, or a fence whose info string is `adf:` alone while its body's `type` could be spelled in the info string, is `unsupported-node-shape`. The reservation claims a fence CommonMark reads as code until `todo.md` item 43 gives CommonMark its own reader. ## A code block is a fence per text node 2026-10-03, the maintainer. Goal 1. Valid while ADF holds a code block's text in more than one node. A `codeBlock` holding several text nodes is the `!adf:codeBlock` container holding one fence per node, so each node keeps its own text. The fences carry one info string, since ADF holds one language, and none is empty beside another, since a text node holds text. The grammar: `spec/flavour.md` §The CommonMark blocks, the `codeBlock` bullet. ## Foreign HTML sorts three ways 2026-08-23, the sort 2026-09-20, the maintainer. Goals 1 and 4. Valid while ADF holds no node for a bare container, a comment or a script. Lands with `todo.md` item 6. Every foreign element `htmlToAdf` and `markdownToAdf` read sorts one of three ways, never a silent drop of content: - A container around document content that ADF has no node for unwraps to its children, its own attributes dropped: `
text
` keeps `text`, losing the alignment. - Content ADF cannot hold is an error result naming it. A comment is one: a person wrote those words, and neither of Atlassian's schemas holds them — `annotation`'s `inlineComment` carries an id, `placeholder` is the editor's own hint, `extension` names a vendor app. - What is not document content drops whole: `