supercov 0.0.5 → 0.0.6

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/query.js CHANGED
@@ -3,11 +3,15 @@ import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { coverageSummaryForTests, isIndependencePair } from "./analyze.js";
5
5
  import { EVIDENCE_ARCHIVE_SCHEMA_VERSION } from "./evidenceArchive.js";
6
- import { analyzeCoverageArchive } from "./runAnalysis.js";
6
+ import { analyzeCoverageArchiveCached, readCoverageQueryIndex, } from "./queryCache.js";
7
7
  import { compareRunIntegrity, createRunIntegrity } from "./integrity.js";
8
8
  import { discoverCoverageProject } from "./project.js";
9
- function parseOptions(args) {
9
+ import { agentPagination, agentSuccessJson, SupercovError, } from "./agentJson.js";
10
+ function parseOptions(command, args) {
10
11
  const options = {
12
+ command: command === "runs" || command === "diff" || command === "help"
13
+ ? command
14
+ : `coverage.${command}`,
11
15
  limit: 20,
12
16
  offset: 0,
13
17
  json: false,
@@ -20,36 +24,58 @@ function parseOptions(args) {
20
24
  const value = args[index];
21
25
  if (value === "--json")
22
26
  options.json = true;
23
- else if (value === "--run")
24
- options.run = args[++index];
25
- else if (value === "--kind")
26
- options.kind = args[++index]?.toLowerCase();
27
- else if (value === "--runner")
28
- options.runner = args[++index]?.toLowerCase();
27
+ else if (value === "--run") {
28
+ const run = args[++index];
29
+ if (!run)
30
+ throw new SupercovError("INVALID_ARGUMENT", "--run requires a run ID");
31
+ options.run = run;
32
+ }
33
+ else if (value === "--kind") {
34
+ const kind = args[++index]?.toLowerCase();
35
+ if (!kind)
36
+ throw new SupercovError("INVALID_ARGUMENT", "--kind requires a test kind");
37
+ options.kind = kind;
38
+ }
39
+ else if (value === "--runner") {
40
+ const runner = args[++index]?.toLowerCase();
41
+ if (!runner)
42
+ throw new SupercovError("INVALID_ARGUMENT", "--runner requires a runner name");
43
+ options.runner = runner;
44
+ }
29
45
  else if (value === "--target") {
30
46
  const target = Number(args[++index]);
31
47
  if (!Number.isFinite(target) || target < 0 || target > 100)
32
- throw new Error("--target must be between 0 and 100");
48
+ throw new SupercovError("INVALID_ARGUMENT", "--target must be between 0 and 100");
33
49
  options.target = target;
34
50
  }
35
51
  else if (value === "--metric") {
36
52
  const metric = args[++index]?.toLowerCase();
37
53
  if (!metric || !["all", "lines", "statements", "functions", "branches", "mcdc"].includes(metric))
38
- throw new Error("--metric must be all, lines, statements, functions, branches, or mcdc");
54
+ throw new SupercovError("INVALID_ARGUMENT", "--metric must be all, lines, statements, functions, branches, or mcdc");
39
55
  options.metric = metric;
40
56
  }
41
57
  else if (value === "--filter") {
42
58
  const filter = args[++index]?.toLowerCase();
43
59
  if (filter !== "all" && filter !== "passed" && filter !== "failed")
44
- throw new Error("--filter must be all, passed, or failed");
60
+ throw new SupercovError("INVALID_ARGUMENT", "--filter must be all, passed, or failed");
45
61
  options.filter = filter;
46
62
  }
47
- else if (value === "--limit")
48
- options.limit = Math.max(1, Number(args[++index]) || 20);
49
- else if (value === "--offset")
50
- options.offset = Math.max(0, Number(args[++index]) || 0);
63
+ else if (value === "--limit") {
64
+ const limit = Number(args[++index]);
65
+ if (!Number.isSafeInteger(limit) || limit < 1)
66
+ throw new SupercovError("INVALID_ARGUMENT", "--limit must be a positive integer");
67
+ options.limit = limit;
68
+ }
69
+ else if (value === "--offset") {
70
+ const offset = Number(args[++index]);
71
+ if (!Number.isSafeInteger(offset) || offset < 0)
72
+ throw new SupercovError("INVALID_ARGUMENT", "--offset must be a non-negative integer");
73
+ options.offset = offset;
74
+ }
51
75
  else if (value.startsWith("--"))
52
- throw new Error(`Unknown option: ${value}`);
76
+ throw new SupercovError("INVALID_ARGUMENT", `Unknown option: ${value}`, {
77
+ details: { option: value },
78
+ });
53
79
  else
54
80
  options.positional.push(value);
55
81
  }
@@ -60,7 +86,7 @@ function filteredCoverage(report, options) {
60
86
  return report;
61
87
  const filtered = report.filters?.[options.filter];
62
88
  if (!filtered) {
63
- throw new Error("This run does not contain outcome-filtered coverage. Create a new coverage run.");
89
+ throw new SupercovError("FILTER_UNAVAILABLE", "This run does not contain outcome-filtered coverage. Create a new coverage run.");
64
90
  }
65
91
  return filtered;
66
92
  }
@@ -94,24 +120,32 @@ function discoverRuns(root) {
94
120
  }
95
121
  return [...runs.values()].sort((left, right) => right.id.localeCompare(left.id));
96
122
  }
97
- function analyzeStoredRun(run) {
98
- return analyzeCoverageArchive(run.evidencePath, {
123
+ function storedRunAnalysisOptions(run) {
124
+ return {
99
125
  runId: run.id,
100
126
  testExitCode: run.metadata?.testExitCode,
101
127
  integrity: run.metadata?.integrity,
102
128
  generatedAt: run.metadata?.startedAt,
103
- });
129
+ };
130
+ }
131
+ function analyzeStoredRun(run) {
132
+ return analyzeCoverageArchiveCached(run.evidencePath, storedRunAnalysisOptions(run));
133
+ }
134
+ function readStoredRunIndex(run) {
135
+ return readCoverageQueryIndex(run.evidencePath, storedRunAnalysisOptions(run));
104
136
  }
105
- function selectRun(root, selector, currentIntegrity) {
137
+ function selectRun(root, selector, currentIntegrity, quiet = false) {
106
138
  const runs = discoverRuns(root);
107
139
  if (runs.length === 0)
108
- throw new Error("No local coverage runs. Run supercov first.");
140
+ throw new SupercovError("NO_RUNS", "No local coverage runs. Run supercov first.");
109
141
  const selected = !selector || selector === "latest"
110
142
  ? runs[0]
111
143
  : (runs.find((run) => run.id === selector) ??
112
144
  runs.find((run) => run.id.startsWith(selector)));
113
145
  if (!selected)
114
- throw new Error(`Coverage run not found: ${selector}`);
146
+ throw new SupercovError("RUN_NOT_FOUND", `Coverage run not found: ${selector}`, {
147
+ details: { selector },
148
+ });
115
149
  const report = analyzeStoredRun(selected);
116
150
  if (currentIntegrity) {
117
151
  const comparison = compareRunIntegrity(selected.metadata?.integrity ?? report.integrity, currentIntegrity);
@@ -120,7 +154,7 @@ function selectRun(root, selector, currentIntegrity) {
120
154
  stale: comparison.stale,
121
155
  staleReasons: comparison.reasons,
122
156
  };
123
- if (comparison.stale) {
157
+ if (comparison.stale && !quiet) {
124
158
  console.error(`[supercov] stale run ${selected.id}: ${comparison.reasons.join(", ")}`);
125
159
  }
126
160
  }
@@ -137,8 +171,11 @@ function currentProjectIntegrity(root) {
137
171
  function page(values, options) {
138
172
  return values.slice(options.offset, options.offset + options.limit);
139
173
  }
140
- function output(value, options, text) {
141
- console.log(options.json ? JSON.stringify(value, null, 2) : text);
174
+ function output(value, options, text, pagination) {
175
+ process.stdout.write(options.json ? agentSuccessJson(options.command, value, pagination) : `${text}\n`);
176
+ }
177
+ function queryPagination(total, returned, options) {
178
+ return agentPagination(options.offset, options.limit, returned, total);
142
179
  }
143
180
  function pct(value) {
144
181
  return `${value.toFixed(2)}%`;
@@ -238,7 +275,7 @@ export function minimumTestSet(report, target = 100, metric = "all") {
238
275
  const unattributed = report.tests.filter((test) => test.role === "background" &&
239
276
  (test.hits.length > 0 || test.decisions.some((decision) => decision.vectors.length > 0)));
240
277
  if (unattributed.length > 0) {
241
- throw new Error("Cannot minimize exactly: this coverage view contains background/unattributed evidence. Use a runner with exact test attribution or select a fully attributed coverage view.");
278
+ throw new SupercovError("UNATTRIBUTED_EVIDENCE", "Cannot minimize exactly: this coverage view contains background/unattributed evidence. Use a runner with exact test attribution or select a fully attributed coverage view.");
242
279
  }
243
280
  const candidateTests = report.tests
244
281
  .filter((test) => test.role === "test")
@@ -265,7 +302,7 @@ export function minimumTestSet(report, target = 100, metric = "all") {
265
302
  const percentage = (selectedMetric, summary) => selectedMetric === "mcdc" ? summary.conditionCoveragePct : summary[selectedMetric].percentage;
266
303
  const impossible = metrics.find((selectedMetric) => percentage(selectedMetric, fullSummary) + 1e-9 < target);
267
304
  if (impossible)
268
- throw new Error(`The full selected test view reaches only ${percentage(impossible, fullSummary).toFixed(2)}% ${impossible}; target ${target}% is impossible`);
305
+ throw new SupercovError("TARGET_UNREACHABLE", `The full selected test view reaches only ${percentage(impossible, fullSummary).toFixed(2)}% ${impossible}; target ${target}% is impossible`, { details: { metric: impossible, target, reachable: percentage(impossible, fullSummary) } });
269
306
  let exploredStates = 0;
270
307
  const seen = new Set();
271
308
  const search = (selected, skipped, skippedByMetric) => {
@@ -351,7 +388,9 @@ function selectedTestIds(report, options) {
351
388
  ]
352
389
  .filter(Boolean)
353
390
  .join(", ");
354
- throw new Error(`No tests match ${filter}`);
391
+ throw new SupercovError("TEST_FILTER_EMPTY", `No tests match ${filter}`, {
392
+ details: { kind: options.kind, runner: options.runner },
393
+ });
355
394
  }
356
395
  return new Set(selected.map((test) => test.id));
357
396
  }
@@ -423,6 +462,13 @@ function filterLabel(options) {
423
462
  .filter(Boolean)
424
463
  .join(", ");
425
464
  }
465
+ function queryFilters(options) {
466
+ return {
467
+ outcome: options.filter,
468
+ kind: options.kind ?? null,
469
+ runner: options.runner ?? null,
470
+ };
471
+ }
426
472
  function attribution(report, selected) {
427
473
  const phases = selected
428
474
  ? report.phases.filter((phase) => selected.has(phase.test))
@@ -434,7 +480,7 @@ function attribution(report, selected) {
434
480
  serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
435
481
  };
436
482
  }
437
- function fileGaps(report, selected) {
483
+ export function fileGaps(report, selected) {
438
484
  const files = new Map();
439
485
  const get = (file) => {
440
486
  const existing = files.get(file);
@@ -447,6 +493,8 @@ function fileGaps(report, selected) {
447
493
  uncoveredFunctions: 0,
448
494
  missingBranches: 0,
449
495
  missingMcdcConditions: 0,
496
+ measurementLimitations: 0,
497
+ limitationKinds: [],
450
498
  coveredByOtherTests: {
451
499
  lines: 0,
452
500
  statements: 0,
@@ -508,37 +556,73 @@ function fileGaps(report, selected) {
508
556
  }
509
557
  }
510
558
  }
559
+ for (const limitation of report.limitations ?? []) {
560
+ const gap = get(limitation.file);
561
+ gap.measurementLimitations += 1;
562
+ if (!gap.limitationKinds.includes(limitation.kind))
563
+ gap.limitationKinds.push(limitation.kind);
564
+ }
511
565
  for (const gap of files.values()) {
566
+ gap.limitationKinds.sort();
512
567
  gap.score =
513
568
  gap.uncoveredLines +
514
569
  gap.uncoveredFunctions * 2 +
515
570
  gap.missingBranches * 2 +
516
- gap.missingMcdcConditions * 3;
571
+ gap.missingMcdcConditions * 3 +
572
+ gap.measurementLimitations * 3;
517
573
  }
518
574
  return [...files.values()].sort((left, right) => right.score - left.score || left.file.localeCompare(right.file));
519
575
  }
520
576
  function findFile(report, selector) {
521
- const files = [...new Set(report.lines.map((line) => line.file))];
577
+ const files = [
578
+ ...new Set([
579
+ ...report.lines.map((line) => line.file),
580
+ ...(report.limitations ?? []).map((limitation) => limitation.file),
581
+ ]),
582
+ ];
522
583
  if (files.includes(selector))
523
584
  return selector;
524
585
  const matches = files.filter((file) => file.includes(selector));
525
586
  if (matches.length === 1)
526
587
  return matches[0];
527
588
  if (matches.length === 0)
528
- throw new Error(`Source file not found: ${selector}`);
529
- throw new Error(`Ambiguous file selector: ${matches.join(", ")}`);
589
+ throw new SupercovError("SOURCE_NOT_FOUND", `Source file not found: ${selector}`, {
590
+ details: { selector },
591
+ });
592
+ throw new SupercovError("AMBIGUOUS_SELECTOR", `Ambiguous file selector: ${matches.join(", ")}`, {
593
+ details: { selector, matches },
594
+ });
595
+ }
596
+ export function coverageMeasurement(report) {
597
+ const limitations = report.limitations ?? [];
598
+ const byKind = {
599
+ "dynamic-code": 0,
600
+ "semantic-safety": 0,
601
+ "source-scope": 0,
602
+ };
603
+ for (const limitation of limitations)
604
+ byKind[limitation.kind] += 1;
605
+ return {
606
+ complete: limitations.length === 0,
607
+ limitations: limitations.length,
608
+ // Every current limitation removes source from the measured denominator.
609
+ blocking: limitations.length,
610
+ files: new Set(limitations.map((limitation) => limitation.file)).size,
611
+ byKind,
612
+ };
530
613
  }
531
614
  function locationSelector(selector) {
532
615
  const match = /^(.*):(\d+)(?::\d+)?$/.exec(selector);
533
616
  if (!match)
534
- throw new Error("Expected <source-file>:<line>");
617
+ throw new SupercovError("INVALID_ARGUMENT", "Expected <source-file>:<line>", {
618
+ details: { selector },
619
+ });
535
620
  return { file: match[1], line: Number(match[2]) };
536
621
  }
537
622
  function vectorText(values, outcome) {
538
623
  return `${values.map((value) => (value === null ? "-" : value ? "T" : "F")).join("")} -> ${outcome ? "T" : "F"}`;
539
624
  }
540
- function help() {
541
- console.log(`Agent-oriented local coverage queries:
625
+ const helpText = `Agent-oriented local coverage queries:
542
626
  supercov runs [--limit N] [--json]
543
627
  supercov runs <run-id> coverage [--filter all|passed|failed] [--kind e2e] [--runner playwright] [--json]
544
628
  supercov runs <run-id> coverage kinds [--json]
@@ -559,7 +643,30 @@ function help() {
559
643
  Use "latest" as <run-id> to query the newest local run.
560
644
 
561
645
  Create a run with:
562
- supercov -- <test command>`);
646
+ supercov -- <test command>`;
647
+ function help(options) {
648
+ if (options.json) {
649
+ return output({
650
+ usage: "supercov -- <test command>",
651
+ runSelector: "Use latest as <run-id> to query the newest local run.",
652
+ queryCommands: [
653
+ "runs",
654
+ "runs <run-id> coverage",
655
+ "runs <run-id> coverage kinds",
656
+ "runs <run-id> coverage runners",
657
+ "runs <run-id> coverage scope",
658
+ "runs <run-id> coverage files",
659
+ "runs <run-id> coverage gaps",
660
+ "runs <run-id> coverage file <source-file>",
661
+ "runs <run-id> coverage decision <id|source-file:line>",
662
+ "runs <run-id> coverage covers <source-file:line>",
663
+ "runs <run-id> coverage test <id|name-fragment>",
664
+ "runs <run-id> coverage minimize",
665
+ "diff <older-run> <newer-run>",
666
+ ],
667
+ }, options, helpText);
668
+ }
669
+ console.log(helpText);
563
670
  }
564
671
  /** Resolve the instance-first coverage resource syntax. */
565
672
  export function resolveCoverageQueryInvocation(command, args) {
@@ -587,7 +694,7 @@ export function resolveCoverageQueryInvocation(command, args) {
587
694
  "minimize",
588
695
  ]);
589
696
  if (!coverageCommands.has(child)) {
590
- throw new Error(`Unknown coverage query: ${child}. Try supercov help.`);
697
+ throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${child}. Try supercov help.`, { details: { command: child } });
591
698
  }
592
699
  return {
593
700
  command: child,
@@ -597,20 +704,22 @@ export function resolveCoverageQueryInvocation(command, args) {
597
704
  export async function runQueryCommand(command, args, root = process.cwd()) {
598
705
  const resolved = resolveCoverageQueryInvocation(command, args);
599
706
  command = resolved.command;
600
- const options = parseOptions(resolved.args);
707
+ const options = parseOptions(command, resolved.args);
601
708
  if (command === "help")
602
- return help();
709
+ return help(options);
603
710
  const currentIntegrity = currentProjectIntegrity(root);
604
711
  if (command === "runs") {
605
712
  const availableRuns = discoverRuns(root);
606
713
  const runs = page(availableRuns, options).map((run) => {
607
- const report = filteredCoverage(analyzeStoredRun(run), options);
714
+ const cached = readStoredRunIndex(run);
715
+ const report = cached ? filteredCoverage(cached, options) : undefined;
608
716
  return {
609
717
  id: run.id,
610
- generatedAt: report.generatedAt,
611
- lines: report.summary.lines.percentage,
612
- branches: report.summary.branches.percentage,
613
- mcdc: report.summary.conditionCoveragePct,
718
+ generatedAt: report?.generatedAt ?? run.metadata?.startedAt,
719
+ coverageIndexed: Boolean(report),
720
+ lines: report?.summary.lines.percentage ?? null,
721
+ branches: report?.summary.branches.percentage ?? null,
722
+ mcdc: report?.summary.conditionCoveragePct ?? null,
614
723
  command: run.metadata?.command,
615
724
  durationMs: run.metadata?.durationMs,
616
725
  timings: run.metadata?.timings,
@@ -624,18 +733,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
624
733
  });
625
734
  const runsBase = `npx supercov runs${options.filter !== "all" ? ` --filter ${options.filter}` : ""}`;
626
735
  const runsNext = nextPageCommand(runsBase, availableRuns.length, runs.length, options);
627
- return output(runs, options, runs
628
- .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(", ")})` : ""}`)
736
+ return output({ filters: queryFilters(options), runs }, options, runs
737
+ .map((run) => `${run.id} ${run.coverageIndexed ? `lines ${pct(run.lines)} branches ${pct(run.branches)} MC/DC ${pct(run.mcdc)}` : "coverage not indexed"}${run.stale ? ` STALE (${run.reasons.join(", ")})` : ""}`)
629
738
  .join("\n") +
630
739
  `\n${pageLabel(availableRuns.length, runs.length, options)}` +
631
- (runsNext ? `\nnext page: ${runsNext}` : ""));
740
+ (runsNext ? `\nnext page: ${runsNext}` : ""), queryPagination(availableRuns.length, runs.length, options));
632
741
  }
633
742
  if (command === "diff") {
634
743
  const [olderSelector, newerSelector] = options.positional;
635
744
  if (!olderSelector || !newerSelector)
636
- throw new Error("Usage: supercov diff <older-run> <newer-run>");
637
- const olderSelected = selectRun(root, olderSelector, currentIntegrity);
638
- const newerSelected = selectRun(root, newerSelector, currentIntegrity);
745
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov diff <older-run> <newer-run>");
746
+ const olderSelected = selectRun(root, olderSelector, currentIntegrity, options.json);
747
+ const newerSelected = selectRun(root, newerSelector, currentIntegrity, options.json);
639
748
  const older = {
640
749
  ...olderSelected,
641
750
  report: filteredCoverage(olderSelected.report, options),
@@ -690,6 +799,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
690
799
  .map(([, label]) => label)
691
800
  .sort();
692
801
  const result = {
802
+ filters: queryFilters(options),
693
803
  older: older.run.id,
694
804
  newer: newer.run.id,
695
805
  delta: {
@@ -728,9 +838,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
728
838
  .filter(Boolean)
729
839
  .join(" ");
730
840
  const diffNext = nextPageCommand(diffBase, diffTotal, diffReturned, options);
731
- 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}` : ""}`);
841
+ 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}` : ""}`, queryPagination(diffTotal, diffReturned, options));
732
842
  }
733
- const selectedRun = selectRun(root, options.run, currentIntegrity);
843
+ const selectedRun = selectRun(root, options.run, currentIntegrity, options.json);
734
844
  const run = selectedRun.run;
735
845
  const report = filteredCoverage(selectedRun.report, options);
736
846
  const selectedTestSet = selectedTestIds(report, options);
@@ -739,6 +849,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
739
849
  ? coverageSummaryForTests(report, selectedTestSet)
740
850
  : report.summary;
741
851
  const gaps = fileGaps(report, selectedTestSet).filter((gap) => gap.score > 0);
852
+ const measurement = coverageMeasurement(report);
742
853
  const selectedTests = report.tests.filter((test) => !selectedTestSet || selectedTestSet.has(test.id));
743
854
  const testCount = selectedTests.filter((test) => (test.role ?? "test") === "test").length;
744
855
  const setupCount = selectedTests.filter((test) => test.role === "setup").length;
@@ -748,18 +859,19 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
748
859
  ]));
749
860
  const result = {
750
861
  run: run.id,
751
- filter: options.filter,
752
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
862
+ filters: queryFilters(options),
753
863
  generatedAt: report.generatedAt,
754
864
  valid: run.metadata?.testExitCode === 0,
755
865
  stale: report.integrity?.stale ?? false,
756
866
  staleReasons: report.integrity?.staleReasons ?? [],
757
- structurallyComplete: summary.coverageComplete,
867
+ structurallyComplete: summary.coverageComplete && measurement.complete,
758
868
  complete: options.filter === "passed" &&
759
869
  run.metadata?.testExitCode === 0 &&
760
870
  !report.integrity?.stale &&
761
- summary.coverageComplete,
871
+ summary.coverageComplete &&
872
+ measurement.complete,
762
873
  coverage: summary,
874
+ measurement,
763
875
  coverageByKind: report.coverageByKind,
764
876
  coverageByRunner: report.coverageByRunner,
765
877
  attribution: attribution(report, selectedTestSet),
@@ -776,6 +888,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
776
888
  }
777
889
  : {}),
778
890
  filesWithGaps: gaps.length,
891
+ filesWithCoverageGaps: gaps.filter((gap) => gap.uncoveredLines > 0 ||
892
+ gap.uncoveredStatements > 0 ||
893
+ gap.uncoveredFunctions > 0 ||
894
+ gap.missingBranches > 0 ||
895
+ gap.missingMcdcConditions > 0).length,
896
+ filesWithMeasurementLimitations: measurement.files,
779
897
  tests: testCount,
780
898
  setups: setupCount,
781
899
  testOutcomes,
@@ -789,7 +907,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
789
907
  }
790
908
  : undefined,
791
909
  };
792
- 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)` : ""}`);
910
+ 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})\nmeasurement: ${measurement.complete ? "complete" : `incomplete — ${measurement.blocking} blocking limitation(s) in ${measurement.files} file(s)`}${!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 unresolved coverage or measurement gaps`);
793
911
  }
794
912
  if (command === "minimize") {
795
913
  const solverReport = selectedTestSet
@@ -814,17 +932,30 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
814
932
  const next = nextPageCommand(base, selectedDetails.length, selectedPage.length, options);
815
933
  return output({
816
934
  run: run.id,
935
+ filters: queryFilters(options),
817
936
  ...minimized,
818
937
  selectedCount: selectedDetails.length,
819
938
  totalCandidateTests: solverReport.tests.filter((test) => test.role === "test").length,
820
- offset: options.offset,
821
939
  tests: selectedPage,
822
- }, options, `exact minimum ${selectedDetails.length}/${solverReport.tests.filter((test) => test.role === "test").length} test(s) for ${options.target}% ${options.metric === "all" ? "coverage across all measured metrics" : options.metric}; explored ${minimized.exploredStates} state(s)\nlines ${pct(minimized.summary.lines.percentage)}, statements ${pct(minimized.summary.statements.percentage)}, functions ${pct(minimized.summary.functions.percentage)}, branches ${pct(minimized.summary.branches.percentage)}, MC/DC ${pct(minimized.summary.conditionCoveragePct)}\n${selectedPage.map((test) => `${test.id} ${test.kind}/${test.runner} ${test.file ?? "unknown"} ${test.name}`).join("\n")}\n${pageLabel(selectedDetails.length, selectedPage.length, options)}${next ? `\nnext page: ${next}` : ""}`);
940
+ }, options, `exact minimum ${selectedDetails.length}/${solverReport.tests.filter((test) => test.role === "test").length} test(s) for ${options.target}% ${options.metric === "all" ? "coverage across all measured metrics" : options.metric}; explored ${minimized.exploredStates} state(s)\nlines ${pct(minimized.summary.lines.percentage)}, statements ${pct(minimized.summary.statements.percentage)}, functions ${pct(minimized.summary.functions.percentage)}, branches ${pct(minimized.summary.branches.percentage)}, MC/DC ${pct(minimized.summary.conditionCoveragePct)}\n${selectedPage.map((test) => `${test.id} ${test.kind}/${test.runner} ${test.file ?? "unknown"} ${test.name}`).join("\n")}\n${pageLabel(selectedDetails.length, selectedPage.length, options)}${next ? `\nnext page: ${next}` : ""}`, queryPagination(selectedDetails.length, selectedPage.length, options));
823
941
  }
824
942
  if (command === "scope") {
825
943
  if (!report.scope)
826
- throw new Error("This run does not contain a source-scope inventory.");
827
- const ordered = [...report.scope.entries].sort((left, right) => {
944
+ throw new SupercovError("SCOPE_UNAVAILABLE", "This run does not contain a source-scope inventory.");
945
+ const limitationsByFile = new Map();
946
+ for (const limitation of report.limitations ?? []) {
947
+ const existing = limitationsByFile.get(limitation.file) ?? [];
948
+ existing.push(limitation);
949
+ limitationsByFile.set(limitation.file, existing);
950
+ }
951
+ const ordered = report.scope.entries.map((entry) => {
952
+ const limitations = limitationsByFile.get(entry.file) ?? [];
953
+ return {
954
+ ...entry,
955
+ measurementLimitations: limitations.length,
956
+ limitationKinds: [...new Set(limitations.map((item) => item.kind))].sort(),
957
+ };
958
+ }).sort((left, right) => {
828
959
  const rank = { ambiguous: 0, included: 1, excluded: 2 };
829
960
  return rank[left.status] - rank[right.status] || left.file.localeCompare(right.file);
830
961
  });
@@ -838,13 +969,13 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
838
969
  };
839
970
  return output({
840
971
  run: run.id,
972
+ filters: queryFilters(options),
841
973
  mode: report.scope.mode,
842
974
  roots: report.scope.roots,
843
975
  counts,
844
- total: ordered.length,
845
- offset: options.offset,
976
+ measurement: coverageMeasurement(report),
846
977
  entries: selectedEntries,
847
- }, options, `mode ${report.scope.mode}; roots ${report.scope.roots.join(", ") || "none"}; included ${counts.included}, excluded ${counts.excluded}, ambiguous ${counts.ambiguous}\n${selectedEntries.map((entry) => `${entry.status.toUpperCase()} ${entry.file} ${entry.reason}${entry.packageRoot ? ` [package ${entry.packageRoot}]` : ""}`).join("\n")}\n${pageLabel(ordered.length, selectedEntries.length, options)}${next ? `\nnext page: ${next}` : ""}`);
978
+ }, options, `mode ${report.scope.mode}; roots ${report.scope.roots.join(", ") || "none"}; included ${counts.included}, excluded ${counts.excluded}, ambiguous ${counts.ambiguous}; measurement ${coverageMeasurement(report).complete ? "complete" : `${coverageMeasurement(report).blocking} blocking limitation(s)`}\n${selectedEntries.map((entry) => `${entry.status.toUpperCase()} ${entry.file} ${entry.reason}${entry.measurementLimitations ? ` [measurement limitations: ${entry.measurementLimitations} ${entry.limitationKinds.join(", ")}]` : ""}${entry.packageRoot ? ` [package ${entry.packageRoot}]` : ""}`).join("\n")}\n${pageLabel(ordered.length, selectedEntries.length, options)}${next ? `\nnext page: ${next}` : ""}`, queryPagination(ordered.length, selectedEntries.length, options));
848
979
  }
849
980
  if (command === "kinds" || command === "runners") {
850
981
  const dimension = command === "kinds" ? report.coverageByKind : report.coverageByRunner;
@@ -852,8 +983,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
852
983
  const dimensionNext = nextPageCommand(coverageCommand(run.id, options, command), dimension.length, selectedDimension.length, options);
853
984
  return output({
854
985
  run: run.id,
855
- total: dimension.length,
856
- offset: options.offset,
986
+ filters: queryFilters(options),
857
987
  [command]: selectedDimension,
858
988
  }, options, selectedDimension
859
989
  .map((entry) => {
@@ -862,7 +992,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
862
992
  })
863
993
  .join("\n") +
864
994
  `\n${pageLabel(dimension.length, selectedDimension.length, options)}` +
865
- (dimensionNext ? `\nnext page: ${dimensionNext}` : ""));
995
+ (dimensionNext ? `\nnext page: ${dimensionNext}` : ""), queryPagination(dimension.length, selectedDimension.length, options));
866
996
  }
867
997
  if (command === "files" || command === "gaps") {
868
998
  const files = fileGaps(report, selectedTestSet);
@@ -876,25 +1006,31 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
876
1006
  : undefined;
877
1007
  return output({
878
1008
  run: run.id,
879
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
880
- total: all.length,
881
- offset: options.offset,
1009
+ filters: queryFilters(options),
882
1010
  [command]: selectedFiles,
883
1011
  }, options, selectedFiles
884
1012
  .map((gap) => {
885
- const status = gap.score === 0
886
- ? "complete"
1013
+ const missing = gap.uncoveredLines +
1014
+ gap.uncoveredStatements +
1015
+ gap.uncoveredFunctions +
1016
+ gap.missingBranches +
1017
+ gap.missingMcdcConditions;
1018
+ const status = missing === 0
1019
+ ? "coverage complete"
887
1020
  : `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}`;
888
- 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)}]` : ""}`;
1021
+ const limitations = gap.measurementLimitations
1022
+ ? ` measurement limitations ${gap.measurementLimitations} (${gap.limitationKinds.join(", ")})`
1023
+ : "";
1024
+ return `${gap.file} ${status}${limitations}${selectedTestSet ? ` [covered elsewhere: ${Object.values(gap.coveredByOtherTests).reduce((sum, value) => sum + value, 0)}; nowhere: ${Object.values(gap.uncoveredEverywhere).reduce((sum, value) => sum + value, 0)}]` : ""}`;
889
1025
  })
890
1026
  .join("\n") +
891
1027
  `\nshowing ${pageStart}-${pageEnd} of ${all.length}` +
892
- (nextCommand ? `\nnext page: ${nextCommand}` : ""));
1028
+ (nextCommand ? `\nnext page: ${nextCommand}` : ""), queryPagination(all.length, selectedFiles.length, options));
893
1029
  }
894
1030
  if (command === "file") {
895
1031
  const selector = options.positional.join(" ");
896
1032
  if (!selector)
897
- throw new Error("Usage: supercov runs <run-id> coverage file <source-file>");
1033
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage file <source-file>");
898
1034
  const file = findFile(report, selector);
899
1035
  const uncoveredLines = report.lines
900
1036
  .filter((line) => line.file === file &&
@@ -959,6 +1095,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
959
1095
  ...branches,
960
1096
  ...mcdc,
961
1097
  ].sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
1098
+ const allFileLimitations = (report.limitations ?? [])
1099
+ .filter((limitation) => limitation.file === file)
1100
+ .map((limitation) => ({
1101
+ ...limitation,
1102
+ blocking: true,
1103
+ effect: "outside-measured-denominator",
1104
+ }))
1105
+ .sort((left, right) => left.line - right.line ||
1106
+ left.column - right.column ||
1107
+ left.id.localeCompare(right.id));
962
1108
  const allFileTests = report.tests
963
1109
  .filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
964
1110
  test.lines.some((line) => line.file === file))
@@ -969,15 +1115,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
969
1115
  }));
970
1116
  const tests = page(allFileTests, options);
971
1117
  const selected = page(obligations, options);
972
- const filePageTotal = Math.max(obligations.length, allFileTests.length);
973
- const filePageReturned = Math.max(selected.length, tests.length);
1118
+ const limitations = page(allFileLimitations, options);
1119
+ const filePageTotal = Math.max(obligations.length, allFileTests.length, allFileLimitations.length);
1120
+ const filePageReturned = Math.max(selected.length, tests.length, limitations.length);
974
1121
  const nextFileOffset = options.offset + filePageReturned;
975
1122
  const nextFileCommand = filePageReturned > 0 && nextFileOffset < filePageTotal
976
1123
  ? `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --offset ${nextFileOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
977
1124
  : undefined;
978
1125
  const result = {
979
1126
  run: run.id,
980
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
1127
+ filters: queryFilters(options),
981
1128
  file,
982
1129
  counts: {
983
1130
  uncoveredLines: uncoveredLines.length,
@@ -985,14 +1132,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
985
1132
  uncoveredFunctions: functions.length,
986
1133
  missingBranches: branches.length,
987
1134
  missingMcdcConditions: mcdc.length,
1135
+ measurementLimitations: allFileLimitations.length,
988
1136
  },
989
1137
  tests,
990
1138
  totalTests: allFileTests.length,
991
1139
  totalObligations: obligations.length,
992
- offset: options.offset,
993
1140
  obligations: selected,
1141
+ totalLimitations: allFileLimitations.length,
1142
+ limitations,
994
1143
  };
995
- 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
1144
+ return output(result, options, `${file}\nlines ${uncoveredLines.length}, statements ${statements.length}, functions ${functions.length}, branches ${branches.length}, MC/DC ${mcdc.length}, measurement limitations ${allFileLimitations.length}\ncovered by ${allFileTests.length} test(s)\n${selected
996
1145
  .map((item) => item.kind === "line"
997
1146
  ? `line ${item.line}: ${item.otherCoverage.coveredElsewhere ? `covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}` : "uncovered everywhere"}`
998
1147
  : item.kind === "statement"
@@ -1002,12 +1151,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1002
1151
  : item.kind === "branch"
1003
1152
  ? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
1004
1153
  : `MC/DC ${item.line}:${item.column} [${item.id}]: ${item.missingCondition}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`)
1005
- .join("\n")}\n${pageLabel(filePageTotal, filePageReturned, options)} obligations/tests${nextFileCommand ? `\nnext page: ${nextFileCommand}` : ""}`);
1154
+ .join("\n")}${selected.length && limitations.length ? "\n" : ""}${limitations.map((limitation) => `LIMITATION ${limitation.kind} ${limitation.line}:${limitation.column} [${limitation.id}]\n ${limitation.reason}\n source: ${limitation.source}\n effect: outside measured denominator`).join("\n")}\n${pageLabel(filePageTotal, filePageReturned, options)} obligations/tests/limitations per category${nextFileCommand ? `\nnext page: ${nextFileCommand}` : ""}`, queryPagination(filePageTotal, filePageReturned, options));
1006
1155
  }
1007
1156
  if (command === "decision") {
1008
1157
  const selector = options.positional[0];
1009
1158
  if (!selector)
1010
- throw new Error("Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
1159
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
1011
1160
  let matches = report.decisions.filter((decision) => decision.meta.id === selector);
1012
1161
  if (matches.length === 0 && /:\d+(?::\d+)?$/.test(selector)) {
1013
1162
  const location = locationSelector(selector);
@@ -1015,7 +1164,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1015
1164
  decision.meta.line === location.line);
1016
1165
  }
1017
1166
  if (matches.length === 0)
1018
- throw new Error(`Decision not found: ${selector}`);
1167
+ throw new SupercovError("DECISION_NOT_FOUND", `Decision not found: ${selector}`, {
1168
+ details: { selector },
1169
+ });
1019
1170
  if (matches.length > 1) {
1020
1171
  const matchingDecisions = page(matches, options).map((decision) => ({
1021
1172
  id: decision.meta.id,
@@ -1027,10 +1178,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1027
1178
  const matchesNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, matches.length, matchingDecisions.length, options);
1028
1179
  return output({
1029
1180
  run: run.id,
1030
- total: matches.length,
1031
- offset: options.offset,
1181
+ filters: queryFilters(options),
1032
1182
  decisions: matchingDecisions,
1033
- }, 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}` : ""}`);
1183
+ }, 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}` : ""}`, queryPagination(matches.length, matchingDecisions.length, options));
1034
1184
  }
1035
1185
  matches = matches.map((decision) => filterDecision(decision, selectedTestSet));
1036
1186
  const totalDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
@@ -1048,7 +1198,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1048
1198
  const decisionNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, totalDecisionEvidence, returnedDecisionEvidence, options);
1049
1199
  const result = {
1050
1200
  run: run.id,
1051
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
1201
+ filters: queryFilters(options),
1052
1202
  decisions: matches,
1053
1203
  };
1054
1204
  return output(result, options, matches
@@ -1057,12 +1207,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1057
1207
  .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"}`)
1058
1208
  .join("\n\n") +
1059
1209
  `\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
1060
- (decisionNext ? `\nnext page: ${decisionNext}` : ""));
1210
+ (decisionNext ? `\nnext page: ${decisionNext}` : ""), queryPagination(totalDecisionEvidence, returnedDecisionEvidence, options));
1061
1211
  }
1062
1212
  if (command === "covers") {
1063
1213
  const selector = options.positional[0];
1064
1214
  if (!selector)
1065
- throw new Error("Usage: supercov runs <run-id> coverage covers <source-file:line>");
1215
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage covers <source-file:line>");
1066
1216
  const location = locationSelector(selector);
1067
1217
  const line = report.lines.find((candidate) => candidate.file === location.file && candidate.line === location.line);
1068
1218
  const allTests = (line?.tests ?? [])
@@ -1095,7 +1245,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1095
1245
  const coversNext = nextPageCommand(`${coverageCommand(run.id, options, "covers")} ${shellQuote(selector)}`, coversTotal, coversReturned, options);
1096
1246
  const result = {
1097
1247
  run: run.id,
1098
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
1248
+ filters: queryFilters(options),
1099
1249
  location,
1100
1250
  covered: includesSelectedTest(line?.tests ?? [], selectedTestSet),
1101
1251
  confidence: line?.confidence,
@@ -1104,16 +1254,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1104
1254
  tests,
1105
1255
  phases,
1106
1256
  };
1107
- 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}` : ""}`);
1257
+ 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}` : ""}`, queryPagination(coversTotal, coversReturned, options));
1108
1258
  }
1109
1259
  if (command === "test") {
1110
1260
  const selector = options.positional.join(" ").toLowerCase();
1111
1261
  if (!selector)
1112
- throw new Error("Usage: supercov runs <run-id> coverage test <id|name-fragment>");
1262
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage test <id|name-fragment>");
1113
1263
  const matches = report.tests.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
1114
1264
  (test.id === selector || test.name.toLowerCase().includes(selector)));
1115
1265
  if (matches.length === 0)
1116
- throw new Error(`Test not found: ${selector}`);
1266
+ throw new SupercovError("TEST_NOT_FOUND", `Test not found: ${selector}`, {
1267
+ details: { selector },
1268
+ });
1117
1269
  const testBase = `${coverageCommand(run.id, options, "test")} ${shellQuote(options.positional.join(" "))}`;
1118
1270
  if (matches.length > 1) {
1119
1271
  const matchingTests = page(matches, options).map((test) => ({
@@ -1125,10 +1277,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1125
1277
  const matchesNext = nextPageCommand(testBase, matches.length, matchingTests.length, options);
1126
1278
  return output({
1127
1279
  run: run.id,
1128
- total: matches.length,
1129
- offset: options.offset,
1280
+ filters: queryFilters(options),
1130
1281
  tests: matchingTests,
1131
- }, 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}` : ""}`);
1282
+ }, 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}` : ""}`, queryPagination(matches.length, matchingTests.length, options));
1132
1283
  }
1133
1284
  const test = matches[0];
1134
1285
  const allPhases = report.phases
@@ -1161,11 +1312,11 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1161
1312
  const testNext = nextPageCommand(testBase, testTotal, testReturned, options);
1162
1313
  return output({
1163
1314
  run: run.id,
1164
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
1315
+ filters: queryFilters(options),
1165
1316
  tests: [selected],
1166
- }, 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}` : ""}`);
1317
+ }, 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}` : ""}`, queryPagination(testTotal, testReturned, options));
1167
1318
  }
1168
- throw new Error(`Unknown coverage query: ${command}. Try supercov help.`);
1319
+ throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${command}. Try supercov help.`, { details: { command } });
1169
1320
  }
1170
1321
  export const coverageQueryCommands = new Set(["help", "runs", "diff"]);
1171
1322
  //# sourceMappingURL=query.js.map