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
- 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<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
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
+5 -2
View File
@@ -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`
+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` |
| `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)).
+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`,
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