@sackville-mcp/flake 0.0.1-alpha.2 → 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 +2 -8
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/dist/index.d.mts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SpawnedRunner } from "@sackville-mcp/spawn";
|
|
1
2
|
import Database from "better-sqlite3";
|
|
2
3
|
|
|
3
4
|
//#region src/classify.d.ts
|
|
@@ -283,14 +284,7 @@ interface RunAndRecordInput {
|
|
|
283
284
|
runGroup?: string;
|
|
284
285
|
}
|
|
285
286
|
/** Injected command runner — executes `vitest <argv>` and yields its exit status. */
|
|
286
|
-
type TestRunner =
|
|
287
|
-
cwd: string;
|
|
288
|
-
timeoutMs?: number;
|
|
289
|
-
}) => Promise<{
|
|
290
|
-
exitCode: number;
|
|
291
|
-
stdout: string;
|
|
292
|
-
stderr: string;
|
|
293
|
-
}>;
|
|
287
|
+
type TestRunner = SpawnedRunner;
|
|
294
288
|
interface RunAndRecordResult {
|
|
295
289
|
/** False only when repeat <= 0 (the runner was never invoked). */
|
|
296
290
|
ran: boolean;
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/classify.ts","../src/store.ts","../src/report.ts","../src/pytest.ts","../src/quarantine.ts","../src/runner.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/classify.ts","../src/store.ts","../src/report.ts","../src/pytest.ts","../src/quarantine.ts","../src/runner.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
2
|
-
import { execFile } from "node:child_process";
|
|
3
2
|
import { mkdtempSync, readFileSync } from "node:fs";
|
|
4
3
|
import { tmpdir } from "node:os";
|
|
4
|
+
import { spawnRunner } from "@sackville-mcp/spawn";
|
|
5
5
|
import Database from "better-sqlite3";
|
|
6
6
|
//#region src/classify.ts
|
|
7
7
|
const DEFAULT_Z = 1.96;
|
|
@@ -335,22 +335,6 @@ function pytestArgv(files, outFile) {
|
|
|
335
335
|
...files
|
|
336
336
|
];
|
|
337
337
|
}
|
|
338
|
-
/** Spawn a local command as a subprocess, surfacing its exit code (never rejecting on non-zero). */
|
|
339
|
-
function spawnRunner(command) {
|
|
340
|
-
return (argv, opts) => new Promise((res) => {
|
|
341
|
-
execFile(command, argv, {
|
|
342
|
-
cwd: opts.cwd,
|
|
343
|
-
timeout: opts.timeoutMs,
|
|
344
|
-
maxBuffer: 64 * 1024 * 1024
|
|
345
|
-
}, (err, stdout, stderr) => {
|
|
346
|
-
res({
|
|
347
|
-
exitCode: err && typeof err.code === "number" ? err.code : err ? 1 : 0,
|
|
348
|
-
stdout: String(stdout),
|
|
349
|
-
stderr: String(stderr)
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
338
|
/** Default live runner: spawn the local `vitest` as a subprocess (used by the bin, not the gate). */
|
|
355
339
|
const defaultVitestRunner = spawnRunner("vitest");
|
|
356
340
|
/** Default live runner: spawn the local `pytest` as a subprocess (used by the bin, not the gate). */
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["outcome","migrate"],"sources":["../src/classify.ts","../src/pytest.ts","../src/quarantine.ts","../src/report.ts","../src/runner.ts","../src/store.ts"],"sourcesContent":["/**\n * Pure flakiness classifier — the first slice of `@sackville-mcp/flake`, and the only one\n * that touches no I/O. Given each test's run history (an ordered list of pass/fail\n * outcomes), it labels the test and quantifies *how* flaky it is with a binomial\n * confidence bound, so a later operator-gated quarantine slice has a defensible,\n * sample-size-aware number to threshold on rather than a raw \"it failed once\" reflex.\n *\n * Why Wilson and not the naive p̂ = failures/runs:\n * - The naive rate is wildly overconfident on small samples (1 failure in 2 runs reads\n * as a 50% failure rate; 1 in 100 reads as 1%, but with no sense of how trustworthy\n * either is). The **Wilson score interval** for a binomial proportion gives an\n * asymmetric, always-in-[0,1] confidence interval that stays sane at small n and at\n * the p̂=0 / p̂=1 boundaries (where the normal-approximation Wald interval collapses to\n * a useless zero-width point). We expose its lower bound as `flakeScore`: the\n * conservative \"we're confident the test fails at least this often\" magnitude — a test\n * that failed 1/100 from infra noise scores far below one failing 30/100, even though a\n * naive \"has failed\" flag treats them alike.\n *\n * Classification policy (deliberately conservative toward *catching* flakes, but\n * cautious about *condemning* a test as reliable/broken on thin evidence):\n * - A history with **both** a pass and a failure is `flaky` at any run count — observed\n * inconsistency is the definition of flaky; one mixed pair is enough to flag it.\n * - An all-pass or all-fail history is only trusted as `reliable` / `broken` once it\n * clears `minRuns`; below that it is `insufficient-data` (a brand-new all-pass test may\n * simply not have hit its flake yet; a single failure may be a one-off).\n * - An empty history is `insufficient-data`.\n */\n\n/** A single recorded execution of a test. */\nexport interface TestRun {\n passed: boolean\n /**\n * ISO timestamp of the run. Carried through from the (future) history store for later\n * time-windowing slices; the pure classifier reads only `passed`.\n */\n at?: string\n}\n\nexport interface TestHistory {\n /** Stable test identifier, e.g. `<file> > <test name>`. */\n id: string\n /** Runs in any order — the classifier only counts pass/fail, never their sequence. */\n runs: TestRun[]\n}\n\nexport type FlakeState = 'flaky' | 'reliable' | 'broken' | 'insufficient-data'\n\n/** A Wilson score interval, clamped to [0, 1]. */\nexport interface WilsonInterval {\n lower: number\n center: number\n upper: number\n}\n\nexport interface FlakeVerdict {\n id: string\n state: FlakeState\n runs: number\n passes: number\n failures: number\n /** Observed failure rate failures/runs (0 when there are no runs). */\n failureRate: number\n /** Wilson score interval for the true failure rate at the configured confidence. */\n wilson: WilsonInterval\n /**\n * Conservative flakiness magnitude = the Wilson lower bound of the failure rate. The\n * number a quarantine policy thresholds on: high only when the test fails often AND we\n * have enough runs to be confident. 0 for reliable / empty histories.\n */\n flakeScore: number\n}\n\nexport interface ClassifyOptions {\n /** z-score for the Wilson interval; default 1.96 (two-sided 95%). */\n z?: number\n /**\n * Minimum runs before an all-pass / all-fail history is trusted as `reliable` /\n * `broken`. Below it (with no observed inconsistency) the verdict is\n * `insufficient-data`. A *mixed* history is `flaky` at any run count. Default 5.\n */\n minRuns?: number\n}\n\nconst DEFAULT_Z = 1.96\nconst DEFAULT_MIN_RUNS = 5\n\n/**\n * The Wilson score interval for `failures` successes in `runs` Bernoulli trials at\n * confidence `z`. Bounds are clamped to [0, 1]. Zero runs yields a degenerate zero\n * interval (the rate is undefined; the caller marks it insufficient-data).\n */\nexport function wilsonInterval(failures: number, runs: number, z = DEFAULT_Z): WilsonInterval {\n if (runs <= 0) return { lower: 0, center: 0, upper: 0 }\n const n = runs\n const p = failures / n\n const z2 = z * z\n const denom = 1 + z2 / n\n const center = (p + z2 / (2 * n)) / denom\n const margin = (z / denom) * Math.sqrt((p * (1 - p)) / n + z2 / (4 * n * n))\n return {\n lower: Math.max(0, center - margin),\n center,\n upper: Math.min(1, center + margin),\n }\n}\n\n/** Classify a single test's run history into a {@link FlakeVerdict}. */\nexport function classifyHistory(history: TestHistory, opts: ClassifyOptions = {}): FlakeVerdict {\n const z = opts.z ?? DEFAULT_Z\n const minRuns = opts.minRuns ?? DEFAULT_MIN_RUNS\n\n const runs = history.runs.length\n const passes = history.runs.reduce((n, r) => n + (r.passed ? 1 : 0), 0)\n const failures = runs - passes\n const failureRate = runs > 0 ? failures / runs : 0\n const wilson = wilsonInterval(failures, runs, z)\n\n let state: FlakeState\n if (runs === 0) {\n state = 'insufficient-data'\n } else if (passes > 0 && failures > 0) {\n state = 'flaky'\n } else if (runs < minRuns) {\n // All-pass or all-fail, but too few runs to trust the verdict.\n state = 'insufficient-data'\n } else if (failures === 0) {\n state = 'reliable'\n } else {\n state = 'broken'\n }\n\n return {\n id: history.id,\n state,\n runs,\n passes,\n failures,\n failureRate,\n wilson,\n flakeScore: wilson.lower,\n }\n}\n\n/**\n * Classify many histories, preserving input order. Callers rank quarantine candidates by\n * sorting on `flakeScore` (or filtering `state === 'flaky'`).\n */\nexport function classifyHistories(\n histories: TestHistory[],\n opts: ClassifyOptions = {},\n): FlakeVerdict[] {\n return histories.map((h) => classifyHistory(h, opts))\n}\n","/**\n * pytest-json-report ingestion — turns a `pytest --json-report` report into the\n * {@link RecordedRun}s the history store records. The Python sibling of {@link parseVitestJson}.\n *\n * The store / classifier / quarantine are all **test-id-opaque** (they operate on the\n * `testId` string + pass/fail only), so the Python adapter is purely this shape converter —\n * no change to the engine. Like the vitest parser this module is pure: no spawning, no I/O.\n *\n * Two things differ from the vitest report:\n * - **Stable id:** pytest's `nodeid` (`tests/test_x.py::TestC::test_y`) is already\n * file-qualified, rootdir-relative, and stable, so we use it **verbatim** — none of the\n * `ancestorTitles + title` reconstruction the lossy vitest `fullName` forces.\n * - **Durations are seconds, split across phases.** pytest-json-report records a per-phase\n * `{setup, call, teardown}` duration in *seconds*; we sum the present phases and convert\n * to milliseconds to match {@link RecordedRun.durationMs} (and istanbul/vitest's ms unit).\n *\n * Outcome mapping (mirrors the vitest \"pass/fail-signal vs no-signal\" split):\n * - `passed` → recorded as a pass.\n * - `failed` → recorded as a failure.\n * - `error` → recorded as a failure: an errored test (a flaky fixture / setup / teardown)\n * did not pass, and that nondeterminism is exactly what the flake pillar hunts.\n * - `skipped` / `xfailed` / `xpassed` → dropped: no clean pass/fail flake signal (an\n * `xfailed` test behaved as declared; a strict `xpassed` surfaces as `failed`).\n */\n\nimport { isAbsolute, relative } from 'node:path'\nimport type { ParseReportOptions } from './report.js'\nimport type { RecordedRun } from './store.js'\n\n/** One phase (setup/call/teardown) of a pytest test item; `duration` is in seconds. */\nexport interface PytestPhase {\n duration?: number | null\n outcome?: string\n}\n\n/** The subset of a pytest-json-report test item we read. */\nexport interface PytestTest {\n nodeid?: string\n outcome?: string\n setup?: PytestPhase\n call?: PytestPhase\n teardown?: PytestPhase\n}\n\nexport interface PytestJsonReport {\n tests?: PytestTest[]\n}\n\n/** A status that carries a pass/fail signal; everything else returns undefined (dropped). */\nfunction outcome(status: string | undefined): boolean | undefined {\n if (status === 'passed') return true\n if (status === 'failed' || status === 'error') return false\n return undefined\n}\n\n/** Sum the present phase durations (seconds) → milliseconds, or undefined when none exist. */\nfunction durationMs(t: PytestTest): number | undefined {\n let seconds = 0\n let seen = false\n for (const phase of [t.setup, t.call, t.teardown]) {\n if (typeof phase?.duration === 'number') {\n seconds += phase.duration\n seen = true\n }\n }\n // Round to microsecond precision (in ms) to shed float-sum artifacts.\n return seen ? Math.round(seconds * 1_000_000) / 1000 : undefined\n}\n\n/**\n * Make a nodeid machine-stable. The nodeid is `<file>::<test path>`; pytest already emits\n * `<file>` rootdir-relative, so normally we pass it through. Only when a `projectRoot` is given\n * AND the file part is absolute do we relativize *just that part*, preserving the `::` structure\n * (a blind `relative()` over the whole string would mangle the `::`-delimited test path).\n */\nfunction stableId(nodeid: string, projectRoot?: string): string {\n if (projectRoot === undefined) return nodeid\n const sep = nodeid.indexOf('::')\n const file = sep === -1 ? nodeid : nodeid.slice(0, sep)\n if (!isAbsolute(file)) return nodeid\n const rel = relative(projectRoot, file)\n return sep === -1 ? rel : rel + nodeid.slice(sep)\n}\n\n/**\n * Parse a pytest-json-report report into recorded runs — one per pass/fail/error test.\n * Skipped / xfailed / xpassed tests are dropped (no pass/fail signal). Pure: no spawning, no I/O.\n */\nexport function parsePytestJson(report: PytestJsonReport, opts: ParseReportOptions): RecordedRun[] {\n const runs: RecordedRun[] = []\n for (const t of report.tests ?? []) {\n const passed = outcome(t.outcome)\n if (passed === undefined) continue\n const run: RecordedRun = {\n testId: stableId(t.nodeid ?? '<unknown>', opts.projectRoot),\n passed,\n at: opts.at,\n }\n const ms = durationMs(t)\n if (ms !== undefined) run.durationMs = ms\n if (opts.runGroup !== undefined) run.runGroup = opts.runGroup\n runs.push(run)\n }\n return runs\n}\n","/**\n * Quarantine — the only flake surface that **writes**, and therefore the one with teeth.\n *\n * Quarantining a test tells the gate to tolerate its failure for a bounded window. That\n * is exactly the capability an agent could abuse to turn a red suite green, so per ADR\n * 0010 it sits behind the house **paired deny-by-default operator gate**, adapted to this\n * surface: the pair is `allowQuarantine` (the boolean) + `maxExpiryMs` (the load-bearing\n * bound — a zero/absent cap denies every write even when the boolean is set, and an\n * **expiry is mandatory** so a quarantine can never be permanent). Both are operator-set;\n * no caller input can self-authorize, lengthen past the cap (we fail loud rather than\n * silently clamp), or make a quarantine open-ended.\n *\n * Reads (`isQuarantined`/`active`/`all`) and `release` are ungated: an expired quarantine\n * is automatically inactive, and releasing a test only ever makes the gate stricter.\n */\n\nimport type Database from 'better-sqlite3'\nimport type { FlakeVerdict } from './classify.js'\nimport type { HistoryStore } from './store.js'\n\n/** Thrown when the paired operator gate denies a quarantine write. */\nexport class QuarantineGateError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'QuarantineGateError'\n }\n}\n\n/** Operator-set quarantine policy (the paired gate). */\nexport interface QuarantinePolicy {\n /** OPERATOR opt-in to allow quarantine writes. Deny-by-default. */\n allowQuarantine: boolean\n /**\n * OPERATOR cap on quarantine duration (ms from `quarantinedAt`). Load-bearing: a\n * zero/non-positive cap denies every write even with `allowQuarantine`, and a request\n * whose expiry exceeds it is refused (never silently clamped).\n */\n maxExpiryMs: number\n}\n\nexport interface QuarantineRequest {\n testId: string\n /** Why it is quarantined — mandatory, non-empty (audit trail). */\n reason: string\n /** ISO expiry; mandatory, must be in the future and within `maxExpiryMs` of `now`. */\n expiresAt: string\n /** The flakeScore that justified it (for audit/ranking). */\n flakeScore?: number\n /** Reference time; defaults to now. */\n now?: string\n}\n\nexport interface QuarantineEntry {\n testId: string\n reason: string\n flakeScore: number | null\n quarantinedAt: string\n expiresAt: string\n}\n\ninterface QRow {\n test_id: string\n reason: string\n flake_score: number | null\n quarantined_at: string\n expires_at: string\n}\n\nfunction migrate(db: Database.Database): void {\n db.exec(`\n CREATE TABLE IF NOT EXISTS quarantine (\n test_id TEXT PRIMARY KEY,\n reason TEXT NOT NULL,\n flake_score REAL,\n quarantined_at TEXT NOT NULL,\n expires_at TEXT NOT NULL\n );\n `)\n}\n\nfunction toEntry(r: QRow): QuarantineEntry {\n return {\n testId: r.test_id,\n reason: r.reason,\n flakeScore: r.flake_score,\n quarantinedAt: r.quarantined_at,\n expiresAt: r.expires_at,\n }\n}\n\nexport class Quarantine {\n private readonly db: Database.Database\n private readonly policy: QuarantinePolicy\n\n constructor(store: HistoryStore | Database.Database, policy: QuarantinePolicy) {\n this.db = 'database' in store ? store.database : store\n this.policy = policy\n migrate(this.db)\n }\n\n /** Quarantine a test for a bounded window. Throws {@link QuarantineGateError} on denial. */\n quarantine(req: QuarantineRequest): QuarantineEntry {\n if (!this.policy.allowQuarantine) {\n throw new QuarantineGateError(\n 'quarantine writes are not enabled (the operator must set allowQuarantine)',\n )\n }\n if (!(this.policy.maxExpiryMs > 0)) {\n throw new QuarantineGateError(\n 'no quarantine expiry bound is configured (operator maxExpiryMs must be > 0)',\n )\n }\n const reason = req.reason.trim()\n if (reason === '') {\n throw new QuarantineGateError('a non-empty quarantine reason is required')\n }\n const now = req.now ?? new Date().toISOString()\n const nowMs = Date.parse(now)\n const expiryMs = Date.parse(req.expiresAt)\n if (Number.isNaN(expiryMs)) {\n throw new QuarantineGateError(`unparseable expiresAt: ${req.expiresAt}`)\n }\n if (expiryMs <= nowMs) {\n throw new QuarantineGateError('expiresAt must be in the future')\n }\n if (expiryMs - nowMs > this.policy.maxExpiryMs) {\n throw new QuarantineGateError(\n `expiry exceeds the operator cap of ${this.policy.maxExpiryMs}ms`,\n )\n }\n this.db\n .prepare(\n `INSERT INTO quarantine (test_id, reason, flake_score, quarantined_at, expires_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(test_id) DO UPDATE SET\n reason = excluded.reason,\n flake_score = excluded.flake_score,\n quarantined_at = excluded.quarantined_at,\n expires_at = excluded.expires_at`,\n )\n .run(req.testId, reason, req.flakeScore ?? null, now, req.expiresAt)\n return {\n testId: req.testId,\n reason,\n flakeScore: req.flakeScore ?? null,\n quarantinedAt: now,\n expiresAt: req.expiresAt,\n }\n }\n\n /** Lift a quarantine. Ungated. Returns true if a row was removed. */\n release(testId: string): boolean {\n const info = this.db.prepare('DELETE FROM quarantine WHERE test_id = ?').run(testId)\n return info.changes > 0\n }\n\n /** Whether a test is currently quarantined (expiry-aware). */\n isQuarantined(testId: string, now: string = new Date().toISOString()): boolean {\n const row = this.db\n .prepare('SELECT expires_at FROM quarantine WHERE test_id = ?')\n .get(testId) as { expires_at: string } | undefined\n return row !== undefined && Date.parse(row.expires_at) > Date.parse(now)\n }\n\n /** Currently-active (unexpired) quarantines, ordered by expiry. */\n active(now: string = new Date().toISOString()): QuarantineEntry[] {\n const rows = this.db\n .prepare('SELECT * FROM quarantine WHERE expires_at > ? ORDER BY expires_at, test_id')\n .all(now) as QRow[]\n return rows.map(toEntry)\n }\n\n /** Every quarantine row, including expired ones (audit). */\n all(): QuarantineEntry[] {\n const rows = this.db.prepare('SELECT * FROM quarantine ORDER BY test_id').all() as QRow[]\n return rows.map(toEntry)\n }\n}\n\nexport interface CandidateOptions {\n /** Only verdicts with `flakeScore >= minFlakeScore` (default 0 — every flaky test). */\n minFlakeScore?: number\n}\n\n/**\n * Pure helper: rank quarantine candidates from classifier verdicts — `flaky` tests whose\n * `flakeScore` clears the floor, highest first. Never selects `broken` (a real, consistent\n * failure to FIX, not hide) or `reliable`/`insufficient-data`. The write itself is still\n * gated; this only proposes.\n */\nexport function quarantineCandidates(\n verdicts: FlakeVerdict[],\n opts: CandidateOptions = {},\n): FlakeVerdict[] {\n const floor = opts.minFlakeScore ?? 0\n return verdicts\n .filter((v) => v.state === 'flaky' && v.flakeScore >= floor)\n .sort((a, b) => b.flakeScore - a.flakeScore)\n}\n","/**\n * Vitest JSON-report ingestion — turns a `vitest run --reporter=json` report into the\n * {@link RecordedRun}s the history store records.\n *\n * Per ADR 0010 the flake pillar **spawns** `vitest run --reporter=json` and parses its\n * output (a different execution model from coverage's in-process/child-process run and\n * mutation's Stryker delegation — there is no shared runner seam). This module is the\n * pure parser half: no spawning, no I/O — it just maps the report's shape to runs, so it\n * is unit-tested against a committed real-shaped fixture. The gated spawn that produces\n * the report lives in a later slice.\n *\n * The report's `fullName` is the ancestor titles + title joined by a single space, which\n * is lossy (a describe/test boundary is indistinguishable from a space inside a title).\n * We therefore build a stable, file-qualified id from `ancestorTitles + title` joined by\n * ` > ` ourselves, falling back to `fullName`, then `title`, when those are absent.\n */\n\nimport { relative } from 'node:path'\nimport type { RecordedRun } from './store.js'\n\n/** The subset of a vitest json assertion result we read. */\nexport interface VitestAssertion {\n ancestorTitles?: string[]\n title?: string\n fullName?: string\n status?: string\n duration?: number | null\n}\n\nexport interface VitestFileResult {\n /** Test file path (absolute as vitest emits it). */\n name?: string\n assertionResults?: VitestAssertion[]\n}\n\nexport interface VitestJsonReport {\n testResults?: VitestFileResult[]\n}\n\nexport interface ParseReportOptions {\n /** ISO timestamp stamped on every parsed run. */\n at: string\n /** When set, file paths are made relative to it for stable, machine-independent ids. */\n projectRoot?: string\n /** Optional id grouping all runs from this report (a CI run / batch). */\n runGroup?: string\n}\n\n/** A status that carries a pass/fail signal. Skipped/pending/todo do not. */\nfunction outcome(status: string | undefined): boolean | undefined {\n if (status === 'passed') return true\n if (status === 'failed') return false\n return undefined\n}\n\nfunction titlePart(a: VitestAssertion): string {\n if (a.ancestorTitles?.length) return [...a.ancestorTitles, a.title ?? ''].join(' > ')\n return a.fullName ?? a.title ?? '<unknown>'\n}\n\nfunction fileLabel(name: string | undefined, projectRoot?: string): string {\n if (!name) return ''\n return projectRoot ? relative(projectRoot, name) : name\n}\n\n/**\n * Parse a vitest json report into recorded runs — one per pass/fail assertion. Skipped /\n * pending / todo assertions are dropped (no pass/fail signal). Pure: no spawning, no I/O.\n */\nexport function parseVitestJson(report: VitestJsonReport, opts: ParseReportOptions): RecordedRun[] {\n const runs: RecordedRun[] = []\n for (const file of report.testResults ?? []) {\n const label = fileLabel(file.name, opts.projectRoot)\n for (const a of file.assertionResults ?? []) {\n const passed = outcome(a.status)\n if (passed === undefined) continue\n const id = label ? `${label} > ${titlePart(a)}` : titlePart(a)\n const run: RecordedRun = { testId: id, passed, at: opts.at }\n if (typeof a.duration === 'number') run.durationMs = a.duration\n if (opts.runGroup !== undefined) run.runGroup = opts.runGroup\n runs.push(run)\n }\n }\n return runs\n}\n","/**\n * The gated vitest runner — the live half of the flake pillar. It **spawns**\n * `vitest run --reporter=json` (per ADR 0010: flake's execution model is spawn-and-parse,\n * distinct from coverage's child-process coverage run and mutation's Stryker delegation),\n * reads the JSON report, and records every outcome into the {@link HistoryStore}. Run the\n * suite `repeat` times to actually surface flakiness, then classify.\n *\n * Two ADR-0010 constraints, mirroring `@sackville-mcp/coverage`'s `runScoped`:\n * 1. **It runs code**, so it is behind a *paired* deny-by-default operator gate — an\n * `allowRun` boolean AND an `allowedRoots` allowlist (load-bearing on its own), with a\n * wall-clock cap. All operator-set; no caller input self-authorizes.\n * 2. **Child-process boundary.** The real `vitest` invocation is an injected\n * {@link TestRunner} (the bin wires a subprocess); the engine owns the gate, argv,\n * report-file plumbing, ingestion, and classification, and is unit-tested with a fake\n * runner — no real spawn in the green gate.\n */\n\nimport { execFile } from 'node:child_process'\nimport { mkdtempSync, readFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport type { FlakeVerdict } from './classify.js'\nimport type { PytestJsonReport } from './pytest.js'\nimport type { ParseReportOptions, VitestJsonReport } from './report.js'\nimport type { HistoryStore } from './store.js'\n\n/** Thrown when the paired operator gate denies a run. */\nexport class FlakeGateError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'FlakeGateError'\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 RunHistoryConfig {\n /** The project to run tests 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 tests. Deny-by-default. */\n allowRun: boolean\n /** Wall-clock cap (ms) per iteration, passed to the runner. */\n timeoutMs?: number\n}\n\nexport interface RunAndRecordInput {\n /** How many times to run the suite — flakiness needs repeats. Default 1. */\n repeat?: number\n /** Positional vitest file filters; default runs the whole suite. */\n files?: string[]\n /** Batch id; each iteration is recorded under `${runGroup}#<i>`. */\n runGroup?: string\n}\n\n/** Injected command runner — executes `vitest <argv>` and yields its exit status. */\nexport type TestRunner = (\n argv: string[],\n opts: { cwd: string; timeoutMs?: number },\n) => Promise<{ exitCode: number; stdout: string; stderr: string }>\n\nexport interface RunAndRecordResult {\n /** False only when repeat <= 0 (the runner was never invoked). */\n ran: boolean\n iterations: number\n /** Total runs recorded across all iterations. */\n recorded: number\n results: { exitCode: number; passed: boolean }[]\n /** Classifier verdicts over the store AFTER recording this batch. */\n verdicts: FlakeVerdict[]\n}\n\n/** Build the argv for one vitest suite run with the JSON reporter writing to `outFile`. */\nfunction vitestArgv(files: string[], outFile: string): string[] {\n return ['run', ...files, '--reporter=json', `--outputFile=${outFile}`]\n}\n\n/**\n * Build the argv for one pytest run with the `pytest-json-report` plugin writing to `outFile`\n * (ADR 0010 addendum: json-report now, `pytest-reportlog` staged). No `run` subcommand — pytest\n * takes positional file filters directly. The plugin is an operator-installed dev dependency.\n */\nfunction pytestArgv(files: string[], outFile: string): string[] {\n return ['--json-report', `--json-report-file=${outFile}`, ...files]\n}\n\n/** Spawn a local command as a subprocess, surfacing its exit code (never rejecting on non-zero). */\nfunction spawnRunner(command: string): TestRunner {\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 // The tool exits non-zero on a test failure — surface the code, don't reject.\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 `vitest` as a subprocess (used by the bin, not the gate). */\nexport const defaultVitestRunner: TestRunner = spawnRunner('vitest')\n\n/** Default live runner: spawn the local `pytest` as a subprocess (used by the bin, not the gate). */\nexport const defaultPytestRunner: TestRunner = spawnRunner('pytest')\n\n/**\n * A test framework's runner specifics: the default subprocess runner, how to build its argv with\n * a per-iteration report file, and how to ingest the parsed report into the store. Everything else\n * (gate, repeat loop, report-file plumbing, classification) is framework-agnostic.\n */\ninterface FrameworkAdapter {\n defaultRunner: TestRunner\n buildArgv(files: string[], outFile: string): string[]\n ingest(store: HistoryStore, parsed: unknown, opts: ParseReportOptions): number\n}\n\nconst VITEST: FrameworkAdapter = {\n defaultRunner: defaultVitestRunner,\n buildArgv: vitestArgv,\n ingest: (store, parsed, opts) => store.ingestReport(parsed as VitestJsonReport, opts),\n}\n\nconst PYTEST: FrameworkAdapter = {\n defaultRunner: defaultPytestRunner,\n buildArgv: pytestArgv,\n ingest: (store, parsed, opts) => store.ingestPytestReport(parsed as PytestJsonReport, opts),\n}\n\nfunction assertAllowed(config: RunHistoryConfig): void {\n if (!config.allowRun) {\n throw new FlakeGateError('test execution is 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 FlakeGateError(`project root ${config.projectRoot} is not in the operator allowlist`)\n }\n}\n\n/**\n * Run a test suite `repeat` times behind the operator gate via the given {@link FrameworkAdapter},\n * recording every outcome into the store, then classify. The actual invocation is the injected\n * `runner` (default = the adapter's subprocess runner); no real spawn in the green gate.\n */\nasync function runAndRecordWith(\n fw: FrameworkAdapter,\n store: HistoryStore,\n config: RunHistoryConfig,\n input: RunAndRecordInput,\n deps: { runner?: TestRunner; reportDir?: string },\n): Promise<RunAndRecordResult> {\n assertAllowed(config)\n\n const repeat = input.repeat ?? 1\n if (repeat <= 0) {\n return { ran: false, iterations: 0, recorded: 0, results: [], verdicts: store.classify() }\n }\n\n const runner = deps.runner ?? fw.defaultRunner\n const reportDir = deps.reportDir ?? mkdtempSync(join(tmpdir(), 'sackville-flake-'))\n const files = input.files ?? []\n const results: { exitCode: number; passed: boolean }[] = []\n let recorded = 0\n\n for (let i = 0; i < repeat; i++) {\n const outFile = join(reportDir, `report-${i}.json`)\n const { exitCode } = await runner(fw.buildArgv(files, outFile), {\n cwd: config.projectRoot,\n timeoutMs: config.timeoutMs,\n })\n let parsed: unknown\n try {\n parsed = JSON.parse(readFileSync(outFile, 'utf8'))\n } catch {\n throw new Error(\n `flake run did not produce a JSON report at ${outFile} (exit code ${exitCode})`,\n )\n }\n recorded += fw.ingest(store, parsed, {\n at: new Date().toISOString(),\n projectRoot: config.projectRoot,\n runGroup: input.runGroup !== undefined ? `${input.runGroup}#${i}` : undefined,\n })\n results.push({ exitCode, passed: exitCode === 0 })\n }\n\n return { ran: true, iterations: repeat, recorded, results, verdicts: store.classify() }\n}\n\n/**\n * Run the vitest suite `repeat` times behind the operator gate, recording every outcome into the\n * store, then classify. The actual `vitest` invocation is the injected `runner` (default\n * {@link defaultVitestRunner}).\n */\nexport async function runAndRecord(\n store: HistoryStore,\n config: RunHistoryConfig,\n input: RunAndRecordInput,\n deps: { runner?: TestRunner; reportDir?: string } = {},\n): Promise<RunAndRecordResult> {\n return runAndRecordWith(VITEST, store, config, input, deps)\n}\n\n/**\n * The pytest sibling of {@link runAndRecord} (ADR 0010 addendum): spawn `pytest --json-report`\n * `repeat` times, ingest via the existing `parsePytestJson` (unchanged), classify. Repeats re-run\n * the WHOLE suite — never `pytest-repeat`, whose `[i-N]` nodeid suffix would fragment the\n * one-history-per-nodeid invariant the classifier relies on.\n */\nexport async function runAndRecordPytest(\n store: HistoryStore,\n config: RunHistoryConfig,\n input: RunAndRecordInput,\n deps: { runner?: TestRunner; reportDir?: string } = {},\n): Promise<RunAndRecordResult> {\n return runAndRecordWith(PYTEST, store, config, input, deps)\n}\n","/**\n * The private run-history store — `@sackville-mcp/flake`'s own SQLite database.\n *\n * Per ADR 0010 this is a **second SQLite owner**, deliberately OUTSIDE the docs-pillar\n * \"only `@sackville-mcp/core` touches SQLite\" invariant: it is a new, private store for test\n * run outcomes, not a crossing of the Python↔TS polyglot contract (which remains the\n * `schema/sackville.schema.sql` index that `core` reads). It records each test's pass/fail\n * history over time and reads it back as the `TestHistory[]` the pure classifier consumes.\n *\n * The schema is intentionally tiny: one append-only `test_run` row per recorded outcome,\n * plus a `flake_meta` version marker. Quarantine state is a separate table added by the\n * quarantine slice.\n */\n\nimport Database from 'better-sqlite3'\nimport {\n type ClassifyOptions,\n classifyHistories,\n type FlakeVerdict,\n type TestHistory,\n type TestRun,\n} from './classify.js'\nimport { type PytestJsonReport, parsePytestJson } from './pytest.js'\nimport { type ParseReportOptions, parseVitestJson, type VitestJsonReport } from './report.js'\n\nconst SCHEMA_VERSION = 1\n\n/** A test outcome to record. */\nexport interface RecordedRun {\n testId: string\n passed: boolean\n /** ISO timestamp; defaults to now. */\n at?: string\n /** Optional wall-clock duration of the run. */\n durationMs?: number\n /** Optional id grouping all tests from one suite execution (a CI run / batch). */\n runGroup?: string\n}\n\nexport interface HistoryQueryOptions {\n /** Keep only the most recent N runs per test (chronological tail). */\n limitPerTest?: number\n /** Only include runs at/after this ISO timestamp. */\n since?: string\n}\n\ninterface RunRow {\n test_id: string\n passed: number\n at: string\n}\n\nfunction migrate(db: Database.Database): void {\n db.pragma('journal_mode = WAL')\n db.exec(`\n CREATE TABLE IF NOT EXISTS flake_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);\n CREATE TABLE IF NOT EXISTS test_run (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n test_id TEXT NOT NULL,\n passed INTEGER NOT NULL,\n at TEXT NOT NULL,\n duration_ms REAL,\n run_group TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_test_run_test_id_at ON test_run(test_id, at, id);\n `)\n const row = db.prepare('SELECT value FROM flake_meta WHERE key = ?').get('schema_version') as\n | { value: string }\n | undefined\n if (!row) {\n db.prepare('INSERT INTO flake_meta (key, value) VALUES (?, ?)').run(\n 'schema_version',\n String(SCHEMA_VERSION),\n )\n }\n}\n\nexport class HistoryStore {\n private readonly db: Database.Database\n private readonly insert: Database.Statement\n\n constructor(db: Database.Database) {\n this.db = db\n migrate(db)\n this.insert = db.prepare(\n 'INSERT INTO test_run (test_id, passed, at, duration_ms, run_group) VALUES (?, ?, ?, ?, ?)',\n )\n }\n\n /** Open (creating if needed) a file-backed history store and run migrations. */\n static open(path: string): HistoryStore {\n return new HistoryStore(new Database(path))\n }\n\n /** An in-memory store (tests, ephemeral analysis). */\n static memory(): HistoryStore {\n return new HistoryStore(new Database(':memory:'))\n }\n\n /** The underlying database — shared with sibling tables (e.g. quarantine). */\n get database(): Database.Database {\n return this.db\n }\n\n recordRun(run: RecordedRun): void {\n this.insert.run(\n run.testId,\n run.passed ? 1 : 0,\n run.at ?? new Date().toISOString(),\n run.durationMs ?? null,\n run.runGroup ?? null,\n )\n }\n\n /** Record many runs in a single transaction. */\n recordRuns(runs: RecordedRun[]): void {\n const tx = this.db.transaction((batch: RecordedRun[]) => {\n for (const r of batch) this.recordRun(r)\n })\n tx(runs)\n }\n\n private rows(opts: HistoryQueryOptions): RunRow[] {\n const params: string[] = []\n let sql = 'SELECT test_id, passed, at FROM test_run'\n if (opts.since !== undefined) {\n sql += ' WHERE at >= ?'\n params.push(opts.since)\n }\n sql += ' ORDER BY test_id, at, id'\n return this.db.prepare(sql).all(...params) as RunRow[]\n }\n\n private static toRun(r: RunRow): TestRun {\n return { passed: r.passed !== 0, at: r.at }\n }\n\n /** All histories, grouped per test and sorted by test id. */\n histories(opts: HistoryQueryOptions = {}): TestHistory[] {\n const map = new Map<string, TestRun[]>()\n for (const r of this.rows(opts)) {\n const list = map.get(r.test_id) ?? []\n list.push(HistoryStore.toRun(r))\n map.set(r.test_id, list)\n }\n const limit = opts.limitPerTest\n return [...map.entries()]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([id, runs]) => ({ id, runs: limit !== undefined ? runs.slice(-limit) : runs }))\n }\n\n /** One test's history (empty `runs` when never recorded). */\n history(testId: string, opts: HistoryQueryOptions = {}): TestHistory {\n const params: string[] = [testId]\n let sql = 'SELECT test_id, passed, at FROM test_run WHERE test_id = ?'\n if (opts.since !== undefined) {\n sql += ' AND at >= ?'\n params.push(opts.since)\n }\n sql += ' ORDER BY at, id'\n let runs = (this.db.prepare(sql).all(...params) as RunRow[]).map(HistoryStore.toRun)\n if (opts.limitPerTest !== undefined) runs = runs.slice(-opts.limitPerTest)\n return { id: testId, runs }\n }\n\n /**\n * Parse a vitest json report and record every pass/fail assertion as a run. Returns the\n * number of runs recorded (skipped/pending/todo assertions are not counted).\n */\n ingestReport(report: VitestJsonReport, opts: ParseReportOptions): number {\n const runs = parseVitestJson(report, opts)\n this.recordRuns(runs)\n return runs.length\n }\n\n /**\n * Parse a pytest-json-report report and record every pass/fail/error test as a run. Returns\n * the number of runs recorded (skipped/xfailed/xpassed tests are not counted). The Python\n * sibling of {@link ingestReport}.\n */\n ingestPytestReport(report: PytestJsonReport, opts: ParseReportOptions): number {\n const runs = parsePytestJson(report, opts)\n this.recordRuns(runs)\n return runs.length\n }\n\n /** Classify every test's history straight from the store. */\n classify(opts: ClassifyOptions & HistoryQueryOptions = {}): FlakeVerdict[] {\n return classifyHistories(this.histories(opts), opts)\n }\n\n close(): void {\n this.db.close()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["outcome","migrate"],"sources":["../src/classify.ts","../src/pytest.ts","../src/quarantine.ts","../src/report.ts","../src/runner.ts","../src/store.ts"],"sourcesContent":["/**\n * Pure flakiness classifier — the first slice of `@sackville-mcp/flake`, and the only one\n * that touches no I/O. Given each test's run history (an ordered list of pass/fail\n * outcomes), it labels the test and quantifies *how* flaky it is with a binomial\n * confidence bound, so a later operator-gated quarantine slice has a defensible,\n * sample-size-aware number to threshold on rather than a raw \"it failed once\" reflex.\n *\n * Why Wilson and not the naive p̂ = failures/runs:\n * - The naive rate is wildly overconfident on small samples (1 failure in 2 runs reads\n * as a 50% failure rate; 1 in 100 reads as 1%, but with no sense of how trustworthy\n * either is). The **Wilson score interval** for a binomial proportion gives an\n * asymmetric, always-in-[0,1] confidence interval that stays sane at small n and at\n * the p̂=0 / p̂=1 boundaries (where the normal-approximation Wald interval collapses to\n * a useless zero-width point). We expose its lower bound as `flakeScore`: the\n * conservative \"we're confident the test fails at least this often\" magnitude — a test\n * that failed 1/100 from infra noise scores far below one failing 30/100, even though a\n * naive \"has failed\" flag treats them alike.\n *\n * Classification policy (deliberately conservative toward *catching* flakes, but\n * cautious about *condemning* a test as reliable/broken on thin evidence):\n * - A history with **both** a pass and a failure is `flaky` at any run count — observed\n * inconsistency is the definition of flaky; one mixed pair is enough to flag it.\n * - An all-pass or all-fail history is only trusted as `reliable` / `broken` once it\n * clears `minRuns`; below that it is `insufficient-data` (a brand-new all-pass test may\n * simply not have hit its flake yet; a single failure may be a one-off).\n * - An empty history is `insufficient-data`.\n */\n\n/** A single recorded execution of a test. */\nexport interface TestRun {\n passed: boolean\n /**\n * ISO timestamp of the run. Carried through from the (future) history store for later\n * time-windowing slices; the pure classifier reads only `passed`.\n */\n at?: string\n}\n\nexport interface TestHistory {\n /** Stable test identifier, e.g. `<file> > <test name>`. */\n id: string\n /** Runs in any order — the classifier only counts pass/fail, never their sequence. */\n runs: TestRun[]\n}\n\nexport type FlakeState = 'flaky' | 'reliable' | 'broken' | 'insufficient-data'\n\n/** A Wilson score interval, clamped to [0, 1]. */\nexport interface WilsonInterval {\n lower: number\n center: number\n upper: number\n}\n\nexport interface FlakeVerdict {\n id: string\n state: FlakeState\n runs: number\n passes: number\n failures: number\n /** Observed failure rate failures/runs (0 when there are no runs). */\n failureRate: number\n /** Wilson score interval for the true failure rate at the configured confidence. */\n wilson: WilsonInterval\n /**\n * Conservative flakiness magnitude = the Wilson lower bound of the failure rate. The\n * number a quarantine policy thresholds on: high only when the test fails often AND we\n * have enough runs to be confident. 0 for reliable / empty histories.\n */\n flakeScore: number\n}\n\nexport interface ClassifyOptions {\n /** z-score for the Wilson interval; default 1.96 (two-sided 95%). */\n z?: number\n /**\n * Minimum runs before an all-pass / all-fail history is trusted as `reliable` /\n * `broken`. Below it (with no observed inconsistency) the verdict is\n * `insufficient-data`. A *mixed* history is `flaky` at any run count. Default 5.\n */\n minRuns?: number\n}\n\nconst DEFAULT_Z = 1.96\nconst DEFAULT_MIN_RUNS = 5\n\n/**\n * The Wilson score interval for `failures` successes in `runs` Bernoulli trials at\n * confidence `z`. Bounds are clamped to [0, 1]. Zero runs yields a degenerate zero\n * interval (the rate is undefined; the caller marks it insufficient-data).\n */\nexport function wilsonInterval(failures: number, runs: number, z = DEFAULT_Z): WilsonInterval {\n if (runs <= 0) return { lower: 0, center: 0, upper: 0 }\n const n = runs\n const p = failures / n\n const z2 = z * z\n const denom = 1 + z2 / n\n const center = (p + z2 / (2 * n)) / denom\n const margin = (z / denom) * Math.sqrt((p * (1 - p)) / n + z2 / (4 * n * n))\n return {\n lower: Math.max(0, center - margin),\n center,\n upper: Math.min(1, center + margin),\n }\n}\n\n/** Classify a single test's run history into a {@link FlakeVerdict}. */\nexport function classifyHistory(history: TestHistory, opts: ClassifyOptions = {}): FlakeVerdict {\n const z = opts.z ?? DEFAULT_Z\n const minRuns = opts.minRuns ?? DEFAULT_MIN_RUNS\n\n const runs = history.runs.length\n const passes = history.runs.reduce((n, r) => n + (r.passed ? 1 : 0), 0)\n const failures = runs - passes\n const failureRate = runs > 0 ? failures / runs : 0\n const wilson = wilsonInterval(failures, runs, z)\n\n let state: FlakeState\n if (runs === 0) {\n state = 'insufficient-data'\n } else if (passes > 0 && failures > 0) {\n state = 'flaky'\n } else if (runs < minRuns) {\n // All-pass or all-fail, but too few runs to trust the verdict.\n state = 'insufficient-data'\n } else if (failures === 0) {\n state = 'reliable'\n } else {\n state = 'broken'\n }\n\n return {\n id: history.id,\n state,\n runs,\n passes,\n failures,\n failureRate,\n wilson,\n flakeScore: wilson.lower,\n }\n}\n\n/**\n * Classify many histories, preserving input order. Callers rank quarantine candidates by\n * sorting on `flakeScore` (or filtering `state === 'flaky'`).\n */\nexport function classifyHistories(\n histories: TestHistory[],\n opts: ClassifyOptions = {},\n): FlakeVerdict[] {\n return histories.map((h) => classifyHistory(h, opts))\n}\n","/**\n * pytest-json-report ingestion — turns a `pytest --json-report` report into the\n * {@link RecordedRun}s the history store records. The Python sibling of {@link parseVitestJson}.\n *\n * The store / classifier / quarantine are all **test-id-opaque** (they operate on the\n * `testId` string + pass/fail only), so the Python adapter is purely this shape converter —\n * no change to the engine. Like the vitest parser this module is pure: no spawning, no I/O.\n *\n * Two things differ from the vitest report:\n * - **Stable id:** pytest's `nodeid` (`tests/test_x.py::TestC::test_y`) is already\n * file-qualified, rootdir-relative, and stable, so we use it **verbatim** — none of the\n * `ancestorTitles + title` reconstruction the lossy vitest `fullName` forces.\n * - **Durations are seconds, split across phases.** pytest-json-report records a per-phase\n * `{setup, call, teardown}` duration in *seconds*; we sum the present phases and convert\n * to milliseconds to match {@link RecordedRun.durationMs} (and istanbul/vitest's ms unit).\n *\n * Outcome mapping (mirrors the vitest \"pass/fail-signal vs no-signal\" split):\n * - `passed` → recorded as a pass.\n * - `failed` → recorded as a failure.\n * - `error` → recorded as a failure: an errored test (a flaky fixture / setup / teardown)\n * did not pass, and that nondeterminism is exactly what the flake pillar hunts.\n * - `skipped` / `xfailed` / `xpassed` → dropped: no clean pass/fail flake signal (an\n * `xfailed` test behaved as declared; a strict `xpassed` surfaces as `failed`).\n */\n\nimport { isAbsolute, relative } from 'node:path'\nimport type { ParseReportOptions } from './report.js'\nimport type { RecordedRun } from './store.js'\n\n/** One phase (setup/call/teardown) of a pytest test item; `duration` is in seconds. */\nexport interface PytestPhase {\n duration?: number | null\n outcome?: string\n}\n\n/** The subset of a pytest-json-report test item we read. */\nexport interface PytestTest {\n nodeid?: string\n outcome?: string\n setup?: PytestPhase\n call?: PytestPhase\n teardown?: PytestPhase\n}\n\nexport interface PytestJsonReport {\n tests?: PytestTest[]\n}\n\n/** A status that carries a pass/fail signal; everything else returns undefined (dropped). */\nfunction outcome(status: string | undefined): boolean | undefined {\n if (status === 'passed') return true\n if (status === 'failed' || status === 'error') return false\n return undefined\n}\n\n/** Sum the present phase durations (seconds) → milliseconds, or undefined when none exist. */\nfunction durationMs(t: PytestTest): number | undefined {\n let seconds = 0\n let seen = false\n for (const phase of [t.setup, t.call, t.teardown]) {\n if (typeof phase?.duration === 'number') {\n seconds += phase.duration\n seen = true\n }\n }\n // Round to microsecond precision (in ms) to shed float-sum artifacts.\n return seen ? Math.round(seconds * 1_000_000) / 1000 : undefined\n}\n\n/**\n * Make a nodeid machine-stable. The nodeid is `<file>::<test path>`; pytest already emits\n * `<file>` rootdir-relative, so normally we pass it through. Only when a `projectRoot` is given\n * AND the file part is absolute do we relativize *just that part*, preserving the `::` structure\n * (a blind `relative()` over the whole string would mangle the `::`-delimited test path).\n */\nfunction stableId(nodeid: string, projectRoot?: string): string {\n if (projectRoot === undefined) return nodeid\n const sep = nodeid.indexOf('::')\n const file = sep === -1 ? nodeid : nodeid.slice(0, sep)\n if (!isAbsolute(file)) return nodeid\n const rel = relative(projectRoot, file)\n return sep === -1 ? rel : rel + nodeid.slice(sep)\n}\n\n/**\n * Parse a pytest-json-report report into recorded runs — one per pass/fail/error test.\n * Skipped / xfailed / xpassed tests are dropped (no pass/fail signal). Pure: no spawning, no I/O.\n */\nexport function parsePytestJson(report: PytestJsonReport, opts: ParseReportOptions): RecordedRun[] {\n const runs: RecordedRun[] = []\n for (const t of report.tests ?? []) {\n const passed = outcome(t.outcome)\n if (passed === undefined) continue\n const run: RecordedRun = {\n testId: stableId(t.nodeid ?? '<unknown>', opts.projectRoot),\n passed,\n at: opts.at,\n }\n const ms = durationMs(t)\n if (ms !== undefined) run.durationMs = ms\n if (opts.runGroup !== undefined) run.runGroup = opts.runGroup\n runs.push(run)\n }\n return runs\n}\n","/**\n * Quarantine — the only flake surface that **writes**, and therefore the one with teeth.\n *\n * Quarantining a test tells the gate to tolerate its failure for a bounded window. That\n * is exactly the capability an agent could abuse to turn a red suite green, so per ADR\n * 0010 it sits behind the house **paired deny-by-default operator gate**, adapted to this\n * surface: the pair is `allowQuarantine` (the boolean) + `maxExpiryMs` (the load-bearing\n * bound — a zero/absent cap denies every write even when the boolean is set, and an\n * **expiry is mandatory** so a quarantine can never be permanent). Both are operator-set;\n * no caller input can self-authorize, lengthen past the cap (we fail loud rather than\n * silently clamp), or make a quarantine open-ended.\n *\n * Reads (`isQuarantined`/`active`/`all`) and `release` are ungated: an expired quarantine\n * is automatically inactive, and releasing a test only ever makes the gate stricter.\n */\n\nimport type Database from 'better-sqlite3'\nimport type { FlakeVerdict } from './classify.js'\nimport type { HistoryStore } from './store.js'\n\n/** Thrown when the paired operator gate denies a quarantine write. */\nexport class QuarantineGateError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'QuarantineGateError'\n }\n}\n\n/** Operator-set quarantine policy (the paired gate). */\nexport interface QuarantinePolicy {\n /** OPERATOR opt-in to allow quarantine writes. Deny-by-default. */\n allowQuarantine: boolean\n /**\n * OPERATOR cap on quarantine duration (ms from `quarantinedAt`). Load-bearing: a\n * zero/non-positive cap denies every write even with `allowQuarantine`, and a request\n * whose expiry exceeds it is refused (never silently clamped).\n */\n maxExpiryMs: number\n}\n\nexport interface QuarantineRequest {\n testId: string\n /** Why it is quarantined — mandatory, non-empty (audit trail). */\n reason: string\n /** ISO expiry; mandatory, must be in the future and within `maxExpiryMs` of `now`. */\n expiresAt: string\n /** The flakeScore that justified it (for audit/ranking). */\n flakeScore?: number\n /** Reference time; defaults to now. */\n now?: string\n}\n\nexport interface QuarantineEntry {\n testId: string\n reason: string\n flakeScore: number | null\n quarantinedAt: string\n expiresAt: string\n}\n\ninterface QRow {\n test_id: string\n reason: string\n flake_score: number | null\n quarantined_at: string\n expires_at: string\n}\n\nfunction migrate(db: Database.Database): void {\n db.exec(`\n CREATE TABLE IF NOT EXISTS quarantine (\n test_id TEXT PRIMARY KEY,\n reason TEXT NOT NULL,\n flake_score REAL,\n quarantined_at TEXT NOT NULL,\n expires_at TEXT NOT NULL\n );\n `)\n}\n\nfunction toEntry(r: QRow): QuarantineEntry {\n return {\n testId: r.test_id,\n reason: r.reason,\n flakeScore: r.flake_score,\n quarantinedAt: r.quarantined_at,\n expiresAt: r.expires_at,\n }\n}\n\nexport class Quarantine {\n private readonly db: Database.Database\n private readonly policy: QuarantinePolicy\n\n constructor(store: HistoryStore | Database.Database, policy: QuarantinePolicy) {\n this.db = 'database' in store ? store.database : store\n this.policy = policy\n migrate(this.db)\n }\n\n /** Quarantine a test for a bounded window. Throws {@link QuarantineGateError} on denial. */\n quarantine(req: QuarantineRequest): QuarantineEntry {\n if (!this.policy.allowQuarantine) {\n throw new QuarantineGateError(\n 'quarantine writes are not enabled (the operator must set allowQuarantine)',\n )\n }\n if (!(this.policy.maxExpiryMs > 0)) {\n throw new QuarantineGateError(\n 'no quarantine expiry bound is configured (operator maxExpiryMs must be > 0)',\n )\n }\n const reason = req.reason.trim()\n if (reason === '') {\n throw new QuarantineGateError('a non-empty quarantine reason is required')\n }\n const now = req.now ?? new Date().toISOString()\n const nowMs = Date.parse(now)\n const expiryMs = Date.parse(req.expiresAt)\n if (Number.isNaN(expiryMs)) {\n throw new QuarantineGateError(`unparseable expiresAt: ${req.expiresAt}`)\n }\n if (expiryMs <= nowMs) {\n throw new QuarantineGateError('expiresAt must be in the future')\n }\n if (expiryMs - nowMs > this.policy.maxExpiryMs) {\n throw new QuarantineGateError(\n `expiry exceeds the operator cap of ${this.policy.maxExpiryMs}ms`,\n )\n }\n this.db\n .prepare(\n `INSERT INTO quarantine (test_id, reason, flake_score, quarantined_at, expires_at)\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT(test_id) DO UPDATE SET\n reason = excluded.reason,\n flake_score = excluded.flake_score,\n quarantined_at = excluded.quarantined_at,\n expires_at = excluded.expires_at`,\n )\n .run(req.testId, reason, req.flakeScore ?? null, now, req.expiresAt)\n return {\n testId: req.testId,\n reason,\n flakeScore: req.flakeScore ?? null,\n quarantinedAt: now,\n expiresAt: req.expiresAt,\n }\n }\n\n /** Lift a quarantine. Ungated. Returns true if a row was removed. */\n release(testId: string): boolean {\n const info = this.db.prepare('DELETE FROM quarantine WHERE test_id = ?').run(testId)\n return info.changes > 0\n }\n\n /** Whether a test is currently quarantined (expiry-aware). */\n isQuarantined(testId: string, now: string = new Date().toISOString()): boolean {\n const row = this.db\n .prepare('SELECT expires_at FROM quarantine WHERE test_id = ?')\n .get(testId) as { expires_at: string } | undefined\n return row !== undefined && Date.parse(row.expires_at) > Date.parse(now)\n }\n\n /** Currently-active (unexpired) quarantines, ordered by expiry. */\n active(now: string = new Date().toISOString()): QuarantineEntry[] {\n const rows = this.db\n .prepare('SELECT * FROM quarantine WHERE expires_at > ? ORDER BY expires_at, test_id')\n .all(now) as QRow[]\n return rows.map(toEntry)\n }\n\n /** Every quarantine row, including expired ones (audit). */\n all(): QuarantineEntry[] {\n const rows = this.db.prepare('SELECT * FROM quarantine ORDER BY test_id').all() as QRow[]\n return rows.map(toEntry)\n }\n}\n\nexport interface CandidateOptions {\n /** Only verdicts with `flakeScore >= minFlakeScore` (default 0 — every flaky test). */\n minFlakeScore?: number\n}\n\n/**\n * Pure helper: rank quarantine candidates from classifier verdicts — `flaky` tests whose\n * `flakeScore` clears the floor, highest first. Never selects `broken` (a real, consistent\n * failure to FIX, not hide) or `reliable`/`insufficient-data`. The write itself is still\n * gated; this only proposes.\n */\nexport function quarantineCandidates(\n verdicts: FlakeVerdict[],\n opts: CandidateOptions = {},\n): FlakeVerdict[] {\n const floor = opts.minFlakeScore ?? 0\n return verdicts\n .filter((v) => v.state === 'flaky' && v.flakeScore >= floor)\n .sort((a, b) => b.flakeScore - a.flakeScore)\n}\n","/**\n * Vitest JSON-report ingestion — turns a `vitest run --reporter=json` report into the\n * {@link RecordedRun}s the history store records.\n *\n * Per ADR 0010 the flake pillar **spawns** `vitest run --reporter=json` and parses its\n * output (a different execution model from coverage's in-process/child-process run and\n * mutation's Stryker delegation — there is no shared runner seam). This module is the\n * pure parser half: no spawning, no I/O — it just maps the report's shape to runs, so it\n * is unit-tested against a committed real-shaped fixture. The gated spawn that produces\n * the report lives in a later slice.\n *\n * The report's `fullName` is the ancestor titles + title joined by a single space, which\n * is lossy (a describe/test boundary is indistinguishable from a space inside a title).\n * We therefore build a stable, file-qualified id from `ancestorTitles + title` joined by\n * ` > ` ourselves, falling back to `fullName`, then `title`, when those are absent.\n */\n\nimport { relative } from 'node:path'\nimport type { RecordedRun } from './store.js'\n\n/** The subset of a vitest json assertion result we read. */\nexport interface VitestAssertion {\n ancestorTitles?: string[]\n title?: string\n fullName?: string\n status?: string\n duration?: number | null\n}\n\nexport interface VitestFileResult {\n /** Test file path (absolute as vitest emits it). */\n name?: string\n assertionResults?: VitestAssertion[]\n}\n\nexport interface VitestJsonReport {\n testResults?: VitestFileResult[]\n}\n\nexport interface ParseReportOptions {\n /** ISO timestamp stamped on every parsed run. */\n at: string\n /** When set, file paths are made relative to it for stable, machine-independent ids. */\n projectRoot?: string\n /** Optional id grouping all runs from this report (a CI run / batch). */\n runGroup?: string\n}\n\n/** A status that carries a pass/fail signal. Skipped/pending/todo do not. */\nfunction outcome(status: string | undefined): boolean | undefined {\n if (status === 'passed') return true\n if (status === 'failed') return false\n return undefined\n}\n\nfunction titlePart(a: VitestAssertion): string {\n if (a.ancestorTitles?.length) return [...a.ancestorTitles, a.title ?? ''].join(' > ')\n return a.fullName ?? a.title ?? '<unknown>'\n}\n\nfunction fileLabel(name: string | undefined, projectRoot?: string): string {\n if (!name) return ''\n return projectRoot ? relative(projectRoot, name) : name\n}\n\n/**\n * Parse a vitest json report into recorded runs — one per pass/fail assertion. Skipped /\n * pending / todo assertions are dropped (no pass/fail signal). Pure: no spawning, no I/O.\n */\nexport function parseVitestJson(report: VitestJsonReport, opts: ParseReportOptions): RecordedRun[] {\n const runs: RecordedRun[] = []\n for (const file of report.testResults ?? []) {\n const label = fileLabel(file.name, opts.projectRoot)\n for (const a of file.assertionResults ?? []) {\n const passed = outcome(a.status)\n if (passed === undefined) continue\n const id = label ? `${label} > ${titlePart(a)}` : titlePart(a)\n const run: RecordedRun = { testId: id, passed, at: opts.at }\n if (typeof a.duration === 'number') run.durationMs = a.duration\n if (opts.runGroup !== undefined) run.runGroup = opts.runGroup\n runs.push(run)\n }\n }\n return runs\n}\n","/**\n * The gated vitest runner — the live half of the flake pillar. It **spawns**\n * `vitest run --reporter=json` (per ADR 0010: flake's execution model is spawn-and-parse,\n * distinct from coverage's child-process coverage run and mutation's Stryker delegation),\n * reads the JSON report, and records every outcome into the {@link HistoryStore}. Run the\n * suite `repeat` times to actually surface flakiness, then classify.\n *\n * Two ADR-0010 constraints, mirroring `@sackville-mcp/coverage`'s `runScoped`:\n * 1. **It runs code**, so it is behind a *paired* deny-by-default operator gate — an\n * `allowRun` boolean AND an `allowedRoots` allowlist (load-bearing on its own), with a\n * wall-clock cap. All operator-set; no caller input self-authorizes.\n * 2. **Child-process boundary.** The real `vitest` invocation is an injected\n * {@link TestRunner} (the bin wires a subprocess); the engine owns the gate, argv,\n * report-file plumbing, ingestion, and classification, and is unit-tested with a fake\n * runner — no real spawn in the green gate.\n */\n\nimport { mkdtempSync, readFileSync } from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { join, resolve } from 'node:path'\nimport { type SpawnedRunner, spawnRunner } from '@sackville-mcp/spawn'\nimport type { FlakeVerdict } from './classify.js'\nimport type { PytestJsonReport } from './pytest.js'\nimport type { ParseReportOptions, VitestJsonReport } from './report.js'\nimport type { HistoryStore } from './store.js'\n\n/** Thrown when the paired operator gate denies a run. */\nexport class FlakeGateError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'FlakeGateError'\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 RunHistoryConfig {\n /** The project to run tests 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 tests. Deny-by-default. */\n allowRun: boolean\n /** Wall-clock cap (ms) per iteration, passed to the runner. */\n timeoutMs?: number\n}\n\nexport interface RunAndRecordInput {\n /** How many times to run the suite — flakiness needs repeats. Default 1. */\n repeat?: number\n /** Positional vitest file filters; default runs the whole suite. */\n files?: string[]\n /** Batch id; each iteration is recorded under `${runGroup}#<i>`. */\n runGroup?: string\n}\n\n/** Injected command runner — executes `vitest <argv>` and yields its exit status. */\nexport type TestRunner = SpawnedRunner\n\nexport interface RunAndRecordResult {\n /** False only when repeat <= 0 (the runner was never invoked). */\n ran: boolean\n iterations: number\n /** Total runs recorded across all iterations. */\n recorded: number\n results: { exitCode: number; passed: boolean }[]\n /** Classifier verdicts over the store AFTER recording this batch. */\n verdicts: FlakeVerdict[]\n}\n\n/** Build the argv for one vitest suite run with the JSON reporter writing to `outFile`. */\nfunction vitestArgv(files: string[], outFile: string): string[] {\n return ['run', ...files, '--reporter=json', `--outputFile=${outFile}`]\n}\n\n/**\n * Build the argv for one pytest run with the `pytest-json-report` plugin writing to `outFile`\n * (ADR 0010 addendum: json-report now, `pytest-reportlog` staged). No `run` subcommand — pytest\n * takes positional file filters directly. The plugin is an operator-installed dev dependency.\n */\nfunction pytestArgv(files: string[], outFile: string): string[] {\n return ['--json-report', `--json-report-file=${outFile}`, ...files]\n}\n\n/** Default live runner: spawn the local `vitest` as a subprocess (used by the bin, not the gate). */\nexport const defaultVitestRunner: TestRunner = spawnRunner('vitest')\n\n/** Default live runner: spawn the local `pytest` as a subprocess (used by the bin, not the gate). */\nexport const defaultPytestRunner: TestRunner = spawnRunner('pytest')\n\n/**\n * A test framework's runner specifics: the default subprocess runner, how to build its argv with\n * a per-iteration report file, and how to ingest the parsed report into the store. Everything else\n * (gate, repeat loop, report-file plumbing, classification) is framework-agnostic.\n */\ninterface FrameworkAdapter {\n defaultRunner: TestRunner\n buildArgv(files: string[], outFile: string): string[]\n ingest(store: HistoryStore, parsed: unknown, opts: ParseReportOptions): number\n}\n\nconst VITEST: FrameworkAdapter = {\n defaultRunner: defaultVitestRunner,\n buildArgv: vitestArgv,\n ingest: (store, parsed, opts) => store.ingestReport(parsed as VitestJsonReport, opts),\n}\n\nconst PYTEST: FrameworkAdapter = {\n defaultRunner: defaultPytestRunner,\n buildArgv: pytestArgv,\n ingest: (store, parsed, opts) => store.ingestPytestReport(parsed as PytestJsonReport, opts),\n}\n\nfunction assertAllowed(config: RunHistoryConfig): void {\n if (!config.allowRun) {\n throw new FlakeGateError('test execution is 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 FlakeGateError(`project root ${config.projectRoot} is not in the operator allowlist`)\n }\n}\n\n/**\n * Run a test suite `repeat` times behind the operator gate via the given {@link FrameworkAdapter},\n * recording every outcome into the store, then classify. The actual invocation is the injected\n * `runner` (default = the adapter's subprocess runner); no real spawn in the green gate.\n */\nasync function runAndRecordWith(\n fw: FrameworkAdapter,\n store: HistoryStore,\n config: RunHistoryConfig,\n input: RunAndRecordInput,\n deps: { runner?: TestRunner; reportDir?: string },\n): Promise<RunAndRecordResult> {\n assertAllowed(config)\n\n const repeat = input.repeat ?? 1\n if (repeat <= 0) {\n return { ran: false, iterations: 0, recorded: 0, results: [], verdicts: store.classify() }\n }\n\n const runner = deps.runner ?? fw.defaultRunner\n const reportDir = deps.reportDir ?? mkdtempSync(join(tmpdir(), 'sackville-flake-'))\n const files = input.files ?? []\n const results: { exitCode: number; passed: boolean }[] = []\n let recorded = 0\n\n for (let i = 0; i < repeat; i++) {\n const outFile = join(reportDir, `report-${i}.json`)\n const { exitCode } = await runner(fw.buildArgv(files, outFile), {\n cwd: config.projectRoot,\n timeoutMs: config.timeoutMs,\n })\n let parsed: unknown\n try {\n parsed = JSON.parse(readFileSync(outFile, 'utf8'))\n } catch {\n throw new Error(\n `flake run did not produce a JSON report at ${outFile} (exit code ${exitCode})`,\n )\n }\n recorded += fw.ingest(store, parsed, {\n at: new Date().toISOString(),\n projectRoot: config.projectRoot,\n runGroup: input.runGroup !== undefined ? `${input.runGroup}#${i}` : undefined,\n })\n results.push({ exitCode, passed: exitCode === 0 })\n }\n\n return { ran: true, iterations: repeat, recorded, results, verdicts: store.classify() }\n}\n\n/**\n * Run the vitest suite `repeat` times behind the operator gate, recording every outcome into the\n * store, then classify. The actual `vitest` invocation is the injected `runner` (default\n * {@link defaultVitestRunner}).\n */\nexport async function runAndRecord(\n store: HistoryStore,\n config: RunHistoryConfig,\n input: RunAndRecordInput,\n deps: { runner?: TestRunner; reportDir?: string } = {},\n): Promise<RunAndRecordResult> {\n return runAndRecordWith(VITEST, store, config, input, deps)\n}\n\n/**\n * The pytest sibling of {@link runAndRecord} (ADR 0010 addendum): spawn `pytest --json-report`\n * `repeat` times, ingest via the existing `parsePytestJson` (unchanged), classify. Repeats re-run\n * the WHOLE suite — never `pytest-repeat`, whose `[i-N]` nodeid suffix would fragment the\n * one-history-per-nodeid invariant the classifier relies on.\n */\nexport async function runAndRecordPytest(\n store: HistoryStore,\n config: RunHistoryConfig,\n input: RunAndRecordInput,\n deps: { runner?: TestRunner; reportDir?: string } = {},\n): Promise<RunAndRecordResult> {\n return runAndRecordWith(PYTEST, store, config, input, deps)\n}\n","/**\n * The private run-history store — `@sackville-mcp/flake`'s own SQLite database.\n *\n * Per ADR 0010 this is a **second SQLite owner**, deliberately OUTSIDE the docs-pillar\n * \"only `@sackville-mcp/core` touches SQLite\" invariant: it is a new, private store for test\n * run outcomes, not a crossing of the Python↔TS polyglot contract (which remains the\n * `schema/sackville.schema.sql` index that `core` reads). It records each test's pass/fail\n * history over time and reads it back as the `TestHistory[]` the pure classifier consumes.\n *\n * The schema is intentionally tiny: one append-only `test_run` row per recorded outcome,\n * plus a `flake_meta` version marker. Quarantine state is a separate table added by the\n * quarantine slice.\n */\n\nimport Database from 'better-sqlite3'\nimport {\n type ClassifyOptions,\n classifyHistories,\n type FlakeVerdict,\n type TestHistory,\n type TestRun,\n} from './classify.js'\nimport { type PytestJsonReport, parsePytestJson } from './pytest.js'\nimport { type ParseReportOptions, parseVitestJson, type VitestJsonReport } from './report.js'\n\nconst SCHEMA_VERSION = 1\n\n/** A test outcome to record. */\nexport interface RecordedRun {\n testId: string\n passed: boolean\n /** ISO timestamp; defaults to now. */\n at?: string\n /** Optional wall-clock duration of the run. */\n durationMs?: number\n /** Optional id grouping all tests from one suite execution (a CI run / batch). */\n runGroup?: string\n}\n\nexport interface HistoryQueryOptions {\n /** Keep only the most recent N runs per test (chronological tail). */\n limitPerTest?: number\n /** Only include runs at/after this ISO timestamp. */\n since?: string\n}\n\ninterface RunRow {\n test_id: string\n passed: number\n at: string\n}\n\nfunction migrate(db: Database.Database): void {\n db.pragma('journal_mode = WAL')\n db.exec(`\n CREATE TABLE IF NOT EXISTS flake_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);\n CREATE TABLE IF NOT EXISTS test_run (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n test_id TEXT NOT NULL,\n passed INTEGER NOT NULL,\n at TEXT NOT NULL,\n duration_ms REAL,\n run_group TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_test_run_test_id_at ON test_run(test_id, at, id);\n `)\n const row = db.prepare('SELECT value FROM flake_meta WHERE key = ?').get('schema_version') as\n | { value: string }\n | undefined\n if (!row) {\n db.prepare('INSERT INTO flake_meta (key, value) VALUES (?, ?)').run(\n 'schema_version',\n String(SCHEMA_VERSION),\n )\n }\n}\n\nexport class HistoryStore {\n private readonly db: Database.Database\n private readonly insert: Database.Statement\n\n constructor(db: Database.Database) {\n this.db = db\n migrate(db)\n this.insert = db.prepare(\n 'INSERT INTO test_run (test_id, passed, at, duration_ms, run_group) VALUES (?, ?, ?, ?, ?)',\n )\n }\n\n /** Open (creating if needed) a file-backed history store and run migrations. */\n static open(path: string): HistoryStore {\n return new HistoryStore(new Database(path))\n }\n\n /** An in-memory store (tests, ephemeral analysis). */\n static memory(): HistoryStore {\n return new HistoryStore(new Database(':memory:'))\n }\n\n /** The underlying database — shared with sibling tables (e.g. quarantine). */\n get database(): Database.Database {\n return this.db\n }\n\n recordRun(run: RecordedRun): void {\n this.insert.run(\n run.testId,\n run.passed ? 1 : 0,\n run.at ?? new Date().toISOString(),\n run.durationMs ?? null,\n run.runGroup ?? null,\n )\n }\n\n /** Record many runs in a single transaction. */\n recordRuns(runs: RecordedRun[]): void {\n const tx = this.db.transaction((batch: RecordedRun[]) => {\n for (const r of batch) this.recordRun(r)\n })\n tx(runs)\n }\n\n private rows(opts: HistoryQueryOptions): RunRow[] {\n const params: string[] = []\n let sql = 'SELECT test_id, passed, at FROM test_run'\n if (opts.since !== undefined) {\n sql += ' WHERE at >= ?'\n params.push(opts.since)\n }\n sql += ' ORDER BY test_id, at, id'\n return this.db.prepare(sql).all(...params) as RunRow[]\n }\n\n private static toRun(r: RunRow): TestRun {\n return { passed: r.passed !== 0, at: r.at }\n }\n\n /** All histories, grouped per test and sorted by test id. */\n histories(opts: HistoryQueryOptions = {}): TestHistory[] {\n const map = new Map<string, TestRun[]>()\n for (const r of this.rows(opts)) {\n const list = map.get(r.test_id) ?? []\n list.push(HistoryStore.toRun(r))\n map.set(r.test_id, list)\n }\n const limit = opts.limitPerTest\n return [...map.entries()]\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))\n .map(([id, runs]) => ({ id, runs: limit !== undefined ? runs.slice(-limit) : runs }))\n }\n\n /** One test's history (empty `runs` when never recorded). */\n history(testId: string, opts: HistoryQueryOptions = {}): TestHistory {\n const params: string[] = [testId]\n let sql = 'SELECT test_id, passed, at FROM test_run WHERE test_id = ?'\n if (opts.since !== undefined) {\n sql += ' AND at >= ?'\n params.push(opts.since)\n }\n sql += ' ORDER BY at, id'\n let runs = (this.db.prepare(sql).all(...params) as RunRow[]).map(HistoryStore.toRun)\n if (opts.limitPerTest !== undefined) runs = runs.slice(-opts.limitPerTest)\n return { id: testId, runs }\n }\n\n /**\n * Parse a vitest json report and record every pass/fail assertion as a run. Returns the\n * number of runs recorded (skipped/pending/todo assertions are not counted).\n */\n ingestReport(report: VitestJsonReport, opts: ParseReportOptions): number {\n const runs = parseVitestJson(report, opts)\n this.recordRuns(runs)\n return runs.length\n }\n\n /**\n * Parse a pytest-json-report report and record every pass/fail/error test as a run. Returns\n * the number of runs recorded (skipped/xfailed/xpassed tests are not counted). The Python\n * sibling of {@link ingestReport}.\n */\n ingestPytestReport(report: PytestJsonReport, opts: ParseReportOptions): number {\n const runs = parsePytestJson(report, opts)\n this.recordRuns(runs)\n return runs.length\n }\n\n /** Classify every test's history straight from the store. */\n classify(opts: ClassifyOptions & HistoryQueryOptions = {}): FlakeVerdict[] {\n return classifyHistories(this.histories(opts), opts)\n }\n\n close(): void {\n this.db.close()\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sackville-mcp/flake",
|
|
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
|
-
"better-sqlite3": "^12.10.0"
|
|
19
|
+
"better-sqlite3": "^12.10.0",
|
|
20
|
+
"@sackville-mcp/spawn": "0.0.1-alpha.4"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@types/better-sqlite3": "^7.6.13"
|