@fairfox/polly 0.80.0 → 0.81.0

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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../tools/mutate/src/args.ts", "../tools/mutate/src/config.ts", "../tools/test/src/coverage-policy/mutate-targets.ts", "../tools/mutate/src/decisions.ts", "../tools/mutate/src/ingest.ts", "../tools/mutate/src/init.ts", "../tools/mutate/src/run.ts", "../tools/mutate/src/report.ts", "../tools/mutate/src/verify-matrix.ts", "../tools/mutate/src/cli.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * Argument parsing for `polly mutate`. Hand-rolled (matching the rest of the\n * polly CLI), with the value-flag / bool-flag split that lets a flag value\n * (`--report path.json`) not be mistaken for a positional verb.\n */\n\nexport interface MutateArgs {\n /** First positional: \"run\" (default when bare) | \"report\" | \"decisions\" | \"verify\" | an unknown token to error on. */\n verb: string;\n /** Positionals after the verb (e.g. [\"decide\", file, verdict, ...rationale] for `decisions decide …`). */\n rest: string[];\n config?: string;\n report?: string;\n decisions?: string;\n db?: string;\n noReport: boolean;\n /** `verify --run` (run a fresh Stryker pass before checking the contract). */\n run: boolean;\n /** `init --force` (overwrite an existing stryker.conf.json). */\n force: boolean;\n help: boolean;\n}\n\nconst KNOWN_VERBS = new Set([\"run\", \"report\", \"decisions\", \"verify\", \"init\", \"help\"]);\nconst VALUE_FLAGS = new Set([\"--config\", \"--report\", \"--decisions\", \"--db\"]);\n\nexport function parseMutateArgs(argv: string[]): MutateArgs {\n const positionals: string[] = [];\n const flags = new Map<string, string>();\n const bools = new Set<string>();\n\n let i = 0;\n while (i < argv.length) {\n const a = argv[i];\n if (a === undefined) break;\n if (VALUE_FLAGS.has(a)) {\n flags.set(a, argv[i + 1] ?? \"\");\n i += 2;\n } else {\n if (a.startsWith(\"-\")) bools.add(a);\n else positionals.push(a);\n i += 1;\n }\n }\n\n const verbRaw = positionals[0];\n const help = bools.has(\"--help\") || bools.has(\"-h\") || verbRaw === \"help\";\n const verb = verbRaw === undefined ? \"run\" : KNOWN_VERBS.has(verbRaw) ? verbRaw : verbRaw;\n\n return {\n verb,\n rest: positionals.slice(1),\n config: flags.get(\"--config\"),\n report: flags.get(\"--report\"),\n decisions: flags.get(\"--decisions\"),\n db: flags.get(\"--db\"),\n noReport: bools.has(\"--no-report\"),\n run: bools.has(\"--run\"),\n force: bools.has(\"--force\"),\n help,\n };\n}\n",
6
+ "/**\n * Resolve the paths and Stryker config `polly mutate` works with. Everything is\n * resolved against the cwd the command was invoked from, so a consumer's project\n * tree works the same as this repo. Stryker-config discovery reuses the existing\n * `findStrykerConfigs` helper that `polly coverage` already depends on.\n */\nimport { resolve } from \"node:path\";\nimport { findStrykerConfigs } from \"../../test/src/coverage-policy/mutate-targets.ts\";\nimport type { MutateArgs } from \"./args.ts\";\n\nconst DEFAULT_REPORT = \"reports/mutation/mutation.json\";\nconst DEFAULT_DECISIONS = \".polly/test-debt/decisions.jsonl\";\n\nexport interface MutateConfig {\n cwd: string;\n /** The Stryker config to use — explicit `--config`, else the first discovered. Null if none found. */\n strykerConfigPath: string | null;\n /** mutation-testing-report-schema JSON the analysis reads. */\n reportPath: string;\n /** Intermediate SQLite db (\":memory:\" by default; a path persists it). */\n dbPath: string;\n /** Version-controlled human-verdict log. */\n decisionsPath: string;\n}\n\nexport async function resolveMutateConfig(cwd: string, args: MutateArgs): Promise<MutateConfig> {\n let strykerConfigPath: string | null = null;\n if (args.config) {\n strykerConfigPath = resolve(cwd, args.config);\n } else {\n // findStrykerConfigs returns sorted absolute paths; the root stryker.conf.json\n // sorts before any stryker/ shard, so [0] is the root config when present.\n const found = await findStrykerConfigs(cwd);\n strykerConfigPath = found[0] ?? null;\n }\n\n return {\n cwd,\n strykerConfigPath,\n reportPath: resolve(cwd, args.report ?? DEFAULT_REPORT),\n dbPath: args.db ? resolve(cwd, args.db) : \":memory:\",\n decisionsPath: resolve(cwd, args.decisions ?? DEFAULT_DECISIONS),\n };\n}\n",
7
+ "/**\n * @fairfox/polly/test/coverage — Stryker mutate-/test-target validation.\n *\n * A Stryker config's `mutate` and `testFiles` lists are hand-curated. A path\n * that is renamed, or a glob whose directory moves, silently resolves to\n * nothing — Stryker mutates fewer files (or none) and the only signal is a\n * long mutation run whose score quietly drops. This asserts, in milliseconds,\n * that every entry still resolves to at least one file.\n *\n * Discovers configs two ways, covering both layouts in the wild: a single\n * `stryker.conf.json` at the root, and a `stryker/` directory of per-package\n * shards.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { Glob } from \"bun\";\n\ninterface StrykerConfig {\n mutate?: string[];\n testFiles?: string[];\n bun?: { testFiles?: string[] };\n}\n\nexport interface MutateTargetIssue {\n config: string;\n field: \"mutate\" | \"testFiles\";\n pattern: string;\n}\n\nexport interface MutateTargetReport {\n configs: string[];\n issues: MutateTargetIssue[];\n}\n\n/** Locate every Stryker config under the project root. */\nexport async function findStrykerConfigs(root: string): Promise<string[]> {\n const found: string[] = [];\n const single = join(root, \"stryker.conf.json\");\n if (existsSync(single)) found.push(single);\n\n const glob = new Glob(\"stryker/*.{json,conf.json}\");\n for await (const rel of glob.scan({ cwd: root, onlyFiles: true })) {\n found.push(join(root, rel));\n }\n return found.sort();\n}\n\nfunction isGlob(pattern: string): boolean {\n return pattern.includes(\"*\") || pattern.includes(\"?\") || pattern.includes(\"[\");\n}\n\nasync function resolvesToFile(pattern: string, cwd: string): Promise<boolean> {\n if (pattern.startsWith(\"!\")) return true; // negations are filters, not targets\n if (!isGlob(pattern)) return existsSync(resolve(cwd, pattern));\n const glob = new Glob(pattern);\n for await (const _ of glob.scan({ cwd, onlyFiles: true })) return true;\n return false;\n}\n\nasync function checkField(\n configPath: string,\n field: \"mutate\" | \"testFiles\",\n patterns: string[] | undefined,\n cwd: string\n): Promise<MutateTargetIssue[]> {\n const issues: MutateTargetIssue[] = [];\n for (const pattern of patterns ?? []) {\n if (!(await resolvesToFile(pattern, cwd))) {\n issues.push({ config: configPath, field, pattern });\n }\n }\n return issues;\n}\n\n/**\n * Validate every Stryker config under `root`. Paths in a Stryker config are\n * relative to that config's directory; for the monorepo-root configs that is\n * the root, so we resolve globs against `root`.\n */\nexport async function validateMutateTargets(root: string): Promise<MutateTargetReport> {\n const configs = await findStrykerConfigs(root);\n const issues: MutateTargetIssue[] = [];\n for (const configPath of configs) {\n const config = JSON.parse(readFileSync(configPath, \"utf8\")) as unknown as StrykerConfig;\n const testFiles = config.testFiles ?? config.bun?.testFiles;\n issues.push(...(await checkField(configPath, \"mutate\", config.mutate, root)));\n issues.push(...(await checkField(configPath, \"testFiles\", testFiles, root)));\n }\n return { configs, issues };\n}\n",
8
+ "/**\n * Decisions log — the version-controlled human-judgement layer.\n *\n * The signals (kill matrix, subsumption) are derived and regenerable; they live\n * in the gitignored SQLite db. *Decisions* about them — \"this subsumed file is a\n * legitimate set of input-class guards, keep it\" — are precious, can't be\n * regenerated, and are committed here as JSONL (one record per line: mergeable,\n * diffable, append-only, last-write-wins per file).\n *\n * Identity is the test FILE (stable across describe restructuring; git tracks\n * renames). Each decision stores two freshness stamps:\n * - hash: a normalised content hash of the test file at decision time.\n * Differs → the test changed since you judged it → re-review.\n * - signal: the subsumption snapshot at decision time. Drifts (e.g. a test\n * elsewhere was deleted, so this file's kills became unique) → the\n * matrix around it changed → re-review.\n *\n * A decision is trusted only while BOTH still hold. That's what stops a \"keep\"\n * verdict from silently outliving the reason it was made.\n *\n * polly mutate decisions # status (the join)\n * polly mutate decisions decide <file> <verdict> \"<rationale>\"\n *\n * verdicts: keep | prune | rewrite | investigate\n */\nimport type { Database } from \"bun:sqlite\";\nimport { createHash } from \"node:crypto\";\nimport { buildDb, loadMutationReport, queryAll } from \"./ingest.ts\";\n\nconst DEFAULT_LOG = \".polly/test-debt/decisions.jsonl\";\nconst DEFAULT_REPORT = \"reports/mutation/mutation.json\";\nconst VERDICTS = [\"keep\", \"prune\", \"rewrite\", \"investigate\"];\n\nexport interface FileSignal {\n kills: number;\n uniqueKills: number;\n subsumed: boolean;\n}\ninterface Decision {\n file: string;\n verdict: string;\n rationale: string;\n signal: FileSignal;\n hash: string | null;\n decided: string;\n}\n\n/** Strip comments + collapse whitespace so formatting/comment edits don't trip the stamp.\n * Heuristic (not a full parser): a false change only triggers a harmless re-review. */\nfunction normalizeSource(src: string): string {\n return src\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, \"\")\n .replace(/(^|[^:])\\/\\/[^\\n]*/g, \"$1\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\nasync function fileHash(file: string): Promise<string | null> {\n const f = Bun.file(file);\n if (!(await f.exists())) return null;\n return createHash(\"sha256\")\n .update(normalizeSource(await f.text()))\n .digest(\"hex\")\n .slice(0, 16);\n}\n\nexport function fileSignals(db: Database): Map<string, FileSignal> {\n const rows = queryAll<{ file: string; kills: number; unique_kills: number }>(\n db,\n `\n\t\t\tWITH file_kill AS (SELECT DISTINCT t.file file, k.mutant_id mid FROM kills k JOIN tests t ON t.id=k.test_id),\n\t\t\t owner AS (SELECT mid, count(DISTINCT file) nf FROM file_kill GROUP BY mid)\n\t\t\tSELECT fk.file file, count(DISTINCT fk.mid) kills,\n\t\t\t sum(CASE WHEN o.nf=1 THEN 1 ELSE 0 END) unique_kills\n\t\t\tFROM file_kill fk JOIN owner o ON o.mid=fk.mid GROUP BY fk.file`\n );\n const m = new Map<string, FileSignal>();\n for (const r of rows)\n m.set(r.file, {\n kills: r.kills,\n uniqueKills: r.unique_kills,\n subsumed: r.kills > 0 && r.unique_kills === 0,\n });\n return m;\n}\n\nasync function loadDecisions(logPath: string): Promise<Map<string, Decision>> {\n const m = new Map<string, Decision>();\n const f = Bun.file(logPath);\n if (!(await f.exists())) return m;\n for (const line of (await f.text()).split(\"\\n\")) {\n const t = line.trim();\n if (!t) continue;\n const d = JSON.parse(t) as unknown as Decision;\n m.set(d.file, d); // last write wins\n }\n return m;\n}\n\n/** Why a decision no longer applies, or null if it still holds. */\nfunction staleReason(\n d: Decision,\n currentHash: string | null,\n current: FileSignal | undefined\n): string | null {\n if (!current) return \"file no longer kills any mutant (renamed/deleted/refactored?)\";\n if (d.hash !== currentHash) return \"test file changed since the decision\";\n if (d.signal.subsumed !== current.subsumed)\n return `subsumption flipped (${d.signal.subsumed} → ${current.subsumed})`;\n if (d.signal.uniqueKills > 0 !== current.uniqueKills > 0)\n return `unique-kill status changed (${d.signal.uniqueKills} → ${current.uniqueKills})`;\n return null;\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: a linear join of signals against decisions — clearer as one pass than split.\nexport async function status(db: Database, logPath: string = DEFAULT_LOG): Promise<string> {\n const signals = fileSignals(db);\n const decisions = await loadDecisions(logPath);\n const needsReview: string[] = [];\n const stale: string[] = [];\n const settled: string[] = [];\n\n for (const [file, sig] of [...signals].sort((a, b) => b[1].kills - a[1].kills)) {\n const d = decisions.get(file);\n if (!d) {\n if (sig.subsumed) needsReview.push(` ${file} (${sig.kills} kills, 0 unique)`);\n continue;\n }\n const reason = staleReason(d, await fileHash(file), sig);\n if (reason) stale.push(` ${file} [${d.verdict}] — ${reason}`);\n else settled.push(` ${file} [${d.verdict}] ${d.rationale ? `— ${d.rationale}` : \"\"}`);\n }\n // decided files that have dropped out of the signal set entirely\n for (const [file, d] of decisions) {\n if (!signals.has(file)) stale.push(` ${file} [${d.verdict}] — file no longer in matrix`);\n }\n\n const out: string[] = [];\n out.push(`NEEDS REVIEW (subsumed files with no decision — ${needsReview.length})`);\n out.push(needsReview.length ? needsReview.join(\"\\n\") : \" none\");\n out.push(`\\nSTALE (decision's basis drifted — re-review — ${stale.length})`);\n out.push(stale.length ? stale.join(\"\\n\") : \" none\");\n out.push(`\\nSETTLED (fresh decisions, suppressed from action list — ${settled.length})`);\n out.push(settled.length ? settled.join(\"\\n\") : \" none\");\n return out.join(\"\\n\");\n}\n\nexport async function decide(\n file: string,\n verdict: string,\n rationale: string,\n db: Database,\n logPath: string = DEFAULT_LOG\n) {\n if (!VERDICTS.includes(verdict)) {\n console.log(`✗ verdict must be one of: ${VERDICTS.join(\", \")}`);\n process.exit(1);\n }\n const sig = fileSignals(db).get(file);\n if (!sig) {\n console.log(`✗ ${file} is not a test file that kills any mutant in the current matrix`);\n process.exit(1);\n }\n const decided = new Date().toISOString().slice(0, 10);\n const record: Decision = {\n file,\n verdict,\n rationale,\n signal: sig,\n hash: await fileHash(file),\n decided,\n };\n await Bun.write(\n logPath,\n ((await Bun.file(logPath).exists()) ? await Bun.file(logPath).text() : \"\") +\n JSON.stringify(record) +\n \"\\n\"\n );\n console.log(`✓ recorded: ${file} [${verdict}]${rationale ? ` — ${rationale}` : \"\"}`);\n}\n\nif (import.meta.main) {\n const db = buildDb(await loadMutationReport(DEFAULT_REPORT), \"reports/test-debt.sqlite\");\n const [cmd, ...rest] = process.argv.slice(2);\n if (cmd === \"decide\") {\n const [file, verdict, ...r] = rest;\n if (!file || !verdict) {\n console.log('usage: decide <file> <keep|prune|rewrite|investigate> \"<rationale>\"');\n process.exit(1);\n }\n await decide(file, verdict, r.join(\" \").replace(/^\"|\"$/g, \"\"), db);\n } else {\n console.log(await status(db));\n }\n db.close();\n}\n",
9
+ "/**\n * Ingest a Stryker mutation report (mutation-testing-report-schema) into a\n * regenerable SQLite database holding the kill matrix.\n *\n * The tool consumes the *schema*, never a specific runner — so the same\n * analysis works whether the matrix came from the patched bun runner, the\n * tap runner, or an eventual official Bun plugin. The db is a build artefact\n * (gitignored); decisions are kept elsewhere in version control.\n */\nimport { Database } from \"bun:sqlite\";\n\nexport interface MutationReport {\n schemaVersion: string;\n files: Record<string, { mutants: Mutant[] }>;\n testFiles?: Record<string, { tests: TestDef[] }>;\n}\ninterface Mutant {\n id: string;\n mutatorName: string;\n status: string;\n location: { start: { line: number; column: number } };\n killedBy?: string[];\n coveredBy?: string[];\n}\ninterface TestDef {\n id: string;\n name: string;\n location?: { start: { line: number; column: number } };\n}\n\nexport function buildDb(report: MutationReport, dbPath = \":memory:\"): Database {\n const db = new Database(dbPath);\n db.exec(\"PRAGMA journal_mode = WAL;\");\n db.exec(`\n\t\tDROP TABLE IF EXISTS kills;\n\t\tDROP TABLE IF EXISTS covers;\n\t\tDROP TABLE IF EXISTS mutants;\n\t\tDROP TABLE IF EXISTS tests;\n\t\tCREATE TABLE tests (\n\t\t\tid TEXT PRIMARY KEY,\n\t\t\tfile TEXT NOT NULL,\n\t\t\tname TEXT NOT NULL,\n\t\t\tline INTEGER\n\t\t);\n\t\tCREATE TABLE mutants (\n\t\t\tid TEXT PRIMARY KEY,\n\t\t\tmutator TEXT NOT NULL,\n\t\t\tfile TEXT NOT NULL,\n\t\t\tline INTEGER,\n\t\t\tstatus TEXT NOT NULL\n\t\t);\n\t\t-- the kill matrix: (mutant, test) where test detects mutant\n\t\tCREATE TABLE kills (\n\t\t\tmutant_id TEXT NOT NULL REFERENCES mutants(id),\n\t\t\ttest_id TEXT NOT NULL REFERENCES tests(id),\n\t\t\tPRIMARY KEY (mutant_id, test_id)\n\t\t);\n\t\t-- coverage matrix (populated only when the runner reports coveredBy)\n\t\tCREATE TABLE covers (\n\t\t\tmutant_id TEXT NOT NULL REFERENCES mutants(id),\n\t\t\ttest_id TEXT NOT NULL REFERENCES tests(id),\n\t\t\tPRIMARY KEY (mutant_id, test_id)\n\t\t);\n\t\tCREATE INDEX idx_kills_test ON kills(test_id);\n\t\tCREATE INDEX idx_kills_mutant ON kills(mutant_id);\n\t`);\n\n const insTest = db.prepare(\n \"INSERT OR IGNORE INTO tests (id, file, name, line) VALUES (?, ?, ?, ?)\"\n );\n const insMut = db.prepare(\n \"INSERT INTO mutants (id, mutator, file, line, status) VALUES (?, ?, ?, ?, ?)\"\n );\n const insKill = db.prepare(\"INSERT OR IGNORE INTO kills (mutant_id, test_id) VALUES (?, ?)\");\n const insCover = db.prepare(\"INSERT OR IGNORE INTO covers (mutant_id, test_id) VALUES (?, ?)\");\n\n // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: two flat insert loops inside one transaction — keeping them together is the point.\n const ingest = db.transaction(() => {\n for (const [file, tf] of Object.entries(report.testFiles ?? {})) {\n for (const t of tf.tests) {\n insTest.run(t.id, file || \"(unknown)\", t.name, t.location?.start.line ?? null);\n }\n }\n for (const [file, f] of Object.entries(report.files)) {\n for (const m of f.mutants) {\n insMut.run(m.id, m.mutatorName, file, m.location.start.line, m.status);\n for (const tid of m.killedBy ?? []) insKill.run(m.id, tid);\n for (const tid of m.coveredBy ?? []) insCover.run(m.id, tid);\n }\n }\n });\n ingest();\n return db;\n}\n\n// SQL rows and the on-disk report arrive untyped; these three helpers are the\n// single sanctioned cast boundary (the `as unknown as` escape hatch) so call\n// sites stay assertion-free and the column/shape types live next to the query.\nexport function queryAll<T>(db: Database, sql: string): T[] {\n return db.query(sql).all() as unknown as T[];\n}\nexport function queryGet<T>(db: Database, sql: string): T | null {\n return db.query(sql).get() as unknown as T | null;\n}\nexport async function loadMutationReport(path: string): Promise<MutationReport> {\n return (await Bun.file(path).json()) as unknown as MutationReport;\n}\n\nif (import.meta.main) {\n const reportPath = process.argv[2] ?? \"reports/mutation/mutation.json\";\n const dbPath = process.argv[3] ?? \"reports/test-debt.sqlite\";\n const db = buildDb(await loadMutationReport(reportPath), dbPath);\n const n = queryGet<{ t: number; m: number; k: number }>(\n db,\n \"SELECT (SELECT count(*) FROM tests) t, (SELECT count(*) FROM mutants) m, (SELECT count(*) FROM kills) k\"\n ) ?? { t: 0, m: 0, k: 0 };\n console.log(`Ingested ${reportPath} -> ${dbPath}`);\n console.log(` ${n.t} tests, ${n.m} mutants, ${n.k} kill-edges`);\n db.close();\n}\n",
10
+ "/**\n * `polly mutate init` — scaffold a `stryker.conf.json` for a project that\n * depends on @fairfox/polly, wired with the verify-primitive ignorer preset.\n *\n * The one fiddly bit is the plugin reference. Stryker's plugin loader resolves\n * specifiers from inside its OWN package, so the bare \"@fairfox/polly/stryker\"\n * only resolves on a flat node_modules layout. In a workspace (monorepo) or a\n * Bun-isolated install it must be referenced by path to the built file. We\n * resolve the actual plugin via Bun's resolver and pick the form that will load.\n */\nimport { existsSync } from \"node:fs\";\nimport { join, relative } from \"node:path\";\n\nexport interface InitResult {\n configPath: string;\n /** false when a config already existed and --force was not passed. */\n created: boolean;\n pluginRef: string;\n mutate: string[];\n testGlob: string;\n warnings: string[];\n}\n\n/**\n * The reference to write for the @fairfox/polly/stryker plugin: the bare\n * specifier when @fairfox/polly is hoisted into a flat node_modules (Stryker\n * resolves it), else a relative path to the built file (workspace / Bun-isolated).\n */\nfunction resolvePluginRef(projectDir: string): string {\n const resolved = Bun.resolveSync(\"@fairfox/polly/stryker\", projectDir);\n const hoisted = resolved.includes(\"/node_modules/\") && !resolved.includes(\"/.bun/\");\n return hoisted ? \"@fairfox/polly/stryker\" : relative(projectDir, resolved);\n}\n\nfunction detectMutate(projectDir: string): string[] {\n if (existsSync(join(projectDir, \"src\"))) {\n return [\"src/**/*.ts\", \"!src/**/*.test.ts\", \"!src/**/*.d.ts\"];\n }\n return [\"**/*.ts\", \"!**/*.test.ts\", \"!**/*.d.ts\", \"!node_modules/**\", \"!dist/**\"];\n}\n\nfunction detectTestGlob(projectDir: string): string {\n for (const d of [\"tests\", \"test\"]) {\n if (existsSync(join(projectDir, d))) return `${d}/**/*.test.ts`;\n }\n return \"**/*.test.ts\";\n}\n\nexport async function initConfig(\n projectDir: string,\n opts: { force?: boolean } = {}\n): Promise<InitResult> {\n const configPath = join(projectDir, \"stryker.conf.json\");\n const warnings: string[] = [];\n\n let pluginRef: string;\n try {\n pluginRef = resolvePluginRef(projectDir);\n } catch {\n throw new Error(\n \"Cannot resolve @fairfox/polly from here. Run `polly mutate init` in a project that depends on @fairfox/polly.\"\n );\n }\n\n const mutate = detectMutate(projectDir);\n const testGlob = detectTestGlob(projectDir);\n if (!existsSync(join(projectDir, \"src\"))) {\n warnings.push(\"no src/ directory — adjust the `mutate` globs to point at your source\");\n }\n if (!existsSync(join(projectDir, \"tests\")) && !existsSync(join(projectDir, \"test\"))) {\n warnings.push(`no tests/ directory — using \"${testGlob}\"; adjust bun.testFiles if needed`);\n }\n\n if (existsSync(configPath) && !opts.force) {\n return { configPath, created: false, pluginRef, mutate, testGlob, warnings };\n }\n\n // Note: redundancy/subsumption need the no-bail patched bun runner (disableBail\n // + coverageAnalysis \"all\"); without the patch the matrix is partial and\n // `polly mutate report` degrades to score + gaps + theatre.\n const config = {\n testRunner: \"bun\",\n bun: { testFiles: [testGlob], timeout: 30000 },\n plugins: [\"stryker-mutator-bun-runner\", pluginRef],\n ignorers: [\"polly-verify\"],\n coverageAnalysis: \"all\",\n disableBail: true,\n reporters: [\"json\", \"clear-text\"],\n mutate,\n thresholds: { high: 90, low: 70, break: null },\n ignorePatterns: [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/.git/**\",\n \"**/.stryker-tmp/**\",\n \"**/reports/**\",\n ],\n timeoutMS: 60000,\n concurrency: 4,\n };\n await Bun.write(configPath, `${JSON.stringify(config, null, 2)}\\n`);\n return { configPath, created: true, pluginRef, mutate, testGlob, warnings };\n}\n",
11
+ "/**\n * Orchestration for `polly mutate run` / `report`: spawn Stryker, then read the\n * mutation report and produce the useless-test analysis — gating the redundancy\n * /subsumption sections on a complete (no-bail) kill matrix.\n */\nimport { resolve } from \"node:path\";\nimport type { MutateConfig } from \"./config.ts\";\nimport { buildDb, loadMutationReport } from \"./ingest.ts\";\nimport { report } from \"./report.ts\";\nimport { isMatrixComplete } from \"./verify-matrix.ts\";\n\n/** Run Stryker against the resolved config. Inherits stdio for the live reporter; returns the exit code. */\nexport async function runStryker(cfg: MutateConfig): Promise<number> {\n // Pass --configFile only when the located config is NOT the auto-discoverable\n // root stryker.conf.json — that keeps a bare repo run identical to `stryker run`.\n const autoDiscoverable = cfg.strykerConfigPath === resolve(cfg.cwd, \"stryker.conf.json\");\n const configFlag =\n cfg.strykerConfigPath && !autoDiscoverable ? [\"--configFile\", cfg.strykerConfigPath] : [];\n const proc = Bun.spawn([\"bunx\", \"stryker\", \"run\", ...configFlag], {\n cwd: cfg.cwd,\n stdout: \"inherit\",\n stderr: \"inherit\",\n stdin: \"inherit\",\n env: process.env,\n });\n return await proc.exited;\n}\n\n/** Build the useless-test report from an existing mutation.json (no Stryker run). */\nexport async function reportFromFile(cfg: MutateConfig): Promise<string> {\n if (!(await Bun.file(cfg.reportPath).exists())) {\n throw new Error(\n `no mutation report at ${cfg.reportPath}. Run 'polly mutate run' (or 'bun run mutation') first, or pass --report <path>.`\n );\n }\n const mreport = await loadMutationReport(cfg.reportPath);\n const db = buildDb(mreport, cfg.dbPath);\n try {\n return report(db, { matrixComplete: isMatrixComplete(mreport) });\n } finally {\n db.close();\n }\n}\n\n/**\n * If the Stryker config doesn't already load polly's verify-primitive ignorer,\n * return a one-line tip. The ignorer must be in the config before the run (it's\n * read at Stryker startup), so this advises rather than mutating the user's config.\n */\nexport async function presetAdvisory(cfg: MutateConfig): Promise<string | null> {\n if (!cfg.strykerConfigPath?.endsWith(\".json\")) return null;\n try {\n const conf = (await Bun.file(cfg.strykerConfigPath).json()) as unknown as {\n plugins?: string[];\n };\n if ((conf.plugins ?? []).includes(\"@fairfox/polly/stryker\")) return null;\n return (\n \"Tip: spread pollyStrykerPreset (from @fairfox/polly/stryker) into your Stryker config — \" +\n \"verify primitives (requires/ensures/…) are runtime no-ops and their mutants always survive, \" +\n \"dragging the score down with noise. See polly#143.\"\n );\n } catch {\n return null;\n }\n}\n",
12
+ "/**\n * Test-debt report: derive the three signals from the kill matrix.\n *\n * gaps survived mutants — code no test pins (the Stryker direction)\n * redundancy tests/files whose every kill is also caught elsewhere\n * theatre covered-but-survived — code a test runs but no test detects\n * (the Survived status under coverageAnalysis \"all\")\n *\n * Redundancy is reported at BOTH file and case level. Decisions are keyed at\n * file level (stable identity); case level is the drill-down.\n */\nimport type { Database } from \"bun:sqlite\";\nimport { buildDb, loadMutationReport, queryAll, queryGet } from \"./ingest.ts\";\n\ntype SubsumedCase = { file: string; name: string; total: number };\ntype LineCount = { line: number; mutator: string; n: number };\n\n/** Greedy set cover: smallest set of `units` whose kill-sets cover all killed mutants. */\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: a textbook greedy set-cover loop — clearer whole than split.\nfunction greedyCover(units: Map<string, Set<string>>, universe: Set<string>): string[] {\n const remaining = new Set(universe);\n const chosen: string[] = [];\n const pool = new Map([...units].map(([k, v]) => [k, new Set(v)]));\n while (remaining.size > 0) {\n let best: string | null = null;\n let bestGain = 0;\n for (const [unit, set] of pool) {\n let gain = 0;\n for (const m of set) if (remaining.has(m)) gain++;\n if (gain > bestGain) {\n bestGain = gain;\n best = unit;\n }\n }\n if (best === null || bestGain === 0) break; // remaining mutants killed by nobody (shouldn't happen for killed set)\n chosen.push(best);\n for (const m of pool.get(best) ?? []) remaining.delete(m);\n pool.delete(best);\n }\n return chosen;\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: one report builder emitting all sections in order — splitting would scatter the shared db/out.\nexport function report(db: Database, opts: { matrixComplete?: boolean } = {}): string {\n // Defaults to true so `report(db)` is byte-identical to before (the parity\n // guard depends on this). When false — the kill matrix is partial (an\n // unpatched/bail runner) — the redundancy/subsumption sections would lie, so\n // they are suppressed and a warning shown; score/gaps/theatre still hold.\n const matrixComplete = opts.matrixComplete ?? true;\n const out: string[] = [];\n const p = (s = \"\") => out.push(s);\n\n // --- summary ---\n const tot = queryGet<{\n n: number;\n killed: number;\n survived: number;\n timeout: number;\n nocov: number;\n }>(\n db,\n \"SELECT count(*) n, sum(status='Killed') killed, sum(status='Survived') survived, sum(status='Timeout') timeout, sum(status='NoCoverage') nocov FROM mutants\"\n ) ?? { n: 0, killed: 0, survived: 0, timeout: 0, nocov: 0 };\n const nTests = queryGet<{ n: number }>(db, \"SELECT count(*) n FROM tests\")?.n ?? 0;\n const detected = tot.killed + tot.timeout;\n const score = ((detected / (tot.n - tot.nocov || 1)) * 100).toFixed(1);\n p(\n `MUTATION score ${score}% ${tot.killed} killed · ${tot.timeout} timeout · ${tot.survived} survived · ${tot.nocov} no-coverage (${tot.n} mutants, ${nTests} tests)`\n );\n\n if (matrixComplete) {\n // --- kill-multiplicity: how much slack is in the matrix ---\n const mult = queryAll<{ bucket: string; n: number }>(\n db,\n `\n\t\t\tSELECT CASE WHEN c=1 THEN '1' WHEN c=2 THEN '2' WHEN c BETWEEN 3 AND 5 THEN '3-5' ELSE '6+' END bucket, count(*) n\n\t\t\tFROM (SELECT mutant_id, count(*) c FROM kills GROUP BY mutant_id)\n\t\t\tGROUP BY bucket ORDER BY min(c)`\n );\n p(`\\nKILL MULTIPLICITY (killers per killed mutant — higher = more redundancy slack)`);\n for (const r of mult) p(` killed by ${String(r.bucket).padStart(3)} test(s): ${r.n}`);\n\n // --- redundancy ratio via greedy cover (case + file) ---\n const universe = new Set(\n queryAll<{ mutant_id: string }>(db, \"SELECT DISTINCT mutant_id FROM kills\").map(\n (r) => r.mutant_id\n )\n );\n const byTest = new Map<string, Set<string>>();\n for (const r of queryAll<{ test_id: string; mutant_id: string }>(\n db,\n \"SELECT test_id, mutant_id FROM kills\"\n )) {\n const set = byTest.get(r.test_id) ?? new Set<string>();\n byTest.set(r.test_id, set);\n set.add(r.mutant_id);\n }\n const byFile = new Map<string, Set<string>>();\n for (const r of queryAll<{ file: string; mid: string }>(\n db,\n \"SELECT t.file file, k.mutant_id mid FROM kills k JOIN tests t ON t.id=k.test_id\"\n )) {\n const set = byFile.get(r.file) ?? new Set<string>();\n byFile.set(r.file, set);\n set.add(r.mid);\n }\n const caseCover = greedyCover(byTest, universe);\n const fileCover = greedyCover(byFile, universe);\n p(`\\nREDUNDANCY RATIO (tests that kill something ÷ minimal covering set)`);\n p(\n ` case level: ${byTest.size} killing tests → ${caseCover.length} needed (${(byTest.size / Math.max(1, caseCover.length)).toFixed(2)}×, ~${byTest.size - caseCover.length} prunable)`\n );\n p(\n ` file level: ${byFile.size} killing files → ${fileCover.length} needed (${(byFile.size / Math.max(1, fileCover.length)).toFixed(2)}×, ~${byFile.size - fileCover.length} prunable)`\n );\n\n // --- fully subsumed FILES (every mutant they kill is killed elsewhere too) ---\n const subsumedFiles = queryAll<{ file: string; kills: number }>(\n db,\n `\n\t\t\tWITH file_kill AS (SELECT DISTINCT t.file file, k.mutant_id mid FROM kills k JOIN tests t ON t.id=k.test_id),\n\t\t\t owner AS (SELECT mid, count(DISTINCT file) nf, min(file) only_file FROM file_kill GROUP BY mid)\n\t\t\tSELECT fk.file, count(DISTINCT fk.mid) kills,\n\t\t\t sum(CASE WHEN o.nf=1 THEN 1 ELSE 0 END) unique_kills\n\t\t\tFROM file_kill fk JOIN owner o ON o.mid=fk.mid\n\t\t\tGROUP BY fk.file HAVING unique_kills=0 ORDER BY kills DESC`\n );\n p(`\\nSUBSUMED FILES (file-level identity — safe-to-review deletion candidates)`);\n if (subsumedFiles.length === 0)\n p(` none — every test file kills at least one mutant no other file catches`);\n for (const r of subsumedFiles) p(` ${r.file} (${r.kills} kills, 0 unique)`);\n\n // --- fully subsumed CASES (drill-down) ---\n const subsumedCases = queryAll<SubsumedCase>(\n db,\n `\n\t\t\tWITH mk AS (SELECT mutant_id, count(*) c FROM kills GROUP BY mutant_id),\n\t\t\t tk AS (SELECT test_id, count(*) total, sum(CASE WHEN mk.c=1 THEN 1 ELSE 0 END) uniq\n\t\t\t FROM kills JOIN mk USING(mutant_id) GROUP BY test_id)\n\t\t\tSELECT t.file, t.name, tk.total FROM tk JOIN tests t ON t.id=tk.test_id\n\t\t\tWHERE tk.total>0 AND tk.uniq=0 ORDER BY t.file, tk.total DESC`\n );\n const byFileCases = new Map<string, SubsumedCase[]>();\n for (const r of subsumedCases) {\n const rows = byFileCases.get(r.file) ?? [];\n byFileCases.set(r.file, rows);\n rows.push(r);\n }\n p(\n `\\nSUBSUMED CASES (${subsumedCases.length} individual tests whose every kill is caught elsewhere)`\n );\n for (const [file, rows] of byFileCases) {\n p(` ${file}`);\n for (const r of rows) p(` “${r.name}” (${r.total} kills, 0 unique)`);\n }\n } else {\n p(`\\n⚠ KILL MATRIX INCOMPLETE — redundancy/subsumption analysis suppressed.`);\n p(` Every mutant has at most one recorded killer, so \"which tests are redundant\"`);\n p(` cannot be derived. This needs the no-bail patched Bun runner (not shipped with`);\n p(\n ` @fairfox/polly). Run 'polly mutate verify' to diagnose. Score/gaps/theatre below still hold.`\n );\n }\n\n // Under coverageAnalysis \"all\", Stryker classifies a non-killed mutant as\n // NoCoverage (no test runs it — a true gap) or Survived (a test runs it but\n // nothing detects the mutation — theatre). That status split IS the\n // gap/theatre distinction; it requires coverage to be on (see README).\n const srcFile = queryGet<{ file: string }>(db, \"SELECT file FROM mutants LIMIT 1\")?.file;\n\n // --- gaps: code no test even executes ---\n const gaps = queryAll<LineCount>(\n db,\n \"SELECT line, mutator, count(*) n FROM mutants WHERE status='NoCoverage' GROUP BY line, mutator ORDER BY line\"\n );\n const gapTotal = gaps.reduce((a, r) => a + r.n, 0);\n p(`\\nGAPS (NoCoverage — no test executes this code: ${gapTotal})`);\n if (gapTotal === 0) p(` none — every mutable location is reached by some test`);\n for (const r of gaps) p(` ${srcFile}:${r.line} ${r.mutator}${r.n > 1 ? ` ×${r.n}` : \"\"}`);\n\n // --- theatre: covered but survived ---\n const theatre = queryAll<LineCount>(\n db,\n \"SELECT line, mutator, count(*) n FROM mutants WHERE status='Survived' GROUP BY line, mutator ORDER BY line\"\n );\n const theatreTotal = theatre.reduce((a, r) => a + r.n, 0);\n p(`\\nTHEATRE (Survived — code is covered but no test detects the mutation: ${theatreTotal})`);\n if (theatreTotal === 0) p(` none`);\n for (const r of theatre) p(` ${srcFile}:${r.line} ${r.mutator}${r.n > 1 ? ` ×${r.n}` : \"\"}`);\n\n return out.join(\"\\n\");\n}\n\nif (import.meta.main) {\n const reportPath = process.argv[2] ?? \"reports/mutation/mutation.json\";\n const db = buildDb(await loadMutationReport(reportPath), \"reports/test-debt.sqlite\");\n console.log(report(db));\n db.close();\n}\n",
13
+ "/**\n * Verification artefact for the kill-matrix contract.\n *\n * The whole useless-test analysis rests on one fact: the patched bun runner\n * emits a COMPLETE kill matrix — every test that detects a mutant appears in\n * killedBy, not just the first. That depends on a pinned pair: (Bun version,\n * plugin version + our patch). A Bun upgrade that changes JUnit output, or a\n * patch that stops applying, would silently collapse killedBy to one id each —\n * a green board with the redundancy signal quietly dead (the classic trap).\n *\n * `verifyMatrix` runs the full contract and returns the result; `isMatrixComplete`\n * is the side-effect-free gate the report path consults to decide whether the\n * redundancy/subsumption sections are trustworthy. Run the CLI form after any\n * bump of Bun or the plugin:\n *\n * polly mutate verify # uses the existing report\n * polly mutate verify --run # runs stryker first\n */\nimport { loadMutationReport, type MutationReport } from \"./ingest.ts\";\n\nconst DEFAULT_REPORT = \"reports/mutation/mutation.json\";\n\nexport interface MatrixCheck {\n ok: boolean;\n checks: { name: string; ok: boolean; detail: string }[];\n}\n\n/**\n * Side-effect-free gate: is the kill matrix trustworthy for redundancy /\n * subsumption analysis? True only when\n * (a) some killed mutant has >1 killer — proof the no-bail patched runner\n * recorded EVERY killer, not just the first (verifyMatrix check #4), and\n * (b) coverage was collected — not every mutant is NoCoverage, which would\n * mean the coverage dump broke (verifyMatrix check #5).\n * Without both, REDUNDANCY/SUBSUMED would be computed off a partial matrix and\n * lie, so the report path falls back to score + gaps + theatre only.\n */\nexport function isMatrixComplete(report: MutationReport): boolean {\n const mutants = Object.values(report.files).flatMap((f) => f.mutants);\n if (mutants.length === 0) return false;\n const killed = mutants.filter((m) => m.status === \"Killed\");\n const multiKiller = killed.some((m) => (m.killedBy ?? []).length > 1);\n const noCov = mutants.filter((m) => m.status === \"NoCoverage\").length;\n const coverageCollected = noCov < mutants.length;\n return multiKiller && coverageCollected;\n}\n\n/**\n * Run the full kill-matrix contract (all five checks), optionally running a\n * fresh Stryker pass first. Returns the result rather than exiting, so callers\n * decide what to do with it; the `import.meta.main` wrapper prints and sets the\n * exit code.\n */\nexport async function verifyMatrix(\n opts: { reportPath?: string; run?: boolean; strykerConfig?: string } = {}\n): Promise<MatrixCheck> {\n const reportPath = opts.reportPath ?? DEFAULT_REPORT;\n\n if (opts.run) {\n console.log(\"Running mutation pass (this can take many minutes for a large mutate set)...\");\n const cmd = [\n \"bunx\",\n \"stryker\",\n \"run\",\n ...(opts.strykerConfig ? [\"--configFile\", opts.strykerConfig] : []),\n ];\n const proc = Bun.spawn(cmd, { stdout: \"inherit\", stderr: \"inherit\" });\n const code = await proc.exited;\n if (code !== 0) {\n return { ok: false, checks: [{ name: \"stryker run\", ok: false, detail: `exited ${code}` }] };\n }\n }\n\n if (!(await Bun.file(reportPath).exists())) {\n return {\n ok: false,\n checks: [\n {\n name: \"report exists\",\n ok: false,\n detail: `no report at ${reportPath}. Run with --run, or 'bun run mutation' first.`,\n },\n ],\n };\n }\n\n const report = await loadMutationReport(reportPath);\n const checks: { name: string; ok: boolean; detail: string }[] = [];\n const add = (name: string, ok: boolean, detail: string) => checks.push({ name, ok, detail });\n\n // 1. schema present\n add(\"schema version present\", !!report.schemaVersion, `schemaVersion=${report.schemaVersion}`);\n\n // 2. testFiles keyed by real paths, not the \"\" bucket (file-level identity intact)\n const tfKeys = Object.keys(report.testFiles ?? {});\n const realPaths = tfKeys.filter((k) => k && k !== \"(unknown)\");\n add(\n \"testFiles keyed by real paths\",\n realPaths.length > 0 && !tfKeys.includes(\"\"),\n `${realPaths.length} file(s): ${realPaths.slice(0, 2).join(\", \")}${realPaths.length > 2 ? \"…\" : \"\"}`\n );\n\n // 3. test ids resolve\n const testIds = new Set(\n Object.values(report.testFiles ?? {}).flatMap((tf) => tf.tests.map((t) => t.id))\n );\n const mutants = Object.values(report.files).flatMap((f) => f.mutants);\n const killed = mutants.filter((m) => m.status === \"Killed\");\n const danglingKb = killed.filter((m) => (m.killedBy ?? []).some((id) => !testIds.has(id)));\n add(\n \"every killedBy id resolves to a test\",\n danglingKb.length === 0,\n `${danglingKb.length} dangling`\n );\n\n // 4. THE contract: a complete matrix means some killed mutant has >1 killer.\n // If bail silently re-engaged, every killedBy would have length 1.\n const withKb = killed.filter((m) => (m.killedBy ?? []).length > 0);\n const multi = killed.filter((m) => (m.killedBy ?? []).length > 1);\n add(\n \"killed mutants carry killedBy\",\n killed.length > 0 && withKb.length === killed.length,\n `${withKb.length}/${killed.length}`\n );\n add(\n \"matrix is complete (no-bail): >1 killer exists\",\n multi.length > 0,\n `${multi.length} mutant(s) killed by >1 test — collapses to 0 if bail re-engages`\n );\n\n // 5. coverage extraction works: if the afterAll dump fails, the coverage map is\n // empty and Stryker marks EVERY mutant NoCoverage (theatre/gap split dies).\n const noCov = mutants.filter((m) => m.status === \"NoCoverage\").length;\n add(\n \"coverage collected (not all NoCoverage)\",\n noCov < mutants.length,\n `${noCov}/${mutants.length} NoCoverage — all-NoCoverage means the coverage dump broke`\n );\n\n return { ok: checks.every((c) => c.ok), checks };\n}\n\nif (import.meta.main) {\n const result = await verifyMatrix({ run: process.argv.includes(\"--run\") });\n console.log(\"\\nKill-matrix contract:\");\n for (const c of result.checks) {\n console.log(` ${c.ok ? \"✓\" : \"✗\"} ${c.name} (${c.detail})`);\n }\n if (!result.ok) {\n console.log(\n \"\\n✗ Contract broken. The pinned pair has drifted — re-check Bun version and that the patch applies (bun install).\"\n );\n process.exit(1);\n }\n console.log(\"\\n✓ Kill-matrix contract holds.\");\n}\n",
14
+ "#!/usr/bin/env bun\n/**\n * `polly mutate` — run Stryker mutation testing and surface useless/redundant\n * tests (the inverse reading of the kill matrix). Thin dispatcher over the\n * reusable functions in this directory.\n */\nimport { parseMutateArgs } from \"./args.ts\";\nimport { resolveMutateConfig } from \"./config.ts\";\nimport { decide, status } from \"./decisions.ts\";\nimport { buildDb, loadMutationReport } from \"./ingest.ts\";\nimport { initConfig } from \"./init.ts\";\nimport { presetAdvisory, reportFromFile, runStryker } from \"./run.ts\";\nimport { verifyMatrix } from \"./verify-matrix.ts\";\n\nconst HELP = `polly mutate — mutation testing + useless-test detection\n\nUsage:\n polly mutate init [--force] Scaffold a stryker.conf.json for this project\n polly mutate [run] Run Stryker, then print the useless-test report\n polly mutate report Print the report from an existing mutation.json\n polly mutate decisions Review verdicts: needs-review / stale / settled\n polly mutate decisions decide <file> <keep|prune|rewrite|investigate> \"<rationale>\"\n polly mutate verify [--run] Assert the kill-matrix contract (exit 1 on drift)\n polly mutate help Show this help\n\nFlags:\n --config <path> Stryker config (default: auto-discovered)\n --report <path> mutation report JSON (default: reports/mutation/mutation.json)\n --decisions <path> verdict log (default: .polly/test-debt/decisions.jsonl)\n --db <path> intermediate SQLite (default: in-memory)\n --no-report 'run' only — skip the analysis\n -h, --help\n\nThe redundancy/subsumption sections need a COMPLETE kill matrix (the no-bail\npatched Bun runner). Without it the report degrades to score + gaps + theatre;\n'polly mutate verify' diagnoses the matrix.`;\n\nasync function openDb(\n reportPath: string,\n dbPath: string\n): Promise<ReturnType<typeof buildDb> | null> {\n if (!(await Bun.file(reportPath).exists())) {\n console.log(\n `no mutation report at ${reportPath}. Run 'polly mutate run' first, or pass --report <path>.`\n );\n return null;\n }\n return buildDb(await loadMutationReport(reportPath), dbPath);\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: a flat verb dispatcher — the branches are the command surface.\nasync function main(): Promise<number> {\n const args = parseMutateArgs(process.argv.slice(2));\n if (args.help) {\n console.log(HELP);\n return 0;\n }\n\n const cwd = process.cwd();\n const cfg = await resolveMutateConfig(cwd, args);\n\n switch (args.verb) {\n case \"init\": {\n const result = await initConfig(cwd, { force: args.force });\n if (!result.created) {\n console.log(\n `stryker.conf.json already exists at ${result.configPath}. Re-run with --force to overwrite.`\n );\n return 1;\n }\n console.log(`✓ wrote ${result.configPath}`);\n console.log(` plugin: ${result.pluginRef}`);\n console.log(` mutate: ${result.mutate.join(\", \")}`);\n console.log(` testFiles: ${result.testGlob}`);\n for (const w of result.warnings) console.log(` ⚠ ${w}`);\n console.log(\"\\nNext: polly mutate run\");\n return 0;\n }\n\n case \"run\": {\n const tip = await presetAdvisory(cfg);\n if (tip) console.log(`${tip}\\n`);\n const code = await runStryker(cfg);\n if (code !== 0) return code;\n if (args.noReport) return 0;\n console.log(`\\n${await reportFromFile(cfg)}`);\n return 0;\n }\n\n case \"report\": {\n console.log(await reportFromFile(cfg));\n return 0;\n }\n\n case \"decisions\": {\n const db = await openDb(cfg.reportPath, cfg.dbPath);\n if (!db) return 1;\n try {\n if (args.rest[0] === \"decide\") {\n const [, file, verdict, ...r] = args.rest;\n if (!file || !verdict) {\n console.log(\n 'usage: polly mutate decisions decide <file> <keep|prune|rewrite|investigate> \"<rationale>\"'\n );\n return 1;\n }\n await decide(file, verdict, r.join(\" \").replace(/^\"|\"$/g, \"\"), db, cfg.decisionsPath);\n } else {\n console.log(await status(db, cfg.decisionsPath));\n }\n } finally {\n db.close();\n }\n return 0;\n }\n\n case \"verify\": {\n const result = await verifyMatrix({\n reportPath: cfg.reportPath,\n run: args.run,\n strykerConfig: cfg.strykerConfigPath ?? undefined,\n });\n console.log(\"\\nKill-matrix contract:\");\n for (const c of result.checks) console.log(` ${c.ok ? \"✓\" : \"✗\"} ${c.name} (${c.detail})`);\n if (!result.ok) {\n console.log(\n \"\\n✗ Contract broken. The pinned pair has drifted — re-check Bun version and that the patch applies (bun install).\"\n );\n return 1;\n }\n console.log(\"\\n✓ Kill-matrix contract holds.\");\n return 0;\n }\n\n default:\n console.log(`Unknown subcommand: ${args.verb}\\n`);\n console.log(HELP);\n return 1;\n }\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((err) => {\n console.log(`\\n❌ ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n });\n"
15
+ ],
16
+ "mappings": "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",
17
+ "debugId": "768AC36DADFC60B164756E2164756E21",
18
+ "names": []
19
+ }
@@ -0,0 +1,13 @@
1
+ import type { MutateArgs } from "./args.ts";
2
+ export interface MutateConfig {
3
+ cwd: string;
4
+ /** The Stryker config to use — explicit `--config`, else the first discovered. Null if none found. */
5
+ strykerConfigPath: string | null;
6
+ /** mutation-testing-report-schema JSON the analysis reads. */
7
+ reportPath: string;
8
+ /** Intermediate SQLite db (":memory:" by default; a path persists it). */
9
+ dbPath: string;
10
+ /** Version-controlled human-verdict log. */
11
+ decisionsPath: string;
12
+ }
13
+ export declare function resolveMutateConfig(cwd: string, args: MutateArgs): Promise<MutateConfig>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Decisions log — the version-controlled human-judgement layer.
3
+ *
4
+ * The signals (kill matrix, subsumption) are derived and regenerable; they live
5
+ * in the gitignored SQLite db. *Decisions* about them — "this subsumed file is a
6
+ * legitimate set of input-class guards, keep it" — are precious, can't be
7
+ * regenerated, and are committed here as JSONL (one record per line: mergeable,
8
+ * diffable, append-only, last-write-wins per file).
9
+ *
10
+ * Identity is the test FILE (stable across describe restructuring; git tracks
11
+ * renames). Each decision stores two freshness stamps:
12
+ * - hash: a normalised content hash of the test file at decision time.
13
+ * Differs → the test changed since you judged it → re-review.
14
+ * - signal: the subsumption snapshot at decision time. Drifts (e.g. a test
15
+ * elsewhere was deleted, so this file's kills became unique) → the
16
+ * matrix around it changed → re-review.
17
+ *
18
+ * A decision is trusted only while BOTH still hold. That's what stops a "keep"
19
+ * verdict from silently outliving the reason it was made.
20
+ *
21
+ * polly mutate decisions # status (the join)
22
+ * polly mutate decisions decide <file> <verdict> "<rationale>"
23
+ *
24
+ * verdicts: keep | prune | rewrite | investigate
25
+ */
26
+ import type { Database } from "bun:sqlite";
27
+ export interface FileSignal {
28
+ kills: number;
29
+ uniqueKills: number;
30
+ subsumed: boolean;
31
+ }
32
+ export declare function fileSignals(db: Database): Map<string, FileSignal>;
33
+ export declare function status(db: Database, logPath?: string): Promise<string>;
34
+ export declare function decide(file: string, verdict: string, rationale: string, db: Database, logPath?: string): Promise<void>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @fairfox/polly/mutate — programmatic surface for mutation-testing analysis.
3
+ *
4
+ * The CLI (`polly mutate`) is the primary entry point; this barrel exposes the
5
+ * reusable pieces for consumers who want to drive the analysis themselves:
6
+ * ingest a mutation report into the kill matrix, derive the useless-test signals,
7
+ * and assert the matrix contract.
8
+ */
9
+ export { decide, type FileSignal, fileSignals, status } from "./decisions.ts";
10
+ export { buildDb, type MutationReport } from "./ingest.ts";
11
+ export { type InitResult, initConfig } from "./init.ts";
12
+ export { report } from "./report.ts";
13
+ export { isMatrixComplete, type MatrixCheck, verifyMatrix } from "./verify-matrix.ts";