diff --git a/docs/reading-cost.md b/docs/reading-cost.md index 4acfe88..7558e2d 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -902,3 +902,20 @@ Read most (opens and searches): - `AGENTS.md` 3 - `src/index.ts` 3 - `CHANGELOG.md` 2 + +## Reading cost, chunk 8d75c35b392b, finished 2026-11-09T07:18:15Z + +92011 new input tokens before the first commit. + +Read most (opens and searches): + +- `todo.md` 6 +- `src/json-text.test.ts` 3 +- `ci.sh` 2 +- `docker-runner.sh` 2 +- `package.json` 2 +- `src/adf/document.test.ts` 2 +- `src/markdown/commonmark/entity-references.ts` 2 +- `src/markdown/parse/markdown-to-adf.test.ts` 2 +- `src/result.test.ts` 2 +- `README.md` 1 diff --git a/src/markdown/portable/adf-to-portable-markdown.test.ts b/src/markdown/portable/adf-to-portable-markdown.test.ts index 16d3c3d..0ed37c9 100644 --- a/src/markdown/portable/adf-to-portable-markdown.test.ts +++ b/src/markdown/portable/adf-to-portable-markdown.test.ts @@ -158,6 +158,30 @@ test('spells a task list as a bullet list whose items lead with their state', () assert.equal(portable(node('taskList', {}, task('DONE', 'a'), node('taskList', {}, task('TODO', '')))), '- [x] a\n - [ ]\n') }) +test('leaves a block task item only when it opens with a paragraph', () => { + const task = (state: string, value: string): AdfNode => node('taskItem', { state }, text(value)) + const blockTask = (...content: AdfNode[]): AdfNode => node('blockTaskItem', { state: 'TODO' }, ...content) + const codeBlock: AdfNode = { content: [text('x')], type: 'codeBlock' } + const nested = node('taskList', {}, task('TODO', 'n')) + const lists = [ + node('taskList', {}, blockTask(said('a'), said('b')), blockTask(codeBlock, said('c')), blockTask(bulletList(item(said('d'))), said('e'))), + node('taskList', {}, blockTask(nested, said('f')), blockTask(said('g'), codeBlock, said('h')), blockTask(), blockTask(said('i'))), + node('taskList', {}, blockTask(said('j'), nested, said('k'), nested), blockTask(said('l'), said('m'), nested)), + ] + const reduced = reduceToPortable(document(...lists)) + assert.ok(reduced.ok) + const pending = [...(reduced.value.content ?? [])] + let blockTasks = 0 + for (let at = pending.pop(); at !== undefined; at = pending.pop()) { + for (const child of at.content ?? []) pending.push(child) + if (at.type !== 'blockTaskItem') continue + blockTasks += 1 + assert.equal(at.content?.[0]?.type, 'paragraph') + assert.ok((at.content ?? []).filter((child) => child.type === 'paragraph').length >= 2) + } + assert.equal(blockTasks, 2) +}) + test('spells a decision list as a plain bullet list', () => { assert.equal(portable(node('decisionList', {}, node('decisionItem', { state: 'DECIDED' }, text('Ship')), said('Stray'))), '- Ship\n- Stray\n') }) diff --git a/src/markdown/portable/block-spelling.ts b/src/markdown/portable/block-spelling.ts index d4709b5..665d14b 100644 --- a/src/markdown/portable/block-spelling.ts +++ b/src/markdown/portable/block-spelling.ts @@ -65,19 +65,17 @@ function placedBlock(node: AdfNode, path: ConvertErrorPath, depth: number, writi return block.ok ? success({ blocks: [{ ...block.value, node }], headroom: block.value.headroom }) : block } -// The marker leads the first paragraph, or stands as one where the blocks open with another. +// A block task item reaches here holding two or more paragraphs, so the marker leads the first (`itemParts` in task-list.ts). function taskBlocks(task: AdfNode, path: ConvertErrorPath, depth: number, writing: Writing): Result { const marker = taskMarker(nodeAttrs(task)['state']) - const markerBlock: PlacedBlock = { node: { type: 'paragraph' }, spelling: 'commonmark', text: blockText(marker) } if (task.type === 'taskItem') { const content = nodeContent(task) const line = content.length === 0 ? success('') : emitInlineLine(content, 'paragraph', path, writing.flavour) if (!line.ok) return line - return success({ blocks: [{ ...markerBlock, text: blockText(line.value === '' ? marker : `${marker} ${line.value}`) }], headroom: Number.POSITIVE_INFINITY }) + return success({ blocks: [{ node: { type: 'paragraph' }, spelling: 'commonmark', text: blockText(line.value === '' ? marker : `${marker} ${line.value}`) }], headroom: Number.POSITIVE_INFINITY }) } const walk = walkBlocks(nodeContent(task), path, depth, writing) if (!walk.ok) return walk - const [first, ...rest] = walk.value.blocks - const blocks = first?.node.type === 'paragraph' ? [{ ...first, text: prefixedText(`${marker} `, '', first.text) }, ...rest] : [markerBlock, ...walk.value.blocks] + const blocks = walk.value.blocks.map((block, index) => (index === 0 ? { ...block, text: prefixedText(`${marker} `, '', block.text) } : block)) return success({ blocks, headroom: walk.value.headroom }) } diff --git a/src/result.test.ts b/src/result.test.ts index 6bf3964..afa4e4d 100644 --- a/src/result.test.ts +++ b/src/result.test.ts @@ -1,4 +1,7 @@ import assert from 'node:assert/strict' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { readFileSync } from 'node:fs' import test from 'node:test' import type { AdfDocument, AdfMark } from './adf/document.ts' @@ -44,3 +47,15 @@ for (const [code, cause] of Object.entries(causes)) { assert.equal(!result.ok && result.error.code, code) }) } + +// A code added or removed must reach the README's tables, which are the contract a consumer switches on. +test('the README error tables list exactly the codes ConvertErrorCode holds', () => { + const readme = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'README.md'), 'utf8') + const section = readme.slice(readme.indexOf('\n## The errors\n')) + const listed = new Set() + for (const line of section.slice(1, section.indexOf('\n## ', 1)).split('\n')) { + const code = /^\| `([a-z-]+)` \|/.exec(line)?.[1] + if (code !== undefined) listed.add(code) + } + assert.deepEqual([...listed].sort(), Object.keys(causes).sort()) +}) diff --git a/todo.md b/todo.md index a3dcf9b..5606a2b 100644 --- a/todo.md +++ b/todo.md @@ -13,7 +13,6 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| | 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 | -| 159 | 0.4.0 | | | **Delete `taskBlocks`'s branch that writes the task marker as its own paragraph, once a test shows no block task item reaches it.** | 1 | 1 | 1 | 1 | 1 | code health by 1 | 10.5 | | 144 | 0.4.0 | | | **Give every container `docker-runner.sh` and `publish.sh` start a memory limit and an equal swap limit, sized from the largest a gate leg measurably needs.** | 1 | 1 | 1 | 1 | 2 | code health by 1 | 8.7 | | 143 | 0.4.0 | | defect | **Keep, in `adfToPortableMarkdown`, the link around a note, so a linked stored image writes every target.** | 2 | 1 | 3 | 4 | 3 | 7 by 1 | 6.5 | | 169 | 0.4.0 | | owner | **Investigate `kovetskiy/mark` in depth and record in a report how its formats differ from ours, where the two projects overlap and how they compare.** | 1 | 1 | 1 | 1 | 3 | 3 by 4 | 6.3 | @@ -21,7 +20,6 @@ | 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.0 | | 171 | 0.4.0 | | principle | **Assert the facts an error message must carry, never its whole sentence, in `markdown-to-adf.test.ts`, `document.test.ts` and `html-to-adf.test.ts`.** | 3 | 1 | 1 | 3 | 6 | code health by 3 | 4.4 | | 173 | 0.4.0 | | principle | **Move each CommonMark spelling's description out of `commonMarkSpellings`, beside the emitter's spelling it describes.** | 2 | 1 | 1 | 2 | 4 | code health by 1 | 4.4 | -| 81 | 0.4.0 | | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 1 | 1 | 1 | 2 | 3 by 2 | 4.2 | | 147 | 0.4.0 | | | **Ship `adfToPlainText`, writing a document's content with no markup, under a README goal for plain text.** | 2 | 2 | 1 | 3 | 4 | 3 by 4 | 4.2 | | 141 | 0.4.0 | | decision | **Write `strike` as HTML in `adfToHtml` and read it back in `htmlToAdf`, in place of the carry element.** | 1 | 1 | 1 | 1 | 2 | 6 by 2 | 4.0 | | 135 | 0.4.0 | | | **Write media, media groups and captions as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 4 | 6 by 2 | 2.0 | @@ -67,13 +65,6 @@ a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded). `src/markdown/**` bans only `html/`; banning `emit/`, `parse/` and `portable/` for the root's own files, with its `ci.sh` probe, extends the decision. -### 159. Delete `taskBlocks`'s branch that writes the task marker as its own paragraph, once a test shows no block task item reaches it. - -`taskBlocks` (`portable/block-spelling.ts`) writes the marker as a paragraph of its own where a -block task item's content opens with another block. Mutation testing finds that branch unreached: -a block task item is left only when it holds two or more paragraphs. Found by the stability review -of item 33, 2026-10-29. - ### 144. Give every container `docker-runner.sh` and `publish.sh` start a memory limit and an equal swap limit, sized from the largest a gate leg measurably needs. `in_image`, `with_firefox` and `publish.sh` run `docker run` with neither `--memory` nor @@ -151,13 +142,6 @@ export it through `emit/spelling-questions.ts`, the one import `parse/` may take drop the comment at `markdown-to-adf.ts:145` that keeps them apart. Technical principle: DRY. Found by the principles audit of the release review of 0.3.0, 2026-11-08. -### 81. Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds. - -Nothing checks the README's tables against the codes: `src/result.test.ts` holds a refusal per code, -but a code added or removed leaves the tables a false contract under a green gate. Assert that the -tables list exactly the codes `causes` holds. Found by the technical-principles audit of item 78, -2026-10-05. - ### 147. Ship `adfToPlainText`, writing a document's content with no markup, under a README goal for plain text. Asked on the Atlassian developer community for search indexing, notifications, emails and CSV