diff --git a/docs/reading-cost.md b/docs/reading-cost.md index 42b5285..4acfe88 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -885,3 +885,20 @@ Read most (opens and searches): - `AGENTS.md` 1 - `docker-runner.sh` 1 - `package.json` 1 + +## Reading cost, chunk 0a04be76fd43, finished 2026-11-09T06:51:38Z + +152169 new input tokens before the first commit. + +Read most (opens and searches): + +- `README.md` 14 +- `todo.md` 12 +- `src/adf/document.test.ts` 11 +- `docs/decisions.md` 6 +- `src/adf/document.ts` 6 +- `src/conformance/corpus.test.ts` 6 +- `MIGRATION.md` 4 +- `AGENTS.md` 3 +- `src/index.ts` 3 +- `CHANGELOG.md` 2 diff --git a/src/json-text.test.ts b/src/json-text.test.ts index 67f287f..de8b49e 100644 --- a/src/json-text.test.ts +++ b/src/json-text.test.ts @@ -1,13 +1,17 @@ import assert from 'node:assert/strict' -import { dirname, join } from 'node:path' import fc from 'fast-check' -import { fileURLToPath } from 'node:url' -import { readFileSync, readdirSync } from 'node:fs' import test from 'node:test' +import type { AdfDocument, AdfNode } from './adf/document.ts' import type { JsonSpelling } from './canonical-json.ts' +import { adfToHtml } from './html/emit/adf-to-html.ts' +import { adfToLosslessMarkdown } from './markdown/emit/adf-to-markdown.ts' +import { adfToPortableMarkdown } from './markdown/portable/adf-to-portable-markdown.ts' +import { htmlToAdf } from './html/parse/html-to-adf.ts' import { jsonValue, propertyRuns } from './conformance/property-harness.ts' +import { losslessMarkdownToAdf } from './markdown/parse/markdown-to-adf.ts' import { parseJsonText } from './json-text.ts' +import { portableMarkdownToAdf } from './markdown/portable/portable-markdown-to-adf.ts' import { serializeCanonicalJson } from './canonical-json.ts' test('reads every JSON value kind', () => { @@ -87,10 +91,32 @@ test('accepts exactly the text JSON.parse accepts', () => { ) }) -test('no shipped file reads JSON with JSON.parse, which parseJsonText stands in for', () => { - const sourceRoot = dirname(fileURLToPath(import.meta.url)) - const readers = readdirSync(sourceRoot, { encoding: 'utf8', recursive: true }).filter( - (name) => name.endsWith('.ts') && !name.endsWith('.test.ts') && !name.startsWith('conformance') && readFileSync(join(sourceRoot, name), 'utf8').includes('JSON.parse('), - ) - assert.deepEqual(readers, []) +test('no reader reads JSON text with JSON.parse, which parseJsonText stands in for', () => { + const unknownBlock: AdfNode = { attrs: { a: [1, { b: 'c' }] }, content: [{ text: 'x', type: 'text' }], type: 'unknownBlock' } + const unknownInline: AdfNode = { attrs: { a: { b: 2 } }, type: 'unknownInline' } + const table: AdfNode = { + content: [{ content: [{ attrs: { colwidth: [100, 200] }, content: [{ content: [{ text: 'x', type: 'text' }], type: 'paragraph' }], type: 'tableCell' }], type: 'tableRow' }], + type: 'table', + } + const marked: AdfNode = { content: [{ marks: [{ attrs: { color: '#fff' }, type: 'textColor' }], text: 'x', type: 'text' }, unknownInline], type: 'paragraph' } + const document: AdfDocument = { content: [unknownBlock, table, marked, { attrs: { panelType: 'info' }, content: [marked], type: 'panel' }], type: 'doc', version: 1 } + const spellings = [adfToLosslessMarkdown(document), adfToPortableMarkdown(document), adfToHtml(document)] + const [lossless, portable, html] = spellings.map((spelled) => (spelled.ok ? spelled.value : undefined)) + assert.ok(lossless !== undefined && portable !== undefined && html !== undefined) + const parse = JSON.parse + let called = 0 + JSON.parse = (...args: Parameters) => { + called += 1 + return parse(...args) + } + try { + const read = [losslessMarkdownToAdf(lossless), portableMarkdownToAdf(portable), htmlToAdf(html)] + assert.deepEqual( + read.map((result) => result.ok), + [true, true, true], + ) + } finally { + JSON.parse = parse + } + assert.equal(called, 0) }) diff --git a/src/markdown/commonmark/entity-references.test.ts b/src/markdown/commonmark/entity-references.test.ts index fc0c65f..699e849 100644 --- a/src/markdown/commonmark/entity-references.test.ts +++ b/src/markdown/commonmark/entity-references.test.ts @@ -1,21 +1,24 @@ 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 { readEntityReference } from './entity-references.ts' - -const source = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'entity-references.ts'), 'utf8') +import { namedReferences, readEntityReference } from './entity-references.ts' // A value holding a space or a tilde mis-splits into a wrong name and a lost one, silently, at load. -test('every packed reference parts into one name and one value the table decodes', () => { - const packed = /const packedReferences =\n {2}'([\s\S]*?)'\n/.exec(source)?.[1] - assert.ok(packed !== undefined) - const references = packed.split(/(?:\\\n)? +/) - assert.equal(references.length, 2125) - for (const reference of references) { - assert.match(reference, /^[A-Za-z][A-Za-z0-9]*~[^ ~]+$/) - assert.ok((readEntityReference(`&${reference.slice(0, reference.indexOf('~'))};`, 0)?.text ?? '') !== '') +test('every named reference parts into one name and one value', () => { + assert.equal(namedReferences.size, 2125) + for (const [name, value] of namedReferences) { + assert.match(name, /^[A-Za-z][A-Za-z0-9]*$/, name) + assert.match(value, /^[^ ~]+$/u, name) } }) + +test('decodes a reference to the characters HTML5 gives its name', () => { + assert.deepEqual(readEntityReference('⁡', 0), { length: 15, text: '⁡' }) + assert.deepEqual(readEntityReference(' ', 0), { length: 9, text: '\n' }) + assert.deepEqual(readEntityReference(' ', 0), { length: 5, text: '\t' }) + assert.deepEqual(readEntityReference('  ', 0), { length: 12, text: '  ' }) + assert.deepEqual(readEntityReference('⊂⃒', 0), { length: 11, text: '⊂⃒' }) + assert.deepEqual(readEntityReference(' ', 0), { length: 6, text: ' ' }) + assert.deepEqual(readEntityReference('‌', 0), { length: 6, text: '‌' }) + assert.equal(readEntityReference('¬areference;', 0), undefined) +}) diff --git a/src/markdown/commonmark/entity-references.ts b/src/markdown/commonmark/entity-references.ts index d048864..8e0745e 100644 --- a/src/markdown/commonmark/entity-references.ts +++ b/src/markdown/commonmark/entity-references.ts @@ -200,7 +200,7 @@ const packedReferences = xsqcup~⨆ xuplus~⨄ xutri~△ xvee~⋁ xwedge~⋀ yacute~ý yacy~я ycirc~ŷ ycy~ы yen~¥ yfr~𝔶 yicy~ї yopf~𝕪 yscr~𝓎 yucy~ю\ yuml~ÿ zacute~ź zcaron~ž zcy~з zdot~ż zeetrf~ℨ zeta~ζ zfr~𝔷 zhcy~ж zigrarr~⇝ zopf~𝕫 zscr~𝓏 zwj~‍ zwnj~‌' -const namedReferences = new Map(packedReferences.split(/ +/).map(namedReference)) +export const namedReferences: ReadonlyMap = new Map(packedReferences.split(/ +/).map(namedReference)) export function holdsEntityReference(text: string): boolean { for (let index = text.indexOf('&'); index !== -1; index = text.indexOf('&', index + 1)) { diff --git a/src/result.test.ts b/src/result.test.ts index 8503d13..02947ad 100644 --- a/src/result.test.ts +++ b/src/result.test.ts @@ -1,31 +1,46 @@ import assert from 'node:assert/strict' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { readFileSync, readdirSync } from 'node:fs' import test from 'node:test' -const sourceRoot = dirname(fileURLToPath(import.meta.url)) -const union = /export type ConvertErrorCode =\n((?:\s+\| '[a-z-]+'\n)+)/ -const declared = /'([a-z-]+)'/g -const callSite = /(?:failure\(|code: )'([a-z-]+)'/g +import type { AdfDocument, AdfMark } from './adf/document.ts' +import type { ConvertErrorCode, Result } from './result.ts' +import type { JsonValue } from './json-value.ts' +import { adfToLosslessMarkdown } from './markdown/emit/adf-to-markdown.ts' +import { htmlToAdf } from './html/parse/html-to-adf.ts' +import { largestNesting } from './nesting.ts' +import { losslessMarkdownToAdf } from './markdown/parse/markdown-to-adf.ts' +import { readAdfDocument } from './adf/document.ts' +import { withinEngineLimits } from './length-bound.ts' -function declaredCodes(): string[] { - const source = readFileSync(join(sourceRoot, 'result.ts'), 'utf8') - const members = union.exec(source)?.[1] - assert.notEqual(members, undefined, 'result.ts declares no ConvertErrorCode union') - return [...(members ?? '').matchAll(declared)].map(([, name]) => name ?? '').sort() +const em: AdfMark = { type: 'em' } +const duplicateMarks: AdfDocument = { content: [{ content: [{ marks: [em, em], text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 } + +// One refusal per code. The Record type fails the build where a code has no entry, or an entry names no code. +const causes: Record Result> = { + 'engine-limit': () => + withinEngineLimits(() => { + throw new RangeError('Invalid array length') + }), + 'malformed-directive': () => losslessMarkdownToAdf('!adf:/panel\n'), + 'malformed-pipe-table': () => losslessMarkdownToAdf('| Part | Qty |\n'), + 'not-a-string': () => Reflect.apply(losslessMarkdownToAdf, undefined, [null]), + 'not-an-adf-document': () => readAdfDocument(null), + 'unknown-directive-name': () => losslessMarkdownToAdf('!adf:widget[a]\n'), + 'unmappable-html': () => htmlToAdf('
\n'), + 'unsupported-document-version': () => adfToLosslessMarkdown({ type: 'doc', version: 2 }), + 'unsupported-nesting-depth': () => adfToLosslessMarkdown(nestedAttribute(largestNesting + 1)), + 'unsupported-node-shape': () => adfToLosslessMarkdown(duplicateMarks), } -function calledCodes(): string[] { - const called = new Set() - for (const name of readdirSync(sourceRoot, { encoding: 'utf8', recursive: true })) { - if (!name.endsWith('.ts') || name.endsWith('.test.ts') || name === 'result.ts') continue - for (const [, code] of readFileSync(join(sourceRoot, name), 'utf8').matchAll(callSite)) called.add(code ?? '') - } - return [...called].sort() +function nestedAttribute(depth: number): AdfDocument { + let value: JsonValue = 1 + for (let level = 0; level < depth; level += 1) value = { a: value } + return { content: [{ attrs: { a: value }, type: 'paragraph' }], type: 'doc', version: 1 } } // Removing a code is breaking (docs/decisions.md §The code list), so a code must not outlive its cause. -test('every ConvertErrorCode is the code of a production call site, and every call site names a declared one', () => { - assert.deepEqual(calledCodes(), declaredCodes()) -}) +for (const [code, cause] of Object.entries(causes)) { + test(`${code} is the code of a refusal the library makes`, () => { + const result = cause() + assert.equal(!result.ok && result.error.code, code) + }) +} diff --git a/todo.md b/todo.md index cdb82b0..9d8ccf1 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 | -| 127 | 0.4.0 | | principle | **Hold what `entity-references.test.ts`, `json-text.test.ts` and `result.test.ts` check by asserting outcomes, so none reads source text.** | 2 | 2 | 1 | 3 | 3 | code health by 2 | 13.1 | | 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 | @@ -68,24 +67,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. -### 127. Hold what `entity-references.test.ts`, `json-text.test.ts` and `result.test.ts` check by asserting outcomes, so none reads source text. - -Technical principle "Assert the outcome", which bans scanning the source with `ast` or a regex. -Three tests read source text: - -- `src/markdown/commonmark/entity-references.test.ts` regex-extracts the `packedReferences` literal - from `entity-references.ts`, to guard that each of the 2125 packed references parts into one name - and one value the table decodes. Reformatting the literal fails it with the table unchanged. -- `src/json-text.test.ts` greps every shipped file for `JSON.parse(`, to guard that only - `parseJsonText` reads JSON text. `JSON.parse (` or `JSON['parse'](` escapes it. -- `src/result.test.ts` regex-parses the `ConvertErrorCode` union out of `result.ts`, and each - `failure('…'` and `code: '…'` out of every shipped file, to guard that every declared code has a - production cause and every call site names a declared code. A code held in a variable or passed - through another helper escapes it. - -Item 81 changes `result.test.ts` too. Found by the technical-principles audit of -item 60, 2026-10-22. - ### 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 @@ -172,9 +153,10 @@ Technical principle: DRY. Found by the principles audit of the release review of ### 81. Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds. -`src/result.test.ts` checks the union against the call sites only, so a code added or removed -leaves the README's tables a false contract under a green gate. Assert that the tables list exactly -the union. Found by the technical-principles audit of item 78, 2026-10-05. +`src/result.test.ts` holds one refusal per code in a `Record`, so a code added +or removed leaves the README's tables a false contract under a green gate. Assert that the tables +list exactly the codes of that record's keys. 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.