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.
- 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).
+14 -14
View File
@@ -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<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 {
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<EmittedBlock> | undefined {
function tryBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | 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<EmittedBlock> | undefined {
function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | 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<string> {
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
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<EmittedBlock
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'
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<EmittedBlock> | undefined {
function tryParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | 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<EmittedBlo
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
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`)
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<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
`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
+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
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<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.
- [ ] **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`).**