diff --git a/AGENTS.md b/AGENTS.md index 5a75173..3b76250 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -180,6 +180,7 @@ types in `src/result.ts` hold its shape. deliberate semver judgment. `publish.sh` is that job, and `private: true` stops it before it reads the token, so the pipeline is live and silent until the maintainer's first bump drops the field. +- The bump commit renames `CHANGELOG.md`'s `## Unreleased` to the version. - Docs on `main` describe the release being built rather than the version npm holds, so they match it the moment the bump publishes; add no interim note marking the gap (the maintainer, 2026-09-16). @@ -396,11 +397,13 @@ figure is promised. 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 +`todo.md` lists what is left under the release that ships it, in shipping order. One item per +session — the first under the earliest release — in the smallest PR-able chunk; split a big item into sub-items in `todo.md` before starting it. A chunk running a little over or under that is not worth deliberating; what matters is that nothing is left undone in the end. The session stops there -whatever it was asked to finish: a release is a chain of sessions, and `todo.md`'s "Next session" is -the handover, so an instruction to work until a release is checked names the chain, not the session. +whatever it was asked to finish: a release is a chain of sessions, so an instruction to work until a +release is done names the chain, not the session. An open PR is a chunk already in flight, and +finishing it is the session. Per chunk: 1. Fresh worktree off updated `origin/main`; implement tests-first (§10). @@ -409,8 +412,8 @@ Per chunk: 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, granted through the `0.2.0` release — - the maintainer, 2026-09-13), check the box in `todo.md` and move the item's text to - `todo-history.md`, leaving its title behind, report, stop. + the maintainer, 2026-09-13), delete the item from `todo.md` — what a consumer sees of it is + reworded for them into `CHANGELOG.md`'s `## Unreleased` — report, stop. Reserved for the maintainer whatever any rule here says: changing `version` in `package.json` (a bump on `main` publishes, §9 — every release is the maintainer's) and the `NPM_TOKEN` secret. @@ -442,9 +445,9 @@ The verdict lands in the item it settles (the maintainer, 2026-09-25). outweighs moves later. A weighing no rule decides is asked as a gap. - A stated number — 500 levels, the branch floor — is kept; a chunk that cannot keep it asks, naming the number it can reach. A number the code needs and no rule states is a gap. -- Where the shipping order names no release for the next unchecked item, the chunk is planning that - release: every unscheduled item weighed as above, the order written in `todo.md`, and the - maintainer's approval taken before any code. +- An earliest release with no items left and nothing shipped toward it is planned as the chunk: + every later item weighed as above, the order written in `todo.md`, and the maintainer's approval + taken before any code. With work shipped toward it, it is ready to cut: report that and stop. ### The continuous loop diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..442e445 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## Unreleased + +- **Breaking:** directives, the opaque carry among them (now `carry`), are spelled under an `!adf:` + prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`, `!adf:name arg {attrs}`) in place + of the `:::`/`::`/`:name` forms: text holding an unescaped `!adf:` is claimed, and `adf` is an + ordinary code block language. Convert stored markdown per `MIGRATION.md`. +- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no + CommonMark escape spells is written as `!adf:link[text]{attrs}`. +- **Breaking:** some directive refusals carry `malformed-directive` where they carried + `unsupported-node-shape`, and an empty node's leaf and closed spellings swap which one parses; + `MIGRATION.md` lists each. +- **Breaking:** a link whose text holds another link keeps the inner link and leaves the outer + brackets literal text, where `0.1.0` split the outer link around it; see `MIGRATION.md`. +- Add `adfToPlainMarkdown` and `plainMarkdownToAdf`, a lossy pair converting ADF to and from + markdown GitHub, GitLab and Obsidian render: alerts, callouts, task lists, `==highlights==` and + pipe tables. +- Spell `rule`'s `color`, `style` and `weight`, `layoutSection`'s `columnRuleStyle` and a link's + `collection`, `id` and `occurrenceKey` directly where they rode the opaque carry. +- Fix an image inside another image's description: it flattens into the alt text, where it was + refused. + +## 0.1.0 + +- First release: lossless conversion between ADF and an extended markdown flavour — + `adfToMarkdown`, `markdownToAdf` and `isAdfDocument`. Nothing throws, and every error carries a `code` from a + closed list. diff --git a/README.md b/README.md index 2484e59..584205f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,8 @@ an HTML dialect. **Status: published — the markdown round-trip (`adfToMarkdown`, `markdownToAdf`); HTML at `0.2.0`.** -Plan: `todo.md`. Decisions: `AGENTS.md`. The lossless flavour's grammar: +Plan: `todo.md`. Decisions: `AGENTS.md`. Changes: +[`CHANGELOG.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/CHANGELOG.md). The lossless flavour's grammar: [`spec/flavour.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/spec/flavour.md). Upgrading from `0.1.0`: [convert your markdown first](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/MIGRATION.md). diff --git a/todo-history.md b/todo-history.md index 4eec577..bea0c43 100644 --- a/todo-history.md +++ b/todo-history.md @@ -1,6 +1,7 @@ # Todo history -The done `todo.md` items in full, as they were written. `todo.md` keeps a one-line summary of each. +The done `todo.md` items in full, as they were written, until 36 moves their decisions to +`docs/decisions.md` and deletes this file. ## Milestones @@ -1072,6 +1073,75 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li `text` and `spelling`, and `emitDirectiveBlock` takes the level from the `Walk`. Of the two subtractions three readers flagged as double-counting, this is the one that is dead. - [ ] **10 — Lossy conversion (`0.2.0`).** +- [x] **10 — Lossy conversion (`0.2.0`).** Markdown other tools render readably, to and from ADF, + keeping the content while dropping what markdown cannot hold — format, design and the richer + nodes. + **Settled** (the maintainer, 2026-09-14, reshaped by 35 on 2026-09-27): two exports, the + markdown grammar's reader and writer with the plain flavour set (AGENTS.md §1). The markdown + is the lossless flavour without directives — CommonMark, the pipe table and `~~` — + plus the conventions below, chosen for readability from a survey of GitHub, GitLab, Gitea, + Obsidian, Pandoc, MkDocs, Docusaurus, Typora, Joplin, Logseq, Bear, Notion, Azure DevOps and + Discord, GitHub's renderer confirming each shape. Writing refuses only what the document guard + refuses (`not-an-adf-document`, `unsupported-document-version`, `unsupported-nesting-depth`) + and degrades every other shape; reading refuses what `markdownToAdf` refuses. A read node + carries no `localId`, save a task node's position id (10f, the maintainer, 2026-09-26). Reading also takes other tools' spellings — type words in any case, + Obsidian's aliases, `[X]` — since it reads their output and never writes those spellings. + - A `panel` is an alert: the marker alone on the quote's first line, a blank `>`, then the body + (`> [!WARNING]`), in GitHub's five words by colour — info `NOTE`, note `IMPORTANT`, tip and + success `TIP`, warning `WARNING`, error `CAUTION`, custom `NOTE`. Reading takes those words + back (`NOTE` info, `IMPORTANT` note, `TIP` tip, `WARNING` warning, `CAUTION` error) and + Obsidian's by meaning (hint tip; success, check and done success; attention warning; danger, + failure, fail, missing, bug and error error — `error` by a panel, 3 of 3, 2026-09-25; any + other word info). Text after a marker on its line is the panel's first body paragraph, and + the lines after it open the body (35a). + - An `expand` or `nestedExpand` is Obsidian's folded callout, `> [!NOTE]- Title`, a blank `>`, + then the body. Reading takes a fold sign (`-` or `+`) as an expand whatever the word, the + rest of the marker's line as its title and the lines after it as the body (35a), and an + expand inside an expand as a `nestedExpand`. + - A `taskList` is a bullet list whose items lead with `[x]` or `[ ]` (`- [x] Write the spec`). + Reading takes a list whose every item is so marked back as a `taskList` — a `blockTaskItem` + where an item holds more than one block, a nested task list moved beside its item — and + leaves mixed and ordered lists plain. A `decisionList` is a plain bullet list. + - `backgroundColor` is `==text==`, and reading gives `==text==` the Atlassian editor's default + highlight colour where whitespace, punctuation or a line edge bounds each `==` outside, so + `a==b and c==d` stays text (a panel, 3 of 3, 2026-09-25). + - `layoutSection`/`layoutColumn`, `bodiedExtension`, `bodiedSyncBlock`, `multiBodiedExtension` + and `extensionFrame` unwrap to their body blocks in order; the CommonMark blocks keep their + spelling, attributes dropped. + - `mention` and `status` become their text, the mention's `@` kept; `emoji` its text or else its + `shortName`; `date` its ISO date in UTC (`2026-09-13`); `inlineCard`, `blockCard` and + `embedCard` a link to their `url`, or to their `data`'s `url` named by its `name` — the name + alone without a `url`; an external image `![alt](url)` in a block and `[alt](url)` inline, where no ADF node spelled + `![alt](url)` stands (a panel, 6 of 7, 2026-09-25); `media`, + `mediaGroup` and `mediaInline` holding a stored file their `alt` text; `caption` its text as + a paragraph; `extension` and `inlineExtension` their `text` attribute; `placeholder` nothing, + its text being the editor's prompt rather than the document's; a node no row names, or one + standing where no spelling holds it, its blocks or its text. + - Content the document only references — a stored file with no `alt`, an extension with no + `text`, a `syncBlock`, a card with neither `url` nor `data` naming one — leaves an italic note + naming it: `_(image not included)_`, `_(jira-issues-table not included)_`, `_(synced block not + included)_`, `_(link card not included)_`, `_(extension not included)_` without a key; a mention + with no text is `@` and its id (panels, 3 of 3, 2026-09-25). + - A table stays a pipe table: the first row becomes the header, a cell's blocks join on one line + with spaces, and a span keeps its cell under its header by empty cells in the columns and + rows it covered, padding at most to the table's cell count. + - A list stays a list: where CommonMark cannot hold a block inside an item, what gives way is + what a reader does not see — the spaces of a whitespace-only code line — and a rule opening an + item drops (a panel, 3 of 3, 2026-09-25); an ordered list running past `999999999`, or adjacent + ordered lists whose numbering does not continue, is one bullet list keeping its numbers as text + (panels, 3 of 3 and 5 of 7, 2026-09-25). + - `code`, `em`, `link`, `strike` and `strong` stay and every other mark drops, keeping its text — + `subsup` too, since `~2~` is a strike on GitHub; a link no CommonMark escape writes has its + `href` percent-encoded until one does, and a mark run CommonMark's flanking or matching cannot + spell drops its mark. + - A newline in text becomes a hard break and edge whitespace is trimmed; carriage returns and + null characters are removed; a paragraph line opening with a code span whose backticks would + read as a fence loses the code mark; an empty paragraph drops, and adjacent lists of one type + merge. + - Rejected in the survey: `~sub~` and `^sup^`, underline and colour spellings, raw HTML + (`
`, ``), MkDocs `!!!` and the `:::` admonition family, footnotes, definition + lists, wikilinks, embeds, tags, comments, TOC tokens, spoilers, task states past `[x]`/`[ ]`, + and lifting bare URLs, `@name`, `:shortcode:` or ISO dates into nodes. - [x] **10a — The reduction.** `adfToPlainMarkdown`'s ADF→ADF reduction, tests first, a test per row above. - [x] **10b — The lift.** `plainMarkdownToAdf`'s ADF→ADF lift, tests first, a test per row it reads, diff --git a/todo.md b/todo.md index 39d9501..34ffddc 100644 --- a/todo.md +++ b/todo.md @@ -1,333 +1,118 @@ # Todo -The plan. Design questions are settled in `AGENTS.md`; remaining spec detail is settled at its own -milestone. A done item shrinks to its title here; its full text moves to `todo-history.md`. +## 0.2.0 -## Next session +- **36 — Move every decision into `docs/decisions.md`, indexed from `AGENTS.md`.** Each entry states + the decision, its date, who made it, the README goal it serves and the premise it is valid while; + one no goal serves is proposed as a goal and asked. Sources: `AGENTS.md`'s body, the settled text + in this file's items, and `todo-history.md`, deleted with the bare `(28)` citations into it once + nothing cites it. Split by `AGENTS.md` section where one chunk is too big. +- **35 — Read and write plain markdown as a flavour of the markdown grammar.** Per Goal 2 and + AGENTS.md §1, `plainMarkdownToAdf` is `markdownToAdf`'s parser and `adfToPlainMarkdown` + `adfToMarkdown`'s writer, each with the plain flavour set; 10's rows are read and written there, + and the lift goes (the maintainer, 2026-09-27). The exports, their refusals and 10's rows stay as + they are. + - **35a — Read the plain flavour in the parser and delete the lift.** 10's rows are read while + parsing, and `plain-lift.ts` is deleted, its tests reading through `plainMarkdownToAdf`. `> + [!faq]- Why?` with the body on the next `>` line reads to an expand titled `Why?` whose body + keeps the next lines' link targets and marks, and `> [!tip] Title` then `> body` to a panel + whose paragraphs are `Title` and `body`: the rest of the marker's line is the title (an expand) + or the first body paragraph (a panel). A CommonMark backslash keeps a marker literal — `\==x==`, + `> \[!NOTE]`, `- \[x]`. + - **35b — Spell the plain flavour in the writer.** Panels, expands, task lists and highlights are + written by the writer, which escapes text that would read back as one, so + `plainMarkdownToAdf(adfToPlainMarkdown(doc))` keeps a literal `==x==`, a quote opening `[!NOTE]` + and a list whose items all open `[x] ` as text. A highlighted `=` (today `=====`) and `a==b` + (today `==a==b==`, highlighting `a` alone) come back highlighted whole, or lose the highlight + where no spelling holds them; 10c's byte-for-byte property misses both, since the wrong document + re-spells to the same bytes. The reduction keeps only degrading what the flavour cannot spell. +- **10f — Give task nodes read from plain markdown position ids.** `plainMarkdownToAdf` gives each + `taskList`, `taskItem` and `blockTaskItem` a deterministic `localId` from its position in document + order, so a site that rejects a missing `localId` takes the document and the same markdown reads + to the same ids every run; README §Plain markdown's `localId` bullet says so (the maintainer, + 2026-09-26). The id spelling — unique within the document, no host API — is part of the chunk. +- **6 — Specify the HTML dialect.** Element-by-element mapping, the `data-*` fidelity scheme, the + opaque-carry form, and the documented foreign-element set `htmlToAdf` accepts — the set + `markdownToAdf` shares (`spec/flavour.md` §Raw HTML in input; 29). + **Settled** (the maintainer, 2026-09-20), the four answers that shape the set: + - A container ADF has no node for unwraps to its children, its own attributes dropped, so `
text
` keeps `text` and loses the box and the alignment ADF cannot hold. + - `
Title…
` is an `expand`, the summary its `title`; one + inside another is a `nestedExpand`, as 10 already spells for the lossy pair. An empty + `
` is still refused — `expand` requires content, so there is nothing to build. + - A comment stays an error result. Neither schema holds a comment node: across 84 and 98 + definitions the only "comment" in either file is `annotationType: "inlineComment"` on the + `annotation` mark, which carries an `id` and no text, the words living behind an Atlassian API. + `placeholder` is the editor's own visible hint, and `extension` demands an `extensionKey` naming + a vendor app. Nothing can hold the words, so nothing accepts them. + - `