From 770be6be050a9797df4c3dfc4ebf7c00669190ad Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 9 Oct 2026 02:42:55 +0200 Subject: [PATCH] Answer architecture round 1: the claims type pins what every writer spells, the HTML calls take flavour names, model-written GFM reads in portableMarkdownToAdf --- CHANGELOG.md | 3 ++- MIGRATION.md | 8 +++---- README.md | 14 ++++++------ docs/comprehension-history.md | 26 +++++++++++++++++++++++ docs/decisions.md | 3 ++- src/conformance/markdown-property.test.ts | 1 - src/markdown/portable/conventions.ts | 6 +++++- todo.md | 10 +++++---- 8 files changed, 52 insertions(+), 19 deletions(-) create mode 100644 docs/comprehension-history.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d2d9ac3..43659d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,7 +62,8 @@ once the process had read a key holding a backslash ([V8 bug](https://issues.chromium.org/issues/521080746)). - Fix a cyclic document, in which an object holds itself: `adfToLosslessMarkdown` and - `adfToPortableMarkdown` refuse it as `not-an-adf-document` and `isAdfDocument` returns `false`, where all three hung. + `adfToPortableMarkdown` refuse it as `not-an-adf-document` and `isAdfDocument` returns `false`, + where all three hung. ## 0.1.0 diff --git a/MIGRATION.md b/MIGRATION.md index 596805d..bb6a1b9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,7 +8,7 @@ Each call names the markdown it reads or writes: | --- | --- | | `adfToMarkdown` | `adfToLosslessMarkdown` | | `markdownToAdf`, for markdown `adfToMarkdown` wrote | `losslessMarkdownToAdf` | -| `markdownToAdf`, for markdown a person or a model wrote | `commonMarkToAdf`, which reads a directive, a pipe table, `~~` and an `adf:` fence as CommonMark does, as text and code | +| `markdownToAdf`, for markdown a person or a model wrote | `portableMarkdownToAdf`, which keeps GFM's pipe tables and `~~`; or `commonMarkToAdf`, which reads them, a directive and an `adf:` fence as CommonMark does, as text and code | Directives moved under the `!adf:` prefix. `losslessMarkdownToAdf` reads `0.1.0`'s spelling without an error, turning each directive into text and each carried node into an `adf` code block. Before @@ -72,9 +72,9 @@ Markdown the spelling table leaves alone, which `0.2.0` reads as a different doc `unspellable-character`, `unspellable-line-start` and `unspellable-link` leave `ConvertErrorCode` and `not-a-string` joins it: a `switch` naming one of the three stops compiling, and one naming -every code needs the new case. A document `adfToMarkdown` refused with one of the three -`adfToLosslessMarkdown` now converts; markdown the parser refused with one of the three now refuses with the code the table -gives. +every code needs the new case. A document `adfToMarkdown` refused with one of the three now +converts in `adfToLosslessMarkdown`; markdown the parser refused with one of the three now refuses +with the code the table gives. | Input | `0.1.0` | `0.2.0` | | --- | --- | --- | diff --git a/README.md b/README.md index 9fc0c57..9e7e676 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,8 @@ products store as ADF. - **Lossless:** `adfToLosslessMarkdown` and `losslessMarkdownToAdf` give back the document they started from — panels, mentions, statuses and node types this version does not know included. - **CommonMark in:** `commonMarkToAdf` reads a bot's or a model's markdown as the CommonMark spec - says, apart from raw HTML and the exceptions [The guarantees](#the-guarantees) lists. + says, apart from raw HTML and the exceptions [The guarantees](#the-guarantees) lists; markdown + holding GFM's pipe tables or `~~` reads in `portableMarkdownToAdf`. - **Portable markdown, lossy:** `adfToPortableMarkdown` and `portableMarkdownToAdf` convert to and from the markdown that GitHub, GitLab and Obsidian render — GitHub Flavored Markdown's alerts, task lists and pipe tables, Obsidian Flavored Markdown's callouts and highlights. @@ -19,7 +20,8 @@ products store as ADF. save edits back, use the lossless pair. - **No throws:** every call returns a result; a refusal carries a code from a closed list. - **Runs anywhere:** pure functions with no runtime dependencies, in Node, Deno, Bun and browsers. -- **HTML at `0.3.0`:** `adfToHtml`, `htmlToAdf`, `markdownToHtml`, `htmlToMarkdown`. +- **HTML at `0.3.0`:** `adfToHtml`, `htmlToAdf`, and markdown to and from HTML through ADF, each + call named by its markdown flavour. [The shape](#the-shape) · [Goals](#goals) · [Audience](#audience) · [Portable markdown](#portable-markdown) · [The errors](#the-errors) · @@ -49,8 +51,8 @@ if (result.ok) { ``` Serves [Goals](#goals) 1, 2 and 8. Pure functions, each taking a whole document and returning a -whole result; no I/O, no configuration. `markdownToHtml` and `htmlToMarkdown` convert through ADF: -they keep only what ADF holds, and refuse what `losslessMarkdownToAdf` or `htmlToAdf` refuses. +whole result; no I/O, no configuration. The `0.3.0` calls between markdown and HTML convert through +ADF: they keep only what ADF holds, and refuse what their markdown reader or `htmlToAdf` refuses. ```ts adfToLosslessMarkdown(doc: AdfDocument): Result @@ -64,8 +66,6 @@ portableMarkdownToAdf(markdown: string): Result adfToHtml(doc: AdfDocument): Result // 0.3.0 htmlToAdf(html: string): Result // 0.3.0 -markdownToHtml(markdown: string): Result // 0.3.0 -htmlToMarkdown(html: string): Result // 0.3.0 ``` `Result` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws. @@ -170,7 +170,7 @@ stable across minors and safe to `switch` on exhaustively with no `default`; `me and may change in any release. A parse always names a position, so the three markdown readers return `ParseError`, whose `position` reads without a guard; an emit reads no source and carries `path` alone; one handler typed on `ConvertError` takes both, which is what the -composed `markdownToHtml` and `htmlToMarkdown` hand back. `path` is the node's place from the +calls between markdown and HTML hand back. `path` is the node's place from the document root, alternating `'content'` and an index, so `path.map((step) => '/' + step).join('')` is a JSON Pointer at the node — the empty path being the document itself. diff --git a/docs/comprehension-history.md b/docs/comprehension-history.md new file mode 100644 index 0000000..8c0bc38 --- /dev/null +++ b/docs/comprehension-history.md @@ -0,0 +1,26 @@ +# Comprehension history + +## 2026-10-09T00:41:03Z, PR #19 at 859c952, against 9519606 + +Ruling: better + +| Seat | Vote | +|---|---| +| Mid A | better | +| Inherited architect | better | + +Mid A, decided by: + +- `src/markdown/portable/task-ids.ts:7` `mintTaskIds`: one walk, no carried set filled by the parser as a side effect +- `src/markdown/parse/markdown-to-adf.ts:26` `Reading`: plain data, `codeBlockNode` no longer mutates `carried` +- `src/markdown/portable/conventions.ts:8` `flavourClaims`: one table states what each reader claims; branches name the claim they test +- `src/markdown/parse/markdown-to-adf.ts:161` `quoteNode`: the title is read in the reader's own flavour, no hop through `inlineLeaves` +- `src/index.ts:5` exports: each call named by its flavour; "plain" no longer means two things + +Inherited architect, decided by: + +- `src/markdown/parse/markdown-to-adf.ts:26` `Reading`: the hidden `carried` set and the retrying `mintTaskIds` are gone +- `src/markdown/parse/markdown-to-adf.ts:161` `quoteNode`: parser and emitter no longer correct only together +- `src/markdown/parse/markdown-to-adf.ts:113` `readBlock`: branches name the claim (`claims.alerts`, `claims.taskMarkers`) +- `src/index.ts:5` exports: flavour names say which file to open +- `src/markdown/portable/conventions.ts:3` `flavourClaims`: cost, all three flavours' rows live under `portable/` diff --git a/docs/decisions.md b/docs/decisions.md index d7a1562..fbd9f94 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -13,7 +13,8 @@ A function's name says which markdown it reads or writes, and a reader reads tha | `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 | -No `adfToCommonMark`: the portable writer serves every persona a CommonMark writer would. The panel +The calls between markdown and HTML that `todo.md` item 7 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. diff --git a/src/conformance/markdown-property.test.ts b/src/conformance/markdown-property.test.ts index 85c52ad..1a7b93c 100644 --- a/src/conformance/markdown-property.test.ts +++ b/src/conformance/markdown-property.test.ts @@ -444,7 +444,6 @@ test('generated markdown refuses, or its spelling reads back and spells itself', assert.ok(directiveShaped >= directiveShapedFloor, `${directiveShaped} runs reaching the fixpoint held a node or mark outside CommonMark's own types, under the floor of ${directiveShapedFloor}`) }) - test('generated markdown read as CommonMark refuses, or its document reads back through the lossless pair', { timeout: propertyTimeout }, () => { fc.assert( fc.property(document, (input) => { diff --git a/src/markdown/portable/conventions.ts b/src/markdown/portable/conventions.ts index e303ef8..632d6fd 100644 --- a/src/markdown/portable/conventions.ts +++ b/src/markdown/portable/conventions.ts @@ -15,7 +15,11 @@ export type Claims = { taskMarkers: boolean } -export const flavourClaims: Readonly> = { +// The writer spells pipe tables and `~~` in every flavour it writes. +type WrittenClaims = Claims & { pipeTables: true; strikethrough: true } + +// No flavour claims directives beside alerts or highlights: an expand directive leaves `inExpand` unset and a directive's slot reads `==`. +export const flavourClaims: Readonly<{ commonmark: Claims } & Record> = { commonmark: { alerts: false, carryFence: false, directives: false, highlights: false, pipeTables: false, strikethrough: false, taskMarkers: false }, lossless: { alerts: false, carryFence: true, directives: true, highlights: false, pipeTables: true, strikethrough: true, taskMarkers: false }, portable: { alerts: true, carryFence: false, directives: false, highlights: true, pipeTables: true, strikethrough: true, taskMarkers: true }, diff --git a/todo.md b/todo.md index a36b7a0..bb8a3f0 100644 --- a/todo.md +++ b/todo.md @@ -32,7 +32,7 @@ | 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 | | 62 | 0.2.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/`.** | 4 | 5 | 2 | 6 | 2 | 9.4 | | 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 4, 5, 7 | 9.0 | -| 7 | 0.3.0 | | **Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 | +| 7 | 0.3.0 | decision | **Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 | | 45 | 0.3.0 | | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 3 | 6 | 8 | 1 | 25.2 | | 6 | 0.3.0 | decision | **Specify the HTML dialect.** | 2 | 6 | 7 | 8 | 2, 4 | 24.7 | | 77 | 0.3.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.** | 4 | 4 | 5 | 9 | 1 | 23.0 | @@ -106,6 +106,8 @@ parts by ADF and format true as they grow. Eight directive files sit across three directories, and the `markdown/` root holds 14 entries. HTML needs `Read` and `Flavour` out of the portable flavour and `markdown/`; it needs the carry and the mark spellings too, which item 7 moves where it learns what HTML shares. +Once moved, a reader takes a `Flavour` and looks up its claims, as a writer does, so no reader is +handed a set of claims no flavour holds. ### 62. Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`. @@ -124,10 +126,10 @@ link mark, so a link whose text is empty takes its URL as its text (the maintain Goals 4 and 7. Breaking: `MIGRATION.md`'s Readings table gains its row. Its `pending` exceptions go, and its spelling leaves the README's "Four CommonMark spellings" bullet, which counts one fewer. -### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF. +### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour. -Lands after items 6, 59, 60, 61 and 62. The CommonMark spec suite also runs against -`markdownToHtml`. The README documents HTML as it documents markdown. `htmlToAdf` refuses a value +Lands after items 6, 59, 60, 61 and 62. Which flavours get a composed call is this item's to +settle; the CommonMark spec suite also runs against the one reading CommonMark. The README documents HTML as it documents markdown. `htmlToAdf` refuses a value that is not a string as `not-a-string` (`docs/decisions.md` §Which code a cause takes). ### 45. Replace `isAdfDocument` with a reader returning `Result`.