From 6391928fb402a92a0b67f96774701c5acebfb7b1 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 21 Sep 2026 10:23:42 +0200 Subject: [PATCH] 20 - the give-way emitters take the try prefix, and tryRule the narrower type --- AGENTS.md | 8 ++++---- src/markdown/emit/adf-to-markdown.ts | 28 ++++++++++++++-------------- todo-history.md | 13 +++++++++++++ todo.md | 21 +++++++++++++-------- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 21a55cb..5744d0b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -308,10 +308,10 @@ someone spells it or pins it. the whole point of. - A readable spelling tried ahead of a general one — a CommonMark block, the image, the pipe table, a pipe cell — gives way with `undefined` for every shape it cannot spell, and fails only - where the 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 — save the nested list a tight spelling would swallow, whose refusal the - tight-versus-blank answer owns (`todo.md` 2b). A readable spelling that must spell its subtree + where the general form fails on the same node; it takes the `try` prefix, so both arms read at + the call site (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 — save the nested list a + tight spelling would swallow, whose refusal the tight-versus-blank answer owns (`todo.md` 2b). 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). diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index 6eb0da3..d3e28c0 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -26,7 +26,7 @@ type Walk = { blocks: readonly PlacedBlock[]; headroom: number } type WalkedItem = { node: AdfNode; walk: Walk } const largestListMarker = 999999999 -// Bare because emitList admits no item carrying attributes, marks or text. +// Bare because tryList admits no item carrying attributes, marks or text. const listItemOpener = spellDirectiveOpener('listItem', undefined, '') export function adfToMarkdown(document: AdfDocument): Result { @@ -115,13 +115,13 @@ function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, mem } function spellReadableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result | undefined { - if (node.type === 'blockquote') return emitBlockquote(node, path, depth, memo) - if (node.type === 'bulletList' || node.type === 'orderedList') return emitList(node, path, depth, memo) - if (node.type === 'codeBlock') return emitCodeBlock(node, path) - if (node.type === 'heading') return emitHeading(node, path) + if (node.type === 'blockquote') return tryBlockquote(node, path, depth, memo) + if (node.type === 'bulletList' || node.type === 'orderedList') return tryList(node, path, depth, memo) + if (node.type === 'codeBlock') return tryCodeBlock(node, path) + if (node.type === 'heading') return tryHeading(node, path) if (node.type === 'mediaSingle') return readableText(tryImage(node, path)) - if (node.type === 'paragraph') return emitParagraph(node, path) - if (node.type === 'rule') return emitRule(node) + if (node.type === 'paragraph') return tryParagraph(node, path) + if (node.type === 'rule') return readableText(tryRule(node)) if (node.type === 'table') return readableText(tryPipeTable(node, path)) return undefined } @@ -164,7 +164,7 @@ function emitDirectiveBody(node: AdfNode, directive: BlockDirective, opener: str return success(directivePair(node, opener, joinBlocks(walk.value.blocks, 'directive'), walk.value.headroom)) } -function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result | undefined { +function tryBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result | undefined { if (!carriesOnly(node, [])) return undefined const inner = walkBlocks(nodeContent(node), path, depth + 1, memo) if (!inner.ok) return inner @@ -175,7 +175,7 @@ function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, me return success(commonMarkText(text, inner.value.headroom)) } -function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result | undefined { +function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result | undefined { if (!carriesOnly(node, ['language'])) return undefined const slot = languageSlot(nodeAttrs(node)['language']) if (slot.kind === 'attribute') return undefined @@ -214,7 +214,7 @@ function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result { return success(text) } -function emitHeading(node: AdfNode, path: ConvertErrorPath): Result | undefined { +function tryHeading(node: AdfNode, path: ConvertErrorPath): Result | undefined { if (!carriesOnly(node, ['level'])) return undefined const level = nodeAttrs(node)['level'] if (typeof level !== 'number' || !Number.isInteger(level) || level < 1 || level > 6) return undefined @@ -226,7 +226,7 @@ function emitHeading(node: AdfNode, path: ConvertErrorPath): Result | undefined { +function tryList(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result | undefined { const ordered = node.type === 'orderedList' if (!carriesOnly(node, ordered ? ['order'] : [])) return undefined const items = nodeContent(node) @@ -276,7 +276,7 @@ function listItemLines(blocks: readonly PlacedBlock[], marker: string): string | return lines.join('\n') } -function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result | undefined { +function tryParagraph(node: AdfNode, path: ConvertErrorPath): Result | undefined { const content = nodeContent(node) if (content.length === 0 || !carriesOnly(node, [])) return undefined const line = emitInlineLine(content, 'paragraph', path) @@ -284,7 +284,7 @@ function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result | undefined { +function tryRule(node: AdfNode): string | undefined { if (!carriesOnly(node, []) || nodeContent(node).length > 0) return undefined - return success(commonMarkText('---')) + return '---' } diff --git a/todo-history.md b/todo-history.md index db888f2..ea3d371 100644 --- a/todo-history.md +++ b/todo-history.md @@ -922,6 +922,19 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li moved: a construct rises on its second consumer, so `linkHref` (`markdown/mark-spellings.ts`) and the external-image read (`markdown/emit/image.ts`), both pure ADF attribute reads, move when `html/` reads them (7). +- [x] **20 — The give-way channel is unmistakable (`0.2.0`).** `emitBlockquote`, `emitCodeBlock`, + `emitHeading`, `emitList`, `emitParagraph` and `emitRule` return + `Result | undefined`, where `undefined` gives way to the directive form and an + error refuses the document. Give the six the `try` prefix the repo already uses for + `tryImage`, `tryPipeTable` and `tryPipeCell`, or a return type that cannot hold both, so a + newcomer meeting an unspellable shape cannot reach for `failure` and silently narrow what + converts. Named as the first thing a new senior would break, and it lands on §1. + **Done** (2026-09-21): both answers, each where it fits. Five of the six reach a refusal the + directive form shares — a walk, `codeBlockText`, `emitInlineLine` — so moving it out is the + second walk 4b removed, and they take the prefix alone. `tryRule` refuses nothing, so it + takes the narrower type as well, `string | undefined` through `readableText`, which is what + `tryImage` and `tryPipeTable` already return. §11's readable-spelling rule carries the + prefix, so the next spelling is named by the rule rather than by the six beside it. - [x] **28 — `emitLine`'s retry loop cannot spin (`0.2.0`).** `emit/inline-line.ts:67` is a `for (;;)` that re-emits the line until every unspellable node has been carried, and its termination rests on a comment: each pass carries at least one more node, or flips diff --git a/todo.md b/todo.md index 603e7ea..199145f 100644 --- a/todo.md +++ b/todo.md @@ -20,7 +20,7 @@ Start a session with: `Read AGENTS.md and todo.md, then do what todo.md's "Next ## Milestones Shipping order: 3h, 3i, 3j, 5a, 5b, 5c, 5d, 5 → `0.1.0` (shipped 2026-09-05); 3k, 11, 4, 12, 13, 4b, -4c, 14, 15, 16, 18, 4d, 28, 17, 29, 19, 20, 21, 22, 23, 24, 25, 30, 26, 27, 10, 6, 7, 5f, 5g → +4c, 14, 15, 16, 18, 4d, 28, 17, 29, 19, 20, 21, 22, 23, 24, 25, 30, 26, 27, 10, 6, 7, 31, 5f, 5g → `0.2.0`; 8, 9 → TBD; 5e last. The numbering is the order the work was planned in, not the order it ships. Everything known and @@ -41,14 +41,18 @@ HTML doubles the importers and the file count they touch, and 25 to 27 because t panel says the next reader pays for. 29 and 30 come from 17's prose pass (2026-09-20). 29 reads first because every goal is what a later ask is settled against, 19's included; 30 sits beside 25, the other chunk rereading AGENTS.md. +31 comes from 20's gate runs (2026-09-21) and reads beside 5f, the other chunk putting a measured +number under the pipeline. -- [ ] **20 — The give-way channel is unmistakable (`0.2.0`).** `emitBlockquote`, `emitCodeBlock`, - `emitHeading`, `emitList`, `emitParagraph` and `emitRule` return - `Result | undefined`, where `undefined` gives way to the directive form and an - error refuses the document. Give the six the `try` prefix the repo already uses for - `tryImage`, `tryPipeTable` and `tryPipeCell`, or a return type that cannot hold both, so a - newcomer meeting an unspellable shape cannot reach for `failure` and silently narrow what - converts. Named as the first thing a new senior would break, and it lands on §1. +- [ ] **31 — The branch figure the floor is read against is stable (`0.2.0`).** 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 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. - [ ] **21 — The ADF tables carry ADF's nouns (`0.2.0`).** `adf/block-directives.ts` and `adf/inline-directives.ts` hold the ADF node tables — `paragraph`, `heading`, `blockquote` and `rule` among them — under the markdown flavour's word, inside the directory §11 forbids @@ -280,6 +284,7 @@ ask is settled against, 19's included; 30 sits beside 25, the other chunk reread - [x] **17 — A machine-enforced size ratchet (`0.2.0`).** - [x] **18 — The subtree the directive spelling asks about (`0.2.0`).** - [x] **19 — A home for what both formats read (`0.2.0`).** +- [x] **20 — The give-way channel is unmistakable (`0.2.0`).** - [x] **28 — `emitLine`'s retry loop cannot spin (`0.2.0`).** - [x] **29 — The README reads raw HTML as refused for good (`0.2.0`).** - [x] **30 — AGENTS.md says each thing once (`0.2.0`).**