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