Goals become one-line aims, their detail moves to the sections that meet them #152

Merged
lilleman merged 7 commits from goals-slogans into main 2026-10-02 21:58:15 +02:00
5 changed files with 100 additions and 99 deletions
+12 -14
View File
@@ -130,13 +130,11 @@ One-line commit messages and PR titles; short PR summaries. No AI-attribution ma
## 7. The working loop ## 7. The working loop
`todo.md` lists what is left under the release that ships it, in shipping order. One item per `todo.md` lists what is left under the release that ships it, in shipping order. A session works
session — the first under the earliest release — in the smallest PR-able chunk; split a big item one chunk, starting from the first item under the earliest release, and stops when that chunk
into sub-items in `todo.md` before starting it. A chunk running a little over or under that is not merges, whatever it was asked to finish: a release is a chain of sessions, so an instruction to
worth deliberating; what matters is that nothing is left undone in the end. The session stops there work until a release is done names the chain, not the session. An open PR is a chunk already in
whatever it was asked to finish: a release is a chain of sessions, so an instruction to work until a flight, and finishing it is the session.
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: Per chunk:
1. Fresh worktree off updated `origin/main`; implement tests-first (§3). 1. Fresh worktree off updated `origin/main`; implement tests-first (§3).
@@ -145,8 +143,8 @@ Per chunk:
result exists for the commit under review, or when the diff since that result cannot affect 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. 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 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` — the part a consumer sees goes to
reworded for them into `CHANGELOG.md`'s `## Unreleased` — report, stop. `CHANGELOG.md`'s `## Unreleased`, reworded for consumers — report, stop.
Reserved for the maintainer whatever any rule here says: changing `version` in `package.json` (a 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 bump on `main` publishes, `docs/decisions.md` §Publish on a version bump — every release is the
@@ -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 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. 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 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 `## 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 allow, rendered, shuffled, with no rationale and nothing saying what is implemented. Three agreeing
@@ -174,10 +172,10 @@ The verdict lands in `docs/decisions.md`.
### Findings, numbers and empty releases ### 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 them, a new `todo.md` item,
in a release, weighed against every item on that release by the personas and `docs/decisions.md` always in a release, weighed against every item on that release by the personas and
§Plain markdown is a flavour of the grammar through §Names stay text — an item it outweighs moves `docs/decisions.md` §Plain markdown is a flavour of the grammar through §Names stay text — an item
later. A weighing no entry decides is asked as a gap. 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, - 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. 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: - An earliest release with no items left and nothing shipped toward it is planned as the chunk:
+34 -50
View File
@@ -23,48 +23,23 @@ represent.
## Goals ## Goals
In priority order. The most useful ADF conversion library available, judged by these goals, in priority order:
1. **Lossless, and every call returns.** The round-trip holds for every document the lossless 1. **Lossless, and every call returns a result, never a throw.**
conversions take, node types this version does not know included; one that has no spelling is 2. **ADF is the hub.**
refused and says where, never silently reduced. No input makes a call loop forever or overflow 3. **Each format reads and writes as its standard says.**
the stack. Every goal below gives way to this one. 4. **Our markdown is CommonMark, extended only where CommonMark has no spelling.**
2. **ADF is the hub.** Every format and flavour converts to and from ADF, and no two others 5. **No surprises: output reads and edits the way its audience expects.**
convert directly: markdown↔HTML composes through ADF. Adding a format or flavour costs one 6. **Lossy conversion drops form, never content.**
reader and one writer. A flavour of a grammar shares that grammar's reader and writer and adds 7. **Runs in any JavaScript engine, with no runtime dependencies and nothing to configure or connect.**
only its own spellings. 8. **Fast, and linear in the document's size.**
3. **Plain CommonMark is input.** Markdown written for something else converts — the exceptions 9. **Easy to find, and clear at a glance what it does.**
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.
## Audience ## Audience
Application developers embedding the library, addressed as personas rather than named consumers Application developers embedding the library, in four personas. All four rely on the guarantees
(AGENTS.md §1). All four rely on the guarantees below and on `code` being a closed list; none may below and on an error's `code` being a closed list; none may rely on an error message's wording,
rely on an error message's wording, which is free text. which is free text.
- **Viewer/editor app** — shows a document, lets a human edit, posts it back. Relies on the - **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 round-trip holding for whatever the site's editor wrote, unknown node types included, and on a
@@ -94,7 +69,9 @@ if (result.ok) {
} }
``` ```
Pure functions, 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. `markdownToHtml` and `htmlToMarkdown` convert through ADF:
they keep only what ADF holds, and refuse what `markdownToAdf` or `htmlToAdf` refuses.
```ts ```ts
adfToMarkdown(doc: AdfDocument): Result<string> adfToMarkdown(doc: AdfDocument): Result<string>
@@ -106,17 +83,18 @@ plainMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError>
adfToHtml(doc: AdfDocument): Result<string> // 0.2.0 adfToHtml(doc: AdfDocument): Result<string> // 0.2.0
htmlToAdf(html: string): Result<AdfDocument, ParseError> // 0.2.0 htmlToAdf(html: string): Result<AdfDocument, ParseError> // 0.2.0
markdownToHtml(markdown: string): Result<string> // 0.2.0, via ADF markdownToHtml(markdown: string): Result<string> // 0.2.0
htmlToMarkdown(html: string): Result<string> // 0.2.0, via ADF htmlToMarkdown(html: string): Result<string> // 0.2.0
``` ```
`Result<T>` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws. `Result<T>` is `{ ok: true; value: T } | { ok: false; error: ConvertError }` — nothing throws.
## Plain markdown ## Plain markdown
Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes markdown other Serves Goal 6. Plain markdown is a second flavour of the same grammar. `adfToPlainMarkdown` writes
tools render — GitHub, GitLab, Obsidian and the like — keeping the content and dropping the rest: markdown other tools render — GitHub, GitLab, Obsidian and the like — keeping the content and
attributes, colours, layout, identity. It refuses only 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 `not-an-adf-document`, `unsupported-document-version` and `unsupported-nesting-depth`, and writes
no directive. no directive.
@@ -167,8 +145,8 @@ read replaces mentions, attachments and macros with text.
## The errors ## 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
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)). 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`, `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 stable across minors and safe to `switch` on exhaustively with no `default`; `message` is free text
@@ -219,8 +197,13 @@ emit refuses:
## The guarantees ## The guarantees
Serves Goals 1, 3 and 4.
- `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely - `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)). ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
- 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` - 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 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 syntax is claimed (escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as
@@ -247,7 +230,8 @@ emit refuses:
input. input.
- Past that and `~~`, no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist - 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`. 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 - 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)). ([`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 - **`0.2.0`** — `htmlToAdf(adfToHtml(doc))` equals `doc`; fidelity HTML cannot express rides
@@ -257,8 +241,8 @@ emit refuses:
## The package ## 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`
Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs under Node, beside it. Pure ECMAScript at an ES2022 baseline, reaching for no host API; the test suite runs
Deno and Bun, and a headless Firefox converts the corpus through the built entrypoint. 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 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. ES2022 engine to §Public on npm.
+32 -32
View File
@@ -23,7 +23,7 @@ sanitized and a mention keeps the test user's real account id.
## Markdown in is a canonical fixpoint ## 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 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 — 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 ## 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. 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 Every foreign element `htmlToAdf` and `markdownToAdf` read sorts one of three ways, never a silent
@@ -76,14 +76,14 @@ than they buy.
## Names stay text ## 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 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. mention/emoji/media nodes; resolving names to ids is the consumer's job.
## Directives under `!adf:` ## 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:`. write `!adf:`.
Directives are one grammar for everything markdown lacks, namespaced under `!adf:`: Directives are one grammar for everything markdown lacks, namespaced under `!adf:`:
@@ -94,21 +94,21 @@ opener nests by itself and leaf versus container falls out of the node's content
## CommonMark is a subset ## 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 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. directive, a pipe table or a `~~` pair is claimed — plus one image gap.
## Tables ## 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. cells.
One header row plus plain inline cells → pipe table; anything richer → directive form. One header row plus plain inline cells → pipe table; anything richer → directive form.
## Links ## 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. syntax is what readers edit.
`[text](url "title")`, or `<url>` for a bare autolink-shaped text, wherever CommonMark spells the `[text](url "title")`, or `<url>` for a bare autolink-shaped text, wherever CommonMark spells the
@@ -127,7 +127,7 @@ accepted.
## Plain task ids come from position ## 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`. node with no `localId`.
`plainMarkdownToAdf` gives each `taskList`, `taskItem` and `blockTaskItem` lacking one a `localId` `plainMarkdownToAdf` gives each `taskList`, `taskItem` and `blockTaskItem` lacking one a `localId`
@@ -167,7 +167,7 @@ past `[x]`/`[ ]`, and lifting bare URLs, `@name`, `:shortcode:` or ISO dates int
## The HTML dialect ## 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. themselves.
The HTML dialect mirrors the markdown flavour: semantic elements, stable `adf-*` classes, `data-*` The HTML dialect mirrors the markdown flavour: semantic elements, stable `adf-*` classes, `data-*`
@@ -175,7 +175,7 @@ for what HTML cannot express, text always escaped. No stylesheet ships.
## No runtime dependencies ## 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. job a dependency would.
`dependencies` is empty. A runtime dependency enters only through an entry here stating why ~20 `dependencies` is empty. A runtime dependency enters only through an entry here stating why ~20
@@ -185,7 +185,7 @@ and HTML parsers are written in this repo.
## Standards ship as data ## Standards ship as data
2026-08-30, the CommonMark suite 2026-09-05 and ADF's schemas 2026-09-13, the maintainer. Goals 1, 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 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. character references ship packed in their own module, so entity decoding is complete without one.
@@ -208,7 +208,7 @@ nodes that break it.
## Any ES2022 engine ## 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 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. shipped source is ECMAScript and nothing else: no host import, no host global, no DOM.
@@ -222,13 +222,13 @@ never the higher one those repo-only tools want.
## ESM only ## 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. No CommonJS build, no dual-package hazard.
## One built entrypoint ## 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 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 type-strip under `node_modules` (`ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`), so it cannot serve
@@ -236,7 +236,7 @@ an npm consumer.
## Public on npm ## 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. stays public beside it.
Published to public npm as `@larvit/adf-codec`. Public source: the Gitea repo goes public, Published to public npm as `@larvit/adf-codec`. Public source: the Gitea repo goes public,
@@ -245,7 +245,7 @@ for the hub rather than the formats around it.
## The formats are API ## 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. Valid while consumers store what the library emits.
The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing The emitted markdown and HTML are contracts. After 1.0: previously-emitted output parsing
@@ -260,7 +260,7 @@ types in `src/result.ts` hold its shape.
## The code list ## 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`. switches on `code` with no `default`.
- Adding, removing or renaming a code is breaking, so a new cause takes an existing code whose - Adding, removing or renaming a code is breaking, so a new cause takes an existing code whose
@@ -276,7 +276,7 @@ switches on `code` with no `default`.
## Which code a cause takes ## 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. 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 - A code names the cause; where one cause recurs across node types, across one mark's attributes
@@ -309,7 +309,7 @@ handles one cause alike whichever node, attribute or direction raised it.
## `message` and `path` ## `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`. input reads `message`.
- A message names the violation, not the rule alone — a rule by itself states a truth the reader - A message names the violation, not the rule alone — a rule by itself states a truth the reader
@@ -323,7 +323,7 @@ input reads `message`.
## Publish on a version bump ## 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. token.
`package.json` version on `main` is the source of truth. CI on `main`: tests green and the version `package.json` version on `main` is the source of truth. CI on `main`: tests green and the version
@@ -339,7 +339,7 @@ depend on a store that the gate would then have to keep.
## Docs describe the release being built ## 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 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 moment the bump publishes; add no interim note marking the gap.
@@ -355,7 +355,7 @@ nesting a spelling inside its own kind (`*(*a*)*`) names that mark once.
## The gate runs on Deno and Bun ## 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. any ES2022 engine.
The gate runs the suite under Deno and Bun as well as Node. Bun runs JavaScriptCore, the one engine The gate runs the suite under Deno and Bun as well as Node. Bun runs JavaScriptCore, the one engine
@@ -367,7 +367,7 @@ over the corpus rather than over a smoke import.
## The gate installs the tarball ## 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` 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 and `types` are proved on the artifact that ships rather than on the source tree a self-reference
@@ -379,7 +379,7 @@ resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` s
## Firefox reads the build ## 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. SpiderMonkey.
A headless Firefox loads `dist/index.js` over HTTP and converts the round-trip, normalization and A headless Firefox loads `dist/index.js` over HTTP and converts the round-trip, normalization and
@@ -407,8 +407,8 @@ compared against `undefined` — have a half no valid document reaches.
## The size ratchet ## The size ratchet
2026-09-20, the maintainer. Goal 11. Valid while no measure picks out what readers find hard 2026-09-20, the maintainer. KISS, a technical principle. Valid while no measure picks out what
better than a function's length. 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 `.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 ceiling, set at that set's worst and moving only downward. It covers the built files alone, since
@@ -431,7 +431,7 @@ markdown, on a fixed seed in the gate; a counterexample found becomes a round-tr
## The CommonMark suite checks three ways ## 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. compared against.
Each example is a named error or markdown that parses and emits to itself byte for byte; its Each example is a named error or markdown that parses and emits to itself byte for byte; its
@@ -494,7 +494,7 @@ termination is the loop's own check.
## Readers scan by index ## 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. feeds documents nobody typed.
A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the line A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the line
@@ -506,7 +506,7 @@ two cannot disagree — which is what makes the kept value a memo rather than a
## The spelling memo ## 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. per level above it otherwise.
The parse and the plain reduction keep each node's readable spelling in a memo, so the The parse and the plain reduction keep each node's readable spelling in a memo, so the
@@ -518,7 +518,7 @@ one. Only what succeeded is kept, so no path minted at another position is ever
## Cost fixes are measured, never timed ## 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. rather than a figure.
A cost fix that changes no behaviour lands on the suite staying green with no fixture output A cost fix that changes no behaviour lands on the suite staying green with no fixture output
@@ -537,7 +537,7 @@ refuses it — which is how the whitespace carry finds a line edge.
## Emphasis follows CommonMark's matching ## 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. reader's.
Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text
@@ -546,7 +546,7 @@ only ones in play, and a pair that matching hands to another delimiter rides the
## Readable spellings take the `try` prefix ## 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. document the general form spells.
A readable spelling tried ahead of a general one takes the `try` prefix and fails only where the A readable spelling tried ahead of a general one takes the `try` prefix and fails only where the
+1
View File
@@ -1,3 +1,4 @@
// 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 { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf/document.ts'
export type { ConvertError, ConvertErrorCode, ConvertErrorPath, ParseError, Result, SourcePosition } from './result.ts' export type { ConvertError, ConvertErrorCode, ConvertErrorPath, ParseError, Result, SourcePosition } from './result.ts'
export type { JsonValue } from './json-value.ts' export type { JsonValue } from './json-value.ts'
+21 -3
View File
@@ -2,6 +2,16 @@
## 0.2.0 ## 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: 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.
- **45 — Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** 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.** - **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 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 merge, an empty `attrs`, `marks` or `content` drops, and `-0` reads back `0` — shapes pipelines
@@ -26,11 +36,11 @@
measured one. measured one.
- **33 — Make a carried mark run cost the line one re-emit.** `adfToMarkdown` spends 23 s on 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 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. `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 - **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 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. as the expand title's trim does.
- **34 — Read emphasis flanking by the whole character beside an astral symbol.** Check whether - **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 `line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an astral
@@ -59,6 +69,10 @@
`description` naming both, the lossy pair, and the flavours it writes and reads by name — GitHub `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 Flavored Markdown's alerts and task lists, Obsidian Flavored Markdown's callouts — so a search for
either finds the package. either finds the package.
- **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 ## 0.3.0
@@ -72,6 +86,10 @@
Revisit: whether npm has added Gitea or self-hosted OIDC, and otherwise whether the release moves 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 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. 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 - **9 — Ship an online sandbox: a web page with two textboxes converting between ADF and markdown on
the library's browser build.** 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.