25 - AGENTS.md §8 and §11 have sub-headings and say only what the code cannot #124

Merged
lilleman merged 2 commits from 25 into main 2026-09-23 23:38:27 +02:00
3 changed files with 114 additions and 105 deletions
Showing only changes of commit d0088fc922 - Show all commits
+105 -100
View File
@@ -113,72 +113,70 @@ Pre-1.0, normal 0.x rules. A spelled node's content model is part of that contra
container is the model, not the syntax — so giving a spelled node's model content it had not, or
taking it away, is MAJOR whatever ADF's own schema does.
The error surface is a contract too. `ConvertError` is `{ code, message, path, position? }` — the
code from a closed list a consumer may switch exhaustively, the message free text, the path the
node's place from the document root, the position where a parse read the refusal in its input.
A message names the violation, not the rule alone — a rule by itself states a truth the reader
must invert before it reads as a failure — and where the flavour's claim refuses ordinary prose it
names the escape that unclaims the form claimed: `\!adf:` for a directive, block line and inline
alike, `\|` for every pipe row.
Adding, removing or renaming a code is breaking, so a new cause takes an existing code whose
name reads true of it in both directions; where none does and a plain name exists, a new code — in
any 0.x minor, and after 1.0 only in a MAJOR (the maintainer, 2026-09-18). A refusal whose cause is
this library's own invariant rather than the input takes the existing code nearest what the consumer
sees — a document that does not convert is `unsupported-node-shape` — since a code no input reaches
is one no consumer can switch on (the maintainer, 2026-09-20). A code names the
cause; where one cause recurs across node types, across one mark's attributes or across
directions, one code covers them all and
`path` and `message` say which — `unsupported-nesting-depth` is the 500-level guard whichever
direction hits it, `unspellable-character` the text node and the code block alike. Where two codes stay
apart, the line between them is what they name: `unspellable-character` is a character CommonMark
rewrites wherever text holds it, `unspellable-whitespace` the newline no inline directive's
content slot spans, in either direction. A claim code names the spelling claimed, never the node that spelling would have built:
a malformed `!adf:table` is a `malformed-directive`, and an alignment colon a `malformed-pipe-table` —
the flavour's own delimiter row is `-` runs, so the grammar refuses the colon rather than ADF's
missing column model doing it. A refusal no spelling recovers from is a gap in the flavour rather
than a code: give the flavour the spelling and the code goes, which the freeze is the last moment
for — `unspellable-link` went at `0.2.0`, the directive link spelling the `href` and `title` it
refused and the attributes the carry held (the maintainer, 2026-09-13). A cause the carry answers gets no code: a mark no
spelling writes rides the carry with its node. A directive whose name reads back to no node is
`unknown-directive-name` rather than a claim code — the spelling is well formed, and telling that
apart from a typo is what a consumer switches on when a later MINOR gives the name meaning. A
reserved name is a known name, so never that code, and the two the flavour reserves part on form:
a form the grammar does not have is a claim code — `!adf:carry`, whose carry is the fence — and a
well-formed form in the wrong place is `unsupported-node-shape`, `!adf:listBreak` parting anything
but two adjacent lists of one type. What
the grammar itself refuses stays a claim code, key order among it, and a leaf given a body is refused
at its opener, as a container missing its closer is (the maintainer, 2026-09-16); a well-formed
directive the node tables refuse — an attribute a node does not hold or spells elsewhere, a value
outside its kind or its canonical spelling, an argument, or a body of a shape its content model does
not take — is
`unsupported-node-shape`, the emitter's code for the same mismatch read the other way — one code
across both directions for good, since the call site knows which direction it called and parting
them after `0.1.0` is MAJOR. `unmappable-html` names the version rather than the element: this one
converts no raw HTML, so at `0.2.0` the mapped elements stop erroring and the code stays for what
no ADF node carries. A refusal found before its path is known — the block walk's, a directive
reader's — is a `ConvertFault`, the code and message alone; the node walk attaches the path as it
descends, so a document reports its first error in document order. `not-an-adf-document` carries
the document's own path throughout: seven of the guard's eight branches read the document's own
shape, and threading a path to the eighth — a malformed node anywhere in the tree — wants the
manual stack §11's no-recursion rule forces, whose empty half no input reaches. The message names
the violation instead. An attribute value past 500 levels is `unsupported-nesting-depth` in both
directions, the guard included. An attribute is counted from its value; a spelling that nests it
deeper — the block directive's `marks`, the carry — refuses in its own format, as its parser does.
A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document`
emitting — no document holds one, so no round-trip crosses them.
The error surface is a contract too; `README.md` §The errors states it to the consumer, and the
types in `src/result.ts` hold its shape.
`position` is the parse side's alone: an emitter reads no source, so an emit error carries `path`
and nothing more. It is `{ line, offset }` at the start of the line the block holding the refusal
begins on — the offset indexing the string the caller passed, the line counted from 1 — minted by
the block walk and attached as results return, so the innermost block wins, the emitter's own
refusals the parser re-enters for the CommonMark spelling included.
### The code list
A parse names a position for every refusal it returns, so the type says so rather than the prose:
`Result<T, E extends ConvertError = ConvertError>`, and a direction reading a source returns
`Result<T, ParseError>` — `ConvertError` with `position` required. An optional field a direction
always fills is a branch a consumer cannot take, and the `!` §11 bans is how they take it anyway.
`htmlToAdf` inherits this at `0.2.0`; the composed `markdownToHtml` and `htmlToMarkdown` keep the
wide `Result<T>`, since half their refusals come from an emit stage that read no source.
- Adding, removing or renaming a code is breaking, so a new cause takes an existing code whose
name reads true of it in both directions; where none does and a plain name exists, a new code —
in any 0.x minor, and after 1.0 only in a MAJOR (the maintainer, 2026-09-18).
- A refusal whose cause is this library's own invariant rather than the input takes the existing
code nearest what the consumer sees — a document that does not convert is
`unsupported-node-shape` — since a code no input reaches is one no consumer can switch on (the
maintainer, 2026-09-20).
- A refusal no spelling recovers from is a gap in the flavour rather than a code: give the flavour
the spelling and the code goes, which the freeze is the last moment for (`unspellable-link`, the
maintainer, 2026-09-13). A cause the carry answers gets no code: a mark no spelling writes rides
the carry with its node.
- `unmappable-html` names the version rather than the element: this one converts no raw HTML, so
at `0.2.0` the mapped elements stop erroring and the code stays for what no ADF node carries.
### Which code a cause takes
- A code names the cause; where one cause recurs across node types, across one mark's attributes
or across directions, one code covers them all and `path` and `message` say which —
`unsupported-nesting-depth` is the 500-level guard whichever direction hits it,
`unspellable-character` the text node and the code block alike. Where two codes stay apart, the
line between them is what they name: `unspellable-character` is a character CommonMark rewrites
wherever text holds it, `unspellable-whitespace` the newline no inline directive's content slot
spans, in either direction.
- A claim code names the spelling claimed, never the node that spelling would have built: a
malformed `!adf:table` is a `malformed-directive`, and an alignment colon a
`malformed-pipe-table` — the flavour's own delimiter row is `-` runs, so the grammar refuses the
colon rather than ADF's missing column model doing it. What the grammar itself refuses stays a
claim code, key order among it, and a leaf given a body is refused at its opener, as a container
missing its closer is (the maintainer, 2026-09-16).
- A directive whose name reads back to no node is `unknown-directive-name` rather than a claim
code — the spelling is well formed, and telling that apart from a typo is what a consumer
switches on when a later MINOR gives the name meaning. A reserved name is a known name, so never
that code, and the two the flavour reserves part on form: a form the grammar does not have is a
claim code — `!adf:carry`, whose carry is the fence — and a well-formed form in the wrong place
is `unsupported-node-shape`, `!adf:listBreak` parting anything but two adjacent lists of one
type.
- A well-formed directive the node tables refuse — an attribute a node does not hold or spells
elsewhere, a value outside its kind or its canonical spelling, an argument, or a body of a shape
its content model does not take — is `unsupported-node-shape`, the emitter's code for the same
mismatch read the other way: one code across both directions for good, since the call site
knows which direction it called and parting them after `0.1.0` is MAJOR.
- An attribute is counted from its value; a spelling that nests it deeper — the block directive's
`marks`, the carry — refuses in its own format, as its parser does.
- A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document`
emitting — no document holds one, so no round-trip crosses them.
### `message`, `path` and `position`
- A message names the violation, not the rule alone — a rule by itself states a truth the reader
must invert before it reads as a failure — and where the flavour's claim refuses ordinary prose
it names the escape that unclaims the form claimed: `\!adf:` for a directive, block line and
inline alike, `\|` for every pipe row.
- `not-an-adf-document` carries the document's own path throughout: seven of the guard's eight
branches read the document's own shape, and threading a path to the eighth — a malformed node
anywhere in the tree — wants the manual stack §11's no-recursion rule forces, whose empty half
no input reaches. The message names the violation instead.
- A direction reading a source returns `Result<T, ParseError>`: an optional field a direction
always fills is a branch a consumer cannot take, and the `!` §11 bans is how they take it
anyway.
## 9. Release automation
@@ -283,29 +281,23 @@ someone spells it or pins it.
## 11. Code rules
### Style
- Two-space indent, strict TypeScript, English everywhere. Alphabetical order wherever order
carries no meaning, keyed on the name a line introduces rather than where it came from: an
import sorts on its first binding, type imports ahead of value imports, so moving or renaming a
module reorders nothing (the maintainer, 2026-09-18).
- Failures are values: everything returns
`Result<T>` — `{ ok: true; value } | { ok: false; error: ConvertError }` — nothing throws.
`try/catch` only wrapped tightly around a call that genuinely throws, converted to a result on
the spot. A reader with no path to name returns `Read<T>` instead, the same two arms over a
`ConvertFault`, and `faulted` attaches the path where the walk knows it.
- Only the hard break's inline segment holds a raw newline — every other spelling escapes one or
refuses it — which is how the whitespace carry finds a line edge.
- Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text
escapes wherever CommonMark could open or close with it, leaving the emitter's own delimiters the
only ones in play, and a pair that matching hands to another delimiter rides the carry instead.
`matchEmphasis` transcribes the reference `process_emphasis` line for line, and its closer walk and
opener search stay whole: broken into named steps they drift from the algorithm being faithful is
the whole point of.
- A readable spelling tried ahead of a general one takes the `try` prefix and fails only where the
general form fails on the same node (20): refusing there refuses a document the general form
spells, so a refusal the general form does not share belongs in the general form or nowhere. A
readable spelling that must spell its subtree before it can give way — the list, whose
thematic-break first line and blank lines exist only spelled — hands that one walk to the general
form instead: giving way after the walk walks again at every level, doubling per level (4b).
- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the
fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states
unrepresentable.
- Failures are values: everything returns `Result<T>`, nothing throws. `try/catch` only wrapped
tightly around a call that genuinely throws, converted to a result on the spot. A reader with no
path to name returns `Read<T>`, and the walk attaches the path where it knows it.
- Reuse before adding; the smallest sufficient diff is the benchmark; no speculative generality —
a second consumer, or it goes.
### Bounds
- Nothing recurses unbounded: the guards walk iteratively, and blocks, marks and JSON values — an
attribute's and a carried node's alike — are all held to 500 levels, so a deep document is a
`Result` rather than the stack overflow that waits near 2000. A level is one block-list
@@ -337,18 +329,39 @@ someone spells it or pins it.
ordered list past the marker cap gives way, spending two emitter levels where the parser spent
one. A give-way is kept too and serves any depth, reading the node's shape alone. Only what
succeeded is kept, so no path minted at another position is ever read.
- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the
fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states
unrepresentable.
### Spellings
- Only the hard break's inline segment holds a raw newline — every other spelling escapes one or
refuses it — which is how the whitespace carry finds a line edge.
- Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text
escapes wherever CommonMark could open or close with it, leaving the emitter's own delimiters the
only ones in play, and a pair that matching hands to another delimiter rides the carry instead.
`matchEmphasis` transcribes the reference `process_emphasis` line for line, and its closer walk and
opener search stay whole: broken into named steps they drift from the algorithm being faithful is
the whole point of.
- A readable spelling tried ahead of a general one takes the `try` prefix and fails only where the
general form fails on the same node (20): refusing there refuses a document the general form
spells, so a refusal the general form does not share belongs in the general form or nowhere. A
readable spelling that must spell its subtree before it can give way — the list, whose
thematic-break first line and blank lines exist only spelled — hands that one walk to the general
form instead: giving way after the walk walks again at every level, doubling per level (4b).
- The attribute vocabulary is ADF's: `adf/` walks it and narrows each value to its kind, and a
format spells the narrowed value. A spelling that re-checks the type is the check's second copy.
Reading a spelling back is the format's own: the reader sits beside the spelling it inverts, so
decode-respell-compare cannot drift, and each format writes its own — canonical JSON for a
number is the markdown flavour's choice, not ADF's.
### Layout
- `src/adf/` holds ADF's own knowledge, imports no format, and is where a construct both formats
read lives: the question is answered in ADF's vocabulary — a node type, an attribute kind, a
content model — and no delimiter, element name or escape reaches it. A helper that cannot answer
that way is two constructs, the ADF question there and the spelling in each format, the seam
`markAttributes` and `markSpellings` already draw; one that cannot be split is a gap to ask (§15).
`markdown/` and `html/` are peers: neither imports the other, and no third directory sits between
them. A primitive knowing neither ADF nor a format — `result.ts`, `json-value.ts`, `nesting.ts`,
`canonical-json.ts` — stays at `src/` root. A construct rises to `adf/` on its second consumer,
not in anticipation of one (the maintainer, 2026-09-21).
them. A primitive knowing neither ADF nor a format stays at `src/` root. A construct rises to
`adf/` on its second consumer, not in anticipation of one (the maintainer, 2026-09-21).
- Each format directory (`markdown/`, `html/`) parts into `emit/` (ADF→format) and `parse/`
(format→ADF), the rest of it holding what both directions read. A construct's reader lives there
beside the regex the emitter escapes against, so the two cannot drift; a reader with no emit
@@ -357,22 +370,14 @@ someone spells it or pins it.
alike — whether a list marker interrupts a paragraph — is one function there too, never a copy
per direction, however conservative the copy would be. Where the rule is the emitter's own
choice, input consults it rather than restating it, and that is the only import `parse/` takes
from `emit/`: the parser asks `commonMarkSpelling` which form the emitter picks, and
`openingLinkTakesDirective` whether the line a paragraph's opening link starts forces the
directive link, so no fixture the emitter writes can be refused, and a spelling the emitter
refuses gives its own error rather than a second name for it.
- The attribute vocabulary is ADF's: `adf/` walks it and narrows each value to its kind, and a
format spells the narrowed value. A spelling that re-checks the type is the check's second copy.
Reading a spelling back is the format's own: the reader sits beside the spelling it inverts, so
decode-respell-compare cannot drift, and each format writes its own — canonical JSON for a
number is the markdown flavour's choice, not ADF's.
from `emit/` — `commonMarkSpelling` and `openingLinkTakesDirective` — so no fixture the emitter
writes can be refused, and a spelling the emitter refuses gives its own error rather than a
second name for it.
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`); a
file does not repeat its directory in its name — `adf/document.ts`, never
`adf/adf-document.ts`. A name is the noun `spec/flavour.md` or ADF's schema uses for the
thing; a directory follows a split the spec draws; a placement these rules leave open goes
beside its only reader, or in what both read where there are two (the maintainer, 2026-09-18).
- Reuse before adding; the smallest sufficient diff is the benchmark; no speculative generality —
a second consumer, or it goes.
## 12. Prose to a minimum
+8
View File
@@ -1044,6 +1044,14 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
executable, and they read as leftovers. Give them one, so `src/` root shows what it holds.
**Done** (2026-09-23): `src/conformance/` holds the six gate tests and the property harness,
so `src/` root holds the primitives and the entrypoint.
- [x] **25 — AGENTS.md §8 and §11 are findable (`0.2.0`).** Both are single unindexed paragraphs
holding the answer to nearly every question the panel had, and six readers and both
architects independently reported that finding the sentence cost more than reading the code
it governed. Sub-headings or an index at each section's head; delete whatever the code,
a type or a test name already says rather than reorganising it.
**Done** (2026-09-23): §8 parts into the code list, which code a cause takes, and `message`,
`path` and `position`, as bullets one rule each; §11 into style, bounds, spellings and
layout. What `src/result.ts`, `README.md` §The errors and `ls src` already say went.
## 5 — Ship `0.1.0`
+1 -5
View File
@@ -57,11 +57,7 @@ chunk clearing a §11 seam.
the measurement repeatable, or state the number the floor may be raised to and why it is not
the measured one.
- [x] **24 — The conformance gates have a directory (`0.2.0`).**
- [ ] **25 — AGENTS.md §8 and §11 are findable (`0.2.0`).** Both are single unindexed paragraphs
holding the answer to nearly every question the panel had, and six readers and both
architects independently reported that finding the sentence cost more than reading the code
it governed. Sub-headings or an index at each section's head; delete whatever the code,
a type or a test name already says rather than reorganising it.
- [x] **25 — AGENTS.md §8 and §11 are findable (`0.2.0`).**
- [ ] **26 — The two mutable structures say what they guarantee (`0.2.0`).** `escapedIndexes` fills
the `escaped` set left to right while the predicates it calls read the half-built set, then
`escapeClosedRuns` walks the same set right to left and adds to it; the order is load-bearing