From ebe4092292aa23b7c2d005c3edbc8c44077cdff9 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 07:38:07 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20product-owner=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=204=20=F0=9F=9F=A1=20(0=20misread);=20correct=20t?= =?UTF-8?q?he=20README=20claims,=20name=20the=20getter=20remedy,=20record?= =?UTF-8?q?=20the=20name=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 ++-- CHANGELOG.md | 7 ++++--- MIGRATION.md | 7 +++++-- README.md | 20 ++++++++++---------- docs/decisions.md | 32 ++++++++++++++++++++------------ 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fce1059..e5acf35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,7 +73,7 @@ In `docs/decisions.md`: - HTML writes a URL as the ADF holds it, whatever its scheme - Nothing recurses unbounded - JSON text is read by `parseJsonText` -- A call reads its input once into a plain copy +- An ADF document is read once into a plain copy - Nothing spreads an unbounded array - A retry loop checks its own termination - After 32 passes, a line carries or drops every em, strike and strong mark @@ -134,7 +134,7 @@ parenthesized value set reading `string`; fenced examples are skipped. Keep pros - Two-space indent, English everywhere. Alphabetical order wherever order carries no meaning, keyed on the name a line introduces: an import sorts on its first binding, type imports ahead of value imports, so moving or renaming a module reorders nothing. -- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the +- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a reader validating the fields it claims (`readAdfDocument`); past it everything is typed. Make invalid states unrepresentable. - Failures are values: everything returns `Result`, nothing throws. `try/catch` only wrapped diff --git a/CHANGELOG.md b/CHANGELOG.md index fab32ab..8e76d48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,10 @@ - **Breaking:** `readAdfDocument` replaces `isAdfDocument`: it returns a `Result` whose error names the branch that refused, and `MIGRATION.md` shows the change. - `readAdfDocument`, `adfToHtml`, `adfToLosslessMarkdown` and `adfToPortableMarkdown` read the - document once into a plain copy, so a document holding an accessor property, or a Proxy that - throws, returns `not-an-adf-document` where it ran the getter, looped or threw. A document of - more than 10,000,000 JSON values returns `unsupported-node-shape`. + document once into a plain copy. A document holding an accessor property, or a Proxy that throws, + returns `not-an-adf-document`, where `0.3.1` ran the getter, looped or threw. The copy keeps own + enumerable keys alone, so an inherited or non-enumerable key drops. A document of more than + 10,000,000 JSON values returns `unsupported-node-shape`. ## 0.3.1 diff --git a/MIGRATION.md b/MIGRATION.md index 610e0a2..77574f8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -9,6 +9,7 @@ | --- | --- | | `if (isAdfDocument(value)) convert(value)` | `const read = readAdfDocument(value); if (read.ok) convert(read.value)` | | `const valid = isAdfDocument(value)` | `const valid = readAdfDocument(value).ok` | +| `docs.filter(isAdfDocument)` | `docs.filter((value) => readAdfDocument(value).ok)` | Convert `read.value`: it is a copy that shares no object with `value`. The copy keeps own enumerable keys, as `JSON.stringify` does, so an inherited or non-enumerable key drops. A document @@ -16,8 +17,10 @@ nesting past 500 levels still reads; the converters refuse it as `unsupported-ne `readAdfDocument`, `adfToHtml`, `adfToLosslessMarkdown` and `adfToPortableMarkdown` read a document once into that copy. A document holding an accessor property, or a Proxy that throws, is -`not-an-adf-document` in `0.4.0`, where `0.3.1` ran the getter or threw. A document holding more -than 10,000,000 JSON values is `unsupported-node-shape`. +`not-an-adf-document` in `0.4.0`, where `0.3.1` ran the getter, looped or threw. A document of a +class that keeps its fields behind getters (Vue 2, MobX) is refused too: hand in a snapshot, +`JSON.parse(JSON.stringify(document))`. A document holding more than 10,000,000 JSON values is +`unsupported-node-shape`. ## From `0.2.0` to `0.3.0` diff --git a/README.md b/README.md index 952da89..632bb26 100644 --- a/README.md +++ b/README.md @@ -333,12 +333,11 @@ Each call's name says which format it reads or writes: | `htmlToAdf` | reads the HTML `adfToHtml` writes, also as a browser's `innerHTML` or a form post spells it, a carry element from its `data-adf`; refuses any other HTML, an edit to what a carry element shows included | `engine-limit`, `not-a-string`, `unmappable-html`, `unsupported-nesting-depth`, `unsupported-node-shape` | | `losslessMarkdownToAdf` | reads the lossless flavour, markdown `adfToLosslessMarkdown` wrote, edited or not | every code in [The errors](#the-errors) except `not-an-adf-document` and `unsupported-document-version` | | `portableMarkdownToAdf` | reads CommonMark, GFM's pipe tables and `~~`, alerts and task markers, Obsidian's callouts and `==`; a directive and an `adf:` fence read as text and code | `engine-limit`, `malformed-pipe-table`, `not-a-string`, `unmappable-html`, `unsupported-nesting-depth`, `unsupported-node-shape` | -| `readAdfDocument` | reads ADF as JSON, a value you hold from a parser or a store, into an `AdfDocument`, and writes nothing | `not-an-adf-document`, `unsupported-node-shape` | +| `readAdfDocument` | checks a parsed value you hold from a parser or a store, never JSON text, and returns a copy of it as an `AdfDocument`; writes nothing | `engine-limit`, `not-an-adf-document`, `unsupported-node-shape` | -`readAdfDocument` reads the JSON you receive into an `AdfDocument`, copying it once. It reads data -properties and own enumerable keys alone, as `JSON.stringify` does, so an accessor property or a -Proxy that throws is `not-an-adf-document` and never a throw, and the three writers read their -document the same way. It checks the shape the converters require: `type: 'doc'`, a finite +`readAdfDocument` reads the JSON you receive into an `AdfDocument`, copying it once. It copies own +enumerable keys and data properties alone, so an accessor property or a Proxy that throws is +`not-an-adf-document` and never a throw. The three writers read their document the same way. It checks the shape the converters require: `type: 'doc'`, a finite `version` (the converters take only 1), nodes holding only `attrs`, `content`, `marks`, `text` and `type`, attribute values that are JSON values, and no object holding itself. It reads a document nesting past 500 levels; the converters refuse that one with `unsupported-nesting-depth`. @@ -611,16 +610,17 @@ Which reader raises which code: [The calls](#the-calls). Codes every direction r ### Emitting `adfToLosslessMarkdown`, `adfToPortableMarkdown` and `adfToHtml` raise these, and the codes under -[Either direction](#either-direction): +[Either direction](#either-direction). `readAdfDocument` raises `not-an-adf-document` and +`unsupported-node-shape`, and `engine-limit`. | Code | Fires when | What you can do | | --- | --- | --- | -| `not-an-adf-document` | the value handed in is no ADF document — a missing or wrong `type`, a stray key, a node that is not a node, an object in the document holding itself, an accessor property, a Proxy that throws, an attribute value that is not a plain object, array or JSON primitive, such as a `Date` or `Map` | read the boundary you receive JSON at with `readAdfDocument`; the message names the branch that refused. Build an attribute value as JSON holds it: `date.toISOString()`, `Object.fromEntries(map)`, `{ ...instance }` | +| `not-an-adf-document` | the value handed in is no ADF document — a missing or wrong `type`, a stray key, a node that is not a node, an object in the document holding itself, an accessor property, a Proxy that throws, an attribute value that is not a plain object, array or JSON primitive, such as a `Date` or `Map` | read the boundary you receive JSON at with `readAdfDocument`; the message names the branch that refused. Where your objects hold getters (Vue 2, MobX, a class model), hand in a snapshot, `JSON.parse(JSON.stringify(document))`. Build an attribute value as JSON holds it: `date.toISOString()`, `Object.fromEntries(map)`, `{ ...instance }` | | `unsupported-document-version` | the document's `version` is not 1 | keep the ADF and pass the document over, or show it read-only; the version is the site's, not yours to change | ### Either direction -Every call can raise these: +Every call can raise `engine-limit`, and every conversion `unsupported-nesting-depth`: | Code | Fires when | What you can do | | --- | --- | --- | @@ -737,8 +737,8 @@ Serves [Goals](#goals) 1, 4 and 5. - Past that and `~~`, `losslessMarkdownToAdf` reads no GFM: an autolink literal and a `- [ ]` marker stay text, and a checklist is the `taskList` directive — `portableMarkdownToAdf` turns the marker into a `taskList`. -- A document nested deeper than 500 levels is an error result, not a stack overflow, and no call - loops forever on a string, or on a value `JSON.parse` or `structuredClone` builds. +- A document nested deeper than 500 levels is an error result from every conversion, not a stack + overflow, and no call loops forever on a string, or on any value you hand it. - The emitted formats are semver surface ([The formats are API](https://github.com/larvit/adf-codec/blob/main/docs/decisions.md#the-formats-are-api)). diff --git a/docs/decisions.md b/docs/decisions.md index 2a51ad4..921186d 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -963,21 +963,29 @@ can read an escaped key as another key it read before, and the round-trip then r output. A test fails on `JSON.parse(` in a shipped file. The tests' own oracles keep `JSON.parse`, since a misread there fails the gate and never hides a bug. -## A call reads its input once into a plain copy +## An ADF document is read once into a plain copy -2026-10-04, the maintainer; the 10 million 2026-10-05; the code and the name 2026-11-09, applying -§Which code a cause takes and the entry for the long output. Goals 1 and 9. Valid while a persona -can hand in a value with accessors or a Proxy. +2026-10-04, the maintainer. The 10 million 2026-10-05, applying §Output past V8's longest string is +refused on every engine. The code, 2026-11-09, applying §Which code a cause takes. The name by a +developer panel, 2026-11-09. Goals 1 and 9. Valid while a persona can hand in a value with +accessors or a Proxy. `readAdfDocument` and the three writers copy the input once, before any check, reading property descriptors so no getter runs: an accessor property, or a Proxy trap that throws, is `not-an-adf-document`. The copy keeps own enumerable string keys, as `JSON.stringify` does, so an -inherited or non-enumerable key drops, and reads an array by index, so no iterator runs. It refuses -a cycle by the objects above the one it copies. Every later step reads the copy. The copy takes at -most 10 million JSON values, each object, array, string, number, boolean and null counting one, -past which the document is `unsupported-node-shape`, since the document does not convert; an array -claiming more elements than the values left is refused before it is read. Goal 9's size counts ADF -as JSON, so a shared object costs once per place it is held. +inherited or non-enumerable key drops, and reads an array by index, so no iterator runs. Every +later step reads the copy. + +A cycle is an object that holds one of the objects above it on the walk's path; the copy refuses +it. The copy takes at most 10 million JSON values, each object, array, string, number, boolean and +null counting one, past which the document is `unsupported-node-shape`, since the document does +not convert. An array whose length passes the values still free is refused before any element is +read. Goal 9's size counts ADF as JSON, so a shared object costs once per place it is held. The +width check on an object's keys is the array's, and no test pins it: a Proxy would need to return +10 million key names to reach it. + +The panel read 5 of 7 for `readAdfDocument` over `parseAdfDocument`, which two read as taking JSON +text, and over `validateAdfDocument`, `checkAdfDocument`, `toAdfDocument` and `adfDocumentFrom`. ## Nothing spreads an unbounded array @@ -1093,8 +1101,8 @@ rather than a figure. A cost fix that changes no behaviour lands on the suite staying green with no fixture output changed, and a before-and-after figure in its PR; the gate times nothing. Measured and kept: -`adfDocumentFault`'s shape and depth walks stay two — the parting gives depth its own code — at -52 ms for a 9 MB document the emit takes 314 ms over. The 500-level guard runs after the block walk, +The shape walk of `readAdfDocument` and the depth walk of `writableContent` stay two — the parting +gives depth its own code — at 52 ms for a 9 MB document the emit takes 314 ms over. The 500-level guard runs after the block walk, so it doesn't bound the walk's cost. ## Only the hard break holds a raw newline