@sackville-mcp/mutate 0.0.1-alpha.3 → 0.0.1-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +3 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +4 -20
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SpawnedRunner } from "@sackville-mcp/spawn";
|
|
2
|
+
|
|
1
3
|
//#region src/config.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* Diff-scoping config EMITTERS for the Python mutation tools (ADR 0010 addendum 2). These turn a
|
|
@@ -217,14 +219,7 @@ interface RunMutationInput {
|
|
|
217
219
|
ownedRoots?: string[];
|
|
218
220
|
}
|
|
219
221
|
/** Injected command runner — executes `stryker <argv>` and yields its exit status. */
|
|
220
|
-
type MutationRunner =
|
|
221
|
-
cwd: string;
|
|
222
|
-
timeoutMs?: number;
|
|
223
|
-
}) => Promise<{
|
|
224
|
-
exitCode: number;
|
|
225
|
-
stdout: string;
|
|
226
|
-
stderr: string;
|
|
227
|
-
}>;
|
|
222
|
+
type MutationRunner = SpawnedRunner;
|
|
228
223
|
interface RunMutationResult {
|
|
229
224
|
ran: boolean;
|
|
230
225
|
exitCode: number;
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/summarize.ts","../src/cosmic-ray.ts","../src/mutmut.ts","../src/run.ts","../src/scope.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/config.ts","../src/summarize.ts","../src/cosmic-ray.ts","../src/mutmut.ts","../src/run.ts","../src/scope.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { parse, stringify } from "smol-toml";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
2
|
import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
5
4
|
import { join, resolve } from "node:path";
|
|
5
|
+
import { spawnRunner } from "@sackville-mcp/spawn";
|
|
6
6
|
//#region src/config.ts
|
|
7
7
|
/**
|
|
8
8
|
* Diff-scoping config EMITTERS for the Python mutation tools (ADR 0010 addendum 2). These turn a
|
|
@@ -559,28 +559,12 @@ function runArgv(input) {
|
|
|
559
559
|
if (input.incremental) argv.push("--incremental");
|
|
560
560
|
return argv;
|
|
561
561
|
}
|
|
562
|
-
/** Spawn a local command as a subprocess, surfacing its exit code (never rejecting on non-zero). */
|
|
563
|
-
function spawnMutationRunner(command) {
|
|
564
|
-
return (argv, opts) => new Promise((res) => {
|
|
565
|
-
execFile(command, argv, {
|
|
566
|
-
cwd: opts.cwd,
|
|
567
|
-
timeout: opts.timeoutMs,
|
|
568
|
-
maxBuffer: 64 * 1024 * 1024
|
|
569
|
-
}, (err, stdout, stderr) => {
|
|
570
|
-
res({
|
|
571
|
-
exitCode: err && typeof err.code === "number" ? err.code : err ? 1 : 0,
|
|
572
|
-
stdout: String(stdout),
|
|
573
|
-
stderr: String(stderr)
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
562
|
/** Default live runner: spawn the local `stryker` as a subprocess (used by the bin, not the gate). */
|
|
579
|
-
const defaultStrykerRunner =
|
|
563
|
+
const defaultStrykerRunner = spawnRunner("stryker");
|
|
580
564
|
/** Default live runner: spawn the local `mutmut` as a subprocess (used by the bin, not the gate). */
|
|
581
|
-
const defaultMutmutRunner =
|
|
565
|
+
const defaultMutmutRunner = spawnRunner("mutmut");
|
|
582
566
|
/** Default live runner: spawn the local `cosmic-ray` as a subprocess (used by the bin, not the gate). */
|
|
583
|
-
const defaultCosmicRayRunner =
|
|
567
|
+
const defaultCosmicRayRunner = spawnRunner("cosmic-ray");
|
|
584
568
|
function assertAllowed(config) {
|
|
585
569
|
if (!config.allowRun) throw new MutateGateError("mutation runs are not enabled (the operator must set allowRun)");
|
|
586
570
|
const root = resolve(config.projectRoot);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["normalizePath"],"sources":["../src/config.ts","../src/cosmic-ray.ts","../src/mutmut.ts","../src/scope.ts","../src/summarize.ts","../src/run.ts"],"sourcesContent":["/**\n * Diff-scoping config EMITTERS for the Python mutation tools (ADR 0010 addendum 2). These turn a\n * selected-file scope into a per-run tool config, PINNED to the slice-0 captures of the installed\n * cosmic-ray 8.4.6 / mutmut 3.5.0 (see `test/fixtures/README.md`) — never doc-derived guesses:\n *\n * - cosmic-ray scopes via a `module-path` FILE LIST (Fork A — verified 8.4.6 accepts a list); its\n * `excluded-modules` SUBTRACTS from the scope via exact path AND fnmatch glob, so an inherited\n * exclusion that matches a selected file is reconciled (stripped), never copied blind (blocker #3).\n * - mutmut scopes via `paths_to_mutate` (Fork B was WRONG — 3.5.0 has NO `only_mutate`/`source_paths`;\n * `paths_to_mutate` + `do_not_mutate` are the real keys, Fork F). Scoping a subset breaks the\n * baseline unless the rest of the source tree is `also_copy`'d so unscoped tests still import; an\n * inherited `do_not_mutate` glob matching a selected file is stripped (blocker #3, mutmut form).\n *\n * Both emitters are PURE (TOML in → TOML out via `smol-toml`). They reduce SPURIOUS inconclusives by\n * reconciling exclusions up front; the load-bearing under-scope guarantee is the POST-SPAWN\n * {@link reconcileScope} guard in the runner, which folds any genuinely-unmutated selected file to\n * inconclusive regardless of why (absence-is-never-a-pass).\n */\n\nimport { parse, stringify } from 'smol-toml'\n\n/** Thrown when a scoped config cannot be safely synthesized (empty scope, missing base table, etc.). */\nexport class ScopeEmitError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ScopeEmitError'\n }\n}\n\n/** Normalize a path to a comparable repo-relative POSIX form (backslashes → `/`, drop a leading `./`). */\nfunction normalizePath(p: string): string {\n return p.replace(/\\\\/g, '/').replace(/^\\.\\//, '')\n}\n\n/** Dedupe + normalize + sort, the canonical scope form used everywhere. */\nfunction canonicalize(paths: string[]): string[] {\n return [...new Set(paths.map(normalizePath))].sort()\n}\n\n/**\n * Translate a Python `fnmatch` pattern to an anchored RegExp. Mirrors `fnmatch.translate`: a star →\n * `.*` (crosses `/`, unlike shell glob — so a `star + /strutil.py` glob matches `pkg/strutil.py`),\n * `?` → any one char, `[seq]`/`[!seq]` → char class, everything else escaped. This is the matcher\n * cosmic-ray's `excluded-modules` and mutmut's `do_not_mutate` both use (the latter via\n * `fnmatch.fnmatch` in `Config.should_ignore_for_mutation`).\n */\nfunction fnmatchToRegExp(pattern: string): RegExp {\n let re = ''\n let i = 0\n const n = pattern.length\n while (i < n) {\n const c = pattern[i++]\n if (c === '*') {\n re += '.*'\n } else if (c === '?') {\n re += '.'\n } else if (c === '[') {\n let j = i\n if (j < n && (pattern[j] === '!' || pattern[j] === ']')) j++\n while (j < n && pattern[j] !== ']') j++\n if (j >= n) {\n re += '\\\\[' // unterminated class → literal '['\n } else {\n let stuff = pattern.slice(i, j).replace(/\\\\/g, '\\\\\\\\')\n i = j + 1\n if (stuff.startsWith('!')) stuff = `^${stuff.slice(1)}`\n else if (stuff.startsWith('^')) stuff = `\\\\${stuff}`\n re += `[${stuff}]`\n }\n } else {\n re += (c ?? '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n }\n return new RegExp(`^(?:${re})$`, 's')\n}\n\n/** True if an exclusion entry (exact path or fnmatch glob) matches any of the selected files. */\nfunction exclusionCollides(entry: string, selected: string[]): boolean {\n const norm = normalizePath(entry)\n if (selected.includes(norm)) return true // exact path\n const re = fnmatchToRegExp(norm)\n return selected.some((f) => re.test(f))\n}\n\n/**\n * The operator's declared source tree, read from a cosmic-ray base config's `module-path` (a single\n * string or a list). Used as the default `ownedRoots` to confine the diff scope (Fork C): a changed\n * `.py` outside this tree is `unmatched` (report-gap), never silently scoped. Pure.\n */\nexport function cosmicModulePathRoots(baseToml: string): string[] {\n const data = parse(baseToml) as Record<string, unknown>\n const cr = data['cosmic-ray']\n if (!cr || typeof cr !== 'object') return []\n const mp = (cr as Record<string, unknown>)['module-path']\n if (typeof mp === 'string') return [normalizePath(mp)]\n if (Array.isArray(mp)) return mp.map((x) => normalizePath(String(x)))\n return []\n}\n\nexport interface ScopedCosmicRayConfig {\n /** The synthesized per-run `cosmic-ray.toml` text. */\n toml: string\n /** The selected file list written to `module-path` (canonical form). */\n modulePath: string[]\n /** Inherited `excluded-modules` entries removed because they would subtract a selected file. */\n strippedExclusions: string[]\n}\n\n/**\n * Synthesize a per-run cosmic-ray config from a base config + the selected files. Overrides\n * `module-path` to the canonical selected FILE LIST and reconciles `excluded-modules`: any entry\n * (exact or fnmatch glob) that matches a selected file is stripped (it would otherwise silently\n * subtract that file — blocker #3); non-colliding entries are preserved. All other keys/tables\n * (`timeout`, `test-command`, `[cosmic-ray.distributor]`, …) are preserved verbatim. Pure.\n */\nexport function synthesizeScopedCosmicRayConfig(\n baseToml: string,\n selectedFiles: string[],\n): ScopedCosmicRayConfig {\n const selected = canonicalize(selectedFiles)\n if (selected.length === 0) {\n throw new ScopeEmitError('cannot synthesize a scoped cosmic-ray config for an empty selection')\n }\n const data = parse(baseToml) as Record<string, unknown>\n const cr = data['cosmic-ray']\n if (cr === undefined || typeof cr !== 'object') {\n throw new ScopeEmitError('base config has no [cosmic-ray] table')\n }\n const table = cr as Record<string, unknown>\n table['module-path'] = selected\n\n const strippedExclusions: string[] = []\n const inherited = Array.isArray(table['excluded-modules'])\n ? (table['excluded-modules'] as unknown[]).map(String)\n : []\n const kept = inherited.filter((entry) => {\n if (exclusionCollides(entry, selected)) {\n strippedExclusions.push(entry)\n return false\n }\n return true\n })\n table['excluded-modules'] = kept\n\n return { toml: stringify(data), modulePath: selected, strippedExclusions }\n}\n\n/** The `[tool.mutmut]` table of a pyproject (empty when absent / unparseable-as-table). */\nfunction mutmutTable(basePyproject: string): Record<string, unknown> {\n if (!basePyproject.trim()) return {}\n const data = parse(basePyproject) as Record<string, unknown>\n const tool = data.tool\n if (!tool || typeof tool !== 'object') return {}\n const m = (tool as Record<string, unknown>).mutmut\n return m && typeof m === 'object' ? (m as Record<string, unknown>) : {}\n}\n\n/** The operator's `[tool.mutmut] paths_to_mutate` — the default `ownedRoots` for a scoped mutmut run. */\nexport function mutmutPathsToMutate(basePyproject: string): string[] {\n const v = mutmutTable(basePyproject).paths_to_mutate\n return Array.isArray(v) ? v.map((x) => normalizePath(String(x))) : []\n}\n\n/** The operator's inherited `[tool.mutmut] do_not_mutate` globs (reconciled against the scope). */\nexport function mutmutDoNotMutate(basePyproject: string): string[] {\n const v = mutmutTable(basePyproject).do_not_mutate\n return Array.isArray(v) ? v.map(String) : []\n}\n\nexport interface MutmutScopePlan {\n /** The selected files written to `paths_to_mutate` (canonical form). */\n pathsToMutate: string[]\n /** Sibling source files to `also_copy` so unscoped tests still import (source tree minus scope). */\n alsoCopy: string[]\n /** Inherited `do_not_mutate` globs preserved (those that do NOT collide with the scope). */\n doNotMutate: string[]\n /** Inherited `do_not_mutate` globs removed because they would exclude a selected file (blocker #3). */\n strippedDoNotMutate: string[]\n}\n\n/**\n * Plan a scoped mutmut run. `paths_to_mutate` = the selected files; `also_copy` = every other source\n * file in `allSourceFiles` (so a test importing an unscoped sibling module still resolves in mutmut's\n * `mutants/` sandbox — verified necessary in slice 0). An inherited `do_not_mutate` glob that matches\n * a selected file is stripped (it would otherwise exclude a file we deliberately scoped). Pure given\n * `allSourceFiles` (the runner derives it by walking the owned roots).\n */\nexport function planMutmutScope(\n selectedFiles: string[],\n allSourceFiles: string[],\n inheritedDoNotMutate: string[] = [],\n): MutmutScopePlan {\n const pathsToMutate = canonicalize(selectedFiles)\n if (pathsToMutate.length === 0) {\n throw new ScopeEmitError('cannot plan a scoped mutmut run for an empty selection')\n }\n const scope = new Set(pathsToMutate)\n const alsoCopy = canonicalize(allSourceFiles).filter((f) => !scope.has(f))\n\n const strippedDoNotMutate: string[] = []\n const doNotMutate = inheritedDoNotMutate.filter((entry) => {\n if (exclusionCollides(entry, pathsToMutate)) {\n strippedDoNotMutate.push(entry)\n return false\n }\n return true\n })\n return { pathsToMutate, alsoCopy, doNotMutate, strippedDoNotMutate }\n}\n\n/**\n * Render a `pyproject.toml` for a scoped mutmut run: merge the {@link MutmutScopePlan}'s\n * `paths_to_mutate`/`also_copy` (+ reconciled `do_not_mutate`) into the base pyproject's\n * `[tool.mutmut]` table (Fork F: only slice-0-verified keys), preserving every other section and any\n * other verified `[tool.mutmut]` key the operator set (e.g. `pytest_add_cli_args`). An empty\n * `do_not_mutate` is omitted entirely. Pure.\n */\nexport function synthesizeScopedMutmutPyproject(\n basePyproject: string,\n plan: MutmutScopePlan,\n): string {\n const data = (basePyproject.trim() ? parse(basePyproject) : {}) as Record<string, unknown>\n const tool = (data.tool && typeof data.tool === 'object' ? data.tool : {}) as Record<\n string,\n unknown\n >\n const mutmut = (tool.mutmut && typeof tool.mutmut === 'object' ? tool.mutmut : {}) as Record<\n string,\n unknown\n >\n mutmut.paths_to_mutate = plan.pathsToMutate\n mutmut.also_copy = plan.alsoCopy\n if (plan.doNotMutate.length > 0) mutmut.do_not_mutate = plan.doNotMutate\n else delete mutmut.do_not_mutate\n tool.mutmut = mutmut\n data.tool = tool\n return stringify(data)\n}\n","/**\n * cosmic-ray adapter (a Python mutation-testing tool) — converts `cosmic-ray dump <session.sqlite>`\n * JSON-lines into the mutation-testing-elements {@link MutationReport} that {@link summarizeMutation}\n * already consumes, so the summarizer is reused unchanged across Stryker (JS), mutmut, and cosmic-ray.\n *\n * Unlike mutmut, cosmic-ray's dump carries the REAL source path + line + operator per mutant, so its\n * survivors are actionable (file:line:operator). Each dump line is a JSON array\n * `[work_item, work_result | null]` (captured from cosmic-ray 8.4.6 — see\n * `test/fixtures/cosmic-ray-dump.jsonl`):\n *\n * - `work_item.mutations[]` — one entry per mutation in the job (single-mutation jobs in practice);\n * each has `module_path` (the file key), `operator_name` (the mutator), and `start_pos: [line, col]`.\n * - `work_result` — `{ worker_outcome, test_outcome? }`, or `null` when the job has not been executed.\n *\n * Status mapping (cosmic-ray → mutation-testing-elements), chosen to never overstate the score and to\n * fold ambiguity to a neutral `Pending` (never `Survived`/`Killed`):\n * - `work_result === null` → Pending (not yet executed)\n * - worker_outcome `normal` + test_outcome `killed`/`survived` → Killed / Survived\n * - worker_outcome `normal` + test_outcome `incompetent` → RuntimeError (the mutant broke the run)\n * - worker_outcome `no_test` → NoCoverage (no test exercised the mutation)\n * - worker_outcome `skipped` → Ignored\n * - worker_outcome `exception`/`abnormal`→ RuntimeError (invalid — excluded from the score)\n * - worker_outcome `timeout` → Timeout\n * - anything else / missing test_outcome → Pending (ambiguity ⇒ neutral, never a phantom survivor)\n */\n\nimport type { Mutant, MutantStatus, MutationFile, MutationReport } from './summarize.js'\n\ninterface CosmicMutation {\n module_path?: string\n operator_name?: string\n start_pos?: [number, number] | { line?: number; column?: number }\n}\ninterface CosmicWorkItem {\n mutations?: CosmicMutation[]\n}\ninterface CosmicWorkResult {\n worker_outcome?: string\n test_outcome?: string | null\n}\n\n/** worker_outcome (other than `normal`) → status. `normal` defers to test_outcome. */\nconst WORKER_STATUS: Record<string, MutantStatus> = {\n no_test: 'NoCoverage',\n skipped: 'Ignored',\n exception: 'RuntimeError',\n abnormal: 'RuntimeError',\n timeout: 'Timeout',\n}\n/** test_outcome (when worker_outcome === 'normal') → status. */\nconst TEST_STATUS: Record<string, MutantStatus> = {\n killed: 'Killed',\n survived: 'Survived',\n incompetent: 'RuntimeError',\n}\n\nfunction statusOf(result: CosmicWorkResult | null): MutantStatus {\n if (result === null) return 'Pending' // not yet executed\n const worker = result.worker_outcome\n if (worker === 'normal') return TEST_STATUS[result.test_outcome ?? ''] ?? 'Pending'\n return (worker !== undefined && WORKER_STATUS[worker]) || 'Pending'\n}\n\nfunction lineColOf(\n pos: CosmicMutation['start_pos'],\n): { line: number; column?: number } | undefined {\n if (Array.isArray(pos)) return { line: pos[0], column: pos[1] }\n if (pos && typeof pos.line === 'number') return { line: pos.line, column: pos.column }\n return undefined\n}\n\n/** Parse `cosmic-ray dump <session.sqlite>` JSON-lines into a mutation-testing-elements report. Pure. */\nexport function parseCosmicRayDump(jsonl: string): MutationReport {\n const files: Record<string, MutationFile> = {}\n let index = 0\n for (const raw of jsonl.split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line) continue\n let parsed: [CosmicWorkItem, CosmicWorkResult | null]\n try {\n parsed = JSON.parse(line) as [CosmicWorkItem, CosmicWorkResult | null]\n } catch {\n continue // a non-JSON line (e.g. a stray log) is skipped, never a phantom mutant\n }\n const [workItem, workResult] = parsed\n const mutation = workItem?.mutations?.[0]\n const path = mutation?.module_path\n if (!path) continue\n const status = statusOf(workResult ?? null)\n const loc = lineColOf(mutation.start_pos)\n const mutant: Mutant = {\n id: `${path}:${index}`,\n mutatorName: mutation.operator_name ?? 'unknown',\n status,\n }\n if (loc) mutant.location = { start: loc }\n const entry = files[path] ?? { language: 'python', mutants: [] }\n entry.mutants.push(mutant)\n files[path] = entry\n index++\n }\n return { files }\n}\n","/**\n * mutmut adapter (the Python mutation-testing tool) — converts `mutmut results --all true`\n * output into the mutation-testing-elements {@link MutationReport} that {@link summarizeMutation}\n * already consumes, so the summarizer is reused unchanged across Stryker (JS) and mutmut (Python).\n *\n * mutmut 3.x has no native mutation-testing-elements / JSON-per-mutant report; its richest\n * machine-readable per-mutant output is `mutmut results --all true`, one line per mutant:\n *\n * calc.x_add__mutmut_1: killed\n * calc.x_sub__mutmut_1: survived\n * calc.x_mul__mutmut_1: no tests\n *\n * (Captured from mutmut 3.5.0 — see `test/fixtures/mutmut-results.txt`.) The mutant name is\n * `<dotted module>.x_<function>__mutmut_<n>`, so we group mutants by their module as the \"file\"\n * key (mutmut does not surface a source path or line here — `line` is 0, `mutatorName` is the\n * mutant name, the best identifier available).\n *\n * Status vocabulary mapping (mutmut → mutation-testing-elements), chosen to never overstate the\n * score: `suspicious` (the suite behaved oddly — not a confirmed kill) maps to `Survived` so it\n * surfaces as a gap; `segfault` maps to `RuntimeError` (invalid, excluded from the score); an\n * unrecognized status maps to `Pending` (neutral — excluded from `valid`, not a survivor).\n */\n\nimport type { MutantStatus, MutationFile, MutationReport } from './summarize.js'\n\nconst MUTMUT_STATUS: Record<string, MutantStatus> = {\n killed: 'Killed',\n survived: 'Survived',\n 'no tests': 'NoCoverage',\n timeout: 'Timeout',\n suspicious: 'Survived',\n skipped: 'Ignored',\n segfault: 'RuntimeError',\n}\n\n/** The module portion of a mutmut mutant name (`pkg.mod.x_fn__mutmut_3` → `pkg.mod`). */\nfunction moduleOf(name: string): string {\n const m = /^(.*)\\.x_.+__mutmut_\\d+$/.exec(name)\n if (m?.[1]) return m[1]\n const dot = name.lastIndexOf('.')\n return dot > 0 ? name.slice(0, dot) : name\n}\n\n/** Parse `mutmut results --all true` text into a mutation-testing-elements report. Pure. */\nexport function parseMutmutResults(text: string): MutationReport {\n const files: Record<string, MutationFile> = {}\n for (const raw of text.split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line) continue\n const idx = line.indexOf(':')\n if (idx === -1) continue\n const name = line.slice(0, idx).trim()\n if (!name) continue\n const statusText = line\n .slice(idx + 1)\n .trim()\n .toLowerCase()\n const status = MUTMUT_STATUS[statusText] ?? 'Pending'\n const file = moduleOf(name)\n const entry = files[file] ?? { mutants: [] }\n entry.mutants.push({ id: name, mutatorName: name, status })\n files[file] = entry\n }\n return { files }\n}\n","/**\n * Pure scope-selection + post-spawn reconciliation primitives for Python mutation diff-scoping\n * (ADR 0010 addendum 2). The config EMITTERS (cosmic-ray TOML / mutmut pyproject) and the runner\n * wiring are STAGED pending slice-0 tool-fact capture; these two functions are the load-bearing,\n * tool-agnostic SAFETY core and ship first (pure, fixture-tested, no real tool in `pnpm gate`):\n *\n * - {@link selectMutationScope} mirrors coverage's `selectPytestScope` (incl. its INJECTED\n * existence predicate): turn the changed-file list into the set of mutable, existing, in-tree\n * `.py` files to scope a mutation run to, surfacing everything else as `unmatched` (report-gap;\n * never silently folded into the scope).\n * - {@link reconcileScope} is the POST-SPAWN partial-under-scope guard (the design's load-bearing\n * correction): the existing `assertComplete` only catches TOTAL-zero mutants, so it is blind to\n * a run that mutated only a SUBSET of the selected files — a clean score for files that were\n * never mutated, i.e. absence-as-a-pass. reconcileScope reports which selected files the tool\n * actually mutated and which it NEVER SAW; the (staged) runner throws ⇒ inconclusive on any\n * never-seen selected file.\n */\n\nimport type { MutationSummary } from './summarize.js'\n\n/** Normalize a path to a comparable repo-relative POSIX form (backslashes → `/`, drop a leading `./`). */\nfunction normalizePath(p: string): string {\n return p.replace(/\\\\/g, '/').replace(/^\\.\\//, '')\n}\n\nexport interface MutationScope {\n /** Mutable, existing, in-tree `.py` files to scope the run to (repo-relative, normalized, deduped, sorted). */\n files: string[]\n /** Changed `.py` files that are out-of-tree, deleted, or otherwise unplaceable — surfaced as a\n * gap (report-gap), NEVER silently folded into `files` (that would be absence-as-a-pass). */\n unmatched: string[]\n}\n\n/**\n * Derive the mutation scope from the changed files. A changed file is selected only when it is a\n * `.py` file, lives under one of the operator's `ownedRoots`, AND exists on disk (`exists`,\n * injected — FS by default in the runner, faked in tests, mirroring `selectPytestScope`'s\n * `testExists`). A `.py` file that is out-of-tree or non-existent (deleted/renamed/typo) is\n * `unmatched`, never scoped. Non-`.py` files are irrelevant to mutation and dropped. Pure given\n * `exists`. Whole-project fallback is NOT decided here — it is the caller's (`mutateFiles ===\n * undefined`) or the cosmic-ray emitter's (`no-scope` on the base config) concern.\n */\nexport function selectMutationScope(\n mutateFiles: string[],\n ownedRoots: string[],\n exists: (path: string) => boolean,\n): MutationScope {\n const roots = ownedRoots.map(normalizePath)\n const underRoot = (p: string): boolean => roots.some((r) => p === r || p.startsWith(`${r}/`))\n const files = new Set<string>()\n const unmatched = new Set<string>()\n for (const raw of mutateFiles) {\n const p = normalizePath(raw)\n if (!p.endsWith('.py')) continue // not a Python source file — irrelevant to mutation\n if (!underRoot(p) || !exists(p)) {\n unmatched.add(p) // out-of-tree, deleted, renamed, or typo'd — a gap, never scoped\n continue\n }\n files.add(p)\n }\n return { files: [...files].sort(), unmatched: [...unmatched].sort() }\n}\n\nexport interface ScopeReconciliation {\n /** Selected files the tool ACTUALLY mutated (≥1 mutant) — report this as the run's true scope,\n * never the merely-requested set. */\n mutatedFiles: string[]\n /** Selected files the tool NEVER SAW (absent from the per-file summary) — partial under-scope.\n * A non-empty `missing` MUST make the run inconclusive (absence-as-a-pass otherwise). */\n missing: string[]\n}\n\n/**\n * Post-spawn partial-under-scope guard. Given the files the run was SELECTED to mutate and the\n * tool's {@link MutationSummary} (whose `files[]` carries a per-file record for every file the\n * tool SAW — even one it found no mutants in), determine which selected files were genuinely\n * mutated and which the tool never saw at all. A selected file present in the summary with zero\n * mutants is SEEN-BUT-EMPTY (benign — no mutable code); a selected file ABSENT from the summary\n * was never mutated (the partial-scope sentinel) and goes in `missing`. Paths are normalized on\n * both sides before comparison. Pure.\n */\nexport function reconcileScope(selected: string[], summary: MutationSummary): ScopeReconciliation {\n const seen = new Set(summary.files.map((f) => normalizePath(f.path)))\n const mutated = new Set(\n summary.files.filter((f) => f.metrics.totalMutants > 0).map((f) => normalizePath(f.path)),\n )\n const sel = [...new Set(selected.map(normalizePath))].sort()\n return {\n mutatedFiles: sel.filter((p) => mutated.has(p)),\n missing: sel.filter((p) => !seen.has(p)),\n }\n}\n\n/**\n * Convert a Python source PATH to its dotted module form (`pkg/calc.py` → `pkg.calc`,\n * `pkg/__init__.py` → `pkg`). The package-relative prefix is unknown here, so this is the FULL\n * relative-path dotted form; {@link reconcileMutmutScope} matches it against mutmut's module names\n * by suffix to tolerate src-layout projects.\n */\nexport function pyPathToModule(path: string): string {\n let p = normalizePath(path).replace(/\\.py$/, '')\n if (p.endsWith('/__init__')) p = p.slice(0, -'/__init__'.length)\n return p.replace(/\\//g, '.')\n}\n\n/**\n * The mutmut-specific post-spawn guard. Unlike cosmic-ray, mutmut's {@link MutationSummary} is keyed\n * by DOTTED MODULE (`pkg.calc`), not path, AND it emits NO record for a scoped file that produced\n * zero mutants (slice 0: seen-but-empty is INDISTINGUISHABLE from never-seen — Fork B2). So this is\n * deliberately CONSERVATIVE: a selected file is \"mutated\" only if some module with ≥1 mutant matches\n * its dotted form (equal, or a suffix either way — tolerating flat AND src layouts); every other\n * selected file is `missing` (⇒ the runner throws inconclusive). Some false-inconclusives, ZERO\n * false-passes (absence-is-never-a-pass). Pure.\n */\nexport function reconcileMutmutScope(\n selectedPaths: string[],\n summary: MutationSummary,\n): ScopeReconciliation {\n const mutatedModules = summary.files\n .filter((f) => f.metrics.totalMutants > 0)\n .map((f) => normalizePath(f.path)) // mutmut paths ARE dotted modules\n const matches = (mod: string, sel: string): boolean =>\n mod === sel || mod.endsWith(`.${sel}`) || sel.endsWith(`.${mod}`)\n const sel = [...new Set(selectedPaths.map(normalizePath))].sort()\n const mutatedFiles: string[] = []\n const missing: string[] = []\n for (const path of sel) {\n const mod = pyPathToModule(path)\n if (mutatedModules.some((m) => matches(m, mod))) mutatedFiles.push(path)\n else missing.push(path)\n }\n return { mutatedFiles, missing }\n}\n","/**\n * Pure mutation-report summarizer — the first slice of `@sackville-mcp/mutate`, and the one\n * with no I/O and no Stryker dependency.\n *\n * Mutation testing asks \"are the tests meaningful?\" — it perturbs the source (a `+`\n * becomes `-`, a `true` becomes `false`) and re-runs the suite; a mutant that survives is\n * a behaviour the tests do not actually pin down. Under Sackville's TDD gate the agent\n * wrote a passing test, but a passing test can still assert nothing useful — surviving\n * mutants are the catch for that, the natural complement to coverage's forgotten-assertion\n * catch (covered-but-unkilled vs added-but-uncovered).\n *\n * This module reads the **mutation-testing-elements report schema** (`schemaVersion`,\n * `files[path].mutants[].status`) that Stryker emits as `mutation-report.json`. The schema\n * is stable and decoupled from the Stryker version (ADR 0010 update 2026-06-01), so this\n * core carries no `@stryker-mutator/*` import — it is unit-tested against a committed\n * golden report. Producing a real report is a gated, injected `stryker run` (a later slice).\n *\n * Metric definitions mirror mutation-testing-elements exactly:\n * detected = killed + timeout\n * undetected = survived + noCoverage\n * covered = detected + survived (NoCoverage excluded — it was never run)\n * valid = detected + undetected\n * invalid = compileErrors + runtimeErrors\n * total = valid + invalid + ignored + pending\n * mutationScore = detected / valid (null when valid === 0)\n * mutationScoreBasedOnCoveredCode = detected / covered (null when covered === 0)\n */\n\n/** Mutant statuses from the mutation-testing-elements schema. */\nexport type MutantStatus =\n | 'Killed'\n | 'Survived'\n | 'NoCoverage'\n | 'Timeout'\n | 'CompileError'\n | 'RuntimeError'\n | 'Ignored'\n | 'Pending'\n\nexport interface MutantPosition {\n line: number\n column?: number\n}\n\nexport interface Mutant {\n id: string\n mutatorName: string\n status: MutantStatus\n replacement?: string\n location?: { start: MutantPosition; end?: MutantPosition }\n}\n\nexport interface MutationFile {\n language?: string\n source?: string\n mutants: Mutant[]\n}\n\nexport interface MutationReport {\n schemaVersion?: string\n thresholds?: { high: number; low: number }\n files: Record<string, MutationFile>\n}\n\nexport interface StatusCounts {\n killed: number\n survived: number\n timeout: number\n noCoverage: number\n compileErrors: number\n runtimeErrors: number\n ignored: number\n pending: number\n}\n\nexport interface MutationMetrics {\n counts: StatusCounts\n detected: number\n undetected: number\n covered: number\n valid: number\n invalid: number\n totalMutants: number\n /** detected / valid (percent); null when there are no valid mutants. */\n mutationScore: number | null\n /** detected / covered (percent); null when no covered mutants. */\n mutationScoreBasedOnCoveredCode: number | null\n}\n\nexport interface FileSummary {\n path: string\n metrics: MutationMetrics\n}\n\n/** A surviving (or never-covered) mutant — the actionable test gap. */\nexport interface Survivor {\n file: string\n mutatorName: string\n status: 'Survived' | 'NoCoverage'\n line: number\n}\n\nexport interface MutationSummary {\n metrics: MutationMetrics\n files: FileSummary[]\n /** Survived + NoCoverage mutants, sorted by file then line — what to go fix. */\n survivors: Survivor[]\n}\n\nconst ZERO: StatusCounts = {\n killed: 0,\n survived: 0,\n timeout: 0,\n noCoverage: 0,\n compileErrors: 0,\n runtimeErrors: 0,\n ignored: 0,\n pending: 0,\n}\n\nfunction tally(mutants: Mutant[]): StatusCounts {\n const c: StatusCounts = { ...ZERO }\n for (const m of mutants) {\n switch (m.status) {\n case 'Killed':\n c.killed++\n break\n case 'Survived':\n c.survived++\n break\n case 'Timeout':\n c.timeout++\n break\n case 'NoCoverage':\n c.noCoverage++\n break\n case 'CompileError':\n c.compileErrors++\n break\n case 'RuntimeError':\n c.runtimeErrors++\n break\n case 'Ignored':\n c.ignored++\n break\n case 'Pending':\n c.pending++\n break\n }\n }\n return c\n}\n\nfunction metricsFrom(c: StatusCounts): MutationMetrics {\n const detected = c.killed + c.timeout\n const undetected = c.survived + c.noCoverage\n const covered = detected + c.survived\n const valid = detected + undetected\n const invalid = c.compileErrors + c.runtimeErrors\n const totalMutants = valid + invalid + c.ignored + c.pending\n return {\n counts: c,\n detected,\n undetected,\n covered,\n valid,\n invalid,\n totalMutants,\n mutationScore: valid > 0 ? (detected / valid) * 100 : null,\n mutationScoreBasedOnCoveredCode: covered > 0 ? (detected / covered) * 100 : null,\n }\n}\n\nfunction sumCounts(a: StatusCounts, b: StatusCounts): StatusCounts {\n return {\n killed: a.killed + b.killed,\n survived: a.survived + b.survived,\n timeout: a.timeout + b.timeout,\n noCoverage: a.noCoverage + b.noCoverage,\n compileErrors: a.compileErrors + b.compileErrors,\n runtimeErrors: a.runtimeErrors + b.runtimeErrors,\n ignored: a.ignored + b.ignored,\n pending: a.pending + b.pending,\n }\n}\n\n/** Summarize a Stryker mutation report into aggregate + per-file metrics and the survivor list. Pure. */\nexport function summarizeMutation(report: MutationReport): MutationSummary {\n const files: FileSummary[] = []\n const survivors: Survivor[] = []\n let total: StatusCounts = { ...ZERO }\n\n for (const path of Object.keys(report.files).sort()) {\n const file = report.files[path]\n if (!file) continue\n const counts = tally(file.mutants)\n total = sumCounts(total, counts)\n files.push({ path, metrics: metricsFrom(counts) })\n\n for (const m of file.mutants) {\n if (m.status === 'Survived' || m.status === 'NoCoverage') {\n survivors.push({\n file: path,\n mutatorName: m.mutatorName,\n status: m.status,\n line: m.location?.start.line ?? 0,\n })\n }\n }\n }\n\n survivors.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line))\n\n return { metrics: metricsFrom(total), files, survivors }\n}\n","/**\n * The gated, diff-scoped mutation run — the live half of `@sackville-mcp/mutate`. It **spawns**\n * Stryker (`stryker run`, an injected subprocess like flake's `vitest` and coverage's\n * `vitest related`), then reads the JSON report Stryker writes and feeds it to the pure\n * {@link summarizeMutation}.\n *\n * Per ADR 0010 (+ its 2026-06-01 spike update):\n * 1. **It runs code** — and a mutation run is *expensive* (the suite re-runs per mutant) —\n * so it sits behind the house paired deny-by-default operator gate (`allowRun` +\n * `allowedRoots` allowlist, load-bearing on its own, + a wall-clock cap). All\n * operator-set; no caller input self-authorizes.\n * 2. **Stryker is NOT a dependency of this package.** A real mutation run is slow and\n * non-deterministic, so it never runs in `pnpm gate`. The `stryker` invocation is the\n * injected {@link MutationRunner} (the bin spawns the operator's local Stryker); the\n * engine owns the gate, argv, report plumbing, and summary, and is unit-tested with a\n * fake runner.\n * 3. **Diff-scoped.** `mutateFiles` (the changed source files) become Stryker's `--mutate`\n * glob list, and `--incremental` reuses Stryker's cache — so a change mutates only what\n * it touched, not the whole tree.\n */\n\nimport { execFile } from 'node:child_process'\nimport {\n cpSync,\n existsSync,\n mkdtempSync,\n readdirSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport {\n cosmicModulePathRoots,\n mutmutDoNotMutate,\n mutmutPathsToMutate,\n planMutmutScope,\n synthesizeScopedCosmicRayConfig,\n synthesizeScopedMutmutPyproject,\n} from './config.js'\nimport { parseCosmicRayDump } from './cosmic-ray.js'\nimport { parseMutmutResults } from './mutmut.js'\nimport { reconcileMutmutScope, reconcileScope, selectMutationScope } from './scope.js'\nimport { type MutationReport, type MutationSummary, summarizeMutation } from './summarize.js'\n\n/** The zero-mutant summary returned by a pre-spawn noop (folds to no-signal ⇒ inconclusive). */\nfunction emptyMutationSummary(): MutationSummary {\n return summarizeMutation({ files: {} })\n}\n\n/** Read a file, or undefined if it is absent (a missing base config is not an error). */\nfunction readFileIfExists(path: string): string | undefined {\n try {\n return readFileSync(path, 'utf8')\n } catch {\n return undefined\n }\n}\n\n/** Directories never copied into the mutmut sandbox (heavy / irrelevant / the sticky mutants cache). */\nconst SANDBOX_EXCLUDE =\n /(?:^|\\/)(?:node_modules|\\.git|\\.venv|venv|__pycache__|mutants|\\.mutmut-cache|dist|\\.tox)(?:\\/|$)/\n\n/** Recursively list the `.py` files under each owned root, repo-relative (FS default for runMutmut). */\nfunction defaultListSources(ownedRoots: string[], projectRoot: string): string[] {\n const out: string[] = []\n for (const root of ownedRoots) {\n const abs = join(projectRoot, root)\n let entries: string[]\n try {\n entries = readdirSync(abs, { recursive: true }) as string[]\n } catch {\n continue // a declared root that is a single file or absent — skip (selectMutationScope handles files)\n }\n for (const rel of entries) {\n const p = rel.replace(/\\\\/g, '/')\n if (p.endsWith('.py') && !SANDBOX_EXCLUDE.test(p))\n out.push(`${root}/${p}`.replace(/\\/+/g, '/'))\n }\n }\n return out\n}\n\n/** Copy a project into a fresh sandbox dir, excluding heavy dirs + the sticky `mutants/` cache. */\nfunction copyProjectInto(from: string, to: string): void {\n cpSync(from, to, {\n recursive: true,\n filter: (src) => !SANDBOX_EXCLUDE.test(src.slice(from.length).replace(/\\\\/g, '/')),\n })\n}\n\n/** Thrown when the paired operator gate denies a run. */\nexport class MutateGateError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'MutateGateError'\n // Brand as a gate DENIAL (ADR 0013 Addendum, milestone 5c): the run-driving\n // `@sackville-mcp/verify` reads this global-registry symbol via `isGateDenial` to map a\n // denial to `skipReason:'gate-not-set'` (never `errored`) WITHOUT importing engine\n // code. The `Symbol.for` key string is the cross-package contract.\n ;(this as unknown as Record<symbol, unknown>)[Symbol.for('sackville.gate-denial')] = true\n }\n}\n\nexport interface RunMutationConfig {\n /** The project to run Stryker in. */\n projectRoot: string\n /** OPERATOR allowlist of roots the runner may execute in. Load-bearing even with allowRun. */\n allowedRoots: string[]\n /** OPERATOR opt-in to actually run mutation testing. Deny-by-default. */\n allowRun: boolean\n /** Wall-clock cap (ms) passed to the runner. */\n timeoutMs?: number\n}\n\nexport interface RunMutationInput {\n /**\n * Changed source files to scope mutation to. Stryker → `--mutate`; the Python tools → a synthesized\n * scoped config (cosmic-ray `module-path` list / mutmut `paths_to_mutate`). `undefined` ⇒ the\n * project default (whole project, today's behavior); a supplied list scopes the run, and a selection\n * that resolves to no mutable in-tree `.py` is a pre-spawn noop (`ran:false`, never a spawn).\n */\n mutateFiles?: string[]\n /** Reuse Stryker's incremental cache (`--incremental`) — faster re-runs. */\n incremental?: boolean\n /** cosmic-ray config path (relative to projectRoot). Default `cosmic-ray.toml`. */\n configPath?: string\n /**\n * OPTIONAL operator source roots the diff scope is confined to (Fork C). A changed `.py` outside\n * them is `unmatched` (report-gap), never scoped. Default: the tool config's declared source tree\n * (cosmic-ray `module-path`).\n */\n ownedRoots?: string[]\n}\n\n/** Injected command runner — executes `stryker <argv>` and yields its exit status. */\nexport type MutationRunner = (\n argv: string[],\n opts: { cwd: string; timeoutMs?: number },\n) => Promise<{ exitCode: number; stdout: string; stderr: string }>\n\nexport interface RunMutationResult {\n ran: boolean\n exitCode: number\n /** Files mutation was scoped to (empty ⇒ the project's configured set). */\n scopedFiles: string[]\n /** Which mutation tool produced the summary. */\n tool?: 'stryker' | 'mutmut' | 'cosmic-ray'\n /**\n * The on-disk JSON report path (Stryker). Optional: the Python tools (mutmut/cosmic-ray) emit\n * their report to STDOUT, so there is no report file to surface.\n */\n reportPath?: string\n summary: MutationSummary\n /** A scope was requested but resolved to no mutable in-tree `.py` ⇒ pre-spawn noop (case (a)). */\n scopeEmpty?: boolean\n /** Changed `.py` files outside the owned tree / deleted — surfaced as a gap (Fork C), never scoped. */\n unmatched?: string[]\n /** The files the run was SELECTED to mutate (before reconciliation) — what we asked the tool for. */\n requestedFiles?: string[]\n}\n\n/** Stryker's default JSON-report location, relative to the project root. */\nfunction defaultReportPath(projectRoot: string): string {\n return join(projectRoot, 'reports', 'mutation', 'mutation.json')\n}\n\n/** Build the `stryker run` argv: JSON reporter, optional diff scope + incremental cache. */\nfunction runArgv(input: RunMutationInput): string[] {\n const argv = ['run', '--reporters', 'json']\n if (input.mutateFiles && input.mutateFiles.length > 0) {\n argv.push('--mutate', input.mutateFiles.join(','))\n }\n if (input.incremental) argv.push('--incremental')\n return argv\n}\n\n/** Spawn a local command as a subprocess, surfacing its exit code (never rejecting on non-zero). */\nfunction spawnMutationRunner(command: string): MutationRunner {\n return (argv, opts) =>\n new Promise((res) => {\n execFile(\n command,\n argv,\n { cwd: opts.cwd, timeout: opts.timeoutMs, maxBuffer: 64 * 1024 * 1024 },\n (err, stdout, stderr) => {\n const code =\n err && typeof (err as { code?: unknown }).code === 'number'\n ? (err as { code: number }).code\n : err\n ? 1\n : 0\n res({ exitCode: code, stdout: String(stdout), stderr: String(stderr) })\n },\n )\n })\n}\n\n/** Default live runner: spawn the local `stryker` as a subprocess (used by the bin, not the gate). */\nexport const defaultStrykerRunner: MutationRunner = spawnMutationRunner('stryker')\n\n/** Default live runner: spawn the local `mutmut` as a subprocess (used by the bin, not the gate). */\nexport const defaultMutmutRunner: MutationRunner = spawnMutationRunner('mutmut')\n\n/** Default live runner: spawn the local `cosmic-ray` as a subprocess (used by the bin, not the gate). */\nexport const defaultCosmicRayRunner: MutationRunner = spawnMutationRunner('cosmic-ray')\n\nfunction assertAllowed(config: RunMutationConfig): void {\n if (!config.allowRun) {\n throw new MutateGateError('mutation runs are not enabled (the operator must set allowRun)')\n }\n const root = resolve(config.projectRoot)\n const allowed = config.allowedRoots.map((r) => resolve(r))\n if (!allowed.includes(root)) {\n throw new MutateGateError(`project root ${config.projectRoot} is not in the operator allowlist`)\n }\n}\n\n/**\n * Run mutation testing behind the operator gate and summarize the report. The actual\n * `stryker` invocation is the injected `runner` (default {@link defaultStrykerRunner}); the\n * JSON report is read from `deps.reportPath` (default: Stryker's\n * `<projectRoot>/reports/mutation/mutation.json`).\n */\nexport async function runMutation(\n config: RunMutationConfig,\n input: RunMutationInput,\n deps: { runner?: MutationRunner; reportPath?: string } = {},\n): Promise<RunMutationResult> {\n assertAllowed(config)\n\n const runner = deps.runner ?? defaultStrykerRunner\n const reportPath = deps.reportPath ?? defaultReportPath(config.projectRoot)\n const argv = runArgv(input)\n\n const { exitCode } = await runner(argv, {\n cwd: config.projectRoot,\n timeoutMs: config.timeoutMs,\n })\n\n let report: MutationReport\n try {\n report = JSON.parse(readFileSync(reportPath, 'utf8')) as MutationReport\n } catch {\n throw new Error(\n `mutation run did not produce a JSON report at ${reportPath} (exit code ${exitCode}); ` +\n 'ensure the project enables the Stryker `json` reporter',\n )\n }\n\n return {\n ran: true,\n exitCode,\n scopedFiles: input.mutateFiles ?? [],\n tool: 'stryker',\n reportPath,\n summary: summarizeMutation(report),\n }\n}\n\n/**\n * Transport-completeness guard for the Python tools, mirroring the capture/HAR guards: a run that\n * produced NO mutants (an empty/failed session), or a cosmic-ray session with unexecuted/ambiguous\n * (`Pending`) mutants, is INCONCLUSIVE — it must never be reported as a clean pass\n * (absence-is-never-a-pass). Throws so the caller (verify) folds it to inconclusive.\n */\nfunction assertComplete(tool: string, summary: MutationSummary): void {\n if (summary.metrics.totalMutants === 0) {\n throw new Error(`${tool} produced no mutants — inconclusive (never a clean pass)`)\n }\n if (summary.metrics.counts.pending > 0) {\n throw new Error(\n `${tool} session is incomplete: ${summary.metrics.counts.pending} unexecuted/ambiguous ` +\n 'mutant(s) — inconclusive',\n )\n }\n}\n\n/**\n * mutmut sibling of {@link runMutation} (ADR 0010 addendum, the lightweight Python option). Spawns\n * `mutmut run` (mutate + test; a non-zero exit just means survivors exist, not an error) then reads\n * `mutmut results --all true` from STDOUT and feeds the pure {@link parseMutmutResults}. No report\n * file. (Diff-scoping is staged — mutmut 3.x scopes via its own config, not a clean CLI file list.)\n */\nexport async function runMutmut(\n config: RunMutationConfig,\n input: RunMutationInput,\n deps: {\n runner?: MutationRunner\n /** Existence check for the selected files (FS by default; injected in tests). */\n exists?: (path: string) => boolean\n /** Enumerate the `.py` files under the owned roots (for `also_copy`); FS walk by default. */\n listSourceFiles?: (ownedRoots: string[], projectRoot: string) => string[]\n /** The fresh sandbox cwd to run in (the sticky `mutants/` cache can't leak). Default mkdtemp. */\n sandboxDir?: string\n } = {},\n): Promise<RunMutationResult> {\n assertAllowed(config)\n const runner = deps.runner ?? defaultMutmutRunner\n\n // Whole-project (today's behavior) in projectRoot when no scope is requested.\n if (input.mutateFiles === undefined) {\n const opts = { cwd: config.projectRoot, timeoutMs: config.timeoutMs }\n await runner(['run'], opts)\n const { exitCode, stdout } = await runner(['results', '--all', 'true'], opts)\n const summary = summarizeMutation(parseMutmutResults(stdout))\n assertComplete('mutmut', summary)\n return { ran: true, exitCode, scopedFiles: [], tool: 'mutmut', summary }\n }\n\n // Diff-scoped: confine to the owned tree, then select mutable existing .py files.\n const pyprojectPath = input.configPath ?? 'pyproject.toml'\n const basePyproject = readFileIfExists(join(config.projectRoot, pyprojectPath)) ?? ''\n const ownedRoots = input.ownedRoots ?? mutmutPathsToMutate(basePyproject)\n const exists = deps.exists ?? ((p: string) => existsSync(join(config.projectRoot, p)))\n const { files, unmatched } = selectMutationScope(input.mutateFiles, ownedRoots, exists)\n const unmatchedOut = unmatched.length > 0 ? unmatched : undefined\n\n // Case (a): nothing mutable in-tree remains — DO NOT spawn (noop).\n if (files.length === 0) {\n return {\n ran: false,\n exitCode: 0,\n scopedFiles: [],\n tool: 'mutmut',\n summary: emptyMutationSummary(),\n scopeEmpty: true,\n unmatched: unmatchedOut,\n requestedFiles: [],\n }\n }\n\n // Plan the scope: paths_to_mutate = selected files; also_copy = the rest of the source tree so\n // unscoped tests still import (slice 0); strip a colliding inherited do_not_mutate glob.\n const listSources = deps.listSourceFiles ?? defaultListSources\n const allSources = listSources(ownedRoots, config.projectRoot)\n const plan = planMutmutScope(files, allSources, mutmutDoNotMutate(basePyproject))\n const scopedPyproject = synthesizeScopedMutmutPyproject(basePyproject, plan)\n\n // Fresh sandbox cwd (the mutants/ cache is sticky), with the scoped pyproject written over the copy.\n const sandbox = deps.sandboxDir ?? mkdtempSync(join(tmpdir(), 'sackville-mutmut-'))\n copyProjectInto(config.projectRoot, sandbox)\n writeFileSync(join(sandbox, 'pyproject.toml'), scopedPyproject)\n\n const opts = { cwd: sandbox, timeoutMs: config.timeoutMs }\n await runner(['run'], opts)\n const { exitCode, stdout } = await runner(['results', '--all', 'true'], opts)\n const summary = summarizeMutation(parseMutmutResults(stdout))\n // A broken scoped baseline yields \"not checked\" ⇒ Pending ⇒ assertComplete throws (slice 0): this\n // IS the baseline-smoke gate. Total-zero is likewise inconclusive (case b).\n assertComplete('mutmut', summary)\n // Case (c): mutmut emits NO record for a 0-mutant selected file (Fork B2), so a selected file with\n // no matching mutated module was never mutated ⇒ inconclusive (conservative; never a false pass).\n const { mutatedFiles, missing } = reconcileMutmutScope(files, summary)\n if (missing.length > 0) {\n throw new Error(\n `mutmut under-scoped: ${missing.join(', ')} selected but produced no mutants — inconclusive`,\n )\n }\n return {\n ran: true,\n exitCode,\n scopedFiles: mutatedFiles,\n tool: 'mutmut',\n summary,\n requestedFiles: files,\n unmatched: unmatchedOut,\n }\n}\n\n/**\n * cosmic-ray sibling of {@link runMutation} (ADR 0010 addendum, the PRIMARY Python tool — its dump\n * carries real file:line:operator, so survivors are actionable). Drives the three-step workflow\n * against an operator-authored config (`input.configPath`, default `cosmic-ray.toml` — it carries\n * the project's test-command + module scope) over a throwaway session DB: `init` → `exec` → `dump`,\n * reading the `dump` JSON-lines from STDOUT and feeding the pure {@link parseCosmicRayDump}. The\n * {@link assertComplete} guard makes an empty or partially-executed session inconclusive, never a\n * clean pass. (Diff-scoping by synthesizing the per-run config from `mutateFiles` is staged.)\n */\nexport async function runCosmicRay(\n config: RunMutationConfig,\n input: RunMutationInput,\n deps: {\n runner?: MutationRunner\n sessionDir?: string\n /** Existence check for the selected files (FS by default; injected in tests). */\n exists?: (path: string) => boolean\n /** Scoped config filename written into projectRoot (relative). Default `.sackville-cosmic.toml`. */\n scopedConfigName?: string\n } = {},\n): Promise<RunMutationResult> {\n assertAllowed(config)\n const runner = deps.runner ?? defaultCosmicRayRunner\n const opts = { cwd: config.projectRoot, timeoutMs: config.timeoutMs }\n const configPath = input.configPath ?? 'cosmic-ray.toml'\n const sessionDir = deps.sessionDir ?? mkdtempSync(join(tmpdir(), 'sackville-mutate-'))\n const session = join(sessionDir, 'session.sqlite')\n\n // Whole-project (today's behavior) when no scope is requested.\n if (input.mutateFiles === undefined) {\n await runner(['init', configPath, session], opts)\n const exec = await runner(['exec', configPath, session], opts)\n const { stdout } = await runner(['dump', session], opts)\n const summary = summarizeMutation(parseCosmicRayDump(stdout))\n assertComplete('cosmic-ray', summary)\n return { ran: true, exitCode: exec.exitCode, scopedFiles: [], tool: 'cosmic-ray', summary }\n }\n\n // Diff-scoped: confine the changed files to the owned source tree, then select mutable existing .py.\n const baseToml = readFileSync(join(config.projectRoot, configPath), 'utf8')\n const ownedRoots = input.ownedRoots ?? cosmicModulePathRoots(baseToml)\n const exists = deps.exists ?? ((p: string) => existsSync(join(config.projectRoot, p)))\n const { files, unmatched } = selectMutationScope(input.mutateFiles, ownedRoots, exists)\n const unmatchedOut = unmatched.length > 0 ? unmatched : undefined\n\n // Case (a): a scope was requested but nothing mutable in-tree remains — DO NOT spawn (noop).\n if (files.length === 0) {\n return {\n ran: false,\n exitCode: 0,\n scopedFiles: [],\n tool: 'cosmic-ray',\n summary: emptyMutationSummary(),\n scopeEmpty: true,\n unmatched: unmatchedOut,\n requestedFiles: [],\n }\n }\n\n // Synthesize a scoped config (module-path = selected files, excluded-modules reconciled). It must\n // live in projectRoot so its RELATIVE module-path resolves there (cosmic-ray then reports relative\n // module_path keys, which reconcileScope compares against the selected files directly).\n const scoped = synthesizeScopedCosmicRayConfig(baseToml, files)\n const scopedName = deps.scopedConfigName ?? '.sackville-cosmic.toml'\n const scopedAbs = join(config.projectRoot, scopedName)\n writeFileSync(scopedAbs, scoped.toml)\n try {\n await runner(['init', scopedName, session], opts)\n const exec = await runner(['exec', scopedName, session], opts)\n const { stdout } = await runner(['dump', session], opts)\n const summary = summarizeMutation(parseCosmicRayDump(stdout))\n assertComplete('cosmic-ray', summary) // case (b): total-zero / pending ⇒ inconclusive\n // Case (c): a selected file the tool never SAW was silently never mutated ⇒ inconclusive.\n const { mutatedFiles, missing } = reconcileScope(files, summary)\n if (missing.length > 0) {\n throw new Error(\n `cosmic-ray under-scoped: ${missing.join(', ')} selected but never mutated — inconclusive`,\n )\n }\n // Case (d): clean scoped run — report what was GENUINELY mutated, not what was requested.\n return {\n ran: true,\n exitCode: exec.exitCode,\n scopedFiles: mutatedFiles,\n tool: 'cosmic-ray',\n summary,\n requestedFiles: files,\n unmatched: unmatchedOut,\n }\n } finally {\n rmSync(scopedAbs, { force: true })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["normalizePath"],"sources":["../src/config.ts","../src/cosmic-ray.ts","../src/mutmut.ts","../src/scope.ts","../src/summarize.ts","../src/run.ts"],"sourcesContent":["/**\n * Diff-scoping config EMITTERS for the Python mutation tools (ADR 0010 addendum 2). These turn a\n * selected-file scope into a per-run tool config, PINNED to the slice-0 captures of the installed\n * cosmic-ray 8.4.6 / mutmut 3.5.0 (see `test/fixtures/README.md`) — never doc-derived guesses:\n *\n * - cosmic-ray scopes via a `module-path` FILE LIST (Fork A — verified 8.4.6 accepts a list); its\n * `excluded-modules` SUBTRACTS from the scope via exact path AND fnmatch glob, so an inherited\n * exclusion that matches a selected file is reconciled (stripped), never copied blind (blocker #3).\n * - mutmut scopes via `paths_to_mutate` (Fork B was WRONG — 3.5.0 has NO `only_mutate`/`source_paths`;\n * `paths_to_mutate` + `do_not_mutate` are the real keys, Fork F). Scoping a subset breaks the\n * baseline unless the rest of the source tree is `also_copy`'d so unscoped tests still import; an\n * inherited `do_not_mutate` glob matching a selected file is stripped (blocker #3, mutmut form).\n *\n * Both emitters are PURE (TOML in → TOML out via `smol-toml`). They reduce SPURIOUS inconclusives by\n * reconciling exclusions up front; the load-bearing under-scope guarantee is the POST-SPAWN\n * {@link reconcileScope} guard in the runner, which folds any genuinely-unmutated selected file to\n * inconclusive regardless of why (absence-is-never-a-pass).\n */\n\nimport { parse, stringify } from 'smol-toml'\n\n/** Thrown when a scoped config cannot be safely synthesized (empty scope, missing base table, etc.). */\nexport class ScopeEmitError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ScopeEmitError'\n }\n}\n\n/** Normalize a path to a comparable repo-relative POSIX form (backslashes → `/`, drop a leading `./`). */\nfunction normalizePath(p: string): string {\n return p.replace(/\\\\/g, '/').replace(/^\\.\\//, '')\n}\n\n/** Dedupe + normalize + sort, the canonical scope form used everywhere. */\nfunction canonicalize(paths: string[]): string[] {\n return [...new Set(paths.map(normalizePath))].sort()\n}\n\n/**\n * Translate a Python `fnmatch` pattern to an anchored RegExp. Mirrors `fnmatch.translate`: a star →\n * `.*` (crosses `/`, unlike shell glob — so a `star + /strutil.py` glob matches `pkg/strutil.py`),\n * `?` → any one char, `[seq]`/`[!seq]` → char class, everything else escaped. This is the matcher\n * cosmic-ray's `excluded-modules` and mutmut's `do_not_mutate` both use (the latter via\n * `fnmatch.fnmatch` in `Config.should_ignore_for_mutation`).\n */\nfunction fnmatchToRegExp(pattern: string): RegExp {\n let re = ''\n let i = 0\n const n = pattern.length\n while (i < n) {\n const c = pattern[i++]\n if (c === '*') {\n re += '.*'\n } else if (c === '?') {\n re += '.'\n } else if (c === '[') {\n let j = i\n if (j < n && (pattern[j] === '!' || pattern[j] === ']')) j++\n while (j < n && pattern[j] !== ']') j++\n if (j >= n) {\n re += '\\\\[' // unterminated class → literal '['\n } else {\n let stuff = pattern.slice(i, j).replace(/\\\\/g, '\\\\\\\\')\n i = j + 1\n if (stuff.startsWith('!')) stuff = `^${stuff.slice(1)}`\n else if (stuff.startsWith('^')) stuff = `\\\\${stuff}`\n re += `[${stuff}]`\n }\n } else {\n re += (c ?? '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n }\n }\n return new RegExp(`^(?:${re})$`, 's')\n}\n\n/** True if an exclusion entry (exact path or fnmatch glob) matches any of the selected files. */\nfunction exclusionCollides(entry: string, selected: string[]): boolean {\n const norm = normalizePath(entry)\n if (selected.includes(norm)) return true // exact path\n const re = fnmatchToRegExp(norm)\n return selected.some((f) => re.test(f))\n}\n\n/**\n * The operator's declared source tree, read from a cosmic-ray base config's `module-path` (a single\n * string or a list). Used as the default `ownedRoots` to confine the diff scope (Fork C): a changed\n * `.py` outside this tree is `unmatched` (report-gap), never silently scoped. Pure.\n */\nexport function cosmicModulePathRoots(baseToml: string): string[] {\n const data = parse(baseToml) as Record<string, unknown>\n const cr = data['cosmic-ray']\n if (!cr || typeof cr !== 'object') return []\n const mp = (cr as Record<string, unknown>)['module-path']\n if (typeof mp === 'string') return [normalizePath(mp)]\n if (Array.isArray(mp)) return mp.map((x) => normalizePath(String(x)))\n return []\n}\n\nexport interface ScopedCosmicRayConfig {\n /** The synthesized per-run `cosmic-ray.toml` text. */\n toml: string\n /** The selected file list written to `module-path` (canonical form). */\n modulePath: string[]\n /** Inherited `excluded-modules` entries removed because they would subtract a selected file. */\n strippedExclusions: string[]\n}\n\n/**\n * Synthesize a per-run cosmic-ray config from a base config + the selected files. Overrides\n * `module-path` to the canonical selected FILE LIST and reconciles `excluded-modules`: any entry\n * (exact or fnmatch glob) that matches a selected file is stripped (it would otherwise silently\n * subtract that file — blocker #3); non-colliding entries are preserved. All other keys/tables\n * (`timeout`, `test-command`, `[cosmic-ray.distributor]`, …) are preserved verbatim. Pure.\n */\nexport function synthesizeScopedCosmicRayConfig(\n baseToml: string,\n selectedFiles: string[],\n): ScopedCosmicRayConfig {\n const selected = canonicalize(selectedFiles)\n if (selected.length === 0) {\n throw new ScopeEmitError('cannot synthesize a scoped cosmic-ray config for an empty selection')\n }\n const data = parse(baseToml) as Record<string, unknown>\n const cr = data['cosmic-ray']\n if (cr === undefined || typeof cr !== 'object') {\n throw new ScopeEmitError('base config has no [cosmic-ray] table')\n }\n const table = cr as Record<string, unknown>\n table['module-path'] = selected\n\n const strippedExclusions: string[] = []\n const inherited = Array.isArray(table['excluded-modules'])\n ? (table['excluded-modules'] as unknown[]).map(String)\n : []\n const kept = inherited.filter((entry) => {\n if (exclusionCollides(entry, selected)) {\n strippedExclusions.push(entry)\n return false\n }\n return true\n })\n table['excluded-modules'] = kept\n\n return { toml: stringify(data), modulePath: selected, strippedExclusions }\n}\n\n/** The `[tool.mutmut]` table of a pyproject (empty when absent / unparseable-as-table). */\nfunction mutmutTable(basePyproject: string): Record<string, unknown> {\n if (!basePyproject.trim()) return {}\n const data = parse(basePyproject) as Record<string, unknown>\n const tool = data.tool\n if (!tool || typeof tool !== 'object') return {}\n const m = (tool as Record<string, unknown>).mutmut\n return m && typeof m === 'object' ? (m as Record<string, unknown>) : {}\n}\n\n/** The operator's `[tool.mutmut] paths_to_mutate` — the default `ownedRoots` for a scoped mutmut run. */\nexport function mutmutPathsToMutate(basePyproject: string): string[] {\n const v = mutmutTable(basePyproject).paths_to_mutate\n return Array.isArray(v) ? v.map((x) => normalizePath(String(x))) : []\n}\n\n/** The operator's inherited `[tool.mutmut] do_not_mutate` globs (reconciled against the scope). */\nexport function mutmutDoNotMutate(basePyproject: string): string[] {\n const v = mutmutTable(basePyproject).do_not_mutate\n return Array.isArray(v) ? v.map(String) : []\n}\n\nexport interface MutmutScopePlan {\n /** The selected files written to `paths_to_mutate` (canonical form). */\n pathsToMutate: string[]\n /** Sibling source files to `also_copy` so unscoped tests still import (source tree minus scope). */\n alsoCopy: string[]\n /** Inherited `do_not_mutate` globs preserved (those that do NOT collide with the scope). */\n doNotMutate: string[]\n /** Inherited `do_not_mutate` globs removed because they would exclude a selected file (blocker #3). */\n strippedDoNotMutate: string[]\n}\n\n/**\n * Plan a scoped mutmut run. `paths_to_mutate` = the selected files; `also_copy` = every other source\n * file in `allSourceFiles` (so a test importing an unscoped sibling module still resolves in mutmut's\n * `mutants/` sandbox — verified necessary in slice 0). An inherited `do_not_mutate` glob that matches\n * a selected file is stripped (it would otherwise exclude a file we deliberately scoped). Pure given\n * `allSourceFiles` (the runner derives it by walking the owned roots).\n */\nexport function planMutmutScope(\n selectedFiles: string[],\n allSourceFiles: string[],\n inheritedDoNotMutate: string[] = [],\n): MutmutScopePlan {\n const pathsToMutate = canonicalize(selectedFiles)\n if (pathsToMutate.length === 0) {\n throw new ScopeEmitError('cannot plan a scoped mutmut run for an empty selection')\n }\n const scope = new Set(pathsToMutate)\n const alsoCopy = canonicalize(allSourceFiles).filter((f) => !scope.has(f))\n\n const strippedDoNotMutate: string[] = []\n const doNotMutate = inheritedDoNotMutate.filter((entry) => {\n if (exclusionCollides(entry, pathsToMutate)) {\n strippedDoNotMutate.push(entry)\n return false\n }\n return true\n })\n return { pathsToMutate, alsoCopy, doNotMutate, strippedDoNotMutate }\n}\n\n/**\n * Render a `pyproject.toml` for a scoped mutmut run: merge the {@link MutmutScopePlan}'s\n * `paths_to_mutate`/`also_copy` (+ reconciled `do_not_mutate`) into the base pyproject's\n * `[tool.mutmut]` table (Fork F: only slice-0-verified keys), preserving every other section and any\n * other verified `[tool.mutmut]` key the operator set (e.g. `pytest_add_cli_args`). An empty\n * `do_not_mutate` is omitted entirely. Pure.\n */\nexport function synthesizeScopedMutmutPyproject(\n basePyproject: string,\n plan: MutmutScopePlan,\n): string {\n const data = (basePyproject.trim() ? parse(basePyproject) : {}) as Record<string, unknown>\n const tool = (data.tool && typeof data.tool === 'object' ? data.tool : {}) as Record<\n string,\n unknown\n >\n const mutmut = (tool.mutmut && typeof tool.mutmut === 'object' ? tool.mutmut : {}) as Record<\n string,\n unknown\n >\n mutmut.paths_to_mutate = plan.pathsToMutate\n mutmut.also_copy = plan.alsoCopy\n if (plan.doNotMutate.length > 0) mutmut.do_not_mutate = plan.doNotMutate\n else delete mutmut.do_not_mutate\n tool.mutmut = mutmut\n data.tool = tool\n return stringify(data)\n}\n","/**\n * cosmic-ray adapter (a Python mutation-testing tool) — converts `cosmic-ray dump <session.sqlite>`\n * JSON-lines into the mutation-testing-elements {@link MutationReport} that {@link summarizeMutation}\n * already consumes, so the summarizer is reused unchanged across Stryker (JS), mutmut, and cosmic-ray.\n *\n * Unlike mutmut, cosmic-ray's dump carries the REAL source path + line + operator per mutant, so its\n * survivors are actionable (file:line:operator). Each dump line is a JSON array\n * `[work_item, work_result | null]` (captured from cosmic-ray 8.4.6 — see\n * `test/fixtures/cosmic-ray-dump.jsonl`):\n *\n * - `work_item.mutations[]` — one entry per mutation in the job (single-mutation jobs in practice);\n * each has `module_path` (the file key), `operator_name` (the mutator), and `start_pos: [line, col]`.\n * - `work_result` — `{ worker_outcome, test_outcome? }`, or `null` when the job has not been executed.\n *\n * Status mapping (cosmic-ray → mutation-testing-elements), chosen to never overstate the score and to\n * fold ambiguity to a neutral `Pending` (never `Survived`/`Killed`):\n * - `work_result === null` → Pending (not yet executed)\n * - worker_outcome `normal` + test_outcome `killed`/`survived` → Killed / Survived\n * - worker_outcome `normal` + test_outcome `incompetent` → RuntimeError (the mutant broke the run)\n * - worker_outcome `no_test` → NoCoverage (no test exercised the mutation)\n * - worker_outcome `skipped` → Ignored\n * - worker_outcome `exception`/`abnormal`→ RuntimeError (invalid — excluded from the score)\n * - worker_outcome `timeout` → Timeout\n * - anything else / missing test_outcome → Pending (ambiguity ⇒ neutral, never a phantom survivor)\n */\n\nimport type { Mutant, MutantStatus, MutationFile, MutationReport } from './summarize.js'\n\ninterface CosmicMutation {\n module_path?: string\n operator_name?: string\n start_pos?: [number, number] | { line?: number; column?: number }\n}\ninterface CosmicWorkItem {\n mutations?: CosmicMutation[]\n}\ninterface CosmicWorkResult {\n worker_outcome?: string\n test_outcome?: string | null\n}\n\n/** worker_outcome (other than `normal`) → status. `normal` defers to test_outcome. */\nconst WORKER_STATUS: Record<string, MutantStatus> = {\n no_test: 'NoCoverage',\n skipped: 'Ignored',\n exception: 'RuntimeError',\n abnormal: 'RuntimeError',\n timeout: 'Timeout',\n}\n/** test_outcome (when worker_outcome === 'normal') → status. */\nconst TEST_STATUS: Record<string, MutantStatus> = {\n killed: 'Killed',\n survived: 'Survived',\n incompetent: 'RuntimeError',\n}\n\nfunction statusOf(result: CosmicWorkResult | null): MutantStatus {\n if (result === null) return 'Pending' // not yet executed\n const worker = result.worker_outcome\n if (worker === 'normal') return TEST_STATUS[result.test_outcome ?? ''] ?? 'Pending'\n return (worker !== undefined && WORKER_STATUS[worker]) || 'Pending'\n}\n\nfunction lineColOf(\n pos: CosmicMutation['start_pos'],\n): { line: number; column?: number } | undefined {\n if (Array.isArray(pos)) return { line: pos[0], column: pos[1] }\n if (pos && typeof pos.line === 'number') return { line: pos.line, column: pos.column }\n return undefined\n}\n\n/** Parse `cosmic-ray dump <session.sqlite>` JSON-lines into a mutation-testing-elements report. Pure. */\nexport function parseCosmicRayDump(jsonl: string): MutationReport {\n const files: Record<string, MutationFile> = {}\n let index = 0\n for (const raw of jsonl.split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line) continue\n let parsed: [CosmicWorkItem, CosmicWorkResult | null]\n try {\n parsed = JSON.parse(line) as [CosmicWorkItem, CosmicWorkResult | null]\n } catch {\n continue // a non-JSON line (e.g. a stray log) is skipped, never a phantom mutant\n }\n const [workItem, workResult] = parsed\n const mutation = workItem?.mutations?.[0]\n const path = mutation?.module_path\n if (!path) continue\n const status = statusOf(workResult ?? null)\n const loc = lineColOf(mutation.start_pos)\n const mutant: Mutant = {\n id: `${path}:${index}`,\n mutatorName: mutation.operator_name ?? 'unknown',\n status,\n }\n if (loc) mutant.location = { start: loc }\n const entry = files[path] ?? { language: 'python', mutants: [] }\n entry.mutants.push(mutant)\n files[path] = entry\n index++\n }\n return { files }\n}\n","/**\n * mutmut adapter (the Python mutation-testing tool) — converts `mutmut results --all true`\n * output into the mutation-testing-elements {@link MutationReport} that {@link summarizeMutation}\n * already consumes, so the summarizer is reused unchanged across Stryker (JS) and mutmut (Python).\n *\n * mutmut 3.x has no native mutation-testing-elements / JSON-per-mutant report; its richest\n * machine-readable per-mutant output is `mutmut results --all true`, one line per mutant:\n *\n * calc.x_add__mutmut_1: killed\n * calc.x_sub__mutmut_1: survived\n * calc.x_mul__mutmut_1: no tests\n *\n * (Captured from mutmut 3.5.0 — see `test/fixtures/mutmut-results.txt`.) The mutant name is\n * `<dotted module>.x_<function>__mutmut_<n>`, so we group mutants by their module as the \"file\"\n * key (mutmut does not surface a source path or line here — `line` is 0, `mutatorName` is the\n * mutant name, the best identifier available).\n *\n * Status vocabulary mapping (mutmut → mutation-testing-elements), chosen to never overstate the\n * score: `suspicious` (the suite behaved oddly — not a confirmed kill) maps to `Survived` so it\n * surfaces as a gap; `segfault` maps to `RuntimeError` (invalid, excluded from the score); an\n * unrecognized status maps to `Pending` (neutral — excluded from `valid`, not a survivor).\n */\n\nimport type { MutantStatus, MutationFile, MutationReport } from './summarize.js'\n\nconst MUTMUT_STATUS: Record<string, MutantStatus> = {\n killed: 'Killed',\n survived: 'Survived',\n 'no tests': 'NoCoverage',\n timeout: 'Timeout',\n suspicious: 'Survived',\n skipped: 'Ignored',\n segfault: 'RuntimeError',\n}\n\n/** The module portion of a mutmut mutant name (`pkg.mod.x_fn__mutmut_3` → `pkg.mod`). */\nfunction moduleOf(name: string): string {\n const m = /^(.*)\\.x_.+__mutmut_\\d+$/.exec(name)\n if (m?.[1]) return m[1]\n const dot = name.lastIndexOf('.')\n return dot > 0 ? name.slice(0, dot) : name\n}\n\n/** Parse `mutmut results --all true` text into a mutation-testing-elements report. Pure. */\nexport function parseMutmutResults(text: string): MutationReport {\n const files: Record<string, MutationFile> = {}\n for (const raw of text.split(/\\r?\\n/)) {\n const line = raw.trim()\n if (!line) continue\n const idx = line.indexOf(':')\n if (idx === -1) continue\n const name = line.slice(0, idx).trim()\n if (!name) continue\n const statusText = line\n .slice(idx + 1)\n .trim()\n .toLowerCase()\n const status = MUTMUT_STATUS[statusText] ?? 'Pending'\n const file = moduleOf(name)\n const entry = files[file] ?? { mutants: [] }\n entry.mutants.push({ id: name, mutatorName: name, status })\n files[file] = entry\n }\n return { files }\n}\n","/**\n * Pure scope-selection + post-spawn reconciliation primitives for Python mutation diff-scoping\n * (ADR 0010 addendum 2). The config EMITTERS (cosmic-ray TOML / mutmut pyproject) and the runner\n * wiring are STAGED pending slice-0 tool-fact capture; these two functions are the load-bearing,\n * tool-agnostic SAFETY core and ship first (pure, fixture-tested, no real tool in `pnpm gate`):\n *\n * - {@link selectMutationScope} mirrors coverage's `selectPytestScope` (incl. its INJECTED\n * existence predicate): turn the changed-file list into the set of mutable, existing, in-tree\n * `.py` files to scope a mutation run to, surfacing everything else as `unmatched` (report-gap;\n * never silently folded into the scope).\n * - {@link reconcileScope} is the POST-SPAWN partial-under-scope guard (the design's load-bearing\n * correction): the existing `assertComplete` only catches TOTAL-zero mutants, so it is blind to\n * a run that mutated only a SUBSET of the selected files — a clean score for files that were\n * never mutated, i.e. absence-as-a-pass. reconcileScope reports which selected files the tool\n * actually mutated and which it NEVER SAW; the (staged) runner throws ⇒ inconclusive on any\n * never-seen selected file.\n */\n\nimport type { MutationSummary } from './summarize.js'\n\n/** Normalize a path to a comparable repo-relative POSIX form (backslashes → `/`, drop a leading `./`). */\nfunction normalizePath(p: string): string {\n return p.replace(/\\\\/g, '/').replace(/^\\.\\//, '')\n}\n\nexport interface MutationScope {\n /** Mutable, existing, in-tree `.py` files to scope the run to (repo-relative, normalized, deduped, sorted). */\n files: string[]\n /** Changed `.py` files that are out-of-tree, deleted, or otherwise unplaceable — surfaced as a\n * gap (report-gap), NEVER silently folded into `files` (that would be absence-as-a-pass). */\n unmatched: string[]\n}\n\n/**\n * Derive the mutation scope from the changed files. A changed file is selected only when it is a\n * `.py` file, lives under one of the operator's `ownedRoots`, AND exists on disk (`exists`,\n * injected — FS by default in the runner, faked in tests, mirroring `selectPytestScope`'s\n * `testExists`). A `.py` file that is out-of-tree or non-existent (deleted/renamed/typo) is\n * `unmatched`, never scoped. Non-`.py` files are irrelevant to mutation and dropped. Pure given\n * `exists`. Whole-project fallback is NOT decided here — it is the caller's (`mutateFiles ===\n * undefined`) or the cosmic-ray emitter's (`no-scope` on the base config) concern.\n */\nexport function selectMutationScope(\n mutateFiles: string[],\n ownedRoots: string[],\n exists: (path: string) => boolean,\n): MutationScope {\n const roots = ownedRoots.map(normalizePath)\n const underRoot = (p: string): boolean => roots.some((r) => p === r || p.startsWith(`${r}/`))\n const files = new Set<string>()\n const unmatched = new Set<string>()\n for (const raw of mutateFiles) {\n const p = normalizePath(raw)\n if (!p.endsWith('.py')) continue // not a Python source file — irrelevant to mutation\n if (!underRoot(p) || !exists(p)) {\n unmatched.add(p) // out-of-tree, deleted, renamed, or typo'd — a gap, never scoped\n continue\n }\n files.add(p)\n }\n return { files: [...files].sort(), unmatched: [...unmatched].sort() }\n}\n\nexport interface ScopeReconciliation {\n /** Selected files the tool ACTUALLY mutated (≥1 mutant) — report this as the run's true scope,\n * never the merely-requested set. */\n mutatedFiles: string[]\n /** Selected files the tool NEVER SAW (absent from the per-file summary) — partial under-scope.\n * A non-empty `missing` MUST make the run inconclusive (absence-as-a-pass otherwise). */\n missing: string[]\n}\n\n/**\n * Post-spawn partial-under-scope guard. Given the files the run was SELECTED to mutate and the\n * tool's {@link MutationSummary} (whose `files[]` carries a per-file record for every file the\n * tool SAW — even one it found no mutants in), determine which selected files were genuinely\n * mutated and which the tool never saw at all. A selected file present in the summary with zero\n * mutants is SEEN-BUT-EMPTY (benign — no mutable code); a selected file ABSENT from the summary\n * was never mutated (the partial-scope sentinel) and goes in `missing`. Paths are normalized on\n * both sides before comparison. Pure.\n */\nexport function reconcileScope(selected: string[], summary: MutationSummary): ScopeReconciliation {\n const seen = new Set(summary.files.map((f) => normalizePath(f.path)))\n const mutated = new Set(\n summary.files.filter((f) => f.metrics.totalMutants > 0).map((f) => normalizePath(f.path)),\n )\n const sel = [...new Set(selected.map(normalizePath))].sort()\n return {\n mutatedFiles: sel.filter((p) => mutated.has(p)),\n missing: sel.filter((p) => !seen.has(p)),\n }\n}\n\n/**\n * Convert a Python source PATH to its dotted module form (`pkg/calc.py` → `pkg.calc`,\n * `pkg/__init__.py` → `pkg`). The package-relative prefix is unknown here, so this is the FULL\n * relative-path dotted form; {@link reconcileMutmutScope} matches it against mutmut's module names\n * by suffix to tolerate src-layout projects.\n */\nexport function pyPathToModule(path: string): string {\n let p = normalizePath(path).replace(/\\.py$/, '')\n if (p.endsWith('/__init__')) p = p.slice(0, -'/__init__'.length)\n return p.replace(/\\//g, '.')\n}\n\n/**\n * The mutmut-specific post-spawn guard. Unlike cosmic-ray, mutmut's {@link MutationSummary} is keyed\n * by DOTTED MODULE (`pkg.calc`), not path, AND it emits NO record for a scoped file that produced\n * zero mutants (slice 0: seen-but-empty is INDISTINGUISHABLE from never-seen — Fork B2). So this is\n * deliberately CONSERVATIVE: a selected file is \"mutated\" only if some module with ≥1 mutant matches\n * its dotted form (equal, or a suffix either way — tolerating flat AND src layouts); every other\n * selected file is `missing` (⇒ the runner throws inconclusive). Some false-inconclusives, ZERO\n * false-passes (absence-is-never-a-pass). Pure.\n */\nexport function reconcileMutmutScope(\n selectedPaths: string[],\n summary: MutationSummary,\n): ScopeReconciliation {\n const mutatedModules = summary.files\n .filter((f) => f.metrics.totalMutants > 0)\n .map((f) => normalizePath(f.path)) // mutmut paths ARE dotted modules\n const matches = (mod: string, sel: string): boolean =>\n mod === sel || mod.endsWith(`.${sel}`) || sel.endsWith(`.${mod}`)\n const sel = [...new Set(selectedPaths.map(normalizePath))].sort()\n const mutatedFiles: string[] = []\n const missing: string[] = []\n for (const path of sel) {\n const mod = pyPathToModule(path)\n if (mutatedModules.some((m) => matches(m, mod))) mutatedFiles.push(path)\n else missing.push(path)\n }\n return { mutatedFiles, missing }\n}\n","/**\n * Pure mutation-report summarizer — the first slice of `@sackville-mcp/mutate`, and the one\n * with no I/O and no Stryker dependency.\n *\n * Mutation testing asks \"are the tests meaningful?\" — it perturbs the source (a `+`\n * becomes `-`, a `true` becomes `false`) and re-runs the suite; a mutant that survives is\n * a behaviour the tests do not actually pin down. Under Sackville's TDD gate the agent\n * wrote a passing test, but a passing test can still assert nothing useful — surviving\n * mutants are the catch for that, the natural complement to coverage's forgotten-assertion\n * catch (covered-but-unkilled vs added-but-uncovered).\n *\n * This module reads the **mutation-testing-elements report schema** (`schemaVersion`,\n * `files[path].mutants[].status`) that Stryker emits as `mutation-report.json`. The schema\n * is stable and decoupled from the Stryker version (ADR 0010 update 2026-06-01), so this\n * core carries no `@stryker-mutator/*` import — it is unit-tested against a committed\n * golden report. Producing a real report is a gated, injected `stryker run` (a later slice).\n *\n * Metric definitions mirror mutation-testing-elements exactly:\n * detected = killed + timeout\n * undetected = survived + noCoverage\n * covered = detected + survived (NoCoverage excluded — it was never run)\n * valid = detected + undetected\n * invalid = compileErrors + runtimeErrors\n * total = valid + invalid + ignored + pending\n * mutationScore = detected / valid (null when valid === 0)\n * mutationScoreBasedOnCoveredCode = detected / covered (null when covered === 0)\n */\n\n/** Mutant statuses from the mutation-testing-elements schema. */\nexport type MutantStatus =\n | 'Killed'\n | 'Survived'\n | 'NoCoverage'\n | 'Timeout'\n | 'CompileError'\n | 'RuntimeError'\n | 'Ignored'\n | 'Pending'\n\nexport interface MutantPosition {\n line: number\n column?: number\n}\n\nexport interface Mutant {\n id: string\n mutatorName: string\n status: MutantStatus\n replacement?: string\n location?: { start: MutantPosition; end?: MutantPosition }\n}\n\nexport interface MutationFile {\n language?: string\n source?: string\n mutants: Mutant[]\n}\n\nexport interface MutationReport {\n schemaVersion?: string\n thresholds?: { high: number; low: number }\n files: Record<string, MutationFile>\n}\n\nexport interface StatusCounts {\n killed: number\n survived: number\n timeout: number\n noCoverage: number\n compileErrors: number\n runtimeErrors: number\n ignored: number\n pending: number\n}\n\nexport interface MutationMetrics {\n counts: StatusCounts\n detected: number\n undetected: number\n covered: number\n valid: number\n invalid: number\n totalMutants: number\n /** detected / valid (percent); null when there are no valid mutants. */\n mutationScore: number | null\n /** detected / covered (percent); null when no covered mutants. */\n mutationScoreBasedOnCoveredCode: number | null\n}\n\nexport interface FileSummary {\n path: string\n metrics: MutationMetrics\n}\n\n/** A surviving (or never-covered) mutant — the actionable test gap. */\nexport interface Survivor {\n file: string\n mutatorName: string\n status: 'Survived' | 'NoCoverage'\n line: number\n}\n\nexport interface MutationSummary {\n metrics: MutationMetrics\n files: FileSummary[]\n /** Survived + NoCoverage mutants, sorted by file then line — what to go fix. */\n survivors: Survivor[]\n}\n\nconst ZERO: StatusCounts = {\n killed: 0,\n survived: 0,\n timeout: 0,\n noCoverage: 0,\n compileErrors: 0,\n runtimeErrors: 0,\n ignored: 0,\n pending: 0,\n}\n\nfunction tally(mutants: Mutant[]): StatusCounts {\n const c: StatusCounts = { ...ZERO }\n for (const m of mutants) {\n switch (m.status) {\n case 'Killed':\n c.killed++\n break\n case 'Survived':\n c.survived++\n break\n case 'Timeout':\n c.timeout++\n break\n case 'NoCoverage':\n c.noCoverage++\n break\n case 'CompileError':\n c.compileErrors++\n break\n case 'RuntimeError':\n c.runtimeErrors++\n break\n case 'Ignored':\n c.ignored++\n break\n case 'Pending':\n c.pending++\n break\n }\n }\n return c\n}\n\nfunction metricsFrom(c: StatusCounts): MutationMetrics {\n const detected = c.killed + c.timeout\n const undetected = c.survived + c.noCoverage\n const covered = detected + c.survived\n const valid = detected + undetected\n const invalid = c.compileErrors + c.runtimeErrors\n const totalMutants = valid + invalid + c.ignored + c.pending\n return {\n counts: c,\n detected,\n undetected,\n covered,\n valid,\n invalid,\n totalMutants,\n mutationScore: valid > 0 ? (detected / valid) * 100 : null,\n mutationScoreBasedOnCoveredCode: covered > 0 ? (detected / covered) * 100 : null,\n }\n}\n\nfunction sumCounts(a: StatusCounts, b: StatusCounts): StatusCounts {\n return {\n killed: a.killed + b.killed,\n survived: a.survived + b.survived,\n timeout: a.timeout + b.timeout,\n noCoverage: a.noCoverage + b.noCoverage,\n compileErrors: a.compileErrors + b.compileErrors,\n runtimeErrors: a.runtimeErrors + b.runtimeErrors,\n ignored: a.ignored + b.ignored,\n pending: a.pending + b.pending,\n }\n}\n\n/** Summarize a Stryker mutation report into aggregate + per-file metrics and the survivor list. Pure. */\nexport function summarizeMutation(report: MutationReport): MutationSummary {\n const files: FileSummary[] = []\n const survivors: Survivor[] = []\n let total: StatusCounts = { ...ZERO }\n\n for (const path of Object.keys(report.files).sort()) {\n const file = report.files[path]\n if (!file) continue\n const counts = tally(file.mutants)\n total = sumCounts(total, counts)\n files.push({ path, metrics: metricsFrom(counts) })\n\n for (const m of file.mutants) {\n if (m.status === 'Survived' || m.status === 'NoCoverage') {\n survivors.push({\n file: path,\n mutatorName: m.mutatorName,\n status: m.status,\n line: m.location?.start.line ?? 0,\n })\n }\n }\n }\n\n survivors.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line))\n\n return { metrics: metricsFrom(total), files, survivors }\n}\n","/**\n * The gated, diff-scoped mutation run — the live half of `@sackville-mcp/mutate`. It **spawns**\n * Stryker (`stryker run`, an injected subprocess like flake's `vitest` and coverage's\n * `vitest related`), then reads the JSON report Stryker writes and feeds it to the pure\n * {@link summarizeMutation}.\n *\n * Per ADR 0010 (+ its 2026-06-01 spike update):\n * 1. **It runs code** — and a mutation run is *expensive* (the suite re-runs per mutant) —\n * so it sits behind the house paired deny-by-default operator gate (`allowRun` +\n * `allowedRoots` allowlist, load-bearing on its own, + a wall-clock cap). All\n * operator-set; no caller input self-authorizes.\n * 2. **Stryker is NOT a dependency of this package.** A real mutation run is slow and\n * non-deterministic, so it never runs in `pnpm gate`. The `stryker` invocation is the\n * injected {@link MutationRunner} (the bin spawns the operator's local Stryker); the\n * engine owns the gate, argv, report plumbing, and summary, and is unit-tested with a\n * fake runner.\n * 3. **Diff-scoped.** `mutateFiles` (the changed source files) become Stryker's `--mutate`\n * glob list, and `--incremental` reuses Stryker's cache — so a change mutates only what\n * it touched, not the whole tree.\n */\n\nimport {\n cpSync,\n existsSync,\n mkdtempSync,\n readdirSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport { type SpawnedRunner, spawnRunner } from '@sackville-mcp/spawn'\nimport {\n cosmicModulePathRoots,\n mutmutDoNotMutate,\n mutmutPathsToMutate,\n planMutmutScope,\n synthesizeScopedCosmicRayConfig,\n synthesizeScopedMutmutPyproject,\n} from './config.js'\nimport { parseCosmicRayDump } from './cosmic-ray.js'\nimport { parseMutmutResults } from './mutmut.js'\nimport { reconcileMutmutScope, reconcileScope, selectMutationScope } from './scope.js'\nimport { type MutationReport, type MutationSummary, summarizeMutation } from './summarize.js'\n\n/** The zero-mutant summary returned by a pre-spawn noop (folds to no-signal ⇒ inconclusive). */\nfunction emptyMutationSummary(): MutationSummary {\n return summarizeMutation({ files: {} })\n}\n\n/** Read a file, or undefined if it is absent (a missing base config is not an error). */\nfunction readFileIfExists(path: string): string | undefined {\n try {\n return readFileSync(path, 'utf8')\n } catch {\n return undefined\n }\n}\n\n/** Directories never copied into the mutmut sandbox (heavy / irrelevant / the sticky mutants cache). */\nconst SANDBOX_EXCLUDE =\n /(?:^|\\/)(?:node_modules|\\.git|\\.venv|venv|__pycache__|mutants|\\.mutmut-cache|dist|\\.tox)(?:\\/|$)/\n\n/** Recursively list the `.py` files under each owned root, repo-relative (FS default for runMutmut). */\nfunction defaultListSources(ownedRoots: string[], projectRoot: string): string[] {\n const out: string[] = []\n for (const root of ownedRoots) {\n const abs = join(projectRoot, root)\n let entries: string[]\n try {\n entries = readdirSync(abs, { recursive: true }) as string[]\n } catch {\n continue // a declared root that is a single file or absent — skip (selectMutationScope handles files)\n }\n for (const rel of entries) {\n const p = rel.replace(/\\\\/g, '/')\n if (p.endsWith('.py') && !SANDBOX_EXCLUDE.test(p))\n out.push(`${root}/${p}`.replace(/\\/+/g, '/'))\n }\n }\n return out\n}\n\n/** Copy a project into a fresh sandbox dir, excluding heavy dirs + the sticky `mutants/` cache. */\nfunction copyProjectInto(from: string, to: string): void {\n cpSync(from, to, {\n recursive: true,\n filter: (src) => !SANDBOX_EXCLUDE.test(src.slice(from.length).replace(/\\\\/g, '/')),\n })\n}\n\n/** Thrown when the paired operator gate denies a run. */\nexport class MutateGateError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'MutateGateError'\n // Brand as a gate DENIAL (ADR 0013 Addendum, milestone 5c): the run-driving\n // `@sackville-mcp/verify` reads this global-registry symbol via `isGateDenial` to map a\n // denial to `skipReason:'gate-not-set'` (never `errored`) WITHOUT importing engine\n // code. The `Symbol.for` key string is the cross-package contract.\n ;(this as unknown as Record<symbol, unknown>)[Symbol.for('sackville.gate-denial')] = true\n }\n}\n\nexport interface RunMutationConfig {\n /** The project to run Stryker in. */\n projectRoot: string\n /** OPERATOR allowlist of roots the runner may execute in. Load-bearing even with allowRun. */\n allowedRoots: string[]\n /** OPERATOR opt-in to actually run mutation testing. Deny-by-default. */\n allowRun: boolean\n /** Wall-clock cap (ms) passed to the runner. */\n timeoutMs?: number\n}\n\nexport interface RunMutationInput {\n /**\n * Changed source files to scope mutation to. Stryker → `--mutate`; the Python tools → a synthesized\n * scoped config (cosmic-ray `module-path` list / mutmut `paths_to_mutate`). `undefined` ⇒ the\n * project default (whole project, today's behavior); a supplied list scopes the run, and a selection\n * that resolves to no mutable in-tree `.py` is a pre-spawn noop (`ran:false`, never a spawn).\n */\n mutateFiles?: string[]\n /** Reuse Stryker's incremental cache (`--incremental`) — faster re-runs. */\n incremental?: boolean\n /** cosmic-ray config path (relative to projectRoot). Default `cosmic-ray.toml`. */\n configPath?: string\n /**\n * OPTIONAL operator source roots the diff scope is confined to (Fork C). A changed `.py` outside\n * them is `unmatched` (report-gap), never scoped. Default: the tool config's declared source tree\n * (cosmic-ray `module-path`).\n */\n ownedRoots?: string[]\n}\n\n/** Injected command runner — executes `stryker <argv>` and yields its exit status. */\nexport type MutationRunner = SpawnedRunner\n\nexport interface RunMutationResult {\n ran: boolean\n exitCode: number\n /** Files mutation was scoped to (empty ⇒ the project's configured set). */\n scopedFiles: string[]\n /** Which mutation tool produced the summary. */\n tool?: 'stryker' | 'mutmut' | 'cosmic-ray'\n /**\n * The on-disk JSON report path (Stryker). Optional: the Python tools (mutmut/cosmic-ray) emit\n * their report to STDOUT, so there is no report file to surface.\n */\n reportPath?: string\n summary: MutationSummary\n /** A scope was requested but resolved to no mutable in-tree `.py` ⇒ pre-spawn noop (case (a)). */\n scopeEmpty?: boolean\n /** Changed `.py` files outside the owned tree / deleted — surfaced as a gap (Fork C), never scoped. */\n unmatched?: string[]\n /** The files the run was SELECTED to mutate (before reconciliation) — what we asked the tool for. */\n requestedFiles?: string[]\n}\n\n/** Stryker's default JSON-report location, relative to the project root. */\nfunction defaultReportPath(projectRoot: string): string {\n return join(projectRoot, 'reports', 'mutation', 'mutation.json')\n}\n\n/** Build the `stryker run` argv: JSON reporter, optional diff scope + incremental cache. */\nfunction runArgv(input: RunMutationInput): string[] {\n const argv = ['run', '--reporters', 'json']\n if (input.mutateFiles && input.mutateFiles.length > 0) {\n argv.push('--mutate', input.mutateFiles.join(','))\n }\n if (input.incremental) argv.push('--incremental')\n return argv\n}\n\n/** Default live runner: spawn the local `stryker` as a subprocess (used by the bin, not the gate). */\nexport const defaultStrykerRunner: MutationRunner = spawnRunner('stryker')\n\n/** Default live runner: spawn the local `mutmut` as a subprocess (used by the bin, not the gate). */\nexport const defaultMutmutRunner: MutationRunner = spawnRunner('mutmut')\n\n/** Default live runner: spawn the local `cosmic-ray` as a subprocess (used by the bin, not the gate). */\nexport const defaultCosmicRayRunner: MutationRunner = spawnRunner('cosmic-ray')\n\nfunction assertAllowed(config: RunMutationConfig): void {\n if (!config.allowRun) {\n throw new MutateGateError('mutation runs are not enabled (the operator must set allowRun)')\n }\n const root = resolve(config.projectRoot)\n const allowed = config.allowedRoots.map((r) => resolve(r))\n if (!allowed.includes(root)) {\n throw new MutateGateError(`project root ${config.projectRoot} is not in the operator allowlist`)\n }\n}\n\n/**\n * Run mutation testing behind the operator gate and summarize the report. The actual\n * `stryker` invocation is the injected `runner` (default {@link defaultStrykerRunner}); the\n * JSON report is read from `deps.reportPath` (default: Stryker's\n * `<projectRoot>/reports/mutation/mutation.json`).\n */\nexport async function runMutation(\n config: RunMutationConfig,\n input: RunMutationInput,\n deps: { runner?: MutationRunner; reportPath?: string } = {},\n): Promise<RunMutationResult> {\n assertAllowed(config)\n\n const runner = deps.runner ?? defaultStrykerRunner\n const reportPath = deps.reportPath ?? defaultReportPath(config.projectRoot)\n const argv = runArgv(input)\n\n const { exitCode } = await runner(argv, {\n cwd: config.projectRoot,\n timeoutMs: config.timeoutMs,\n })\n\n let report: MutationReport\n try {\n report = JSON.parse(readFileSync(reportPath, 'utf8')) as MutationReport\n } catch {\n throw new Error(\n `mutation run did not produce a JSON report at ${reportPath} (exit code ${exitCode}); ` +\n 'ensure the project enables the Stryker `json` reporter',\n )\n }\n\n return {\n ran: true,\n exitCode,\n scopedFiles: input.mutateFiles ?? [],\n tool: 'stryker',\n reportPath,\n summary: summarizeMutation(report),\n }\n}\n\n/**\n * Transport-completeness guard for the Python tools, mirroring the capture/HAR guards: a run that\n * produced NO mutants (an empty/failed session), or a cosmic-ray session with unexecuted/ambiguous\n * (`Pending`) mutants, is INCONCLUSIVE — it must never be reported as a clean pass\n * (absence-is-never-a-pass). Throws so the caller (verify) folds it to inconclusive.\n */\nfunction assertComplete(tool: string, summary: MutationSummary): void {\n if (summary.metrics.totalMutants === 0) {\n throw new Error(`${tool} produced no mutants — inconclusive (never a clean pass)`)\n }\n if (summary.metrics.counts.pending > 0) {\n throw new Error(\n `${tool} session is incomplete: ${summary.metrics.counts.pending} unexecuted/ambiguous ` +\n 'mutant(s) — inconclusive',\n )\n }\n}\n\n/**\n * mutmut sibling of {@link runMutation} (ADR 0010 addendum, the lightweight Python option). Spawns\n * `mutmut run` (mutate + test; a non-zero exit just means survivors exist, not an error) then reads\n * `mutmut results --all true` from STDOUT and feeds the pure {@link parseMutmutResults}. No report\n * file. (Diff-scoping is staged — mutmut 3.x scopes via its own config, not a clean CLI file list.)\n */\nexport async function runMutmut(\n config: RunMutationConfig,\n input: RunMutationInput,\n deps: {\n runner?: MutationRunner\n /** Existence check for the selected files (FS by default; injected in tests). */\n exists?: (path: string) => boolean\n /** Enumerate the `.py` files under the owned roots (for `also_copy`); FS walk by default. */\n listSourceFiles?: (ownedRoots: string[], projectRoot: string) => string[]\n /** The fresh sandbox cwd to run in (the sticky `mutants/` cache can't leak). Default mkdtemp. */\n sandboxDir?: string\n } = {},\n): Promise<RunMutationResult> {\n assertAllowed(config)\n const runner = deps.runner ?? defaultMutmutRunner\n\n // Whole-project (today's behavior) in projectRoot when no scope is requested.\n if (input.mutateFiles === undefined) {\n const opts = { cwd: config.projectRoot, timeoutMs: config.timeoutMs }\n await runner(['run'], opts)\n const { exitCode, stdout } = await runner(['results', '--all', 'true'], opts)\n const summary = summarizeMutation(parseMutmutResults(stdout))\n assertComplete('mutmut', summary)\n return { ran: true, exitCode, scopedFiles: [], tool: 'mutmut', summary }\n }\n\n // Diff-scoped: confine to the owned tree, then select mutable existing .py files.\n const pyprojectPath = input.configPath ?? 'pyproject.toml'\n const basePyproject = readFileIfExists(join(config.projectRoot, pyprojectPath)) ?? ''\n const ownedRoots = input.ownedRoots ?? mutmutPathsToMutate(basePyproject)\n const exists = deps.exists ?? ((p: string) => existsSync(join(config.projectRoot, p)))\n const { files, unmatched } = selectMutationScope(input.mutateFiles, ownedRoots, exists)\n const unmatchedOut = unmatched.length > 0 ? unmatched : undefined\n\n // Case (a): nothing mutable in-tree remains — DO NOT spawn (noop).\n if (files.length === 0) {\n return {\n ran: false,\n exitCode: 0,\n scopedFiles: [],\n tool: 'mutmut',\n summary: emptyMutationSummary(),\n scopeEmpty: true,\n unmatched: unmatchedOut,\n requestedFiles: [],\n }\n }\n\n // Plan the scope: paths_to_mutate = selected files; also_copy = the rest of the source tree so\n // unscoped tests still import (slice 0); strip a colliding inherited do_not_mutate glob.\n const listSources = deps.listSourceFiles ?? defaultListSources\n const allSources = listSources(ownedRoots, config.projectRoot)\n const plan = planMutmutScope(files, allSources, mutmutDoNotMutate(basePyproject))\n const scopedPyproject = synthesizeScopedMutmutPyproject(basePyproject, plan)\n\n // Fresh sandbox cwd (the mutants/ cache is sticky), with the scoped pyproject written over the copy.\n const sandbox = deps.sandboxDir ?? mkdtempSync(join(tmpdir(), 'sackville-mutmut-'))\n copyProjectInto(config.projectRoot, sandbox)\n writeFileSync(join(sandbox, 'pyproject.toml'), scopedPyproject)\n\n const opts = { cwd: sandbox, timeoutMs: config.timeoutMs }\n await runner(['run'], opts)\n const { exitCode, stdout } = await runner(['results', '--all', 'true'], opts)\n const summary = summarizeMutation(parseMutmutResults(stdout))\n // A broken scoped baseline yields \"not checked\" ⇒ Pending ⇒ assertComplete throws (slice 0): this\n // IS the baseline-smoke gate. Total-zero is likewise inconclusive (case b).\n assertComplete('mutmut', summary)\n // Case (c): mutmut emits NO record for a 0-mutant selected file (Fork B2), so a selected file with\n // no matching mutated module was never mutated ⇒ inconclusive (conservative; never a false pass).\n const { mutatedFiles, missing } = reconcileMutmutScope(files, summary)\n if (missing.length > 0) {\n throw new Error(\n `mutmut under-scoped: ${missing.join(', ')} selected but produced no mutants — inconclusive`,\n )\n }\n return {\n ran: true,\n exitCode,\n scopedFiles: mutatedFiles,\n tool: 'mutmut',\n summary,\n requestedFiles: files,\n unmatched: unmatchedOut,\n }\n}\n\n/**\n * cosmic-ray sibling of {@link runMutation} (ADR 0010 addendum, the PRIMARY Python tool — its dump\n * carries real file:line:operator, so survivors are actionable). Drives the three-step workflow\n * against an operator-authored config (`input.configPath`, default `cosmic-ray.toml` — it carries\n * the project's test-command + module scope) over a throwaway session DB: `init` → `exec` → `dump`,\n * reading the `dump` JSON-lines from STDOUT and feeding the pure {@link parseCosmicRayDump}. The\n * {@link assertComplete} guard makes an empty or partially-executed session inconclusive, never a\n * clean pass. (Diff-scoping by synthesizing the per-run config from `mutateFiles` is staged.)\n */\nexport async function runCosmicRay(\n config: RunMutationConfig,\n input: RunMutationInput,\n deps: {\n runner?: MutationRunner\n sessionDir?: string\n /** Existence check for the selected files (FS by default; injected in tests). */\n exists?: (path: string) => boolean\n /** Scoped config filename written into projectRoot (relative). Default `.sackville-cosmic.toml`. */\n scopedConfigName?: string\n } = {},\n): Promise<RunMutationResult> {\n assertAllowed(config)\n const runner = deps.runner ?? defaultCosmicRayRunner\n const opts = { cwd: config.projectRoot, timeoutMs: config.timeoutMs }\n const configPath = input.configPath ?? 'cosmic-ray.toml'\n const sessionDir = deps.sessionDir ?? mkdtempSync(join(tmpdir(), 'sackville-mutate-'))\n const session = join(sessionDir, 'session.sqlite')\n\n // Whole-project (today's behavior) when no scope is requested.\n if (input.mutateFiles === undefined) {\n await runner(['init', configPath, session], opts)\n const exec = await runner(['exec', configPath, session], opts)\n const { stdout } = await runner(['dump', session], opts)\n const summary = summarizeMutation(parseCosmicRayDump(stdout))\n assertComplete('cosmic-ray', summary)\n return { ran: true, exitCode: exec.exitCode, scopedFiles: [], tool: 'cosmic-ray', summary }\n }\n\n // Diff-scoped: confine the changed files to the owned source tree, then select mutable existing .py.\n const baseToml = readFileSync(join(config.projectRoot, configPath), 'utf8')\n const ownedRoots = input.ownedRoots ?? cosmicModulePathRoots(baseToml)\n const exists = deps.exists ?? ((p: string) => existsSync(join(config.projectRoot, p)))\n const { files, unmatched } = selectMutationScope(input.mutateFiles, ownedRoots, exists)\n const unmatchedOut = unmatched.length > 0 ? unmatched : undefined\n\n // Case (a): a scope was requested but nothing mutable in-tree remains — DO NOT spawn (noop).\n if (files.length === 0) {\n return {\n ran: false,\n exitCode: 0,\n scopedFiles: [],\n tool: 'cosmic-ray',\n summary: emptyMutationSummary(),\n scopeEmpty: true,\n unmatched: unmatchedOut,\n requestedFiles: [],\n }\n }\n\n // Synthesize a scoped config (module-path = selected files, excluded-modules reconciled). It must\n // live in projectRoot so its RELATIVE module-path resolves there (cosmic-ray then reports relative\n // module_path keys, which reconcileScope compares against the selected files directly).\n const scoped = synthesizeScopedCosmicRayConfig(baseToml, files)\n const scopedName = deps.scopedConfigName ?? '.sackville-cosmic.toml'\n const scopedAbs = join(config.projectRoot, scopedName)\n writeFileSync(scopedAbs, scoped.toml)\n try {\n await runner(['init', scopedName, session], opts)\n const exec = await runner(['exec', scopedName, session], opts)\n const { stdout } = await runner(['dump', session], opts)\n const summary = summarizeMutation(parseCosmicRayDump(stdout))\n assertComplete('cosmic-ray', summary) // case (b): total-zero / pending ⇒ inconclusive\n // Case (c): a selected file the tool never SAW was silently never mutated ⇒ inconclusive.\n const { mutatedFiles, missing } = reconcileScope(files, summary)\n if (missing.length > 0) {\n throw new Error(\n `cosmic-ray under-scoped: ${missing.join(', ')} selected but never mutated — inconclusive`,\n )\n }\n // Case (d): clean scoped run — report what was GENUINELY mutated, not what was requested.\n return {\n ran: true,\n exitCode: exec.exitCode,\n scopedFiles: mutatedFiles,\n tool: 'cosmic-ray',\n summary,\n requestedFiles: files,\n unmatched: unmatchedOut,\n }\n } finally {\n rmSync(scopedAbs, { force: true })\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sackville-mcp/mutate",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"exports": {
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"dist"
|
|
17
17
|
],
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"smol-toml": "^1.6.1"
|
|
19
|
+
"smol-toml": "^1.6.1",
|
|
20
|
+
"@sackville-mcp/spawn": "0.0.1-alpha.4"
|
|
20
21
|
},
|
|
21
22
|
"publishConfig": {
|
|
22
23
|
"access": "public"
|