A read below the depth that filled the memo re-spells, so the depth guards still run
CI / gate (push) Successful in 28s
CI / publish (push) Has been skipped

This commit is contained in:
2026-09-19 13:37:58 +02:00
parent 3f8afcb9d7
commit d6756ea5e6
4 changed files with 33 additions and 12 deletions
+6 -3
View File
@@ -316,9 +316,12 @@ someone spells it or pins it.
- The parse keeps each node's readable spelling in a memo, so the `commonMarkSpelling` ask spells a - The parse keeps each node's readable spelling in a memo, so the `commonMarkSpelling` ask spells a
node once rather than once per level above it (18). The node reference is the key, which holds node once rather than once per level above it (18). The node reference is the key, which holds
because the parse builds one object per position; `adfToMarkdown` passes no memo, where a because the parse builds one object per position; `adfToMarkdown` passes no memo, where a
consumer's document may hold one node at two positions (4b). A kept spelling rebases: `text` and consumer's document may hold one node at two positions (4b). `text` and `spelling` carry no depth
`spelling` carry no depth, `headroom` is affine in it, and a reuse sits at or above the depth that and `headroom` is affine in it, so a read above the depth that filled the entry rebases; a read
filled it. Only what succeeded is kept, so no path minted at another position is ever read. below re-spells, because a hit skips the depth guards the walk it replaces runs and an ordered
list past the marker cap gives way, spending two emitter levels where the parser spent one. A
give-way is kept too and serves any depth, reading the node's shape alone. Only what succeeded is
kept, so no path minted at another position is ever read.
- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the - No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the
fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states
unrepresentable. unrepresentable.
+5 -2
View File
@@ -103,8 +103,11 @@ export function commonMarkSpelling(node: AdfNode, path: ConvertErrorPath, depth:
function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined { function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, memo: SpellingMemo | undefined): Result<EmittedBlock> | undefined {
const kept = memo?.get(node) const kept = memo?.get(node)
// A reuse sits no deeper than the fill it reads, so the kept headroom only ever grows (AGENTS.md §11). if (kept !== undefined) {
if (kept !== undefined) return kept.block === undefined ? undefined : success({ ...kept.block, headroom: kept.block.headroom + kept.depth - depth }) if (kept.block === undefined) return undefined
// A read below the fill would skip the depth guards the walk it replaces runs (AGENTS.md §11).
if (depth <= kept.depth) return success({ ...kept.block, headroom: kept.block.headroom + kept.depth - depth })
}
const spelled = spellReadableBlock(node, path, depth, memo) const spelled = spellReadableBlock(node, path, depth, memo)
if (spelled === undefined) memo?.set(node, { block: undefined, depth }) if (spelled === undefined) memo?.set(node, { block: undefined, depth })
else if (spelled.ok) memo?.set(node, { block: spelled.value, depth }) else if (spelled.ok) memo?.set(node, { block: spelled.value, depth })
+11 -1
View File
@@ -672,7 +672,7 @@ test('refuses input nested deeper than the parser carries', () => {
assert.equal(code(markdownToAdf(marks(largestNesting + 1))), 'unsupported-nesting-depth') assert.equal(code(markdownToAdf(marks(largestNesting + 1))), 'unsupported-nesting-depth')
assert.deepEqual(content(markdownToAdf(marks(largestNesting))), [{ content: [marked('a', underline)], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf(marks(largestNesting))), [{ content: [marked('a', underline)], type: 'paragraph' }])
const nest = (names: readonly string[], body: string): string => [...names.map((name) => `!adf:${name}\n`), body, ...names.map((name) => `!adf:/${name}\n`).reverse()].join('') const nest = (names: readonly string[], body: string): string => [...names.map((name) => `!adf:${name}\n`), body, ...names.map((name) => `!adf:/${name}\n`).reverse()].join('')
const repeated = (name: string): string[] => Array.from({ length: largestNesting }, () => name) const repeated = (name: string, levels: number = largestNesting): string[] => Array.from({ length: levels }, () => name)
assert.ok(markdownToAdf(nest(repeated('panel'), '!adf:paragraph {localId=a-1}\nPart.\n!adf:/paragraph\n')).ok) assert.ok(markdownToAdf(nest(repeated('panel'), '!adf:paragraph {localId=a-1}\nPart.\n!adf:/paragraph\n')).ok)
assert.deepEqual(position(markdownToAdf(nest(['expand', ...repeated('panel'), 'expand'], 'Part.\n'))), { line: 501, offset: 5501 }) assert.deepEqual(position(markdownToAdf(nest(['expand', ...repeated('panel'), 'expand'], 'Part.\n'))), { line: 501, offset: 5501 })
assert.equal(code(markdownToAdf(nest(['panel', ...repeated('expand'), 'panel'], 'Part.\n'))), 'unsupported-nesting-depth') assert.equal(code(markdownToAdf(nest(['panel', ...repeated('expand'), 'panel'], 'Part.\n'))), 'unsupported-nesting-depth')
@@ -680,6 +680,16 @@ test('refuses input nested deeper than the parser carries', () => {
const directiveLists = largestNesting / 2 const directiveLists = largestNesting / 2
assert.ok(markdownToAdf(listed(directiveLists)).ok) assert.ok(markdownToAdf(listed(directiveLists)).ok)
assert.equal(code(markdownToAdf(listed(directiveLists + 1))), 'unsupported-nesting-depth') assert.equal(code(markdownToAdf(listed(directiveLists + 1))), 'unsupported-nesting-depth')
const asking = (body: string): string => `!adf:bulletList\n!adf:listItem\n${body}!adf:/listItem\n!adf:/bulletList\n`
// Two items past the largest list marker leave the list no readable spelling, so the emitter
// walks it as list plus item where the parser counted one level.
const overflowing = (body: string): string => {
const marker = '999999999. '
return `${marker}${body.replace(/^(?!$)/gm, ' '.repeat(marker.length)).slice(marker.length)}\n${marker}z\n`
}
const overflowed = (panels: number): string => asking(overflowing(overflowing(asking(`---\n${nest(repeated('panel', panels), 'Part.\n')}`))))
assert.equal(code(markdownToAdf(overflowed(493))), 'unsupported-node-shape')
assert.equal(code(markdownToAdf(overflowed(494))), 'unsupported-nesting-depth')
assert.ok(markdownToAdf(`${'- '.repeat(largestNesting)}a\n`).ok) assert.ok(markdownToAdf(`${'- '.repeat(largestNesting)}a\n`).ok)
assert.equal(code(markdownToAdf(`${'- '.repeat(largestNesting + 1)}a\n`)), 'unsupported-nesting-depth') assert.equal(code(markdownToAdf(`${'- '.repeat(largestNesting + 1)}a\n`)), 'unsupported-nesting-depth')
}) })
+11 -6
View File
@@ -837,12 +837,17 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
rejected: it reports the outer offender where the build reports the inner one (the rejected: it reports the outer offender where the build reports the inner one (the
maintainer, 2026-09-18). maintainer, 2026-09-18).
**Measured** (2026-09-19): the parse keeps each node's readable spelling (AGENTS.md §11), and **Measured** (2026-09-19): the parse keeps each node's readable spelling (AGENTS.md §11), and
250 nested directive lists fall from 1.22 s to 0.07 s at 16.1 kB and from 5.20 s to 0.19 s at 250 nested directive lists fall from 1.22 s to 0.04 s at 16.1 kB and from 5.20 s to 0.12 s at
261 kB; a panel between every pair of lists 1.00 s to 0.07 s, an opaque carry in every item 261 kB; a panel between every pair of lists 1.00 s to 0.05 s, an opaque carry in every item
1.08 s to 0.04 s. The two depth-boundary tests are what pin the rebase — dropping it reddens 1.08 s to 0.03 s. No figure enters the gate (§14), as 4c settled for a behaviour-preserving
both — so no figure enters the gate (§14), as 4c settled for a behaviour-preserving cost fix. cost fix; what the gate holds is the refusal. The rebase's sign is pinned by the two
The one list accounting was not taken: 4b settled that accounting the day this was filed, and depth-boundary tests already there, its magnitude by a third case the review found: an ordered
reopening it is an ask rather than a chunk. list past the marker cap gives way, so the emitter spends two levels where the parser spent
one, and two such lists put a read below the depth that filled its entry — which a hit would
answer without the depth guards the walk runs, naming a different refusal at a different path.
That read re-spells (the stability-reviewer, 2026-09-19). The one list accounting was not
taken: 4b settled that accounting the day this was filed, and reopening it is an ask rather
than a chunk.
## 5 — Ship `0.1.0` ## 5 — Ship `0.1.0`