From d5dae87c9bfc0ae149aa16abd1c3419e5c5da0c4 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 28 Sep 2026 10:55:07 +0200 Subject: [PATCH 1/5] =?UTF-8?q?36c=20-=20=C2=A710=20and=20=C2=A711's=20dec?= =?UTF-8?q?isions=20move=20to=20docs/decisions.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 167 +++----------------- docs/decisions.md | 228 ++++++++++++++++++++++++++- src/markdown/emit/adf-to-markdown.ts | 2 +- todo.md | 8 +- 4 files changed, 256 insertions(+), 149 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ae7e0dd..0a6ef28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,6 +35,25 @@ In `docs/decisions.md`: - Publish on a version bump - Docs describe the release being built - No schema validation +- The gate runs on Deno and Bun +- The gate installs the tarball +- 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 +- Nothing recurses unbounded +- Nothing spreads an unbounded array +- A retry loop checks its own termination +- Readers scan by index +- The spelling memo +- Only the hard break holds a raw newline +- Emphasis follows CommonMark's matching +- Readable spellings take the `try` prefix +- The attribute vocabulary is ADF's +- The source parts by ADF and format ## 7. Nothing about any consumer @@ -54,35 +73,8 @@ against the README's personas. Test for the behaviour wanted first, then implement until green. `node --test`, beside the code. Node, tsc and npm never run on the host — only via the pinned images (§9). Tests are independent, -containers are torn down after a run. - -The gate runs that same 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 then 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 would resolve against. `consumer.ts` typechecks the emitted `.d.ts` from outside -`tsconfig.build.json` — declaration emit leaves the `.ts` specifiers -`rewriteRelativeImportExtensions` rewrites in the JavaScript, and this is what says a consumer's -resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` stays unproven. -`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 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. +containers are torn down after a run. A test reaches only for what Node's, Deno's and Bun's `node:` +shims all carry. 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 @@ -92,49 +84,15 @@ calls. A leg whose output is both streamed and grepped keeps the copy in a `mkte file: `tee /dev/stderr` reopens fd 2, and under `./ci.sh > log 2>&1` the two offsets punch NUL holes through each other's lines (4d). -The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and -functions, and a branch floor that only ever moves upward. It sits below 100 because the guards -`noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index -compared against `undefined` — have a half no valid document reaches. +`PROPERTY_RUNS=` raises the property runs and randomizes the seed for local digging. The +generators and run parameters properties share live in `src/conformance/property-harness.ts`, +outside the build and coverage. -The size ratchet is the other such number, `.oxlintrc.json`'s single rule over the files -`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`. - -Beside the corpus, properties run over documents generated from the node tables and over generated -markdown, on a fixed seed in the gate; `PROPERTY_RUNS=` raises the runs and randomizes the -seed for local digging, and a counterexample found becomes a round-trip fixture. The generators and -run parameters properties share live in `src/conformance/property-harness.ts`, outside the build and coverage. - -`spec/flavour.md` is read as a source too, 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/`. Keep prose in those sections out -of a bullet; 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. - -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. +Keep prose in `spec/flavour.md`'s `## Block nodes`, `## Inline nodes` and `## Marks` out of a `- ` +bullet. ## 11. Code rules -### Style - - Two-space indent, English everywhere. Alphabetical order wherever order carries no meaning, keyed on the name a line introduces rather than where it came from: an import sorts on its first binding, type imports ahead of value imports, so moving or renaming a @@ -147,79 +105,6 @@ re-pin adding either goes red until someone spells it or pins it. path to name returns `Read`, 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. - -### Bounds - -- Nothing recurses unbounded: the guards walk iteratively, and blocks, marks and JSON values — an - attribute's and a carried node's alike — are all held to 500 levels (`largestNesting`), so a - deep document is a `Result` rather than the stack overflow that waits near 2000. An attribute - is counted from its value; a spelling that nests it deeper — the block directive's `marks`, the - carry — refuses in its own format, as its parser does. A list giving way to the directive form - refuses at zero headroom rather than walking again; counting every list twice halved the list - limit, counting the directive form once doubled the parser's frames per level (the maintainer, - 2026-09-18). -- 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). -- 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). -- A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the - line before it reads, `indexOf` — never a fresh slice per character, and a per-character walk - hoists the scan that does not vary with the character. The pipeline persona feeds documents - nobody typed, and 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). -- 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. - -### Spellings - -- Only the hard break's inline segment holds a raw newline — every other spelling escapes one or - refuses it — which is how the whitespace carry finds a line edge. -- Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text - escapes wherever CommonMark could open or close with it, leaving the emitter's own delimiters the - only ones in play, and a pair that matching hands to another delimiter rides the carry instead. -- 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). -- The attribute vocabulary is ADF's: `adf/` walks it and narrows each value to its kind, and a - format spells the narrowed value. A spelling that re-checks the type is the check's second copy. - Reading a spelling back is the format's own: the reader sits beside the spelling it inverts, so - decode-respell-compare cannot drift, and each format writes its own — canonical JSON for a - number is the markdown flavour's choice, not ADF's. - -### Layout - -- `src/adf/` holds ADF's own knowledge, imports no format, and is where a construct both formats - read lives: the question is answered in ADF's vocabulary — a node type, an attribute kind, a - content model — and no delimiter, element name or escape reaches it. A helper that cannot answer - that way is two constructs, the ADF question there and the spelling in each format, the seam - `markAttributes` and `markSpellings` already draw; one that cannot be split is a gap to ask (§15). - `markdown/` and `html/` are peers: neither imports the other, and no third directory sits between - them. A primitive knowing neither ADF nor a format stays at `src/` root. A construct rises to - `adf/` on its second consumer, not in anticipation of one (the maintainer, 2026-09-21). -- Each format directory (`markdown/`, `html/`) parts into `emit/` (ADF→format) and `parse/` - (format→ADF), the rest of it holding what both directions read. A construct's reader lives there - beside the regex the emitter escapes against, so the two cannot drift; a reader with no emit - counterpart goes in `parse/`, unless it is part of a construct that side already holds — a grammar - stays in one file rather than splitting across the seam. A rule both directions must answer - alike — whether a list marker interrupts a paragraph — is one function there too, never a copy - per direction, however conservative the copy would be. Where the rule is the emitter's own - choice, input consults it rather than restating it, and that is the only import `parse/` takes - from `emit/` — `commonMarkSpelling` and `openingLinkTakesDirective` — so no fixture the emitter - writes can be refused, and a spelling the emitter refuses gives its own error rather than a - second name for it. - 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 diff --git a/docs/decisions.md b/docs/decisions.md index b5c3f33..dd9bebb 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -257,8 +257,8 @@ input reads `message`. inline alike, `\|` for every pipe row. - `not-an-adf-document` carries the document's own path throughout: seven of the guard's eight branches read the document's own shape, and threading a path to the eighth — a malformed node - anywhere in the tree — wants the manual stack the no-recursion rule (`AGENTS.md` §11) forces, - whose empty half no input reaches. The message names the violation instead. + anywhere in the tree — wants the manual stack §Nothing recurses unbounded forces, whose empty + half no input reaches. The message names the violation instead. ## Publish on a version bump @@ -291,3 +291,227 @@ is saved to validates it. 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. + +## 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. + +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 installs the tarball + +2026-09-03, the maintainer. Goal 7. Valid while consumers install the packed package. + +The gate packs the build and installs the tarball under `package-tests/`, so `files`, `exports` +and `types` are proved on the artifact that ships rather than on the source tree a self-reference +would resolve against. `consumer.ts` typechecks the emitted `.d.ts` from outside +`tsconfig.build.json` — declaration emit leaves the `.ts` specifiers +`rewriteRelativeImportExtensions` rewrites in the JavaScript, and this is what says a consumer's +resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` stays unproven. +`node-floor.js` round-trips the installed package under a Node pinned to `engines.node`'s floor. + +## Firefox reads the build + +2026-09-04, the maintainer. Goal 7. Valid while 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 — +which is the browser half of §Any ES2022 engine and the only SpiderMonkey there is — the gate's +other 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. + +## The coverage floors + +2026-08-24, the maintainer. Goal 1. Valid while `noUncheckedIndexedAccess` and ADF's optional keys +force guards with a half no valid document reaches. + +The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and +functions, and a branch floor that only ever moves upward. It sits below 100 because the guards +`noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index +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. + +`.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 +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). `oxlint` measures it since TypeScript 7 is a native compiler publishing no in-process +parser, only the `unstable/` AST surface an out-of-process handshake reaches. 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 + +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. + +Beside the corpus, properties run over documents generated from the node tables and over generated +markdown, on a fixed seed in the gate; a counterexample found becomes a round-trip fixture. + +## 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. + +`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. + +## The node tables answer to Atlassian's schema + +2026-09-13, the maintainer. Goal 1. Valid while a site's editor writes what Atlassian's schema +holds. + +For every node and mark the tables spell, the attribute names and kinds equal what `full.json` and +`stage-0.json` (§Standards ship as data) 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. + +## Nothing recurses unbounded + +2026-08-25, the directive form's count 2026-09-18, the maintainer. Goal 1. Valid while an engine's +stack overflows near 2000 frames. + +The guards walk iteratively, and blocks, marks and JSON values — an attribute's and a carried +node's alike — are all held to 500 levels (`largestNesting`), so a deep document is a `Result` +rather than the stack overflow that waits near 2000. An attribute is counted from its value; a +spelling that nests it deeper — the block directive's `marks`, the carry — refuses in its own +format, as its parser does. A list giving way to the directive form refuses at zero headroom rather +than walking again; counting every list twice halved the list limit, counting the directive form +once doubled the parser's frames per level. + +## Nothing spreads an unbounded array + +2026-09-18, the maintainer. Goal 1. Valid while engines cap a call's arguments. + +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). + +## A retry loop checks its own termination + +2026-09-20, the maintainer. Goal 1. Valid while a line retries until a fallback spells it. + +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). + +## Readers scan by index + +2026-08-30, the kept scan 2026-09-18, the maintainer. Goal 9. Valid while the pipeline persona +feeds documents nobody typed. + +A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the line +before it reads, `indexOf` — never a fresh slice per character, and a per-character walk hoists the +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). + +## The spelling memo + +2026-09-19, the maintainer. Goal 9. Valid while the `commonMarkSpelling` ask spells a node once +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. + +## Only the hard break holds a raw newline + +2026-08-26, the maintainer. Goal 1. Valid while the whitespace carry finds a line edge by its raw +newline. + +Only the hard break's inline segment holds a raw newline — every other spelling escapes one or +refuses it — which is how the whitespace carry finds a line edge. + +## Emphasis follows CommonMark's matching + +2026-08-27, the maintainer. Goals 1 and 8. Valid while CommonMark's emphasis rules are the +reader's. + +Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text +escapes wherever CommonMark could open or close with it, leaving the emitter's own delimiters the +only ones in play, and a pair that matching hands to another delimiter rides the carry instead. + +## 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. + +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). + +## The attribute vocabulary is ADF's + +2026-08-27, the maintainer. Goal 2. Valid while every format spells the same ADF attributes. + +`adf/` walks the attribute vocabulary and narrows each value to its kind, and a format spells the +narrowed value. A spelling that re-checks the type is the check's second copy. Reading a spelling +back is the format's own: the reader sits beside the spelling it inverts, so +decode-respell-compare cannot drift, and each format writes its own — canonical JSON for a number +is the markdown flavour's choice, not ADF's. + +## The source parts by ADF and format + +2026-08-27, `adf/`'s bar 2026-09-21, the maintainer. Goal 2. Valid while each format has a reader +and a writer through ADF. + +`src/adf/` holds ADF's own knowledge, imports no format, and is where a construct both formats read +lives: the question is answered in ADF's vocabulary — a node type, an attribute kind, a content +model — and no delimiter, element name or escape reaches it. A helper that cannot answer that way +is two constructs, the ADF question there and the spelling in each format, the seam +`markAttributes` and `markSpellings` already draw; one that cannot be split is a gap to ask. +`markdown/` and `html/` are peers: neither imports the other, and no third directory sits between +them. A primitive knowing neither ADF nor a format stays at `src/` root. A construct rises to +`adf/` on its second consumer, not in anticipation of one. + +Each format directory parts into `emit/` (ADF→format) and `parse/` (format→ADF), the rest of it +holding what both directions read. A construct's reader lives there beside the regex the emitter +escapes against, so the two cannot drift; a reader with no emit counterpart goes in `parse/`, +unless it is part of a construct that side already holds — a grammar stays in one file rather than +splitting across the seam. A rule both directions must answer alike — whether a list marker +interrupts a paragraph — is one function there too, never a copy per direction, however +conservative the copy would be. Where the rule is the emitter's own choice, input consults it +rather than restating it, and that is the only import `parse/` takes from `emit/` — +`commonMarkSpelling` and `openingLinkTakesDirective` — so no fixture the emitter writes can be +refused, and a spelling the emitter refuses gives its own error rather than a second name for it. diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index fda1bb9..b214f3c 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -104,7 +104,7 @@ function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, mem const kept = memo?.get(node) if (kept !== undefined) { if (kept.block === undefined) return undefined - // A read below the fill would skip the depth guards the walk it replaces runs (AGENTS.md §11). + // A read below the fill would skip the depth guards the walk it replaces runs (docs/decisions.md §The spelling memo). if (depth <= kept.depth) return success({ ...kept.block, headroom: kept.block.headroom + kept.depth - depth }) } const spelled = spellReadableBlock(node, path, depth, memo) diff --git a/todo.md b/todo.md index 778f9c8..a2fd571 100644 --- a/todo.md +++ b/todo.md @@ -7,8 +7,6 @@ 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. - - **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. @@ -64,19 +62,19 @@ `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 + through ADF.** CommonMark spec suite runs against `markdownToHtml` from here (`docs/decisions.md` §The corpus). 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 §10 says it only ever moves upward, so the first raise to the measured + 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 (§11 Bounds), and the plain reduction's + before riding the carry, quadratic in the runs (Goal 9), and the plain reduction's `spellableLine` drops one mark per re-emit the same way. Make both linear. - **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 -- 2.52.0 From 621dc75f8698e5ad10affe892efba0c65a838079 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 28 Sep 2026 11:00:39 +0200 Subject: [PATCH 2/5] 36c - Goal 10: source a contributor can hold --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index d69df67..f5d3ab3 100644 --- a/README.md +++ b/README.md @@ -55,6 +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. ## Audience -- 2.52.0 From 8a898436ea7ffe57d36d99b10b971981757efc66 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 28 Sep 2026 11:00:42 +0200 Subject: [PATCH 3/5] 36c - Goal 10's continuation indent --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5d3ab3..6300d54 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ In priority order. 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. + source drifting longer. ## Audience -- 2.52.0 From fa6df339870bdaa70b5f0a2c287a1cab0f7e74b1 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 28 Sep 2026 11:08:17 +0200 Subject: [PATCH 4/5] 36c - review: premises that expire, no todo-history citations, the memo's key at the code, Goal 10 checkable --- AGENTS.md | 17 +++--- README.md | 4 +- docs/decisions.md | 80 ++++++++++++---------------- src/markdown/emit/adf-to-markdown.ts | 1 + todo.md | 15 +++--- 5 files changed, 54 insertions(+), 63 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0a6ef28..895786a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`, 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 diff --git a/README.md b/README.md index 6300d54..3ceda72 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/decisions.md b/docs/decisions.md index dd9bebb..9632390 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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 diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index b214f3c..9808159 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -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 & { 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 type Walk = { blocks: readonly PlacedBlock[]; headroom: number } type WalkedItem = { node: AdfNode; walk: Walk } diff --git a/todo.md b/todo.md index a2fd571..2426e22 100644 --- a/todo.md +++ b/todo.md @@ -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 -- 2.52.0 From bbfc8724fc774bf0860d929868b076601a722cfa Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 28 Sep 2026 11:14:27 +0200 Subject: [PATCH 5/5] 36c - review: a Deno premise that holds today, the plain reduction's memo named --- docs/decisions.md | 27 ++++++++++++++------------- src/markdown/emit/adf-to-markdown.ts | 2 +- todo.md | 3 +++ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/decisions.md b/docs/decisions.md index 9632390..9fa33ed 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -294,8 +294,8 @@ 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 Deno is the only leg refusing an -extensionless specifier and Bun the only engine that is not V8. +2026-09-01, the maintainer. Goals 7 and 8. Valid while Bun is the gate's only engine that is +not V8. 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 @@ -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 -(the comprehension panel, 2026-09-20). +better than a function's length. `.oxlintrc.json`'s single rule, over the files `tsconfig.build.json` builds, is a per-function line ceiling, set at that set's worst and moving only downward. It covers the built files alone, since @@ -371,10 +371,11 @@ markdown, on a fixed seed in the gate; a counterexample found becomes a round-tr 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: 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. +`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 outside the +bullet's attribute list, 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 @@ -433,12 +434,12 @@ two cannot disagree — which is what makes the kept value a memo rather than a 2026-09-19, the maintainer. Goal 9. Valid while the `commonMarkSpelling` ask spells a node once 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. `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. +The parse and the plain reduction keep each node's readable spelling in a memo, so the +`commonMarkSpelling` ask stops 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 diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index 9808159..7077bd7 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -20,7 +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 & { node: AdfNode } -// Keyed by reference: the parse builds one object per position; a consumer's document may share one, so adfToMarkdown passes none. +// Keyed by reference: only a caller building one object per position (the parse, the plain reduction) passes one; a consumer's document may share a node. export type SpellingMemo = Map type Walk = { blocks: readonly PlacedBlock[]; headroom: number } type WalkedItem = { node: AdfNode; walk: Walk } diff --git a/todo.md b/todo.md index 2426e22..38c4a36 100644 --- a/todo.md +++ b/todo.md @@ -73,6 +73,9 @@ 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. +- **37 — State what the Deno leg proves that Node's does not, or drop it.** `docs/decisions.md` §The + gate runs on Deno and Bun credits Deno with holding the module graph to fully-spelled specifiers, + which Node already refuses under `"type": "module"`, and `tsc` under `NodeNext`. - **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 -- 2.52.0