supercov 0.0.3 → 0.0.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/README.md CHANGED
@@ -36,8 +36,8 @@ The coverage engine has seven independent release gates:
36
36
  - release CI shards the pinned TC39 Test262 corpus across 16 workers, runs the
37
37
  official Test262 harness on original and instrumented sources, and rejects
38
38
  any scenario that passes originally but fails after transformation; and
39
- - checked performance budgets cover transform latency, output expansion, and
40
- runtime probe overhead.
39
+ - checked performance budgets cover transform latency, transactional workspace
40
+ preparation, output expansion, and runtime probe overhead.
41
41
 
42
42
  ```sh
43
43
  npm test
@@ -51,7 +51,9 @@ computed logical assignments, defaults, `try`/`catch`/`finally`, iterator
51
51
  closing, switch fallthrough, labeled loops, async functions, and generators.
52
52
  The compatibility workflow additionally runs Node 22/24/25, Playwright
53
53
  1.55/current, Vite 5/current, Vitest 2/current, Chromium, Firefox, WebKit, and
54
- modern JavaScript/JSX/TypeScript/TSX syntax fixtures. Test262's module, async,
54
+ modern JavaScript/JSX/TypeScript/TSX syntax fixtures. Filesystem publication,
55
+ symlink, copy fallback, ENOSPC, failed rename, and forced-termination recovery
56
+ also run on Ubuntu, macOS, and Windows. Test262's module, async,
55
57
  raw, parse/resolution-negative, Annex B sloppy-script extension, and explicit
56
58
  `Function.prototype.toString`/function-source-coercion tests are intentionally
57
59
  excluded from the source-rewrite comparison, with reason counts printed for
@@ -66,11 +68,14 @@ blocks the trusted-publishing workflow.
66
68
 
67
69
  ## Agent query workflow
68
70
 
69
- Each run is stored locally as a compressed, immutable report under
70
- `.supercov/runs/<run-id>/`. Its `report.json.gz`, `run.json`, and
71
- `report.html` keep the machine report, run metadata, and human report together.
72
- Agents should use bounded CLI queries instead of loading the complete report
73
- into context.
71
+ Each run is stored locally as two immutable files under
72
+ `.supercov/runs/<run-id>/`: `evidence.raw.gz` and `run.json`. The archive
73
+ contains the exact coverage denominator manifest plus raw per-worker and
74
+ background evidence. Every query reconstructs its requested coverage view
75
+ directly from that archive; no derived report or query cache is retained.
76
+ Loose evidence is removed only after the whole run directory is atomically
77
+ visible. HTML is not generated during a test run; agents should use bounded
78
+ CLI queries instead of loading the complete derived model into context.
74
79
 
75
80
  ```sh
76
81
  # Orient using only a few lines.
@@ -102,8 +107,8 @@ Use `--filter passed` for verified coverage from successful attempts of
102
107
  ultimately passing tests, or `--filter failed` to inspect only execution from
103
108
  failed attempts (including failed retries of flaky tests). Reports record
104
109
  attempt status and classify each test as passed, failed, flaky, skipped, timed
105
- out, interrupted, or unknown. The HTML equivalents are `report.html`,
106
- `report-passed.html`, and `report-failed.html`.
110
+ out, interrupted, or unknown. The passed and failed subsets are stored inside
111
+ the same compressed report rather than duplicated into presentation files.
107
112
 
108
113
  The run ID is positional because all coverage queries operate on one immutable
109
114
  run. `latest` is a convenience selector for interactive use. Every query
@@ -120,8 +125,12 @@ CLI:
120
125
  1. refreshes a stable isolated source namespace under
121
126
  `.supercov/cache/instrumented-workspace/<project>/`, links the existing
122
127
  dependency tree, and creates all Vite, Vitest, Playwright, and build output
123
- only there; the stable path lets VM/container snapshot systems reuse a
124
- coverage build without touching the application's ordinary build;
128
+ only there; file data uses copy-on-write reflinks where the filesystem
129
+ supports them, and falls back to copying where it does not; the stable path
130
+ lets VM/container snapshot systems reuse a coverage build without touching
131
+ the application's ordinary build; when the complete source/config/toolchain
132
+ fingerprint is unchanged, the prior instrumented output and manifest are
133
+ carried into the refreshed source snapshot and the build is skipped;
125
134
  2. inventories every `app/**/*.ts(x)` and `src/**/*.ts(x)` file for the
126
135
  denominator, then instruments modules loaded by Vite without changing the
127
136
  project's config; when no build script exists, it instruments only the
@@ -136,30 +145,55 @@ CLI:
136
145
  5. attributes every source hit and decision vector to its individual test,
137
146
  automatically wraps Playwright actions and assertions, and records the
138
147
  action/assertion phase responsible for each correlated hit; then merges
139
- server and browser evidence into HTML and JSON reports; and
140
- 6. atomically publishes the evidence/report into `.supercov/` and retains only
141
- the disposable isolated build namespace as a provider snapshot cache. The
148
+ server and browser evidence into one compressed JSON report; and
149
+ 6. atomically publishes the compressed evidence/report into `.supercov/`, then
150
+ removes loose evidence and terminal per-run work state, retaining only the
151
+ immutable run and disposable isolated build namespace. The
142
152
  ordinary application build is never read as an input, overwritten, or
143
153
  rebuilt afterward.
144
154
 
145
155
  Only `.supercov/` is modified in the user's checkout. A per-project lock
146
156
  rejects overlapping runs before either can build. Run state is durably written
147
- through preparing/building/testing/reporting/terminal phases; SIGINT, SIGTERM,
157
+ through preparing/building/testing/reporting phases; SIGINT, SIGTERM,
148
158
  and SIGHUP are forwarded to the entire child process group. If the process is
149
159
  killed without a cleanup opportunity, the next invocation marks the dead PID's
150
- run abandoned and refreshes the isolated namespace before using it. Report,
151
- evidence, and state writes use sibling-temp files, fsync, and atomic rename;
152
- lock acquisition uses exclusive creation and fsync.
160
+ run abandoned and refreshes the isolated namespace before using it. Cache
161
+ refresh is transactional: a new sibling generation is prepared completely,
162
+ the stable name is switched only at publication, and the prior complete
163
+ generation is retained until that switch succeeds. The next invocation
164
+ discards orphan staging trees or restores the prior generation if a host crash
165
+ landed between the two same-filesystem renames. Report, evidence archive, and
166
+ state writes use sibling-temp files, fsync, and atomic rename; lock acquisition
167
+ uses exclusive creation and fsync. Published `run.json` is the durable terminal
168
+ record, so terminal work state is not retained.
153
169
 
154
170
  Retention is deterministic because UTC run IDs sort chronologically:
155
171
 
156
172
  ```sh
157
- npx supercov clean --keep 20
158
- npx supercov clean --keep 20 --dry-run
173
+ npx supercov prune --keep 20
174
+ npx supercov prune --keep 20 --dry-run
175
+ npx supercov clean --keep 20 # also removes the shared build cache
159
176
  ```
160
177
 
161
- The cleanup command never removes an active workspace and never touches files
162
- outside `.supercov/`.
178
+ Neither operation runs automatically. `prune` removes explicit history beyond
179
+ the requested retention and orphan/terminal transient data while preserving
180
+ the shared cache. `clean` also removes that cache. Both acquire the same lock
181
+ as a coverage run, refuse to race an active run, and never touch files outside
182
+ `.supercov/`.
183
+
184
+ The complete ownership, crash-recovery, symlink, and future copy-free design is
185
+ documented in [Workspace isolation](docs/workspace-isolation.md).
186
+
187
+ Every run prints and stores monotonic phase timings for initialization,
188
+ workspace preparation, adapter setup, the instrumented build, the unchanged
189
+ test command, and report preparation. They are available in
190
+ `.supercov/runs/<run-id>/run.json` and in the JSON form of `supercov runs`.
191
+ These phase timings do not pretend to be end-to-end overhead: that percentage
192
+ requires an explicit comparison with the same command run without Supercov,
193
+ which Supercov never executes automatically because an arbitrary test command
194
+ may have side effects or external cost. See
195
+ [Performance and storage](docs/performance.md) for the comparison methodology,
196
+ strategy trade-offs, and a measured real-suite reference.
163
197
 
164
198
  The automatic adapters currently support standard Playwright suites (ESM and
165
199
  CommonJS specs in arbitrary project directories), project-owned Playwright
@@ -242,7 +276,7 @@ phase travels on browser requests into automatically wrapped Remix loaders,
242
276
  actions, and the server document renderer. Node async context preserves that
243
277
  ID through awaited helpers. An assertion also retains the preceding action ID,
244
278
  making chains such as “click -> application lines/decisions -> visible
245
- assertion” queryable in JSON and visible in HTML.
279
+ assertion” queryable in JSON.
246
280
 
247
281
  Server evidence is safe when Playwright uses multiple workers against one
248
282
  application server. Every routed request carries a run/worker/test/retry scope;
@@ -0,0 +1,14 @@
1
+ import type { CoverageRunIntegrity } from "./types.ts";
2
+ import type { CoverageProject } from "./project.ts";
3
+ export declare const INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION = 1;
4
+ export interface InstrumentedBuildCache {
5
+ schemaVersion: typeof INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION;
6
+ key: string;
7
+ createdAt: string;
8
+ artifactPaths: string[];
9
+ }
10
+ export declare function instrumentedBuildCacheKey(integrity: CoverageRunIntegrity, project: CoverageProject): string;
11
+ export declare function readInstrumentedBuildCache(workspace: string, key: string): InstrumentedBuildCache | undefined;
12
+ export declare function writeInstrumentedBuildCache(workspace: string, key: string): InstrumentedBuildCache | undefined;
13
+ export declare function buildCacheReusePaths(metadata: InstrumentedBuildCache): string[];
14
+ //# sourceMappingURL=buildCache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildCache.d.ts","sourceRoot":"","sources":["../src/buildCache.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,eAAO,MAAM,uCAAuC,IAAI,CAAC;AAUzD,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,OAAO,uCAAuC,CAAC;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAgBD,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,oBAAoB,EAC/B,OAAO,EAAE,eAAe,GACvB,MAAM,CAeR;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,sBAAsB,GAAG,SAAS,CAqBpC;AAED,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,sBAAsB,GAAG,SAAS,CAqCpC;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,sBAAsB,GAC/B,MAAM,EAAE,CAEV"}
@@ -0,0 +1,84 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { isAbsolute, resolve, sep } from "node:path";
4
+ import { atomicWriteFileSync } from "./atomic.js";
5
+ export const INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION = 1;
6
+ const BUILD_OUTPUT_CANDIDATES = [
7
+ "build",
8
+ "dist",
9
+ ".next",
10
+ ".nuxt",
11
+ ".output",
12
+ ];
13
+ function metadataPath(workspace) {
14
+ return resolve(workspace, ".supercov/build-cache.json");
15
+ }
16
+ function safeRelativePath(path) {
17
+ return Boolean(path &&
18
+ !isAbsolute(path) &&
19
+ path !== ".." &&
20
+ !path.startsWith(`..${sep}`) &&
21
+ !path.split(/[\\/]/).includes(".."));
22
+ }
23
+ export function instrumentedBuildCacheKey(integrity, project) {
24
+ return createHash("sha256")
25
+ .update(JSON.stringify({
26
+ schemaVersion: INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION,
27
+ executionFingerprint: integrity.fingerprint.execution,
28
+ adapter: project.buildAdapter,
29
+ command: project.buildCommand,
30
+ environment: project.buildEnvironment,
31
+ node: process.versions.node,
32
+ platform: process.platform,
33
+ architecture: process.arch,
34
+ }))
35
+ .digest("hex");
36
+ }
37
+ export function readInstrumentedBuildCache(workspace, key) {
38
+ let metadata;
39
+ try {
40
+ metadata = JSON.parse(readFileSync(metadataPath(workspace), "utf8"));
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ if (metadata.schemaVersion !== INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION ||
46
+ metadata.key !== key ||
47
+ !Array.isArray(metadata.artifactPaths) ||
48
+ metadata.artifactPaths.length === 0 ||
49
+ metadata.artifactPaths.some((path) => !safeRelativePath(path) || !existsSync(resolve(workspace, path)))) {
50
+ return undefined;
51
+ }
52
+ return metadata;
53
+ }
54
+ export function writeInstrumentedBuildCache(workspace, key) {
55
+ let declared = {};
56
+ try {
57
+ declared = JSON.parse(readFileSync(resolve(workspace, ".supercov/build-outputs.json"), "utf8"));
58
+ }
59
+ catch {
60
+ // Older/native Vite integrations use the conservative known outputs.
61
+ }
62
+ const existingOutputs = [
63
+ ...new Set([
64
+ ...BUILD_OUTPUT_CANDIDATES,
65
+ ...(declared.paths ?? []).filter(safeRelativePath),
66
+ ]),
67
+ ].filter((path) => existsSync(resolve(workspace, path)));
68
+ const outputs = existingOutputs.filter((path) => !existingOutputs.some((parent) => parent !== path && path.startsWith(`${parent}/`)));
69
+ const manifest = ".supercov/manifest.json";
70
+ if (outputs.length === 0 || !existsSync(resolve(workspace, manifest)))
71
+ return undefined;
72
+ const metadata = {
73
+ schemaVersion: INSTRUMENTED_BUILD_CACHE_SCHEMA_VERSION,
74
+ key,
75
+ createdAt: new Date().toISOString(),
76
+ artifactPaths: [...outputs, manifest],
77
+ };
78
+ atomicWriteFileSync(metadataPath(workspace), `${JSON.stringify(metadata, null, 2)}\n`);
79
+ return metadata;
80
+ }
81
+ export function buildCacheReusePaths(metadata) {
82
+ return [...metadata.artifactPaths, ".supercov/build-cache.json"];
83
+ }
84
+ //# sourceMappingURL=buildCache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildCache.js","sourceRoot":"","sources":["../src/buildCache.ts"],"names":[],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,14 +1,36 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, } from "node:fs";
3
3
  import { relative, resolve, sep } from "node:path";
4
+ import { performance } from "node:perf_hooks";
4
5
  import { fileURLToPath, pathToFileURL } from "node:url";
5
6
  import { atomicRenameSync, atomicWriteFileSync } from "./atomic.js";
6
- import { writeMcdcReport } from "./reporter.js";
7
+ import { printCoverageSummary } from "./reporter.js";
8
+ import { analyzeCoverageArchive } from "./runAnalysis.js";
7
9
  import { coverageQueryCommands, runQueryCommand } from "./query.js";
8
10
  import { discoverCoverageProject } from "./project.js";
9
11
  import { createRunIntegrity } from "./integrity.js";
12
+ import { writeEvidenceArchive } from "./evidenceArchive.js";
13
+ import { buildCacheReusePaths, instrumentedBuildCacheKey, readInstrumentedBuildCache, writeInstrumentedBuildCache, } from "./buildCache.js";
10
14
  import { instrumentDirectWorkspace } from "./directInstrumenter.js";
11
- import { acquireProjectLock, cachedWorkspacePath, cleanCoverageStorage, prepareCachedWorkspace, recoverAbandonedRuns, updateRunState, writeRunState, } from "./workspace.js";
15
+ import { acquireProjectLock, cachedWorkspacePath, cleanCoverageStorage, finalizePublishedRunStorage, prepareCachedWorkspace, pruneCoverageStorage, recoverAbandonedRuns, updateRunState, writeRunState, } from "./workspace.js";
16
+ function roundedTimings(timings) {
17
+ return Object.fromEntries(Object.entries(timings).map(([phase, duration]) => [
18
+ phase,
19
+ Math.round(duration * 10) / 10,
20
+ ]));
21
+ }
22
+ function formatTimings(timings, totalMs) {
23
+ const rounded = roundedTimings(timings);
24
+ return [
25
+ `initialization=${rounded.initializationMs}ms`,
26
+ `workspace=${rounded.workspacePreparationMs}ms`,
27
+ `setup=${rounded.adapterSetupMs}ms`,
28
+ `build=${rounded.instrumentedBuildMs}ms`,
29
+ `tests=${rounded.testCommandMs}ms`,
30
+ `evidence=${rounded.evidencePublicationMs}ms`,
31
+ `total=${Math.round(totalMs * 10) / 10}ms`,
32
+ ].join(" ");
33
+ }
12
34
  let activeChild;
13
35
  let signalEscalation;
14
36
  function terminateChild(signal) {
@@ -68,7 +90,7 @@ function signalExitCode(signal) {
68
90
  return 143;
69
91
  return 128;
70
92
  }
71
- function parseCleanOptions(args) {
93
+ function parseRetentionOptions(command, args) {
72
94
  let keep = 20;
73
95
  let dryRun = false;
74
96
  for (let index = 0; index < args.length; index += 1) {
@@ -82,21 +104,37 @@ function parseCleanOptions(args) {
82
104
  keep = value;
83
105
  }
84
106
  else
85
- throw new Error(`Unknown clean option: ${argument}`);
107
+ throw new Error(`Unknown ${command} option: ${argument}`);
86
108
  }
87
109
  return { keep, dryRun };
88
110
  }
89
111
  function cleanCommand(args) {
90
- const options = parseCleanOptions(args);
112
+ const options = parseRetentionOptions("clean", args);
91
113
  const result = cleanCoverageStorage(process.cwd(), options);
92
114
  console.log(`[supercov] ${options.dryRun ? "would remove" : "removed"} ${result.removedRuns.length} stored run(s), ${result.removedWorkspaces.length} per-run workspace(s), and ${result.removedBuildCache ? "the" : "no"} isolated build cache; keeping ${options.keep} newest run(s)`);
93
115
  for (const id of result.removedRuns)
94
116
  console.log(id);
95
117
  }
118
+ function pruneCommand(args) {
119
+ const options = parseRetentionOptions("prune", args);
120
+ const result = pruneCoverageStorage(process.cwd(), options);
121
+ console.log(`[supercov] ${options.dryRun ? "would remove" : "removed"} ${result.removedRuns.length} stored run(s), ${result.removedWorkspaces.length} terminal/orphan work director${result.removedWorkspaces.length === 1 ? "y" : "ies"}, and ${result.removedEvidence.length} loose evidence director${result.removedEvidence.length === 1 ? "y" : "ies"}; keeping ${options.keep} newest run(s) and preserving the shared cache`);
122
+ for (const id of result.removedRuns)
123
+ console.log(id);
124
+ }
96
125
  async function createCoverageRun(command) {
97
126
  const root = process.cwd();
98
127
  const runId = new Date().toISOString().replace(/[:.]/g, "-");
99
128
  const runStartedAt = Date.now();
129
+ const runStartedMonotonic = performance.now();
130
+ const timings = {
131
+ initializationMs: 0,
132
+ workspacePreparationMs: 0,
133
+ adapterSetupMs: 0,
134
+ instrumentedBuildMs: 0,
135
+ testCommandMs: 0,
136
+ evidencePublicationMs: 0,
137
+ };
100
138
  const recovered = recoverAbandonedRuns(root);
101
139
  if (recovered.length > 0)
102
140
  console.error(`[supercov] recovered abandoned run(s): ${recovered.join(", ")}`);
@@ -105,7 +143,12 @@ async function createCoverageRun(command) {
105
143
  const packageSource = fileURLToPath(new URL(".", import.meta.url));
106
144
  const runIntegrity = createRunIntegrity(root, project, packageSource);
107
145
  const workspace = cachedWorkspacePath(root);
108
- const reportStagingDirectory = resolve(root, ".supercov/work", runId, "report-publication");
146
+ const buildCacheKey = instrumentedBuildCacheKey(runIntegrity, project);
147
+ const reusableBuild = project.buildAdapter === "vite"
148
+ ? readInstrumentedBuildCache(workspace, buildCacheKey)
149
+ : undefined;
150
+ const serverEvidenceRoot = resolve(workspace, ".supercov/server-evidence");
151
+ const runStagingDirectory = resolve(root, ".supercov/work", runId, "run-publication");
109
152
  const storedRunDirectory = resolve(root, ".supercov/runs", runId);
110
153
  const startedAt = new Date(runStartedAt).toISOString();
111
154
  writeRunState(root, runId, {
@@ -138,11 +181,21 @@ async function createCoverageRun(command) {
138
181
  }
139
182
  let buildResult;
140
183
  let testResult;
141
- let reportFailed = false;
184
+ let publicationFailed = false;
185
+ let runPublished = false;
186
+ let timingsPrinted = false;
187
+ timings.initializationMs = performance.now() - runStartedMonotonic;
142
188
  try {
143
- const isolatedRoot = prepareCachedWorkspace(root);
189
+ let phaseStarted = performance.now();
190
+ const isolatedRoot = prepareCachedWorkspace(root, {
191
+ ...(reusableBuild
192
+ ? { reusePaths: buildCacheReusePaths(reusableBuild) }
193
+ : {}),
194
+ });
195
+ timings.workspacePreparationMs = performance.now() - phaseStarted;
144
196
  if (receivedSignal)
145
197
  throw new Error(`Interrupted by ${receivedSignal}`);
198
+ phaseStarted = performance.now();
146
199
  const generatedDirectory = resolve(isolatedRoot, ".supercov");
147
200
  const evidenceDirectoryRelative = `.supercov/evidence/${runId}`;
148
201
  const isolatedEvidenceDirectory = resolve(isolatedRoot, evidenceDirectoryRelative);
@@ -151,6 +204,7 @@ async function createCoverageRun(command) {
151
204
  const generatedViteConfig = resolve(generatedDirectory, "vite.config.mjs");
152
205
  const generatedVitestConfig = resolve(generatedDirectory, "vitest.config.mjs");
153
206
  const manifestPath = resolve(generatedDirectory, "manifest.json");
207
+ const buildOutputMetadataPath = resolve(generatedDirectory, "build-outputs.json");
154
208
  const isolatedPlaywrightConfig = project.playwrightConfig
155
209
  ? resolve(isolatedRoot, relative(root, project.playwrightConfig))
156
210
  : undefined;
@@ -258,7 +312,7 @@ async function createCoverageRun(command) {
258
312
  ` const relocateOutput = output => output ? ({ ...output, dir: output.dir ? relocate(output.dir, 'Rollup output') : output.dir, file: output.file ? relocate(output.file, 'Rollup output') : output.file }) : output;`,
259
313
  ` const rollupOutput = config.build?.rollupOptions?.output;`,
260
314
  ` const safe = { ...config, cacheDir: resolve(isolatedRoot, '.supercov/vite-cache'), build: { ...config.build, outDir: relocate(config.build?.outDir ?? 'dist', 'Vite build output'), rollupOptions: { ...config.build?.rollupOptions, output: Array.isArray(rollupOutput) ? rollupOutput.map(relocateOutput) : relocateOutput(rollupOutput) } } };`,
261
- ` return mergeConfig(safe, { plugins: [mcdcVitePlugin(${JSON.stringify({ root: isolatedRoot, sourceRoots: project.sourceRoots, manifestPath })})] });`,
315
+ ` return mergeConfig(safe, { plugins: [mcdcVitePlugin(${JSON.stringify({ root: isolatedRoot, sourceRoots: project.sourceRoots, manifestPath, buildOutputMetadataPath })})] });`,
262
316
  `}`,
263
317
  "",
264
318
  ].join("\n"));
@@ -291,6 +345,7 @@ async function createCoverageRun(command) {
291
345
  SUPERCOV_EXECUTION_FINGERPRINT: runIntegrity.fingerprint.execution,
292
346
  SUPERCOV_EXECUTION_LOG: resolve(isolatedEvidenceDirectory, "execution.jsonl"),
293
347
  SUPERCOV_RUN_ID: runId,
348
+ SUPERCOV_SERVER_EVIDENCE_ROOT: serverEvidenceRoot,
294
349
  SUPERCOV_MANIFEST: manifestPath,
295
350
  SUPERCOV_PLAYWRIGHT_MODULE: project.playwrightModule,
296
351
  SUPERCOV_PLAYWRIGHT_TEST_EXPORT: project.playwrightTestExport,
@@ -311,13 +366,19 @@ async function createCoverageRun(command) {
311
366
  ]
312
367
  .filter(Boolean)
313
368
  .join(" ");
369
+ timings.adapterSetupMs = performance.now() - phaseStarted;
314
370
  updateRunState(root, runId, { status: "building" });
315
371
  console.error(`[supercov] instrumenting isolated workspace ${isolatedRoot}`);
316
372
  if (Object.keys(project.buildEnvironment).length > 0)
317
373
  console.error(`[supercov] inferred build mode from command/config: ${Object.entries(project.buildEnvironment)
318
374
  .map(([key, value]) => `${key}=${value}`)
319
375
  .join(" ")}`);
320
- if (project.buildAdapter === "direct") {
376
+ phaseStarted = performance.now();
377
+ if (reusableBuild) {
378
+ console.error(`[supercov] reusing exact-fingerprint instrumented build ${buildCacheKey.slice(0, 12)}`);
379
+ buildResult = { status: 0, signal: null };
380
+ }
381
+ else if (project.buildAdapter === "direct") {
321
382
  instrumentDirectWorkspace(isolatedRoot, project.sourceRoots, manifestPath);
322
383
  buildResult = { status: 0, signal: null };
323
384
  }
@@ -336,6 +397,12 @@ async function createCoverageRun(command) {
336
397
  },
337
398
  });
338
399
  }
400
+ if (buildResult.status === 0 &&
401
+ project.buildAdapter === "vite" &&
402
+ !reusableBuild) {
403
+ writeInstrumentedBuildCache(isolatedRoot, buildCacheKey);
404
+ }
405
+ timings.instrumentedBuildMs = performance.now() - phaseStarted;
339
406
  if (receivedSignal)
340
407
  throw new Error(`Interrupted by ${receivedSignal}`);
341
408
  if (buildResult.error)
@@ -343,6 +410,7 @@ async function createCoverageRun(command) {
343
410
  if (buildResult.status === 0) {
344
411
  updateRunState(root, runId, { status: "testing" });
345
412
  console.error(`[supercov] running in isolated workspace: ${command.join(" ")}`);
413
+ phaseStarted = performance.now();
346
414
  testResult = await runChild(command[0], command.slice(1), {
347
415
  cwd: isolatedRoot,
348
416
  env: {
@@ -351,38 +419,65 @@ async function createCoverageRun(command) {
351
419
  SUPERCOV_CJS_INTERCEPT: "1",
352
420
  },
353
421
  });
422
+ timings.testCommandMs = performance.now() - phaseStarted;
354
423
  if (receivedSignal)
355
424
  throw new Error(`Interrupted by ${receivedSignal}`);
356
425
  if (testResult.error)
357
426
  throw testResult.error;
358
- updateRunState(root, runId, { status: "reporting" });
427
+ updateRunState(root, runId, { status: "publishing" });
428
+ phaseStarted = performance.now();
359
429
  rmSync(persistedEvidenceDirectory, { recursive: true, force: true });
360
430
  if (existsSync(isolatedEvidenceDirectory))
361
431
  atomicRenameSync(isolatedEvidenceDirectory, persistedEvidenceDirectory);
362
432
  try {
363
- rmSync(reportStagingDirectory, { recursive: true, force: true });
364
- writeMcdcReport(persistedEvidenceDirectory, runId, runStartedAt, manifestPath, testResult.status, runIntegrity, {
365
- directory: reportStagingDirectory,
366
- displayDirectory: storedRunDirectory,
367
- });
368
- atomicWriteFileSync(resolve(reportStagingDirectory, "run.json"), `${JSON.stringify({
433
+ rmSync(runStagingDirectory, { recursive: true, force: true });
434
+ const evidenceArchivePath = resolve(runStagingDirectory, "evidence.raw.gz");
435
+ const rawEvidence = writeEvidenceArchive([
436
+ { file: manifestPath, path: "manifest.json" },
437
+ { directory: persistedEvidenceDirectory },
438
+ {
439
+ directory: resolve(serverEvidenceRoot, runId),
440
+ prefix: "server",
441
+ },
442
+ ], evidenceArchivePath);
443
+ printCoverageSummary(analyzeCoverageArchive(evidenceArchivePath, {
444
+ runId,
445
+ testExitCode: testResult.status,
446
+ integrity: runIntegrity,
447
+ generatedAt: startedAt,
448
+ }));
449
+ timings.evidencePublicationMs = performance.now() - phaseStarted;
450
+ atomicWriteFileSync(resolve(runStagingDirectory, "run.json"), `${JSON.stringify({
369
451
  id: runId,
370
452
  startedAt,
371
453
  durationMs: Date.now() - runStartedAt,
372
454
  command,
373
455
  testExitCode: testResult.status,
374
456
  integrity: runIntegrity,
457
+ rawEvidence,
375
458
  isolatedBuild: true,
459
+ instrumentedBuildCache: {
460
+ key: buildCacheKey,
461
+ reused: Boolean(reusableBuild),
462
+ },
463
+ timings: roundedTimings(timings),
376
464
  }, null, 2)}\n`);
377
- atomicRenameSync(reportStagingDirectory, storedRunDirectory);
465
+ atomicRenameSync(runStagingDirectory, storedRunDirectory);
466
+ runPublished = true;
467
+ console.log(`[coverage] evidence: ${resolve(storedRunDirectory, "evidence.raw.gz")}`);
378
468
  }
379
469
  catch (error) {
380
- reportFailed = true;
381
- console.error("[supercov] failed to generate report", error);
470
+ timings.evidencePublicationMs = performance.now() - phaseStarted;
471
+ publicationFailed = true;
472
+ console.error("[supercov] failed to publish coverage evidence", error);
382
473
  }
383
474
  }
384
- const resultCode = exitCode(buildResult) || exitCode(testResult) || (reportFailed ? 1 : 0);
475
+ const resultCode = exitCode(buildResult) || exitCode(testResult) || (publicationFailed ? 1 : 0);
385
476
  updateRunState(root, runId, { status: resultCode === 0 ? "complete" : "failed" });
477
+ if (runPublished && !finalizePublishedRunStorage(root, runId))
478
+ throw new Error(`Published run ${runId} is missing a durable artifact`);
479
+ console.error(`[supercov] timings ${formatTimings(timings, performance.now() - runStartedMonotonic)}`);
480
+ timingsPrinted = true;
386
481
  return resultCode;
387
482
  }
388
483
  catch (error) {
@@ -403,6 +498,8 @@ async function createCoverageRun(command) {
403
498
  return receivedSignal ? signalExitCode(receivedSignal) : 1;
404
499
  }
405
500
  finally {
501
+ if (!timingsPrinted)
502
+ console.error(`[supercov] timings ${formatTimings(timings, performance.now() - runStartedMonotonic)}`);
406
503
  if (signalEscalation) {
407
504
  clearTimeout(signalEscalation);
408
505
  signalEscalation = undefined;
@@ -410,7 +507,11 @@ async function createCoverageRun(command) {
410
507
  for (const [signal, handler] of signalHandlers)
411
508
  process.removeListener(signal, handler);
412
509
  try {
413
- rmSync(reportStagingDirectory, { recursive: true, force: true });
510
+ rmSync(runStagingDirectory, { recursive: true, force: true });
511
+ rmSync(resolve(serverEvidenceRoot, runId), {
512
+ recursive: true,
513
+ force: true,
514
+ });
414
515
  // The stable isolated namespace is a deliberate build/snapshot cache.
415
516
  // It never overlaps the user's ordinary build and `supercov clean`
416
517
  // removes it deterministically.
@@ -426,9 +527,12 @@ const rawArgs = commandArgs[0] === "--help" || commandArgs[0] === "-h"
426
527
  ? ["help", ...commandArgs.slice(1)]
427
528
  : commandArgs;
428
529
  const queryCommand = rawArgs[0];
429
- if (queryCommand === "clean") {
530
+ if (queryCommand === "clean" || queryCommand === "prune") {
430
531
  try {
431
- cleanCommand(rawArgs.slice(1));
532
+ if (queryCommand === "clean")
533
+ cleanCommand(rawArgs.slice(1));
534
+ else
535
+ pruneCommand(rawArgs.slice(1));
432
536
  }
433
537
  catch (error) {
434
538
  console.error(`[supercov] ${error instanceof Error ? error.message : String(error)}`);