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

Answer stability round 1: 1 🔴, 3 🟡 (0 misread); start an empty line's walk at its innermost quote, pin two guards, file item 162

This commit is contained in:
2026-10-31 01:18:45 +01:00
committed by Lillem4n
parent 085ad7f508
commit 088494511d
5 changed files with 52 additions and 11 deletions
@@ -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', () => {
+20
View File
@@ -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))
})
+11 -9
View File
@@ -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)
}
+1 -1
View File
@@ -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),
+16 -1
View File
@@ -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<AdfDocument>`.** | 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<AdfDocument>`.
Lands with item 77: the reader returns item 77's copy. Goal 1 has every call return a result; the