From 3c8f5e8ef2e561f9bb65a9b86760bcf9cfa63d74 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Mon, 9 Nov 2026 04:45:13 +0100 Subject: [PATCH] Pin each rule of the emphasis sweep with a hand-worked line --- docs/reading-cost.md | 13 +++ src/markdown/emit/unspellable-runs.test.ts | 112 +++++++++++++++++++++ todo.md | 15 --- 3 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 src/markdown/emit/unspellable-runs.test.ts diff --git a/docs/reading-cost.md b/docs/reading-cost.md index 84f6a86..db61566 100644 --- a/docs/reading-cost.md +++ b/docs/reading-cost.md @@ -805,3 +805,16 @@ Read most (opens and searches): - `AGENTS.md` 1 - `ci.sh` 1 - `docs/decisions.md` 1 + +## Reading cost, chunk a65075e633b3, finished 2026-11-09T00:57:29Z + +67156 new input tokens before the first commit. + +Read most (opens and searches): + +- `src/markdown/inline-content.ts` 9 +- `todo.md` 3 +- `README.md` 2 +- `CHANGELOG.md` 1 +- `docs/decisions.md` 1 +- `src/markdown/emit/line-escaping.ts` 1 diff --git a/src/markdown/emit/unspellable-runs.test.ts b/src/markdown/emit/unspellable-runs.test.ts new file mode 100644 index 0000000..5f626fe --- /dev/null +++ b/src/markdown/emit/unspellable-runs.test.ts @@ -0,0 +1,112 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import type { NodeRange } from './line-escaping.ts' +import { isAdfMark, type AdfMark, type AdfNode } from '../../adf/document.ts' +import { linePass, withheldMarkFallbacks } from './inline-line.ts' +import { portableJudgement, portableLeaves } from '../portable/inline-reduction.ts' + +// Each line is worked by hand from docs/decisions.md §With the first misread emphasis run, `unspellableRuns` returns only runs that, taken back early, leave the line as the one-run-per-pass reference writes it. +// A run reads `mark@first-last`, nodes named by index. `carry` is the lossless writer's first read-back pass, `drop` the portable writer's. +const lines: { carry?: string[]; drop?: string[]; name: string; nodes: AdfNode[] }[] = [ + { + carry: ['em@0-0', 'strong@0-0'], + name: 'a strong whose em cannot close, with the em', + nodes: [marked(['em', 'strong', 'strike'], ')'), plain('a')], + }, + { + carry: ['strike@1-1'], + drop: ['em@1-1', 'strike@1-1'], + name: 'the strike that cannot open', + nodes: [marked(['strong'], 'r'), marked(['strong', 'strike', 'em'], '_'), plain('a')], + }, + { + carry: ['strike@0-0'], + drop: ['strike@0-0', 'strong@0-0'], + name: 'the strike that cannot close', + nodes: [marked(['strike', 'em', 'strong'], '_'), plain('0')], + }, + { + carry: ['strike@1-1', 'strike@3-3'], + drop: ['strike@1-1', 'strike@3-3'], + name: 'two strikes that cannot open, not the strong over them', + nodes: [plain('b'), marked(['strike', 'strong'], '_'), marked(['strong'], 'r'), marked(['strong', 'strike', 'em'], '(')], + }, + { + carry: ['strong@0-0'], + drop: ['strike@0-1', 'strong@0-0'], + name: 'the strong that cannot close, and under a drop the strike', + nodes: [marked(['strike', 'em', 'strong'], '_'), marked(['strike', 'em'], 'b)'), plain('0')], + }, + { + carry: ['strong@0-0'], + drop: ['em@2-3', 'strong@0-0', 'strong@2-3'], + name: 'the strong that cannot close, the em holding a strike staying under a carry', + nodes: [marked(['strong'], '*'), plain('0'), marked(['em', 'strong', 'strike'], '['), marked(['em', 'strong'], '(')], + }, + { + carry: ['strike@1-1'], + drop: ['em@2-2', 'strike@1-1', 'strong@2-2'], + name: 'the strike that cannot open, and under a drop the strong and em after it', + nodes: [plain('b'), marked(['strike'], ')'), marked(['em', 'strong'], '_'), plain('n')], + }, + { + drop: ['strong@2-2'], + name: 'a strong that cannot open, the em beside a strike that merges staying', + nodes: [marked(['strong', 'strike'], 'a'), marked(['strike'], 'a'), marked(['strike', 'strong', 'em'], '_')], + }, + { + drop: ['strong@1-2'], + name: 'a strong that cannot open, the em beside it staying as its end can merge', + nodes: [plain('a'), marked(['strong', 'em'], '[ '), marked(['strong'], 'a'), marked(['em', 'strong'], 'a')], + }, + { + carry: ['strong@0-0', 'strong@2-2'], + name: 'a strong whose opener follows a word beside an em written `_`', + nodes: [marked(['strong'], '_'), plain('0'), marked(['strong', 'em'], 'a)')], + }, + { + carry: ['em@1-1', 'strong@1-1'], + name: 'a strong and em that cannot close inside a strike that reads', + nodes: [marked(['strong'], '!'), marked(['strike', 'em', 'strong'], '['), marked(['strike'], '0')], + }, + { + carry: ['em@2-2'], + name: 'the em that cannot open, not the strong over more nodes holding it', + nodes: [plain('b'), marked(['strong'], '(a'), marked(['strong', 'em'], ')'), plain('c')], + }, +] + +for (const { carry, drop, name, nodes } of lines) { + test(`unspellableRuns returns ${name}`, () => { + for (const grouping of ['spanned', 'parted'] as const) { + if (carry !== undefined) { + const pass = linePass(nodes, 'paragraph', [], withheldMarkFallbacks(nodes, 'lossless'), grouping) + assert.ok(pass.ok) + assert.ok(pass.value.fallback !== undefined && pass.value.fallback !== 'opening-link') + assert.deepEqual(named(pass.value.fallback), carry) + } + if (drop !== undefined) { + const leaves = portableLeaves(nodes, 'paragraph', { depth: 0, path: [], sources: new Map() }) + assert.ok(leaves.ok) + const judged = portableJudgement(leaves.value, grouping, 'paragraph', []) + assert.ok(judged.ok) + assert.ok(judged.value.kind === 'fallback' && judged.value.fallback.kind === 'unspellable-run') + assert.deepEqual(named(judged.value.fallback.runs), drop) + } + } + }) +} + +function marked(types: readonly string[], text: string): AdfNode { + const marks: AdfMark[] = types.map((type) => ({ type })) + return { marks, text, type: 'text' } +} + +function named(runs: readonly NodeRange[]): string[] { + return runs.map((run) => `${'mark' in run && isAdfMark(run.mark) ? run.mark.type : 'unmarked'}@${run.first}-${run.last}`).sort() +} + +function plain(text: string): AdfNode { + return { text, type: 'text' } +} diff --git a/todo.md b/todo.md index 2e80d70..b5abbea 100644 --- a/todo.md +++ b/todo.md @@ -13,7 +13,6 @@ | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | |---|---|---|---|---|---|---|---|---|---|---|---| | 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 | -| 162 | 0.4.0 | | | **Pin each rule of the emphasis sweep with a hand-worked line, so every mutant that lets a pair fall where the one-run-per-pass reference keeps it fails a test.** | 2 | 1 | 1 | 2 | 4 | code health by 4 | 17.4 | | 175 | 0.4.0 | | | **Delete the guards in `tryTaskList` that `reduceToPortable` makes unreachable, once a test shows `adfToPortableMarkdown` reaches none of them.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 | | 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 13.1 | | 127 | 0.4.0 | | principle | **Hold what `entity-references.test.ts`, `json-text.test.ts` and `result.test.ts` check by asserting outcomes, so none reads source text.** | 2 | 2 | 1 | 3 | 3 | code health by 2 | 13.1 | @@ -72,20 +71,6 @@ a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded). `src/markdown/**` bans only `html/`; banning `emit/`, `parse/` and `portable/` for the root's own files, with its `ci.sh` probe, extends the decision. -### 162. Pin each rule of the emphasis sweep with a hand-worked line, so every mutant that lets a pair fall where the one-run-per-pass reference keeps it fails a test. - -About 20 mutants of `src/markdown/emit/unspellable-runs.ts` survive the gate. Each lets a pair fall -with the first where the one-run-per-pass reference keeps it, so the line comes out other than the -reference writes it, against `docs/decisions.md` §With the first misread emphasis run, -`unspellableRuns` returns only runs that, taken back early, leave the line as the one-run-per-pass -reference writes it. They sit in `endsAlone`'s mark check, `holdsAlike`'s `held >= 0`, -`mayBeWrittenWith`, the `'moves'` give-ups in `runEdge`, `wordBeside` and `pairStanding`, -`pairSpellings`' carry-split rule and `holdsPairBefore`'s `<=`. The properties beside -`inline-line.ts` and `inline-reduction.ts` miss them; the `runEdge` one survived 20000 runs on a -random seed. Write one line per rule of that entry, its expected markdown worked from the entry, -never from the code, then run Stryker on the file. A mutant that only takes fewer runs back costs a -pass and may survive. - ### 175. Delete the guards in `tryTaskList` that `reduceToPortable` makes unreachable, once a test shows `adfToPortableMarkdown` reaches none of them. `reduceToPortable` runs first and turns a task list opening with a non-task into a bullet list. So