From 088494511df1cda92b7cc0059cdf2dde5ecd02bb Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Sat, 31 Oct 2026 01:18:45 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20stability=20round=201:=201=20?= =?UTF-8?q?=F0=9F=94=B4,=203=20=F0=9F=9F=A1=20(0=20misread);=20start=20an?= =?UTF-8?q?=20empty=20line's=20walk=20at=20its=20innermost=20quote,=20pin?= =?UTF-8?q?=20two=20guards,=20file=20item=20162?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/markdown/emit/adf-to-markdown.test.ts | 4 ++++ src/markdown/emit/block-text.test.ts | 20 ++++++++++++++++++++ src/markdown/emit/block-text.ts | 20 +++++++++++--------- src/markdown/portable/conventions.test.ts | 2 +- todo.md | 17 ++++++++++++++++- 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/markdown/emit/adf-to-markdown.test.ts b/src/markdown/emit/adf-to-markdown.test.ts index 402f403..07a913f 100644 --- a/src/markdown/emit/adf-to-markdown.test.ts +++ b/src/markdown/emit/adf-to-markdown.test.ts @@ -322,6 +322,10 @@ test('carries every inline node of a line that only the carry spells', () => { markdown(adfToLosslessMarkdown(document(paragraph(odd, { text: ' ', type: 'text' }, { marks: [{ type: 'link' }], text: 'x', type: 'text' }, { text: ' ', type: 'text' }, odd)))), `${carriedOdd} ${unlinked} ${carriedOdd}\n`, ) + assert.equal( + markdown(adfToLosslessMarkdown(document(paragraph({ text: 'a', type: 'text' }, { attrs: { foo: 1 }, type: 'hardBreak' }, { text: 'b', type: 'text' })))), + 'a!adf:carry{json="{\\"attrs\\":{\\"foo\\":1},\\"type\\":\\"hardBreak\\"}"}b\n', + ) }) test('carries an inline node holding a mark Atlassian\'s schema withholds from it', () => { diff --git a/src/markdown/emit/block-text.test.ts b/src/markdown/emit/block-text.test.ts index 4b0f840..e0de663 100644 --- a/src/markdown/emit/block-text.test.ts +++ b/src/markdown/emit/block-text.test.ts @@ -31,3 +31,23 @@ test('measures a block text as the text it writes', () => { propertyRuns(2000), ) }) + +test('writes an empty line without reading the started prefixes inside its innermost quote', () => { + const reads = (emptyLines: number): number => { + let count = 0 + let body = blockText(`a${'\n'.repeat(emptyLines)}`) + for (let depth = 0; depth < 3; depth += 1) { + body = new Proxy(prefixedText('- ', ' ', body), { + get(target, key, receiver) { + count += 1 + return Reflect.get(target, key, receiver) + }, + }) + } + const text = quotedText(body) + count = 0 + writtenText(text) + return count + } + assert.equal(reads(1000), reads(1)) +}) diff --git a/src/markdown/emit/block-text.ts b/src/markdown/emit/block-text.ts index d533399..26e9e52 100644 --- a/src/markdown/emit/block-text.ts +++ b/src/markdown/emit/block-text.ts @@ -8,9 +8,10 @@ type Lines = | { kind: 'joined'; parts: readonly BlockText[] } | { kind: 'text'; lines: readonly string[] } type Prefix = Lines & { kind: 'prefixed' | 'quoted' } -// The written first line. The flags describe `text` so no caller reads it: reading a concatenated string copies it whole, once per nesting level. +type OpenPrefix = { innermostQuote: number; prefix: Prefix } +// The written first line. A caller tests `character` or a flag before it reads the characters of `text`: reading a concatenated string copies it whole, once per nesting level. type FirstLine = Readonly<{ blank: boolean; character: string; ruleCharacters: boolean; text: string }> -// How many written lines after the first are blank, empty, or hold text, blank ones included. +// Counts of the lines after the first: `blank` ones hold only spaces or tabs, `empty` ones nothing, and `written` ones at least one character, `blank` ones included. type LaterLines = Readonly<{ blank: number; empty: number; written: number }> export const emptyLine = blockText('') @@ -70,7 +71,7 @@ export function opening(text: BlockText): string { export function writtenText(text: BlockText): string { const lines: string[] = [] // The open prefixes, outermost first. A line starts every open prefix, so the unstarted ones are the innermost. - const open: Prefix[] = [] + const open: OpenPrefix[] = [] let started = 0 // Each pending text with how many prefixes stay open around it. const pending: { text: BlockText; within: number }[] = [{ text, within: 0 }] @@ -85,18 +86,19 @@ export function writtenText(text: BlockText): string { } } else if (current.kind === 'joined') for (const part of current.parts.slice().reverse()) pending.push({ text: part, within: open.length }) else { - open.push(current) + open.push({ innermostQuote: current.kind === 'quoted' ? open.length : (open.at(-1)?.innermostQuote ?? -1), prefix: current }) pending.push({ text: current.body, within: open.length }) } } return lines.join('\n') } -// Innermost first, since an empty line takes no indent and a quote turns it into `>`. -function prefixedLine(line: string, open: readonly Prefix[], started: number): string { - return open.reduceRight((written, prefix, depth) => { +// Innermost first: whether a line is empty when an outer prefix meets it depends on the prefixes inside. +function prefixedLine(line: string, open: readonly OpenPrefix[], started: number): string { + // An empty line takes no indent, so where every prefix has started its walk begins at the innermost quote. + const walked = line === '' && started === open.length ? open.slice(0, (open.at(-1)?.innermostQuote ?? -1) + 1) : open + return walked.reduceRight((written, { prefix }, depth) => { if (prefix.kind === 'quoted') return written === '' ? '>' : `> ${written}` - if (depth >= started) return `${prefix.head}${written}` - return written === '' ? '' : `${prefix.indent}${written}` + return depth >= started ? `${prefix.head}${written}` : `${prefix.indent}${written}` }, line) } diff --git a/src/markdown/portable/conventions.test.ts b/src/markdown/portable/conventions.test.ts index 4a7c917..4531619 100644 --- a/src/markdown/portable/conventions.test.ts +++ b/src/markdown/portable/conventions.test.ts @@ -7,7 +7,7 @@ import { propertyRuns } from '../../conformance/property-harness.ts' test('reads a task or alert marker only where the text opens with the marker opening', () => { fc.assert( - fc.property(fc.string({ maxLength: 8, unit: fc.constantFrom(' ', '!', '+', '-', 'N', 'x', '[', '\\', ']') }), (text) => { + fc.property(fc.array(fc.constantFrom(' ', '!', '+', '-', 'N', 'x', '[', '\\', ']', '[ ] ', '[x]', '[!NOTE]'), { maxLength: 4 }).map((pieces) => pieces.join('')), (text) => { if (readTaskMarker(text) !== undefined || readAlertMarker(text) !== undefined) assert.equal(text.charAt(0), markerOpening) }), propertyRuns(2000), diff --git a/todo.md b/todo.md index 088737c..812c74f 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 0.1` -`Next ID = 162` +`Next ID = 163` ## Items @@ -25,6 +25,7 @@ | 75 | 0.4.0 | | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 | | 154 | 0.4.0 | | | **Pair highlights over the inline reader's pieces by their offsets before building nodes, so no placeholder text node stands in for a delimiter.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 | | 155 | 0.4.0 | | | **Have `closeBracket` return the link, image or literal bracket it builds, and apply it to the scan in one caller.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 | +| 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 | | 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 13.1 | | 68 | 0.4.0 | | defect | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 2 | 1 | 2 | 3 | 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 | @@ -180,6 +181,20 @@ A comprehension seat found `src/markdown/inline-content.ts` `closeBracket` hard: `popBracket`, which write `scan.pieces`, `scan.pending` and the link openers. Make that place easier to read. +### 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. + ### 45. Replace `isAdfDocument` with a reader returning `Result`. Lands with item 77: the reader returns item 77's copy. Goal 1 has every call return a result; the