36c - §10 and §11's decisions move to docs/decisions.md #138
@@ -35,6 +35,24 @@ In `docs/decisions.md`:
|
|||||||
- Publish on a version bump
|
- Publish on a version bump
|
||||||
- Docs describe the release being built
|
- Docs describe the release being built
|
||||||
- No schema validation
|
- No schema validation
|
||||||
|
- The gate runs on Deno and Bun
|
||||||
|
- The gate installs the tarball
|
||||||
|
- Firefox reads the build
|
||||||
|
- The coverage floors
|
||||||
|
- The size ratchet
|
||||||
|
- Properties on a fixed seed
|
||||||
|
- The flavour spec is read as a source
|
||||||
|
- The node tables answer to Atlassian's schema
|
||||||
|
- Nothing recurses unbounded
|
||||||
|
- Nothing spreads an unbounded array
|
||||||
|
- A retry loop checks its own termination
|
||||||
|
- Readers scan by index
|
||||||
|
- The spelling memo
|
||||||
|
- Only the hard break holds a raw newline
|
||||||
|
- Emphasis follows CommonMark's matching
|
||||||
|
- Readable spellings take the `try` prefix
|
||||||
|
- The attribute vocabulary is ADF's
|
||||||
|
- The source parts by ADF and format
|
||||||
|
|
||||||
## 7. Nothing about any consumer
|
## 7. Nothing about any consumer
|
||||||
|
|
||||||
@@ -54,35 +72,8 @@ against the README's personas.
|
|||||||
|
|
||||||
Test for the behaviour wanted first, then implement until green. `node --test`, beside the code.
|
Test for the behaviour wanted first, then implement until green. `node --test`, beside the code.
|
||||||
Node, tsc and npm never run on the host — only via the pinned images (§9). Tests are independent,
|
Node, tsc and npm never run on the host — only via the pinned images (§9). Tests are independent,
|
||||||
containers are torn down after a run.
|
containers are torn down after a run. A test reaches only for what Node's, Deno's and Bun's `node:`
|
||||||
|
shims all carry.
|
||||||
The gate runs that same suite under Deno and Bun as well as Node, the three images pinned alike,
|
|
||||||
and neither extra leg is Node's proof twice. Deno refuses an extensionless or directory specifier,
|
|
||||||
so it holds the module graph to the fully-spelled form a browser can load; Bun runs
|
|
||||||
JavaScriptCore, the one engine of the three that is not V8, where the Unicode property escapes
|
|
||||||
emphasis matching leans on can disagree. Both refuse a run matching no test, so Node's is the only
|
|
||||||
vacuous-green guard, and a test may reach only for what all three `node:` shims carry — the price
|
|
||||||
of proving those engines over the corpus rather than over a smoke import.
|
|
||||||
|
|
||||||
The gate then packs the build and installs the tarball under `package-tests/`, so `files`,
|
|
||||||
`exports` and `types` are proved on the artifact that ships rather than on the source tree a
|
|
||||||
self-reference would resolve against. `consumer.ts` typechecks the emitted `.d.ts` from outside
|
|
||||||
`tsconfig.build.json` — declaration emit leaves the `.ts` specifiers
|
|
||||||
`rewriteRelativeImportExtensions` rewrites in the JavaScript, and this is what says a consumer's
|
|
||||||
resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` stays unproven.
|
|
||||||
`node-floor.js` round-trips the installed package under a Node pinned to `engines.node`'s floor.
|
|
||||||
|
|
||||||
A fourth engine reads the build rather than the source: a headless Firefox loads `dist/index.js`
|
|
||||||
over HTTP and converts the round-trip, normalization and error fixtures and the real payloads — the
|
|
||||||
`commonmark-spec` sort is the Node suite's to check — which is the browser half of
|
|
||||||
`docs/decisions.md` §Any ES2022 engine and the only SpiderMonkey there is — the gate's other three
|
|
||||||
engines are two V8s and a JavaScriptCore that is not Safari's. A WebDriver session is what carries a
|
|
||||||
verdict back out, the driver and the page's server sharing one network namespace so each is the
|
|
||||||
other's `127.0.0.1`; `--headless --screenshot` has no such channel, and loading `dist/index.js` in a
|
|
||||||
globals-stripped realm buys one by not running a browser. The leg re-checks the conversions and
|
|
||||||
nothing else — each fixture's emitted markdown, its parsed document, its error code — leaving the
|
|
||||||
corpus's pairing, uniqueness, source positions and byte-level equality to the Node suite that owns
|
|
||||||
them.
|
|
||||||
|
|
||||||
Every leg announces its name and, where a container is in play, the image, before it runs and its
|
Every leg announces its name and, where a container is in play, the image, before it runs and its
|
||||||
elapsed time after, `publish.sh` alongside `ci.sh`, so a long run reads as progress rather than as
|
elapsed time after, `publish.sh` alongside `ci.sh`, so a long run reads as progress rather than as
|
||||||
@@ -92,49 +83,16 @@ calls. A leg whose output is both streamed and grepped keeps the copy in a `mkte
|
|||||||
file: `tee /dev/stderr` reopens fd 2, and under `./ci.sh > log 2>&1` the two offsets punch NUL
|
file: `tee /dev/stderr` reopens fd 2, and under `./ci.sh > log 2>&1` the two offsets punch NUL
|
||||||
holes through each other's lines (4d).
|
holes through each other's lines (4d).
|
||||||
|
|
||||||
The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and
|
`PROPERTY_RUNS=<runs>` raises the property runs and randomizes the seed for local digging. The
|
||||||
functions, and a branch floor that only ever moves upward. It sits below 100 because the guards
|
generators and run parameters properties share live in `src/conformance/property-harness.ts`,
|
||||||
`noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index
|
outside the build and coverage.
|
||||||
compared against `undefined` — have a half no valid document reaches.
|
|
||||||
|
|
||||||
The size ratchet is the other such number, `.oxlintrc.json`'s single rule over the files
|
Each `- ` bullet in `spec/flavour.md`'s `## Block nodes`, `## Inline nodes` and `## Marks` declares
|
||||||
`tsconfig.build.json` builds, measured by `oxlint` since TypeScript 7 is a native compiler
|
the nodes named before its first em dash, with the attributes following `Attributes: ` — a
|
||||||
publishing no in-process parser, only the `unstable/` AST surface an out-of-process handshake
|
parenthesized value set reading `string`; fenced examples are skipped. Keep prose out of a bullet.
|
||||||
reaches. It is a per-function line ceiling, set at that set's worst and moving only downward. It
|
|
||||||
covers the built files alone, since one ceiling over the tests too would have to be their worst,
|
|
||||||
loosening the guard over the shipped code. It guards against drift and never drives a refactor, so
|
|
||||||
no cyclomatic rule and no second lint rule join it: neither measure picked out what nine readers
|
|
||||||
found hard (the comprehension panel, 2026-09-20). Three switches guard a silent green: `IIFEs:
|
|
||||||
true`, since oxlint exempts an IIFE otherwise; an explicit `-c`, so a config gone missing fails the
|
|
||||||
leg instead of falling back to oxlint's own defaults; and `--deny-warnings`, since a rule from a
|
|
||||||
category this config never names arrives as a warning it exits 0 on.
|
|
||||||
|
|
||||||
The corpus, all checked in: hand-built fixtures per node and combination; real ADF Atlassian's
|
|
||||||
editor wrote; the CommonMark spec suite against `markdownToAdf` and `markdownToHtml`.
|
|
||||||
|
|
||||||
Beside the corpus, properties run over documents generated from the node tables and over generated
|
|
||||||
markdown, on a fixed seed in the gate; `PROPERTY_RUNS=<runs>` raises the runs and randomizes the
|
|
||||||
seed for local digging, and a counterexample found becomes a round-trip fixture. The generators and
|
|
||||||
run parameters properties share live in `src/conformance/property-harness.ts`, outside the build and coverage.
|
|
||||||
|
|
||||||
`spec/flavour.md` is read as a source too, so the node tables cannot drift from the prose they
|
|
||||||
copy: each `- ` bullet in `## Block nodes`, `## Inline nodes` and `## Marks` declares the nodes
|
|
||||||
named before its first em dash, with the attributes following `Attributes: ` — a parenthesized
|
|
||||||
value set reading `string` — and must equal the tables in `adf/`. Keep prose in those sections out
|
|
||||||
of a bullet; fenced examples are skipped. It guards the attributes alone: nodes that differ in
|
|
||||||
content model share a bullet, and the argument attribute is spelled ahead of `Attributes: `, so
|
|
||||||
both answer to the round-trip corpus and to nothing else where a node has no fixture.
|
|
||||||
|
|
||||||
The tables answer to Atlassian's schema too (`docs/decisions.md` §Standards ship as data): for every
|
|
||||||
node and mark they spell, the attribute names and kinds equal what `full.json` and `stage-0.json`
|
|
||||||
hold between them. Value sets stay documentation, since any value round-trips. What the schema holds
|
|
||||||
and the tables do not spell is pinned by name — an attribute as a gap, a type as carried — so a
|
|
||||||
re-pin adding either goes red until someone spells it or pins it.
|
|
||||||
|
|
||||||
## 11. Code rules
|
## 11. Code rules
|
||||||
|
|
||||||
### Style
|
|
||||||
|
|
||||||
- Two-space indent, English everywhere. Alphabetical order wherever order
|
- Two-space indent, English everywhere. Alphabetical order wherever order
|
||||||
carries no meaning, keyed on the name a line introduces rather than where it came from: an
|
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
|
import sorts on its first binding, type imports ahead of value imports, so moving or renaming a
|
||||||
@@ -147,84 +105,12 @@ re-pin adding either goes red until someone spells it or pins it.
|
|||||||
path to name returns `Read<T>`, and the walk attaches the path where it knows it.
|
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 —
|
- Reuse before adding; the smallest sufficient diff is the benchmark; no speculative generality —
|
||||||
a second consumer, or it goes.
|
a second consumer, or it goes.
|
||||||
|
- Explicit over implicit; descriptive names; no catch-all files (`utils`, `helpers`, `misc`); a file
|
||||||
### Bounds
|
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
|
||||||
- Nothing recurses unbounded: the guards walk iteratively, and blocks, marks and JSON values — an
|
spec draws; a placement neither this section nor `docs/decisions.md` §The source parts by ADF and
|
||||||
attribute's and a carried node's alike — are all held to 500 levels (`largestNesting`), so a
|
format settles goes beside its only reader, or in what both read where there are two (the
|
||||||
deep document is a `Result` rather than the stack overflow that waits near 2000. An attribute
|
maintainer, 2026-09-18).
|
||||||
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 list giving way to the directive form
|
|
||||||
refuses at zero headroom rather than walking again; counting every list twice halved the list
|
|
||||||
limit, counting the directive form once doubled the parser's frames per level (the maintainer,
|
|
||||||
2026-09-18).
|
|
||||||
- Nothing spreads an unbounded array into a call — a node's siblings, a code block's held lines, a
|
|
||||||
mark run's segments: the argument list caps near 125k and throws a `RangeError` where a `Result`
|
|
||||||
is owed. A walk pushes one at a time. A literal spread (`[...value]`) is not the same thing and
|
|
||||||
is fine (4c).
|
|
||||||
- A loop retrying an input until a fallback spells it refuses the pass taking no fallback, so its
|
|
||||||
termination is the loop's own check (28).
|
|
||||||
- A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the
|
|
||||||
line before it reads, `indexOf` — never a fresh slice per character, and a per-character walk
|
|
||||||
hoists the scan that does not vary with the character. The pipeline persona feeds documents
|
|
||||||
nobody typed, and a megabyte through a quadratic walk is a minute rather than a millisecond. A
|
|
||||||
scan may keep what it read for a later walk of the same text, and the fallback where it kept
|
|
||||||
nothing must be the same reader over the same text at the same index, so the two cannot disagree
|
|
||||||
— which is what makes the kept value a memo rather than a second spelling (4c).
|
|
||||||
- The parse keeps each node's readable spelling in a memo, so the `commonMarkSpelling` ask stops
|
|
||||||
spelling a node once per level above it (18). The node reference is the key, which holds
|
|
||||||
because the parse builds one object per position; `adfToMarkdown` passes no memo, where a
|
|
||||||
consumer's document may hold one node at two positions (4b). `text` 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, so no path minted at another position is ever read.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
- 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 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
|
|
||||||
counterpart goes in `parse/`, unless it is part of a construct that side already holds — a grammar
|
|
||||||
stays in one file rather than splitting across the seam. A rule both directions must answer
|
|
||||||
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/` — `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).
|
|
||||||
|
|
||||||
## 12. Prose to a minimum
|
## 12. Prose to a minimum
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ In priority order.
|
|||||||
document and returns a whole result.
|
document and returns a whole result.
|
||||||
9. **Fast once correct.** Conversion time grows linearly with the document wherever the goals above
|
9. **Fast once correct.** Conversion time grows linearly with the document wherever the goals above
|
||||||
allow it; a faster path that risks one of them is not taken.
|
allow it; a faster path that risks one of them is not taken.
|
||||||
|
10. **Source a contributor can hold.** Any one function reads in one sitting, and no change makes
|
||||||
|
the longest one longer.
|
||||||
|
|
||||||
## Audience
|
## Audience
|
||||||
|
|
||||||
|
|||||||
+216
-3
@@ -16,7 +16,7 @@ a backslash reach them intact; what the flavour cannot spell reduces ADF→ADF a
|
|||||||
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything
|
`markdownToAdf(adfToMarkdown(doc))` and `htmlToAdf(adfToHtml(doc))` must equal `doc` — anything
|
||||||
less silently destroys content an editor could not represent, in a document it did not author.
|
less silently destroys content an editor could not represent, in a document it did not author.
|
||||||
When losslessness and readability conflict, losslessness wins. Round-trip equality is a property
|
When losslessness and readability conflict, losslessness wins. Round-trip equality is a property
|
||||||
tested over a corpus, not a claim made in prose.
|
tested over a checked-in corpus (`corpus/README.md`), not a claim made in prose.
|
||||||
|
|
||||||
## Markdown in is a canonical fixpoint
|
## Markdown in is a canonical fixpoint
|
||||||
|
|
||||||
@@ -257,8 +257,8 @@ input reads `message`.
|
|||||||
inline alike, `\|` for every pipe row.
|
inline alike, `\|` for every pipe row.
|
||||||
- `not-an-adf-document` carries the document's own path throughout: seven of the guard's eight
|
- `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
|
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 the no-recursion rule (`AGENTS.md` §11) forces,
|
anywhere in the tree — wants the manual stack §Nothing recurses unbounded forces, whose empty
|
||||||
whose empty half no input reaches. The message names the violation instead.
|
half no input reaches. The message names the violation instead.
|
||||||
|
|
||||||
## Publish on a version bump
|
## Publish on a version bump
|
||||||
|
|
||||||
@@ -291,3 +291,216 @@ is saved to validates it.
|
|||||||
No ADF schema validation or exported validator. A refusal that keeps the round-trip is not schema
|
No ADF schema validation or exported validator. A refusal that keeps the round-trip is not schema
|
||||||
validation, so the one a spelled node carrying the same mark type twice earns stays, and input
|
validation, so the one a spelled node carrying the same mark type twice earns stays, and input
|
||||||
nesting a spelling inside its own kind (`*(*a*)*`) names that mark once.
|
nesting a spelling inside its own kind (`*(*a*)*`) names that mark once.
|
||||||
|
|
||||||
|
## The gate runs on Deno and Bun
|
||||||
|
|
||||||
|
2026-09-01, the maintainer. Goals 7 and 8. Valid while Bun is the gate's only engine that is
|
||||||
|
not V8.
|
||||||
|
|
||||||
|
The gate runs the suite under Deno and Bun as well as Node, and neither extra leg is Node's proof
|
||||||
|
twice. Deno refuses an extensionless or directory specifier, so it holds the module graph to the
|
||||||
|
fully-spelled form a browser can load; Bun runs JavaScriptCore, the one engine of the three that is
|
||||||
|
not V8, where the Unicode property escapes emphasis matching leans on can disagree. Both refuse a
|
||||||
|
run matching no test, so Node's is the only vacuous-green guard, and `AGENTS.md` §10's `node:` shims
|
||||||
|
rule is the price of proving those engines over the corpus rather than over a smoke import.
|
||||||
|
|
||||||
|
## The gate installs the tarball
|
||||||
|
|
||||||
|
2026-09-03, the maintainer. Goal 7. Valid while consumers install the packed package.
|
||||||
|
|
||||||
|
The gate packs the build and installs the tarball under `package-tests/`, so `files`, `exports`
|
||||||
|
and `types` are proved on the artifact that ships rather than on the source tree a self-reference
|
||||||
|
would resolve against. `consumer.ts` typechecks the emitted `.d.ts` from outside
|
||||||
|
`tsconfig.build.json` — declaration emit leaves the `.ts` specifiers
|
||||||
|
`rewriteRelativeImportExtensions` rewrites in the JavaScript, and this is what says a consumer's
|
||||||
|
resolver maps them, under `NodeNext` alone; a `.d.ts` reader that is not `tsc` stays unproven.
|
||||||
|
`node-floor.js` round-trips the installed package under a Node pinned to `engines.node`'s floor.
|
||||||
|
|
||||||
|
## Firefox reads the build
|
||||||
|
|
||||||
|
2026-09-04, the maintainer. Goal 7. Valid while the library claims a browser and no other leg runs
|
||||||
|
SpiderMonkey.
|
||||||
|
|
||||||
|
A headless Firefox loads `dist/index.js` over HTTP and converts the round-trip, normalization and
|
||||||
|
error fixtures and the real payloads — the `commonmark-spec` sort is the Node suite's to check —
|
||||||
|
which is the browser half of §Any ES2022 engine and the only SpiderMonkey there is — the gate's
|
||||||
|
other engines are two V8s and a JavaScriptCore that is not Safari's. A WebDriver session is what
|
||||||
|
carries a verdict back out, the driver and the page's server sharing one network namespace so each
|
||||||
|
is the other's `127.0.0.1`; `--headless --screenshot` has no such channel, and loading
|
||||||
|
`dist/index.js` in a globals-stripped realm buys one by not running a browser. The leg re-checks
|
||||||
|
the conversions and nothing else — each fixture's emitted markdown, its parsed document, its error
|
||||||
|
code — leaving the corpus's pairing, uniqueness, source positions and byte-level equality to the
|
||||||
|
Node suite that owns them.
|
||||||
|
|
||||||
|
## The coverage floors
|
||||||
|
|
||||||
|
2026-08-24, the maintainer. Goal 1. Valid while `noUncheckedIndexedAccess` and ADF's optional keys
|
||||||
|
force guards with a half no valid document reaches.
|
||||||
|
|
||||||
|
The floors live in the `test` script, so `npm test` and the gate are one path: 100% of lines and
|
||||||
|
functions, and a branch floor that only ever moves upward. It sits below 100 because the guards
|
||||||
|
`noUncheckedIndexedAccess` and ADF's optional keys force — `?? []`, `?? {}`, `?.`, an index
|
||||||
|
compared against `undefined` — have a half no valid document reaches.
|
||||||
|
|
||||||
|
## The size ratchet
|
||||||
|
|
||||||
|
2026-09-20, the maintainer. Goal 10. Valid while no measure picks out what readers find hard
|
||||||
|
better than a function's length.
|
||||||
|
|
||||||
|
`.oxlintrc.json`'s single rule, over the files `tsconfig.build.json` builds, is a per-function line
|
||||||
|
ceiling, set at that set's worst and moving only downward. It covers the built files alone, since
|
||||||
|
one ceiling over the tests too would have to be their worst, loosening the guard over the shipped
|
||||||
|
code. It guards against drift and never drives a refactor, so no cyclomatic rule and no second lint
|
||||||
|
rule join it: neither measure picked out what nine readers found hard (the comprehension panel,
|
||||||
|
2026-09-20). `oxlint` measures it since TypeScript 7 is a native compiler publishing no in-process
|
||||||
|
parser, only the `unstable/` AST surface an out-of-process handshake reaches. Three switches guard
|
||||||
|
a silent green: `IIFEs: true`, since oxlint exempts an IIFE otherwise; an explicit `-c`, so a
|
||||||
|
config gone missing fails the leg instead of falling back to oxlint's own defaults; and
|
||||||
|
`--deny-warnings`, since a rule from a category this config never names arrives as a warning it
|
||||||
|
exits 0 on.
|
||||||
|
|
||||||
|
## Properties on a fixed seed
|
||||||
|
|
||||||
|
2026-09-14, the maintainer. Goal 1. Valid while a red gate must reproduce.
|
||||||
|
|
||||||
|
Beside the corpus, properties run over documents generated from the node tables and over generated
|
||||||
|
markdown, on a fixed seed in the gate; a counterexample found becomes a round-trip fixture.
|
||||||
|
|
||||||
|
## The flavour spec is read as a source
|
||||||
|
|
||||||
|
2026-09-01, the maintainer. Goal 1. Valid while `spec/flavour.md` restates the node tables in
|
||||||
|
prose.
|
||||||
|
|
||||||
|
`spec/flavour.md` is read as a source, so the node tables cannot drift from the prose they copy:
|
||||||
|
its node and mark bullets must equal the tables in `adf/`. It guards the attributes alone: nodes
|
||||||
|
that differ in content model share a bullet, and the argument attribute is spelled outside the
|
||||||
|
bullet's attribute list, so both answer to the round-trip corpus and to nothing else where a node
|
||||||
|
has no fixture.
|
||||||
|
|
||||||
|
## The node tables answer to Atlassian's schema
|
||||||
|
|
||||||
|
2026-09-13, the maintainer. Goal 1. Valid while a site's editor writes what Atlassian's schema
|
||||||
|
holds.
|
||||||
|
|
||||||
|
For every node and mark the tables spell, the attribute names and kinds equal what `full.json` and
|
||||||
|
`stage-0.json` (§Standards ship as data) hold between them. Value sets stay documentation, since
|
||||||
|
any value round-trips. What the schema holds and the tables do not spell is pinned by name — an
|
||||||
|
attribute as a gap, a type as carried — so a re-pin adding either goes red until someone spells it
|
||||||
|
or pins it.
|
||||||
|
|
||||||
|
## Nothing recurses unbounded
|
||||||
|
|
||||||
|
2026-08-25, the directive form's count 2026-09-18, the maintainer. Goal 1. Valid while an engine's
|
||||||
|
stack overflows near 2000 frames.
|
||||||
|
|
||||||
|
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 (`largestNesting`), so a deep document is a `Result`
|
||||||
|
rather than the stack overflow that waits near 2000. 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 list giving way to the directive form refuses at zero headroom rather
|
||||||
|
than walking again; counting every list twice halved the list limit, counting the directive form
|
||||||
|
once doubled the parser's frames per level.
|
||||||
|
|
||||||
|
## Nothing spreads an unbounded array
|
||||||
|
|
||||||
|
2026-09-18, the maintainer. Goal 1. Valid while engines cap a call's arguments.
|
||||||
|
|
||||||
|
Nothing spreads an unbounded array into a call — a node's siblings, a code block's held lines, a
|
||||||
|
mark run's segments: the argument list caps near 125k and throws a `RangeError` where a `Result` is
|
||||||
|
owed. A walk pushes one at a time. A literal spread (`[...value]`) is not the same thing and is
|
||||||
|
fine.
|
||||||
|
|
||||||
|
## A retry loop checks its own termination
|
||||||
|
|
||||||
|
2026-09-20, the maintainer. Goal 1. Valid while a fallback can fail to spell what it is handed.
|
||||||
|
|
||||||
|
A loop retrying an input until a fallback spells it refuses the pass taking no fallback, so its
|
||||||
|
termination is the loop's own check.
|
||||||
|
|
||||||
|
## Readers scan by index
|
||||||
|
|
||||||
|
2026-08-30, the kept scan 2026-09-18, the maintainer. Goal 9. Valid while the pipeline persona
|
||||||
|
feeds documents nobody typed.
|
||||||
|
|
||||||
|
A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the line
|
||||||
|
before it reads, `indexOf` — never a fresh slice per character, and a per-character walk hoists the
|
||||||
|
scan that does not vary with the character: a megabyte through a quadratic walk is a minute rather
|
||||||
|
than a millisecond. A scan may keep what it read for a later walk of the same text, and the
|
||||||
|
fallback where it kept nothing must be the same reader over the same text at the same index, so the
|
||||||
|
two cannot disagree — which is what makes the kept value a memo rather than a second spelling.
|
||||||
|
|
||||||
|
## The spelling memo
|
||||||
|
|
||||||
|
2026-09-19, the maintainer. Goal 9. Valid while the `commonMarkSpelling` ask spells a node once
|
||||||
|
per level above it otherwise.
|
||||||
|
|
||||||
|
The parse and the plain reduction keep each node's readable spelling in a memo, so the
|
||||||
|
`commonMarkSpelling` ask stops spelling a node once per level above it. `text` 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, so no path minted at another position is ever read.
|
||||||
|
|
||||||
|
## Only the hard break holds a raw newline
|
||||||
|
|
||||||
|
2026-08-26, the maintainer. Goal 1. Valid while the whitespace carry finds a line edge by its raw
|
||||||
|
newline.
|
||||||
|
|
||||||
|
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 follows CommonMark's matching
|
||||||
|
|
||||||
|
2026-08-27, the maintainer. Goals 1 and 8. Valid while CommonMark's emphasis rules are the
|
||||||
|
reader's.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Readable spellings take the `try` prefix
|
||||||
|
|
||||||
|
2026-09-21, the maintainer. Goals 1 and 4. Valid while a readable spelling's refusal would cost a
|
||||||
|
document the general form spells.
|
||||||
|
|
||||||
|
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: 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.
|
||||||
|
|
||||||
|
## The attribute vocabulary is ADF's
|
||||||
|
|
||||||
|
2026-08-27, the maintainer. Goal 2. Valid while every format spells the same ADF attributes.
|
||||||
|
|
||||||
|
`adf/` walks the attribute vocabulary 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.
|
||||||
|
|
||||||
|
## The source parts by ADF and format
|
||||||
|
|
||||||
|
2026-08-27, `adf/`'s bar 2026-09-21, the maintainer. Goal 2. Valid while each format has a reader
|
||||||
|
and a writer through ADF.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
`markdown/` and `html/` are peers: neither imports the other, and no third directory sits between
|
||||||
|
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.
|
||||||
|
|
||||||
|
Each format directory 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 counterpart goes in `parse/`,
|
||||||
|
unless it is part of a construct that side already holds — a grammar stays in one file rather than
|
||||||
|
splitting across the seam. A rule both directions must answer 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/` —
|
||||||
|
`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.
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ type BlockSpelling = 'commonmark' | 'directive' | 'list'
|
|||||||
type EmittedBlock = { headroom: number; spelling: BlockSpelling; text: string }
|
type EmittedBlock = { headroom: number; spelling: BlockSpelling; text: string }
|
||||||
type KeptSpelling = { block: EmittedBlock | undefined; depth: number }
|
type KeptSpelling = { block: EmittedBlock | undefined; depth: number }
|
||||||
type PlacedBlock = Omit<EmittedBlock, 'headroom'> & { node: AdfNode }
|
type PlacedBlock = Omit<EmittedBlock, 'headroom'> & { node: AdfNode }
|
||||||
|
// Keyed by reference: only a caller building one object per position (the parse, the plain reduction) passes one; a consumer's document may share a node.
|
||||||
export type SpellingMemo = Map<AdfNode, KeptSpelling>
|
export type SpellingMemo = Map<AdfNode, KeptSpelling>
|
||||||
type Walk = { blocks: readonly PlacedBlock[]; headroom: number }
|
type Walk = { blocks: readonly PlacedBlock[]; headroom: number }
|
||||||
type WalkedItem = { node: AdfNode; walk: Walk }
|
type WalkedItem = { node: AdfNode; walk: Walk }
|
||||||
@@ -104,7 +105,7 @@ function readableBlock(node: AdfNode, path: ConvertErrorPath, depth: number, mem
|
|||||||
const kept = memo?.get(node)
|
const kept = memo?.get(node)
|
||||||
if (kept !== undefined) {
|
if (kept !== undefined) {
|
||||||
if (kept.block === undefined) return undefined
|
if (kept.block === undefined) return undefined
|
||||||
// A read below the fill would skip the depth guards the walk it replaces runs (AGENTS.md §11).
|
// A read below the fill would skip the depth guards the walk it replaces runs (docs/decisions.md §The spelling memo).
|
||||||
if (depth <= kept.depth) return success({ ...kept.block, headroom: kept.block.headroom + kept.depth - depth })
|
if (depth <= kept.depth) return success({ ...kept.block, headroom: kept.block.headroom + kept.depth - depth })
|
||||||
}
|
}
|
||||||
const spelled = spellReadableBlock(node, path, depth, memo)
|
const spelled = spellReadableBlock(node, path, depth, memo)
|
||||||
|
|||||||
@@ -7,11 +7,9 @@
|
|||||||
one no goal serves is proposed as a goal and asked. Sources: `AGENTS.md`'s body, the settled text
|
one no goal serves is proposed as a goal and asked. Sources: `AGENTS.md`'s body, the settled text
|
||||||
in this file's items, and `todo-history.md`, deleted with the bare `(28)` citations into it once
|
in this file's items, and `todo-history.md`, deleted with the bare `(28)` citations into it once
|
||||||
nothing cites it. Split by `AGENTS.md` section where one chunk is too big.
|
nothing cites it. Split by `AGENTS.md` section where one chunk is too big.
|
||||||
- **36c — Move §10 and §11's decisions.** The engine legs, floors, size ratchet, bounds, spellings
|
- **36d — Move the settled text in `todo.md`'s items and §11 and §15's dated rules, and point
|
||||||
and layout; the style rules stay working rules.
|
§15's "the rule that closes it, landing here" at `docs/decisions.md`.** `AGENTS.md`'s sections
|
||||||
- **36d — Move the settled text in `todo.md`'s items and §15's dated rules, and point §15's
|
are renumbered once only working rules remain, their citations with them.
|
||||||
"the rule that closes it, landing here" at `docs/decisions.md`.** `AGENTS.md`'s sections are
|
|
||||||
renumbered once only working rules remain, their citations with them.
|
|
||||||
- **36e — Move `todo-history.md`'s decisions, re-point its citations and delete it.**
|
- **36e — Move `todo-history.md`'s decisions, re-point its citations and delete it.**
|
||||||
- **35 — Read and write plain markdown as a flavour of the markdown grammar.** Per Goal 2 and
|
- **35 — Read and write plain markdown as a flavour of the markdown grammar.** Per Goal 2 and
|
||||||
`docs/decisions.md` §Plain markdown is a flavour of the grammar, `plainMarkdownToAdf` is
|
`docs/decisions.md` §Plain markdown is a flavour of the grammar, `plainMarkdownToAdf` is
|
||||||
@@ -64,19 +62,23 @@
|
|||||||
`plainMarkdownToAdf` reads through `markdownToAdf`'s parser and so inherits whatever this set
|
`plainMarkdownToAdf` reads through `markdownToAdf`'s parser and so inherits whatever this set
|
||||||
accepts.
|
accepts.
|
||||||
- **7 — Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed
|
- **7 — Ship HTML: `adfToHtml`, `htmlToAdf`, and `markdownToHtml` / `htmlToMarkdown` composed
|
||||||
through ADF.** CommonMark spec suite runs against `markdownToHtml` from here (§10). The README's
|
through ADF.** CommonMark spec suite runs against `markdownToHtml` from here. The README's
|
||||||
tagline and `package.json`'s `description` regain HTML (5g).
|
tagline and `package.json`'s `description` regain HTML (5g).
|
||||||
- **31 — Make the branch figure the coverage floor is read against repeatable.** Three Node test
|
- **31 — Make the branch figure the coverage floor is read against repeatable.** Three Node test
|
||||||
legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and 96.23%, and the
|
legs over one unchanged tree reported `emit/inline-line.ts` at 95.83%, 96.23% and 96.23%, and the
|
||||||
total at 98.80%, 98.84% and 98.84% (2026-09-21). `--experimental-test-coverage` counts branches
|
total at 98.80%, 98.84% and 98.84% (2026-09-21). `--experimental-test-coverage` counts branches
|
||||||
off V8's own coverage, which the runner's parallel files and V8's optimization make run-dependent,
|
off V8's own coverage, which the runner's parallel files and V8's optimization make run-dependent,
|
||||||
so the number the floor is read against is not the code's alone. The floor of 98 holds today on
|
so the number the floor is read against is not the code's alone. The floor of 98 holds today on
|
||||||
0.8 points of slack and §10 says it only ever moves upward, so the first raise to the measured
|
0.8 points of slack and `docs/decisions.md` §The coverage floors says it only ever moves upward,
|
||||||
figure reddens a run that changed nothing. Make the measurement repeatable, or state the number
|
so the first raise to the measured figure reddens a run that changed nothing. Make the
|
||||||
the floor may be raised to and why it is not the measured one.
|
measurement repeatable, or state the number the floor may be raised to and why it is not the
|
||||||
|
measured one.
|
||||||
|
- **37 — State what the Deno leg proves that Node's does not, or drop it.** `docs/decisions.md` §The
|
||||||
|
gate runs on Deno and Bun credits Deno with holding the module graph to fully-spelled specifiers,
|
||||||
|
which Node already refuses under `"type": "module"`, and `tsc` under `NodeNext`.
|
||||||
- **33 — Make a carried mark run cost the line one re-emit.** `adfToMarkdown` spends 23 s on one
|
- **33 — Make a carried mark run cost the line one re-emit.** `adfToMarkdown` spends 23 s on one
|
||||||
paragraph of 2000 × `un` plus `**-r**`: each run its flanking cannot spell re-emits the whole line
|
paragraph of 2000 × `un` plus `**-r**`: each run its flanking cannot spell re-emits the whole line
|
||||||
before riding the carry, quadratic in the runs (§11 Bounds), and the plain reduction's
|
before riding the carry, quadratic in the runs (Goal 9), and the plain reduction's
|
||||||
`spellableLine` drops one mark per re-emit the same way. Make both linear.
|
`spellableLine` drops one mark per re-emit the same way. Make both linear.
|
||||||
- **34 — Read emphasis flanking by the whole character beside an astral symbol.** Check whether
|
- **34 — Read emphasis flanking by the whole character beside an astral symbol.** Check whether
|
||||||
`line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an astral
|
`line-escaping.ts`'s `charAt` and the parser's flanking read one UTF-16 unit beside an astral
|
||||||
|
|||||||
Reference in New Issue
Block a user