mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer architecture round 1: the guard refuses only the measured overflow errors, the README keeps such a document read-only, and items 106 and 107 take the heap and the carry's indentation
This commit is contained in:
@@ -442,7 +442,7 @@ The emitter raises these, and so does a parse, which asks the emitter how to spe
|
||||
| Code | Fires when | What you can do |
|
||||
| --- | --- | --- |
|
||||
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
||||
| `unsupported-node-shape` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, wraps a directive mark around nothing or only hard breaks, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting, in `adfToLosslessMarkdown`: a node not riding the carry that carries one mark type twice, a block or a non-text inline node holding `text`, or a leaf block or a non-text inline node holding `content`. Emitting, in both writers: a document whose markdown runs past the longest string the JavaScript engine holds, about 537 million characters on V8 | fix what the message names; [`spec/flavour.md`](https://github.com/larvit/adf-codec/blob/main/spec/flavour.md) lists every type's attributes and body |
|
||||
| `unsupported-node-shape` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, wraps a directive mark around nothing or only hard breaks, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting, in `adfToLosslessMarkdown`: a node not riding the carry that carries one mark type twice, a block or a non-text inline node holding `text`, or a leaf block or a non-text inline node holding `content`. Emitting, in both writers: a document whose markdown runs past the longest string the JavaScript engine holds, about 537 million characters on V8 | fix what the message names; [`spec/flavour.md`](https://github.com/larvit/adf-codec/blob/main/spec/flavour.md) lists every type's attributes and body. Past the engine's longest string, keep the ADF and show the document read-only |
|
||||
|
||||
## The guarantees
|
||||
|
||||
|
||||
+3
-1
@@ -463,7 +463,9 @@ handles one cause alike whichever node, attribute or direction raised it.
|
||||
emitting — no document holds one, so no round-trip crosses them (2026-09-23).
|
||||
- A document whose markdown runs past the longest string the JavaScript engine holds is
|
||||
`unsupported-node-shape` at the document's path, in either writer: the document does not convert,
|
||||
and the bound is the engine's own (2026-10-11).
|
||||
and the bound is the engine's own. A carried node reaches it on about 1 MB of deeply nested JSON,
|
||||
since the block carry indents two spaces a level; `todo.md` item 107 spells it in a length linear
|
||||
in its JSON (2026-10-11).
|
||||
- The markdown readers and `htmlToAdf` refuse a value that is not a string as `not-a-string`, at
|
||||
position `{ line: 1, offset: 0 }` with an empty path: no existing code reads true of it, and
|
||||
`not-an-adf-document` names the emitters' input (2026-10-05).
|
||||
|
||||
@@ -8,7 +8,10 @@ type Pending = string | { depth: number; value: JsonValue }
|
||||
|
||||
export function serializeCanonicalJson(value: JsonValue, spelling: JsonSpelling): string {
|
||||
const text: string[] = []
|
||||
writeCanonicalJson(value, spelling, (piece) => text.push(piece) > 0)
|
||||
writeCanonicalJson(value, spelling, (piece) => {
|
||||
text.push(piece)
|
||||
return true
|
||||
})
|
||||
return text.join('')
|
||||
}
|
||||
|
||||
|
||||
+6
-1
@@ -32,7 +32,7 @@ test('every ConvertErrorCode is the code of a production call site, and every ca
|
||||
assert.deepEqual(calledCodes(), declaredCodes())
|
||||
})
|
||||
|
||||
test('refuses where SpiderMonkey runs out of string, and lets any other throw through', () => {
|
||||
test('refuses where SpiderMonkey runs out of string, and lets a stack overflow or any other throw through', () => {
|
||||
const overflow = new Error('allocation size overflow')
|
||||
overflow.name = 'InternalError'
|
||||
assert.deepEqual(
|
||||
@@ -41,6 +41,11 @@ test('refuses where SpiderMonkey runs out of string, and lets any other throw th
|
||||
}),
|
||||
{ error: { code: 'unsupported-node-shape', message: 'the output runs past what this JavaScript engine holds: allocation size overflow', path: [] }, ok: false },
|
||||
)
|
||||
assert.throws(() =>
|
||||
withinEngineLimits(() => {
|
||||
throw new RangeError('Maximum call stack size exceeded')
|
||||
}),
|
||||
)
|
||||
assert.throws(() =>
|
||||
withinEngineLimits(() => {
|
||||
throw new TypeError('a bug')
|
||||
|
||||
+5
-2
@@ -42,12 +42,15 @@ export function success<T>(value: T): { ok: true; value: T } {
|
||||
return { ok: true, value }
|
||||
}
|
||||
|
||||
// Any string concatenation throws past the engine's longest string: RangeError in V8 and JavaScriptCore, InternalError in SpiderMonkey.
|
||||
// Measured past the longest string or array: V8 throws the first two, JavaScriptCore the third, SpiderMonkey the fourth. A stack overflow stays a throw.
|
||||
const engineLimitFaults = ['RangeError: Invalid array length', 'RangeError: Invalid string length', 'RangeError: Out of memory', 'InternalError: allocation size overflow']
|
||||
|
||||
// Any string concatenation can throw one, so this wraps a whole conversion.
|
||||
export function withinEngineLimits<T>(convert: () => Result<T>): Result<T> {
|
||||
try {
|
||||
return convert()
|
||||
} catch (error) {
|
||||
if (!(error instanceof RangeError || (error instanceof Error && error.name === 'InternalError'))) throw error
|
||||
if (!(error instanceof Error) || !engineLimitFaults.includes(`${error.name}: ${error.message}`)) throw error
|
||||
return failure('unsupported-node-shape', `the output runs past what this JavaScript engine holds: ${error.message}`, [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
`Bar = 9`
|
||||
|
||||
`Next ID = 106`
|
||||
`Next ID = 108`
|
||||
|
||||
| Goal | W |
|
||||
|---|---|
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 106 | 0.2.0 | defect | **Refuse a document or markdown whose spelling would pass a stated length before building it, so no call exhausts the heap.** | 4 | 6 | 5 | 8 | 1, 9 | 20.5 |
|
||||
| 103 | 0.2.0 | | **Fold `unspellable-whitespace` into `unsupported-node-shape`, so a line ending and a null character in a content slot take one code.** | 2 | 2 | 5 | 6 | 1 | 19.5 |
|
||||
| 84 | 0.2.0 | | **Read GFM's pipe table without leading pipes, and with alignment colons, in `portableMarkdownToAdf`, dropping the alignment.** | 3 | 4 | 7 | 7 | 4, 7 | 19.4 |
|
||||
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 6 | 11.5 |
|
||||
@@ -52,6 +53,7 @@
|
||||
| 81 | 0.3.0 | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 2 | 5 | 5 | 3 | 16.3 |
|
||||
| 46 | 0.3.0 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 4 | 5 | 5 | 3, 8 | 14.8 |
|
||||
| 72 | 0.3.0 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 2 | 1 | 3 | 6 | 3 | 13.1 |
|
||||
| 107 | 0.3.0 | decision | **Spell the block carry's JSON so its length stays linear in the node's JSON.** | 4 | 5 | 3 | 5 | 1 | 10.8 |
|
||||
| 51 | 0.3.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 4 | 2 | 7 | 4, 5 | 10.4 |
|
||||
| 95 | 0.3.0 | decision | **Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them.** | 5 | 6 | 2 | 6 | 1 | 9.5 |
|
||||
| 91 | 0.3.0 | | **Spell an emphasis or link once across the hard breaks between two nodes it marks, in both writers.** | 5 | 5 | 5 | 6 | 6 | 9.0 |
|
||||
@@ -71,6 +73,18 @@
|
||||
|
||||
## Details
|
||||
|
||||
### 106. Refuse a document or markdown whose spelling would pass a stated length before building it, so no call exhausts the heap.
|
||||
|
||||
The writers return `unsupported-node-shape` where the engine throws past its longest string, but
|
||||
some documents exhaust the heap first, which no catch survives. `losslessMarkdownToAdf` on 2.1 MB
|
||||
of markdown, a `!adf:blockquote` holding a list nested 490 levels whose paragraph runs on in 700,000
|
||||
lazy lines `b\`, crashed Node 24 out of heap: `directiveNode` re-spells the subtree, indenting each
|
||||
line to its depth. `adfToLosslessMarkdown` on nine text nodes sharing one 2^28-character string
|
||||
got Bun OOM-killed under 8 GB: `escape` in `line-escaping.ts` pushes one array element per
|
||||
character. The length to refuse past is a number no entry states yet (AGENTS.md §Stated numbers):
|
||||
V8's 2^29 − 24 on every engine, so the same document converts alike everywhere, is the candidate.
|
||||
Found by the principles audit of item 83, 2026-10-11.
|
||||
|
||||
### 103. Fold `unspellable-whitespace` into `unsupported-node-shape`, so a line ending and a null character in a content slot take one code.
|
||||
|
||||
`slotFault` in `directive-syntax.ts` gives a newline in an `emoji`, `mention` or `status` content
|
||||
@@ -295,6 +309,14 @@ Gitea copy holds no issues or releases to move. Actions stay off on the mirror:
|
||||
otherwise run there. When this lands, the `docs/decisions.md` entry "GitHub is canonical" loses its
|
||||
interim sentence.
|
||||
|
||||
### 107. Spell the block carry's JSON so its length stays linear in the node's JSON.
|
||||
|
||||
The block carry indents two spaces a level, so a carried node's JSON about 1 MB deep in nested
|
||||
arrays spells past V8's longest string, which the writers refuse as `unsupported-node-shape`; the
|
||||
inline carry's compact JSON would fit. §The code list calls a refusal no spelling recovers from a
|
||||
gap in the flavour. A format change: §The formats are API. Found by the README-goals audit of item
|
||||
83, 2026-10-11.
|
||||
|
||||
### 51. Match a reference label to its definition under Unicode case folding.
|
||||
|
||||
`link-syntax.ts` normalizes a label with `toLowerCase`, so `[ẞ]` misses its `[SS]` definition (spec
|
||||
|
||||
Reference in New Issue
Block a user