36a - §1–§6's decisions move to docs/decisions.md, indexed from AGENTS.md
This commit is contained in:
@@ -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 `<url>` 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 `<a>` 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 — `oxlint`, since TypeScript 7 is a native compiler publishing no
|
||||
in-process parser, only the `unstable/` AST surface an out-of-process handshake reaches: 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,8 @@ 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 Goals 1 and 2 — 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:
|
||||
|
||||
Reference in New Issue
Block a user