From 94eaee6baea371831f75677dfec61017ecafbd9b Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 2 Oct 2026 17:34:17 +0200 Subject: [PATCH 1/7] Goals become one-line aims, their detail moves to the sections that meet them --- AGENTS.md | 18 ++++++------ README.md | 70 +++++++++++++++++----------------------------- docs/decisions.md | 71 ++++++++++++++++++++++++----------------------- src/index.ts | 1 + todo.md | 15 ++++++++-- 5 files changed, 83 insertions(+), 92 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d212a03..4a87822 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,13 +130,11 @@ One-line commit messages and PR titles; short PR summaries. No AI-attribution ma ## 7. The working loop -`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, 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. +`todo.md` lists what is left under the release that ships it, in shipping order. A session works +one chunk, starting from the first item under the earliest release, and stops there 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 (§3). @@ -145,7 +143,7 @@ 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 by the maintainer through the - `0.2.0` release), delete the item from `todo.md` — what a consumer sees of it is + `0.2.0` release), delete the chunk's items 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 @@ -165,7 +163,7 @@ reading is the ask. Never ask "A or B?": state the gap, the earlier entries of i nearest text, a candidate entry in that file's voice, and the instance it yields. An entry that keeps collecting instances is wrong: rewrite it. -Which output the audience expects — README goal 6 — is settled by a reader panel rather than +Which output the audience expects — README goal 5 — is settled by a reader panel rather than asked: three fresh-context readers, one per README persona the conversion serves, each given only `## Audience` and the input, writing what they expect before picking among outputs the goals allow, rendered, shuffled, with no rationale and nothing saying what is implemented. Three agreeing @@ -174,7 +172,7 @@ The verdict lands in `docs/decisions.md`. ### Findings, numbers and empty releases -- A finding inside the chunk's item is fixed in the chunk. Outside it, a new `todo.md` item, always +- A finding inside the chunk's items is fixed in the chunk. Outside it, a new `todo.md` item, always in a release, weighed against every item on that release by the personas and `docs/decisions.md` §Plain markdown is a flavour of the grammar through §Names stay text — an item it outweighs moves later. A weighing no entry decides is asked as a gap. diff --git a/README.md b/README.md index e43453f..e2efbe9 100644 --- a/README.md +++ b/README.md @@ -23,47 +23,21 @@ represent. ## Goals -In priority order. +The most useful ADF conversion library available, by these goals in priority order: -1. **Lossless, and every call returns.** The round-trip holds for every document the lossless - conversions take, node types this version does not know included; one that has no spelling is - refused and says where, never silently reduced. No input makes a call loop forever or overflow - the stack. Every goal below gives way to this one. -2. **ADF is the hub.** Every format and flavour converts to and from ADF, and no two others - convert directly: markdown↔HTML composes through ADF. Adding a format or flavour costs one - reader and one writer. A flavour of a grammar shares that grammar's reader and writer and adds - only its own spellings. -3. **Plain CommonMark is input.** Markdown written for something else converts — the exceptions - below are the whole of them — and every spelling a flavour claims on top of CommonMark is - escapable, so each flavour is opt-in. -4. **Output a person can edit.** A node CommonMark can spell gets that spelling; the lossless - flavour's directive form carries only what CommonMark cannot hold. -5. **Lossy conversion keeps the content.** `adfToPlainMarkdown` and `plainMarkdownToAdf` drop what - plain markdown cannot hold — format, design, structure — never content the document holds: what - a reader of the rendered document sees or follows, its text, images and link targets. The lossy - pair creates and exports; it never saves back over the document it read — a document's identity - (task, mention, media ids) survives a round trip only through the lossless pair. -6. **What happens is what the audience expects.** Where the goals leave a choice, a conversion - takes the one its audience would predict, reading the input as written. -7. **Failures are values.** Nothing throws, and `code` is a closed list — as much a contract as - the emitted formats are. -8. **Nothing in the way.** No runtime dependencies, no I/O, no configuration, no host API: ESM on - any ES2022 engine, in a browser as readily as on a server, installed from public npm. The public - surface is the conversions, their types, `isAdfDocument`, and what a consumer needs to check a - guarantee this README makes; a helper is exported only when a persona cannot do without it. -9. **Correct before fast.** Each format means what its own specification says — markdown as the - CommonMark spec reads it, well-formed HTML as the HTML standard parses it — both in what this - library reads and in what a conforming parser reads from what it writes. A call takes a whole - document and returns a whole result. -10. **Fast once correct.** Conversion time grows linearly with the document wherever the goals - above allow it; a faster path that risks one of them is not taken. -11. **Source a contributor can hold.** Any one function reads in one sitting, and no change makes - the longest one longer. +1. **Lossless, and every call returns a result, never a throw.** +2. **ADF is the hub.** +3. **Each format reads and writes as its standard says.** +4. **Our markdown is CommonMark, extended only where CommonMark has no spelling.** +5. **No surprises: output reads and edits the way its audience expects.** +6. **Lossy conversion drops form, never content.** +7. **Runs in any JavaScript engine, with nothing to install, configure or connect.** +8. **Fast, and linear in the document's size.** +9. **Easy to find, and clear at a glance what it does.** ## Audience -Application developers embedding the library, addressed as personas rather than named consumers -(AGENTS.md §1). All four rely on the guarantees below and on `code` being a closed list; none may +Application developers embedding the library, in four personas. All four rely on the guarantees below and on `code` being a closed list; none may rely on an error message's wording, which is free text. - **Viewer/editor app** — shows a document, lets a human edit, posts it back. Relies on the @@ -94,7 +68,8 @@ if (result.ok) { } ``` -Pure functions, no I/O, no configuration. +Serves Goals 1 and 7. Pure functions, each taking a whole document and returning a whole result; +no I/O, no configuration. ```ts adfToMarkdown(doc: AdfDocument): Result @@ -114,9 +89,10 @@ htmlToMarkdown(html: string): Result // 0.2.0, via ADF ## Plain markdown -Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes markdown other -tools render — GitHub, GitLab, Obsidian and the like — keeping the content and dropping the rest: -attributes, colours, layout, identity. It refuses only +Serves Goal 6. Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes +markdown other tools render — GitHub, GitLab, Obsidian and the like — keeping the content and +dropping the rest: attributes, colours, layout, identity. Content is what a reader of the rendered +document sees or follows: its text, images and link targets. It refuses only `not-an-adf-document`, `unsupported-document-version` and `unsupported-nesting-depth`, and writes no directive. @@ -167,7 +143,7 @@ read replaces mentions, attachments and macros with text. ## The errors -An ADF node type this version does not know is not an error: the lossless pair carries it opaquely +Serves Goal 1. An ADF node type this version does not know is not an error: the lossless pair carries it opaquely and restores it unchanged ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)). `ConvertError` is `{ code, message, path, position? }`. `code` is the exported `ConvertErrorCode`, @@ -219,6 +195,11 @@ emit refuses: ## The guarantees +Serves Goals 1, 3 and 4. + +- Markdown means what the CommonMark spec says, and well-formed HTML what the HTML standard + parses, both in what this library reads and in what a conforming parser reads from what it + writes; the bullets below name every exception. - `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)). - Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html` @@ -247,7 +228,8 @@ emit refuses: input. - Past that and `~~`, no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist is the `taskList` directive — `plainMarkdownToAdf` turns the marker into a `taskList`. -- A document nested deeper than 500 levels is an error result, not a stack overflow. +- A document nested deeper than 500 levels is an error result, not a stack overflow, and no input + makes a call loop forever. - The emitted formats are semver surface ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#the-formats-are-api)). - **`0.2.0`** — `htmlToAdf(adfToHtml(doc))` equals `doc`; fidelity HTML cannot express rides @@ -257,7 +239,7 @@ emit refuses: ## The package -ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts` beside it. +Serves Goal 7. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts` beside it. Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node, Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint. Contract: [`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#any-es2022-engine), §Any diff --git a/docs/decisions.md b/docs/decisions.md index a1d0d34..a16329a 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -23,7 +23,7 @@ sanitized and a mention keeps the test user's real account id. ## Markdown in is a canonical fixpoint -2026-08-23, the maintainer. Goals 1 and 3. Valid while markdown input may be written by hand. +2026-08-23, the maintainer. Goals 1 and 4. Valid while markdown input may be written by hand. 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 — @@ -55,7 +55,7 @@ Where a container's own spelling cannot hold the child it has — a `bulletList` ## Foreign HTML sorts three ways -2026-08-23, the sort 2026-09-20, the maintainer. Goals 1, 3 and 7. Valid while ADF holds no node +2026-08-23, the sort 2026-09-20, the maintainer. Goals 1 and 4. Valid while ADF holds no node for a bare container, a comment or a script. Lands with `todo.md` 6. Every foreign element `htmlToAdf` and `markdownToAdf` read sorts one of three ways, never a silent @@ -76,39 +76,40 @@ than they buy. ## Names stay text -2026-08-23, the maintainer. Goal 8. Valid while resolving a name to an id needs I/O. +2026-08-23, the maintainer. Goal 7. Valid while resolving a name to an id needs I/O. A bare `@name` or `:smile:` in typed text stays a text node. Only directives produce mention/emoji/media nodes; resolving names to ids is the consumer's job. ## Directives under `!adf:` -2026-08-23, prefixed `!adf:` 2026-09-16, the maintainer. Goals 3 and 4. Valid while prose does not +2026-08-23, prefixed `!adf:` 2026-09-16, the maintainer. Goals 4 and 5. Valid while prose does not write `!adf:`. Directives are one grammar for everything markdown lacks, namespaced under `!adf:`: `!adf:panel info` … `!adf:/panel` blocks, `!adf:mention[@Mikael]{id=5b10a2}` inline, `\!adf:` the -one escape. Not CommonMark's generic-directives proposal: its `:::` claims a form prose writes, and -its fence-length discipline ties a container's opener to its own body, where closing from the -opener nests by itself and leaf versus container falls out of the node's content model. +one escape. A node CommonMark can spell takes that spelling, never a directive. Not CommonMark's +generic-directives proposal: its `:::` claims a form prose writes, and its fence-length discipline +ties a container's opener to its own body, where closing from the opener nests by itself and leaf +versus container falls out of the node's content model. ## CommonMark is a subset -2026-08-23, the maintainer. Goal 3. Valid while prose rarely writes the shapes the carve-outs claim. +2026-08-23, the maintainer. Goal 4. Valid while prose rarely writes the shapes the carve-outs claim. Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a directive, a pipe table or a `~~` pair is claimed — plus one image gap. ## Tables -2026-08-23, the maintainer. Goal 4. Valid while a pipe table holds only one header row and inline +2026-08-23, the maintainer. Goal 5. Valid while a pipe table holds only one header row and inline cells. One header row plus plain inline cells → pipe table; anything richer → directive form. ## Links -2026-09-13, nesting 2026-09-17, the maintainer. Goals 1 and 4. Valid while CommonMark's link +2026-09-13, nesting 2026-09-17, the maintainer. Goals 1 and 5. Valid while CommonMark's link syntax is what readers edit. `[text](url "title")`, or `` for a bare autolink-shaped text, wherever CommonMark spells the @@ -127,7 +128,7 @@ accepted. ## Plain task ids come from position -2026-09-26, spelling 2026-09-29, the maintainer. Goals 5 and 8. Valid while a site rejects a task +2026-09-26, spelling 2026-09-29, the maintainer. Goals 6 and 7. Valid while a site rejects a task node with no `localId`. `plainMarkdownToAdf` gives each `taskList`, `taskItem` and `blockTaskItem` lacking one a `localId` @@ -167,7 +168,7 @@ past `[x]`/`[ ]`, and lifting bare URLs, `@name`, `:shortcode:` or ISO dates int ## The HTML dialect -2026-08-23, the maintainer. Goals 4 and 8. Valid while HTML output is read by consumers styling it +2026-08-23, the maintainer. Goals 5 and 7. Valid while HTML output is read by consumers styling it themselves. The HTML dialect mirrors the markdown flavour: semantic elements, stable `adf-*` classes, `data-*` @@ -175,7 +176,7 @@ for what HTML cannot express, text always escaped. No stylesheet ships. ## No runtime dependencies -2026-08-23, the maintainer. Goal 8. Valid while ~20 lines of own code, or a vendored table, do each +2026-08-23, the maintainer. Goal 7. Valid while ~20 lines of own code, or a vendored table, do each job a dependency would. `dependencies` is empty. A runtime dependency enters only through an entry here stating why ~20 @@ -185,7 +186,7 @@ and HTML parsers are written in this repo. ## Standards ship as data 2026-08-30, the CommonMark suite 2026-09-05 and ADF's schemas 2026-09-13, the maintainer. Goals 1, -3 and 8. Valid while each table is fixed data a dependency would only wrap. +4 and 7. Valid while each table is fixed data a dependency would only wrap. A table a standard fixes is data rather than a dependency: HTML5's 2125 semicolon-terminated character references ship packed in their own module, so entity decoding is complete without one. @@ -208,7 +209,7 @@ nodes that break it. ## Any ES2022 engine -2026-09-01, the maintainer. Goal 8. Valid while ES2022 is the floor browsers and servers share. +2026-09-01, the maintainer. Goal 7. Valid while ES2022 is the floor browsers and servers share. The library runs on any ES2022 engine, not only Node — a browser as readily as a server. The shipped source is ECMAScript and nothing else: no host import, no host global, no DOM. @@ -222,13 +223,13 @@ never the higher one those repo-only tools want. ## ESM only -2026-08-23, the maintainer. Goal 8. Valid while the audience's toolchains all import ES modules. +2026-08-23, the maintainer. Goal 7. Valid while the audience's toolchains all import ES modules. No CommonJS build, no dual-package hazard. ## One built entrypoint -2026-08-23, the maintainer. Goal 8. Valid while Node refuses to type-strip under `node_modules`. +2026-08-23, the maintainer. Goal 7. Valid while Node refuses to type-strip under `node_modules`. Built JavaScript, `.d.ts` beside it. Do not add a TypeScript-source entrypoint — Node refuses to type-strip under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so it cannot serve @@ -236,7 +237,7 @@ an npm consumer. ## Public on npm -2026-08-23, the name 2026-09-01, the maintainer. Goals 2 and 8. Valid while the package's source +2026-08-23, the name 2026-09-01, the maintainer. Goals 2 and 7. Valid while the package's source stays public beside it. Published to public npm as `@larvit/adf-codec`. Public source: the Gitea repo goes public, @@ -245,7 +246,7 @@ for the hub rather than the formats around it. ## The formats are API -2026-08-23, strict input 2026-09-01, content models 2026-09-16, the maintainer. Goals 1 and 7. +2026-08-23, strict input 2026-09-01, content models 2026-09-16, the maintainer. Goal 1. Valid while consumers store what the library emits. The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing @@ -260,7 +261,7 @@ types in `src/result.ts` hold its shape. ## The code list -2026-08-25, the maintainer; dated below where a rule came later. Goal 7. Valid while a consumer +2026-08-25, the maintainer; dated below where a rule came later. Goal 1. Valid while a consumer switches on `code` with no `default`. - Adding, removing or renaming a code is breaking, so a new cause takes an existing code whose @@ -276,7 +277,7 @@ switches on `code` with no `default`. ## Which code a cause takes -2026-08-28, the maintainer; dated below where a rule came later. Goal 7. Valid while a consumer +2026-08-28, the maintainer; dated below where a rule came later. Goal 1. Valid while a consumer handles one cause alike whichever node, attribute or direction raised it. - A code names the cause; where one cause recurs across node types, across one mark's attributes @@ -309,7 +310,7 @@ handles one cause alike whichever node, attribute or direction raised it. ## `message` and `path` -2026-09-03, the path 2026-09-23, the maintainer. Goals 3 and 7. Valid while a person fixing the +2026-09-03, the path 2026-09-23, the maintainer. Goals 1 and 4. Valid while a person fixing the input reads `message`. - A message names the violation, not the rule alone — a rule by itself states a truth the reader @@ -323,7 +324,7 @@ input reads `message`. ## Publish on a version bump -2026-08-23, converging 2026-09-03, the maintainer. Goal 8. Valid while CI on `main` holds the npm +2026-08-23, converging 2026-09-03, the maintainer. Goal 7. Valid while CI on `main` holds the npm token. `package.json` version on `main` is the source of truth. CI on `main`: tests green and the version @@ -339,7 +340,7 @@ depend on a store that the gate would then have to keep. ## Docs describe the release being built -2026-09-16, the maintainer. Goal 8. Valid while a bump on `main` publishes. +2026-09-16, the maintainer. Goal 7. Valid while a bump on `main` publishes. 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. @@ -355,7 +356,7 @@ nesting a spelling inside its own kind (`*(*a*)*`) names that mark once. ## The gate runs on Deno and Bun -2026-09-01, Deno's reason 2026-09-28, the maintainer. Goals 8 and 9. Valid while the library claims +2026-09-01, Deno's reason 2026-09-28, the maintainer. Goals 3 and 7. Valid while the library claims any ES2022 engine. The gate runs the suite under Deno and Bun as well as Node. Bun runs JavaScriptCore, the one engine @@ -367,7 +368,7 @@ over the corpus rather than over a smoke import. ## The gate installs the tarball -2026-09-03, the maintainer. Goal 8. Valid while consumers install the packed package. +2026-09-03, the maintainer. Goal 7. Valid while consumers install the packed package. The gate packs the build and installs the tarball under `package-tests/`, so `files`, `exports` and `types` are proved on the artifact that ships rather than on the source tree a self-reference @@ -379,7 +380,7 @@ resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` s ## Firefox reads the build -2026-09-04, the maintainer. Goal 8. Valid while the library claims a browser and no other leg runs +2026-09-04, the maintainer. Goal 7. Valid while the library claims a browser and no other leg runs SpiderMonkey. A headless Firefox loads `dist/index.js` over HTTP and converts the round-trip, normalization and @@ -407,8 +408,8 @@ compared against `undefined` — have a half no valid document reaches. ## The size ratchet -2026-09-20, the maintainer. Goal 11. Valid while no measure picks out what readers find hard -better than a function's length. +2026-09-20, the maintainer. KISS, a technical principle. Valid while no measure picks out what +readers find hard better than a function's length. `.oxlintrc.json`'s single rule, over the files `tsconfig.build.json` builds, is a per-function line ceiling, set at that set's worst and moving only downward. It covers the built files alone, since @@ -431,7 +432,7 @@ markdown, on a fixed seed in the gate; a counterexample found becomes a round-tr ## The CommonMark suite checks three ways -2026-08-27, the maintainer. Goals 1 and 9. Valid while the suite's answers are HTML ADF cannot be +2026-08-27, the maintainer. Goals 1 and 3. Valid while the suite's answers are HTML ADF cannot be compared against. Each example is a named error or markdown that parses and emits to itself byte for byte; its @@ -494,7 +495,7 @@ termination is the loop's own check. ## Readers scan by index -2026-08-30, the kept scan 2026-09-18, the maintainer. Goal 10. Valid while the pipeline persona +2026-08-30, the kept scan 2026-09-18, the maintainer. Goal 8. Valid while the pipeline persona feeds documents nobody typed. A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the line @@ -506,7 +507,7 @@ two cannot disagree — which is what makes the kept value a memo rather than a ## The spelling memo -2026-09-19, the maintainer. Goal 10. Valid while the `commonMarkSpelling` ask spells a node once +2026-09-19, the maintainer. Goal 8. Valid while the `commonMarkSpelling` ask spells a node once per level above it otherwise. The parse and the plain reduction keep each node's readable spelling in a memo, so the @@ -518,7 +519,7 @@ one. Only what succeeded is kept, so no path minted at another position is ever ## Cost fixes are measured, never timed -2026-09-18, the maintainer and the stability-reviewer. Goal 10. Valid while Goal 10 promises growth +2026-09-18, the maintainer and the stability-reviewer. Goal 8. Valid while Goal 8 promises growth rather than a figure. A cost fix that changes no behaviour lands on the suite staying green with no fixture output @@ -537,7 +538,7 @@ refuses it — which is how the whitespace carry finds a line edge. ## Emphasis follows CommonMark's matching -2026-08-27, the maintainer. Goals 1 and 9. Valid while CommonMark's emphasis rules are the +2026-08-27, the maintainer. Goals 1 and 3. Valid while CommonMark's emphasis rules are the reader's. Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text @@ -546,7 +547,7 @@ only ones in play, and a pair that matching hands to another delimiter rides the ## Readable spellings take the `try` prefix -2026-09-21, the maintainer. Goals 1 and 4. Valid while a readable spelling's refusal would cost a +2026-09-21, the maintainer. Goals 1 and 5. Valid while a readable spelling's refusal would cost a document the general form spells. A readable spelling tried ahead of a general one takes the `try` prefix and fails only where the diff --git a/src/index.ts b/src/index.ts index 3587e8c..7df6ee0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,4 @@ +// Export only the conversions, their types, isAdfDocument and what checking a README guarantee needs. export type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf/document.ts' export type { ConvertError, ConvertErrorCode, ConvertErrorPath, ParseError, Result, SourcePosition } from './result.ts' export type { JsonValue } from './json-value.ts' diff --git a/todo.md b/todo.md index 60724b2..b04b916 100644 --- a/todo.md +++ b/todo.md @@ -2,6 +2,11 @@ ## 0.2.0 +- **43 — Give each markdown input its own reader, strict to its own standard.** Today + `markdownToAdf` reads CommonMark and the lossless flavour as one input, so text CommonMark reads + one way — shaped like a directive, a pipe table or a `~~` pair — the flavour claims (Goals 3 and + 4). A caller names the markdown it hands in: CommonMark, read as its spec says, or the lossless + flavour, read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a caller takes. - **40 — Make `markdownToAdf(adfToMarkdown(doc))` deep-equal `doc` for every document it takes.** Today it holds for editor-normal documents only: two adjacent text nodes with the same marks merge, an empty `attrs`, `marks` or `content` drops, and `-0` reads back `0` — shapes pipelines @@ -26,11 +31,11 @@ measured one. - **33 — Make a carried mark run cost the line one re-emit.** `adfToMarkdown` spends 23 s on one paragraph of 2000 × `un` plus `**-r**`: each run its flanking cannot spell re-emits the whole line - before riding the carry, quadratic in the runs (Goal 10), and the plain reduction's + before riding the carry, quadratic in the runs (Goal 8), and the plain reduction's `spellableLine` drops one mark per re-emit the same way. Make both linear. - **42 — Trim a text leaf's trailing blanks in linear time.** `plain-inline.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic in a run of blanks inside one leaf: a - paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in `adfToPlainMarkdown` (Goal 10). Scan backward, + paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in `adfToPlainMarkdown` (Goal 8). Scan backward, as the expand title's trim does. - **34 — Read emphasis flanking by the whole character beside an astral symbol.** Check whether `line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an astral @@ -72,6 +77,10 @@ Revisit: whether npm has added Gitea or self-hosted OIDC, and otherwise whether the release moves to the staged publish — which fits badly with publish-on-merge, and is the trade to weigh rather than discover on a red release run. It stays out of `0.2.0` knowing the cutoff may land first. -- **8 — Ship a CLI, shaped around the personas.** - **9 — Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on the library's browser build.** + +## 0.4.0 + +- **8 — Ship a CLI.** Its goal and its persona land in the README's `## Goals` and `## Audience` + with it. -- 2.52.0 From cd2b573372f4d2f8e369cff2d9cd0003065a5ab4 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 2 Oct 2026 17:39:25 +0200 Subject: [PATCH 2/7] Answer the prose and product-owner reviews' first round --- AGENTS.md | 8 ++++---- README.md | 24 +++++++++++++----------- docs/decisions.md | 7 +++---- src/index.ts | 2 +- todo.md | 12 ++++++++---- 5 files changed, 29 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4a87822..f3ab460 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -131,10 +131,10 @@ One-line commit messages and PR titles; short PR summaries. No AI-attribution ma ## 7. The working loop `todo.md` lists what is left under the release that ships it, in shipping order. A session works -one chunk, starting from the first item under the earliest release, and stops there 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. +one chunk, starting from the first item under the earliest release, and stops when that chunk +merges, 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 (§3). diff --git a/README.md b/README.md index e2efbe9..33a9ad1 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,9 @@ The most useful ADF conversion library available, by these goals in priority ord ## Audience -Application developers embedding the library, in four personas. All four rely on the guarantees below and on `code` being a closed list; none may -rely on an error message's wording, which is free text. +Application developers embedding the library, in four personas. All four rely on the guarantees +below and on `code` being a closed list; none may rely on an error message's wording, which is +free text. - **Viewer/editor app** — shows a document, lets a human edit, posts it back. Relies on the round-trip holding for whatever the site's editor wrote, unknown node types included, and on a @@ -68,8 +69,9 @@ if (result.ok) { } ``` -Serves Goals 1 and 7. Pure functions, each taking a whole document and returning a whole result; -no I/O, no configuration. +Serves Goals 1, 2 and 7. Pure functions, each taking a whole document and returning a whole +result; no I/O, no configuration. Every conversion goes through ADF, so `markdownToHtml` keeps +exactly what ADF holds. ```ts adfToMarkdown(doc: AdfDocument): Result @@ -143,8 +145,8 @@ read replaces mentions, attachments and macros with text. ## The errors -Serves Goal 1. An ADF node type this version does not know is not an error: the lossless pair carries it opaquely -and restores it unchanged ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)). +Serves Goal 1. An ADF node type this version does not know is not an error: the lossless pair +carries it opaquely and restores it unchanged ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)). `ConvertError` is `{ code, message, path, position? }`. `code` is the exported `ConvertErrorCode`, stable across minors and safe to `switch` on exhaustively with no `default`; `message` is free text @@ -197,11 +199,11 @@ emit refuses: Serves Goals 1, 3 and 4. -- Markdown means what the CommonMark spec says, and well-formed HTML what the HTML standard - parses, both in what this library reads and in what a conforming parser reads from what it - writes; the bullets below name every exception. - `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)). +- Markdown means what the CommonMark spec says — and, at `0.2.0`, well-formed HTML what the HTML + standard parses — both in what this library reads and in what a conforming parser reads back + from its output; the bullets below name every exception. - Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html` names, with three carve-outs — literal text matching directive, pipe-table or strikethrough syntax is claimed (escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as @@ -239,8 +241,8 @@ Serves Goals 1, 3 and 4. ## The package -Serves Goal 7. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts` beside it. -Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node, +Serves Goal 7. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts` +beside it. Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node, Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint. Contract: [`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#any-es2022-engine), §Any ES2022 engine to §Public on npm. diff --git a/docs/decisions.md b/docs/decisions.md index a16329a..efc13e3 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -88,10 +88,9 @@ write `!adf:`. Directives are one grammar for everything markdown lacks, namespaced under `!adf:`: `!adf:panel info` … `!adf:/panel` blocks, `!adf:mention[@Mikael]{id=5b10a2}` inline, `\!adf:` the -one escape. A node CommonMark can spell takes that spelling, never a directive. Not CommonMark's -generic-directives proposal: its `:::` claims a form prose writes, and its fence-length discipline -ties a container's opener to its own body, where closing from the opener nests by itself and leaf -versus container falls out of the node's content model. +one escape. Not CommonMark's generic-directives proposal: its `:::` claims a form prose writes, and +its fence-length discipline ties a container's opener to its own body, where closing from the +opener nests by itself and leaf versus container falls out of the node's content model. ## CommonMark is a subset diff --git a/src/index.ts b/src/index.ts index 7df6ee0..258c0bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -// Export only the conversions, their types, isAdfDocument and what checking a README guarantee needs. +// Export only the conversions, their types, isAdfDocument and what a README guarantee or a persona needs. export type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf/document.ts' export type { ConvertError, ConvertErrorCode, ConvertErrorPath, ParseError, Result, SourcePosition } from './result.ts' export type { JsonValue } from './json-value.ts' diff --git a/todo.md b/todo.md index b04b916..c017739 100644 --- a/todo.md +++ b/todo.md @@ -3,10 +3,11 @@ ## 0.2.0 - **43 — Give each markdown input its own reader, strict to its own standard.** Today - `markdownToAdf` reads CommonMark and the lossless flavour as one input, so text CommonMark reads - one way — shaped like a directive, a pipe table or a `~~` pair — the flavour claims (Goals 3 and - 4). A caller names the markdown it hands in: CommonMark, read as its spec says, or the lossless - flavour, read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a caller takes. + `markdownToAdf` reads CommonMark and the lossless flavour as one input: text shaped like a + directive, a pipe table or a `~~` pair becomes a flavour node where CommonMark reads plain text + (Goals 3 and 4). A caller names the markdown it hands in: CommonMark, read as its spec says, or + the lossless flavour, read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a + caller takes. - **40 — Make `markdownToAdf(adfToMarkdown(doc))` deep-equal `doc` for every document it takes.** Today it holds for editor-normal documents only: two adjacent text nodes with the same marks merge, an empty `attrs`, `marks` or `content` drops, and `-0` reads back `0` — shapes pipelines @@ -64,6 +65,9 @@ `description` naming both, the lossy pair, and the flavours it writes and reads by name — GitHub Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a search for either finds the package. +- **44 — Delete `AGENTS.md` §7's empty-release bullet, leaving the maintainer's global working loop + to rule it.** "An earliest release with no items left and nothing shipped toward it is planned as + the chunk" restates that loop, in a sentence its own prose rules ban. ## 0.3.0 -- 2.52.0 From dccd8dcf3a4d482ec765388c18cfce3f38acffa9 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 2 Oct 2026 17:41:00 +0200 Subject: [PATCH 3/7] Answer the prose review's second round and the product-owner review's second --- AGENTS.md | 4 ++-- README.md | 22 +++++++++++----------- todo.md | 7 ++++--- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f3ab460..1501d2f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -143,7 +143,7 @@ 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 by the maintainer through the - `0.2.0` release), delete the chunk's items from `todo.md` — what a consumer sees of it is + `0.2.0` release), delete the chunk's items from `todo.md` — what a consumer sees of them 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 @@ -172,7 +172,7 @@ The verdict lands in `docs/decisions.md`. ### Findings, numbers and empty releases -- A finding inside the chunk's items is fixed in the chunk. Outside it, a new `todo.md` item, always +- A finding inside the chunk's items is fixed in the chunk. Outside them, a new `todo.md` item, always in a release, weighed against every item on that release by the personas and `docs/decisions.md` §Plain markdown is a flavour of the grammar through §Names stay text — an item it outweighs moves later. A weighing no entry decides is asked as a gap. diff --git a/README.md b/README.md index 33a9ad1..2341e7a 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ The most useful ADF conversion library available, by these goals in priority ord ## Audience Application developers embedding the library, in four personas. All four rely on the guarantees -below and on `code` being a closed list; none may rely on an error message's wording, which is -free text. +below and on an error's `code` being a closed list; none may rely on an error message's wording, +which is free text. - **Viewer/editor app** — shows a document, lets a human edit, posts it back. Relies on the round-trip holding for whatever the site's editor wrote, unknown node types included, and on a @@ -70,8 +70,8 @@ if (result.ok) { ``` Serves Goals 1, 2 and 7. Pure functions, each taking a whole document and returning a whole -result; no I/O, no configuration. Every conversion goes through ADF, so `markdownToHtml` keeps -exactly what ADF holds. +result; no I/O, no configuration. `markdownToHtml` and `htmlToMarkdown` convert through ADF, so +they drop whatever ADF cannot hold. ```ts adfToMarkdown(doc: AdfDocument): Result @@ -83,8 +83,8 @@ plainMarkdownToAdf(markdown: string): Result adfToHtml(doc: AdfDocument): Result // 0.2.0 htmlToAdf(html: string): Result // 0.2.0 -markdownToHtml(markdown: string): Result // 0.2.0, via ADF -htmlToMarkdown(html: string): Result // 0.2.0, via ADF +markdownToHtml(markdown: string): Result // 0.2.0 +htmlToMarkdown(html: string): Result // 0.2.0 ``` `Result` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws. @@ -201,9 +201,9 @@ Serves Goals 1, 3 and 4. - `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)). -- Markdown means what the CommonMark spec says — and, at `0.2.0`, well-formed HTML what the HTML - standard parses — both in what this library reads and in what a conforming parser reads back - from its output; the bullets below name every exception. +- Markdown this library reads, and markdown it writes, means what the CommonMark spec says; from + `0.2.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below + name every exception. - Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html` names, with three carve-outs — literal text matching directive, pipe-table or strikethrough syntax is claimed (escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as @@ -242,7 +242,7 @@ Serves Goals 1, 3 and 4. ## The package Serves Goal 7. ESM only, no runtime dependencies, public npm. Built JavaScript with `.d.ts` -beside it. Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node, -Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint. +beside it. Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs +under Node, Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint. Contract: [`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#any-es2022-engine), §Any ES2022 engine to §Public on npm. diff --git a/todo.md b/todo.md index c017739..4424c54 100644 --- a/todo.md +++ b/todo.md @@ -65,9 +65,10 @@ `description` naming both, the lossy pair, and the flavours it writes and reads by name — GitHub Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a search for either finds the package. -- **44 — Delete `AGENTS.md` §7's empty-release bullet, leaving the maintainer's global working loop - to rule it.** "An earliest release with no items left and nothing shipped toward it is planned as - the chunk" restates that loop, in a sentence its own prose rules ban. +- **44 — Cut `AGENTS.md` §7's empty-release bullet to what the maintainer's global working loop + leaves open.** The bullet restates that loop's rule for an empty release, in a sentence + `~/.claude/CLAUDE.md` → "Prose" bans; keep only its weighing by the personas and + `docs/decisions.md`, which the global loop does not hold. ## 0.3.0 -- 2.52.0 From 361139a4d4f26fb62c93787e4c46ac445298dcce Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 2 Oct 2026 17:41:07 +0200 Subject: [PATCH 4/7] Rewrap two lines past 100 columns --- AGENTS.md | 8 ++++---- src/index.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1501d2f..89a5ea4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -172,10 +172,10 @@ The verdict lands in `docs/decisions.md`. ### Findings, numbers and empty releases -- A finding inside the chunk's items is fixed in the chunk. Outside them, a new `todo.md` item, always - in a release, weighed against every item on that release by the personas and `docs/decisions.md` - §Plain markdown is a flavour of the grammar through §Names stay text — an item it outweighs moves - later. A weighing no entry decides is asked as a gap. +- A finding inside the chunk's items is fixed in the chunk. Outside them, a new `todo.md` item, + always in a release, weighed against every item on that release by the personas and + `docs/decisions.md` §Plain markdown is a flavour of the grammar through §Names stay text — an item + it outweighs moves later. A weighing no entry 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 entry states is a gap. - An earliest release with no items left and nothing shipped toward it is planned as the chunk: diff --git a/src/index.ts b/src/index.ts index 258c0bb..a25f670 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -// Export only the conversions, their types, isAdfDocument and what a README guarantee or a persona needs. +// Export only the conversions, their types, isAdfDocument and what a guarantee or a persona needs. export type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf/document.ts' export type { ConvertError, ConvertErrorCode, ConvertErrorPath, ParseError, Result, SourcePosition } from './result.ts' export type { JsonValue } from './json-value.ts' -- 2.52.0 From 95af770e0ab8cefb9ecfc775b2e4982531da5b0e Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 2 Oct 2026 17:41:47 +0200 Subject: [PATCH 5/7] Answer the prose review's third round --- AGENTS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 89a5ea4..ad414a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -143,8 +143,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 by the maintainer through the - `0.2.0` release), delete the chunk's items from `todo.md` — what a consumer sees of them is - reworded for them into `CHANGELOG.md`'s `## Unreleased` — report, stop. + `0.2.0` release), delete the chunk's items from `todo.md` — the part a consumer sees goes to + `CHANGELOG.md`'s `## Unreleased`, reworded for consumers — report, stop. Reserved for the maintainer whatever any rule here says: changing `version` in `package.json` (a bump on `main` publishes, `docs/decisions.md` §Publish on a version bump — every release is the -- 2.52.0 From 4d3231c86b802c87da3c5800c248e03caf712ee0 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 2 Oct 2026 17:41:54 +0200 Subject: [PATCH 6/7] Answer the product-owner review's third round --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2341e7a..a7bc773 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ if (result.ok) { ``` Serves Goals 1, 2 and 7. Pure functions, each taking a whole document and returning a whole -result; no I/O, no configuration. `markdownToHtml` and `htmlToMarkdown` convert through ADF, so -they drop whatever ADF cannot hold. +result; no I/O, no configuration. `markdownToHtml` and `htmlToMarkdown` convert through ADF: +they keep only what ADF holds, and refuse what `markdownToAdf` or `htmlToAdf` refuses. ```ts adfToMarkdown(doc: AdfDocument): Result -- 2.52.0 From 2252232fd64d9ed51019362e951218abd290e952 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 2 Oct 2026 21:57:03 +0200 Subject: [PATCH 7/7] Goals judged in order, Goal 7 names no runtime dependencies, file the isAdfDocument result --- README.md | 4 ++-- todo.md | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a7bc773..bfcb695 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ represent. ## Goals -The most useful ADF conversion library available, by these goals in priority order: +The most useful ADF conversion library available, judged by these goals, in priority order: 1. **Lossless, and every call returns a result, never a throw.** 2. **ADF is the hub.** @@ -31,7 +31,7 @@ The most useful ADF conversion library available, by these goals in priority ord 4. **Our markdown is CommonMark, extended only where CommonMark has no spelling.** 5. **No surprises: output reads and edits the way its audience expects.** 6. **Lossy conversion drops form, never content.** -7. **Runs in any JavaScript engine, with nothing to install, configure or connect.** +7. **Runs in any JavaScript engine, with no runtime dependencies and nothing to configure or connect.** 8. **Fast, and linear in the document's size.** 9. **Easy to find, and clear at a glance what it does.** diff --git a/todo.md b/todo.md index 4424c54..857eb06 100644 --- a/todo.md +++ b/todo.md @@ -8,6 +8,10 @@ (Goals 3 and 4). A caller names the markdown it hands in: CommonMark, read as its spec says, or the lossless flavour, read as `spec/flavour.md` says. Breaking: `MIGRATION.md` says which call a caller takes. +- **45 — Replace `isAdfDocument` with a reader returning `Result`.** Goal 1 has every + call return a result; the boolean guard is the one export that does not, and it cannot say which + branch refused, where `not-an-adf-document`'s message already does. Breaking: `MIGRATION.md` + shows the guard's replacement. - **40 — Make `markdownToAdf(adfToMarkdown(doc))` deep-equal `doc` for every document it takes.** Today it holds for editor-normal documents only: two adjacent text nodes with the same marks merge, an empty `attrs`, `marks` or `content` drops, and `-0` reads back `0` — shapes pipelines -- 2.52.0