36c - §10 and §11's decisions move to docs/decisions.md
This commit is contained in:
@@ -35,6 +35,25 @@ In `docs/decisions.md`:
|
||||
- Publish on a version bump
|
||||
- Docs describe the release being built
|
||||
- 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
|
||||
- The corpus
|
||||
- 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
|
||||
|
||||
@@ -54,35 +73,8 @@ against the README's personas.
|
||||
|
||||
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,
|
||||
containers are torn down after a run.
|
||||
|
||||
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.
|
||||
containers are torn down after a run. A test reaches only for what Node's, Deno's and Bun's `node:`
|
||||
shims all carry.
|
||||
|
||||
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
|
||||
@@ -92,49 +84,15 @@ 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
|
||||
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
|
||||
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.
|
||||
`PROPERTY_RUNS=<runs>` raises the property runs and randomizes the seed for local digging. The
|
||||
generators and run parameters properties share live in `src/conformance/property-harness.ts`,
|
||||
outside the build and coverage.
|
||||
|
||||
The size ratchet is the other such number, `.oxlintrc.json`'s single rule over the files
|
||||
`tsconfig.build.json` builds, measured by `oxlint` since TypeScript 7 is a native compiler
|
||||
publishing no in-process parser, only the `unstable/` AST surface an out-of-process handshake
|
||||
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.
|
||||
Keep prose in `spec/flavour.md`'s `## Block nodes`, `## Inline nodes` and `## Marks` out of a `- `
|
||||
bullet.
|
||||
|
||||
## 11. Code rules
|
||||
|
||||
### Style
|
||||
|
||||
- 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
|
||||
import sorts on its first binding, type imports ahead of value imports, so moving or renaming a
|
||||
@@ -147,79 +105,6 @@ 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.
|
||||
- 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 (`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 (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
|
||||
|
||||
Reference in New Issue
Block a user