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

Answer product-owner round 1: 0 🔴, 4 🟡 (0 misread); correct the README claims, name the getter remedy, record the name panel

This commit is contained in:
2026-11-09 07:38:07 +01:00
committed by Lillem4n
parent d255b17d37
commit ebe4092292
5 changed files with 41 additions and 29 deletions
+2 -2
View File
@@ -73,7 +73,7 @@ In `docs/decisions.md`:
- HTML writes a URL as the ADF holds it, whatever its scheme - HTML writes a URL as the ADF holds it, whatever its scheme
- Nothing recurses unbounded - Nothing recurses unbounded
- JSON text is read by `parseJsonText` - 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 - Nothing spreads an unbounded array
- A retry loop checks its own termination - A retry loop checks its own termination
- After 32 passes, a line carries or drops every em, strike and strong mark - 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, - 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 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. 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 fields it claims (`readAdfDocument`); past it everything is typed. Make invalid states
unrepresentable. unrepresentable.
- Failures are values: everything returns `Result<T>`, nothing throws. `try/catch` only wrapped - Failures are values: everything returns `Result<T>`, nothing throws. `try/catch` only wrapped
+4 -3
View File
@@ -5,9 +5,10 @@
- **Breaking:** `readAdfDocument` replaces `isAdfDocument`: it returns a `Result<AdfDocument>` whose - **Breaking:** `readAdfDocument` replaces `isAdfDocument`: it returns a `Result<AdfDocument>` whose
error names the branch that refused, and `MIGRATION.md` shows the change. error names the branch that refused, and `MIGRATION.md` shows the change.
- `readAdfDocument`, `adfToHtml`, `adfToLosslessMarkdown` and `adfToPortableMarkdown` read the - `readAdfDocument`, `adfToHtml`, `adfToLosslessMarkdown` and `adfToPortableMarkdown` read the
document once into a plain copy, so a document holding an accessor property, or a Proxy that document once into a plain copy. A document holding an accessor property, or a Proxy that throws,
throws, returns `not-an-adf-document` where it ran the getter, looped or threw. A document of returns `not-an-adf-document`, where `0.3.1` ran the getter, looped or threw. The copy keeps own
more than 10,000,000 JSON values returns `unsupported-node-shape`. 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 ## 0.3.1
+5 -2
View File
@@ -9,6 +9,7 @@
| --- | --- | | --- | --- |
| `if (isAdfDocument(value)) convert(value)` | `const read = readAdfDocument(value); if (read.ok) convert(read.value)` | | `if (isAdfDocument(value)) convert(value)` | `const read = readAdfDocument(value); if (read.ok) convert(read.value)` |
| `const valid = isAdfDocument(value)` | `const valid = readAdfDocument(value).ok` | | `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 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 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 `readAdfDocument`, `adfToHtml`, `adfToLosslessMarkdown` and `adfToPortableMarkdown` read a document
once into that copy. A document holding an accessor property, or a Proxy that throws, is 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 `not-an-adf-document` in `0.4.0`, where `0.3.1` ran the getter, looped or threw. A document of a
than 10,000,000 JSON values is `unsupported-node-shape`. 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` ## From `0.2.0` to `0.3.0`
+10 -10
View File
@@ -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` | | `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` | | `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` | | `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 `readAdfDocument` reads the JSON you receive into an `AdfDocument`, copying it once. It copies own
properties and own enumerable keys alone, as `JSON.stringify` does, so an accessor property or a enumerable keys and data properties alone, so an accessor property or a Proxy that throws is
Proxy that throws is `not-an-adf-document` and never a throw, and the three writers read their `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
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 `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 `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`. 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 ### Emitting
`adfToLosslessMarkdown`, `adfToPortableMarkdown` and `adfToHtml` raise these, and the codes under `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 | | 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 | | `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 ### 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 | | 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 `- [ ]` - 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 stay text, and a checklist is the `taskList` directive — `portableMarkdownToAdf` turns the
marker into a `taskList`. marker into a `taskList`.
- A document nested deeper than 500 levels is an error result, not a stack overflow, and no call - A document nested deeper than 500 levels is an error result from every conversion, not a stack
loops forever on a string, or on a value `JSON.parse` or `structuredClone` builds. overflow, and no call loops forever on a string, or on any value you hand it.
- The emitted formats are semver surface - 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)). ([The formats are API](https://github.com/larvit/adf-codec/blob/main/docs/decisions.md#the-formats-are-api)).
+20 -12
View File
@@ -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`, 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. 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 2026-10-04, the maintainer. The 10 million 2026-10-05, applying §Output past V8's longest string is
§Which code a cause takes and the entry for the long output. Goals 1 and 9. Valid while a persona refused on every engine. The code, 2026-11-09, applying §Which code a cause takes. The name by a
can hand in a value with accessors or a Proxy. 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 `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 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 `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 inherited or non-enumerable key drops, and reads an array by index, so no iterator runs. Every
a cycle by the objects above the one it copies. Every later step reads the copy. The copy takes at later step reads the copy.
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 A cycle is an object that holds one of the objects above it on the walk's path; the copy refuses
claiming more elements than the values left is refused before it is read. Goal 9's size counts ADF it. The copy takes at most 10 million JSON values, each object, array, string, number, boolean and
as JSON, so a shared object costs once per place it is held. 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 ## 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 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: 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 The shape walk of `readAdfDocument` and the depth walk of `writableContent` stay two — the parting
52 ms for a 9 MB document the emit takes 314 ms over. The 500-level guard runs after the block walk, 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. so it doesn't bound the walk's cost.
## Only the hard break holds a raw newline ## Only the hard break holds a raw newline