diff --git a/AGENTS.md b/AGENTS.md index 3b76250..9a92350 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,106 +1,33 @@ # Working in this repo -Decisions a reader would otherwise relitigate, and the rules for every collaborator, human or -agent. Using the library: `README.md`. What is still to build: `todo.md`; a bare `(28)` cites -that item's entry in `todo-history.md`. +The rules for every collaborator, human or agent, and an index of the decisions a reader would +otherwise relitigate. Using the library: `README.md`. What is still to build: `todo.md`; a bare +`(28)` cites that item's entry in `todo-history.md`. -## 1. ADF is the hub +## Decisions -README Goal 2. The lossy pair is the plain flavour: the markdown grammar's reader and writer with -the flavour set, its spellings — alerts, callouts, task markers, `==` — read and written there, so -a marker line and a backslash reach them intact; what the flavour cannot spell reduces ADF→ADF -ahead of the writer (the maintainer, 2026-09-27; 35). +In `docs/decisions.md`: -## 2. The round-trip is the product - -`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything -less silently destroys content an editor could not represent, in a document it did not author. -When losslessness and readability conflict, losslessness wins. - -The other direction is a canonical fixpoint, not byte-identity: human markdown normalizes, the way -back yields the library's canonical spelling, and that spelling round-trips byte-identically — -where there is a way back. CommonMark spells some things the flavour has no escape for — a -paragraph opening with a code span whose backticks read back as a fence — so a parse succeeding -does not imply a spellable document; -`corpus/commonmark-spec/exceptions.json` names those. - -"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks -and no attributes merged, JSON number semantics, an empty attrs object, marks array or content -array the absent key — the only domain markdown can restore. - -Round-trip equality is a property tested over a corpus, not a claim made in prose. - -## 3. Unknown input policy - -- Unknown ADF node: carried opaquely — raw JSON rides a dedicated syntax in both formats and - restores to a deep-equal node. The round-trip holds for documents newer than the library. So - does a known node no section spells where it stands: a markdown serializer spells a node by type - without checking its position, and refusing loses a document ADF itself keeps in an - `unsupportedBlock`. Where a container's own spelling cannot hold the child it has — a - `bulletList` outside `listItem`, a `codeBlock` outside text — the error result names that - instead. -- Unmappable foreign HTML element: error result naming the element — never a silent drop. -- Bare `@name` / `:smile:` in typed text: stays a text node. Only directives produce - mention/emoji/media nodes; resolving names to ids needs I/O, which is the consumer's job. - -## 4. The flavour - -- Directives, 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. -- 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: one header row plus plain inline cells → pipe table; anything richer → directive form. -- Links: `[text](url "title")`, or `` for a bare autolink-shaped text, wherever CommonMark - spells the mark; `!adf:link[text]{attrs}` where it does not — an attribute CommonMark cannot - hold, an `href` or `title` no canonical escape spells, a paragraph opening whose CommonMark - spelling would read as a link reference definition — and a directive link CommonMark could spell - is refused (the maintainer, 2026-09-13). No link wraps a link — the bracket form goes literal, - the directive form refused — which is CommonMark's prose where its reference implementation - nests one `` in another (the maintainer, 2026-09-17). -- Identity-bearing nodes carry their ids in attributes; a document is only portable within its - site — accepted. -- The HTML dialect mirrors this: semantic elements, stable `adf-*` classes, `data-*` for what HTML - cannot express, text always escaped. No stylesheet ships. - -## 5. Dependencies - -`dependencies` is empty. A runtime dependency enters only through a decision entry here stating -why ~20 lines of own code cannot do the job, who maintains it, and what auditing it costs. So the -CommonMark and HTML parsers are written in this repo. 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. The CommonMark spec suite is the same shape of -data and ships vendored at `corpus/commonmark-spec/` rather than as the `commonmark-spec` dev -dependency — that package is CommonJS-only, and Renovate auto-bumping a spec version would silently -point the vendored exception list's example numbers at a renumbered suite. A spec bump is a -deliberate re-pin, exceptions re-derived by hand beside it. Atlassian's ADF JSON Schemas ship -vendored the same way, at `spec/adf-schema/`, rather than as the `@atlaskit/adf-schema` dev -dependency — CommonJS-only, some fifty packages with React among them, and a release most days for -Renovate to automerge — re-pinned by hand when a payload or a report shows the need. -`devDependencies`: `fast-check` earns its place shrinking a failing generated document to the nodes -that break it, `oxlint` measuring §10's size ratchet — TypeScript 7 is a native compiler publishing -no in-process parser, only the `unstable/` AST surface an out-of-process handshake reaches. - -## 6. The package contract - -- 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. `tsconfig.build.json` is - that gate, typechecking and emitting the shipped files alone, so `node:fs`, `process` and an - ES2024 method are compile errors here rather than a consumer's crash there. The standard is the line, never an - engine list: one implementing it in part — Hermes is the live doubt, on §10's property escapes - and on lookbehind — is out of scope rather than a bug. Node's test runner, the corpus reads and - the build are the repo's own, - never the library's, and `engines.node` states the floor the shipped JavaScript needs — `>=18` — - never the higher one those repo-only tools want. -- ESM only — no CommonJS build, no dual-package hazard. -- One entrypoint: 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 an npm consumer. -- Published to public npmjs as `@larvit/adf-codec`. Public source: the Gitea repo - goes public, LICENSE in place, before the first publish. -- Exact versions: `save-exact=true` in `.npmrc`. +- Plain markdown is a flavour of the grammar +- The round-trip is the product +- Markdown in is a canonical fixpoint +- Equality is editor-normal +- Unknown nodes ride the carry +- Foreign HTML is refused by name +- Names stay text +- Directives under `!adf:` +- CommonMark is a subset +- Tables +- Links +- Ids stay site-local +- The HTML dialect +- No runtime dependencies +- Standards ship as data +- fast-check +- Any ES2022 engine +- ESM only +- One built entrypoint +- Public on npm ## 7. Nothing about any consumer @@ -191,6 +118,7 @@ types in `src/result.ts` hold its shape. than taking the gate's `dist`: the lockfile is committed, the image is patch-pinned and `tsc` is deterministic, so the two builds agree, and promoting an artifact would make the release path depend on a store that the gate would then have to keep. +- Exact versions: `save-exact=true` in `.npmrc`. - Renovate watches devDependencies, Docker pins and action tags; automerges everything on green CI. - Docker images pin the full patch version (`node:24.19.0-alpine3.24`, never `node:24`), as specific as the publisher tags: `oven/bun:1.4.0-alpine` pins Bun's patch and leaves the base @@ -219,16 +147,16 @@ resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` s `node-floor.js` round-trips the installed package under a Node pinned to `engines.node`'s floor. A fourth engine reads the build rather than the source: a headless Firefox loads `dist/index.js` -over HTTP and converts the round-trip, normalization and error fixtures and the real payloads — -the `commonmark-spec` sort is the Node suite's to check — which is §6's browser half and the only -SpiderMonkey there is — the gate's other three engines are two V8s and a JavaScriptCore that is -not Safari's. -A WebDriver session is what carries a verdict back out, the driver and the page's server sharing -one network namespace so each is the other's `127.0.0.1`; `--headless --screenshot` has no such -channel, and loading `dist/index.js` in a globals-stripped realm buys one by not running a browser. -The leg re-checks the conversions and nothing else — each fixture's emitted markdown, its parsed -document, its error code — leaving the corpus's pairing, uniqueness, source positions and -byte-level equality to the Node suite that owns them. +over HTTP and converts the round-trip, normalization and error fixtures and the real payloads — the +`commonmark-spec` sort is the Node suite's to check — which is the browser half of +`docs/decisions.md` §Any ES2022 engine and the only SpiderMonkey there is — the gate's other three +engines are two V8s and a JavaScriptCore that is not Safari's. A WebDriver session is what carries a +verdict back out, the driver and the page's server sharing one network namespace so each is the +other's `127.0.0.1`; `--headless --screenshot` has no such channel, and loading `dist/index.js` in a +globals-stripped realm buys one by not running a browser. The leg re-checks the conversions and +nothing else — each fixture's emitted markdown, its parsed document, its error code — leaving the +corpus's pairing, uniqueness, source positions and byte-level equality to the Node suite that owns +them. Every leg announces its name and, where a container is in play, the image, before it runs and its elapsed time after, `publish.sh` alongside `ci.sh`, so a long run reads as progress rather than as @@ -244,14 +172,16 @@ functions, and a branch floor that only ever moves upward. It sits below 100 bec compared against `undefined` — have a half no valid document reaches. The size ratchet is the other such number, `.oxlintrc.json`'s single rule over the files -`tsconfig.build.json` builds: a per-function line ceiling, set at that set's worst and moving only -downward. It covers the built files alone, since one ceiling over the tests too would have to be -their worst, loosening the guard over the shipped code. It guards against drift and never drives a -refactor, so no cyclomatic rule and no second lint rule join it: neither measure picked out what -nine readers found hard (the comprehension panel, 2026-09-20). Three switches guard a silent green: -`IIFEs: true`, since oxlint exempts an IIFE otherwise; an explicit `-c`, so a config gone missing -fails the leg instead of falling back to oxlint's own defaults; and `--deny-warnings`, since a rule -from a category this config never names arrives as a warning it exits 0 on. +`tsconfig.build.json` builds, measured by `oxlint` since TypeScript 7 is a native compiler +publishing no in-process parser, only the `unstable/` AST surface an out-of-process handshake +reaches. It is a per-function line ceiling, set at that set's worst and moving only downward. It +covers the built files alone, since one ceiling over the tests too would have to be their worst, +loosening the guard over the shipped code. It guards against drift and never drives a refactor, so +no cyclomatic rule and no second lint rule join it: neither measure picked out what nine readers +found hard (the comprehension panel, 2026-09-20). Three switches guard a silent green: `IIFEs: +true`, since oxlint exempts an IIFE otherwise; an explicit `-c`, so a config gone missing fails the +leg instead of falling back to oxlint's own defaults; and `--deny-warnings`, since a rule from a +category this config never names arrives as a warning it exits 0 on. The corpus, all checked in: hand-built fixtures per node and combination; real ADF Atlassian's editor wrote; the CommonMark spec suite against `markdownToAdf` and `markdownToHtml`. @@ -269,11 +199,11 @@ of a bullet; fenced examples are skipped. It guards the attributes alone: nodes content model share a bullet, and the argument attribute is spelled ahead of `Attributes: `, so both answer to the round-trip corpus and to nothing else where a node has no fixture. -The tables answer to Atlassian's schema too (§5): for every node and mark they spell, the attribute -names and kinds equal what `full.json` and `stage-0.json` hold between them. Value sets stay -documentation, since any value round-trips. What the schema holds and the tables do not spell is -pinned by name — an attribute as a gap, a type as carried — so a re-pin adding either goes red until -someone spells it or pins it. +The tables answer to Atlassian's schema too (`docs/decisions.md` §Standards ship as data): for every +node and mark they spell, the attribute names and kinds equal what `full.json` and `stage-0.json` +hold between them. Value sets stay documentation, since any value round-trips. What the schema holds +and the tables do not spell is pinned by name — an attribute as a gap, a type as carried — so a +re-pin adding either goes red until someone spells it or pins it. ## 11. Code rules @@ -388,12 +318,13 @@ One-line commit messages and PR titles; short PR summaries. No AI-attribution ma ## 14. Non-goals -No network or filesystem I/O, no name→id resolution (§3), no ADF schema -validation or exported validator — a refusal that keeps the round-trip is not schema validation, -so the one a spelled node carrying the same mark type twice earns stays, and input nesting a -spelling inside its own kind (`*(*a*)*`) names that mark once, no shipped CSS (§4), no -streaming APIs, no performance budget past §11's scanning rule — nothing here is tuned, and no -figure is promised. A CLI is a later goal (`todo.md`), not a non-goal. +No network or filesystem I/O, no name→id resolution (`docs/decisions.md` §Names stay text), no ADF +schema validation or exported validator — a refusal that keeps the round-trip is not schema +validation, so the one a spelled node carrying the same mark type twice earns stays, and input +nesting a spelling inside its own kind (`*(*a*)*`) names that mark once, no shipped CSS +(`docs/decisions.md` §The HTML dialect), no streaming APIs, no performance budget past §11's +scanning rule — nothing here is tuned, and no figure is promised. A CLI is a later goal (`todo.md`), +not a non-goal. ## 15. The working loop @@ -441,8 +372,9 @@ The verdict lands in the item it settles (the maintainer, 2026-09-25). ### Rules the loop has settled (the maintainer, 2026-09-18) - A finding inside the chunk's item 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 §1–§3 — an item it - outweighs moves later. A weighing no rule decides is asked as a gap. + 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 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. - An earliest release with no items left and nothing shipped toward it is planned as the chunk: diff --git a/README.md b/README.md index 584205f..f6d806b 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`. Changes: +Plan: `todo.md`. Decisions: +[`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.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). @@ -143,7 +144,7 @@ saving what this pair read replaces mentions, attachments and macros with text. ## The errors An ADF node type this version does not know is not an error: it is carried opaquely and restores -unchanged (AGENTS.md §3). +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 @@ -195,7 +196,7 @@ emit refuses: ## The guarantees - `markdownToAdf(adfToMarkdown(doc))` equals `doc` — unknown node types included, carried opaquely - (AGENTS.md §3). + ([`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` 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 @@ -234,4 +235,5 @@ emit refuses: ESM only, no runtime dependencies, public npmjs. 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: `AGENTS.md` §5–6. +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/corpus/README.md b/corpus/README.md index ed96da8..e95ad87 100644 --- a/corpus/README.md +++ b/corpus/README.md @@ -3,8 +3,8 @@ One directory per contract kind, each landing with its milestone: - `round-trip/` — `.json` + `.md`: the markdown `adfToMarkdown` must emit for that - document, byte for byte, and that `markdownToAdf` must read back to it (AGENTS.md §2). Grouped - by what the fixture exercises. + document, byte for byte, and that `markdownToAdf` must read back to it (`docs/decisions.md` §The + round-trip is the product). Grouped by what the fixture exercises. - `normalization/` — `.md` + `.json`: markdown input, and the document `markdownToAdf` must build from it, which must in turn emit and read back to itself. The markdown is not canonical. @@ -19,8 +19,8 @@ One directory per contract kind, each landing with its milestone: model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap a later milestone may close). -JSON is editor-normal (AGENTS.md §2), two-space indent, keys sorted. `spec.json` is the vendored, -upstream machine-readable suite, byte-exact from +JSON is editor-normal (`docs/decisions.md` §Equality is editor-normal), two-space indent, keys +sorted. `spec.json` is the vendored, upstream machine-readable suite, byte-exact from [spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json) (CommonMark 0.31.2, © John MacFarlane, [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not re-serialized by the corpus gate. diff --git a/docs/decisions.md b/docs/decisions.md new file mode 100644 index 0000000..84b9a5f --- /dev/null +++ b/docs/decisions.md @@ -0,0 +1,185 @@ +# Decisions + +## Plain markdown is a flavour of the grammar + +2026-09-27, the maintainer. Goal 2. Valid while the plain flavour's spellings are ones the markdown +grammar can read and write. + +The lossy pair is the plain flavour: the markdown grammar's reader and writer with the flavour set, +its spellings — alerts, callouts, task markers, `==` — read and written there, so a marker line and +a backslash reach them intact; what the flavour cannot spell reduces ADF→ADF ahead of the writer. + +## The round-trip is the product + +2026-08-23, the maintainer. Goal 1. Valid while a consumer saves back through the lossless pair. + +`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything +less silently destroys content an editor could not represent, in a document it did not author. +When losslessness and readability conflict, losslessness wins. Round-trip equality is a property +tested over a corpus, not a claim made in prose. + +## Markdown in is a canonical fixpoint + +2026-08-23, the maintainer. Goals 1 and 3. 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 — +where there is a way back. CommonMark spells some things the flavour has no escape for — a +paragraph opening with a code span whose backticks read back as a fence — so a parse succeeding +does not imply a spellable document; `corpus/commonmark-spec/exceptions.json` names those. + +## Equality is editor-normal + +2026-08-24, the maintainer. Goal 1. Valid while markdown cannot tell apart the ADF shapes this +merges. + +"Equals" is structural equality over editor-normal ADF — adjacent text nodes with identical marks +and no attributes merged, JSON number semantics, an empty attrs object, marks array or content +array the absent key — the only domain markdown can restore. + +## Unknown nodes ride the carry + +2026-08-23, extended to misplaced known nodes 2026-08-26, the maintainer. Goal 1. Valid while ADF +holds nodes, or node positions, this library does not spell. + +An unknown ADF node is carried opaquely — raw JSON rides a dedicated syntax in both formats and +restores to a deep-equal node. The round-trip holds for documents newer than the library. So does +a known node no section spells where it stands: a markdown serializer spells a node by type without +checking its position, and refusing loses a document ADF itself keeps in an `unsupportedBlock`. +Where a container's own spelling cannot hold the child it has — a `bulletList` outside `listItem`, +a `codeBlock` outside text — the error result names that instead. + +## Foreign HTML is refused by name + +2026-08-23, the maintainer. Goals 1 and 6. Valid until the HTML dialect's element set lands +(`todo.md`, 6). + +An unmappable foreign HTML element is an error result naming the element — never a silent drop. + +## Names stay text + +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 +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. + +## CommonMark is a subset + +2026-08-23, the maintainer. Goal 3. 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 +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 +syntax is what readers edit. + +`[text](url "title")`, or `` for a bare autolink-shaped text, wherever CommonMark spells the +mark; `!adf:link[text]{attrs}` where it does not — an attribute CommonMark cannot hold, an `href` or +`title` no canonical escape spells, a paragraph opening whose CommonMark spelling would read as a +link reference definition — and a directive link CommonMark could spell is refused. No link wraps a +link — the bracket form goes literal, the directive form refused — which is CommonMark's prose +where its reference implementation nests one `` in another. + +## Ids stay site-local + +2026-08-23, the maintainer. Goal 1. Valid while ADF ids are minted per site. + +Identity-bearing nodes carry their ids in attributes; a document is only portable within its site — +accepted. + +## The HTML dialect + +2026-08-23, the maintainer. Goals 4 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-*` +for what HTML cannot express, text always escaped. No stylesheet ships. + +## No runtime dependencies + +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 +lines of own code cannot do the job, who maintains it, and what auditing it costs. So the CommonMark +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 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. +The CommonMark spec suite is the same shape of data and ships vendored at `corpus/commonmark-spec/` +rather than as the `commonmark-spec` dev dependency — that package is CommonJS-only, and Renovate +auto-bumping a spec version would silently point the vendored exception list's example numbers at a +renumbered suite. A spec bump is a deliberate re-pin, exceptions re-derived by hand beside it. +Atlassian's ADF JSON Schemas ship vendored the same way, at `spec/adf-schema/`, rather than as the +`@atlaskit/adf-schema` dev dependency — CommonJS-only, some fifty packages with React among them, +and a release most days for Renovate to automerge — re-pinned by hand when a payload or a report +shows the need. + +## fast-check + +2026-09-14, the maintainer. Goal 1. Valid while a failing generated document needs shrinking by +hand otherwise. + +`fast-check` earns its place as a devDependency shrinking a failing generated document to the +nodes that break it. + +## Any ES2022 engine + +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. +`tsconfig.build.json` is that gate, typechecking and emitting the shipped files alone, so +`node:fs`, `process` and an ES2024 method are compile errors here rather than a consumer's crash +there. The standard is the line, never an engine list: one implementing it in part — Hermes is the +live doubt, on the Unicode property escapes emphasis matching leans on and on lookbehind — is out +of scope rather than a bug. Node's test runner, the corpus reads and the build are the repo's own, +never the library's, and `engines.node` states the floor the shipped JavaScript needs — `>=18` — +never the higher one those repo-only tools want. + +## ESM only + +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 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 +an npm consumer. + +## Public on npm + +2026-08-23, the maintainer. The Audience. Valid while the audience installs from public +npm. + +Published to public npmjs as `@larvit/adf-codec`. Public source: the Gitea repo goes public, +LICENSE in place, before the first publish. diff --git a/spec/flavour.md b/spec/flavour.md index 74fddfc..f1ed888 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -154,15 +154,15 @@ outside code spans and code blocks, `\!adf:` in input yields the literal text. naming no open container or a node other than the innermost open one, a leaf given a body, an `!adf:` completing no directive, an inline `[content]` or `{attrs}` left unclosed at end of line, unparseable or duplicate-keyed attrs, invalid JSON in an opaque carry. Never a silent literal-text -fallback — a typo that reparses as prose is the silent loss §2 refuses. +fallback — a typo that reparses as prose is the silent loss the round-trip refuses. -## The opaque carry (AGENTS.md §3) +## The opaque carry -A node no section spells where it stands — an unknown type, or a known one whose spelling belongs -to the other position — rides as its raw JSON and restores to a deep-equal node. A carry may hold -a node the emitter spells natively: it restores unreinterpreted, and the next emit spells it -canonically (AGENTS.md §2). Block and inline positions canonicalize differently, each fitting -where it sits: +A node no section spells where it stands (`docs/decisions.md` §Unknown nodes ride the carry) — an +unknown type, or a known one whose spelling belongs to the other position — rides as its raw JSON +and restores to a deep-equal node. A carry may hold a node the emitter spells natively: it restores +unreinterpreted, and the next emit spells it canonically (`docs/decisions.md` §The round-trip is the +product). Block and inline positions canonicalize differently, each fitting where it sits: - **Block position**: a fenced code block with info string `carry`, body = the node's JSON — two-space indent, object keys sorted. @@ -177,9 +177,10 @@ In block-directive position `!adf:carry` is a named error — the carry's block ## Raw HTML in input CommonMark input may contain raw HTML. `markdownToAdf` routes each construct through the foreign -HTML element mapping (AGENTS.md §3; specified with the HTML dialect, todo.md milestone 6) — ADF -has no raw-HTML node, so a construct without a mapping, comments and processing instructions -included, is an error result naming it. The flavour never emits raw HTML. +HTML element mapping (`docs/decisions.md` §Foreign HTML is refused by name; specified with the HTML +dialect, todo.md milestone 6) — ADF has no raw-HTML node, so a construct without a mapping, comments +and processing instructions included, is an error result naming it. The flavour never emits raw +HTML. ## Block nodes @@ -192,10 +193,11 @@ have written as CommonMark is a named error. Each section lists attributes as `name (type)`. A parenthesized value set documents what real payloads hold; the type stays string and any value round-trips verbatim. Values map to attrs by type: strings verbatim, numbers and booleans in canonical JSON spelling — quoted where not bare -(`width="33.33"`) — and `json` values as the inline carry's serialization (compact, keys -sorted), quoted. `markdownToAdf` emits `attrs`, `content` and `marks` keys only when non-empty; -editor-normal ADF reads an empty attrs object, marks array or content array as the absent key -(AGENTS.md §2) — the grammar's empty-`{attrs}` omission already collapses the two spellings. +(`width="33.33"`) — and `json` values as the inline carry's serialization (compact, keys sorted), +quoted. `markdownToAdf` emits `attrs`, `content` and `marks` keys only when non-empty; editor-normal +ADF reads an empty attrs object, marks array or content array as the absent key (`docs/decisions.md` +§Equality is editor-normal) — the grammar's empty-`{attrs}` omission already collapses the two +spellings. Marks on a block node ride the reserved attribute key `marks` — the node's marks array as a `json` value: `!adf:layoutSection {marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"}`. @@ -300,10 +302,10 @@ other text, or one carrying a title, is a named error: `mediaInline` carries a m ### Tables One header row plus plain inline cells is a pipe table; anything richer is the directive form -(AGENTS.md §4). Precisely: a table emits as a pipe table exactly when the `table`, every row -and every cell carry no attrs and no marks, the first row is all `tableHeader` and the rest all -`tableCell`, every row has the header's cell count, and every cell holds exactly one attr-less, -mark-less paragraph — an empty cell holds one empty paragraph — with no `|` anywhere the +(`docs/decisions.md` §Tables). Precisely: a table emits as a pipe table exactly when the `table`, +every row and every cell carry no attrs and no marks, the first row is all `tableHeader` and the +rest all `tableCell`, every row has the header's cell count, and every cell holds exactly one +attr-less, mark-less paragraph — an empty cell holds one empty paragraph — with no `|` anywhere the inline layer spells as syntax: a code span, an autolink, a link destination or title. A `|` there takes the directive form instead. A pipe table parses back to exactly that shape. @@ -449,14 +451,14 @@ Shipped !adf:emoji[🎉]{shortName=":tada:"} on !adf:date{timestamp=175608000000 ``` **Whitespace CommonMark cannot hold.** A newline inside a text node, and a space or tab where -CommonMark strips or refuses one — a block's inline content edges, either side of a line break, -an em, strong or strike spelling's inner edges, a pipe cell's edges — is spelled -`!adf:text{text="…"}`, the reserved key carrying the node's text, escaped by the attribute grammar -and never literal: pipe cells trim and pad. The emitter wraps the whitespace run alone and leaves -the rest plain text; `markdownToAdf` merges adjacent text nodes carrying identical marks and no -attributes (AGENTS.md §2). Input reads that spelling alone: the value is one run of spaces and -tabs, or one run of newlines, and anything else — a mixed run, or text CommonMark carries plainly — -is a named error. +CommonMark strips or refuses one — a block's inline content edges, either side of a line break, an +em, strong or strike spelling's inner edges, a pipe cell's edges — is spelled `!adf:text{text="…"}`, +the reserved key carrying the node's text, escaped by the attribute grammar and never literal: pipe +cells trim and pad. The emitter wraps the whitespace run alone and leaves the rest plain text; +`markdownToAdf` merges adjacent text nodes carrying identical marks and no attributes +(`docs/decisions.md` §Equality is editor-normal). Input reads that spelling alone: the value is one +run of spaces and tabs, or one run of newlines, and anything else — a mixed run, or text CommonMark +carries plainly — is a named error. ``` !adf:text{text=" "}Two leading spaces held, and one text node split!adf:text{text="\n"}over two lines. @@ -488,11 +490,11 @@ the directive form, open to no literal reading, is a named error. A spelling adds its mark to every inline node it wraps, and nesting is the marks array in order, outermost first: `_!adf:underline[x]_` gives marks `[em, underline]`, `!adf:underline[_x_]` the -reverse. -`adfToMarkdown` nests in the order the array holds rather than sorting it — §2's equality -restores the array, not a set — and opens each spelling once over the longest run of adjacent -inline nodes carrying an identical mark, attributes included, at that depth. A run breaks at every -node the emitter carries, so no emitted carry sits inside a mark spelling. +reverse. `adfToMarkdown` nests in the order the array holds rather than sorting it — +`docs/decisions.md` §Equality is editor-normal restores the array, not a set — and opens each +spelling once over the longest run of adjacent inline nodes carrying an identical mark, attributes +included, at that depth. A run breaks at every node the emitter carries, so no emitted carry sits +inside a mark spelling. An inline node whose marks no nesting spells — a mark type not listed here, an attrs key its spelling does not list, a value that is not the spelling's type, an attribute the spelling needs @@ -502,7 +504,7 @@ close where the run sits (`un**-real**istic`), or one CommonMark's matching pair intra-word `*` runs together with a neighbouring `**`, and the multiple-of-3 rule can leave the merged run's pairing to another delimiter — rides the inline carry whole. An opaque carry inside a mark spelling is a named error in input: the carry restores its node exactly, marks included -(AGENTS.md §3). +(`docs/decisions.md` §Unknown nodes ride the carry). ``` !adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed]. diff --git a/todo.md b/todo.md index 34ffddc..305389f 100644 --- a/todo.md +++ b/todo.md @@ -7,11 +7,19 @@ 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. + - **36b — Move §8, §9 and §14's decisions.** The code list's rules, release automation and the + non-goals. + - **36c — Move §10 and §11's decisions.** The engine legs, floors, size ratchet, bounds, spellings + and layout; the style rules stay working rules. + - **36d — Move the settled text in `todo.md`'s items and §15's dated rules, and point §15's + "the rule that closes it, landing here" at `docs/decisions.md`.** `AGENTS.md`'s sections are + renumbered once only working rules remain, their citations with them. + - **36e — Move `todo-history.md`'s decisions, re-point its citations and delete it.** - **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. + `docs/decisions.md` §Plain markdown is a flavour of the grammar, `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 @@ -49,13 +57,14 @@ the document ever saw, so nothing is lost; unwrapping them would put `alert(1)` on the page as prose. A `style` attribute is a separate question — `textColor` and `backgroundColor` are the marks it could reach — and is not read at `0.2.0`, the work outweighing what it buys. - So the set sorts every element three ways, and that is what AGENTS.md §3 gains in place of "error - result naming the element": a container around document content unwraps, content ADF cannot hold - is an error result naming it, and what is not document content at all drops whole. A comment sorts - into the second rather than the third because a person wrote those words on purpose. 10's - "Rejected in the survey" line names raw HTML and comments and does not contradict this: it rejects - them as spellings the lossy pair writes and reads back, where `plainMarkdownToAdf` reads through - `markdownToAdf`'s parser and so inherits whatever this set accepts. + So the set sorts every element three ways, and that is what `docs/decisions.md` §Foreign HTML is + refused by name becomes in place of "error result naming the element": a container around document + content unwraps, content ADF cannot hold is an error result naming it, and what is not document + content at all drops whole. A comment sorts into the second rather than the third because a person + wrote those words on purpose. 10's "Rejected in the survey" line names raw HTML and comments and + does not contradict this: it rejects them as spellings the lossy pair writes and reads back, where + `plainMarkdownToAdf` reads through `markdownToAdf`'s parser and so inherits whatever this set + accepts. - **7 — Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed through ADF.** CommonMark spec suite runs against `markdownToHtml` from here (§10). The README's tagline and `package.json`'s `description` regain HTML (5g).