3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 11:48:29 +01:00

Pin each rule of the emphasis sweep with a hand-worked line

This commit is contained in:
2026-11-09 04:45:13 +01:00
committed by Lillem4n
parent 1939359cb8
commit 3c8f5e8ef2
3 changed files with 125 additions and 15 deletions
+13
View File
@@ -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
+112
View File
@@ -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' }
}
-15
View File
@@ -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<AdfDocument>`.** | 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