Compare commits

..

2 Commits

Author SHA1 Message Date
lilleman ee10b63fe7 Tick 28, moving its text to todo-history.md
CI / gate (push) Successful in 27s
CI / publish (push) Has been skipped
2026-09-20 17:49:54 +02:00
lilleman e34d39fdee 28 - the line's retry takes one fallback per pass, so it cannot spin 2026-09-20 17:49:54 +02:00
4 changed files with 49 additions and 29 deletions
+3
View File
@@ -314,6 +314,9 @@ 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 rather than an argument every give-way site has to honour
(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
+24 -18
View File
@@ -35,6 +35,8 @@ type LineAttempt =
| { carry?: undefined; line?: undefined; openingLinkAsDirective: true } | { carry?: undefined; line?: undefined; openingLinkAsDirective: true }
| { carry?: undefined; line: string; openingLinkAsDirective?: undefined } | { carry?: undefined; line: string; openingLinkAsDirective?: undefined }
type LineFallbacks = { carried: Set<number>; openingLinkAsDirective: boolean }
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)
if (!emitted.ok) return emitted if (!emitted.ok) return emitted
@@ -63,37 +65,41 @@ 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.carry ?? 'opening-link', path)
if (!taken.ok) return taken
} }
} }
function carryRange(carried: Set<number>, range: NodeRange): void { // emitLine ends because each fallback is takeable once: a pass taking none re-emits the line it just emitted.
for (let index = range.first; index <= range.last; index += 1) carried.add(index) function takeFallback(fallbacks: LineFallbacks, fallback: NodeRange | 'opening-link', path: ConvertErrorPath): Result<null> {
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
+21
View File
@@ -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`
+1 -11
View File
@@ -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