20 - the give-way emitters take the try prefix, and tryRule the narrower type
CI / gate (push) Successful in 30s
CI / publish (push) Has been skipped

This commit is contained in:
2026-09-21 10:23:42 +02:00
parent 3d0926dcaf
commit 6391928fb4
4 changed files with 44 additions and 26 deletions
+4 -4
View File
@@ -308,10 +308,10 @@ someone spells it or pins it.
the whole point of. the whole point of.
- A readable spelling tried ahead of a general one — a CommonMark block, the image, the pipe - 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 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 where the general form fails on the same node; it takes the `try` prefix, so both arms read at
form spells, so a refusal the general form does not share belongs in the general form or the call site (20). Refusing there refuses a document the general form spells, so a refusal the
nowhere — save the nested list a tight spelling would swallow, whose refusal the general form does not share belongs in the general form or nowhere — save the nested list a
tight-versus-blank answer owns (`todo.md` 2b). A readable spelling that must spell its subtree 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 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 spelled — hands that one walk to the general form instead: giving way after the walk walks
again at every level, doubling per level (4b). again at every level, doubling per level (4b).
+14 -14
View File
@@ -26,7 +26,7 @@ type Walk = { blocks: readonly PlacedBlock[]; headroom: number }
type WalkedItem = { node: AdfNode; walk: Walk } type WalkedItem = { node: AdfNode; walk: Walk }
const largestListMarker = 999999999 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, '') const listItemOpener = spellDirectiveOpener('listItem', undefined, '')
export function adfToMarkdown(document: AdfDocument): Result<string> { export function adfToMarkdown(document: AdfDocument): Result<string> {
@@ -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<EmittedBlock> | undefined { function spellReadableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined {
if (node.type === 'blockquote') return emitBlockquote(node, path, depth, memo) if (node.type === 'blockquote') return tryBlockquote(node, path, depth, memo)
if (node.type === 'bulletList' || node.type === 'orderedList') return emitList(node, path, depth, memo) if (node.type === 'bulletList' || node.type === 'orderedList') return tryList(node, path, depth, memo)
if (node.type === 'codeBlock') return emitCodeBlock(node, path) if (node.type === 'codeBlock') return tryCodeBlock(node, path)
if (node.type === 'heading') return emitHeading(node, path) if (node.type === 'heading') return tryHeading(node, path)
if (node.type === 'mediaSingle') return readableText(tryImage(node, path)) if (node.type === 'mediaSingle') return readableText(tryImage(node, path))
if (node.type === 'paragraph') return emitParagraph(node, path) if (node.type === 'paragraph') return tryParagraph(node, path)
if (node.type === 'rule') return emitRule(node) if (node.type === 'rule') return readableText(tryRule(node))
if (node.type === 'table') return readableText(tryPipeTable(node, path)) if (node.type === 'table') return readableText(tryPipeTable(node, path))
return undefined 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)) 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<EmittedBlock> | undefined { function tryBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined {
if (!carriesOnly(node, [])) return undefined if (!carriesOnly(node, [])) return undefined
const inner = walkBlocks(nodeContent(node), path, depth + 1, memo) const inner = walkBlocks(nodeContent(node), path, depth + 1, memo)
if (!inner.ok) return inner 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)) return success(commonMarkText(text, inner.value.headroom))
} }
function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined { function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
if (!carriesOnly(node, ['language'])) return undefined if (!carriesOnly(node, ['language'])) return undefined
const slot = languageSlot(nodeAttrs(node)['language']) const slot = languageSlot(nodeAttrs(node)['language'])
if (slot.kind === 'attribute') return undefined if (slot.kind === 'attribute') return undefined
@@ -214,7 +214,7 @@ function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
return success(text) return success(text)
} }
function emitHeading(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined { function tryHeading(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
if (!carriesOnly(node, ['level'])) return undefined if (!carriesOnly(node, ['level'])) return undefined
const level = nodeAttrs(node)['level'] const level = nodeAttrs(node)['level']
if (typeof level !== 'number' || !Number.isInteger(level) || level < 1 || level > 6) return undefined if (typeof level !== 'number' || !Number.isInteger(level) || level < 1 || level > 6) return undefined
@@ -226,7 +226,7 @@ function emitHeading(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock
return success(commonMarkText(`${hashes} ${line.value}`)) return success(commonMarkText(`${hashes} ${line.value}`))
} }
function emitList(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined { function tryList(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined {
const ordered = node.type === 'orderedList' const ordered = node.type === 'orderedList'
if (!carriesOnly(node, ordered ? ['order'] : [])) return undefined if (!carriesOnly(node, ordered ? ['order'] : [])) return undefined
const items = nodeContent(node) const items = nodeContent(node)
@@ -276,7 +276,7 @@ function listItemLines(blocks: readonly PlacedBlock[], marker: string): string |
return lines.join('\n') return lines.join('\n')
} }
function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined { function tryParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
const content = nodeContent(node) const content = nodeContent(node)
if (content.length === 0 || !carriesOnly(node, [])) return undefined if (content.length === 0 || !carriesOnly(node, [])) return undefined
const line = emitInlineLine(content, 'paragraph', path) const line = emitInlineLine(content, 'paragraph', path)
@@ -284,7 +284,7 @@ function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlo
return success(commonMarkText(line.value)) return success(commonMarkText(line.value))
} }
function emitRule(node: AdfNode): Result<EmittedBlock> | undefined { function tryRule(node: AdfNode): string | undefined {
if (!carriesOnly(node, []) || nodeContent(node).length > 0) return undefined if (!carriesOnly(node, []) || nodeContent(node).length > 0) return undefined
return success(commonMarkText('---')) return '---'
} }
+13
View File
@@ -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`) 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 and the external-image read (`markdown/emit/image.ts`), both pure ADF attribute reads, move
when `html/` reads them (7). 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<EmittedBlock> | 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 - [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 `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 termination rests on a comment: each pass carries at least one more node, or flips
+13 -8
View File
@@ -20,7 +20,7 @@ Start a session with: `Read AGENTS.md and todo.md, then do what todo.md's "Next
## Milestones ## Milestones
Shipping order: 3h, 3i, 3j, 5a, 5b, 5c, 5d, 5 → `0.1.0` (shipped 2026-09-05); 3k, 11, 4, 12, 13, 4b, 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`; `0.2.0`;
8, 9 → TBD; 5e last. 8, 9 → TBD; 5e last.
The numbering is the order the work was planned in, not the order it ships. Everything known and 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. 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 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. 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`, - [ ] **31 — The branch figure the floor is read against is stable (`0.2.0`).** Three Node test
`emitHeading`, `emitList`, `emitParagraph` and `emitRule` return legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and 96.23%,
`Result<EmittedBlock> | undefined`, where `undefined` gives way to the directive form and an and the total at 98.80%, 98.84% and 98.84% (2026-09-21). `--experimental-test-coverage`
error refuses the document. Give the six the `try` prefix the repo already uses for counts branches off V8's own coverage, which the runner's parallel files and V8's
`tryImage`, `tryPipeTable` and `tryPipeCell`, or a return type that cannot hold both, so a optimization make run-dependent, so the number the floor is read against is not the code's
newcomer meeting an unspellable shape cannot reach for `failure` and silently narrow what alone. The floor of 98 holds today on 0.8 points of slack and §10 says it only ever moves
converts. Named as the first thing a new senior would break, and it lands on §1. 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 - [ ] **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` `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 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] **17 — A machine-enforced size ratchet (`0.2.0`).**
- [x] **18 — The subtree the directive spelling asks about (`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] **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] **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] **29 — The README reads raw HTML as refused for good (`0.2.0`).**
- [x] **30 — AGENTS.md says each thing once (`0.2.0`).** - [x] **30 — AGENTS.md says each thing once (`0.2.0`).**