# Decisions ## Each call names its flavour 2026-10-09, the maintainer; names by a developer panel, 2026-10-09. Goals 4, 5 and 6.1. Valid while each flavour has a standard or a spec of its own. A function's name says which markdown it reads or writes, and a reader reads that flavour alone: | Call | Flavour | | --- | --- | | `adfToLosslessMarkdown`, `losslessMarkdownToAdf` | the lossless flavour, `spec/flavour.md` | | `commonMarkToAdf` | CommonMark as its spec says: a directive, a pipe table, `~~`, `==` and an `adf:` fence read as CommonMark reads them | | `adfToPortableMarkdown`, `portableMarkdownToAdf` | CommonMark, GFM's pipe tables, `~~`, alerts and task markers, and Obsidian's callouts and `==`; no directive and no `adf:` fence | The calls between markdown and HTML that `todo.md` item 139 ships take their flavour's name too. No `adfToCommonMark`: the portable writer serves every persona a CommonMark writer would. The panel picked `portable` 4 of 7 over `plain` 3, `gfm` and `rendered` none: the export readers read it as "renders anywhere, lossy", and three readers read `plain` as holding no tables or alerts. ## Portable markdown is a flavour of the grammar 2026-09-27, the maintainer. Goal 2. Valid while the portable flavour's spellings are ones the markdown grammar can read and write. The lossy pair is the portable 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. `losslessMarkdownToAdf(adfToLosslessMarkdown(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; every parsed document has converted back since 2026-10-04. Goals 1 and 5. 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. ## 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 portable reader builds what is written, as `losslessMarkdownToAdf` does. Only the portable 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, except the doc's `content`, which ADF's schema requires — so two documents the editor holds equal write the same portable 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{}`: 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**. ## A list right after one of its kind takes the other marker 2026-10-04, ordered lists in portable markdown 2026-10-09, the maintainer. Goals 4 and 5. Valid while CommonMark starts a new list where the bullet character or the ordered delimiter changes. Every reader parts lists where the marker changes, as CommonMark does, so both writers alternate the marker between adjacent lists of a kind and no directive parts them. `*` is the maintainer's pick. For ordered lists in portable markdown, this overrides a reader panel's 2026-09-25 verdict, 5 of 7, for one bullet list with escaped numbers (`- 1\. a`). The panel chose that while no other portable spelling parted the lists, so the maintainer overrode it without a new panel. The grammar: `spec/flavour.md` §Canonical form. ## An empty key spells `empty` 2026-10-03, a writer panel and the maintainer. Goals 1 and 6. 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 a container opener and closer with nothing between them 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`. The grammar: `spec/flavour.md` §Block nodes and §The CommonMark blocks. ## A block's marks are named, and `colwidth` is a comma list 2026-10-03 for `colwidth`, 5 of 7, and 2026-10-14 for `marks`, 3 of 3: writer panels of product managers, engineers and support agents. Goal 6. Valid while ADF types these attributes as `src/adf/` does. `colwidth` is its widths joined by commas, `colwidth="340,420"`, where it was a JSON array. A block's `marks` names each mark with its `{attrs}`, in order, `marks="indentation{level=2} fragment{localId=f1 name=\"Price list\"}"`, chosen over the escaped JSON array and the JSON array unquoted. All three writers first sketched a key per mark, `{align=center}`, which loses the marks' order and can clash with the node's own attributes. Where `src/adf/` does not type a mark or one of its attributes, or a value holds another type, the whole array stays JSON, the general form (§Readable spellings take the `try` prefix). The grammar: `spec/flavour.md` §Block nodes. ## Empty markdown is a document of no blocks 2026-10-03, a writer panel and the maintainer. Goals 1 and 6. 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 6. 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 is the lossless flavour's alone. ## 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 5. Valid while ADF holds no node for a bare container, a comment or a script. Lands with `todo.md` item 137. Every foreign element `htmlToAdf` and the markdown readers 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: `