diff --git a/AGENTS.md b/AGENTS.md index 99e6917..05fa255 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,8 +117,6 @@ holes through each other's lines. generators and run parameters properties share live in `src/conformance/property-harness.ts`, outside the build and coverage. -A review mutation-tests a chunk with `mutation-tests/run.sh` rather than setting Stryker up by hand. - Each `- ` bullet in `spec/flavour.md`'s `## Block nodes`, `## Inline nodes` and `## Marks` declares the nodes named before its first em dash, with the attributes following `Attributes: ` — a parenthesized value set reading `string`; fenced examples are skipped. Keep prose out of a bullet. diff --git a/README.md b/README.md index 67c654f..c939c71 100644 --- a/README.md +++ b/README.md @@ -731,10 +731,10 @@ Contract: [`docs/decisions.md`](https://github.com/larvit/adf-codec/blob/main/do Both commands need only Docker, git and bash 5. -- `./ci.sh` runs the gate: every check CI runs. -- `./mutation-tests/run.sh []` mutation-tests the source lines changed since ``, - `origin/main` by default, running each mutant against only the test files that reach it. It lists - the mutants that survived, and fails only when a mutant's test run crashed. +- `./ci.sh` runs every check CI runs. +- `./mutation-tests/run.sh []` mutation-tests the lines changed since ``, `origin/main` + by default, in source files git tracks. It needs 6 GB of memory for Docker, and lists the mutants + that survived. Survivors don't fail it; a mutant whose test run crashed does. ## Links diff --git a/docs/decisions.md b/docs/decisions.md index 85b1ef3..db85695 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -489,11 +489,11 @@ nodes that break it. ## Stryker in its own package -2026-10-28, item 152. Code health: the code is easy to change safely. Valid while the gate runs no +2026-10-28, the architecture review. Code health: the code is easy to change safely. Valid while the gate runs no mutation tests. Mutation testing with Stryker lives in `mutation-tests/`'s own package, so the gate's `npm ci` never -installs it. CI does not run it, so Renovate does not automerge its bumps. +installs it. ## Any ES2022 engine diff --git a/mutation-tests/check-runtime-errors.mjs b/mutation-tests/check-runtime-errors.mjs index d99aefe..7656d5a 100644 --- a/mutation-tests/check-runtime-errors.mjs +++ b/mutation-tests/check-runtime-errors.mjs @@ -1,9 +1,11 @@ import { readFileSync } from 'node:fs' import config from './stryker.config.mjs' -const mutants = Object.values(JSON.parse(readFileSync(config.jsonReporter.fileName, 'utf8')).files).flatMap((file) => file.mutants) -const errors = mutants.filter((mutant) => mutant.status === 'RuntimeError').length -if (errors > 0) { - console.error(`${errors} of ${mutants.length} mutants ended as RuntimeError, so each may hide a survivor`) +const files = Object.entries(JSON.parse(readFileSync(config.jsonReporter.fileName, 'utf8')).files) +const mutants = files.flatMap(([path, file]) => file.mutants.map((mutant) => ({ ...mutant, path }))) +const errors = mutants.filter((mutant) => mutant.status === 'RuntimeError') +if (errors.length > 0) { + console.error(`${errors.length} of ${mutants.length} mutants ended as RuntimeError, so each may hide a survivor. A test run killed for lack of memory ends so:`) + for (const mutant of errors) console.error(` ${mutant.path}:${mutant.location.start.line} ${mutant.mutatorName}`) process.exit(1) } diff --git a/mutation-tests/run.sh b/mutation-tests/run.sh index f781d16..b1f9b30 100755 --- a/mutation-tests/run.sh +++ b/mutation-tests/run.sh @@ -5,7 +5,7 @@ source ./docker-runner.sh base=$(git merge-base "${1:-origin/main}" HEAD) # Each hunk's added lines as path:first-last, in the working tree's numbering; a hunk that only deletes has none to mutate. -mutate=$(git diff --no-ext-diff --no-prefix -U0 --no-color "$base" -- 'src/*.ts' ':!src/*.test.ts' ':!src/conformance/property-harness.ts' | awk ' +mutate=$(git diff --no-ext-diff --no-prefix -U0 --inter-hunk-context=0 --no-color "$base" -- 'src/*.ts' ':!src/*.test.ts' ':!src/conformance/property-harness.ts' | awk ' /^\+\+\+ / { path = $2 } /^@@ / { split($3, added, ",") @@ -19,5 +19,5 @@ if [ -z "$mutate" ]; then fi leg "install ($node_image)" in_image "$node_image" sh -c 'npm ci --no-audit --no-fund && npm ci --no-audit --no-fund --prefix mutation-tests' -# A test file peaks near 2.6 GB: past four workers 6 GB kills some, and a killed worker's mutant reads as a RuntimeError, which fails the run. +# Four workers fit 6 GB in measured runs where fifteen had some killed, and a killed worker's mutant reads as a RuntimeError, which fails the run. in_image_memory=6g leg "mutation tests ($node_image)" in_image "$node_image" sh -c 'mutation-tests/node_modules/.bin/stryker run mutation-tests/stryker.config.mjs --concurrency 4 --mutate "$1" && node mutation-tests/check-runtime-errors.mjs' sh "$mutate" diff --git a/mutation-tests/stryker.config.mjs b/mutation-tests/stryker.config.mjs index d283005..f655d4f 100644 --- a/mutation-tests/stryker.config.mjs +++ b/mutation-tests/stryker.config.mjs @@ -6,6 +6,6 @@ export default { tap: { testFiles: ['src/**/*.test.ts'] }, tempDirName: '/tmp/stryker', testRunner: 'tap', - // TypeScript 7 has no JavaScript API for Stryker's tsconfig rewrite to call; a file that is not there skips it. + // Stryker skips its tsconfig rewrite, which calls a JavaScript API TypeScript 7 lacks, when this file is missing. tsconfigFile: 'no-tsconfig.json', } diff --git a/todo.md b/todo.md index 320616d..3734585 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 0.1` -`Next ID = 156` +`Next ID = 157` ## Items @@ -25,6 +25,7 @@ | 73 | 0.3.0 | 33, 91, 109, 115, 132, 133, 145, 146, 150, 152 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 2 | 3 | 2 | 3 by 8 | 16.9 | | 72 | 0.3.0 | 33, 91, 109, 115, 132, 133, 145, 146, 150, 152 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 1 | 2 | 1 | 2 | 1 | 3 by 1 | 2.5 | | 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 | +| 156 | 0.4.0 | | | **Stop a leg's container when its script is interrupted, so Ctrl-C ends `ci.sh` or `mutation-tests/run.sh` at once.** | 1 | 2 | 1 | 2 | 1 | code health by 2 | 20.9 | | 128 | 0.4.0 | | | **Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks.** | 2 | 1 | 1 | 2 | 3 | code health by 3 | 19.6 | | 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 | @@ -52,7 +53,7 @@ | 31 | 0.5.0 | | | **Make the branch-coverage figure repeat across runs of an unchanged tree.** | 2 | 1 | 1 | 2 | 3 | code health by 3 | 19.6 | | 57 | 0.5.0 | | | **Make each `ci.sh` leg build what it reads, so one leg run alone tests the current tree.** | 2 | 1 | 1 | 2 | 3 | code health by 3 | 19.6 | | 38 | 0.5.0 | | defect | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 1 | 1 | 2 | 2 | 2 | 1 by 2 | 17.4 | -| 76 | 0.5.0 | | | **State the files the package does not ship once, so the build, the lint ceiling and the coverage exclusions cannot drift apart.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 | +| 76 | 0.5.0 | | | **State the files the package does not ship once, so the build, the lint ceiling, the coverage exclusions and the mutation run cannot drift apart.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 | | 56 | 0.5.0 | | | **Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes.** | 3 | 1 | 2 | 4 | 5 | code health by 5 | 13.1 | | 118 | 0.5.0 | | | **Spell a portable link's title through `oneLine`.** | 1 | 1 | 1 | 1 | 1 | code health by 1 | 10.5 | | 82 | 0.5.0 | | | **Give the directive tests and fixtures random UUID v7 ids in place of `a-1`, `a`, `x` and `h`.** | 2 | 1 | 1 | 2 | 3 | code health by 1 | 6.5 | @@ -198,6 +199,12 @@ 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. +### 156. Stop a leg's container when its script is interrupted, so Ctrl-C ends `ci.sh` or `mutation-tests/run.sh` at once. + +`in_image` runs `sh -c` as the container's process 1, which ignores the SIGINT `docker run` +forwards, so the container runs to its end: a `sleep 30` outlived Ctrl-C by 30 s. `--init` would +forward it. Found by the product-owner review of item 152, 2026-10-28. + ### 128. Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks. Mutation testing left about 40 mutants alive in code moved verbatim into `src/markdown/portable/` @@ -428,17 +435,17 @@ build leg left, so a leg run alone can pass on stale output. `adfToLosslessMarkdown` emits it verbatim, so markdown stored as UTF-8 reads back U+FFFD; attribute values already escape it. -### 76. State the files the package does not ship once, so the build, the lint ceiling and the coverage exclusions cannot drift apart. +### 76. State the files the package does not ship once, so the build, the lint ceiling, the coverage exclusions and the mutation run cannot drift apart. Technical principle DRY: the set is written in `tsconfig.build.json`'s `exclude`, `.oxlintrc.json`'s `ignorePatterns` and the `test` script's two `--test-coverage-exclude` flags, and §The size ratchet assumes they agree. A helper added to one list lints test code under the shipped ceiling, or drops -shipped code from coverage, silently. Derive two from the third, or assert in a test that the three -agree. Found by the technical-principles audit, 2026-10-04. +shipped code from coverage, silently. Derive the others from one, or assert in a test that +they agree. Found by the technical-principles audit, 2026-10-04. -`mutation-tests/run.sh`'s pathspec and `mutation-tests/stryker.config.mjs`'s `testFiles` restate -which files are tests and test helpers, so a helper missing from them reads as survivors. Found by -the architecture review of item 152, 2026-10-28. +`mutation-tests/run.sh`'s pathspec restates the test files and test helpers, and +`mutation-tests/stryker.config.mjs`'s `testFiles` the test files, so a helper missing from the +pathspec has its mutants read as survivors. Found by the architecture review of item 152, 2026-10-28. ### 56. Give each piece of `blocks.ts`'s block-walk state and `inline-content.ts`'s `Scan` one owner that returns what it changes. @@ -469,8 +476,8 @@ Found by the technical-principles audit of item 78, 2026-10-05. ### 58. Port `ci.sh`, `publish.sh`, `docker-runner.sh` and `mutation-tests/run.sh` to standalone Python scripts. Technical principle "Prefer a standalone Python script over a shell script": `AGENTS.md` §3 teaches -bash footguns (`&&` chaining under `||`, `tee /dev/stderr`) the port removes. The port tests -`run.sh`'s turning of diff hunks into mutated line ranges, which a wrong range would pass silently. +bash footguns (`&&` chaining under `||`, `tee /dev/stderr`) the port removes. A wrong line range in +`run.sh` mutates the wrong lines without failing, so the port tests how it reads diff hunks. ### 34. Read emphasis flanking by the whole character beside an astral symbol.