supercov 0.0.0 → 0.0.2

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.
Files changed (97) hide show
  1. package/bin/supercov.js +2 -5
  2. package/dist/analyze.d.ts +12 -0
  3. package/dist/analyze.d.ts.map +1 -0
  4. package/dist/analyze.js +699 -0
  5. package/dist/analyze.js.map +1 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +245 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/instrumenter.d.ts +11 -0
  11. package/dist/instrumenter.d.ts.map +1 -0
  12. package/dist/instrumenter.js +940 -0
  13. package/dist/instrumenter.js.map +1 -0
  14. package/dist/integrity.d.ts +10 -0
  15. package/dist/integrity.d.ts.map +1 -0
  16. package/dist/integrity.js +153 -0
  17. package/dist/integrity.js.map +1 -0
  18. package/dist/playwright.d.ts +13 -0
  19. package/dist/playwright.d.ts.map +1 -0
  20. package/dist/playwright.js +794 -0
  21. package/dist/playwright.js.map +1 -0
  22. package/dist/playwrightReporter.d.ts +6 -0
  23. package/dist/playwrightReporter.d.ts.map +1 -0
  24. package/dist/playwrightReporter.js +40 -0
  25. package/dist/playwrightReporter.js.map +1 -0
  26. package/dist/project.d.ts +11 -0
  27. package/dist/project.d.ts.map +1 -0
  28. package/dist/project.js +112 -0
  29. package/dist/project.js.map +1 -0
  30. package/dist/provenance.d.ts +9 -0
  31. package/dist/provenance.d.ts.map +1 -0
  32. package/dist/provenance.js +51 -0
  33. package/dist/provenance.js.map +1 -0
  34. package/dist/query.d.ts +9 -0
  35. package/dist/query.d.ts.map +1 -0
  36. package/dist/query.js +911 -0
  37. package/dist/query.js.map +1 -0
  38. package/dist/queueAdapters.d.ts +15 -0
  39. package/dist/queueAdapters.d.ts.map +1 -0
  40. package/dist/queueAdapters.js +55 -0
  41. package/dist/queueAdapters.js.map +1 -0
  42. package/dist/register.d.mts +2 -0
  43. package/dist/register.d.mts.map +1 -0
  44. package/dist/register.mjs +112 -0
  45. package/dist/register.mjs.map +1 -0
  46. package/dist/reporter.d.ts +17 -0
  47. package/dist/reporter.d.ts.map +1 -0
  48. package/dist/reporter.js +311 -0
  49. package/dist/reporter.js.map +1 -0
  50. package/dist/resolve-loader.d.mts +2 -0
  51. package/dist/resolve-loader.d.mts.map +1 -0
  52. package/dist/resolve-loader.mjs +40 -0
  53. package/dist/resolve-loader.mjs.map +1 -0
  54. package/dist/runtime.d.ts +46 -0
  55. package/dist/runtime.d.ts.map +1 -0
  56. package/dist/runtime.js +480 -0
  57. package/dist/runtime.js.map +1 -0
  58. package/dist/transport.d.ts +15 -0
  59. package/dist/transport.d.ts.map +1 -0
  60. package/dist/transport.js +100 -0
  61. package/dist/transport.js.map +1 -0
  62. package/dist/types.d.ts +353 -0
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/types.js +2 -0
  65. package/dist/types.js.map +1 -0
  66. package/dist/vitePlugin.d.ts +8 -0
  67. package/dist/vitePlugin.d.ts.map +1 -0
  68. package/dist/vitePlugin.js +83 -0
  69. package/dist/vitePlugin.js.map +1 -0
  70. package/dist/vitest.d.ts +2 -0
  71. package/dist/vitest.d.ts.map +1 -0
  72. package/dist/vitest.js +89 -0
  73. package/dist/vitest.js.map +1 -0
  74. package/dist/vitestReporter.d.ts +28 -0
  75. package/dist/vitestReporter.d.ts.map +1 -0
  76. package/dist/vitestReporter.js +48 -0
  77. package/dist/vitestReporter.js.map +1 -0
  78. package/package.json +21 -9
  79. package/src/analyze.ts +0 -993
  80. package/src/cli.ts +0 -336
  81. package/src/instrumenter.ts +0 -1254
  82. package/src/integrity.ts +0 -187
  83. package/src/playwright.ts +0 -1009
  84. package/src/playwrightReporter.ts +0 -55
  85. package/src/project.ts +0 -149
  86. package/src/provenance.ts +0 -69
  87. package/src/query.ts +0 -1354
  88. package/src/queueAdapters.ts +0 -104
  89. package/src/register.mjs +0 -123
  90. package/src/reporter.ts +0 -431
  91. package/src/resolve-loader.mjs +0 -45
  92. package/src/runtime.ts +0 -656
  93. package/src/transport.ts +0 -132
  94. package/src/types.ts +0 -412
  95. package/src/vitePlugin.ts +0 -121
  96. package/src/vitest.ts +0 -101
  97. package/src/vitestReporter.ts +0 -72
package/dist/query.js ADDED
@@ -0,0 +1,911 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { gunzipSync } from "node:zlib";
5
+ import { coverageSummaryForTests, isIndependencePair } from "./analyze.js";
6
+ import { compareRunIntegrity, createRunIntegrity } from "./integrity.js";
7
+ import { discoverCoverageProject } from "./project.js";
8
+ function parseOptions(args) {
9
+ const options = {
10
+ limit: 20,
11
+ offset: 0,
12
+ json: false,
13
+ filter: "all",
14
+ positional: [],
15
+ };
16
+ for (let index = 0; index < args.length; index += 1) {
17
+ const value = args[index];
18
+ if (value === "--json")
19
+ options.json = true;
20
+ else if (value === "--run")
21
+ options.run = args[++index];
22
+ else if (value === "--kind")
23
+ options.kind = args[++index]?.toLowerCase();
24
+ else if (value === "--runner")
25
+ options.runner = args[++index]?.toLowerCase();
26
+ else if (value === "--filter") {
27
+ const filter = args[++index]?.toLowerCase();
28
+ if (filter !== "all" && filter !== "passed" && filter !== "failed")
29
+ throw new Error("--filter must be all, passed, or failed");
30
+ options.filter = filter;
31
+ }
32
+ else if (value === "--limit")
33
+ options.limit = Math.max(1, Number(args[++index]) || 20);
34
+ else if (value === "--offset")
35
+ options.offset = Math.max(0, Number(args[++index]) || 0);
36
+ else if (value.startsWith("--"))
37
+ throw new Error(`Unknown option: ${value}`);
38
+ else
39
+ options.positional.push(value);
40
+ }
41
+ return options;
42
+ }
43
+ function filteredCoverage(report, options) {
44
+ if (options.filter === "all")
45
+ return report;
46
+ const filtered = report.filters?.[options.filter];
47
+ if (!filtered) {
48
+ throw new Error("This run does not contain outcome-filtered coverage. Create a new coverage run.");
49
+ }
50
+ return filtered;
51
+ }
52
+ function readJson(path) {
53
+ try {
54
+ const contents = readFileSync(path);
55
+ const text = path.endsWith(".gz")
56
+ ? gunzipSync(contents).toString("utf8")
57
+ : contents.toString("utf8");
58
+ return JSON.parse(text);
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ function discoverRuns(root) {
65
+ const runs = new Map();
66
+ const canonical = resolve(root, ".supercov/runs");
67
+ if (existsSync(canonical)) {
68
+ for (const entry of readdirSync(canonical, { withFileTypes: true })) {
69
+ if (!entry.isDirectory())
70
+ continue;
71
+ const reportPath = resolve(canonical, entry.name, "report.json.gz");
72
+ if (!existsSync(reportPath))
73
+ continue;
74
+ runs.set(entry.name, {
75
+ id: entry.name,
76
+ reportPath,
77
+ metadata: readJson(resolve(canonical, entry.name, "run.json")),
78
+ });
79
+ }
80
+ }
81
+ return [...runs.values()].sort((left, right) => right.id.localeCompare(left.id));
82
+ }
83
+ function selectRun(root, selector, currentIntegrity) {
84
+ const runs = discoverRuns(root);
85
+ if (runs.length === 0)
86
+ throw new Error("No local coverage runs. Run supercov first.");
87
+ const selected = !selector || selector === "latest"
88
+ ? runs[0]
89
+ : (runs.find((run) => run.id === selector) ??
90
+ runs.find((run) => run.id.startsWith(selector)));
91
+ if (!selected)
92
+ throw new Error(`Coverage run not found: ${selector}`);
93
+ const report = readJson(selected.reportPath);
94
+ if (!report)
95
+ throw new Error(`Cannot read ${selected.reportPath}`);
96
+ if (currentIntegrity) {
97
+ const comparison = compareRunIntegrity(selected.metadata?.integrity ?? report.integrity, currentIntegrity);
98
+ report.integrity = {
99
+ ...(selected.metadata?.integrity ?? report.integrity ?? currentIntegrity),
100
+ stale: comparison.stale,
101
+ staleReasons: comparison.reasons,
102
+ };
103
+ if (comparison.stale) {
104
+ console.error(`[supercov] stale run ${selected.id}: ${comparison.reasons.join(", ")}`);
105
+ }
106
+ }
107
+ return { run: selected, report };
108
+ }
109
+ function currentProjectIntegrity(root) {
110
+ try {
111
+ return createRunIntegrity(root, discoverCoverageProject(root), fileURLToPath(new URL(".", import.meta.url)));
112
+ }
113
+ catch {
114
+ return undefined;
115
+ }
116
+ }
117
+ function page(values, options) {
118
+ return values.slice(options.offset, options.offset + options.limit);
119
+ }
120
+ function output(value, options, text) {
121
+ console.log(options.json ? JSON.stringify(value, null, 2) : text);
122
+ }
123
+ function pct(value) {
124
+ return `${value.toFixed(2)}%`;
125
+ }
126
+ function shellQuote(value) {
127
+ return `'${value.replaceAll("'", `'\\''`)}'`;
128
+ }
129
+ function coverageCommand(runId, options, child) {
130
+ return [
131
+ "npx supercov runs",
132
+ shellQuote(runId),
133
+ "coverage",
134
+ child,
135
+ options.filter !== "all" ? `--filter ${options.filter}` : undefined,
136
+ options.kind ? `--kind ${shellQuote(options.kind)}` : undefined,
137
+ options.runner ? `--runner ${shellQuote(options.runner)}` : undefined,
138
+ ]
139
+ .filter(Boolean)
140
+ .join(" ");
141
+ }
142
+ function pageLabel(total, returned, options) {
143
+ const start = total === 0 || returned === 0 ? 0 : options.offset + 1;
144
+ const end = Math.min(options.offset + returned, total);
145
+ return `showing ${start}-${end} of ${total}`;
146
+ }
147
+ function nextPageCommand(base, total, returned, options) {
148
+ const nextOffset = options.offset + returned;
149
+ if (returned === 0 || nextOffset >= total)
150
+ return undefined;
151
+ return `${base} --offset ${nextOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`;
152
+ }
153
+ function selectedTestIds(report, options) {
154
+ if (!options.kind && !options.runner)
155
+ return undefined;
156
+ const selected = report.tests.filter((test) => (!options.kind || test.provenance.kind === options.kind) &&
157
+ (!options.runner || test.provenance.runner === options.runner));
158
+ if (selected.length === 0) {
159
+ const filter = [
160
+ options.kind ? `kind=${options.kind}` : undefined,
161
+ options.runner ? `runner=${options.runner}` : undefined,
162
+ ]
163
+ .filter(Boolean)
164
+ .join(", ");
165
+ throw new Error(`No tests match ${filter}`);
166
+ }
167
+ return new Set(selected.map((test) => test.id));
168
+ }
169
+ function includesSelectedTest(tests, selected) {
170
+ return selected ? tests.some((test) => selected.has(test)) : tests.length > 0;
171
+ }
172
+ function otherCoverage(report, testIds, selected) {
173
+ const tests = selected
174
+ ? testIds
175
+ .filter((id) => !selected.has(id))
176
+ .map((id) => report.tests.find((test) => test.id === id))
177
+ .filter((test) => Boolean(test))
178
+ : [];
179
+ return {
180
+ coveredElsewhere: tests.length > 0,
181
+ kinds: [...new Set(tests.map((test) => test.provenance.kind))].sort(),
182
+ runners: [...new Set(tests.map((test) => test.provenance.runner))].sort(),
183
+ tests: tests.map((test) => ({ id: test.id, name: test.name })),
184
+ };
185
+ }
186
+ function filterDecision(decision, selected) {
187
+ if (!selected)
188
+ return decision;
189
+ const vectorObservations = decision.vectorObservations
190
+ .map((observation) => ({
191
+ ...observation,
192
+ tests: observation.tests.filter((test) => selected.has(test)),
193
+ }))
194
+ .filter((observation) => observation.tests.length > 0);
195
+ const vectors = vectorObservations.map((observation) => observation.vector);
196
+ const conditions = decision.meta.conditions.map((source, index) => {
197
+ let witness;
198
+ for (let left = 0; left < vectors.length && !witness; left += 1) {
199
+ for (let right = left + 1; right < vectors.length; right += 1) {
200
+ const first = vectors[left];
201
+ const second = vectors[right];
202
+ if (isIndependencePair(first, second, index)) {
203
+ witness = [first, second];
204
+ break;
205
+ }
206
+ }
207
+ }
208
+ const witnessTests = witness?.map((vector) => vectorObservations.find((observation) => observation.vector === vector)
209
+ ?.tests ?? []);
210
+ return {
211
+ index,
212
+ source,
213
+ covered: Boolean(witness),
214
+ ...(witness ? { witness } : {}),
215
+ ...(witnessTests ? { witnessTests } : {}),
216
+ };
217
+ });
218
+ return {
219
+ ...decision,
220
+ executed: vectors.length > 0,
221
+ covered: conditions.every((condition) => condition.covered),
222
+ vectors,
223
+ vectorObservations,
224
+ conditions,
225
+ tests: decision.tests.filter((test) => selected.has(test)),
226
+ };
227
+ }
228
+ function filterLabel(options) {
229
+ return [
230
+ options.filter !== "all" ? `${options.filter} attempts only` : undefined,
231
+ options.kind ? `kind ${options.kind}` : undefined,
232
+ options.runner ? `runner ${options.runner}` : undefined,
233
+ ]
234
+ .filter(Boolean)
235
+ .join(", ");
236
+ }
237
+ function attribution(report, selected) {
238
+ const phases = selected
239
+ ? report.phases.filter((phase) => selected.has(phase.test))
240
+ : report.phases;
241
+ return {
242
+ browserExplicit: phases.reduce((sum, phase) => sum + phase.explicitBrowserEvents, 0),
243
+ browserFallback: phases.reduce((sum, phase) => sum + phase.inferredBrowserEvents, 0),
244
+ serverExplicit: phases.reduce((sum, phase) => sum + phase.explicitServerEvents, 0),
245
+ serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
246
+ };
247
+ }
248
+ function fileGaps(report, selected) {
249
+ const files = new Map();
250
+ const get = (file) => {
251
+ const existing = files.get(file);
252
+ if (existing)
253
+ return existing;
254
+ const created = {
255
+ file,
256
+ uncoveredLines: 0,
257
+ uncoveredStatements: 0,
258
+ uncoveredFunctions: 0,
259
+ missingBranches: 0,
260
+ missingMcdcConditions: 0,
261
+ coveredByOtherTests: {
262
+ lines: 0,
263
+ statements: 0,
264
+ functions: 0,
265
+ branches: 0,
266
+ mcdcConditions: 0,
267
+ },
268
+ uncoveredEverywhere: {
269
+ lines: 0,
270
+ statements: 0,
271
+ functions: 0,
272
+ branches: 0,
273
+ mcdcConditions: 0,
274
+ },
275
+ score: 0,
276
+ };
277
+ files.set(file, created);
278
+ return created;
279
+ };
280
+ const classify = (gap, dimension, coveredOverall) => {
281
+ if (selected && coveredOverall)
282
+ gap.coveredByOtherTests[dimension] += 1;
283
+ else
284
+ gap.uncoveredEverywhere[dimension] += 1;
285
+ };
286
+ for (const line of report.lines) {
287
+ const gap = get(line.file);
288
+ if (!includesSelectedTest(line.tests, selected)) {
289
+ gap.uncoveredLines += 1;
290
+ classify(gap, "lines", line.covered);
291
+ }
292
+ }
293
+ for (const point of report.points) {
294
+ const gap = get(point.meta.file);
295
+ if (!includesSelectedTest(point.tests, selected)) {
296
+ if (point.meta.kind === "function")
297
+ gap.uncoveredFunctions += 1;
298
+ else
299
+ gap.uncoveredStatements += 1;
300
+ classify(gap, point.meta.kind === "function" ? "functions" : "statements", point.covered);
301
+ }
302
+ }
303
+ for (const branch of report.branches) {
304
+ const gap = get(branch.meta.file);
305
+ for (const alternative of branch.alternatives) {
306
+ if (!includesSelectedTest(alternative.tests, selected)) {
307
+ gap.missingBranches += 1;
308
+ classify(gap, "branches", alternative.covered);
309
+ }
310
+ }
311
+ }
312
+ for (const decision of report.decisions) {
313
+ const gap = get(decision.meta.file);
314
+ const filteredConditions = filterDecision(decision, selected).conditions;
315
+ for (const condition of filteredConditions) {
316
+ if (!condition.covered) {
317
+ gap.missingMcdcConditions += 1;
318
+ classify(gap, "mcdcConditions", decision.conditions[condition.index]?.covered ?? false);
319
+ }
320
+ }
321
+ }
322
+ for (const gap of files.values()) {
323
+ gap.score =
324
+ gap.uncoveredLines +
325
+ gap.uncoveredFunctions * 2 +
326
+ gap.missingBranches * 2 +
327
+ gap.missingMcdcConditions * 3;
328
+ }
329
+ return [...files.values()].sort((left, right) => right.score - left.score || left.file.localeCompare(right.file));
330
+ }
331
+ function findFile(report, selector) {
332
+ const files = [...new Set(report.lines.map((line) => line.file))];
333
+ if (files.includes(selector))
334
+ return selector;
335
+ const matches = files.filter((file) => file.includes(selector));
336
+ if (matches.length === 1)
337
+ return matches[0];
338
+ if (matches.length === 0)
339
+ throw new Error(`Source file not found: ${selector}`);
340
+ throw new Error(`Ambiguous file selector: ${matches.join(", ")}`);
341
+ }
342
+ function locationSelector(selector) {
343
+ const match = /^(.*):(\d+)(?::\d+)?$/.exec(selector);
344
+ if (!match)
345
+ throw new Error("Expected <source-file>:<line>");
346
+ return { file: match[1], line: Number(match[2]) };
347
+ }
348
+ function vectorText(values, outcome) {
349
+ return `${values.map((value) => (value === null ? "-" : value ? "T" : "F")).join("")} -> ${outcome ? "T" : "F"}`;
350
+ }
351
+ function help() {
352
+ console.log(`Agent-oriented local coverage queries:
353
+ supercov runs [--limit N] [--json]
354
+ supercov runs <run-id> coverage [--filter all|passed|failed] [--kind e2e] [--runner playwright] [--json]
355
+ supercov runs <run-id> coverage kinds [--json]
356
+ supercov runs <run-id> coverage runners [--json]
357
+ supercov runs <run-id> coverage files [--filter all|passed|failed] [--limit N] [--offset N] [--json]
358
+ supercov runs <run-id> coverage gaps [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
359
+ supercov runs <run-id> coverage file <source-file> [--kind e2e] [--limit N] [--offset N] [--json]
360
+ supercov runs <run-id> coverage decision <id|source-file:line> [--kind e2e] [--json]
361
+ supercov runs <run-id> coverage covers <source-file:line> [--kind e2e] [--json]
362
+ supercov runs <run-id> coverage test <id|name-fragment> [--kind e2e] [--limit N] [--json]
363
+ supercov diff <older-run> <newer-run> [--limit N] [--json]
364
+
365
+ Use "latest" as <run-id> to query the newest local run.
366
+
367
+ Create a run with:
368
+ supercov -- <test command>`);
369
+ }
370
+ /** Resolve the instance-first coverage resource syntax. */
371
+ export function resolveCoverageQueryInvocation(command, args) {
372
+ if (command !== "runs")
373
+ return { command, args };
374
+ const runId = args[0];
375
+ if (!runId || runId.startsWith("-") || args[1] !== "coverage") {
376
+ return { command, args };
377
+ }
378
+ const childToken = args[2];
379
+ const hasChild = Boolean(childToken && !childToken.startsWith("-"));
380
+ const child = hasChild ? childToken : "summary";
381
+ const childArgs = args.slice(hasChild ? 3 : 2);
382
+ const coverageCommands = new Set([
383
+ "summary",
384
+ "kinds",
385
+ "runners",
386
+ "files",
387
+ "gaps",
388
+ "file",
389
+ "decision",
390
+ "covers",
391
+ "test",
392
+ ]);
393
+ if (!coverageCommands.has(child)) {
394
+ throw new Error(`Unknown coverage query: ${child}. Try supercov help.`);
395
+ }
396
+ return {
397
+ command: child,
398
+ args: ["--run", runId, ...childArgs],
399
+ };
400
+ }
401
+ export async function runQueryCommand(command, args, root = process.cwd()) {
402
+ const resolved = resolveCoverageQueryInvocation(command, args);
403
+ command = resolved.command;
404
+ const options = parseOptions(resolved.args);
405
+ if (command === "help")
406
+ return help();
407
+ const currentIntegrity = currentProjectIntegrity(root);
408
+ if (command === "runs") {
409
+ const availableRuns = discoverRuns(root);
410
+ const runs = page(availableRuns, options).map((run) => {
411
+ const storedReport = readJson(run.reportPath);
412
+ const report = storedReport
413
+ ? filteredCoverage(storedReport, options)
414
+ : undefined;
415
+ return {
416
+ id: run.id,
417
+ generatedAt: report?.generatedAt,
418
+ lines: report?.summary.lines.percentage,
419
+ branches: report?.summary.branches.percentage,
420
+ mcdc: report?.summary.conditionCoveragePct,
421
+ command: run.metadata?.command,
422
+ durationMs: run.metadata?.durationMs,
423
+ testExitCode: run.metadata?.testExitCode,
424
+ ...(currentIntegrity
425
+ ? compareRunIntegrity(run.metadata?.integrity, currentIntegrity)
426
+ : { stale: undefined, reasons: [] }),
427
+ };
428
+ });
429
+ const runsBase = `npx supercov runs${options.filter !== "all" ? ` --filter ${options.filter}` : ""}`;
430
+ const runsNext = nextPageCommand(runsBase, availableRuns.length, runs.length, options);
431
+ return output(runs, options, runs
432
+ .map((run) => `${run.id} lines ${pct(run.lines ?? 0)} branches ${pct(run.branches ?? 0)} MC/DC ${pct(run.mcdc ?? 0)}${run.stale ? ` STALE (${run.reasons.join(", ")})` : ""}`)
433
+ .join("\n") +
434
+ `\n${pageLabel(availableRuns.length, runs.length, options)}` +
435
+ (runsNext ? `\nnext page: ${runsNext}` : ""));
436
+ }
437
+ if (command === "diff") {
438
+ const [olderSelector, newerSelector] = options.positional;
439
+ if (!olderSelector || !newerSelector)
440
+ throw new Error("Usage: supercov diff <older-run> <newer-run>");
441
+ const olderSelected = selectRun(root, olderSelector, currentIntegrity);
442
+ const newerSelected = selectRun(root, newerSelector, currentIntegrity);
443
+ const older = {
444
+ ...olderSelected,
445
+ report: filteredCoverage(olderSelected.report, options),
446
+ };
447
+ const newer = {
448
+ ...newerSelected,
449
+ report: filteredCoverage(newerSelected.report, options),
450
+ };
451
+ const key = (file, line) => `${file}:${line}`;
452
+ const oldLines = new Set(older.report.lines
453
+ .filter((line) => line.covered)
454
+ .map((line) => key(line.file, line.line)));
455
+ const newLines = new Set(newer.report.lines
456
+ .filter((line) => line.covered)
457
+ .map((line) => key(line.file, line.line)));
458
+ const branchKeys = (report) => new Map(report.branches.flatMap((branch) => branch.alternatives
459
+ .filter((alternative) => alternative.covered)
460
+ .map((alternative) => [
461
+ `${branch.meta.id}:${alternative.id}`,
462
+ `${branch.meta.file}:${branch.meta.line} ${alternative.label}`,
463
+ ])));
464
+ const mcdcKeys = (report) => new Map(report.decisions.flatMap((decision) => decision.conditions
465
+ .filter((condition) => condition.covered)
466
+ .map((condition) => [
467
+ `${decision.meta.id}:c${condition.index}`,
468
+ `${decision.meta.file}:${decision.meta.line} C${condition.index + 1} ${condition.source}`,
469
+ ])));
470
+ const oldBranches = branchKeys(older.report);
471
+ const newBranches = branchKeys(newer.report);
472
+ const oldMcdc = mcdcKeys(older.report);
473
+ const newMcdc = mcdcKeys(newer.report);
474
+ const gainedLines = [...newLines]
475
+ .filter((line) => !oldLines.has(line))
476
+ .sort();
477
+ const lostLines = [...oldLines]
478
+ .filter((line) => !newLines.has(line))
479
+ .sort();
480
+ const gainedBranches = [...newBranches]
481
+ .filter(([id]) => !oldBranches.has(id))
482
+ .map(([, label]) => label)
483
+ .sort();
484
+ const lostBranches = [...oldBranches]
485
+ .filter(([id]) => !newBranches.has(id))
486
+ .map(([, label]) => label)
487
+ .sort();
488
+ const gainedMcdc = [...newMcdc]
489
+ .filter(([id]) => !oldMcdc.has(id))
490
+ .map(([, label]) => label)
491
+ .sort();
492
+ const lostMcdc = [...oldMcdc]
493
+ .filter(([id]) => !newMcdc.has(id))
494
+ .map(([, label]) => label)
495
+ .sort();
496
+ const result = {
497
+ older: older.run.id,
498
+ newer: newer.run.id,
499
+ delta: {
500
+ lines: Number((newer.report.summary.lines.percentage -
501
+ older.report.summary.lines.percentage).toFixed(2)),
502
+ branches: Number((newer.report.summary.branches.percentage -
503
+ older.report.summary.branches.percentage).toFixed(2)),
504
+ mcdc: Number((newer.report.summary.conditionCoveragePct -
505
+ older.report.summary.conditionCoveragePct).toFixed(2)),
506
+ },
507
+ gained: {
508
+ lineCount: gainedLines.length,
509
+ branchCount: gainedBranches.length,
510
+ mcdcCount: gainedMcdc.length,
511
+ lines: page(gainedLines, options),
512
+ branches: page(gainedBranches, options),
513
+ mcdc: page(gainedMcdc, options),
514
+ },
515
+ lost: {
516
+ lineCount: lostLines.length,
517
+ branchCount: lostBranches.length,
518
+ mcdcCount: lostMcdc.length,
519
+ lines: page(lostLines, options),
520
+ branches: page(lostBranches, options),
521
+ mcdc: page(lostMcdc, options),
522
+ },
523
+ };
524
+ const diffTotal = Math.max(gainedLines.length, gainedBranches.length, gainedMcdc.length, lostLines.length, lostBranches.length, lostMcdc.length);
525
+ const diffReturned = Math.max(result.gained.lines.length, result.gained.branches.length, result.gained.mcdc.length, result.lost.lines.length, result.lost.branches.length, result.lost.mcdc.length);
526
+ const diffBase = [
527
+ "npx supercov diff",
528
+ shellQuote(older.run.id),
529
+ shellQuote(newer.run.id),
530
+ options.filter !== "all" ? `--filter ${options.filter}` : undefined,
531
+ ]
532
+ .filter(Boolean)
533
+ .join(" ");
534
+ const diffNext = nextPageCommand(diffBase, diffTotal, diffReturned, options);
535
+ return output(result, options, `${older.run.id} -> ${newer.run.id}\nlines ${result.delta.lines >= 0 ? "+" : ""}${result.delta.lines}pp, branches ${result.delta.branches >= 0 ? "+" : ""}${result.delta.branches}pp, MC/DC ${result.delta.mcdc >= 0 ? "+" : ""}${result.delta.mcdc}pp\ngained: ${gainedLines.length} lines, ${gainedBranches.length} branches, ${gainedMcdc.length} MC/DC conditions\nlost: ${lostLines.length} lines, ${lostBranches.length} branches, ${lostMcdc.length} MC/DC conditions\n${result.gained.lines.map((line) => `+ line ${line}`).join("\n")}${result.gained.branches.length ? `\n${result.gained.branches.map((item) => `+ branch ${item}`).join("\n")}` : ""}${result.gained.mcdc.length ? `\n${result.gained.mcdc.map((item) => `+ MC/DC ${item}`).join("\n")}` : ""}\n${pageLabel(diffTotal, diffReturned, options)} per category${diffNext ? `\nnext page: ${diffNext}` : ""}`);
536
+ }
537
+ const selectedRun = selectRun(root, options.run, currentIntegrity);
538
+ const run = selectedRun.run;
539
+ const report = filteredCoverage(selectedRun.report, options);
540
+ const selectedTestSet = selectedTestIds(report, options);
541
+ if (command === "summary") {
542
+ const summary = selectedTestSet
543
+ ? coverageSummaryForTests(report, selectedTestSet)
544
+ : report.summary;
545
+ const gaps = fileGaps(report, selectedTestSet).filter((gap) => gap.score > 0);
546
+ const selectedTests = report.tests.filter((test) => !selectedTestSet || selectedTestSet.has(test.id));
547
+ const testCount = selectedTests.filter((test) => (test.role ?? "test") === "test").length;
548
+ const setupCount = selectedTests.filter((test) => test.role === "setup").length;
549
+ const testOutcomes = Object.fromEntries(["passed", "failed", "flaky", "skipped", "timedOut", "interrupted", "unknown"].map((outcome) => [
550
+ outcome,
551
+ selectedTests.filter((test) => test.role === "test" && test.outcome === outcome).length,
552
+ ]));
553
+ const result = {
554
+ run: run.id,
555
+ filter: options.filter,
556
+ ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
557
+ generatedAt: report.generatedAt,
558
+ valid: run.metadata?.testExitCode === 0,
559
+ stale: report.integrity?.stale ?? false,
560
+ staleReasons: report.integrity?.staleReasons ?? [],
561
+ structurallyComplete: summary.coverageComplete,
562
+ complete: options.filter === "passed" &&
563
+ run.metadata?.testExitCode === 0 &&
564
+ !report.integrity?.stale &&
565
+ summary.coverageComplete,
566
+ coverage: summary,
567
+ coverageByKind: report.coverageByKind,
568
+ coverageByRunner: report.coverageByRunner,
569
+ attribution: attribution(report, selectedTestSet),
570
+ ...(!selectedTestSet
571
+ ? {
572
+ confidence: {
573
+ lines: Object.fromEntries(["unexecuted", "executed", "action", "asserted"].map((level) => [
574
+ level,
575
+ report.lines.filter((line) => line.confidence?.level === level).length,
576
+ ])),
577
+ assertionCoveredMcdcConditions: report.decisions.reduce((total, decision) => total +
578
+ decision.conditions.filter((condition) => condition.assertionCovered).length, 0),
579
+ },
580
+ }
581
+ : {}),
582
+ filesWithGaps: gaps.length,
583
+ tests: testCount,
584
+ setups: setupCount,
585
+ testOutcomes,
586
+ };
587
+ return output(result, options, `run ${run.id}${filterLabel(options) ? ` (${filterLabel(options)})` : ""}${run.metadata?.testExitCode !== 0 ? ` [INVALID: test exit ${run.metadata?.testExitCode ?? "unknown"}]` : ""}${report.integrity?.stale ? ` [STALE: ${(report.integrity.staleReasons ?? []).join(", ")}]` : ""}\nlines ${pct(summary.lines.percentage)} (${summary.lines.covered}/${summary.lines.total})\nbranches ${pct(summary.branches.percentage)} (${summary.branches.covered}/${summary.branches.total})\nMC/DC ${pct(summary.conditionCoveragePct)} (${summary.coveredConditions}/${summary.conditions})${!selectedTestSet ? `\nconfidence: ${report.lines.filter((line) => line.confidence?.level === "asserted").length} asserted lines, ${report.lines.filter((line) => line.confidence?.level === "action").length} action-linked, ${report.lines.filter((line) => line.confidence?.level === "executed").length} execution-only; ${report.decisions.reduce((total, decision) => total + decision.conditions.filter((condition) => condition.assertionCovered).length, 0)} assertion-linked MC/DC conditions` : ""}\n${testCount} test(s)${setupCount ? ` + ${setupCount} setup scope(s)` : ""}; outcomes ${Object.entries(testOutcomes).filter(([, count]) => count > 0).map(([outcome, count]) => `${outcome}=${count}`).join(", ") || "none"}; ${gaps.length} file(s) have remaining obligations${(report.limitations?.length ?? 0) ? `; ${report.limitations.length} completeness blocker(s)` : ""}`);
588
+ }
589
+ if (command === "kinds" || command === "runners") {
590
+ const dimension = command === "kinds" ? report.coverageByKind : report.coverageByRunner;
591
+ const selectedDimension = page(dimension, options);
592
+ const dimensionNext = nextPageCommand(coverageCommand(run.id, options, command), dimension.length, selectedDimension.length, options);
593
+ return output({
594
+ run: run.id,
595
+ total: dimension.length,
596
+ offset: options.offset,
597
+ [command]: selectedDimension,
598
+ }, options, selectedDimension
599
+ .map((entry) => {
600
+ const name = entry.kind ?? entry.runner ?? "unknown";
601
+ return `${name} ${entry.tests} test(s)${entry.setups ? ` + ${entry.setups} setup scope(s)` : ""} lines ${pct(entry.summary.lines.percentage)} branches ${pct(entry.summary.branches.percentage)} MC/DC ${pct(entry.summary.conditionCoveragePct)}`;
602
+ })
603
+ .join("\n") +
604
+ `\n${pageLabel(dimension.length, selectedDimension.length, options)}` +
605
+ (dimensionNext ? `\nnext page: ${dimensionNext}` : ""));
606
+ }
607
+ if (command === "files" || command === "gaps") {
608
+ const files = fileGaps(report, selectedTestSet);
609
+ const all = command === "gaps" ? files.filter((gap) => gap.score > 0) : files;
610
+ const selectedFiles = page(all, options);
611
+ const pageStart = all.length === 0 ? 0 : options.offset + 1;
612
+ const pageEnd = Math.min(options.offset + selectedFiles.length, all.length);
613
+ const nextOffset = options.offset + selectedFiles.length;
614
+ const nextCommand = nextOffset < all.length
615
+ ? `${coverageCommand(run.id, options, command)} --offset ${nextOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
616
+ : undefined;
617
+ return output({
618
+ run: run.id,
619
+ ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
620
+ total: all.length,
621
+ offset: options.offset,
622
+ [command]: selectedFiles,
623
+ }, options, selectedFiles
624
+ .map((gap) => {
625
+ const status = gap.score === 0
626
+ ? "complete"
627
+ : `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}`;
628
+ return `${gap.file} ${status}${selectedTestSet ? ` [covered elsewhere: ${Object.values(gap.coveredByOtherTests).reduce((sum, value) => sum + value, 0)}; nowhere: ${Object.values(gap.uncoveredEverywhere).reduce((sum, value) => sum + value, 0)}]` : ""}`;
629
+ })
630
+ .join("\n") +
631
+ `\nshowing ${pageStart}-${pageEnd} of ${all.length}` +
632
+ (nextCommand ? `\nnext page: ${nextCommand}` : ""));
633
+ }
634
+ if (command === "file") {
635
+ const selector = options.positional.join(" ");
636
+ if (!selector)
637
+ throw new Error("Usage: supercov runs <run-id> coverage file <source-file>");
638
+ const file = findFile(report, selector);
639
+ const uncoveredLines = report.lines
640
+ .filter((line) => line.file === file &&
641
+ !includesSelectedTest(line.tests, selectedTestSet))
642
+ .map((line) => ({
643
+ kind: "line",
644
+ line: line.line,
645
+ otherCoverage: otherCoverage(report, line.tests, selectedTestSet),
646
+ }));
647
+ const functions = report.points
648
+ .filter((point) => point.meta.file === file &&
649
+ point.meta.kind === "function" &&
650
+ !includesSelectedTest(point.tests, selectedTestSet))
651
+ .map((point) => ({
652
+ kind: "function",
653
+ line: point.meta.line,
654
+ column: point.meta.column,
655
+ source: point.meta.label ?? point.meta.source,
656
+ otherCoverage: otherCoverage(report, point.tests, selectedTestSet),
657
+ }));
658
+ const statements = report.points
659
+ .filter((point) => point.meta.file === file &&
660
+ point.meta.kind === "statement" &&
661
+ !includesSelectedTest(point.tests, selectedTestSet))
662
+ .map((point) => ({
663
+ kind: "statement",
664
+ line: point.meta.line,
665
+ column: point.meta.column,
666
+ source: point.meta.label ?? point.meta.source,
667
+ otherCoverage: otherCoverage(report, point.tests, selectedTestSet),
668
+ }));
669
+ const branches = report.branches
670
+ .filter((branch) => branch.meta.file === file)
671
+ .flatMap((branch) => branch.alternatives
672
+ .filter((alternative) => !includesSelectedTest(alternative.tests, selectedTestSet))
673
+ .map((alternative) => ({
674
+ kind: "branch",
675
+ line: branch.meta.line,
676
+ column: branch.meta.column,
677
+ source: branch.meta.source,
678
+ missing: alternative.label,
679
+ otherCoverage: otherCoverage(report, alternative.tests, selectedTestSet),
680
+ })));
681
+ const mcdc = report.decisions
682
+ .filter((decision) => decision.meta.file === file)
683
+ .flatMap((decision) => filterDecision(decision, selectedTestSet)
684
+ .conditions.filter((condition) => !condition.covered)
685
+ .map((condition) => ({
686
+ kind: "mcdc",
687
+ id: decision.meta.id,
688
+ line: decision.meta.line,
689
+ column: decision.meta.column,
690
+ decision: decision.meta.source,
691
+ missingCondition: condition.source,
692
+ observedVectors: filterDecision(decision, selectedTestSet).vectorObservations.map((observation) => vectorText(observation.vector.values, observation.vector.outcome)),
693
+ otherCoverage: otherCoverage(report, (decision.conditions[condition.index]?.witnessTests ?? []).flat(), selectedTestSet),
694
+ })));
695
+ const obligations = [
696
+ ...uncoveredLines,
697
+ ...statements,
698
+ ...functions,
699
+ ...branches,
700
+ ...mcdc,
701
+ ].sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
702
+ const allFileTests = report.tests
703
+ .filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
704
+ test.lines.some((line) => line.file === file))
705
+ .map((test) => ({
706
+ id: test.id,
707
+ name: test.name,
708
+ provenance: test.provenance,
709
+ }));
710
+ const tests = page(allFileTests, options);
711
+ const selected = page(obligations, options);
712
+ const filePageTotal = Math.max(obligations.length, allFileTests.length);
713
+ const filePageReturned = Math.max(selected.length, tests.length);
714
+ const nextFileOffset = options.offset + filePageReturned;
715
+ const nextFileCommand = filePageReturned > 0 && nextFileOffset < filePageTotal
716
+ ? `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --offset ${nextFileOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
717
+ : undefined;
718
+ const result = {
719
+ run: run.id,
720
+ ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
721
+ file,
722
+ counts: {
723
+ uncoveredLines: uncoveredLines.length,
724
+ uncoveredStatements: statements.length,
725
+ uncoveredFunctions: functions.length,
726
+ missingBranches: branches.length,
727
+ missingMcdcConditions: mcdc.length,
728
+ },
729
+ tests,
730
+ totalTests: allFileTests.length,
731
+ totalObligations: obligations.length,
732
+ offset: options.offset,
733
+ obligations: selected,
734
+ };
735
+ return output(result, options, `${file}\nlines ${uncoveredLines.length}, statements ${statements.length}, functions ${functions.length}, branches ${branches.length}, MC/DC ${mcdc.length}\ncovered by ${allFileTests.length} test(s)\n${selected
736
+ .map((item) => item.kind === "line"
737
+ ? `line ${item.line}: ${item.otherCoverage.coveredElsewhere ? `covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}` : "uncovered everywhere"}`
738
+ : item.kind === "statement"
739
+ ? `statement ${item.line}:${item.column}: ${item.source}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
740
+ : item.kind === "function"
741
+ ? `function ${item.line}:${item.column}: ${item.source}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
742
+ : item.kind === "branch"
743
+ ? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
744
+ : `MC/DC ${item.line}:${item.column} [${item.id}]: ${item.missingCondition}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`)
745
+ .join("\n")}\n${pageLabel(filePageTotal, filePageReturned, options)} obligations/tests${nextFileCommand ? `\nnext page: ${nextFileCommand}` : ""}`);
746
+ }
747
+ if (command === "decision") {
748
+ const selector = options.positional[0];
749
+ if (!selector)
750
+ throw new Error("Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
751
+ let matches = report.decisions.filter((decision) => decision.meta.id === selector);
752
+ if (matches.length === 0 && /:\d+(?::\d+)?$/.test(selector)) {
753
+ const location = locationSelector(selector);
754
+ matches = report.decisions.filter((decision) => decision.meta.file === location.file &&
755
+ decision.meta.line === location.line);
756
+ }
757
+ if (matches.length === 0)
758
+ throw new Error(`Decision not found: ${selector}`);
759
+ if (matches.length > 1) {
760
+ const matchingDecisions = page(matches, options).map((decision) => ({
761
+ id: decision.meta.id,
762
+ file: decision.meta.file,
763
+ line: decision.meta.line,
764
+ column: decision.meta.column,
765
+ source: decision.meta.source,
766
+ }));
767
+ const matchesNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, matches.length, matchingDecisions.length, options);
768
+ return output({
769
+ run: run.id,
770
+ total: matches.length,
771
+ offset: options.offset,
772
+ decisions: matchingDecisions,
773
+ }, options, `${matchingDecisions.map((decision) => `${decision.id} ${decision.file}:${decision.line}:${decision.column} ${decision.source}`).join("\n")}\n${pageLabel(matches.length, matchingDecisions.length, options)} matching decisions${matchesNext ? `\nnext page: ${matchesNext}` : ""}`);
774
+ }
775
+ matches = matches.map((decision) => filterDecision(decision, selectedTestSet));
776
+ const totalDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
777
+ matches = matches.map((decision) => {
778
+ const vectorObservations = page(decision.vectorObservations, options);
779
+ return {
780
+ ...decision,
781
+ vectors: vectorObservations.map((observation) => observation.vector),
782
+ vectorObservations,
783
+ conditions: page(decision.conditions, options),
784
+ tests: page(decision.tests, options),
785
+ };
786
+ });
787
+ const returnedDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
788
+ const decisionNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, totalDecisionEvidence, returnedDecisionEvidence, options);
789
+ const result = {
790
+ run: run.id,
791
+ ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
792
+ decisions: matches,
793
+ };
794
+ return output(result, options, matches
795
+ .map((decision) => `${decision.meta.id} ${decision.meta.file}:${decision.meta.line}:${decision.meta.column}\n${decision.meta.source}\n${decision.conditions
796
+ .map((condition) => `C${condition.index + 1} ${condition.covered ? "covered" : "MISSING"}${condition.assertionCovered ? " + asserted" : ""}: ${condition.source}`)
797
+ .join("\n")}\nconfidence ${decision.confidence?.level ?? "unknown"}; asserted MC/DC ${decision.conditions.filter((condition) => condition.assertionCovered).length}/${decision.conditions.length}\nvectors:\n${decision.vectorObservations.map((observation) => ` ${vectorText(observation.vector.values, observation.vector.outcome)} tests=${observation.tests.length} confidence=${observation.confidence?.level ?? "unknown"}`).join("\n") || " none"}`)
798
+ .join("\n\n") +
799
+ `\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
800
+ (decisionNext ? `\nnext page: ${decisionNext}` : ""));
801
+ }
802
+ if (command === "covers") {
803
+ const selector = options.positional[0];
804
+ if (!selector)
805
+ throw new Error("Usage: supercov runs <run-id> coverage covers <source-file:line>");
806
+ const location = locationSelector(selector);
807
+ const line = report.lines.find((candidate) => candidate.file === location.file && candidate.line === location.line);
808
+ const allTests = (line?.tests ?? [])
809
+ .filter((id) => !selectedTestSet || selectedTestSet.has(id))
810
+ .map((id) => {
811
+ const test = report.tests.find((candidate) => candidate.id === id);
812
+ return {
813
+ id,
814
+ name: test?.name ?? id,
815
+ provenance: test?.provenance,
816
+ };
817
+ });
818
+ const allPhases = (line?.phases ?? [])
819
+ .map((id) => report.phases.find((candidate) => candidate.id === id))
820
+ .filter((phase) => Boolean(phase) &&
821
+ (!selectedTestSet || selectedTestSet.has(phase.test)))
822
+ .map((phase) => ({
823
+ id: phase.id,
824
+ kind: phase.kind,
825
+ operation: phase.operation,
826
+ source: phase.source,
827
+ test: phase.test,
828
+ status: phase.status,
829
+ causedByPhaseId: phase.causedByPhaseId,
830
+ }));
831
+ const tests = page(allTests, options);
832
+ const phases = page(allPhases, options);
833
+ const coversTotal = Math.max(allTests.length, allPhases.length);
834
+ const coversReturned = Math.max(tests.length, phases.length);
835
+ const coversNext = nextPageCommand(`${coverageCommand(run.id, options, "covers")} ${shellQuote(selector)}`, coversTotal, coversReturned, options);
836
+ const result = {
837
+ run: run.id,
838
+ ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
839
+ location,
840
+ covered: includesSelectedTest(line?.tests ?? [], selectedTestSet),
841
+ confidence: line?.confidence,
842
+ totalTests: allTests.length,
843
+ totalPhases: allPhases.length,
844
+ tests,
845
+ phases,
846
+ };
847
+ return output(result, options, `${location.file}:${location.line} ${result.covered ? "covered" : "uncovered"}; confidence ${result.confidence?.level ?? "unknown"}${result.confidence?.e2e ? "; E2E-covered" : ""}\n${tests.map((test) => `test: ${test.name} [${test.id}] (${test.provenance?.kind ?? "unknown"}/${test.provenance?.runner ?? "unknown"})`).join("\n") || "no covering tests"}\n${phases.map((phase) => `phase: ${phase.operation}${phase.status ? ` (${phase.status})` : ""}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(coversTotal, coversReturned, options)} tests/phases${coversNext ? `\nnext page: ${coversNext}` : ""}`);
848
+ }
849
+ if (command === "test") {
850
+ const selector = options.positional.join(" ").toLowerCase();
851
+ if (!selector)
852
+ throw new Error("Usage: supercov runs <run-id> coverage test <id|name-fragment>");
853
+ const matches = report.tests.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
854
+ (test.id === selector || test.name.toLowerCase().includes(selector)));
855
+ if (matches.length === 0)
856
+ throw new Error(`Test not found: ${selector}`);
857
+ const testBase = `${coverageCommand(run.id, options, "test")} ${shellQuote(options.positional.join(" "))}`;
858
+ if (matches.length > 1) {
859
+ const matchingTests = page(matches, options).map((test) => ({
860
+ id: test.id,
861
+ name: test.name,
862
+ outcome: test.outcome,
863
+ provenance: test.provenance,
864
+ }));
865
+ const matchesNext = nextPageCommand(testBase, matches.length, matchingTests.length, options);
866
+ return output({
867
+ run: run.id,
868
+ total: matches.length,
869
+ offset: options.offset,
870
+ tests: matchingTests,
871
+ }, options, `${matchingTests.map((test) => `${test.name} [${test.id}] — ${test.outcome}`).join("\n")}\n${pageLabel(matches.length, matchingTests.length, options)} matching tests${matchesNext ? `\nnext page: ${matchesNext}` : ""}`);
872
+ }
873
+ const test = matches[0];
874
+ const allPhases = report.phases
875
+ .filter((phase) => phase.test === test.id)
876
+ .map((phase) => ({
877
+ id: phase.id,
878
+ kind: phase.kind,
879
+ operation: phase.operation,
880
+ source: phase.source,
881
+ status: phase.status,
882
+ causedByPhaseId: phase.causedByPhaseId,
883
+ lines: phase.lines.length,
884
+ decisions: phase.decisions.reduce((sum, decision) => sum + decision.vectors.length, 0),
885
+ }));
886
+ const testTotal = Math.max(test.lines.length, test.hits.length, test.decisions.length, allPhases.length);
887
+ const selected = {
888
+ ...test,
889
+ lines: page(test.lines, options),
890
+ hits: page(test.hits, options),
891
+ decisions: page(test.decisions, options),
892
+ phases: page(allPhases, options),
893
+ totals: {
894
+ lines: test.lines.length,
895
+ hits: test.hits.length,
896
+ decisions: test.decisions.length,
897
+ phases: allPhases.length,
898
+ },
899
+ };
900
+ const testReturned = Math.max(selected.lines.length, selected.hits.length, selected.decisions.length, selected.phases.length);
901
+ const testNext = nextPageCommand(testBase, testTotal, testReturned, options);
902
+ return output({
903
+ run: run.id,
904
+ ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
905
+ tests: [selected],
906
+ }, options, `${selected.name}\noutcome ${selected.outcome}${selected.attempts.length ? `; ${selected.attempts.map((attempt) => `retry ${attempt.retry}=${attempt.status}`).join(", ")}` : ""}\n${selected.totals.lines} lines, ${selected.totals.hits} hits, ${selected.totals.decisions} decisions, ${selected.totals.phases} phases\n${selected.lines.map((line) => `line: ${line.file}:${line.line}`).join("\n")}${selected.lines.length && selected.phases.length ? "\n" : ""}${selected.phases.map((phase) => `${phase.kind}: ${phase.operation}${phase.source ? ` at ${phase.source}` : ""}`).join("\n")}\n${pageLabel(testTotal, testReturned, options)} per evidence category${testNext ? `\nnext page: ${testNext}` : ""}`);
907
+ }
908
+ throw new Error(`Unknown coverage query: ${command}. Try supercov help.`);
909
+ }
910
+ export const coverageQueryCommands = new Set(["help", "runs", "diff"]);
911
+ //# sourceMappingURL=query.js.map