A read below the depth that filled the memo re-spells, so the depth guards still run
This commit is contained in:
@@ -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
|
||||
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
|
||||
consumer's document may hold one node at two positions (4b). A kept spelling rebases: `text` and
|
||||
`spelling` carry no depth, `headroom` is affine in it, and a reuse sits at or above the depth that
|
||||
filled it. Only what succeeded is kept, so no path minted at another position is ever read.
|
||||
consumer's document may hold one node at two positions (4b). `text` and `spelling` carry no depth
|
||||
and `headroom` is affine in it, so a read above the depth that filled the entry rebases; a 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
|
||||
fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states
|
||||
unrepresentable.
|
||||
|
||||
@@ -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 {
|
||||
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) return kept.block === undefined ? undefined : success({ ...kept.block, headroom: kept.block.headroom + kept.depth - depth })
|
||||
if (kept !== undefined) {
|
||||
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)
|
||||
if (spelled === undefined) memo?.set(node, { block: undefined, depth })
|
||||
else if (spelled.ok) memo?.set(node, { block: spelled.value, depth })
|
||||
|
||||
@@ -672,7 +672,7 @@ test('refuses input nested deeper than the parser carries', () => {
|
||||
assert.equal(code(markdownToAdf(marks(largestNesting + 1))), 'unsupported-nesting-depth')
|
||||
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 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.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')
|
||||
@@ -680,6 +680,16 @@ test('refuses input nested deeper than the parser carries', () => {
|
||||
const directiveLists = largestNesting / 2
|
||||
assert.ok(markdownToAdf(listed(directiveLists)).ok)
|
||||
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.equal(code(markdownToAdf(`${'- '.repeat(largestNesting + 1)}a\n`)), 'unsupported-nesting-depth')
|
||||
})
|
||||
|
||||
+11
-6
@@ -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
|
||||
maintainer, 2026-09-18).
|
||||
**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
|
||||
261 kB; a panel between every pair of lists 1.00 s to 0.07 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
|
||||
both — so no figure enters the gate (§14), as 4c settled for a behaviour-preserving cost fix.
|
||||
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.
|
||||
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.05 s, an opaque carry in every item
|
||||
1.08 s to 0.03 s. No figure enters the gate (§14), as 4c settled for a behaviour-preserving
|
||||
cost fix; what the gate holds is the refusal. The rebase's sign is pinned by the two
|
||||
depth-boundary tests already there, its magnitude by a third case the review found: an ordered
|
||||
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`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user