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

Publish what each call writes for chosen corpus examples, and fail the gate where the page is stale

This commit is contained in:
2026-11-05 11:14:27 +01:00
committed by Lillem4n
parent f8157d3ad8
commit 1f86f24870
16 changed files with 876 additions and 46 deletions
+1
View File
@@ -89,6 +89,7 @@ In `docs/decisions.md`:
- The source parts by ADF and format
- The comparison table lists every converter that starts and converts
- The real-world corpus holds anonymized documents from sites whose licence it records
- The feature table is a section per corpus example, led by a summary table
## 1. Nothing about any consumer in the library
+3
View File
@@ -2,6 +2,9 @@
## Unreleased
- `docs/feature-table.md` sets real documents beside what `adfToLosslessMarkdown`,
`adfToPortableMarkdown` and `adfToHtml` write for them, and says whether each output reads back to
the same document.
- **Breaking:** `adfToHtml`, `adfToPortableMarkdown`, `htmlToAdf` and `losslessMarkdownToAdf` refuse
a block or non-text inline node holding `text`, and a leaf block or non-text inline node holding
`content`, as `adfToLosslessMarkdown` already did outside a carried node, which it now also
+5 -2
View File
@@ -300,7 +300,8 @@ documentation says, on 2026-10-28.
## The calls
Serves [Goals](#goals) 1, 2 and 8. Pure functions, each taking a whole document and returning a
whole result; no I/O, no configuration.
whole result; no I/O, no configuration. [What each call writes](https://github.com/larvit/adf-codec/blob/main/docs/feature-table.md)
sets real documents beside each writer's output and says whether that output reads back the same.
```ts
adfToLosslessMarkdown(doc: AdfDocument): Result<string>
@@ -771,9 +772,11 @@ Contract: [`docs/decisions.md`](https://github.com/larvit/adf-codec/blob/main/do
## Changing the source
Both commands need only Docker, git and bash 5.
All three commands need only Docker, git and bash 5.
- `./ci.sh` runs every check CI runs.
- `./feature-table/write.sh` rewrites `docs/feature-table.md` from `feature-table/examples.json` and
the build. `./ci.sh` fails where a change leaves that page stale.
- `./mutation-tests/run.sh [<base>]` mutation-tests the lines changed since `<base>`, `origin/main`
by default, in source files git tracks. It needs 6 GB of memory for Docker, and lists the mutants
that survived. Survivors don't fail it; a mutant whose test run crashed does.
+2
View File
@@ -60,8 +60,10 @@ rm -f "$test_log"
leg "tests ($deno_image)" in_image "$deno_image" deno test --allow-env=PROPERTY_RUNS --allow-read --no-check src/
leg "tests ($bun_image)" in_image "$bun_image" bun test src/
leg "corpus-gathering tests ($node_image)" in_image "$node_image" node --test corpus-gathering/
leg "feature table tests ($node_image)" in_image "$node_image" node --test feature-table/
leg "build ($node_image)" in_image "$node_image" npm run build
leg "feature table is current ($node_image)" in_image "$node_image" node feature-table/build.mjs --check
leg "pack and install the tarball ($node_image)" in_image "$node_image" sh -c 'set -e
rm -rf package-tests/node_modules
npm pack --pack-destination /tmp
@@ -0,0 +1,110 @@
{
"content": [
{
"content": [
{
"content": [
{
"content": [
{
"content": [
{
"text": "Part",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
},
{
"content": [
{
"content": [
{
"text": "Stock",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableHeader"
}
],
"type": "tableRow"
},
{
"content": [
{
"content": [
{
"content": [
{
"text": "Bolt M8",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"content": [
{
"content": [
{
"text": "120",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
},
{
"content": [
{
"content": [
{
"content": [
{
"text": "Washer M8",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
},
{
"content": [
{
"content": [
{
"text": "45",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "tableCell"
}
],
"type": "tableRow"
}
],
"type": "table"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,4 @@
| Part | Stock |
| --- | --- |
| Bolt M8 | 120 |
| Washer M8 | 45 |
@@ -0,0 +1,14 @@
{
"content": [
{
"content": [
{ "marks": [{ "type": "strong" }], "text": "Run ", "type": "text" },
{ "marks": [{ "type": "code" }], "text": "npm ci", "type": "text" },
{ "marks": [{ "type": "strong" }], "text": " first", "type": "text" }
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1 @@
**Run `npm ci` first**
+10
View File
@@ -1221,3 +1221,13 @@ the site, applying them is an inference, which `corpus/README.md` tells the read
None of the 22,773 fetched documents holds a type in `typesNoFetchedDocumentHolds`
(`src/conformance/corpus.test.ts`), and neither did up to 40 pages per site with "decision" in the
title. A regenerated corpus holding one of those types fails that test until the type leaves the list.
## The feature table is a section per corpus example, led by a summary table
2026-11-05, a developer panel, 3 of 3. Goal 3. Valid while the page lists about fifteen examples.
Readers from the viewer/editor, export/indexing and LLM/agent pipeline personas picked labelled code
blocks stacked under a heading per example, with a table of the round-trip results on top, over one
wide pipe table and over one wide HTML table with a `<pre>` per cell. Examples come from
`corpus/round-trip/` and are the ones `feature-table/examples.json` lists, never
`corpus/real-world/`, whose licence is not the package's.
+590
View File
@@ -0,0 +1,590 @@
# What each call writes
`feature-table/build.mjs` writes this page from the corpus and the built package, and `./ci.sh` fails where the page differs.
Each output is read back by its format's reader. `holds` means that gives the same document, `loses` a different one, and `refused` an error whose code the example names.
| Example | Lossless | Portable | HTML |
| --- | --- | --- | --- |
| [Bold instruction with a command in it](#bold-instruction-with-a-command-in-it) | holds | holds | holds |
| [Emphasis and strikethrough](#emphasis-and-strikethrough) | holds | holds | holds |
| [Headings](#headings) | holds | holds | holds |
| [Bullet list](#bullet-list) | holds | holds | holds |
| [Code block](#code-block) | holds | holds | holds |
| [Quote](#quote) | holds | holds | holds |
| [Link](#link) | holds | holds | holds |
| [Table](#table) | holds | holds | holds |
| [Panels](#panels) | holds | loses | holds |
| [Task list](#task-list) | holds | loses | holds |
| [Image](#image) | holds | holds | holds |
| [Mention](#mention) | holds | loses | holds |
| [Status](#status) | holds | loses | holds |
| [Date](#date) | holds | loses | holds |
| [Node types this version does not know](#node-types-this-version-does-not-know) | holds | loses | holds |
## Bold instruction with a command in it
ADF:
```json
{"content":[{"content":[{"marks":[{"type":"strong"}],"text":"Run ","type":"text"},{"marks":[{"type":"code"}],"text":"npm ci","type":"text"},{"marks":[{"type":"strong"}],"text":" first","type":"text"}],"type":"paragraph"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
**Run `npm ci` first**
```
`adfToPortableMarkdown`, round-trip holds:
```markdown
**Run `npm ci` first**
```
`adfToHtml`, round-trip holds:
```html
<p><strong>Run </strong><code>npm ci</code><strong> first</strong></p>
```
## Emphasis and strikethrough
ADF:
```json
{"content":[{"content":[{"text":"Read the ","type":"text"},{"marks":[{"type":"em"}],"text":"manual","type":"text"},{"text":" before ","type":"text"},{"marks":[{"type":"strong"}],"text":"wiring","type":"text"},{"text":" the ","type":"text"},{"marks":[{"type":"strike"}],"text":"relay","type":"text"},{"text":".","type":"text"}],"type":"paragraph"},{"content":[{"text":"An un","type":"text"},{"marks":[{"type":"em"}],"text":"real","type":"text"},{"text":"istic goal.","type":"text"}],"type":"paragraph"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
Read the _manual_ before **wiring** the ~~relay~~.
An un*real*istic goal.
```
`adfToPortableMarkdown`, round-trip holds:
```markdown
Read the _manual_ before **wiring** the ~~relay~~.
An un*real*istic goal.
```
`adfToHtml`, round-trip holds:
```html
<p>Read the <em>manual</em> before <strong>wiring</strong> the <span class="adf-carry" data-adf="{&quot;marks&quot;:[{&quot;type&quot;:&quot;strike&quot;}],&quot;text&quot;:&quot;relay&quot;,&quot;type&quot;:&quot;text&quot;}">relay</span>.</p>
<p>An un<em>real</em>istic goal.</p>
```
## Headings
ADF:
```json
{"content":[{"attrs":{"level":1},"content":[{"text":"Assembly","type":"text"}],"type":"heading"},{"attrs":{"level":2},"content":[{"text":"Parts","type":"text"}],"type":"heading"},{"attrs":{"level":3},"content":[{"text":"Fasteners","type":"text"}],"type":"heading"},{"attrs":{"level":4},"content":[{"text":"Bolts","type":"text"}],"type":"heading"},{"attrs":{"level":5},"content":[{"text":"Sizes","type":"text"}],"type":"heading"},{"attrs":{"level":6},"content":[{"text":"M8","type":"text"}],"type":"heading"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
# Assembly
## Parts
### Fasteners
#### Bolts
##### Sizes
###### M8
```
`adfToPortableMarkdown`, round-trip holds:
```markdown
# Assembly
## Parts
### Fasteners
#### Bolts
##### Sizes
###### M8
```
`adfToHtml`, round-trip holds:
```html
<h1>Assembly</h1>
<h2>Parts</h2>
<h3>Fasteners</h3>
<h4>Bolts</h4>
<h5>Sizes</h5>
<h6>M8</h6>
```
## Bullet list
ADF:
```json
{"content":[{"content":[{"content":[{"content":[{"text":"Bolt M8","type":"text"}],"type":"paragraph"}],"type":"listItem"},{"content":[{"content":[{"text":"Nut M8","type":"text"}],"type":"paragraph"}],"type":"listItem"},{"content":[{"content":[{"text":"Washer M8","type":"text"}],"type":"paragraph"},{"content":[{"content":[{"content":[{"text":"Fibre","type":"text"}],"type":"paragraph"}],"type":"listItem"}],"type":"bulletList"}],"type":"listItem"}],"type":"bulletList"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
- Bolt M8
- Nut M8
- Washer M8
- Fibre
```
`adfToPortableMarkdown`, round-trip holds:
```markdown
- Bolt M8
- Nut M8
- Washer M8
- Fibre
```
`adfToHtml`, round-trip holds:
```html
<ul>
<li>Bolt M8</li>
<li>Nut M8</li>
<li>Washer M8
<ul>
<li>Fibre</li>
</ul>
</li>
</ul>
```
## Code block
ADF:
```json
{"content":[{"attrs":{"language":"sql"},"content":[{"text":"SELECT id\nFROM part\nWHERE qty > 0;\n- 1. the markers a container start would claim","type":"text"}],"type":"codeBlock"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
````markdown
```sql
SELECT id
FROM part
WHERE qty > 0;
- 1. the markers a container start would claim
```
````
`adfToPortableMarkdown`, round-trip holds:
````markdown
```sql
SELECT id
FROM part
WHERE qty > 0;
- 1. the markers a container start would claim
```
````
`adfToHtml`, round-trip holds:
```html
<pre><code class="language-sql">SELECT id
FROM part
WHERE qty &gt; 0;
- 1. the markers a container start would claim</code></pre>
```
## Quote
ADF:
```json
{"content":[{"content":[{"content":[{"text":"Ship it.","type":"text"}],"type":"paragraph"},{"content":[{"text":"Then tell them.","type":"text"}],"type":"paragraph"}],"type":"blockquote"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
> Ship it.
>
> Then tell them.
```
`adfToPortableMarkdown`, round-trip holds:
```markdown
> Ship it.
>
> Then tell them.
```
`adfToHtml`, round-trip holds:
```html
<blockquote>
<p>Ship it.</p>
<p>Then tell them.</p>
</blockquote>
```
## Link
ADF:
```json
{"content":[{"content":[{"text":"See ","type":"text"},{"marks":[{"attrs":{"href":"https://example.com/changelog"},"type":"link"}],"text":"the changelog","type":"text"},{"text":".","type":"text"}],"type":"paragraph"},{"content":[{"marks":[{"attrs":{"href":"https://example.com/"},"type":"link"}],"text":"https://example.com/","type":"text"}],"type":"paragraph"},{"content":[{"text":"Read ","type":"text"},{"marks":[{"attrs":{"href":"https://example.com/guide","title":"Setup guide"},"type":"link"}],"text":"the guide","type":"text"},{"text":".","type":"text"}],"type":"paragraph"},{"content":[{"text":"Open ","type":"text"},{"marks":[{"attrs":{"href":"https://example.com/the plan.pdf"},"type":"link"}],"text":"the plan","type":"text"},{"text":".","type":"text"}],"type":"paragraph"},{"content":[{"marks":[{"attrs":{"href":"/parts/m8"},"type":"link"}],"text":"/parts/m8","type":"text"}],"type":"paragraph"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
See [the changelog](https://example.com/changelog).
<https://example.com/>
Read [the guide](https://example.com/guide "Setup guide").
Open [the plan](<https://example.com/the plan.pdf>).
[/parts/m8](/parts/m8)
```
`adfToPortableMarkdown`, round-trip holds:
```markdown
See [the changelog](https://example.com/changelog).
<https://example.com/>
Read [the guide](https://example.com/guide "Setup guide").
Open [the plan](<https://example.com/the plan.pdf>).
[/parts/m8](/parts/m8)
```
`adfToHtml`, round-trip holds:
```html
<p>See <a href="https://example.com/changelog">the changelog</a>.</p>
<p><a href="https://example.com/">https://example.com/</a></p>
<p>Read <a href="https://example.com/guide" title="Setup guide">the guide</a>.</p>
<p>Open <a href="https://example.com/the plan.pdf">the plan</a>.</p>
<p><a href="/parts/m8">/parts/m8</a></p>
```
## Table
ADF:
```json
{"content":[{"content":[{"content":[{"content":[{"content":[{"text":"Part","type":"text"}],"type":"paragraph"}],"type":"tableHeader"},{"content":[{"content":[{"text":"Stock","type":"text"}],"type":"paragraph"}],"type":"tableHeader"}],"type":"tableRow"},{"content":[{"content":[{"content":[{"text":"Bolt M8","type":"text"}],"type":"paragraph"}],"type":"tableCell"},{"content":[{"content":[{"text":"120","type":"text"}],"type":"paragraph"}],"type":"tableCell"}],"type":"tableRow"},{"content":[{"content":[{"content":[{"text":"Washer M8","type":"text"}],"type":"paragraph"}],"type":"tableCell"},{"content":[{"content":[{"text":"45","type":"text"}],"type":"paragraph"}],"type":"tableCell"}],"type":"tableRow"}],"type":"table"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
| Part | Stock |
| --- | --- |
| Bolt M8 | 120 |
| Washer M8 | 45 |
```
`adfToPortableMarkdown`, round-trip holds:
```markdown
| Part | Stock |
| --- | --- |
| Bolt M8 | 120 |
| Washer M8 | 45 |
```
`adfToHtml`, round-trip holds:
```html
<table>
<thead>
<tr><th>Part</th><th>Stock</th></tr>
</thead>
<tbody>
<tr><td>Bolt M8</td><td>120</td></tr>
<tr><td>Washer M8</td><td>45</td></tr>
</tbody>
</table>
```
## Panels
ADF:
```json
{"content":[{"attrs":{"panelType":"warning"},"content":[{"content":[{"text":"Check the collation before importing.","type":"text"}],"type":"paragraph"}],"type":"panel"},{"attrs":{"localId":"01a0337c-561a-75aa-b629-3801f68839ad","panelType":"info"},"content":[{"content":[{"text":"Backups run at 02:00.","type":"text"}],"type":"paragraph"}],"type":"panel"},{"attrs":{"panelColor":"#c9372c","panelIcon":":rainbow:","panelIconId":"1f308","panelIconText":"🌈","panelType":"custom"},"content":[{"content":[{"text":"Weather permitting.","type":"text"}],"type":"paragraph"}],"type":"panel"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
!adf:panel warning
Check the collation before importing.
!adf:/panel
!adf:panel info {localId=01a0337c-561a-75aa-b629-3801f68839ad}
Backups run at 02:00.
!adf:/panel
!adf:panel custom {panelColor="#c9372c" panelIcon=":rainbow:" panelIconId=1f308 panelIconText="🌈"}
Weather permitting.
!adf:/panel
```
`adfToPortableMarkdown`, round-trip loses:
```markdown
> [!WARNING]
>
> Check the collation before importing.
> [!NOTE]
>
> Backups run at 02:00.
> [!NOTE]
>
> Weather permitting.
```
`adfToHtml`, round-trip holds:
```html
<div class="adf-carry" data-adf="{&quot;attrs&quot;:{&quot;panelType&quot;:&quot;warning&quot;},&quot;content&quot;:[{&quot;content&quot;:[{&quot;text&quot;:&quot;Check the collation before importing.&quot;,&quot;type&quot;:&quot;text&quot;}],&quot;type&quot;:&quot;paragraph&quot;}],&quot;type&quot;:&quot;panel&quot;}"><p>Check the collation before importing.</p></div>
<div class="adf-carry" data-adf="{&quot;attrs&quot;:{&quot;localId&quot;:&quot;01a0337c-561a-75aa-b629-3801f68839ad&quot;,&quot;panelType&quot;:&quot;info&quot;},&quot;content&quot;:[{&quot;content&quot;:[{&quot;text&quot;:&quot;Backups run at 02:00.&quot;,&quot;type&quot;:&quot;text&quot;}],&quot;type&quot;:&quot;paragraph&quot;}],&quot;type&quot;:&quot;panel&quot;}"><p>Backups run at 02:00.</p></div>
<div class="adf-carry" data-adf="{&quot;attrs&quot;:{&quot;panelColor&quot;:&quot;#c9372c&quot;,&quot;panelIcon&quot;:&quot;:rainbow:&quot;,&quot;panelIconId&quot;:&quot;1f308&quot;,&quot;panelIconText&quot;:&quot;🌈&quot;,&quot;panelType&quot;:&quot;custom&quot;},&quot;content&quot;:[{&quot;content&quot;:[{&quot;text&quot;:&quot;Weather permitting.&quot;,&quot;type&quot;:&quot;text&quot;}],&quot;type&quot;:&quot;paragraph&quot;}],&quot;type&quot;:&quot;panel&quot;}"><p>Weather permitting.</p></div>
```
## Task list
ADF:
```json
{"content":[{"attrs":{"localId":"0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90"},"content":[{"attrs":{"localId":"0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1","state":"DONE"},"content":[{"text":"Write the spec","type":"text"}],"type":"taskItem"},{"attrs":{"localId":"0198f3a2-9e63-7d80-a15b-6fa04e3c8db2","state":"TODO"},"content":[{"text":"Ship it","type":"text"}],"type":"taskItem"},{"attrs":{"localId":"01a033b2-95c8-7d62-9b9c-dee7ad510982","state":"TODO"},"content":[{"content":[{"text":"Check the torque.","type":"text"}],"type":"paragraph"},{"content":[{"text":"Then sign off.","type":"text"}],"type":"paragraph"}],"type":"blockTaskItem"},{"attrs":{"localId":"01a033b3-d1fa-7489-8c9f-f5846d2c249d"},"content":[{"attrs":{"localId":"01a033bd-a22d-7fb5-be9d-89d1aa261112","state":"TODO"},"content":[{"text":"Nested step","type":"text"}],"type":"taskItem"},{"attrs":{"state":"TODO"},"type":"taskItem"}],"type":"taskList"}],"type":"taskList"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
!adf:taskList {localId=0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90}
!adf:taskItem DONE {localId=0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1}
Write the spec
!adf:/taskItem
!adf:taskItem TODO {localId=0198f3a2-9e63-7d80-a15b-6fa04e3c8db2}
Ship it
!adf:/taskItem
!adf:blockTaskItem TODO {localId=01a033b2-95c8-7d62-9b9c-dee7ad510982}
Check the torque.
Then sign off.
!adf:/blockTaskItem
!adf:taskList {localId=01a033b3-d1fa-7489-8c9f-f5846d2c249d}
!adf:taskItem TODO {localId=01a033bd-a22d-7fb5-be9d-89d1aa261112}
Nested step
!adf:/taskItem
!adf:taskItem TODO
!adf:/taskItem
!adf:/taskList
!adf:/taskList
```
`adfToPortableMarkdown`, round-trip loses:
```markdown
- [x] Write the spec
- [ ] Ship it
- [ ] Check the torque.
Then sign off.
- [ ] Nested step
- [ ]
```
`adfToHtml`, round-trip holds:
```html
<div class="adf-carry" data-adf="{&quot;attrs&quot;:{&quot;localId&quot;:&quot;0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90&quot;},&quot;content&quot;:[{&quot;attrs&quot;:{&quot;localId&quot;:&quot;0198f3a2-8d52-70b1-8c4f-5e9f3d2b7ca1&quot;,&quot;state&quot;:&quot;DONE&quot;},&quot;content&quot;:[{&quot;text&quot;:&quot;Write the spec&quot;,&quot;type&quot;:&quot;text&quot;}],&quot;type&quot;:&quot;taskItem&quot;},{&quot;attrs&quot;:{&quot;localId&quot;:&quot;0198f3a2-9e63-7d80-a15b-6fa04e3c8db2&quot;,&quot;state&quot;:&quot;TODO&quot;},&quot;content&quot;:[{&quot;text&quot;:&quot;Ship it&quot;,&quot;type&quot;:&quot;text&quot;}],&quot;type&quot;:&quot;taskItem&quot;},{&quot;attrs&quot;:{&quot;localId&quot;:&quot;01a033b2-95c8-7d62-9b9c-dee7ad510982&quot;,&quot;state&quot;:&quot;TODO&quot;},&quot;content&quot;:[{&quot;content&quot;:[{&quot;text&quot;:&quot;Check the torque.&quot;,&quot;type&quot;:&quot;text&quot;}],&quot;type&quot;:&quot;paragraph&quot;},{&quot;content&quot;:[{&quot;text&quot;:&quot;Then sign off.&quot;,&quot;type&quot;:&quot;text&quot;}],&quot;type&quot;:&quot;paragraph&quot;}],&quot;type&quot;:&quot;blockTaskItem&quot;},{&quot;attrs&quot;:{&quot;localId&quot;:&quot;01a033b3-d1fa-7489-8c9f-f5846d2c249d&quot;},&quot;content&quot;:[{&quot;attrs&quot;:{&quot;localId&quot;:&quot;01a033bd-a22d-7fb5-be9d-89d1aa261112&quot;,&quot;state&quot;:&quot;TODO&quot;},&quot;content&quot;:[{&quot;text&quot;:&quot;Nested step&quot;,&quot;type&quot;:&quot;text&quot;}],&quot;type&quot;:&quot;taskItem&quot;},{&quot;attrs&quot;:{&quot;state&quot;:&quot;TODO&quot;},&quot;type&quot;:&quot;taskItem&quot;}],&quot;type&quot;:&quot;taskList&quot;}],&quot;type&quot;:&quot;taskList&quot;}"><p>Write the spec</p><p>Ship it</p><p>Check the torque.</p><p>Then sign off.</p><p>Nested step</p></div>
```
## Image
ADF:
```json
{"content":[{"content":[{"text":"Taken from the roof last night.","type":"text"}],"type":"paragraph"},{"attrs":{"layout":"center"},"content":[{"attrs":{"alt":"The moon, at night.","type":"external","url":"https://example.com/moon.png"},"type":"media"}],"type":"mediaSingle"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
Taken from the roof last night.
![The moon, at night.](https://example.com/moon.png)
```
`adfToPortableMarkdown`, round-trip holds:
```markdown
Taken from the roof last night.
![The moon, at night.](https://example.com/moon.png)
```
`adfToHtml`, round-trip holds:
```html
<p>Taken from the roof last night.</p>
<p><img src="https://example.com/moon.png" alt="The moon, at night."></p>
```
## Mention
ADF:
```json
{"content":[{"content":[{"text":"Ping ","type":"text"},{"attrs":{"accessLevel":"CONTAINER","id":"01a032c3-7a7c-775f-a730-2d79351338b4","localId":"01a033ad-ecb7-7530-aa1c-ff25031e96e2","text":"@Mikael","userType":"DEFAULT"},"type":"mention"},{"text":" before merging.","type":"text"}],"type":"paragraph"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
Ping !adf:mention[@Mikael]{accessLevel=CONTAINER id=01a032c3-7a7c-775f-a730-2d79351338b4 localId=01a033ad-ecb7-7530-aa1c-ff25031e96e2 userType=DEFAULT} before merging.
```
`adfToPortableMarkdown`, round-trip loses:
```markdown
Ping @Mikael before merging.
```
`adfToHtml`, round-trip holds:
```html
<p>Ping <span class="adf-mention" data-access-level="CONTAINER" data-id="01a032c3-7a7c-775f-a730-2d79351338b4" data-local-id="01a033ad-ecb7-7530-aa1c-ff25031e96e2" data-user-type="DEFAULT">@Mikael</span> before merging.</p>
```
## Status
ADF:
```json
{"content":[{"content":[{"text":"The rebuild is ","type":"text"},{"attrs":{"color":"yellow","localId":"01a033a7-d68a-71a9-a58a-edf5757ad176","style":"bold","text":"In review"},"type":"status"},{"text":" until Friday.","type":"text"}],"type":"paragraph"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
The rebuild is !adf:status[In review]{color=yellow localId=01a033a7-d68a-71a9-a58a-edf5757ad176 style=bold} until Friday.
```
`adfToPortableMarkdown`, round-trip loses:
```markdown
The rebuild is In review until Friday.
```
`adfToHtml`, round-trip holds:
```html
<p>The rebuild is <span class="adf-status" data-color="yellow" data-local-id="01a033a7-d68a-71a9-a58a-edf5757ad176" data-style="bold">In review</span> until Friday.</p>
```
## Date
ADF:
```json
{"content":[{"content":[{"text":"Due ","type":"text"},{"attrs":{"timestamp":"1756080000000"},"type":"date"},{"text":", reviewed ","type":"text"},{"attrs":{"localId":"01a033ab-781c-7f79-9c8a-fb63356c1cad","timestamp":"1735689600000"},"type":"date"},{"text":".","type":"text"}],"type":"paragraph"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
```markdown
Due !adf:date{timestamp=1756080000000}, reviewed !adf:date{localId=01a033ab-781c-7f79-9c8a-fb63356c1cad timestamp=1735689600000}.
```
`adfToPortableMarkdown`, round-trip loses:
```markdown
Due 2025-08-25, reviewed 2025-01-01.
```
`adfToHtml`, round-trip holds:
```html
<p>Due <time class="adf-date" datetime="2025-08-25" data-timestamp="1756080000000">2025-08-25</time>, reviewed <time class="adf-date" datetime="2025-01-01" data-local-id="01a033ab-781c-7f79-9c8a-fb63356c1cad" data-timestamp="1735689600000">2025-01-01</time>.</p>
```
## Node types this version does not know
ADF:
```json
{"content":[{"attrs":{"url":"https://example.com/roadmap"},"type":"blockCard"},{"attrs":{"panelType":"info"},"content":[{"content":[{"text":"The card below has no spelling yet.","type":"text"}],"type":"paragraph"},{"attrs":{"layout":"wide","url":"https://example.com/board","width":100},"type":"embedCard"}],"type":"panel"}],"type":"doc","version":1}
```
`adfToLosslessMarkdown`, round-trip holds:
````markdown
```adf:blockCard
{
"attrs": {
"url": "https://example.com/roadmap"
}
}
```
!adf:panel info
The card below has no spelling yet.
```adf:embedCard
{
"attrs": {
"layout": "wide",
"url": "https://example.com/board",
"width": 100
}
}
```
!adf:/panel
````
`adfToPortableMarkdown`, round-trip loses:
```markdown
<https://example.com/roadmap>
> [!NOTE]
>
> The card below has no spelling yet.
>
> <https://example.com/board>
```
`adfToHtml`, round-trip holds:
```html
<div class="adf-carry" data-adf="{&quot;attrs&quot;:{&quot;url&quot;:&quot;https://example.com/roadmap&quot;},&quot;type&quot;:&quot;blockCard&quot;}"><p><a href="https://example.com/roadmap">https://example.com/roadmap</a></p></div>
<div class="adf-carry" data-adf="{&quot;attrs&quot;:{&quot;panelType&quot;:&quot;info&quot;},&quot;content&quot;:[{&quot;content&quot;:[{&quot;text&quot;:&quot;The card below has no spelling yet.&quot;,&quot;type&quot;:&quot;text&quot;}],&quot;type&quot;:&quot;paragraph&quot;},{&quot;attrs&quot;:{&quot;layout&quot;:&quot;wide&quot;,&quot;url&quot;:&quot;https://example.com/board&quot;,&quot;width&quot;:100},&quot;type&quot;:&quot;embedCard&quot;}],&quot;type&quot;:&quot;panel&quot;}"><p>The card below has no spelling yet.</p><p><a href="https://example.com/board">https://example.com/board</a></p></div>
```
+16
View File
@@ -0,0 +1,16 @@
import { readFileSync, writeFileSync } from 'node:fs'
import * as library from '@larvit/adf-codec'
import { renderFeatureTable } from './render.mjs'
const pagePath = new URL('../docs/feature-table.md', import.meta.url)
const readJson = (url) => JSON.parse(readFileSync(url, 'utf8'))
const examples = readJson(new URL('examples.json', import.meta.url)).map(({ id, name }) => ({ doc: readJson(new URL(`../corpus/round-trip/${id}.json`, import.meta.url)), name }))
const page = renderFeatureTable(examples, library)
if (!process.argv.includes('--check')) writeFileSync(pagePath, page)
else if (readFileSync(pagePath, 'utf8') !== page) {
console.error('docs/feature-table.md differs from what the examples and the build give: run ./feature-table/write.sh')
process.exitCode = 1
}
+17
View File
@@ -0,0 +1,17 @@
[
{ "id": "combinations/bold-command", "name": "Bold instruction with a command in it" },
{ "id": "commonmark-subset/em-strong-strike", "name": "Emphasis and strikethrough" },
{ "id": "commonmark-subset/heading", "name": "Headings" },
{ "id": "commonmark-subset/bullet-list", "name": "Bullet list" },
{ "id": "commonmark-subset/code-block", "name": "Code block" },
{ "id": "commonmark-subset/blockquote", "name": "Quote" },
{ "id": "commonmark-subset/link", "name": "Link" },
{ "id": "block-nodes/table-plain", "name": "Table" },
{ "id": "block-nodes/panel", "name": "Panels" },
{ "id": "block-nodes/task-list", "name": "Task list" },
{ "id": "block-nodes/image", "name": "Image" },
{ "id": "inline-nodes/mention", "name": "Mention" },
{ "id": "inline-nodes/status", "name": "Status" },
{ "id": "inline-nodes/date", "name": "Date" },
{ "id": "opaque-carry/unknown-block", "name": "Node types this version does not know" }
]
+41
View File
@@ -0,0 +1,41 @@
import { isDeepStrictEqual } from 'node:util'
const writers = [
{ column: 'Lossless', fence: 'markdown', reader: 'losslessMarkdownToAdf', writer: 'adfToLosslessMarkdown' },
{ column: 'Portable', fence: 'markdown', reader: 'portableMarkdownToAdf', writer: 'adfToPortableMarkdown' },
{ column: 'HTML', fence: 'html', reader: 'htmlToAdf', writer: 'adfToHtml' },
]
const intro = `# What each call writes
\`feature-table/build.mjs\` writes this page from the corpus and the built package, and \`./ci.sh\` fails where the page differs.
Each output is read back by its format's reader. \`holds\` means that gives the same document, \`loses\` a different one, and \`refused\` an error whose code the example names.
`
// `library` holds the package's calls, so a test can hand in what a real document cannot make them do.
export function renderFeatureTable(examples, library) {
const written = examples.map(({ doc, name }) => ({ doc, name, outcomes: writers.map((writer) => outcome(doc, writer, library)) }))
const rows = written.map(({ name, outcomes }) => `| [${name}](#${name.toLowerCase().replaceAll(' ', '-')}) | ${outcomes.map(({ result }) => result).join(' | ')} |`)
const head = `| Example | ${writers.map(({ column }) => column).join(' | ')} |\n| --- | --- | --- | --- |`
return [intro, [head, ...rows].join('\n') + '\n', ...written.map(section)].join('\n')
}
function outcome(doc, { fence, reader, writer }, library) {
const output = library[writer](doc)
if (!output.ok) return { label: `\`${writer}\` refuses with \`${output.error.code}\`.\n`, result: 'refused' }
const read = library[reader](output.value)
if (!read.ok) return { label: `\`${writer}\`, round-trip refused with \`${read.error.code}\`:\n\n${fenced(fence, output.value)}`, result: 'refused' }
const result = isDeepStrictEqual(read.value, doc) ? 'holds' : 'loses'
return { label: `\`${writer}\`, round-trip ${result}:\n\n${fenced(fence, output.value)}`, result }
}
function section({ doc, name, outcomes }) {
return [`## ${name}\n`, `ADF:\n\n${fenced('json', `${JSON.stringify(doc)}\n`)}`, ...outcomes.map(({ label }) => label)].join('\n')
}
function fenced(language, text) {
const longest = Math.max(2, ...[...text.matchAll(/`+/g)].map(([run]) => run.length))
const fence = '`'.repeat(longest + 1)
return `${fence}${language}\n${text}${fence}\n`
}
+43
View File
@@ -0,0 +1,43 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { renderFeatureTable } from './render.mjs'
const doc = { content: [], type: 'doc', version: 1 }
const held = { ok: true, value: doc }
const refused = { error: { code: 'unsupported-node-shape' }, ok: false }
function library(overrides) {
const writes = (value) => () => ({ ok: true, value })
return { adfToHtml: writes('<p>a</p>\n'), adfToLosslessMarkdown: writes('a\n'), adfToPortableMarkdown: writes('a\n'), htmlToAdf: () => held, losslessMarkdownToAdf: () => held, portableMarkdownToAdf: () => held, ...overrides }
}
test('leads with a row per example linking its section, then the section labels each output', () => {
const page = renderFeatureTable([{ doc, name: 'Some example' }], library())
assert.ok(page.includes('| [Some example](#some-example) | holds | holds | holds |\n'))
assert.ok(page.includes(`## Some example\n\nADF:\n\n\`\`\`json\n${JSON.stringify(doc)}\n\`\`\`\n\n\`adfToLosslessMarkdown\`, round-trip holds:\n\n\`\`\`markdown\na\n\`\`\`\n`))
assert.ok(page.includes('`adfToHtml`, round-trip holds:\n\n```html\n<p>a</p>\n```\n'))
})
test('says the round-trip loses where the output reads back to another document', () => {
const page = renderFeatureTable([{ doc, name: 'Some example' }], library({ portableMarkdownToAdf: () => ({ ok: true, value: { ...doc, content: [{ type: 'rule' }] } }) }))
assert.ok(page.includes('| holds | loses | holds |'))
assert.ok(page.includes('`adfToPortableMarkdown`, round-trip loses:'))
})
test('compares documents whatever their key order', () => {
const page = renderFeatureTable([{ doc, name: 'Some example' }], library({ htmlToAdf: () => ({ ok: true, value: { version: 1, type: 'doc', content: [] } }) }))
assert.ok(page.includes('| holds | holds | holds |'))
})
test('names the code where a writer or a reader refuses', () => {
const page = renderFeatureTable([{ doc, name: 'Some example' }], library({ adfToHtml: () => refused, losslessMarkdownToAdf: () => refused }))
assert.ok(page.includes('| refused | holds | refused |'))
assert.ok(page.includes('`adfToLosslessMarkdown`, round-trip refused with `unsupported-node-shape`:\n\n```markdown\na\n```\n'))
assert.ok(page.includes('`adfToHtml` refuses with `unsupported-node-shape`.\n'))
})
test('fences an output in more backticks than it holds', () => {
const page = renderFeatureTable([{ doc, name: 'Some example' }], library({ adfToLosslessMarkdown: () => ({ ok: true, value: '````js\nx\n````\n' }) }))
assert.ok(page.includes('`````markdown\n````js\nx\n````\n`````\n'))
})
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
source ./docker-runner.sh
in_image_memory=4g
leg "install ($node_image)" in_image "$node_image" npm ci
leg "build ($node_image)" in_image "$node_image" npm run build
leg "write the feature table ($node_image)" in_image "$node_image" node feature-table/build.mjs
+10 -44
View File
@@ -6,18 +6,16 @@
`Bar = 0.1`
`Next ID = 168`
`Next ID = 169`
## Items
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 164 | 0.3.0 | | | **Publish a feature table setting chosen corpus examples beside what each call writes for them, readable at a glance by people and parseable by LLMs, and kept current by the gate.** | 3 | 2 | 1 | 4 | 5 | 3 by 6, 6 by 4 | 6.2 |
| 165 | 0.3.0 | 164 | | **Have a reader panel seated from `## Audience` judge each feature-table row reasonable or not, and file an item for each row it rejects.** | 2 | 2 | 1 | 3 | 3 | 6 by 5 | 7.5 |
| 132 | 0.3.0 | | | **Write tables as `<table>` in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 5 | 6 by 4 | 2.4 |
| 73 | 0.3.0 | 132, 165 | | **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 | 16.9 |
| 167 | 0.3.0 | | decision | **Refuse in `refusedShape` the schema-forbidden shapes `adfToLosslessMarkdown` refuses in its walk, and drop them from that writer.** | 3 | 1 | 2 | 4 | 5 | code health by 5 | 13.1 |
| 72 | 0.3.0 | 132 | 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.5 |
| 168 | 0.3.0 | | | **Lower the reading cost of the files a chunk reads most before its first commit.** | 2 | 1 | 1 | 2 | 4 | code health by 5 | 21.8 |
| 165 | 0.3.0 | | | **Have a reader panel seated from `## Audience` judge each feature-table row reasonable or not, and file an item for each row it rejects.** | 2 | 2 | 1 | 3 | 3 | 6 by 5 | 7.5 |
| 73 | 0.3.0 | 165 | | **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 | 16.9 |
| 72 | 0.3.0 | | 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.5 |
| 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 |
| 156 | 0.4.0 | | | **Stop a leg's container when its script is interrupted, so Ctrl-C ends `ci.sh` or `mutation-tests/run.sh` at once.** | 1 | 2 | 1 | 2 | 1 | code health by 2 | 20.9 |
| 128 | 0.4.0 | | | **Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks.** | 2 | 1 | 1 | 2 | 3 | code health by 3 | 19.6 |
@@ -67,19 +65,17 @@
## Details
### 164. Publish a feature table setting chosen corpus examples beside what each call writes for them, readable at a glance by people and parseable by LLMs, and kept current by the gate.
### 168. Lower the reading cost of the files a chunk reads most before its first commit.
The examples are picked by an explicit list of corpus ids, each with a one-line name such as "bold instruction with a command in it". For each one, the table shows the ADF, `adfToLosslessMarkdown`, `adfToPortableMarkdown` and `adfToHtml` output, and whether the round-trip holds. A script builds the table from the built package. The gate fails when the committed table differs from what the script writes, so the table always shows what the release does. A writer panel from `## Audience` picks the layout, rendered or source, before it ships.
The last 5 chunks' median was 215876 new input tokens before their first commit, and the 5 before
them 147097. Most read: `todo.md` 36, `docs/decisions.md` 25, `README.md` 21, `spec/flavour.md` 16,
`ci.sh` 15, `src/html/parse/html-to-adf.test.ts` 13, `src/markdown/emit/adf-to-markdown.test.ts` 11,
`CHANGELOG.md` 9, `src/conformance/corpus.test.ts` 8, `MIGRATION.md` 7.
### 165. Have a reader panel seated from `## Audience` judge each feature-table row reasonable or not, and file an item for each row it rejects.
The panel follows AGENTS.md §7's reader-panel rule: readers, one per persona the row serves, see only `## Audience` and the row, and say whether the output is what they expect. A rejected row becomes an item naming the expected output. The verdicts land in `docs/decisions.md`.
### 132. Write tables as `<table>` in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.
Header cells as `<th>`; a cell's span, width and background in HTML attributes or `data-*`, as a
writer panel picks.
### 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
@@ -96,36 +92,6 @@ solves it, then links the GitHub repository, the README's comparison table and t
opening. The maintainer posts them once `0.3.0` is published (`docs/decisions.md` §Posts
announcing a release wait for its publish). Found while planning discoverability, 2026-10-04.
### 167. Refuse in `refusedShape` the schema-forbidden shapes `adfToLosslessMarkdown` refuses in its walk, and drop them from that writer.
The maintainer's call, 2026-11-04 (`docs/decisions.md` §Schema refusals are made in the first step
alone): the first step holds every schema check, "moved there from wherever else in the code they
are". It replaces item 68, which would have carried these shapes. Move into `refusedShape`:
- `emitDirectiveBlock` in `emit/adf-to-markdown.ts`: a block holding `text`, and a leaf block
holding `content`.
- `emitLeaf` in `emit/inline-line.ts`: a non-text inline node holding `content` or `text`.
Atlassian's schema gives each such node no `text` or `content` (`additionalProperties` is false).
Breaking: `adfToHtml` and `adfToPortableMarkdown` refuse these shapes too, and so does
`adfToLosslessMarkdown` inside a node that rides the carry, where it writes them today. Both carry
readers refuse them too, so a stored markdown or HTML document whose carry holds one stops
reading; `MIGRATION.md` says to convert it once with the last version that reads it. Say so in
`CHANGELOG.md` and `MIGRATION.md`. Search the writers for any other refusal of a shape the schema
forbids, and `carriedNode` for one the readers still build.
The aim is the same behaviour in and out, with the checks alike in every format. The breaking
change above is what "alike" costs: `refusedShape` takes no format, so it cannot spare a node that
only one writer carries. The maintainer accepted it (2026-11-04); the item adds no other refusal.
Stays where it is: a node holding one mark type twice (the schema allows it, no spelling holds it),
nesting depth, output length, a portable task list holding what no list item spells, and a
reader's refusal of markdown or HTML it cannot read. A node type, mark or attribute the schema does
not name, a known node out of place and a mark a node cannot hold keep riding the carry. Drop the
refused shapes from the README's `unsupported-node-shape` cause under writing in
`adfToLosslessMarkdown` and add them to the cause common to the writers, and from `spec/flavour.md`;
the property generators build none of them.
### 72. 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.
Blocked by every other `0.3.0` item but 73 (the maintainer, 2026-10-05; moved to `0.3.0` 2026-10-28,