3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Assert outcomes in the entity, JSON and error-code tests, so none reads source text

This commit is contained in:
2026-11-09 07:56:29 +01:00
committed by Lillem4n
parent 4f23570555
commit f541918bcb
6 changed files with 111 additions and 68 deletions
+17
View File
@@ -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
+35 -9
View File
@@ -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<typeof JSON.parse>) => {
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)
})
@@ -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('&ApplyFunction;', 0), { length: 15, text: '⁡' })
assert.deepEqual(readEntityReference('&NewLine;', 0), { length: 9, text: '\n' })
assert.deepEqual(readEntityReference('&Tab;', 0), { length: 5, text: '\t' })
assert.deepEqual(readEntityReference('&ThickSpace;', 0), { length: 12, text: '  ' })
assert.deepEqual(readEntityReference('&NotSubset;', 0), { length: 11, text: '⊂⃒' })
assert.deepEqual(readEntityReference('&nbsp;', 0), { length: 6, text: ' ' })
assert.deepEqual(readEntityReference('&zwnj;', 0), { length: 6, text: '‌' })
assert.equal(readEntityReference('&notareference;', 0), undefined)
})
+1 -1
View File
@@ -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<string, string> = new Map(packedReferences.split(/ +/).map(namedReference))
export function holdsEntityReference(text: string): boolean {
for (let index = text.indexOf('&'); index !== -1; index = text.indexOf('&', index + 1)) {
+37 -22
View File
@@ -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<ConvertErrorCode, () => Result<unknown>> = {
'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('<table><tr></tr></table>\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<string>()
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)
})
}
+4 -22
View File
@@ -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<ConvertErrorCode, …>`, 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.