diff --git a/src/html/carry-content.ts b/src/html/carry-content.ts
index e58cd3e..716fb0e 100644
--- a/src/html/carry-content.ts
+++ b/src/html/carry-content.ts
@@ -6,7 +6,7 @@ import { nodeAttrs, nodeContent } from '../adf/document.ts'
import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../adf/shown-text.ts'
import { shownValue } from './escaping.ts'
-// One object per block a node stands in, so a block's content after a nested block parts from its content before; `code` the code block it is, if any.
+// Compared by reference: each block makes one, so its content after a nested block starts a new shown block. `code`: this block, where it is a code block.
type BlockPart = { code: AdfNode | undefined }
type ShownBlock = { html: string; part: BlockPart }
diff --git a/src/html/data-attributes.ts b/src/html/data-attributes.ts
index 1d40240..7ed630c 100644
--- a/src/html/data-attributes.ts
+++ b/src/html/data-attributes.ts
@@ -22,7 +22,7 @@ export function dataAttributes(node: AdfNode, vocabulary: AttributeVocabulary, w
return attributes
}
-// What `dataAttributes` wrote, its pairs' values unescaped. A pair it does not write in that order is no HTML of the dialect; a name or value no attribute of the node spells is the node's shape, as a directive's is (docs/decisions.md §Which code a cause takes).
+// What `dataAttributes` wrote, its pairs' values unescaped. A pair it does not write in that order is no HTML of the dialect; a `data-*` name or a value no attribute of the node spells is the node's shape, as a directive's is (docs/decisions.md §Which code a cause takes).
export function readDataAttributes(pairs: readonly (readonly [string, string])[], element: string, vocabulary: AttributeVocabulary, written: readonly string[]): Read {
const attrs: AdfAttributes = {}
let previous = ''
@@ -38,7 +38,7 @@ export function readDataAttributes(pairs: readonly (readonly [string, string])[]
return { value: attrs }
}
-// As `readDataAttributes`, past `leading`, an attribute the element writes ahead of them, where it stands first; the caller reads `leading`.
+// As `readDataAttributes`, skipping `leading` where it is the first pair and refusing it later; the caller reads `leading`'s value.
export function readDataAttributesAfter(leading: string, pairs: readonly (readonly [string, string])[], element: string, vocabulary: AttributeVocabulary, written: readonly string[]): Read {
const repeated = pairs.findIndex(([name], index) => index > 0 && name === leading)
const data = readDataAttributes(pairs.slice(pairs[0]?.[0] === leading ? 1 : 0, repeated < 0 ? pairs.length : repeated), element, vocabulary, written)
diff --git a/src/html/parse/html-to-adf.ts b/src/html/parse/html-to-adf.ts
index df00ad5..fcecb5a 100644
--- a/src/html/parse/html-to-adf.ts
+++ b/src/html/parse/html-to-adf.ts
@@ -28,7 +28,7 @@ type BlockReader = (html: string, tag: StartTag, place: Place, open: Open) => Re
type BlockStep = { end: number; kind: 'node'; node: AdfNode } | { end: number; kind: 'opened'; open: Open }
-// A container whose blocks, items, rows or cells are being read: `node` holds what its opening tag gave, `depth` is its children's as `adfToHtml` counts it, and `closer` ends it. An open table has kind `rows` and no `closer`: `section` names the one its rows stand in, and `closerOf` derives its closer from it.
+// A container whose blocks, items, rows or cells are being read: `node` holds what its opening tag gave, `depth` is its children's as `adfToHtml` counts it, and `closer` ends it. An open table has kind `rows` and no `closer`: `section` names the table section, head or body, its rows stand in.
type Open = { content: AdfNode[]; depth: number; node: AdfNode; place: Place | undefined } & ({ closer: string; kind: 'blocks' | 'item' | 'list' | 'row' } | { kind: 'rows'; section: TableSection })
// A row's step names the section the table's rows stand in from that row on.
diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts
index 466b3d4..c958ea2 100644
--- a/src/markdown/emit/adf-to-markdown.ts
+++ b/src/markdown/emit/adf-to-markdown.ts
@@ -35,7 +35,7 @@ export type PlacedBlocks = { blocks: readonly PlacedBlock[]; headroom: number }
// CommonMark starts a new list where the marker changes, so every second list in a run of one kind takes `alternateText`.
type SpelledBlock = { spelling: 'commonmark' | 'directive'; text: BlockText } | { alternateText: BlockText; interruptsParagraph: boolean; ordered: boolean; spelling: 'list'; text: BlockText }
type Spell = () => Result | undefined
-// ask: spell the node, keeping the spelling unless refused; take: spell the node, reading and dropping what is kept.
+// take: read and drop the node's kept spelling, else spell it; ask: as take, keeping a fresh spelling unless refused.
export type SpellingMemo = Readonly Result | undefined>>
type WalkedItem = { node: AdfNode; walk: PlacedBlocks }
export type WrittenAs = { flavour: 'lossless' } | { blocks: FlavourBlockWriters; flavour: 'portable' }
diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts
index e9349af..b161920 100644
--- a/src/markdown/emit/inline-line.ts
+++ b/src/markdown/emit/inline-line.ts
@@ -30,7 +30,7 @@ type Emission = { carries: [NodeRange, ...NodeRange[]]; segments?: undefined } |
type Fallback = [NodeRange, ...NodeRange[]] | 'opening-link'
-// How a line is judged and what its fallbacks do: the lossless writer carries nodes, the portable reduction drops marks.
+// How a line is judged and what its fallbacks do: the writer carries nodes, the portable reduction drops marks.
export type FallbackAction = {
giveUpEmphasis: (line: Line) => Line
judge: (line: Line, grouping: RunGrouping) => Result>
@@ -118,7 +118,7 @@ function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: Con
// spec/flavour.md, Marks: the spanned line where it reads back, else the parted line, taking one fallback a pass.
export function settledLine(line: Line, action: FallbackAction): Result {
- // Terminates because `take` refuses a pass that changes nothing.
+ // Terminates while each `take` refuses or moves one way: the writer carries more nodes, the reduction keeps fewer marks.
for (let current = line, pass = 1; ; pass += 1) {
let parting = current
if (pass === 1 || action.spansEachPass) {
diff --git a/src/markdown/emit/unspellable-runs.ts b/src/markdown/emit/unspellable-runs.ts
index 8326016..925d3e4 100644
--- a/src/markdown/emit/unspellable-runs.ts
+++ b/src/markdown/emit/unspellable-runs.ts
@@ -26,7 +26,7 @@ type PairSweep = {
// How another pair stands when the judged one falls: `moves` where a carry or a merge may move its delimiters, `unsettled` where it may stand or have fallen.
type Standing = 'fallen' | 'moves' | 'stands' | 'unsettled'
-// `merges` where a drop may merge runs at the pair's ends, so its delimiters may move; `falls` where it goes back with the first misread pair, itself included unless it merges; `kept` otherwise.
+// `merges` where a drop may merge runs at the pair's ends, so its delimiters may move; `falls` where it goes back with the first misread pair, as the first pair does unless it merges; `kept` otherwise.
type Verdict = 'falls' | 'kept' | 'merges'
// The runs this pass takes back: the first emphasis run the line misreads and every run that falls with it, else every misflanked highlight run.
@@ -109,7 +109,6 @@ function endsAlone(edge: AdfNode | undefined, beside: AdfNode | undefined, mark:
return marks.slice(marks.findIndex((held) => identicalMark(held, mark)) + 1).every((held) => !besideTypes.has(held.type))
}
-// A pair falls with the first where it falls cleanly, and cannot read whichever character it is written with.
function fallsWithFirst(sweep: PairSweep, judged: number): boolean {
if (!fallsCleanly(sweep, judged)) return false
const spellings = pairSpellings(sweep, judged, judged)
diff --git a/src/markdown/portable/editor-normal.ts b/src/markdown/portable/editor-normal.ts
index ab65c86..efdf3df 100644
--- a/src/markdown/portable/editor-normal.ts
+++ b/src/markdown/portable/editor-normal.ts
@@ -40,7 +40,7 @@ export function editorNormalForm(document: AdfDocument): { normal: AdfDocument;
return { normal, sources }
}
-// A node's index in a list of siblings, or in a list of one, counted in the document the siblings were normalized from.
+// The node's offset from the list's first node, counted in the document they were normalized from; `index` where either is not in `sources`.
export function sourceIndex(nodes: readonly AdfNode[], index: number, sources: SourceIndexes): number {
const first = nodes[0]
const node = nodes[index]
diff --git a/src/markdown/portable/inline-reduction.ts b/src/markdown/portable/inline-reduction.ts
index 5b8ada5..434807c 100644
--- a/src/markdown/portable/inline-reduction.ts
+++ b/src/markdown/portable/inline-reduction.ts
@@ -17,7 +17,7 @@ import { spellDestination } from '../commonmark/link-syntax.ts'
// What stands beside a leaf's edge: whether a line edge does, and how many runs hold the leaf and its neighbour both.
type Beside = { lineEdge: boolean; sharedRuns: number }
-// How many of the leaf's marks edge whitespace keeps, or that it drops at a line edge.
+// `keep`: how many of the leaf's marks its edge whitespace keeps; `drop`: the whitespace goes, at a line edge.
type EdgeOutcome = { kind: 'drop' } | { depth: number; kind: 'keep' }
type PlacedLeaf = { after: Beside; before: Beside; leaf: AdfNode }
@@ -34,7 +34,7 @@ export function reduceInline(nodes: readonly AdfNode[], container: LineContainer
const leaves = portableLeaves(nodes, container, place)
if (!leaves.ok) return leaves
const { path } = place
- // Ends: withoutFallback refuses a pass that drops no mark, and trimmedEdges adds none.
+ // Terminates: withoutFallback refuses a pass that drops no mark, and trimmedEdges adds none.
return settledLine(leaves.value, {
giveUpEmphasis: withoutEmphasis,
judge: (line, grouping) => portableJudgement(line, grouping, container, path),
@@ -43,7 +43,7 @@ export function reduceInline(nodes: readonly AdfNode[], container: LineContainer
})
}
-// The leaves before the spanned trim `portableJudgement` gives them.
+// Trimmed once for `highlightsInsideSharedMarks`; `portableJudgement` trims each grouping's line again.
export function portableLeaves(nodes: readonly AdfNode[], container: LineContainer, place: ReductionPlace): Result {
const leaves = inlineLeaves(nodes, container, place)
if (!leaves.ok) return leaves
@@ -248,7 +248,7 @@ function edgeOutcome(marks: readonly AdfMark[], { lineEdge, sharedRuns }: Beside
return depth === 0 && lineEdge ? { kind: 'drop' } : { depth, kind: 'keep' }
}
-// Each grouping's line trimmed by its own runs, which a fallback's indexes name.
+// Trims the line by `grouping`'s runs and judges it; a fallback's indexes name the trimmed line.
export function portableJudgement(leaves: readonly AdfNode[], grouping: RunGrouping, container: LineContainer, path: ConvertErrorPath): Result> {
const line = trimmedEdges(leaves, grouping)
const fallback = portableLineFallback(line, container, path, grouping)
diff --git a/todo.md b/todo.md
index e954f03..f9eba48 100644
--- a/todo.md
+++ b/todo.md
@@ -28,7 +28,7 @@
| 112 | 0.4.0 | | | **Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.** | 2 | 1 | 2 | 3 | 3 | 4 by 4 | 6.2 |
| 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.0 |
| 171 | 0.4.0 | | principle | **Assert the facts an error message must carry, never its whole sentence, in `markdown-to-adf.test.ts`, `document.test.ts` and `html-to-adf.test.ts`.** | 3 | 1 | 1 | 3 | 6 | code health by 3 | 4.4 |
-| 173 | 0.4.0 | | principle | **Keep each description `commonMarkSpellings` gives a refused block beside the emitter's spelling it describes.** | 2 | 1 | 1 | 2 | 4 | code health by 1 | 4.4 |
+| 173 | 0.4.0 | | principle | **Move each CommonMark spelling's description out of `commonMarkSpellings`, beside the emitter's spelling it describes.** | 2 | 1 | 1 | 2 | 4 | code health by 1 | 4.4 |
| 81 | 0.4.0 | | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 1 | 1 | 1 | 2 | 3 by 2 | 4.2 |
| 147 | 0.4.0 | | | **Ship `adfToPlainText`, writing a document's content with no markup, under a README goal for plain text.** | 2 | 2 | 1 | 3 | 4 | 3 by 4 | 4.2 |
| 141 | 0.4.0 | | decision | **Write `strike` as HTML in `adfToHtml` and read it back in `htmlToAdf`, in place of the carry element.** | 1 | 1 | 1 | 1 | 2 | 6 by 2 | 4.0 |
@@ -36,7 +36,7 @@
| 136 | 0.4.0 | | | **Write underline, text and background colours, subscript, superscript, alignment and indentation as HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 4 | 6 by 2 | 2.0 |
| 134 | 0.4.0 | | decision | **Write panels, expands, task lists and decision lists as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 5 | 6 by 3 | 1.8 |
| 46 | 0.4.0 | 134, 135, 136 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 2 | 1 | 3 | 4 | 3 by 3 | 3.2 |
-| 174 | 0.4.0 | | defect | **Bound the passes `trimmedEdges` takes by a constant, so a line of alternating code and emphasised blanks trims in linear time.** | 3 | 1 | 2 | 4 | 7 | 9 by 3 | 2.3 |
+| 174 | 0.4.0 | | defect | **Bound the passes `trimmedEdges` takes by a constant, so a line of alternating code and emphasized blanks trims in linear time.** | 3 | 1 | 2 | 4 | 7 | 9 by 3 | 2.3 |
| 51 | 0.4.0 | | | **Match a reference label to its definition under Unicode case folding.** | 2 | 1 | 2 | 3 | 4 | 4 by 2 | 2.1 |
| 148 | 0.4.0 | | | **Ship conversion between ADF and Jira wiki markup both ways, under a README goal for wiki markup.** | 3 | 2 | 2 | 5 | 7 | 3 by 5 | 0.9 |
| 149 | 0.4.0 | | | **Read any HTML into ADF, in a call or option a developer panel picks, under a README goal for foreign HTML.** | 3 | 2 | 2 | 5 | 7 | 3 by 5 | 0.9 |
@@ -216,22 +216,23 @@ document's whole tree. Give the node's path, and name its type and the attribute
### 171. Assert the facts an error message must carry, never its whole sentence, in `markdown-to-adf.test.ts`, `document.test.ts` and `html-to-adf.test.ts`.
-README → Audience says a message is free text that may change in any release, yet `content()` in
+README → Audience says no persona may rely on a message's wording, yet `content()` in
`src/markdown/parse/markdown-to-adf.test.ts` folds a failure into `${code}: ${message}`, about 103
assertions compare that whole sentence, and `document.test.ts` (7) and `html-to-adf.test.ts` (3)
pin `message:` the same way. A rewording fails tests with no behaviour changed, while the facts
§`message` and `path` requires (the violation, the escape named) are never asserted on their own.
-Assert `code`, `path`, `position` and `assert.match(message, …)` on the facts the text carries.
+Assert `code`, `path` and `position` exactly, and match `message` against the facts it must carry.
Technical principle: "Pin wording only where the wording is the contract". Found by the principles
audit of the release review of 0.3.0, 2026-11-08.
-### 173. Keep each description `commonMarkSpellings` gives a refused block beside the emitter's spelling it describes.
+### 173. Move each CommonMark spelling's description out of `commonMarkSpellings`, beside the emitter's spelling it describes.
`commonMarkSpellings` (`src/markdown/parse/markdown-to-adf.ts`) restates in prose the spellings
`spellReadableBlock` (`src/markdown/emit/adf-to-markdown.ts`) owns, such as "each item behind `- `,
or behind `* ` where …". A changed spelling leaves the refusal naming the old one, and a node
missing from the map is refused naming no spelling. Put each description beside the spelling and
-export it through `emit/spelling-questions.ts`, the one import `parse/` may take from `emit/`.
+export it through `emit/spelling-questions.ts`, the one import `parse/` may take from `emit/`, and
+drop the comment at `markdown-to-adf.ts:145` that keeps them apart.
Technical principle: DRY. Found by the principles audit of the release review of 0.3.0, 2026-11-08.
### 81. Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.
@@ -284,11 +285,11 @@ or report the unminified gzip). The figure lands in README §The package beside
dependencies" claim, and in a size column of the comparison table. Measured today,
unminified: tarball 60.4 kB, unpacked 221.5 kB, JS gzipped 45.6 kB.
-### 174. Bound the passes `trimmedEdges` takes by a constant, so a line of alternating code and emphasised blanks trims in linear time.
+### 174. Bound the passes `trimmedEdges` takes by a constant, so a line of alternating code and emphasized blanks trims in linear time.
`trimmedEdges` (`src/markdown/portable/inline-reduction.ts`) repeats a pass until nothing changes, and
each pass costs O(leaves). Leaves alternating `' '` marked code with `' '` marked em take
-floor(n/4) + 2 passes: 26, 251 and 2 501 at n = 100, 1 000 and 10 000, so the trim is quadratic,
+ceil(n/4) + 1 passes: 26, 251 and 2 501 at n = 100, 1 000 and 10 000, so the trim is quadratic,
against goal 9. The same holds for `'x'` code beside `' '` em, and for `[link, code] 'x'` beside
`[link, em] ' '`. The smallest line is six leaves `' '` marked code, em, code, em, code, em, which
takes 3 passes. The passes in a random search of 20 000 short lines never went above 4.
@@ -299,9 +300,9 @@ pass strips em from those two, and the next pair is exposed. A single pass canno
directly: every pass judges all leaves against one snapshot, and `edgeOutcome` (drop at a line edge
while a run is shared, keep when not) isn't monotone. A hard break between `[link, em] ' '` and
`[link, em] ' b'` trims to `b` today, but to a linked space and `b` if the first leaf's drop is seen
-first. Three ways forward: make the edge rule monotone, which changes output in edge cases and
+first. Two ways forward: make the edge rule monotone, which changes output in edge cases and
needs a decision and a writer panel; keep run extents under truncation in `runsSharedWithNext`
-(`emit/inline-runs.ts`), exact but large; or state the cost as an exception under goal 9. Count passes
+(`emit/inline-runs.ts`), exact but large. Count passes
to measure, never time (§Cost fixes are measured, never timed). Related: item 42 trims blanks inside
one leaf. Found by the principles audit of the release review of 0.3.0, 2026-11-08.
@@ -338,7 +339,7 @@ choice. Split into items once the API is picked.
279, 771, 917, 926, 962 and 1118 of `src/markdown/parse/markdown-to-adf.test.ts`; the most are in
`adf-schema.test.ts` (18), `limits.test.ts` (12) and `portable-markdown-to-adf.test.ts` (10). The
first failing reader stops the test and the failure never names the reader. Write
-`for (const read of readers) test(`… in ${read.name}`, …)` at the top level. Technical principle:
+``for (const read of readers) test(`… in ${read.name}`, …)`` at the top level. Technical principle:
"No loops or branches in a test body; parametrize the cases". Found by the principles audit of the
release review of 0.3.0, 2026-11-08.