36c - review: premises that expire, no todo-history citations, the memo's key at the code, Goal 10 checkable
CI / gate (push) Successful in 43s
CI / publish (push) Has been skipped

This commit is contained in:
2026-09-28 11:08:17 +02:00
parent 8a898436ea
commit fa6df33987
5 changed files with 54 additions and 63 deletions
+9 -8
View File
@@ -40,7 +40,6 @@ In `docs/decisions.md`:
- Firefox reads the build
- The coverage floors
- The size ratchet
- The corpus
- Properties on a fixed seed
- The flavour spec is read as a source
- The node tables answer to Atlassian's schema
@@ -88,8 +87,9 @@ holes through each other's lines (4d).
generators and run parameters properties share live in `src/conformance/property-harness.ts`,
outside the build and coverage.
Keep prose in `spec/flavour.md`'s `## Block nodes`, `## Inline nodes` and `## Marks` out of a `- `
bullet.
Each `- ` bullet in `spec/flavour.md`'s `## Block nodes`, `## Inline nodes` and `## Marks` declares
the nodes named before its first em dash, with the attributes following `Attributes: ` — a
parenthesized value set reading `string`; fenced examples are skipped. Keep prose out of a bullet.
## 11. Code rules
@@ -105,11 +105,12 @@ bullet.
path to name returns `Read<T>`, and the walk attaches the path where it knows it.
- Reuse before adding; the smallest sufficient diff is the benchmark; no speculative generality —
a second consumer, or it goes.
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`); a
file does not repeat its directory in its name — `adf/document.ts`, never
`adf/adf-document.ts`. A name is the noun `spec/flavour.md` or ADF's schema uses for the
thing; a directory follows a split the spec draws; a placement these rules leave open goes
beside its only reader, or in what both read where there are two (the maintainer, 2026-09-18).
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`); a file
does not repeat its directory in its name — `adf/document.ts`, never `adf/adf-document.ts`. A name
is the noun `spec/flavour.md` or ADF's schema uses for the thing; a directory follows a split the
spec draws; a placement neither this section nor `docs/decisions.md` §The source parts by ADF and
format settles goes beside its only reader, or in what both read where there are two (the
maintainer, 2026-09-18).
## 12. Prose to a minimum
+2 -2
View File
@@ -55,8 +55,8 @@ In priority order.
document and returns a whole result.
9. **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.
10. **Source a contributor can hold.** Any one function reads at a sitting, and the gate stops the
source drifting longer.
10. **Source a contributor can hold.** Any one function reads in one sitting, and no change makes
the longest one longer.
## Audience
+34 -46
View File
@@ -16,7 +16,7 @@ a backslash reach them intact; what the flavour cannot spell reduces ADF→ADF a
`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.
tested over a checked-in corpus (`corpus/README.md`), not a claim made in prose.
## Markdown in is a canonical fixpoint
@@ -294,16 +294,15 @@ nesting a spelling inside its own kind (`*(*a*)*`) names that mark once.
## The gate runs on Deno and Bun
2026-09-01, the maintainer. Goals 7 and 8. Valid while the suite, rather than a smoke import, is
what proves an engine.
2026-09-01, the maintainer. Goals 7 and 8. Valid while Deno is the only leg refusing an
extensionless specifier and Bun the only engine that is not V8.
The gate runs the suite under Deno and Bun as well as Node, the three images pinned alike, and
neither extra leg is Node's proof twice. Deno refuses an extensionless or directory specifier, so
it holds the module graph to the fully-spelled form a browser can load; Bun runs JavaScriptCore,
the one engine of the three that is not V8, where the Unicode property escapes emphasis matching
leans on can disagree. Both refuse a run matching no test, so Node's is the only vacuous-green
guard, and a test may reach only for what all three `node:` shims carry — the price of proving
those engines over the corpus rather than over a smoke import.
The gate runs the suite under Deno and Bun as well as Node, and neither extra leg is Node's proof
twice. Deno refuses an extensionless or directory specifier, so it holds the module graph to the
fully-spelled form a browser can load; Bun runs JavaScriptCore, the one engine of the three that is
not V8, where the Unicode property escapes emphasis matching leans on can disagree. Both refuse a
run matching no test, so Node's is the only vacuous-green guard, and `AGENTS.md` §10's `node:` shims
rule is the price of proving those engines over the corpus rather than over a smoke import.
## The gate installs the tarball
@@ -319,7 +318,8 @@ resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` s
## Firefox reads the build
2026-09-04, the maintainer. Goal 7. Valid while no other leg runs SpiderMonkey.
2026-09-04, the maintainer. Goal 7. Valid while the library claims a browser and no other leg runs
SpiderMonkey.
A headless Firefox loads `dist/index.js` over HTTP and converts the round-trip, normalization and
error fixtures and the real payloads — the `commonmark-spec` sort is the Node suite's to check —
@@ -345,7 +345,7 @@ compared against `undefined` — have a half no valid document reaches.
## The size ratchet
2026-09-20, the maintainer. Goal 10. Valid while no measure picks out what readers find hard
better than a function's length.
(the comprehension panel, 2026-09-20).
`.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
@@ -359,13 +359,6 @@ config gone missing fails the leg instead of falling back to oxlint's own defaul
`--deny-warnings`, since a rule from a category this config never names arrives as a warning it
exits 0 on.
## The corpus
2026-08-23, the maintainer. Goals 1 and 8. Valid while the round-trip is proved by example.
All checked in: hand-built fixtures per node and combination; real ADF Atlassian's editor wrote; the
CommonMark spec suite against `markdownToAdf` and `markdownToHtml`.
## Properties on a fixed seed
2026-09-14, the maintainer. Goal 1. Valid while a red gate must reproduce.
@@ -375,16 +368,13 @@ markdown, on a fixed seed in the gate; a counterexample found becomes a round-tr
## The flavour spec is read as a source
2026-09-01, the maintainer. Goals 1 and 6. Valid while `spec/flavour.md` restates the node tables
in prose.
2026-09-01, the maintainer. Goal 1. Valid while `spec/flavour.md` restates the node tables in
prose.
`spec/flavour.md` is read as a source, so the node tables cannot drift from the prose they copy:
each `- ` bullet in `## Block nodes`, `## Inline nodes` and `## Marks` declares the nodes named
before its first em dash, with the attributes following `Attributes: ` — a parenthesized value set
reading `string` — and must equal the tables in `adf/`. Fenced examples are skipped. It guards the
attributes alone: nodes that differ in 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.
`spec/flavour.md` is read as a source, so the node tables cannot drift from the prose they copy: its
node and mark bullets must equal the tables in `adf/`. It guards the attributes alone: nodes that
differ in 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 node tables answer to Atlassian's schema
@@ -417,14 +407,14 @@ once doubled the parser's frames per level.
Nothing spreads an unbounded array into a call — a node's siblings, a code block's held lines, a
mark run's segments: the argument list caps near 125k and throws a `RangeError` where a `Result` is
owed. A walk pushes one at a time. A literal spread (`[...value]`) is not the same thing and is
fine (4c).
fine.
## A retry loop checks its own termination
2026-09-20, the maintainer. Goal 1. Valid while a line retries until a fallback spells it.
2026-09-20, the maintainer. Goal 1. Valid while a fallback can fail to spell what it is handed.
A loop retrying an input until a fallback spells it refuses the pass taking no fallback, so its
termination is the loop's own check (28).
termination is the loop's own check.
## Readers scan by index
@@ -436,7 +426,7 @@ before it reads, `indexOf` — never a fresh slice per character, and a per-char
scan that does not vary with the character: a megabyte through a quadratic walk is a minute rather
than a millisecond. A scan may keep what it read for a later walk of the same text, and the
fallback where it kept nothing must be the same reader over the same text at the same index, so the
two cannot disagree — which is what makes the kept value a memo rather than a second spelling (4c).
two cannot disagree — which is what makes the kept value a memo rather than a second spelling.
## The spelling memo
@@ -444,13 +434,11 @@ two cannot disagree — which is what makes the kept value a memo rather than a
per level above it otherwise.
The parse keeps each node's readable spelling in a memo, so the `commonMarkSpelling` ask stops
spelling a node once per level above it (18). The node reference is the key, which holds because
the parse builds one object per position; `adfToMarkdown` passes no memo, where a consumer's
document may hold one node at two positions (4b). `text` and `spelling` carry no depth and
`headroom` is affine in it, so a read at or above the depth that filled the entry rebases; a read
below re-spells, because a hit skips the depth guards the walk it replaces runs and an ordered list
past the marker cap gives way, spending two emitter levels where the parser spent one. Only what
succeeded is kept, so no path minted at another position is ever read.
spelling a node once per level above it. `text` and `spelling` carry no depth and `headroom` is
affine in it, so a read at or above the depth that filled the entry rebases; a read below re-spells,
because a hit skips the depth guards the walk it replaces runs and an ordered list past the marker
cap gives way, spending two emitter levels where the parser spent one. Only what succeeded is kept,
so no path minted at another position is ever read.
## Only the hard break holds a raw newline
@@ -471,15 +459,15 @@ only ones in play, and a pair that matching hands to another delimiter rides the
## Readable spellings take the `try` prefix
2026-09-21, the maintainer. Goals 1 and 4. Valid while a readable spelling is tried ahead of a
general one.
2026-09-21, the maintainer. Goals 1 and 4. Valid while a readable spelling's refusal would cost a
document the general form spells.
A readable spelling tried ahead of a general one takes the `try` prefix and fails only where the
general form fails on the same node (20): refusing there refuses a document the general form
spells, so a refusal the general form does not share belongs in the general form or nowhere. A
readable spelling that must spell its subtree before it can give way — the list, whose
thematic-break first line and blank lines exist only spelled — hands that one walk to the general
form instead: giving way after the walk walks again at every level, doubling per level (4b).
general form fails on the same node: refusing there refuses a document the general form spells, so a
refusal the general form does not share belongs in the general form or nowhere. A readable spelling
that must spell its subtree before it can give way — the list, whose thematic-break first line and
blank lines exist only spelled — hands that one walk to the general form instead: giving way after
the walk walks again at every level, doubling per level.
## The attribute vocabulary is ADF's
+1
View File
@@ -20,6 +20,7 @@ type BlockSpelling = 'commonmark' | 'directive' | 'list'
type EmittedBlock = { headroom: number; spelling: BlockSpelling; text: string }
type KeptSpelling = { block: EmittedBlock | undefined; depth: number }
type PlacedBlock = Omit<EmittedBlock, 'headroom'> & { node: AdfNode }
// Keyed by reference: the parse builds one object per position; a consumer's document may share one, so adfToMarkdown passes none.
export type SpellingMemo = Map<AdfNode, KeptSpelling>
type Walk = { blocks: readonly PlacedBlock[]; headroom: number }
type WalkedItem = { node: AdfNode; walk: Walk }
+8 -7
View File
@@ -7,9 +7,9 @@
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.
- **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.
- **36d — Move the settled text in `todo.md`'s items and §11 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
`docs/decisions.md` §Plain markdown is a flavour of the grammar, `plainMarkdownToAdf` is
@@ -62,16 +62,17 @@
`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 (`docs/decisions.md` §The corpus). The README's
through ADF.** CommonMark spec suite runs against `markdownToHtml` from here. The README's
tagline and `package.json`'s `description` regain HTML (5g).
- **31 — Make the branch figure the coverage floor is read against repeatable.** Three Node test
legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and 96.23%, and the
total at 98.80%, 98.84% and 98.84% (2026-09-21). `--experimental-test-coverage` counts branches
off V8's own coverage, which the runner's parallel files and V8's optimization make run-dependent,
so the number the floor is read against is not the code's alone. The floor of 98 holds today on
0.8 points of slack and `docs/decisions.md` §The coverage floors says it only ever moves upward, so the first raise to the measured
figure reddens a run that changed nothing. Make the measurement repeatable, or state the number
the floor may be raised to and why it is not the measured one.
0.8 points of slack and `docs/decisions.md` §The coverage floors says it only ever moves upward,
so the first raise to the measured figure reddens a run that changed nothing. Make the
measurement repeatable, or state the number the floor may be raised to and why it is not the
measured one.
- **33 — Make a carried mark run cost the line one re-emit.** `adfToMarkdown` spends 23 s on one
paragraph of 2000 × `un` plus `**-r**`: each run its flanking cannot spell re-emits the whole line
before riding the carry, quadratic in the runs (Goal 9), and the plain reduction's