3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-13 12:18:28 +01:00

Answer architecture round 3: 0 🔴, 0 🟡; an out-of-memory throw is engine-limit whatever the engine ran short of, the ruling reads same

This commit is contained in:
2026-10-13 17:42:11 +02:00
committed by Lillem4n
parent a0fdfe377e
commit d13a17565b
5 changed files with 61 additions and 8 deletions
+2 -3
View File
@@ -10,9 +10,8 @@
- **Breaking:** the markdown readers refuse a value that is not a string with the new code
`not-a-string`; `MIGRATION.md` lists what each value did before.
- **Breaking:** every call returns the new code `engine-limit` where the JavaScript engine passes
its longest array, `Map` or `Set`, or finds no memory for a buffer, and, on JavaScriptCore, where
its heap runs short or a string passes its longest. A retry may succeed. Every other refusal stays
deterministic.
its longest array, `Map` or `Set`, or reports itself out of memory, and, on JavaScriptCore, where
a string passes its longest. A retry may succeed. Every other refusal stays deterministic.
- **Breaking:** an attribute value that is not a plain object, array or JSON primitive, such as a
`Date`, `Map`, typed array or class instance, makes `isAdfDocument` return `false` and the
converters refuse it as `not-an-adf-document`. Before, the converters wrote it as `{}` or its own
+1 -1
View File
@@ -114,4 +114,4 @@ with one of the four now reads, or refuses with the code the table gives.
| `null`, `undefined` or a non-empty array, on parse | throws a `TypeError` | `not-a-string` |
| a `String` object, on parse | the document its text spells | `not-a-string`; pass `String(value)` |
| any other value that is not a string, such as a number or `{}`, on parse | an empty document | `not-a-string` |
| a conversion past the JavaScript engine's longest array, `Map` or `Set`, or finding no memory for a buffer, and on JavaScriptCore one running its heap short or past its longest string | throws | `engine-limit`; retry it once |
| a conversion past the JavaScript engine's longest array, `Map` or `Set`, or that the engine reports as out of memory, and on JavaScriptCore one past its longest string | throws | `engine-limit`; retry it once |
+3 -3
View File
@@ -412,8 +412,8 @@ A call reports the first refusal in document order and stops, so a document with
them one per call. Every refusal is deterministic — there is no I/O anywhere — so a retry returns
the identical error: fix the input, or set the document aside. The exception is `engine-limit`,
whose causes include memory running short: retry it once, and treat a repeat as lasting. On V8
(Node, Deno, Chrome) a heap that runs out ends the process instead, so convert input of unbounded
size in a worker.
(Node, Deno, Chrome) a heap that runs out ends the process instead. In Node, a `worker_threads`
worker given `resourceLimits` ends alone, so convert input of unbounded size in one.
### Parsing
@@ -445,7 +445,7 @@ The emitter raises these, and so does a parse, which asks the emitter how to spe
| Code | Fires when | What you can do |
| --- | --- | --- |
| `engine-limit` | in every call: a conversion that passes the longest array, `Map` or `Set` the JavaScript engine builds, or that finds no memory for a buffer; on JavaScriptCore (Bun, Safari), also one that runs its heap short or passes its longest string, 2^31 − 1 characters | retry once; if it repeats, writing: set the document aside, keeping the ADF and passing it over or showing it read-only; reading: shorten the markdown, or nest its lists and quotes less deeply |
| `engine-limit` | in every call: a conversion that passes the longest array, `Map` or `Set` the JavaScript engine builds, or that the engine reports as out of memory; on JavaScriptCore (Bun, Safari), also one that passes its longest string, 2^31 − 1 characters | retry once; if it repeats, writing: set the document aside, keeping the ADF and passing it over or showing it read-only; reading: shorten the markdown, or nest its lists and quotes less deeply |
| `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 one that, written out as plain CommonMark, would be longer than 536870888 characters, 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 `adfToPortableMarkdown`: a line or task list the portable writer cannot spell, a bug in this library. Emitting, in both writers: a document whose markdown would be longer than 536870888 characters, V8's longest string, on every engine; in `adfToPortableMarkdown`, also a document whose text alone is that long, edge whitespace included. In every call: a conversion that passes V8's or SpiderMonkey's longest string | 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. For a portable line or task list, report it as a bug. Past 536870888 characters or the engine's longest string, writing: set the document aside, keeping the ADF and passing it over or showing it read-only; reading: shorten the markdown, or nest its lists and quotes less deeply |
+54
View File
@@ -742,3 +742,57 @@ Inherited architect, decided by:
- Navigation: `src/markdown/length-bound.ts:37` `withinEngineLimits`: the engine guard moves out of the Result module, and engine-limit leads from the symptom to it
- Shape, Locality: `src/markdown/portable/adf-to-portable-markdown.ts:128` `portableSequence`: a refusal propagates with its block's path, where it was silently swallowed
- Shape: `src/canonical-json.ts:23` `canonicalJsonPieces`: a generator, emitLeaf's one refusal and PlacedSegment each read simpler than before
## 2026-10-13T15:41:57Z, PR #37 at f1ea9fd, against 7a27f6b
Ruling: same
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|---|---|---|---|---|---|
| Mid A | better | worse | better | same | same |
| Inherited architect | better | worse | same | same | worse |
| Maintainability senior | better | worse | better | same | same |
| Junior A | better | worse | better | same | same |
Mid A, decided by:
- Locality: `src/markdown/parse/markdown-to-adf.ts:266` `directiveNode`: a second engine guard inside the parse's own, explained by how each engine overflows
- Locality: `src/markdown/emit/adf-to-markdown.ts:127` `readableBlock`: a caller flag decides the fill and every read deletes, so what a later call sees depends on who called first
- Locality: `src/markdown/emit/adf-to-markdown.ts:66` `walkBlocks`: the length bound is checked at four points, each relying on the others to cover what it skips
- Navigation, Shape: `src/markdown/length-bound.ts:37` `withinEngineLimits`: engine and length refusals have their own file and code, and result.ts is back to types
- Shape: `src/markdown/length-bound.ts:17` `tooLong`: a string overflow keeps unsupported-node-shape while array and memory take engine-limit, explained only by a comment
- Shape, Locality: `src/markdown/portable/adf-to-portable-markdown.ts:128` `portableSequence`: a failed spelling returns early with each block's own path, where it was swallowed
- Shape: `src/markdown/emit/adf-to-markdown.ts:117` `commonMarkSpelling`: returns Result<boolean>, which says what it means, where undefined and success(null) encoded the answer
- Shape: `src/canonical-json.ts:23` `canonicalJsonPieces`: a generator replaces a boolean early-stop callback
- Shape: `src/markdown/emit/line-escaping.ts:113` `backslashed`: PlacedSegment drops repeated cursor arithmetic, at the cost of byte codes, chunked joins and RunEscapes
Inherited architect, decided by:
- Locality: `src/markdown/emit/adf-to-markdown.ts:126` `readableBlock`: a read deletes and only an asked caller fills, so whether a node is spelled again depends on call order; the first place to rot
- Locality, Navigation: `src/markdown/length-bound.ts:36` `withinEngineLimits`: wrap-every-builder is a project-wide duty, and six sites refuse with one message and path [], so a report cannot say which fired
- Navigation: `src/markdown/length-bound.ts:1` `withinEngineLimits`: the size concern has a file named for it and engine refusals their own code
- Locality: `src/markdown/parse/markdown-to-adf.ts:265` `directiveNode`: a nested engine guard mid-walk, there only to place V8's throw at this directive
- Shape: `src/markdown/emit/adf-to-markdown.ts:117` `commonMarkSpelling`: Result<boolean> tells the truth and a refusal now propagates with its path; line 134's conditional is dense
- Shape: `src/markdown/emit/line-escaping.ts:40` `RunEscapes`: byte flags, codes and batched joins read less directly, and `escapable` reads close to backwards; PlacedSegment is a real gain
Maintainability senior, decided by:
- Navigation, Shape: `src/markdown/length-bound.ts:37` `withinEngineLimits`: guard, bound and refusals share one file, and result.ts is back to describing results
- Locality: `src/markdown/emit/adf-to-markdown.ts:127` `readableBlock`: deletes on every read and writes only when asked, so a hit depends on call order across the parser, the portable reduction and the walk
- Locality, Self-sufficiency: `src/markdown/parse/markdown-to-adf.ts:266` `directiveNode`: a check's place is justified by where V8 would throw, with a nested engine guard, so moving one needs V8's behaviour reconstructed
- Locality: `src/markdown/portable/adf-to-portable-markdown.ts:128` `portableSequence`: a refused spelling comes back as a value with its block's path, where it was swallowed
- Shape: `src/markdown/emit/adf-to-markdown.ts:117` `commonMarkSpelling`: Result<boolean> where absence coded both answers
- Shape: `src/markdown/length-bound.ts:17` `tooLong`: a length refusal under unsupported-node-shape with path [], and the README row grown into a catch-all
- Shape: `src/markdown/emit/line-escaping.ts:113` `backslashed`: byte codes and chunked joins against PlacedSegment removing duplicated cursor arithmetic, about even
- Shape: `src/canonical-json.ts:23` `canonicalJsonPieces`: a generator replaces a boolean-returning callback
- Locality: `src/markdown/emit/inline-line.ts:235` `emitLeaf`: the content-or-text refusal is hoisted once, a small gain
Junior A, decided by:
- Navigation, Shape: `src/markdown/length-bound.ts:37` `withinEngineLimits`: the engine-overflow catch moves out of result.ts into a file named for the job, with the two message lists saying which throw means what
- Locality: `src/markdown/emit/adf-to-markdown.ts:127` `readableBlock`: a read deletes and a caller flag decides the store, an order dependence across three files
- Locality: `src/markdown/parse/markdown-to-adf.ts:266` `directiveNode`: a nested engine guard to pin V8's throw, and the bound checked in five places
- Shape, Locality: `src/markdown/emit/adf-to-markdown.ts:117` `commonMarkSpelling`: Result<boolean> matches its name, and portableSequence propagates a refusal with each block's path
- Shape: `src/markdown/emit/line-escaping.ts:38` `PlacedSegment`: replaces a parallel placements array and hand-advanced cursor, against byte codes and chunked joins
- Shape: `src/canonical-json.ts:23` `canonicalJsonPieces`: a generator replaces a write callback threading a keep-going flag
- Shape: `src/markdown/emit/inline-line.ts:175` `heldContentOrText`: the content-or-text refusal checked once in emitLeaf
+1 -1
View File
@@ -761,7 +761,7 @@ nodes into its own text, so keeping them would make every level of a deep subtre
whole text (2026-10-11). `text`, `alternateText` and `spelling` carry no depth and `headroom` is
affine in it, so a read at or 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. Only what succeeded is kept,
cap gives way, spending two emitter levels where the parser spent one. A refusal is never kept,
so no path minted at another position is ever read.
## Cost fixes are measured, never timed