From ae519e233e590f6cbb4c607c510c8636ded5972c Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 00:15:26 +0200 Subject: [PATCH 1/7] Flavour spec 1a: the directive grammar, canonical form, opaque carry --- README.md | 3 +- spec/flavour.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++ todo.md | 15 ++++++---- 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 spec/flavour.md diff --git a/README.md b/README.md index 569d55c..974e074 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ Lossless conversion between **Atlassian Document Format** (ADF), an extended markdown flavour, and an HTML dialect. -**Status: scaffold only, no conversion code yet.** Plan: `todo.md`. Decisions: `AGENTS.md`. +**Status: scaffold only, no conversion code yet.** Plan: `todo.md`. Decisions: `AGENTS.md`. The +flavour's grammar: [`spec/flavour.md`](spec/flavour.md). ## What it is for diff --git a/spec/flavour.md b/spec/flavour.md new file mode 100644 index 0000000..964fdda --- /dev/null +++ b/spec/flavour.md @@ -0,0 +1,76 @@ +# The markdown flavour + +The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain +CommonMark is a subset: the flavour adds directives, never changes CommonMark meaning. The emitted +form is contract (AGENTS.md §8). Per-node syntaxes build on this grammar in sections that follow +(todo.md 1b–1c). + +## Canonical form + +`adfToMarkdown` emits exactly one spelling; every CommonMark variant of the same document +normalizes to it through the round-trip. + +- Emphasis `_em_`, strong `**strong**`; `*` replaces `_` only where `_` cannot parse + (intra-word). +- Bullet lists `- `, two-space continuation indent. Ordered lists incrementing `1.` `2.` `3.`, + the first number taken from the node's `order` attribute. +- ATX headings (`#` … `######`); setext input normalizes to ATX. +- Code fences ``` with the node's language as info string, the fence lengthened past any backtick + run in the content; indented-code input normalizes to fences. +- Thematic break `---`. +- Hard break: backslash at end of line (survives editors that trim trailing spaces). +- Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes; + `` autolink when the link text equals its destination. +- Paragraphs on one line — no soft wrapping; soft line breaks in input collapse per CommonMark. +- Entity references in input decode to their characters; output backslash-escapes only where text + would otherwise parse as syntax. +- Blocks separated by one blank line, no trailing whitespace, single trailing newline. + +## Directives + +One grammar for everything CommonMark lacks. Names are the ADF node names (camelCase). + +**Inline**: `:name[content]{attrs}`. `[content]` is inline markdown; each node's section says +whether content and attrs are required. `:` opens a directive only when the name is followed +immediately by `[` or `{` — anything else (`10:30`, `:smile:`) is literal text. + +**Container block**: + +``` +:::name arg {attrs} +block content +::: +``` + +The fence is three or more colons. `arg` is one optional bare token whose meaning each node +defines (e.g. the panel type). The body is block markdown. The closing fence is a line of at +least the opening's length, and a container's fence is longer than every directive fence line in +its body, so closers are unambiguous — the code-fence rule. Canonical form uses minimal lengths. + +**Leaf block**: `::name {attrs}` — a block-position node with no body. + +**Attributes**: `{key=value key2="two words"}`. A bare value matches `[A-Za-z0-9_-]+`; anything +else is double-quoted with `\"` and `\\` as the only escapes. All values are strings at the +grammar level; each node's section assigns types. Canonical form orders keys alphabetically and +spells values bare wherever allowed. + +**Escaping**: the emitter backslash-escapes a `:` whose text would otherwise parse as a +directive; a backslash before `:` in input always yields a literal colon. + +## The opaque carry (AGENTS.md §3) + +A node type the library does not know rides as its raw JSON and restores to a deep-equal node: + +- **Block position**: a fenced code block with info string `adf`, body = the node's JSON, + serialized canonically — two-space indent, object keys sorted. +- **Inline position**: `:adf{json="…"}`, same serialization, quote-escaped. + +The info string `adf` is reserved: a genuine `codeBlock` whose `language` is exactly `adf` is +itself emitted through the opaque carry, so the reservation stays absolute and stays lossless. + +## Raw HTML in input + +CommonMark input may contain raw HTML. `markdownToAdf` routes each construct through the foreign +HTML element mapping (AGENTS.md §3; specified with the HTML dialect, todo.md milestone 6) — ADF +has no raw-HTML node, so a construct without a mapping, comments and processing instructions +included, is an error result naming it. The flavour never emits raw HTML. diff --git a/todo.md b/todo.md index dc365f4..e0ffafe 100644 --- a/todo.md +++ b/todo.md @@ -8,11 +8,16 @@ detail is settled at its own milestone. - [x] **0 — Scaffold.** `package.json` per §6, `tsconfig.json`, `.npmrc` (`save-exact=true`), the Docker tooling, `renovate.json` (§9), and `.gitea/workflows/ci.yml` gating branches: `runs-on: docker-host`, actions pinned to semver tags. -- [ ] **1 — The flavour spec.** The markdown flavour written as this repo's specification before - any implementation: the directive grammar (attributes, escaping, nesting), each node's - syntax from the inventory below, the opaque-carry spelling, the pipe-vs-directive table - rule, and what CommonMark's raw-HTML constructs become in ADF, which has no raw-HTML node — - likely the §3 element mapping, error otherwise. Start the corpus (§10) from this spec. +- [x] **1a — The directive grammar** (`spec/flavour.md`): inline/block/leaf directive forms, + attributes, escaping, nesting, canonical form, the opaque-carry spelling, the raw-HTML + input policy. +- [ ] **1b — Block node syntaxes** in `spec/flavour.md`: panel, expand/nestedExpand, the media + family, the pipe-vs-directive table rule and the directive table form, task and decision + lists, layout, extensions, syncBlock. +- [ ] **1c — Inline node syntaxes and marks** in `spec/flavour.md`: mention, emoji, status, date, + inlineCard, mediaInline; underline, subsup, textColor, border. +- [ ] **1d — Corpus start** (§10): checked-in ADF ↔ canonical-markdown fixture pairs per spec'd + node. - [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives. - [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. - [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and 3. -- 2.52.0 From a4e5718a870d004d0d6205e05530a269d8f2da25 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 00:23:43 +0200 Subject: [PATCH 2/7] Close the grammar's contract holes from the architecture review --- AGENTS.md | 4 +-- spec/flavour.md | 66 ++++++++++++++++++++++++++++++++----------------- todo.md | 4 ++- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6b1dec9..bb0dac7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,8 +22,8 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros ## 3. Unknown input policy -- Unknown ADF node: carried opaquely — raw JSON rides a dedicated syntax in both formats, restored - byte-for-byte. The round-trip holds for documents newer than the library. +- Unknown ADF node: 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. - Unmappable foreign HTML element: error result naming the element — never a silent drop. - Bare `@name` / `:smile:` in typed text: stays a text node. Only directives produce mention/emoji/media nodes; resolving names to ids needs I/O, which is the consumer's job. diff --git a/spec/flavour.md b/spec/flavour.md index 964fdda..8871345 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -1,9 +1,9 @@ # The markdown flavour The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` parses. Plain -CommonMark is a subset: the flavour adds directives, never changes CommonMark meaning. The emitted -form is contract (AGENTS.md §8). Per-node syntaxes build on this grammar in sections that follow -(todo.md 1b–1c). +CommonMark is a subset with one carve-out: literal text that matches directive syntax below is +claimed by the flavour (escape the `:` to keep it literal). The emitted form is contract +(AGENTS.md §8). Per-node syntaxes build on this grammar in sections that follow (todo.md 1b–1c). ## Canonical form @@ -12,15 +12,19 @@ normalizes to it through the round-trip. - Emphasis `_em_`, strong `**strong**`; `*` replaces `_` only where `_` cannot parse (intra-word). -- Bullet lists `- `, two-space continuation indent. Ordered lists incrementing `1.` `2.` `3.`, - the first number taken from the node's `order` attribute. +- Bullet lists `- `; ordered lists incrementing `1.` `2.` `3.`, the first number taken from the + node's `order` attribute. Continuation lines align with the first character after the marker + (two spaces for `- `, three for `1. `). Lists are tight — blank lines between items normalize + away; ADF does not record tightness. +- Blockquotes prefix every line with `> `. - ATX headings (`#` … `######`); setext input normalizes to ATX. - Code fences ``` with the node's language as info string, the fence lengthened past any backtick run in the content; indented-code input normalizes to fences. - Thematic break `---`. - Hard break: backslash at end of line (survives editors that trim trailing spaces). -- Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes; - `` autolink when the link text equals its destination. +- Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes. + `` autolink form only when the text equals the destination and the destination is a valid + CommonMark autolink (absolute URI). - Paragraphs on one line — no soft wrapping; soft line breaks in input collapse per CommonMark. - Entity references in input decode to their characters; output backslash-escapes only where text would otherwise parse as syntax. @@ -28,11 +32,19 @@ normalizes to it through the round-trip. ## Directives -One grammar for everything CommonMark lacks. Names are the ADF node names (camelCase). +One grammar for everything CommonMark lacks. A directive name is `[a-z][A-Za-z0-9]*` — the ADF +node names. Recognition is syntactic and name-set-independent: anything matching the forms below +parses as a directive regardless of whether the name is known, and an unknown name is an error +result naming it — so output an old emitter escaped stays escaped, and erroring input gaining +meaning later is MINOR, never a reparse (§8). The name `adf` is reserved for the opaque carry, as +both directive name and fence info string. -**Inline**: `:name[content]{attrs}`. `[content]` is inline markdown; each node's section says -whether content and attrs are required. `:` opens a directive only when the name is followed -immediately by `[` or `{` — anything else (`10:30`, `:smile:`) is literal text. +**Inline**: `:name[content]{attrs}`, on one line — an inline directive never spans lines. +`[content]` is inline markdown; brackets inside balance as in CommonMark link text, `\]` for a +literal bracket. Each node's section says whether content and attrs are required. `:` opens a +directive only when the name is followed immediately by `[` or `{`, and `{attrs}` must follow +`]` (or the name) with no gap — anything else (`10:30`, `:smile:`, a stray `{…}` in text) is +literal text. **Container block**: @@ -45,25 +57,35 @@ block content The fence is three or more colons. `arg` is one optional bare token whose meaning each node defines (e.g. the panel type). The body is block markdown. The closing fence is a line of at least the opening's length, and a container's fence is longer than every directive fence line in -its body, so closers are unambiguous — the code-fence rule. Canonical form uses minimal lengths. +its body — counting only lines that parse as directive fences in the body's block structure; a +colon run inside a code fence or opaque carry is content. Canonical form uses minimal lengths. **Leaf block**: `::name {attrs}` — a block-position node with no body. -**Attributes**: `{key=value key2="two words"}`. A bare value matches `[A-Za-z0-9_-]+`; anything -else is double-quoted with `\"` and `\\` as the only escapes. All values are strings at the -grammar level; each node's section assigns types. Canonical form orders keys alphabetically and -spells values bare wherever allowed. +**Attributes**: `{key=value key2="two words"}`. A bare value matches `[A-Za-z0-9_-]+`; any other +value is double-quoted with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over +Unicode, and raw newlines never appear inside quotes. All values are strings at the grammar +level; each node's section assigns types. Canonical form orders keys alphabetically and spells +values bare wherever allowed. -**Escaping**: the emitter backslash-escapes a `:` whose text would otherwise parse as a -directive; a backslash before `:` in input always yields a literal colon. +**Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as +directive syntax — the leading `:` of a would-be directive, `]` inside content; a backslash +before `:` in input always yields a literal colon. + +**Malformed directives are error results**, named: an unclosed container at end of input, a body +fence line of the container's length or longer, unparseable or duplicate-keyed attrs, invalid +JSON in an opaque carry. Never a silent literal-text fallback — a typo that reparses as prose is +the silent loss §2 refuses. ## The opaque carry (AGENTS.md §3) -A node type the library does not know rides as its raw JSON and restores to a deep-equal node: +A node type the library does not know rides as its raw JSON and restores to a deep-equal node. +Block and inline positions canonicalize differently, each fitting where it sits: -- **Block position**: a fenced code block with info string `adf`, body = the node's JSON, - serialized canonically — two-space indent, object keys sorted. -- **Inline position**: `:adf{json="…"}`, same serialization, quote-escaped. +- **Block position**: a fenced code block with info string `adf`, body = the node's JSON — + two-space indent, object keys sorted. +- **Inline position**: `:adf{json="…"}` — compact serialization (keys sorted, no whitespace), + JSON-string-escaped into the attribute. The info string `adf` is reserved: a genuine `codeBlock` whose `language` is exactly `adf` is itself emitted through the opaque carry, so the reservation stays absolute and stays lossless. diff --git a/todo.md b/todo.md index e0ffafe..c0415aa 100644 --- a/todo.md +++ b/todo.md @@ -19,7 +19,9 @@ detail is settled at its own milestone. - [ ] **1d — Corpus start** (§10): checked-in ADF ↔ canonical-markdown fixture pairs per spec'd node. - [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives. -- [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. +- [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. The raw-HTML + element mapping is empty until milestone 6, so at `0.1.0` every raw-HTML construct in input + is an error result. - [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and 3. - [ ] **5 — Release pipeline, ship `0.1.0`.** Publish-on-version-change (§9), `NPM_TOKEN` secret, the repo made public first (§6). `0.1.0` is the markdown round-trip: both markdown -- 2.52.0 From 864348b674d27fb9c65e8b72512612f414c1a58d Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 00:25:20 +0200 Subject: [PATCH 3/7] Pin canonical JSON escaping to minimal shortest-form --- spec/flavour.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/flavour.md b/spec/flavour.md index 8871345..d27ce43 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -65,8 +65,9 @@ colon run inside a code fence or opaque carry is content. Canonical form uses mi **Attributes**: `{key=value key2="two words"}`. A bare value matches `[A-Za-z0-9_-]+`; any other value is double-quoted with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over Unicode, and raw newlines never appear inside quotes. All values are strings at the grammar -level; each node's section assigns types. Canonical form orders keys alphabetically and spells -values bare wherever allowed. +level; each node's section assigns types. Canonical form orders keys alphabetically, spells +values bare wherever allowed, and inside quotes escapes only what it must, using the shortest +escape form. **Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as directive syntax — the leading `:` of a would-be directive, `]` inside content; a backslash -- 2.52.0 From b7b4b40923fb21d920c9f7b6b550de3955eec45b Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 00:31:27 +0200 Subject: [PATCH 4/7] Restore the working-loop section lost in the PR 1 merge race; reviewers reuse existing gate results --- AGENTS.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index bb0dac7..9cf4f63 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,3 +124,28 @@ No wiki markup (§1), no network or filesystem I/O, no name→id resolution (§3 validation or exported validator, no shipped CSS (§4), no streaming APIs, no performance budget — conversions are O(n), real documents are kilobytes. A CLI is a later goal (`todo.md`), not a non-goal. + +## 15. The working loop + +One unchecked `todo.md` item per session, in the smallest PR-able chunk — split a big milestone +into sub-items in `todo.md` before starting it. Per chunk: + +1. Fresh worktree off updated `origin/main`; implement tests-first (§10). +2. Run the larv-review flow until it passes and CI is green. A reviewer launch states the latest + gate result (commit and outcome); a reviewer does not re-run `ci.sh` or the tests when a + result for the commit under review already exists — re-run only what its own findings or + fixes invalidate. +3. Merge the PR (standing authorization, this repo only), check the box in `todo.md`, report, + stop. The next chunk gets a fresh session. + +Ask, don't guess: any choice where what the maintainer would pick is not near-certain gets asked, +and the answer lands as a decision in this file. The confidence bar is very high — asking too +often is the accepted cost, guessing wrong is not. + +Reserved for the maintainer, never the agent: changing `version` in `package.json` (a bump on +`main` publishes, §9 — every release including `0.1.0` is the maintainer's), making the repo +public, and creating the `NPM_TOKEN` secret. + +A continuous loop session (`/loop`) counts as a chain of sessions: one chunk per iteration, each +iteration starting by re-reading `AGENTS.md` and `todo.md` and trusting them over anything +remembered from earlier iterations. The loop stops when only maintainer-reserved acts remain. -- 2.52.0 From f0a3db30041750898620d43db08d7bbe0e776fa7 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 00:36:48 +0200 Subject: [PATCH 5/7] Close the stability round's contract holes: block claiming, unspellable states, carve-out qualifiers, deep-equal domain --- AGENTS.md | 6 +++++- README.md | 5 +++-- spec/flavour.md | 33 +++++++++++++++++++++++---------- todo.md | 6 ++++-- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9cf4f63..81a98d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,9 @@ When losslessness and readability conflict, losslessness wins. 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. +"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks +merged, JSON number semantics — the only domain markdown can restore. + Round-trip equality is a property tested over a corpus, not a claim made in prose. ## 3. Unknown input policy @@ -32,7 +35,8 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros - Directives, one grammar for everything markdown lacks: `:::panel info` … `:::` blocks, `:mention[@Mikael]{id=5b10a2}` inline. Prior art: CommonMark's generic-directives proposal. -- Plain CommonMark is a subset: the flavour adds syntax, never changes CommonMark meaning. +- Plain CommonMark is a subset, with one carve-out (`spec/flavour.md`): directive-shaped literal + text is claimed. - Tables: one header row plus plain inline cells → pipe table; anything richer → directive form. - Identity-bearing nodes carry their ids in attributes; a document is only portable within its site — accepted. diff --git a/README.md b/README.md index 974e074..44404e0 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,9 @@ isAdfDocument(v: unknown): v is AdfDocument (AGENTS.md §3). - `htmlToAdf(adfToHtml(doc))` equals `doc` — fidelity HTML cannot express rides `data-*` attributes. -- Plain CommonMark is valid input to `markdownToAdf`; converting back yields the library's - canonical spelling, which round-trips byte-identically. +- Plain CommonMark is valid input to `markdownToAdf`, with one carve-out: literal text matching + directive syntax is claimed (escapable — `spec/flavour.md`). Converting back yields the + library's canonical spelling, which round-trips byte-identically. - Foreign HTML maps a documented element set; an unmappable element is an error, never a silent drop. Well-formed HTML only — no tag-soup recovery. - The emitted formats are semver surface (AGENTS.md §8). diff --git a/spec/flavour.md b/spec/flavour.md index d27ce43..8d4e4e8 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -14,18 +14,20 @@ normalizes to it through the round-trip. (intra-word). - Bullet lists `- `; ordered lists incrementing `1.` `2.` `3.`, the first number taken from the node's `order` attribute. Continuation lines align with the first character after the marker - (two spaces for `- `, three for `1. `). Lists are tight — blank lines between items normalize - away; ADF does not record tightness. -- Blockquotes prefix every line with `> `. + (two spaces for `- `, three for `1. `); blank lines inside an item are empty lines. Lists are + tight — blank lines between items normalize away; ADF does not record tightness. +- Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`. - ATX headings (`#` … `######`); setext input normalizes to ATX. - Code fences ``` with the node's language as info string, the fence lengthened past any backtick run in the content; indented-code input normalizes to fences. - Thematic break `---`. -- Hard break: backslash at end of line (survives editors that trim trailing spaces). +- Hard break: backslash at end of line (survives editors that trim trailing spaces). Where + CommonMark admits no spelling — the end of a block, inside a heading — it is `:hardBreak{}`. +- An empty paragraph — real payloads carry them — is `::paragraph`. - Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes. `` autolink form only when the text equals the destination and the destination is a valid CommonMark autolink (absolute URI). -- Paragraphs on one line — no soft wrapping; soft line breaks in input collapse per CommonMark. +- Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space. - Entity references in input decode to their characters; output backslash-escapes only where text would otherwise parse as syntax. - Blocks separated by one blank line, no trailing whitespace, single trailing newline. @@ -59,11 +61,19 @@ defines (e.g. the panel type). The body is block markdown. The closing fence is least the opening's length, and a container's fence is longer than every directive fence line in its body — counting only lines that parse as directive fences in the body's block structure; a colon run inside a code fence or opaque carry is content. Canonical form uses minimal lengths. +Directive fence lines follow code-fence indentation (up to three spaces relative to their +container); trailing whitespace on a fence line is tolerated in input, never emitted. **Leaf block**: `::name {attrs}` — a block-position node with no body. -**Attributes**: `{key=value key2="two words"}`. A bare value matches `[A-Za-z0-9_-]+`; any other -value is double-quoted with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over +**Claiming at block level**, symmetric with inline: a line whose leading run of two or more +colons is followed immediately by a name character is claimed and must parse fully as a container +opening or a leaf, else it is a named error. A bare colon-run line is a closing fence while a +container is open, a named error otherwise. + +**Attributes**: `{key=value key2="two words"}`. `{attrs}` is optional in every form, and `{}` is +valid — no attributes. A bare value matches `[A-Za-z0-9_-]+`; any other value is double-quoted +with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over Unicode, and raw newlines never appear inside quotes. All values are strings at the grammar level; each node's section assigns types. Canonical form orders keys alphabetically, spells values bare wherever allowed, and inside quotes escapes only what it must, using the shortest @@ -74,9 +84,10 @@ directive syntax — the leading `:` of a would-be directive, `]` inside content before `:` in input always yields a literal colon. **Malformed directives are error results**, named: an unclosed container at end of input, a body -fence line of the container's length or longer, unparseable or duplicate-keyed attrs, invalid -JSON in an opaque carry. Never a silent literal-text fallback — a typo that reparses as prose is -the silent loss §2 refuses. +fence line of the container's length or longer, a bare colon-run line outside any container, an +inline `[content]` or `{attrs}` left unclosed at end of line, unparseable or duplicate-keyed +attrs, invalid JSON in an opaque carry. Never a silent literal-text fallback — a typo that +reparses as prose is the silent loss §2 refuses. ## The opaque carry (AGENTS.md §3) @@ -90,6 +101,8 @@ Block and inline positions canonicalize differently, each fitting where it sits: The info string `adf` is reserved: a genuine `codeBlock` whose `language` is exactly `adf` is itself emitted through the opaque carry, so the reservation stays absolute and stays lossless. +In block-directive positions (`::adf`, `:::adf`) the reserved name is a named error — the +carry's block form is the fence. ## Raw HTML in input diff --git a/todo.md b/todo.md index c0415aa..e0877f3 100644 --- a/todo.md +++ b/todo.md @@ -15,14 +15,16 @@ detail is settled at its own milestone. family, the pipe-vs-directive table rule and the directive table form, task and decision lists, layout, extensions, syncBlock. - [ ] **1c — Inline node syntaxes and marks** in `spec/flavour.md`: mention, emoji, status, date, - inlineCard, mediaInline; underline, subsup, textColor, border. + inlineCard, mediaInline; underline, subsup, textColor, border; the spelling for text nodes + whose whitespace CommonMark cannot hold (literal newlines, leading or trailing spaces). - [ ] **1d — Corpus start** (§10): checked-in ADF ↔ canonical-markdown fixture pairs per spec'd node. - [ ] **2 — `adfToMarkdown`.** First real code — decide here where §10's coverage check lives. - [ ] **3 — `markdownToAdf`.** The CommonMark parser is the largest single component. The raw-HTML element mapping is empty until milestone 6, so at `0.1.0` every raw-HTML construct in input is an error result. -- [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and 3. +- [ ] **4 — Round-trip property tests** over the corpus, both ways — the thing that proves 2 and + 3. Generators emit editor-normal ADF (§2). - [ ] **5 — Release pipeline, ship `0.1.0`.** Publish-on-version-change (§9), `NPM_TOKEN` secret, the repo made public first (§6). `0.1.0` is the markdown round-trip: both markdown directions, the types, `isAdfDocument`. The build lands here: a build tsconfig emitting JS -- 2.52.0 From a549f10ae8fd1b405546ef45d006453938f0bff3 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 00:42:41 +0200 Subject: [PATCH 6/7] Short-closer named error, empty-attrs omission rule, gate-reuse covers docs-only deltas --- AGENTS.md | 4 ++-- spec/flavour.md | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 81a98d5..2440f80 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -137,8 +137,8 @@ into sub-items in `todo.md` before starting it. Per chunk: 1. Fresh worktree off updated `origin/main`; implement tests-first (§10). 2. Run the larv-review flow until it passes and CI is green. A reviewer launch states the latest gate result (commit and outcome); a reviewer does not re-run `ci.sh` or the tests when a - result for the commit under review already exists — re-run only what its own findings or - fixes invalidate. + result exists for the commit under review, or when the diff since that result cannot affect + it (docs-only) — re-run only what its own findings or fixes invalidate. 3. Merge the PR (standing authorization, this repo only), check the box in `todo.md`, report, stop. The next chunk gets a fresh session. diff --git a/spec/flavour.md b/spec/flavour.md index 8d4e4e8..b6bed7a 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -76,17 +76,18 @@ valid — no attributes. A bare value matches `[A-Za-z0-9_-]+`; any other value with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over Unicode, and raw newlines never appear inside quotes. All values are strings at the grammar level; each node's section assigns types. Canonical form orders keys alphabetically, spells -values bare wherever allowed, and inside quotes escapes only what it must, using the shortest -escape form. +values bare wherever allowed, inside quotes escapes only what it must using the shortest escape +form, and omits empty `{attrs}` except where the `{` itself claims the directive +(`:hardBreak{}`). **Escaping**: the emitter backslash-escapes whatever literal text would otherwise parse as directive syntax — the leading `:` of a would-be directive, `]` inside content; a backslash before `:` in input always yields a literal colon. **Malformed directives are error results**, named: an unclosed container at end of input, a body -fence line of the container's length or longer, a bare colon-run line outside any container, an -inline `[content]` or `{attrs}` left unclosed at end of line, unparseable or duplicate-keyed -attrs, invalid JSON in an opaque carry. Never a silent literal-text fallback — a typo that +fence line of the container's length or longer, a bare colon-run line outside any container or +shorter than the fence it would close, an inline `[content]` or `{attrs}` left unclosed at end of +line, unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent literal-text fallback — a typo that reparses as prose is the silent loss §2 refuses. ## The opaque carry (AGENTS.md §3) -- 2.52.0 From 7c9e3fa0af93e98bff5612507fc708ded42e7304 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 24 Aug 2026 00:48:01 +0200 Subject: [PATCH 7/7] Bound the merge authorization to the maintainer's PR 3 grant, rewrap one line --- AGENTS.md | 4 ++-- spec/flavour.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2440f80..e1bb5c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -139,8 +139,8 @@ into sub-items in `todo.md` before starting it. Per chunk: gate result (commit and outcome); a reviewer does not re-run `ci.sh` or the tests when a result exists for the commit under review, or when the diff since that result cannot affect it (docs-only) — re-run only what its own findings or fixes invalidate. -3. Merge the PR (standing authorization, this repo only), check the box in `todo.md`, report, - stop. The next chunk gets a fresh session. +3. Merge the PR (standing authorization, this repo only, granted through the `0.1.0` release — + PR #3), check the box in `todo.md`, report, stop. The next chunk gets a fresh session. Ask, don't guess: any choice where what the maintainer would pick is not near-certain gets asked, and the answer lands as a decision in this file. The confidence bar is very high — asking too diff --git a/spec/flavour.md b/spec/flavour.md index b6bed7a..6edb59b 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -87,8 +87,8 @@ before `:` in input always yields a literal colon. **Malformed directives are error results**, named: an unclosed container at end of input, a body fence line of the container's length or longer, a bare colon-run line outside any container or shorter than the fence it would close, an inline `[content]` or `{attrs}` left unclosed at end of -line, unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent literal-text fallback — a typo that -reparses as prose is the silent loss §2 refuses. +line, unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent +literal-text fallback — a typo that reparses as prose is the silent loss §2 refuses. ## The opaque carry (AGENTS.md §3) -- 2.52.0