36c - §10 and §11's decisions move to docs/decisions.md
CI / gate (push) Successful in 43s
CI / publish (push) Has been cancelled

This commit is contained in:
2026-09-28 10:55:07 +02:00
parent 348865398c
commit d5dae87c9b
4 changed files with 256 additions and 149 deletions
+226 -2
View File
@@ -257,8 +257,8 @@ input reads `message`.
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 the no-recursion rule (`AGENTS.md` §11) forces,
whose empty half no input reaches. The message names the violation instead.
anywhere in the tree — wants the manual stack §Nothing recurses unbounded forces, whose empty
half no input reaches. The message names the violation instead.
## Publish on a version bump
@@ -291,3 +291,227 @@ is saved to validates it.
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
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 the suite, rather than a smoke import, is
what proves an engine.
The gate runs the 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 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 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.
## The corpus
2026-08-23, the maintainer. Goals 1 and 8. Valid while the round-trip is proved by example.
All checked in: hand-built fixtures per node and combination; real ADF Atlassian's editor wrote; the
CommonMark spec suite against `markdownToAdf` and `markdownToHtml`.
## 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. Goals 1 and 6. 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:
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/`. 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 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 (4c).
## A retry loop checks its own termination
2026-09-20, the maintainer. Goal 1. Valid while a line retries until a fallback spells it.
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).
## 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 (4c).
## 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 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.
## 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 is tried ahead of a
general one.
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
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.