# 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 `assert.deepStrictEqual`: every key and value `doc` holds, two adjacent text nodes, an empty `attrs`, `content` or `marks` and `-0` included, with no normalization on either side. 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 — 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 a reader would build back as one — neither carried, neither holding `attrs` or an empty key, their marks identical, attributes included — are parted by the reserved inline leaf `!adf:textBreak{}`, mirroring `!adf:listBreak`: it builds no node, and anywhere but between two such nodes, or given `[content]` or `{attrs}`, it is `unsupported-node-shape`. It sits inside every mark spelling the pair shares, `**Hello, !adf:textBreak{}world**`; a code span holds no directive, so it closes and reopens, `` `a`!adf:textBreak{}`b` ``. A carried node never joins its neighbour, so a carry needs no break. A mark run breaks on any difference in the mark, `attrs: {}` against no `attrs` included. ## 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` on every directive — block, inline node and mark: `{attrs=empty}`, `{content=empty}`, `{marks=empty}`. A writer panel chose the spelling, 5 of 7. A container spelling `content=empty` closes with no body, so an empty pair stays the node holding no content key; a leaf spells it too, `!adf:rule {content=empty}`. A node CommonMark spells takes its directive form to hold an empty key, and a text node holding one rides the inline carry, as does a node under an `em`, `strong`, `strike`, `code` or `link` mark whose `attrs` is empty, since none of those spellings holds attributes. Any other value of a reserved key is `unsupported-node-shape`, except on a block's `marks`, which reads it as the marks array in JSON. ## `-0` is spelled `-0` 2026-10-03, the maintainer. Goal 1. Valid while JSON's own serialization writes `-0` as `0`. Wherever the flavour writes a number or a JSON value — an attribute, a `json` value, the carry — `-0` is `-0`, which JSON's grammar reads back as `-0`. An `orderedList` whose `order` is `-0` takes the directive form, since no list marker spells the sign. ## Empty markdown is a document of no blocks 2026-10-03, a 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}` standing alone 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 an `adf:` fence whose type an info string carries, 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, each fence's info string the language; its other attributes sit on the opener, and a language no info string carries stays the opener's `language` with bare fences. A `codeBlock` spelling `content=empty` has no fence to carry the language, so the opener does. Fences with differing info strings are `unsupported-node-shape` — ADF holds one language — and so is an empty fence beside another, since a text node holds text. ## 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: `