28 - the line's retry takes one fallback per pass, so it cannot spin #110
@@ -121,7 +121,10 @@ names the escape that unclaims the form claimed: `\!adf:` for a directive, block
|
|||||||
alike, `\|` for every pipe row.
|
alike, `\|` for every pipe row.
|
||||||
Adding, removing or renaming a code is breaking, so a new cause takes an existing code whose
|
Adding, removing or renaming a code is breaking, so a new cause takes an existing code whose
|
||||||
name reads true of it in both directions; where none does and a plain name exists, a new code — in
|
name reads true of it in both directions; where none does and a plain name exists, a new code — in
|
||||||
any 0.x minor, and after 1.0 only in a MAJOR (the maintainer, 2026-09-18). A code names the
|
any 0.x minor, and after 1.0 only in a MAJOR (the maintainer, 2026-09-18). A refusal whose cause is
|
||||||
|
this library's own invariant rather than the input takes the existing code nearest what the consumer
|
||||||
|
sees — a document that does not convert is `unsupported-node-shape` — since a code no input reaches
|
||||||
|
is one no consumer can switch on (the maintainer, 2026-09-20). A code names the
|
||||||
cause; where one cause recurs across node types, across one mark's attributes or across
|
cause; where one cause recurs across node types, across one mark's attributes or across
|
||||||
directions, one code covers them all and
|
directions, one code covers them all and
|
||||||
`path` and `message` say which — `unsupported-nesting-depth` is the 500-level guard whichever
|
`path` and `message` say which — `unsupported-nesting-depth` is the 500-level guard whichever
|
||||||
@@ -208,7 +211,7 @@ wide `Result<T>`, since half their refusals come from an emit stage that read no
|
|||||||
|
|
||||||
Test for the behaviour wanted first, then implement until green. `node --test`, beside the code.
|
Test for the behaviour wanted first, then implement until green. `node --test`, beside the code.
|
||||||
Node, tsc and npm never run on the host — only via the pinned images (§9). Tests are independent,
|
Node, tsc and npm never run on the host — only via the pinned images (§9). Tests are independent,
|
||||||
coverage does not decline, containers are torn down after a run.
|
containers are torn down after a run.
|
||||||
|
|
||||||
The gate runs that same suite under Deno and Bun as well as Node, the three images pinned alike,
|
The gate runs that same suite under Deno and Bun as well as Node, the three images pinned alike,
|
||||||
and neither extra leg is Node's proof twice. Deno refuses an extensionless or directory specifier,
|
and neither extra leg is Node's proof twice. Deno refuses an extensionless or directory specifier,
|
||||||
@@ -314,6 +317,8 @@ someone spells it or pins it.
|
|||||||
mark run's segments: the argument list caps near 125k and throws a `RangeError` where a `Result`
|
mark run's segments: the argument list caps near 125k and throws a `RangeError` where a `Result`
|
||||||
is owed. A walk pushes one at a time. A literal spread (`[...value]`) is not the same thing and
|
is owed. A walk pushes one at a time. A literal spread (`[...value]`) is not the same thing and
|
||||||
is fine (4c).
|
is fine (4c).
|
||||||
|
- A loop retrying an input until a fallback spells it refuses the pass taking no fallback, so its
|
||||||
|
termination is the loop's own check (28).
|
||||||
- A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the
|
- A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the
|
||||||
line before it reads, `indexOf` — never a fresh slice per character, and a per-character walk
|
line before it reads, `indexOf` — never a fresh slice per character, and a per-character walk
|
||||||
hoists the scan that does not vary with the character. The pipeline persona feeds documents
|
hoists the scan that does not vary with the character. The pipeline persona feeds documents
|
||||||
|
|||||||
@@ -30,10 +30,9 @@ type InlineContext = {
|
|||||||
|
|
||||||
type InlineRun = { index: number; kind: 'marked'; mark: AdfMark; nodes: AdfNode[] } | { index: number; kind: 'plain'; node: AdfNode }
|
type InlineRun = { index: number; kind: 'marked'; mark: AdfMark; nodes: AdfNode[] } | { index: number; kind: 'plain'; node: AdfNode }
|
||||||
|
|
||||||
type LineAttempt =
|
type LineAttempt = { fallback: NodeRange | 'opening-link'; line?: undefined } | { fallback?: undefined; line: string }
|
||||||
| { carry: NodeRange; line?: undefined; openingLinkAsDirective?: undefined }
|
|
||||||
| { carry?: undefined; line?: undefined; openingLinkAsDirective: true }
|
type LineFallbacks = { carried: Set<number>; openingLinkAsDirective: boolean }
|
||||||
| { carry?: undefined; line: string; openingLinkAsDirective?: undefined }
|
|
||||||
|
|
||||||
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<string> {
|
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<string> {
|
||||||
const emitted = emitLine(nodes, container, path)
|
const emitted = emitLine(nodes, container, path)
|
||||||
@@ -63,37 +62,40 @@ export function tryImageLine(alt: string | undefined, href: string, path: Conver
|
|||||||
return attempt.ok ? attempt.value.line : undefined
|
return attempt.ok ? attempt.value.line : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every pass carries at least one more node, or flips openingLinkAsDirective, which happens once — so the loop ends.
|
|
||||||
function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<EmittedLine> {
|
function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<EmittedLine> {
|
||||||
const carried = new Set<number>()
|
const fallbacks: LineFallbacks = { carried: new Set(), openingLinkAsDirective: false }
|
||||||
let openingLinkAsDirective = false
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const emission = lineSegments(nodes, container, path, carried, openingLinkAsDirective)
|
const emission = lineSegments(nodes, container, path, fallbacks)
|
||||||
if (!emission.ok) return emission
|
if (!emission.ok) return emission
|
||||||
if (emission.value.carry !== undefined) {
|
if (emission.value.carry !== undefined) {
|
||||||
carryRange(carried, emission.value.carry)
|
const taken = takeFallback(fallbacks, emission.value.carry, path)
|
||||||
|
if (!taken.ok) return taken
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const attempt = attemptLine(emission.value.segments, container, path)
|
const attempt = attemptLine(emission.value.segments, container, path)
|
||||||
if (!attempt.ok) return attempt
|
if (!attempt.ok) return attempt
|
||||||
if (attempt.value.line !== undefined) return success({ line: attempt.value.line, openingLinkAsDirective, segments: emission.value.segments })
|
if (attempt.value.line !== undefined) {
|
||||||
if (attempt.value.carry !== undefined) carryRange(carried, attempt.value.carry)
|
return success({ line: attempt.value.line, openingLinkAsDirective: fallbacks.openingLinkAsDirective, segments: emission.value.segments })
|
||||||
else openingLinkAsDirective = true
|
}
|
||||||
|
const taken = takeFallback(fallbacks, attempt.value.fallback, path)
|
||||||
|
if (!taken.ok) return taken
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function carryRange(carried: Set<number>, range: NodeRange): void {
|
function takeFallback(fallbacks: LineFallbacks, fallback: NodeRange | 'opening-link', path: ConvertErrorPath): Result<null> {
|
||||||
for (let index = range.first; index <= range.last; index += 1) carried.add(index)
|
if (fallback === 'opening-link') {
|
||||||
|
if (fallbacks.openingLinkAsDirective) return failure('unsupported-node-shape', 'an opening link spelled as a directive still reads as a link definition, so the line has no spelling left', path)
|
||||||
|
fallbacks.openingLinkAsDirective = true
|
||||||
|
return success(null)
|
||||||
|
}
|
||||||
|
const before = fallbacks.carried.size
|
||||||
|
for (let index = fallback.first; index <= fallback.last; index += 1) fallbacks.carried.add(index)
|
||||||
|
if (fallbacks.carried.size === before) return failure('unsupported-node-shape', 'a carry took no inline node the line had not carried, so the line has no spelling left', path)
|
||||||
|
return success(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineSegments(
|
function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks): Result<Emission> {
|
||||||
nodes: readonly AdfNode[],
|
const context: InlineContext = { atBlockEnd: true, bracketed: false, ...fallbacks, path, spansLines: container === 'paragraph' }
|
||||||
container: LineContainer,
|
|
||||||
path: ConvertErrorPath,
|
|
||||||
carried: ReadonlySet<number>,
|
|
||||||
openingLinkAsDirective: boolean,
|
|
||||||
): Result<Emission> {
|
|
||||||
const context: InlineContext = { atBlockEnd: true, bracketed: false, carried, openingLinkAsDirective, path, spansLines: container === 'paragraph' }
|
|
||||||
const emission = emitRun(nodes, 0, 0, context)
|
const emission = emitRun(nodes, 0, 0, context)
|
||||||
if (!emission.ok) return emission
|
if (!emission.ok) return emission
|
||||||
if (emission.value.carry !== undefined) return emission
|
if (emission.value.carry !== undefined) return emission
|
||||||
@@ -102,8 +104,8 @@ function lineSegments(
|
|||||||
|
|
||||||
function attemptLine(segments: readonly InlineSegment[], container: LineContainer, path: ConvertErrorPath): Result<LineAttempt> {
|
function attemptLine(segments: readonly InlineSegment[], container: LineContainer, path: ConvertErrorPath): Result<LineAttempt> {
|
||||||
const assembled = assembleInlineLine(segments, container)
|
const assembled = assembleInlineLine(segments, container)
|
||||||
if (assembled.openingLinkAsDirective) return success({ openingLinkAsDirective: true })
|
if (assembled.openingLinkAsDirective) return success({ fallback: 'opening-link' })
|
||||||
if (assembled.unspellableRun !== undefined) return success({ carry: assembled.unspellableRun })
|
if (assembled.unspellableRun !== undefined) return success({ fallback: assembled.unspellableRun })
|
||||||
for (const [index, single] of assembled.line.split('\n').entries()) {
|
for (const [index, single] of assembled.line.split('\n').entries()) {
|
||||||
if (container === 'paragraph' && claimsLine(single, index === 0 ? 'first' : 'later')) {
|
if (container === 'paragraph' && claimsLine(single, index === 0 ? 'first' : 'later')) {
|
||||||
return failure('unspellable-line-start', `block parsing would claim the emitted line ${JSON.stringify(single)}`, path)
|
return failure('unspellable-line-start', `block parsing would claim the emitted line ${JSON.stringify(single)}`, path)
|
||||||
|
|||||||
@@ -886,6 +886,27 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
|
|||||||
(the stability-reviewer, 2026-09-19). The one list accounting was not
|
(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
|
taken: 4b settled that accounting the day this was filed, and reopening it is an ask rather
|
||||||
than a chunk.
|
than a chunk.
|
||||||
|
- [x] **28 — `emitLine`'s retry loop cannot spin (`0.2.0`).** `emit/inline-line.ts:67` is a
|
||||||
|
`for (;;)` that re-emits the line until every unspellable node has been carried, and its
|
||||||
|
termination rests on a comment: each pass carries at least one more node, or flips
|
||||||
|
`openingLinkAsDirective`, which happens once. Ten `return success({ carry: … })` sites in
|
||||||
|
that file have to honour it and nothing checks them — a range already inside `carried` loops
|
||||||
|
forever. The library has no I/O and no timeout, so that is a hung caller rather than an
|
||||||
|
error result, and §1's pipeline persona feeds documents nobody typed. Make the loop hold its
|
||||||
|
own guarantee: refuse a carry that adds no node and return an error. Reads first in `0.2.0`
|
||||||
|
because it is the only known way this library fails without a `Result`. Found by the
|
||||||
|
comprehension panel, 2026-09-20; the ten sites are confirmed, a document that reaches the
|
||||||
|
spin is not.
|
||||||
|
**Done** (2026-09-20): the loop's progress is one named state and every pass takes a
|
||||||
|
fallback through `takeFallback`, which refuses a carry adding no node and an opening link
|
||||||
|
asked for the directive form a second time. Both are `unsupported-node-shape` under §8's
|
||||||
|
rule that a new cause takes an existing code reading true of it: the emitter has no spelling
|
||||||
|
left for that node arrangement, and a code a consumer can never switch on costs a removal
|
||||||
|
later. Neither refusal is reachable — a carried node takes `emitLeaf`'s carried branch
|
||||||
|
before any run forms, so every range a site names holds an uncarried node, and the
|
||||||
|
directive-spelled opening link leaves the first segment with no node range for `escape` to
|
||||||
|
ask about — so both are uncovered branches like the repo's other guards, 98.92% to 98.84%
|
||||||
|
against the floor of 98.
|
||||||
|
|
||||||
## 5 — Ship `0.1.0`
|
## 5 — Ship `0.1.0`
|
||||||
|
|
||||||
|
|||||||
@@ -37,17 +37,6 @@ HTML format lands than after: 19 and 20 because HTML has no answer without them,
|
|||||||
HTML doubles the importers and the file count they touch, and 25 to 27 because they are what the
|
HTML doubles the importers and the file count they touch, and 25 to 27 because they are what the
|
||||||
panel says the next reader pays for.
|
panel says the next reader pays for.
|
||||||
|
|
||||||
- [ ] **28 — `emitLine`'s retry loop cannot spin (`0.2.0`).** `emit/inline-line.ts:67` is a
|
|
||||||
`for (;;)` that re-emits the line until every unspellable node has been carried, and its
|
|
||||||
termination rests on a comment: each pass carries at least one more node, or flips
|
|
||||||
`openingLinkAsDirective`, which happens once. Ten `return success({ carry: … })` sites in
|
|
||||||
that file have to honour it and nothing checks them — a range already inside `carried` loops
|
|
||||||
forever. The library has no I/O and no timeout, so that is a hung caller rather than an
|
|
||||||
error result, and §1's pipeline persona feeds documents nobody typed. Make the loop hold its
|
|
||||||
own guarantee: refuse a carry that adds no node and return an error. Reads first in `0.2.0`
|
|
||||||
because it is the only known way this library fails without a `Result`. Found by the
|
|
||||||
comprehension panel, 2026-09-20; the ten sites are confirmed, a document that reaches the
|
|
||||||
spin is not.
|
|
||||||
- [ ] **19 — A home for what both formats read (`0.2.0`).** Settle where a construct both formats
|
- [ ] **19 — A home for what both formats read (`0.2.0`).** Settle where a construct both formats
|
||||||
need lives, and say so in AGENTS.md §11. Today `adf/` may hold no format knowledge and each
|
need lives, and say so in AGENTS.md §11. Today `adf/` may hold no format knowledge and each
|
||||||
format directory holds its own shared layer, so there is no third place; the first ADF-shaped
|
format directory holds its own shared layer, so there is no third place; the first ADF-shaped
|
||||||
@@ -277,6 +266,7 @@ panel says the next reader pays for.
|
|||||||
four and the other the clearest map of the format in the repo. So the ceiling guards against
|
four and the other the clearest map of the format in the repo. So the ceiling guards against
|
||||||
drift and never drives a refactor — 19 to 27 are where the hard work actually is.
|
drift and never drives a refactor — 19 to 27 are where the hard work actually is.
|
||||||
- [x] **18 — The subtree the directive spelling asks about (`0.2.0`).**
|
- [x] **18 — The subtree the directive spelling asks about (`0.2.0`).**
|
||||||
|
- [x] **28 — `emitLine`'s retry loop cannot spin (`0.2.0`).**
|
||||||
|
|
||||||
## The ADF inventory to cover
|
## The ADF inventory to cover
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user