supercov 0.0.5 → 0.0.7

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 (45) hide show
  1. package/README.md +37 -5
  2. package/dist/agentJson.d.ts +44 -0
  3. package/dist/agentJson.d.ts.map +1 -0
  4. package/dist/agentJson.js +84 -0
  5. package/dist/agentJson.js.map +1 -0
  6. package/dist/cli.js +22 -17
  7. package/dist/cli.js.map +1 -1
  8. package/dist/instrumenter.d.ts.map +1 -1
  9. package/dist/instrumenter.js +20 -2
  10. package/dist/instrumenter.js.map +1 -1
  11. package/dist/integrity.d.ts.map +1 -1
  12. package/dist/integrity.js +14 -0
  13. package/dist/integrity.js.map +1 -1
  14. package/dist/launchSupervisor.d.ts +8 -0
  15. package/dist/launchSupervisor.d.ts.map +1 -1
  16. package/dist/launchSupervisor.js +53 -2
  17. package/dist/launchSupervisor.js.map +1 -1
  18. package/dist/nodeTest.d.ts.map +1 -1
  19. package/dist/nodeTest.js +3 -1
  20. package/dist/nodeTest.js.map +1 -1
  21. package/dist/project.d.ts.map +1 -1
  22. package/dist/project.js +11 -14
  23. package/dist/project.js.map +1 -1
  24. package/dist/query.d.ts +36 -1
  25. package/dist/query.d.ts.map +1 -1
  26. package/dist/query.js +316 -107
  27. package/dist/query.js.map +1 -1
  28. package/dist/queryCache.d.ts +15 -0
  29. package/dist/queryCache.d.ts.map +1 -0
  30. package/dist/queryCache.js +95 -0
  31. package/dist/queryCache.js.map +1 -0
  32. package/dist/runAnalysis.d.ts.map +1 -1
  33. package/dist/runAnalysis.js +21 -1
  34. package/dist/runAnalysis.js.map +1 -1
  35. package/dist/runtime.d.ts +7 -1
  36. package/dist/runtime.d.ts.map +1 -1
  37. package/dist/runtime.js +91 -10
  38. package/dist/runtime.js.map +1 -1
  39. package/dist/sourceDiscovery.js +1 -1
  40. package/dist/sourceDiscovery.js.map +1 -1
  41. package/dist/types.d.ts +8 -0
  42. package/dist/types.d.ts.map +1 -1
  43. package/dist/vitest.js +5 -1
  44. package/dist/vitest.js.map +1 -1
  45. package/package.json +3 -3
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
+ };
104
130
  }
105
- function selectRun(root, selector, currentIntegrity) {
131
+ function analyzeStoredRun(run) {
132
+ return analyzeCoverageArchiveCached(run.evidencePath, storedRunAnalysisOptions(run));
133
+ }
134
+ function readStoredRunIndex(run) {
135
+ return readCoverageQueryIndex(run.evidencePath, storedRunAnalysisOptions(run));
136
+ }
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) => {
@@ -324,6 +361,7 @@ function coverageCommand(runId, options, child) {
324
361
  options.filter !== "all" ? `--filter ${options.filter}` : undefined,
325
362
  options.kind ? `--kind ${shellQuote(options.kind)}` : undefined,
326
363
  options.runner ? `--runner ${shellQuote(options.runner)}` : undefined,
364
+ options.metric !== "all" ? `--metric ${options.metric}` : undefined,
327
365
  ]
328
366
  .filter(Boolean)
329
367
  .join(" ");
@@ -351,7 +389,9 @@ function selectedTestIds(report, options) {
351
389
  ]
352
390
  .filter(Boolean)
353
391
  .join(", ");
354
- throw new Error(`No tests match ${filter}`);
392
+ throw new SupercovError("TEST_FILTER_EMPTY", `No tests match ${filter}`, {
393
+ details: { kind: options.kind, runner: options.runner },
394
+ });
355
395
  }
356
396
  return new Set(selected.map((test) => test.id));
357
397
  }
@@ -423,6 +463,13 @@ function filterLabel(options) {
423
463
  .filter(Boolean)
424
464
  .join(", ");
425
465
  }
466
+ function queryFilters(options) {
467
+ return {
468
+ outcome: options.filter,
469
+ kind: options.kind ?? null,
470
+ runner: options.runner ?? null,
471
+ };
472
+ }
426
473
  function attribution(report, selected) {
427
474
  const phases = selected
428
475
  ? report.phases.filter((phase) => selected.has(phase.test))
@@ -434,7 +481,46 @@ function attribution(report, selected) {
434
481
  serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
435
482
  };
436
483
  }
437
- function fileGaps(report, selected) {
484
+ function coverageDiagnostics(report, selected) {
485
+ const observed = attribution(report, selected);
486
+ if ((report.transport?.remoteLaunches ?? 0) > 0 &&
487
+ (report.transport?.scopedServerRecords ?? 0) === 0 &&
488
+ observed.serverExplicit === 0 &&
489
+ observed.serverFallback === 0) {
490
+ return [{
491
+ code: "REMOTE_SERVER_EVIDENCE_MISSING",
492
+ severity: "warning",
493
+ message: "Remote launches were supervised, but no server evidence returned. Coverage may describe only browser/test processes; inspect how the application server is launched.",
494
+ }];
495
+ }
496
+ return [];
497
+ }
498
+ function gapMetricValue(gap, metric) {
499
+ if (metric === "all")
500
+ return gap.score;
501
+ if (metric === "lines")
502
+ return gap.uncoveredLines;
503
+ if (metric === "statements")
504
+ return gap.uncoveredStatements;
505
+ if (metric === "functions")
506
+ return gap.uncoveredFunctions;
507
+ if (metric === "branches")
508
+ return gap.missingBranches;
509
+ return gap.missingMcdcConditions;
510
+ }
511
+ function obligationMatchesMetric(obligation, metric) {
512
+ if (metric === "all")
513
+ return true;
514
+ const kindByMetric = {
515
+ lines: "line",
516
+ statements: "statement",
517
+ functions: "function",
518
+ branches: "branch",
519
+ mcdc: "mcdc",
520
+ };
521
+ return obligation.kind === kindByMetric[metric];
522
+ }
523
+ export function fileGaps(report, selected) {
438
524
  const files = new Map();
439
525
  const get = (file) => {
440
526
  const existing = files.get(file);
@@ -447,6 +533,8 @@ function fileGaps(report, selected) {
447
533
  uncoveredFunctions: 0,
448
534
  missingBranches: 0,
449
535
  missingMcdcConditions: 0,
536
+ measurementLimitations: 0,
537
+ limitationKinds: [],
450
538
  coveredByOtherTests: {
451
539
  lines: 0,
452
540
  statements: 0,
@@ -508,45 +596,81 @@ function fileGaps(report, selected) {
508
596
  }
509
597
  }
510
598
  }
599
+ for (const limitation of report.limitations ?? []) {
600
+ const gap = get(limitation.file);
601
+ gap.measurementLimitations += 1;
602
+ if (!gap.limitationKinds.includes(limitation.kind))
603
+ gap.limitationKinds.push(limitation.kind);
604
+ }
511
605
  for (const gap of files.values()) {
606
+ gap.limitationKinds.sort();
512
607
  gap.score =
513
608
  gap.uncoveredLines +
514
609
  gap.uncoveredFunctions * 2 +
515
610
  gap.missingBranches * 2 +
516
- gap.missingMcdcConditions * 3;
611
+ gap.missingMcdcConditions * 3 +
612
+ gap.measurementLimitations * 3;
517
613
  }
518
614
  return [...files.values()].sort((left, right) => right.score - left.score || left.file.localeCompare(right.file));
519
615
  }
520
616
  function findFile(report, selector) {
521
- const files = [...new Set(report.lines.map((line) => line.file))];
617
+ const files = [
618
+ ...new Set([
619
+ ...report.lines.map((line) => line.file),
620
+ ...(report.limitations ?? []).map((limitation) => limitation.file),
621
+ ]),
622
+ ];
522
623
  if (files.includes(selector))
523
624
  return selector;
524
625
  const matches = files.filter((file) => file.includes(selector));
525
626
  if (matches.length === 1)
526
627
  return matches[0];
527
628
  if (matches.length === 0)
528
- throw new Error(`Source file not found: ${selector}`);
529
- throw new Error(`Ambiguous file selector: ${matches.join(", ")}`);
629
+ throw new SupercovError("SOURCE_NOT_FOUND", `Source file not found: ${selector}`, {
630
+ details: { selector },
631
+ });
632
+ throw new SupercovError("AMBIGUOUS_SELECTOR", `Ambiguous file selector: ${matches.join(", ")}`, {
633
+ details: { selector, matches },
634
+ });
635
+ }
636
+ export function coverageMeasurement(report) {
637
+ const limitations = report.limitations ?? [];
638
+ const byKind = {
639
+ "dynamic-code": 0,
640
+ "semantic-safety": 0,
641
+ "source-scope": 0,
642
+ };
643
+ for (const limitation of limitations)
644
+ byKind[limitation.kind] += 1;
645
+ return {
646
+ complete: limitations.length === 0,
647
+ limitations: limitations.length,
648
+ // Every current limitation removes source from the measured denominator.
649
+ blocking: limitations.length,
650
+ files: new Set(limitations.map((limitation) => limitation.file)).size,
651
+ byKind,
652
+ };
530
653
  }
531
654
  function locationSelector(selector) {
532
655
  const match = /^(.*):(\d+)(?::\d+)?$/.exec(selector);
533
656
  if (!match)
534
- throw new Error("Expected <source-file>:<line>");
657
+ throw new SupercovError("INVALID_ARGUMENT", "Expected <source-file>:<line>", {
658
+ details: { selector },
659
+ });
535
660
  return { file: match[1], line: Number(match[2]) };
536
661
  }
537
662
  function vectorText(values, outcome) {
538
663
  return `${values.map((value) => (value === null ? "-" : value ? "T" : "F")).join("")} -> ${outcome ? "T" : "F"}`;
539
664
  }
540
- function help() {
541
- console.log(`Agent-oriented local coverage queries:
665
+ const helpText = `Agent-oriented local coverage queries:
542
666
  supercov runs [--limit N] [--json]
543
667
  supercov runs <run-id> coverage [--filter all|passed|failed] [--kind e2e] [--runner playwright] [--json]
544
668
  supercov runs <run-id> coverage kinds [--json]
545
669
  supercov runs <run-id> coverage runners [--json]
546
670
  supercov runs <run-id> coverage scope [--limit N] [--offset N] [--json]
547
- supercov runs <run-id> coverage files [--filter all|passed|failed] [--limit N] [--offset N] [--json]
548
- supercov runs <run-id> coverage gaps [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
549
- supercov runs <run-id> coverage file <source-file> [--kind e2e] [--limit N] [--offset N] [--json]
671
+ supercov runs <run-id> coverage files [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--limit N] [--offset N] [--json]
672
+ supercov runs <run-id> coverage gaps [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
673
+ supercov runs <run-id> coverage file <source-file> [--metric all|lines|statements|functions|branches|mcdc] [--kind e2e] [--limit N] [--offset N] [--json]
550
674
  supercov runs <run-id> coverage decision <id|source-file:line> [--kind e2e] [--json]
551
675
  supercov runs <run-id> coverage covers <source-file:line> [--kind e2e] [--json]
552
676
  supercov runs <run-id> coverage test <id|name-fragment> [--kind e2e] [--limit N] [--json]
@@ -559,7 +683,30 @@ function help() {
559
683
  Use "latest" as <run-id> to query the newest local run.
560
684
 
561
685
  Create a run with:
562
- supercov -- <test command>`);
686
+ supercov -- <test command>`;
687
+ function help(options) {
688
+ if (options.json) {
689
+ return output({
690
+ usage: "supercov -- <test command>",
691
+ runSelector: "Use latest as <run-id> to query the newest local run.",
692
+ queryCommands: [
693
+ "runs",
694
+ "runs <run-id> coverage",
695
+ "runs <run-id> coverage kinds",
696
+ "runs <run-id> coverage runners",
697
+ "runs <run-id> coverage scope",
698
+ "runs <run-id> coverage files",
699
+ "runs <run-id> coverage gaps",
700
+ "runs <run-id> coverage file <source-file>",
701
+ "runs <run-id> coverage decision <id|source-file:line>",
702
+ "runs <run-id> coverage covers <source-file:line>",
703
+ "runs <run-id> coverage test <id|name-fragment>",
704
+ "runs <run-id> coverage minimize",
705
+ "diff <older-run> <newer-run>",
706
+ ],
707
+ }, options, helpText);
708
+ }
709
+ console.log(helpText);
563
710
  }
564
711
  /** Resolve the instance-first coverage resource syntax. */
565
712
  export function resolveCoverageQueryInvocation(command, args) {
@@ -587,7 +734,7 @@ export function resolveCoverageQueryInvocation(command, args) {
587
734
  "minimize",
588
735
  ]);
589
736
  if (!coverageCommands.has(child)) {
590
- throw new Error(`Unknown coverage query: ${child}. Try supercov help.`);
737
+ throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${child}. Try supercov help.`, { details: { command: child } });
591
738
  }
592
739
  return {
593
740
  command: child,
@@ -597,20 +744,22 @@ export function resolveCoverageQueryInvocation(command, args) {
597
744
  export async function runQueryCommand(command, args, root = process.cwd()) {
598
745
  const resolved = resolveCoverageQueryInvocation(command, args);
599
746
  command = resolved.command;
600
- const options = parseOptions(resolved.args);
747
+ const options = parseOptions(command, resolved.args);
601
748
  if (command === "help")
602
- return help();
749
+ return help(options);
603
750
  const currentIntegrity = currentProjectIntegrity(root);
604
751
  if (command === "runs") {
605
752
  const availableRuns = discoverRuns(root);
606
753
  const runs = page(availableRuns, options).map((run) => {
607
- const report = filteredCoverage(analyzeStoredRun(run), options);
754
+ const cached = readStoredRunIndex(run);
755
+ const report = cached ? filteredCoverage(cached, options) : undefined;
608
756
  return {
609
757
  id: run.id,
610
- generatedAt: report.generatedAt,
611
- lines: report.summary.lines.percentage,
612
- branches: report.summary.branches.percentage,
613
- mcdc: report.summary.conditionCoveragePct,
758
+ generatedAt: report?.generatedAt ?? run.metadata?.startedAt,
759
+ coverageIndexed: Boolean(report),
760
+ lines: report?.summary.lines.percentage ?? null,
761
+ branches: report?.summary.branches.percentage ?? null,
762
+ mcdc: report?.summary.conditionCoveragePct ?? null,
614
763
  command: run.metadata?.command,
615
764
  durationMs: run.metadata?.durationMs,
616
765
  timings: run.metadata?.timings,
@@ -624,18 +773,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
624
773
  });
625
774
  const runsBase = `npx supercov runs${options.filter !== "all" ? ` --filter ${options.filter}` : ""}`;
626
775
  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(", ")})` : ""}`)
776
+ return output({ filters: queryFilters(options), runs }, options, runs
777
+ .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
778
  .join("\n") +
630
779
  `\n${pageLabel(availableRuns.length, runs.length, options)}` +
631
- (runsNext ? `\nnext page: ${runsNext}` : ""));
780
+ (runsNext ? `\nnext page: ${runsNext}` : ""), queryPagination(availableRuns.length, runs.length, options));
632
781
  }
633
782
  if (command === "diff") {
634
783
  const [olderSelector, newerSelector] = options.positional;
635
784
  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);
785
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov diff <older-run> <newer-run>");
786
+ const olderSelected = selectRun(root, olderSelector, currentIntegrity, options.json);
787
+ const newerSelected = selectRun(root, newerSelector, currentIntegrity, options.json);
639
788
  const older = {
640
789
  ...olderSelected,
641
790
  report: filteredCoverage(olderSelected.report, options),
@@ -690,6 +839,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
690
839
  .map(([, label]) => label)
691
840
  .sort();
692
841
  const result = {
842
+ filters: queryFilters(options),
693
843
  older: older.run.id,
694
844
  newer: newer.run.id,
695
845
  delta: {
@@ -728,9 +878,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
728
878
  .filter(Boolean)
729
879
  .join(" ");
730
880
  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}` : ""}`);
881
+ 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
882
  }
733
- const selectedRun = selectRun(root, options.run, currentIntegrity);
883
+ const selectedRun = selectRun(root, options.run, currentIntegrity, options.json);
734
884
  const run = selectedRun.run;
735
885
  const report = filteredCoverage(selectedRun.report, options);
736
886
  const selectedTestSet = selectedTestIds(report, options);
@@ -739,6 +889,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
739
889
  ? coverageSummaryForTests(report, selectedTestSet)
740
890
  : report.summary;
741
891
  const gaps = fileGaps(report, selectedTestSet).filter((gap) => gap.score > 0);
892
+ const measurement = coverageMeasurement(report);
742
893
  const selectedTests = report.tests.filter((test) => !selectedTestSet || selectedTestSet.has(test.id));
743
894
  const testCount = selectedTests.filter((test) => (test.role ?? "test") === "test").length;
744
895
  const setupCount = selectedTests.filter((test) => test.role === "setup").length;
@@ -746,23 +897,27 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
746
897
  outcome,
747
898
  selectedTests.filter((test) => test.role === "test" && test.outcome === outcome).length,
748
899
  ]));
900
+ const diagnostics = coverageDiagnostics(report, selectedTestSet);
749
901
  const result = {
750
902
  run: run.id,
751
- filter: options.filter,
752
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
903
+ filters: queryFilters(options),
753
904
  generatedAt: report.generatedAt,
754
905
  valid: run.metadata?.testExitCode === 0,
755
906
  stale: report.integrity?.stale ?? false,
756
907
  staleReasons: report.integrity?.staleReasons ?? [],
757
- structurallyComplete: summary.coverageComplete,
908
+ structurallyComplete: summary.coverageComplete && measurement.complete,
758
909
  complete: options.filter === "passed" &&
759
910
  run.metadata?.testExitCode === 0 &&
760
911
  !report.integrity?.stale &&
761
- summary.coverageComplete,
912
+ summary.coverageComplete &&
913
+ measurement.complete,
762
914
  coverage: summary,
915
+ measurement,
763
916
  coverageByKind: report.coverageByKind,
764
917
  coverageByRunner: report.coverageByRunner,
765
918
  attribution: attribution(report, selectedTestSet),
919
+ transport: report.transport,
920
+ diagnostics,
766
921
  ...(!selectedTestSet
767
922
  ? {
768
923
  confidence: {
@@ -776,6 +931,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
776
931
  }
777
932
  : {}),
778
933
  filesWithGaps: gaps.length,
934
+ filesWithCoverageGaps: gaps.filter((gap) => gap.uncoveredLines > 0 ||
935
+ gap.uncoveredStatements > 0 ||
936
+ gap.uncoveredFunctions > 0 ||
937
+ gap.missingBranches > 0 ||
938
+ gap.missingMcdcConditions > 0).length,
939
+ filesWithMeasurementLimitations: measurement.files,
779
940
  tests: testCount,
780
941
  setups: setupCount,
781
942
  testOutcomes,
@@ -789,7 +950,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
789
950
  }
790
951
  : undefined,
791
952
  };
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)` : ""}`);
953
+ 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)`}${diagnostics.length ? `\ndiagnostic: ${diagnostics.map((item) => `${item.code}: ${item.message}`).join("; ")}` : ""}${!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
954
  }
794
955
  if (command === "minimize") {
795
956
  const solverReport = selectedTestSet
@@ -810,21 +971,34 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
810
971
  };
811
972
  });
812
973
  const selectedPage = page(selectedDetails, options);
813
- const base = `${coverageCommand(run.id, options, "minimize")} --target ${options.target}${options.metric !== "all" ? ` --metric ${options.metric}` : ""}`;
974
+ const base = `${coverageCommand(run.id, options, "minimize")} --target ${options.target}`;
814
975
  const next = nextPageCommand(base, selectedDetails.length, selectedPage.length, options);
815
976
  return output({
816
977
  run: run.id,
978
+ filters: queryFilters(options),
817
979
  ...minimized,
818
980
  selectedCount: selectedDetails.length,
819
981
  totalCandidateTests: solverReport.tests.filter((test) => test.role === "test").length,
820
- offset: options.offset,
821
982
  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}` : ""}`);
983
+ }, 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
984
  }
824
985
  if (command === "scope") {
825
986
  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) => {
987
+ throw new SupercovError("SCOPE_UNAVAILABLE", "This run does not contain a source-scope inventory.");
988
+ const limitationsByFile = new Map();
989
+ for (const limitation of report.limitations ?? []) {
990
+ const existing = limitationsByFile.get(limitation.file) ?? [];
991
+ existing.push(limitation);
992
+ limitationsByFile.set(limitation.file, existing);
993
+ }
994
+ const ordered = report.scope.entries.map((entry) => {
995
+ const limitations = limitationsByFile.get(entry.file) ?? [];
996
+ return {
997
+ ...entry,
998
+ measurementLimitations: limitations.length,
999
+ limitationKinds: [...new Set(limitations.map((item) => item.kind))].sort(),
1000
+ };
1001
+ }).sort((left, right) => {
828
1002
  const rank = { ambiguous: 0, included: 1, excluded: 2 };
829
1003
  return rank[left.status] - rank[right.status] || left.file.localeCompare(right.file);
830
1004
  });
@@ -838,13 +1012,13 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
838
1012
  };
839
1013
  return output({
840
1014
  run: run.id,
1015
+ filters: queryFilters(options),
841
1016
  mode: report.scope.mode,
842
1017
  roots: report.scope.roots,
843
1018
  counts,
844
- total: ordered.length,
845
- offset: options.offset,
1019
+ measurement: coverageMeasurement(report),
846
1020
  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}` : ""}`);
1021
+ }, 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
1022
  }
849
1023
  if (command === "kinds" || command === "runners") {
850
1024
  const dimension = command === "kinds" ? report.coverageByKind : report.coverageByRunner;
@@ -852,8 +1026,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
852
1026
  const dimensionNext = nextPageCommand(coverageCommand(run.id, options, command), dimension.length, selectedDimension.length, options);
853
1027
  return output({
854
1028
  run: run.id,
855
- total: dimension.length,
856
- offset: options.offset,
1029
+ filters: queryFilters(options),
857
1030
  [command]: selectedDimension,
858
1031
  }, options, selectedDimension
859
1032
  .map((entry) => {
@@ -862,11 +1035,17 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
862
1035
  })
863
1036
  .join("\n") +
864
1037
  `\n${pageLabel(dimension.length, selectedDimension.length, options)}` +
865
- (dimensionNext ? `\nnext page: ${dimensionNext}` : ""));
1038
+ (dimensionNext ? `\nnext page: ${dimensionNext}` : ""), queryPagination(dimension.length, selectedDimension.length, options));
866
1039
  }
867
1040
  if (command === "files" || command === "gaps") {
868
1041
  const files = fileGaps(report, selectedTestSet);
869
- const all = command === "gaps" ? files.filter((gap) => gap.score > 0) : files;
1042
+ const all = files
1043
+ .filter((gap) => command === "files" ||
1044
+ gapMetricValue(gap, options.metric) > 0 ||
1045
+ gap.measurementLimitations > 0)
1046
+ .sort((left, right) => gapMetricValue(right, options.metric) - gapMetricValue(left, options.metric) ||
1047
+ right.measurementLimitations - left.measurementLimitations ||
1048
+ left.file.localeCompare(right.file));
870
1049
  const selectedFiles = page(all, options);
871
1050
  const pageStart = all.length === 0 ? 0 : options.offset + 1;
872
1051
  const pageEnd = Math.min(options.offset + selectedFiles.length, all.length);
@@ -876,25 +1055,32 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
876
1055
  : undefined;
877
1056
  return output({
878
1057
  run: run.id,
879
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
880
- total: all.length,
881
- offset: options.offset,
1058
+ filters: queryFilters(options),
1059
+ metric: options.metric,
882
1060
  [command]: selectedFiles,
883
1061
  }, options, selectedFiles
884
1062
  .map((gap) => {
885
- const status = gap.score === 0
886
- ? "complete"
1063
+ const missing = gap.uncoveredLines +
1064
+ gap.uncoveredStatements +
1065
+ gap.uncoveredFunctions +
1066
+ gap.missingBranches +
1067
+ gap.missingMcdcConditions;
1068
+ const status = missing === 0
1069
+ ? "coverage complete"
887
1070
  : `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)}]` : ""}`;
1071
+ const limitations = gap.measurementLimitations
1072
+ ? ` measurement limitations ${gap.measurementLimitations} (${gap.limitationKinds.join(", ")})`
1073
+ : "";
1074
+ 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
1075
  })
890
1076
  .join("\n") +
891
1077
  `\nshowing ${pageStart}-${pageEnd} of ${all.length}` +
892
- (nextCommand ? `\nnext page: ${nextCommand}` : ""));
1078
+ (nextCommand ? `\nnext page: ${nextCommand}` : ""), queryPagination(all.length, selectedFiles.length, options));
893
1079
  }
894
1080
  if (command === "file") {
895
1081
  const selector = options.positional.join(" ");
896
1082
  if (!selector)
897
- throw new Error("Usage: supercov runs <run-id> coverage file <source-file>");
1083
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage file <source-file>");
898
1084
  const file = findFile(report, selector);
899
1085
  const uncoveredLines = report.lines
900
1086
  .filter((line) => line.file === file &&
@@ -958,7 +1144,17 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
958
1144
  ...functions,
959
1145
  ...branches,
960
1146
  ...mcdc,
961
- ].sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
1147
+ ].filter((obligation) => obligationMatchesMetric(obligation, options.metric)).sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
1148
+ const allFileLimitations = (report.limitations ?? [])
1149
+ .filter((limitation) => limitation.file === file)
1150
+ .map((limitation) => ({
1151
+ ...limitation,
1152
+ blocking: true,
1153
+ effect: "outside-measured-denominator",
1154
+ }))
1155
+ .sort((left, right) => left.line - right.line ||
1156
+ left.column - right.column ||
1157
+ left.id.localeCompare(right.id));
962
1158
  const allFileTests = report.tests
963
1159
  .filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
964
1160
  test.lines.some((line) => line.file === file))
@@ -969,30 +1165,34 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
969
1165
  }));
970
1166
  const tests = page(allFileTests, options);
971
1167
  const selected = page(obligations, options);
972
- const filePageTotal = Math.max(obligations.length, allFileTests.length);
973
- const filePageReturned = Math.max(selected.length, tests.length);
1168
+ const limitations = page(allFileLimitations, options);
1169
+ const filePageTotal = Math.max(obligations.length, allFileTests.length, allFileLimitations.length);
1170
+ const filePageReturned = Math.max(selected.length, tests.length, limitations.length);
974
1171
  const nextFileOffset = options.offset + filePageReturned;
975
1172
  const nextFileCommand = filePageReturned > 0 && nextFileOffset < filePageTotal
976
1173
  ? `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --offset ${nextFileOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
977
1174
  : undefined;
978
1175
  const result = {
979
1176
  run: run.id,
980
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
1177
+ filters: queryFilters(options),
981
1178
  file,
1179
+ metric: options.metric,
982
1180
  counts: {
983
1181
  uncoveredLines: uncoveredLines.length,
984
1182
  uncoveredStatements: statements.length,
985
1183
  uncoveredFunctions: functions.length,
986
1184
  missingBranches: branches.length,
987
1185
  missingMcdcConditions: mcdc.length,
1186
+ measurementLimitations: allFileLimitations.length,
988
1187
  },
989
1188
  tests,
990
1189
  totalTests: allFileTests.length,
991
1190
  totalObligations: obligations.length,
992
- offset: options.offset,
993
1191
  obligations: selected,
1192
+ totalLimitations: allFileLimitations.length,
1193
+ limitations,
994
1194
  };
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
1195
+ 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
1196
  .map((item) => item.kind === "line"
997
1197
  ? `line ${item.line}: ${item.otherCoverage.coveredElsewhere ? `covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}` : "uncovered everywhere"}`
998
1198
  : item.kind === "statement"
@@ -1002,12 +1202,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1002
1202
  : item.kind === "branch"
1003
1203
  ? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
1004
1204
  : `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}` : ""}`);
1205
+ .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
1206
  }
1007
1207
  if (command === "decision") {
1008
1208
  const selector = options.positional[0];
1009
1209
  if (!selector)
1010
- throw new Error("Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
1210
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
1011
1211
  let matches = report.decisions.filter((decision) => decision.meta.id === selector);
1012
1212
  if (matches.length === 0 && /:\d+(?::\d+)?$/.test(selector)) {
1013
1213
  const location = locationSelector(selector);
@@ -1015,7 +1215,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1015
1215
  decision.meta.line === location.line);
1016
1216
  }
1017
1217
  if (matches.length === 0)
1018
- throw new Error(`Decision not found: ${selector}`);
1218
+ throw new SupercovError("DECISION_NOT_FOUND", `Decision not found: ${selector}`, {
1219
+ details: { selector },
1220
+ });
1019
1221
  if (matches.length > 1) {
1020
1222
  const matchingDecisions = page(matches, options).map((decision) => ({
1021
1223
  id: decision.meta.id,
@@ -1027,17 +1229,22 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1027
1229
  const matchesNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, matches.length, matchingDecisions.length, options);
1028
1230
  return output({
1029
1231
  run: run.id,
1030
- total: matches.length,
1031
- offset: options.offset,
1232
+ filters: queryFilters(options),
1032
1233
  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}` : ""}`);
1234
+ }, 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
1235
  }
1035
1236
  matches = matches.map((decision) => filterDecision(decision, selectedTestSet));
1036
1237
  const totalDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
1037
1238
  matches = matches.map((decision) => {
1239
+ const totals = {
1240
+ conditions: decision.conditions.length,
1241
+ vectorObservations: decision.vectorObservations.length,
1242
+ tests: decision.tests.length,
1243
+ };
1038
1244
  const vectorObservations = page(decision.vectorObservations, options);
1039
1245
  return {
1040
1246
  ...decision,
1247
+ totals,
1041
1248
  vectors: vectorObservations.map((observation) => observation.vector),
1042
1249
  vectorObservations,
1043
1250
  conditions: page(decision.conditions, options),
@@ -1048,7 +1255,8 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1048
1255
  const decisionNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, totalDecisionEvidence, returnedDecisionEvidence, options);
1049
1256
  const result = {
1050
1257
  run: run.id,
1051
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
1258
+ filters: queryFilters(options),
1259
+ paginationAppliesTo: "conditions, vectorObservations, and tests independently within each decision",
1052
1260
  decisions: matches,
1053
1261
  };
1054
1262
  return output(result, options, matches
@@ -1057,12 +1265,12 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1057
1265
  .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
1266
  .join("\n\n") +
1059
1267
  `\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
1060
- (decisionNext ? `\nnext page: ${decisionNext}` : ""));
1268
+ (decisionNext ? `\nnext page: ${decisionNext}` : ""), queryPagination(totalDecisionEvidence, returnedDecisionEvidence, options));
1061
1269
  }
1062
1270
  if (command === "covers") {
1063
1271
  const selector = options.positional[0];
1064
1272
  if (!selector)
1065
- throw new Error("Usage: supercov runs <run-id> coverage covers <source-file:line>");
1273
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage covers <source-file:line>");
1066
1274
  const location = locationSelector(selector);
1067
1275
  const line = report.lines.find((candidate) => candidate.file === location.file && candidate.line === location.line);
1068
1276
  const allTests = (line?.tests ?? [])
@@ -1095,7 +1303,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1095
1303
  const coversNext = nextPageCommand(`${coverageCommand(run.id, options, "covers")} ${shellQuote(selector)}`, coversTotal, coversReturned, options);
1096
1304
  const result = {
1097
1305
  run: run.id,
1098
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
1306
+ filters: queryFilters(options),
1099
1307
  location,
1100
1308
  covered: includesSelectedTest(line?.tests ?? [], selectedTestSet),
1101
1309
  confidence: line?.confidence,
@@ -1104,16 +1312,18 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1104
1312
  tests,
1105
1313
  phases,
1106
1314
  };
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}` : ""}`);
1315
+ 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
1316
  }
1109
1317
  if (command === "test") {
1110
1318
  const selector = options.positional.join(" ").toLowerCase();
1111
1319
  if (!selector)
1112
- throw new Error("Usage: supercov runs <run-id> coverage test <id|name-fragment>");
1320
+ throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage test <id|name-fragment>");
1113
1321
  const matches = report.tests.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
1114
1322
  (test.id === selector || test.name.toLowerCase().includes(selector)));
1115
1323
  if (matches.length === 0)
1116
- throw new Error(`Test not found: ${selector}`);
1324
+ throw new SupercovError("TEST_NOT_FOUND", `Test not found: ${selector}`, {
1325
+ details: { selector },
1326
+ });
1117
1327
  const testBase = `${coverageCommand(run.id, options, "test")} ${shellQuote(options.positional.join(" "))}`;
1118
1328
  if (matches.length > 1) {
1119
1329
  const matchingTests = page(matches, options).map((test) => ({
@@ -1125,10 +1335,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1125
1335
  const matchesNext = nextPageCommand(testBase, matches.length, matchingTests.length, options);
1126
1336
  return output({
1127
1337
  run: run.id,
1128
- total: matches.length,
1129
- offset: options.offset,
1338
+ filters: queryFilters(options),
1130
1339
  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}` : ""}`);
1340
+ }, 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
1341
  }
1133
1342
  const test = matches[0];
1134
1343
  const allPhases = report.phases
@@ -1161,11 +1370,11 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1161
1370
  const testNext = nextPageCommand(testBase, testTotal, testReturned, options);
1162
1371
  return output({
1163
1372
  run: run.id,
1164
- ...(filterLabel(options) ? { filter: filterLabel(options) } : {}),
1373
+ filters: queryFilters(options),
1165
1374
  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}` : ""}`);
1375
+ }, 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
1376
  }
1168
- throw new Error(`Unknown coverage query: ${command}. Try supercov help.`);
1377
+ throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${command}. Try supercov help.`, { details: { command } });
1169
1378
  }
1170
1379
  export const coverageQueryCommands = new Set(["help", "runs", "diff"]);
1171
1380
  //# sourceMappingURL=query.js.map