Prose round 3: retitle the floor decision, put the plain/ exception first, sharpen items 59-62
CI / gate (push) Successful in 57s
CI / publish (push) Has been skipped

This commit is contained in:
2026-10-03 16:11:56 +02:00
parent b39525cad7
commit 7f67998c3b
5 changed files with 26 additions and 27 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ In `docs/decisions.md`:
- Firefox reads the build - Firefox reads the build
- The coverage floors - The coverage floors
- The size ratchet - The size ratchet
- The comprehension floor ships under 7 until items 59, 60, 61 and 62 land - The project ships under the comprehension floor until items 59, 60, 61 and 62 land
- Properties on a fixed seed - Properties on a fixed seed
- The CommonMark suite checks three ways - The CommonMark suite checks three ways
- The flavour spec is read as a source - The flavour spec is read as a source
+1 -1
View File
@@ -198,7 +198,7 @@ emit refuses:
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span | | `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines | | `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it | | `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark, or a reserved directive stands out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` beside another block | fix what the message names, or give the node the shape it names; `spec/flavour.md` lists every type's attributes and body | | `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark, or a reserved directive stands out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` anywhere but as the whole document | fix what the message names; `spec/flavour.md` lists every type's attributes and body |
## The guarantees ## The guarantees
+12 -15
View File
@@ -496,14 +496,13 @@ config gone missing fails the leg instead of falling back to oxlint's own defaul
`--deny-warnings`, since a rule from a category this config never names arrives as a warning it `--deny-warnings`, since a rule from a category this config never names arrives as a warning it
exits 0 on. exits 0 on.
## The comprehension floor ships under 7 until items 59, 60, 61 and 62 land ## The project ships under the comprehension floor until items 59, 60, 61 and 62 land
2026-10-03, the maintainer. KISS, a technical principle, and its comprehension floor of 7. Valid 2026-10-03, the maintainer. KISS, a technical principle, and its comprehension floor of 7. Valid
while `todo.md` items 59, 60, 61 and 62 are open. while `todo.md` items 59, 60, 61 and 62 are open.
A four-seat comprehension panel scored the project 6 against the floor of 7, and the project ships A four-seat comprehension panel scored the project 6 against the floor of 7. The scores are the
under it while the restructures it named land: `todo.md` items 59, 60, 61 and 62 end this. The baseline: a later panel may not score lower on any dimension.
scores are the baseline — a later panel may not score lower on any dimension:
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall | | Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|---|---|---|---|---|---| |---|---|---|---|---|---|
@@ -672,16 +671,14 @@ them. A primitive knowing neither ADF nor a format stays at `src/` root. A const
`spec/flavour.md` draws, and a placement nothing here settles goes beside its only reader, or in `spec/flavour.md` draws, and a placement nothing here settles goes beside its only reader, or in
what both read where there are two. what both read where there are two.
Each format directory parts into `emit/` (ADF→format) and `parse/` (format→ADF), the rest of it A flavour's own code, both directions included, sits in its own directory: `markdown/plain/`.
holding what both directions read. A construct's reader lives there beside the regex the emitter Otherwise each format directory parts into `emit/` (ADF→format) and `parse/` (format→ADF), the rest
escapes against, so the two cannot drift; a reader with no emit counterpart goes in `parse/`, of it holding what both directions read. A construct's reader lives there beside the regex the
unless it is part of a construct that side already holds — a grammar stays in one file rather than emitter escapes against, so the two cannot drift; a reader with no emit counterpart goes in
splitting across the seam. A rule both directions must answer alike — whether a list marker `parse/`, unless it is part of a construct that side already holds — a grammar stays in one file
interrupts a paragraph — is one function there too, never a copy per direction, however rather than splitting across the seam. A rule both directions must answer alike — whether a list
conservative the copy would be. Where the rule is the emitter's own choice, input consults it marker interrupts a paragraph — is one function there too, never a copy per direction, however
rather than restating it, and that is the only import `parse/` takes from `emit/` — 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 value import `parse/` takes from `emit/` —
`commonMarkSpelling` and `openingLinkTakesDirective` — so no fixture the emitter writes can be `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. refused, and a spelling the emitter refuses gives its own error rather than a second name for it.
`markdown/plain/` holds the plain flavour's own code — the writer's reduction to editor-normal ADF
and the conventions and task ids the reader takes — so neither direction reaches into the other
for it.
+1 -1
View File
@@ -14,7 +14,7 @@ export function joinsWhenRead(previous: AdfNode, node: AdfNode): boolean {
return isPlainText(previous) && isPlainText(node) && identicalMarks(nodeMarks(previous), nodeMarks(node)) return isPlainText(previous) && isPlainText(node) && identicalMarks(nodeMarks(previous), nodeMarks(node))
} }
// The editor's rule, parting from the reader's only on shapes editor-normal ADF erases: an empty attrs, content or marks, and -0 in a mark. // The editor's rule, differing from the reader's only on shapes editor-normal ADF erases: an empty attrs, content or marks, and -0 in a mark.
export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean { export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean {
if (!joinsAsEditorText(previous) || !joinsAsEditorText(node)) return false if (!joinsAsEditorText(previous) || !joinsAsEditorText(node)) return false
return identicalMarks(nodeMarks(previous).map(normalMark), nodeMarks(node).map(normalMark)) return identicalMarks(nodeMarks(previous).map(normalMark), nodeMarks(node).map(normalMark))
+11 -9
View File
@@ -34,8 +34,8 @@
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 | | 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 |
| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 3, 4 | 12.4 | | 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 3, 4 | 12.4 |
| 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 5 | 11.5 | | 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 5 | 11.5 |
| 60 | 0.2.0 | decision | **Give the parser's questions to the emitter one named module, so the parse→emit dependency reads as intended.** | 3 | 4 | 2 | 6 | 2 | 10.7 | | 60 | 0.2.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 2 | 6 | 2 | 10.7 |
| 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, and move `Read<T>` to `result.ts` and `Flavour` out of the plain flavour.** | 3 | 5 | 2 | 6 | 2 | 10.4 | | 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 |
| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 | | 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 |
| 62 | 0.2.0 | decision | **Move the plain reader out of `parse/markdown-to-adf.ts` into `markdown/plain/`.** | 3 | 4 | 2 | 5 | 2 | 8.9 | | 62 | 0.2.0 | decision | **Move the plain reader out of `parse/markdown-to-adf.ts` into `markdown/plain/`.** | 3 | 4 | 2 | 5 | 2 | 8.9 |
| 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 | | 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 |
@@ -106,7 +106,7 @@ spellings" bullet, which counts one fewer.
The comprehension panel's worst place: `escapeClaims` and `escapeClosedRuns` re-implement the The comprehension panel's worst place: `escapeClaims` and `escapeClosedRuns` re-implement the
reader's view — flanking, code-span closers, link-definition openings, highlight flanking — and reader's view — flanking, code-span closers, link-definition openings, highlight flanking — and
only the property tests catch drift. It caps Locality. only the property tests catch drift. It caps the panel's Locality score.
### 52. Spell `colwidth` as a comma list, `colwidth="340,420"`. ### 52. Spell `colwidth` as a comma list, `colwidth="340,420"`.
@@ -126,16 +126,17 @@ Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the mar
escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table
gains its row. gains its row.
### 60. Give the parser's questions to the emitter one named module, so the parse→emit dependency reads as intended. ### 60. Collect the questions `parse/` asks `emit/` into one named module.
`parse/` asks `emit/` through `commonMarkSpelling` and `openingLinkTakesDirective`, each imported `parse/` asks `emit/` through `commonMarkSpelling` and `openingLinkTakesDirective`, each imported
from where it happens to live. One module naming the questions keeps `docs/decisions.md` §The source from where it happens to live. One module naming the questions keeps `docs/decisions.md` §The source
parts by ADF and format true as they grow. parts by ADF and format true as they grow.
### 59. Group the directive grammar into `src/markdown/directive/`, and move `Read<T>` to `result.ts` and `Flavour` out of the plain flavour. ### 59. Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` to `markdown/flavour.ts`.
Eight directive files sit across three directories, and the `markdown/` root holds 14 entries. HTML Eight directive files sit across three directories, and the `markdown/` root holds 14 entries. HTML
needs `Read<T>`, `Flavour`, the carry and the mark spellings out of `markdown/`. needs `Read<T>` and `Flavour` out of the plain flavour and `markdown/`; it needs the carry and the
mark spellings too, which item 7 moves where it learns what HTML shares.
### 50. Read `[](/url)` and `[]()` as CommonMark's empty link. ### 50. Read `[](/url)` and `[]()` as CommonMark's empty link.
@@ -148,7 +149,8 @@ which counts one fewer.
### 62. Move the plain reader out of `parse/markdown-to-adf.ts` into `markdown/plain/`. ### 62. Move the plain reader out of `parse/markdown-to-adf.ts` into `markdown/plain/`.
What moves: the plain reader's code in `markdown-to-adf.ts` — the alert and task-marker reads, the
`mintTaskIds` call and the `inlineLeaves` use.
### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode. ### 38. Spell a lone surrogate in a text node so it survives a UTF-8 encode.
@@ -172,8 +174,8 @@ punctuation — and, where they do, read the code point, with a fixture per dire
### 42. Trim a text leaf's trailing blanks in linear time. ### 42. Trim a text leaf's trailing blanks in linear time.
`plain/inline-reduction.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic in a run `plain/inline-reduction.ts`'s `leafEdges` finds the trail with an unanchored `/[ \t]*$/`, quadratic
of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in in a run of blanks inside one leaf: a paragraph of `a`, 80 000 spaces, `b` takes 6.5 s in
`adfToPlainMarkdown`. Scan backward, as the expand title's trim does. `adfToPlainMarkdown`. Scan backward, as the expand title's trim does.
### 48. Keep the release path publishing past npm's bypass-2FA token retirement. ### 48. Keep the release path publishing past npm's bypass-2FA token retirement.