3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 19:58:28 +01:00

Answer prose round 1: 0 🔴, 7 🟡; name the titles Goal 7 covers, and the spellings that drop one

This commit is contained in:
2026-10-18 13:00:44 +02:00
committed by Lillem4n
parent 2db86f6666
commit 021229ab04
3 changed files with 142 additions and 135 deletions
+4 -4
View File
@@ -177,7 +177,7 @@ whatever it costs; the Achieve goals are weighed against each other in the order
- Counter-signal: `spec/flavour.md` gains a special case for each such pick.
- Why: output fitted case by case is output no reader can predict.
1. **Maintain:** A function's name says which markdown it reads or writes.
7. **Maintain:** Lossy conversion drops form, never content. A title counts as content.
7. **Maintain:** Lossy conversion drops form, never content. A link's or an image's title counts as content.
8. **Maintain:** Runs in any JavaScript engine, with no runtime dependencies and nothing to configure or connect.
9. **Maintain:** Fast, and linear in the document's size as JSON.
10. **Achieve:** Easy for a contributor to understand and change safely.
@@ -372,9 +372,9 @@ Ping !adf:mention[@Mikael]{id=5b10a2} — !adf:status[In review]{color=yellow}
Serves [Goals](#goals) 7. Portable markdown is a second flavour of the same grammar.
`adfToPortableMarkdown` writes markdown other tools render — GitHub, GitLab, Obsidian and the like —
keeping the content and dropping the rest: attributes, colours, layout, identity. Content is what a
reader of the rendered document sees or follows, titles included: its text, images and link targets.
It writes no directive.
keeping the content and dropping the rest: colours, layout, identity and every other attribute.
Content is what a reader of the rendered document sees or follows: its text, images, link targets and
titles. It writes no directive.
`portableMarkdownToAdf` reads CommonMark and `~~` as `losslessMarkdownToAdf` reads them, and the
conventions below as nodes, taking other tools' spellings too. It reads GFM's pipe tables with or
+9 -7
View File
@@ -262,11 +262,13 @@ caption when the image stands alone in its paragraph.
## An image's title is its caption when the image stands alone in its paragraph, since a title counts as content
2026-10-18, the maintainer, after a reader panel of seven split: two picked the caption, two the
picture alone, and three neither, and those three described hover text. No output reached the five
of seven that settles a panel, so the goal decided. Goal 7 counts a title as content, so the title keeps its words,
and ADF holds no hover text for an image, so the caption is the one ADF node that keeps them. Valid
while a title counts as content (`README.md` `## Goals`, Goal 7).
2026-10-18, the maintainer. The 2026-10-09 panel read a title as the caption, 3 of 3, and the
2026-10-11 writer panel read it as hover text, so a reader panel tested the reading again. Seven
readers split: two picked the caption, two the picture alone, and three neither. Those three
described hover text. No output reached the five of seven that settles a panel, so Goal 7 decided:
a title counts as content, so it keeps its words. ADF holds no hover text for an image, so the
caption is the one ADF node that keeps them. Valid while a link's or an image's title counts as
content (`README.md` `## Goals`, Goal 7).
## A mark skips a node that cannot hold it, and a directive mark wrapping only such nodes is refused, as is a `[text](url)` link unless its text is hard breaks alone
@@ -293,8 +295,8 @@ Goal 6. Valid while the audience reads a title as hover text.
`adfToLosslessMarkdown` writes a `media` whose only mark is a link as `[![alt](url)](href)`, which
each reader expected before seeing the options. A captioned image keeps the `!adf:caption`
directive: each reader read `![alt](url "title")` as hover text, not a caption. The library's readers
build a title as the image's caption, since a title counts as content (§An image's title is its
caption when the image stands alone in its paragraph).
build a title as the image's caption when the image stands alone in its paragraph, since a title
counts as content (§An image's title is its caption when the image stands alone in its paragraph).
## A linked image standing inline in portable markdown, its link to another target, is written `[alt](href) ([image](url))`
+129 -124
View File
@@ -12,43 +12,43 @@
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 115 | 0.3.0 | | defect | **Build text holding no `border` mark in `losslessMarkdownToAdf`, and have `adfToLosslessMarkdown` carry text holding one.** | 2 | 1 | 2 | 3 | 3 | 4 by 3 | 4.8 |
| 60 | 0.3.0 | | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 2 | 1 | 2 | 3 | 4 | 10 by 4 | 4.0 |
| 92 | 0.3.0 | | | **Lower the reading cost of the files a chunk reads most before its first commit.** | 2 | 1 | 1 | 2 | 4 | 10 by 4 | 4.0 |
| 102 | 0.3.0 | | decision | **Place a block a portable task list refuses at its path in the returned document without replaying `splitTaskList` on stand-in nodes.** | 2 | 1 | 2 | 3 | 4 | 10 by 4 | 4.0 |
| 95 | 0.3.0 | | decision | **Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them.** | 4 | 1 | 3 | 6 | 6 | 10 by 7 | 2.3 |
| 91 | 0.3.0 | 95 | | **Spell an emphasis once across the code spans, an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions and statuses between two nodes it marks, and an emphasis across inline media, in both writers.** | 3 | 2 | 2 | 5 | 5 | 6 by 6 | 3.7 |
| 59 | 0.3.0 | | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.** | 3 | 1 | 2 | 4 | 5 | 10 by 5 | 3.0 |
| 62 | 0.3.0 | | decision | **Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`.** | 3 | 1 | 2 | 4 | 5 | 10 by 5 | 3.0 |
| 33 | 0.3.0 | | defect | **Emit a line in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.** | 3 | 1 | 2 | 4 | 5 | 9 by 3 | 2.0 |
| 109 | 0.3.0 | | defect | **Spell nested lists and quotes in time linear in the markdown.** | 3 | 1 | 2 | 4 | 5 | 9 by 3 | 2.0 |
| 73 | 0.3.0 | 33, 59, 60, 62, 91, 92, 95, 102, 109, 115 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 2 | 3 | 2 | 3 by 8 | 17.4 |
| 123 | 0.3.0 | | defect | **Spell a link title that holds a pipe inside a portable table cell, instead of dropping the title.** | 2 | 1 | 2 | 3 | 2 | 7 by 2 | 4.4 |
| 122 | 0.3.0 | | defect | **Keep an image's URL and title when a link marks the image's alt text, as Goal 7 requires.** | 3 | 1 | 2 | 4 | 5 | 7 by 4 | 2.6 |
| 112 | 0.4.0 | | | **Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.** | 2 | 1 | 2 | 3 | 3 | 4 by 4 | 6.3 |
| 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.2 |
| 81 | 0.4.0 | | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 1 | 1 | 1 | 2 | 3 by 2 | 4.4 |
| 75 | 0.4.0 | | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 1 | 1 | 1 | 1 | 2 | 10 by 2 | 4.0 |
| 115 | 0.3.0 | | defect | **Build text holding no `border` mark in `losslessMarkdownToAdf`, and have `adfToLosslessMarkdown` carry text holding one.** | 2 | 1 | 2 | 3 | 3 | 4 by 3 | 4.5 |
| 33 | 0.3.0 | | defect | **Emit a line in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.** | 3 | 1 | 2 | 4 | 5 | 9 by 3 | 3.6 |
| 109 | 0.3.0 | | defect | **Spell nested lists and quotes in time linear in the markdown.** | 3 | 1 | 2 | 4 | 5 | 9 by 3 | 3.6 |
| 95 | 0.3.0 | | decision | **Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them.** | 4 | 1 | 3 | 6 | 6 | 10 by 7 | 1.2 |
| 91 | 0.3.0 | 95 | | **Spell an emphasis once across the code spans, an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions and statuses between two nodes it marks, and an emphasis across inline media, in both writers.** | 3 | 2 | 2 | 5 | 5 | 6 by 6 | 2.4 |
| 60 | 0.3.0 | | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 2 | 1 | 2 | 3 | 4 | 10 by 4 | 2.0 |
| 92 | 0.3.0 | | | **Lower the reading cost of the files a chunk reads most before its first commit.** | 2 | 1 | 1 | 2 | 4 | 10 by 4 | 2.0 |
| 102 | 0.3.0 | | decision | **Place a block a portable task list refuses at its path in the returned document without replaying `splitTaskList` on stand-in nodes.** | 2 | 1 | 2 | 3 | 4 | 10 by 4 | 2.0 |
| 59 | 0.3.0 | | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.** | 3 | 1 | 2 | 4 | 5 | 10 by 5 | 1.5 |
| 62 | 0.3.0 | | decision | **Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`.** | 3 | 1 | 2 | 4 | 5 | 10 by 5 | 1.5 |
| 73 | 0.3.0 | 33, 59, 60, 62, 91, 92, 95, 102, 109, 115 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 2 | 3 | 2 | 3 by 8 | 32.0 |
| 123 | 0.3.0 | | defect | **Spell a link title holding a backslash, an entity reference or a pipe in a table cell, instead of dropping it.** | 3 | 1 | 2 | 4 | 3 | 7 by 2 | 6.0 |
| 122 | 0.3.0 | | defect | **Keep an image's URL and title when a link's text holds the image, instead of dropping them.** | 3 | 1 | 2 | 4 | 5 | 7 by 4 | 4.8 |
| 81 | 0.4.0 | | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 1 | 1 | 1 | 2 | 3 by 2 | 8.0 |
| 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 6.0 |
| 6 | 0.4.0 | | decision | **Specify the HTML dialect.** | 3 | 2 | 1 | 4 | 6 | | 0.0 |
| 7 | 0.4.0 | 6, 59, 60, 62 | decision | **Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.** | 4 | 2 | 2 | 6 | 9 | 3 by 6 | 0.3 |
| 46 | 0.4.0 | 7 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 2 | 1 | 3 | 4 | 3 by 3 | 3.3 |
| 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 3.3 |
| 68 | 0.4.0 | | defect | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 2 | 1 | 2 | 3 | 3 | 1 by 2 | 3.3 |
| 51 | 0.4.0 | | | **Match a reference label to its definition under Unicode case folding.** | 2 | 1 | 2 | 3 | 4 | 4 by 2 | 2.1 |
| 7 | 0.4.0 | 6, 59, 60, 62 | decision | **Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.** | 4 | 2 | 2 | 6 | 9 | 3 by 6 | 0.6 |
| 46 | 0.4.0 | 7 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 2 | 1 | 3 | 4 | 3 by 3 | 6.0 |
| 68 | 0.4.0 | | defect | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 2 | 1 | 2 | 3 | 3 | 1 by 2 | 6.0 |
| 112 | 0.4.0 | | | **Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.** | 2 | 1 | 2 | 3 | 3 | 4 by 4 | 6.0 |
| 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 4.0 |
| 51 | 0.4.0 | | | **Match a reference label to its definition under Unicode case folding.** | 2 | 1 | 2 | 3 | 4 | 4 by 2 | 2.0 |
| 75 | 0.4.0 | | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 1 | 1 | 1 | 1 | 2 | 10 by 2 | 2.0 |
| 77 | 0.4.0 | | decision | **Read the input once into a plain copy, so an accessor property or a throwing Proxy yields `not-an-adf-document` and every call returns a result.** | 3 | 1 | 3 | 5 | 4 | | 0.0 |
| 107 | 0.4.0 | | decision | **Spell the block carry's JSON so its length stays linear in the node's JSON.** | 3 | 1 | 3 | 5 | 5 | | 0.0 |
| 72 | 0.4.0 | 6, 7, 45, 46, 51, 68, 75, 77, 80, 81, 107, 112 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 1 | 2 | 1 | 2 | 1 | 3 by 1 | 2.6 |
| 42 | 0.5.0 | | defect | **Trim a text leaf's trailing blanks in linear time.** | 1 | 1 | 2 | 2 | 2 | 9 by 3 | 6.5 |
| 31 | 0.5.0 | | | **Make the branch-coverage figure repeat across runs of an unchanged tree.** | 2 | 1 | 1 | 2 | 3 | 10 by 3 | 4.5 |
| 57 | 0.5.0 | | | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 1 | 1 | 2 | 3 | 10 by 3 | 4.5 |
| 38 | 0.5.0 | | defect | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 1 | 1 | 2 | 2 | 2 | 1 by 2 | 4.4 |
| 76 | 0.5.0 | | | **State the files the package does not ship once, so the build, the lint ceiling and the coverage exclusions cannot drift apart.** | 1 | 1 | 1 | 1 | 2 | 10 by 2 | 4.0 |
| 56 | 0.5.0 | | | **Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.** | 3 | 1 | 2 | 4 | 5 | 10 by 5 | 3.0 |
| 118 | 0.5.0 | | | **Spell a portable link's title through `oneLine`.** | 1 | 1 | 1 | 1 | 1 | 10 by 1 | 2.4 |
| 9 | 0.5.0 | | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 3 | 2 | 1 | 4 | 6 | 3 by 6 | 2.2 |
| 34 | 0.5.0 | | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 1 | 2 | 3 | 3 | 4 by 1 | 1.6 |
| 82 | 0.5.0 | | | **Give the directive tests and fixtures random UUID v7 ids in place of `a-1`, `a`, `x` and `h`.** | 2 | 1 | 1 | 2 | 3 | 10 by 1 | 1.5 |
| 58 | 0.5.0 | | | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 3 | 1 | 2 | 4 | 6 | 10 by 3 | 1.0 |
| 72 | 0.4.0 | 6, 7, 45, 46, 51, 68, 75, 77, 80, 81, 107, 112 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 1 | 2 | 1 | 2 | 1 | 3 by 1 | 4.8 |
| 42 | 0.5.0 | | defect | **Trim a text leaf's trailing blanks in linear time.** | 1 | 1 | 2 | 2 | 2 | 9 by 3 | 12.0 |
| 38 | 0.5.0 | | defect | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 1 | 1 | 2 | 2 | 2 | 1 by 2 | 8.0 |
| 9 | 0.5.0 | | | **Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** | 3 | 2 | 1 | 4 | 6 | 3 by 6 | 4.0 |
| 31 | 0.5.0 | | | **Make the branch-coverage figure repeat across runs of an unchanged tree.** | 2 | 1 | 1 | 2 | 3 | 10 by 3 | 2.2 |
| 57 | 0.5.0 | | | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 1 | 1 | 2 | 3 | 10 by 3 | 2.2 |
| 76 | 0.5.0 | | | **State the files the package does not ship once, so the build, the lint ceiling and the coverage exclusions cannot drift apart.** | 1 | 1 | 1 | 1 | 2 | 10 by 2 | 2.0 |
| 34 | 0.5.0 | | | **Read emphasis flanking by the whole character beside an astral symbol.** | 2 | 1 | 2 | 3 | 3 | 4 by 1 | 1.5 |
| 56 | 0.5.0 | | | **Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.** | 3 | 1 | 2 | 4 | 5 | 10 by 5 | 1.5 |
| 118 | 0.5.0 | | | **Spell a portable link's title through `oneLine`.** | 1 | 1 | 1 | 1 | 1 | 10 by 1 | 1.2 |
| 82 | 0.5.0 | | | **Give the directive tests and fixtures random UUID v7 ids in place of `a-1`, `a`, `x` and `h`.** | 2 | 1 | 1 | 2 | 3 | 10 by 1 | 0.8 |
| 58 | 0.5.0 | | | **Port `ci.sh`, `publish.sh` and `docker-runner.sh` to standalone Python scripts.** | 3 | 1 | 2 | 4 | 6 | 10 by 3 | 0.5 |
| 74 | 0.5.0 | | owner | **Keep the release path publishing after npm retires bypass-2FA tokens.** | 3 | 2 | 4 | 7 | 4 | | 0.0 |
| 8 | 0.6.0 | | owner | **Ship a CLI.** | 3 | 2 | 1 | 4 | 7 | | 0.0 |
@@ -66,6 +66,42 @@
(`src/adf/inline-nodes.ts`) restricts text only when it is marked `code`. It changes how stored
markdown reads, so it ships in a minor. Found while building item 114, 2026-10-14.
### 33. Emit a line in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.
`adfToLosslessMarkdown` spends 28 s on one paragraph of 2000 × `un` plus `**-r**`: each run its
flanking cannot spell re-emits the whole line before riding the carry, quadratic in the runs, and
the portable reduction's `spellableLine` drops one mark per re-emit the same way. An inline node
whose attributes no spelling writes re-emits the line the same way before riding the carry. Make all
three linear.
### 109. Spell nested lists and quotes in time linear in the markdown.
Each level of a nested list or quote re-indents its whole text, so the work is the depth times the
text. `losslessMarkdownToAdf` on 3 MB of lazy lines in a list nested 490 levels inside
`!adf:blockquote` spends about 5 seconds re-spelling it. 2 MB of the same lines, split over two
items of a list nested 480 levels, took 11 minutes to refuse on Node 24: a deep subtree past the
length bound refuses only once every level below has re-indented its text. The writers do the same
on such a document. Found while shipping item 106, 2026-10-11.
### 95. Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them.
`escapeClosedRuns` works out which backtick strings an escape leaves, `unspellableRuns` rebuilds the
reader's emphasis and highlight pairing, and `lastLinkClose` guesses which text `[` opens link
text. Only the property tests catch drift between these and the reader, and it holds the project
under the comprehension floor. Reading the line back costs one parse each time a line is tried. The
run it carries may differ from today's, which changes lossless output (§The formats are API).
### 91. Spell an emphasis once across the code spans, an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions and statuses between two nodes it marks, and an emphasis across inline media, in both writers.
It needs item 95, which lets the joined spelling be tried and kept only where it reads back. No
reader marks a hard break, so `*One\` then `Two*` writes back as `_One_\` then `_Two_`, and a link
typed across a line as two links. No reader gives those other nodes a mark Atlassian's schema
withholds from them, and none gives code text a mark but a link. So `**Ping !adf:mention[@A]{id=x}
now**` and ``**Run `npm ci` first**`` each write back as two strong runs, with the space beside the
mention or code spelled `!adf:text{text=" "}`. Joining the run across the break in
`inline-line.ts` ruled worse with two comprehension panels (PR #22): a joined run that cannot be spelled needs its own fallback,
first as a set of parted breaks, then as a flag the first fallback clears.
### 60. Collect the questions `parse/` asks `emit/` into one named module.
`parse/` asks `emit/` through `commonMarkSpelling` and `openingLinkTakesDirective`, each imported
@@ -88,25 +124,6 @@ them 104037. Files read most over the last five chunks, counting opens and searc
`taskOfParagraphs` keeps the original child objects. Both paired-ruling seats on 2026-10-11 named it
the reason the project read worse.
### 95. Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them.
`escapeClosedRuns` works out which backtick strings an escape leaves, `unspellableRuns` rebuilds the
reader's emphasis and highlight pairing, and `lastLinkClose` guesses which text `[` opens link
text. Only the property tests catch drift between these and the reader, and it holds the project
under the comprehension floor. Reading the line back costs one parse each time a line is tried. The
run it carries may differ from today's, which changes lossless output (§The formats are API).
### 91. Spell an emphasis once across the code spans, an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions and statuses between two nodes it marks, and an emphasis across inline media, in both writers.
It needs item 95, which lets the joined spelling be tried and kept only where it reads back. No
reader marks a hard break, so `*One\` then `Two*` writes back as `_One_\` then `_Two_`, and a link
typed across a line as two links. No reader gives those other nodes a mark Atlassian's schema
withholds from them, and none gives code text a mark but a link. So `**Ping !adf:mention[@A]{id=x}
now**` and ``**Run `npm ci` first**`` each write back as two strong runs, with the space beside the
mention or code spelled `!adf:text{text=" "}`. Joining the run across the break in
`inline-line.ts` ruled worse with two comprehension panels (PR #22): a joined run that cannot be spelled needs its own fallback,
first as a set of parted breaks, then as a flag the first fallback clears.
### 59. Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.
Eight directive files sit across three directories, and the `markdown/` root holds 14 entries. HTML
@@ -124,23 +141,6 @@ Writing: `spellPortableBlock`, `quotedUnder`, `tryTaskList` and `taskBlocks` in
`flavourClaims`. Every comprehension reader on 2026-10-03 named the portable flavour's spread across
three directories.
### 33. Emit a line in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.
`adfToLosslessMarkdown` spends 28 s on one paragraph of 2000 × `un` plus `**-r**`: each run its
flanking cannot spell re-emits the whole line before riding the carry, quadratic in the runs, and
the portable reduction's `spellableLine` drops one mark per re-emit the same way. An inline node
whose attributes no spelling writes re-emits the line the same way before riding the carry. Make all
three linear.
### 109. Spell nested lists and quotes in time linear in the markdown.
Each level of a nested list or quote re-indents its whole text, so the work is the depth times the
text. `losslessMarkdownToAdf` on 3 MB of lazy lines in a list nested 490 levels inside
`!adf:blockquote` spends about 5 seconds re-spelling it. 2 MB of the same lines, split over two
items of a list nested 480 levels, took 11 minutes to refuse on Node 24: a deep subtree past the
length bound refuses only once every level below has re-indented its text. The writers do the same
on such a document. Found while shipping item 106, 2026-10-11.
### 73. Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.
Blocked by every other `0.3.0` item (the maintainer, 2026-10-14): the posts announce the finished
@@ -150,38 +150,26 @@ posting, set `converter-comparison/run.sh`'s pins to each package's latest versi
its versions and its date in step with the output. We draft each post for the place it goes; the
maintainer posts them once `0.3.0` is published. Found while planning discoverability, 2026-10-04.
### 123. Spell a link title that holds a pipe inside a portable table cell, instead of dropping the title.
### 123. Spell a link title holding a backslash, an entity reference or a pipe in a table cell, instead of dropping it.
In a table cell, `adfToPortableMarkdown` writes a link whose title holds a `|` without the title
(`portableLink` in `src/markdown/portable/inline-reduction.ts`). The same fallback drops a title
that no CommonMark title spelling holds. README `## Goals` Goal 7 counts a title as content, so the
drop breaks a Maintain goal today. GFM reads `\|` inside a table cell as a pipe, so the title can
keep its pipe as `\|`. The reader must read it back to the same title.
The portable writer drops a link's title when CommonMark cannot spell it as written: a title
holding a backslash or an entity reference (`spellTitle` in `src/markdown/commonmark/link-syntax.ts`),
or, in a table cell, one holding a pipe (`portableLink` in `src/markdown/portable/inline-reduction.ts`).
README `## Goals` Goal 7 counts a title as content, so each drop breaks a Maintain goal today.
Escaping keeps the title: `\\` for a backslash, `\&` for an ampersand that would open an entity
reference, and `\|` for a pipe in a table cell. `portableMarkdownToAdf` must read each spelling
back to the same title.
### 122. Keep an image's URL and title when a link marks the image's alt text, as Goal 7 requires.
### 122. Keep an image's URL and title when a link's text holds the image, instead of dropping them.
Inside any other link, the reader turns an image into its alt text, linked, and drops the image's
URL and title (`docs/decisions.md` §An image reads as an image only alone in its paragraph). README
`## Goals` Goal 7 holds an image and a title as content, and a Maintain goal holds whatever it
costs. ADF holds no inline external image, so the item must find a shape that keeps the image, its
title and the link together.
### 112. Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.
GFM pads a short row with empty cells, reads a line holding no `|` after a table as a row, and
reads `a |` over `--- |` as a one-column table. `portableMarkdownToAdf` refuses a short row as
`malformed-pipe-table`, ends the table at a line holding no `|`, and reads the one-column shape as a
paragraph. A long row stays refused (`docs/decisions.md` §The portable reader refuses what GFM's
reading would drop), so the README's pipe-table guarantee keeps the long row for both readers, and
the short row and the line holding no `|` for `losslessMarkdownToAdf` alone. The one-column shape needs `isBareDelimiterRow` in the writer's escaping to cover it too.
### 80. Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.
Lands with items 45 and 77, whose reader walks every node. Today every refused node, such as one
with a `Date` in an attribute, reads `an ADF document's content holds ADF nodes: one of them is not`
with an empty `path`. An export tool told this about one document in thousands then searches that
document's whole tree. Give the node's path, and name its type and the attribute key as
`attributeNestingMessage` does. Found by the product-owner review of items 66 and 67, 2026-10-05.
When a link's text holds an image, the reader turns the image into its alt text, linked, and drops
the image's URL and title (`docs/decisions.md` §An image reads as an image only alone in its
paragraph). README `## Goals` Goal 7 holds an image and a title as content, and a Maintain goal
holds whatever it costs. ADF holds no inline external image, so the item must find a shape that
keeps the image, its title and the link together. When it lands, it also rewrites two statements
that will no longer hold: the README's guarantee that inside another link the title is dropped,
and the sentence in `docs/decisions.md` §An image reads as an image only alone in its paragraph
that says the drop stands until this item ends it.
### 81. Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.
@@ -189,13 +177,12 @@ document's whole tree. Give the node's path, and name its type and the attribute
leaves the README's tables a false contract under a green gate. Assert that the tables list exactly
the union. Found by the technical-principles audit of item 78, 2026-10-05.
### 75. Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.
### 45. Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.
`blockCarry` and `hostileBlockCarry` (`src/conformance/markdown-property.test.ts`) write a fence
whose info string is `carry`. Under the `docs/decisions.md` entry "The carry fence names the node
type", that is an ordinary code language, so the property reaches `readCarriedBlock`'s refusals only
where a random edit lands in a canonical document. Write `adf:<type>` and `adf:` fences, canonical
and hostile. Technical principle: for pure logic over many inputs, prefer a property test, so the property must reach the fenced carry's refusals itself. Found by the technical-principles audit, 2026-10-04.
Lands with item 77: the reader returns item 77's copy. Goal 1 has every call return a result; the
boolean guard is the one export that does not, and it cannot say which branch refused, where
`not-an-adf-document`'s message already does. Breaking: `MIGRATION.md` shows the guard's
replacement.
### 6. Specify the HTML dialect.
@@ -221,13 +208,6 @@ or report the unminified gzip). The figure lands in README §The package beside
dependencies" claim, and in a size column of the comparison table. Measured today,
unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 kB.
### 45. Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.
Lands with item 77: the reader returns item 77's copy. Goal 1 has every call return a result; the
boolean guard is the one export that does not, and it cannot say which branch refused, where
`not-an-adf-document`'s message already does. Breaking: `MIGRATION.md` shows the guard's
replacement.
### 68. Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.
`emitLeaf` (`emit/inline-line.ts`) and `emitDirectiveBlock` (`emit/adf-to-markdown.ts`)
@@ -236,6 +216,23 @@ round-trips them; `docs/decisions.md` §The code list gives a cause the carry an
them from the README's `unsupported-node-shape` cause "Writing, in `adfToLosslessMarkdown`". Found by the README-goals
audit, 2026-10-04.
### 112. Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.
GFM pads a short row with empty cells, reads a line holding no `|` after a table as a row, and
reads `a |` over `--- |` as a one-column table. `portableMarkdownToAdf` refuses a short row as
`malformed-pipe-table`, ends the table at a line holding no `|`, and reads the one-column shape as a
paragraph. A long row stays refused (`docs/decisions.md` §The portable reader refuses what GFM's
reading would drop), so the README's pipe-table guarantee keeps the long row for both readers, and
the short row and the line holding no `|` for `losslessMarkdownToAdf` alone. The one-column shape needs `isBareDelimiterRow` in the writer's escaping to cover it too.
### 80. Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.
Lands with items 45 and 77, whose reader walks every node. Today every refused node, such as one
with a `Date` in an attribute, reads `an ADF document's content holds ADF nodes: one of them is not`
with an empty `path`. An export tool told this about one document in thousands then searches that
document's whole tree. Give the node's path, and name its type and the attribute key as
`attributeNestingMessage` does. Found by the product-owner review of items 66 and 67, 2026-10-05.
### 51. Match a reference label to its definition under Unicode case folding.
`link-syntax.ts` normalizes a label with `toLowerCase`, so `[ẞ]` misses its `[SS]` definition (spec
@@ -245,6 +242,14 @@ as data per §Standards ship as data: lowercasing and then uppercasing would als
gains its row. Its `pending` exceptions go, and the README's guarantees drop the case-folding
exception and count one fewer.
### 75. Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.
`blockCarry` and `hostileBlockCarry` (`src/conformance/markdown-property.test.ts`) write a fence
whose info string is `carry`. Under the `docs/decisions.md` entry "The carry fence names the node
type", that is an ordinary code language, so the property reaches `readCarriedBlock`'s refusals only
where a random edit lands in a canonical document. Write `adf:<type>` and `adf:` fences, canonical
and hostile. Technical principle: for pure logic over many inputs, prefer a property test, so the property must reach the fenced carry's refusals itself. Found by the technical-principles audit, 2026-10-04.
### 77. Read the input once into a plain copy, so an accessor property or a throwing Proxy yields `not-an-adf-document` and every call returns a result.
Lands with item 45, in one chunk: item 45's reader returns the copy. The maintainer's call,
@@ -287,6 +292,11 @@ interim sentence.
quadratic in a run of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in
`adfToPortableMarkdown`. Scan backward, as the expand title's trim does.
### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode.
`adfToLosslessMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute
values already escape it.
### 31. Make the branch-coverage figure repeat across runs of an unchanged tree.
Three Node test legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and
@@ -304,11 +314,6 @@ Technical principle "Compose, do not entangle": Deno, Bun and the tests read the
`node_modules`, the floor and consumer legs the pack leg's, and the browser leg whatever `dist/` the
build leg left, so a leg run alone can pass on stale output.
### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode.
`adfToLosslessMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute
values already escape it.
### 76. State the files the package does not ship once, so the build, the lint ceiling and the coverage exclusions cannot drift apart.
Technical principle DRY: the set is written in `tsconfig.build.json`'s `exclude`, `.oxlintrc.json`'s
@@ -317,6 +322,13 @@ assumes they agree. A helper added to one list lints test code under the shipped
shipped code from coverage, silently. Derive two from the third, or assert in a test that the three
agree. Found by the technical-principles audit, 2026-10-04.
### 34. Read emphasis flanking by the whole character beside an astral symbol.
Check whether `delimiterRunToken` (`inline-tokens.ts`), `emittedRuns` and `resolveEmphasis`
(`line-escaping.ts`) read one UTF-16 unit beside an astral symbol. A lone surrogate is neither
punctuation nor symbol, but CommonMark reads `😀` as punctuation. Where they do, read the code
point, with a fixture per direction.
### 56. Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.
Technical principle "One owner per value": the `Walk` record passes through functions that mutate it
@@ -338,13 +350,6 @@ for a module of its own.
hand, so a change to what a portable line drops would miss link titles. Technical principle: DRY.
Found by the principles audit of item 99, 2026-10-14.
### 34. Read emphasis flanking by the whole character beside an astral symbol.
Check whether `delimiterRunToken` (`inline-tokens.ts`), `emittedRuns` and `resolveEmphasis`
(`line-escaping.ts`) read one UTF-16 unit beside an astral symbol. A lone surrogate is neither
punctuation nor symbol, but CommonMark reads `😀` as punctuation. Where they do, read the code
point, with a fixture per direction.
### 82. Give the directive tests and fixtures random UUID v7 ids in place of `a-1`, `a`, `x` and `h`.
Technical principle: tests use random UUID v7 ids; `portable-markdown-to-adf.test.ts` already does.