supercov 0.0.11 → 0.0.13

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 (175) hide show
  1. package/README.md +28 -23
  2. package/bin/supercov.js +8 -19
  3. package/package.json +31 -43
  4. package/{dist → runtime/javascript}/atomic.js +0 -1
  5. package/{dist → runtime/javascript}/launchSupervisor.js +45 -2
  6. package/{dist → runtime/javascript}/nodeAssert.js +0 -1
  7. package/{dist → runtime/javascript}/nodeAssertAdapter.js +0 -1
  8. package/{dist → runtime/javascript}/nodeAssertStrict.js +0 -1
  9. package/{dist → runtime/javascript}/nodeTest.js +0 -1
  10. package/{dist → runtime/javascript}/playwright.js +0 -1
  11. package/{dist → runtime/javascript}/playwrightReporter.js +0 -1
  12. package/{dist → runtime/javascript}/provenance.js +0 -1
  13. package/{dist → runtime/javascript}/register.mjs +3 -2
  14. package/{dist → runtime/javascript}/resolve-loader.mjs +0 -42
  15. package/{dist → runtime/javascript}/runnerEvidence.js +0 -1
  16. package/{dist → runtime/javascript}/transport.js +0 -1
  17. package/{dist → runtime/javascript}/vitest.js +0 -1
  18. package/{dist → runtime/javascript}/vitestReporter.js +0 -1
  19. package/dist/agentJson.d.ts +0 -45
  20. package/dist/agentJson.d.ts.map +0 -1
  21. package/dist/agentJson.js +0 -102
  22. package/dist/agentJson.js.map +0 -1
  23. package/dist/analyze.d.ts +0 -12
  24. package/dist/analyze.d.ts.map +0 -1
  25. package/dist/analyze.js +0 -704
  26. package/dist/analyze.js.map +0 -1
  27. package/dist/atomic.d.ts +0 -6
  28. package/dist/atomic.d.ts.map +0 -1
  29. package/dist/atomic.js.map +0 -1
  30. package/dist/buildCache.d.ts +0 -14
  31. package/dist/buildCache.d.ts.map +0 -1
  32. package/dist/buildCache.js +0 -84
  33. package/dist/buildCache.js.map +0 -1
  34. package/dist/cli.d.ts +0 -2
  35. package/dist/cli.d.ts.map +0 -1
  36. package/dist/cli.js +0 -797
  37. package/dist/cli.js.map +0 -1
  38. package/dist/directInstrumenter.d.ts +0 -8
  39. package/dist/directInstrumenter.d.ts.map +0 -1
  40. package/dist/directInstrumenter.js +0 -112
  41. package/dist/directInstrumenter.js.map +0 -1
  42. package/dist/engineEvidence.d.ts +0 -9
  43. package/dist/engineEvidence.d.ts.map +0 -1
  44. package/dist/engineEvidence.js +0 -31
  45. package/dist/engineEvidence.js.map +0 -1
  46. package/dist/engineInstrumenter.d.ts +0 -16
  47. package/dist/engineInstrumenter.d.ts.map +0 -1
  48. package/dist/engineInstrumenter.js +0 -50
  49. package/dist/engineInstrumenter.js.map +0 -1
  50. package/dist/engineProcess.d.ts +0 -11
  51. package/dist/engineProcess.d.ts.map +0 -1
  52. package/dist/engineProcess.js +0 -59
  53. package/dist/engineProcess.js.map +0 -1
  54. package/dist/esmInterceptor.d.ts +0 -6
  55. package/dist/esmInterceptor.d.ts.map +0 -1
  56. package/dist/esmInterceptor.js +0 -63
  57. package/dist/esmInterceptor.js.map +0 -1
  58. package/dist/evidenceArchive.d.ts +0 -38
  59. package/dist/evidenceArchive.d.ts.map +0 -1
  60. package/dist/evidenceArchive.js +0 -156
  61. package/dist/evidenceArchive.js.map +0 -1
  62. package/dist/instrumenter.d.ts +0 -18
  63. package/dist/instrumenter.d.ts.map +0 -1
  64. package/dist/instrumenter.js +0 -1644
  65. package/dist/instrumenter.js.map +0 -1
  66. package/dist/integrity.d.ts +0 -10
  67. package/dist/integrity.d.ts.map +0 -1
  68. package/dist/integrity.js +0 -213
  69. package/dist/integrity.js.map +0 -1
  70. package/dist/launchSupervisor.d.ts +0 -38
  71. package/dist/launchSupervisor.d.ts.map +0 -1
  72. package/dist/launchSupervisor.js.map +0 -1
  73. package/dist/merge.d.ts +0 -2
  74. package/dist/merge.d.ts.map +0 -1
  75. package/dist/merge.js +0 -119
  76. package/dist/merge.js.map +0 -1
  77. package/dist/nodeAssert.d.ts +0 -31
  78. package/dist/nodeAssert.d.ts.map +0 -1
  79. package/dist/nodeAssert.js.map +0 -1
  80. package/dist/nodeAssertAdapter.d.ts +0 -6
  81. package/dist/nodeAssertAdapter.d.ts.map +0 -1
  82. package/dist/nodeAssertAdapter.js.map +0 -1
  83. package/dist/nodeAssertStrict.d.ts +0 -31
  84. package/dist/nodeAssertStrict.d.ts.map +0 -1
  85. package/dist/nodeAssertStrict.js.map +0 -1
  86. package/dist/nodeAssertionInstrumenter.d.ts +0 -8
  87. package/dist/nodeAssertionInstrumenter.d.ts.map +0 -1
  88. package/dist/nodeAssertionInstrumenter.js +0 -271
  89. package/dist/nodeAssertionInstrumenter.js.map +0 -1
  90. package/dist/nodeTest.d.ts +0 -15
  91. package/dist/nodeTest.d.ts.map +0 -1
  92. package/dist/nodeTest.js.map +0 -1
  93. package/dist/playwright.d.ts +0 -9
  94. package/dist/playwright.d.ts.map +0 -1
  95. package/dist/playwright.js.map +0 -1
  96. package/dist/playwrightReporter.d.ts +0 -6
  97. package/dist/playwrightReporter.d.ts.map +0 -1
  98. package/dist/playwrightReporter.js.map +0 -1
  99. package/dist/processDiagnostics.d.ts +0 -34
  100. package/dist/processDiagnostics.d.ts.map +0 -1
  101. package/dist/processDiagnostics.js +0 -155
  102. package/dist/processDiagnostics.js.map +0 -1
  103. package/dist/project.d.ts +0 -21
  104. package/dist/project.d.ts.map +0 -1
  105. package/dist/project.js +0 -331
  106. package/dist/project.js.map +0 -1
  107. package/dist/provenance.d.ts +0 -9
  108. package/dist/provenance.d.ts.map +0 -1
  109. package/dist/provenance.js.map +0 -1
  110. package/dist/query.d.ts +0 -81
  111. package/dist/query.d.ts.map +0 -1
  112. package/dist/query.js +0 -1656
  113. package/dist/query.js.map +0 -1
  114. package/dist/queryCache.d.ts +0 -15
  115. package/dist/queryCache.d.ts.map +0 -1
  116. package/dist/queryCache.js +0 -95
  117. package/dist/queryCache.js.map +0 -1
  118. package/dist/queueAdapters.d.ts +0 -15
  119. package/dist/queueAdapters.d.ts.map +0 -1
  120. package/dist/queueAdapters.js +0 -55
  121. package/dist/queueAdapters.js.map +0 -1
  122. package/dist/register.d.mts +0 -2
  123. package/dist/register.d.mts.map +0 -1
  124. package/dist/register.mjs.map +0 -1
  125. package/dist/reporter.d.ts +0 -13
  126. package/dist/reporter.d.ts.map +0 -1
  127. package/dist/reporter.js +0 -252
  128. package/dist/reporter.js.map +0 -1
  129. package/dist/resolve-loader.d.mts +0 -3
  130. package/dist/resolve-loader.d.mts.map +0 -1
  131. package/dist/resolve-loader.mjs.map +0 -1
  132. package/dist/runAnalysis.d.ts +0 -19
  133. package/dist/runAnalysis.d.ts.map +0 -1
  134. package/dist/runAnalysis.js +0 -171
  135. package/dist/runAnalysis.js.map +0 -1
  136. package/dist/runnerEvidence.d.ts +0 -19
  137. package/dist/runnerEvidence.d.ts.map +0 -1
  138. package/dist/runnerEvidence.js.map +0 -1
  139. package/dist/runtime.d.ts +0 -140
  140. package/dist/runtime.d.ts.map +0 -1
  141. package/dist/runtime.js.map +0 -1
  142. package/dist/runtimeIsolation.d.ts +0 -3
  143. package/dist/runtimeIsolation.d.ts.map +0 -1
  144. package/dist/runtimeIsolation.js +0 -9
  145. package/dist/runtimeIsolation.js.map +0 -1
  146. package/dist/sourceDiscovery.d.ts +0 -9
  147. package/dist/sourceDiscovery.d.ts.map +0 -1
  148. package/dist/sourceDiscovery.js +0 -242
  149. package/dist/sourceDiscovery.js.map +0 -1
  150. package/dist/transport.d.ts +0 -17
  151. package/dist/transport.d.ts.map +0 -1
  152. package/dist/transport.js.map +0 -1
  153. package/dist/types.d.ts +0 -378
  154. package/dist/types.d.ts.map +0 -1
  155. package/dist/types.js +0 -2
  156. package/dist/types.js.map +0 -1
  157. package/dist/vitePlugin.d.ts +0 -13
  158. package/dist/vitePlugin.d.ts.map +0 -1
  159. package/dist/vitePlugin.js +0 -139
  160. package/dist/vitePlugin.js.map +0 -1
  161. package/dist/vitest.d.ts +0 -2
  162. package/dist/vitest.d.ts.map +0 -1
  163. package/dist/vitest.js.map +0 -1
  164. package/dist/vitestReporter.d.ts +0 -43
  165. package/dist/vitestReporter.d.ts.map +0 -1
  166. package/dist/vitestReporter.js.map +0 -1
  167. package/dist/waivers.d.ts +0 -52
  168. package/dist/waivers.d.ts.map +0 -1
  169. package/dist/waivers.js +0 -121
  170. package/dist/waivers.js.map +0 -1
  171. package/dist/workspace.d.ts +0 -117
  172. package/dist/workspace.d.ts.map +0 -1
  173. package/dist/workspace.js +0 -680
  174. package/dist/workspace.js.map +0 -1
  175. /package/{dist → runtime/javascript}/runtime.js +0 -0
package/dist/query.js DELETED
@@ -1,1656 +0,0 @@
1
- import { existsSync, readFileSync, readdirSync } from "node:fs";
2
- import { resolve } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import { coverageSummaryForTests, isIndependencePair } from "./analyze.js";
5
- import { EVIDENCE_ARCHIVE_SCHEMA_VERSION } from "./evidenceArchive.js";
6
- import { analyzeCoverageArchiveCached, readCoverageQueryIndex, } from "./queryCache.js";
7
- import { compareRunIntegrity, createRunIntegrity } from "./integrity.js";
8
- import { evaluateCoverageWaivers, readCoverageWaivers, WAIVERS_FILE, } from "./waivers.js";
9
- import { discoverCoverageProject } from "./project.js";
10
- import { agentPagination, agentSuccessJson, SupercovError, } from "./agentJson.js";
11
- function parseOptions(command, args) {
12
- const options = {
13
- command: command === "runs" || command === "diff" || command === "help"
14
- ? command
15
- : `coverage.${command}`,
16
- limit: 20,
17
- offset: 0,
18
- json: false,
19
- filter: "all",
20
- target: 100,
21
- metric: "all",
22
- group: "none",
23
- sort: "location",
24
- positional: [],
25
- };
26
- for (let index = 0; index < args.length; index += 1) {
27
- const value = args[index];
28
- if (value === "--json")
29
- options.json = true;
30
- else if (value === "--run") {
31
- const run = args[++index];
32
- if (!run)
33
- throw new SupercovError("INVALID_ARGUMENT", "--run requires a run ID");
34
- options.run = run;
35
- }
36
- else if (value === "--kind") {
37
- const kind = args[++index]?.toLowerCase();
38
- if (!kind)
39
- throw new SupercovError("INVALID_ARGUMENT", "--kind requires a test kind");
40
- options.kind = kind;
41
- }
42
- else if (value === "--runner") {
43
- const runner = args[++index]?.toLowerCase();
44
- if (!runner)
45
- throw new SupercovError("INVALID_ARGUMENT", "--runner requires a runner name");
46
- options.runner = runner;
47
- }
48
- else if (value === "--target") {
49
- const target = Number(args[++index]);
50
- if (!Number.isFinite(target) || target < 0 || target > 100)
51
- throw new SupercovError("INVALID_ARGUMENT", "--target must be between 0 and 100");
52
- options.target = target;
53
- }
54
- else if (value === "--metric") {
55
- const metric = args[++index]?.toLowerCase();
56
- if (!metric || !["all", "lines", "statements", "functions", "branches", "mcdc"].includes(metric))
57
- throw new SupercovError("INVALID_ARGUMENT", "--metric must be all, lines, statements, functions, branches, or mcdc");
58
- options.metric = metric;
59
- }
60
- else if (value === "--filter") {
61
- const filter = args[++index]?.toLowerCase();
62
- if (filter !== "all" && filter !== "passed" && filter !== "failed")
63
- throw new SupercovError("INVALID_ARGUMENT", "--filter must be all, passed, or failed");
64
- options.filter = filter;
65
- }
66
- else if (value === "--limit") {
67
- const limit = Number(args[++index]);
68
- if (!Number.isSafeInteger(limit) || limit < 1)
69
- throw new SupercovError("INVALID_ARGUMENT", "--limit must be a positive integer");
70
- options.limit = limit;
71
- }
72
- else if (value === "--offset") {
73
- const offset = Number(args[++index]);
74
- if (!Number.isSafeInteger(offset) || offset < 0)
75
- throw new SupercovError("INVALID_ARGUMENT", "--offset must be a non-negative integer");
76
- options.offset = offset;
77
- }
78
- else if (value === "--group") {
79
- const group = args[++index]?.toLowerCase();
80
- if (group !== "decision")
81
- throw new SupercovError("INVALID_ARGUMENT", "--group must be decision");
82
- if (command !== "file")
83
- throw new SupercovError("INVALID_ARGUMENT", "--group is only supported by: supercov runs <run-id> coverage file <source-file>");
84
- options.group = "decision";
85
- }
86
- else if (value === "--sort") {
87
- const sort = args[++index]?.toLowerCase();
88
- if (sort !== "location" && sort !== "missing")
89
- throw new SupercovError("INVALID_ARGUMENT", "--sort must be location or missing");
90
- options.sort = sort;
91
- }
92
- else if (value.startsWith("--"))
93
- throw new SupercovError("INVALID_ARGUMENT", `Unknown option: ${value}`, {
94
- details: { option: value },
95
- });
96
- else
97
- options.positional.push(value);
98
- }
99
- if (options.sort !== "location" && options.group === "none")
100
- throw new SupercovError("INVALID_ARGUMENT", "--sort requires --group decision");
101
- if (options.group === "decision" && options.metric !== "all" && options.metric !== "mcdc")
102
- throw new SupercovError("INVALID_ARGUMENT", "--group decision lists MC/DC decisions; omit --metric or use --metric mcdc");
103
- return options;
104
- }
105
- function filteredCoverage(report, options) {
106
- if (options.filter === "all")
107
- return report;
108
- const filtered = report.filters?.[options.filter];
109
- if (!filtered) {
110
- throw new SupercovError("FILTER_UNAVAILABLE", "This run does not contain outcome-filtered coverage. Create a new coverage run.");
111
- }
112
- return filtered;
113
- }
114
- function readJson(path) {
115
- try {
116
- return JSON.parse(readFileSync(path, "utf8"));
117
- }
118
- catch {
119
- return undefined;
120
- }
121
- }
122
- function discoverRuns(root) {
123
- const runs = new Map();
124
- const canonical = resolve(root, ".supercov/runs");
125
- if (existsSync(canonical)) {
126
- for (const entry of readdirSync(canonical, { withFileTypes: true })) {
127
- if (!entry.isDirectory())
128
- continue;
129
- const evidencePath = resolve(canonical, entry.name, "evidence.raw.gz");
130
- const metadata = readJson(resolve(canonical, entry.name, "run.json"));
131
- if (!existsSync(evidencePath) ||
132
- !metadata ||
133
- metadata.rawEvidence?.schemaVersion !== EVIDENCE_ARCHIVE_SCHEMA_VERSION)
134
- continue;
135
- runs.set(entry.name, {
136
- id: entry.name,
137
- evidencePath,
138
- metadata,
139
- });
140
- }
141
- }
142
- return [...runs.values()].sort((left, right) => right.id.localeCompare(left.id));
143
- }
144
- function storedRunAnalysisOptions(run) {
145
- return {
146
- runId: run.id,
147
- testExitCode: run.metadata?.testExitCode,
148
- integrity: run.metadata?.integrity,
149
- generatedAt: run.metadata?.startedAt,
150
- };
151
- }
152
- function analyzeStoredRun(run) {
153
- return analyzeCoverageArchiveCached(run.evidencePath, storedRunAnalysisOptions(run));
154
- }
155
- function readStoredRunIndex(run) {
156
- return readCoverageQueryIndex(run.evidencePath, storedRunAnalysisOptions(run));
157
- }
158
- function selectRun(root, selector, currentIntegrity, quiet = false) {
159
- const runs = discoverRuns(root);
160
- if (runs.length === 0)
161
- throw new SupercovError("NO_RUNS", "No local coverage runs. Run supercov first.");
162
- const selected = !selector || selector === "latest"
163
- ? runs[0]
164
- : (runs.find((run) => run.id === selector) ??
165
- runs.find((run) => run.id.startsWith(selector)));
166
- if (!selected)
167
- throw new SupercovError("RUN_NOT_FOUND", `Coverage run not found: ${selector}`, {
168
- details: { selector },
169
- });
170
- const report = analyzeStoredRun(selected);
171
- if (currentIntegrity) {
172
- const comparison = compareRunIntegrity(selected.metadata?.integrity ?? report.integrity, currentIntegrity);
173
- report.integrity = {
174
- ...(selected.metadata?.integrity ?? report.integrity ?? currentIntegrity),
175
- stale: comparison.stale,
176
- staleReasons: comparison.reasons,
177
- };
178
- if (comparison.stale && !quiet) {
179
- console.error(`[supercov] stale run ${selected.id}: ${comparison.reasons.join(", ")}`);
180
- }
181
- }
182
- return { run: selected, report };
183
- }
184
- function currentProjectIntegrity(root) {
185
- try {
186
- return createRunIntegrity(root, discoverCoverageProject(root), fileURLToPath(new URL(".", import.meta.url)));
187
- }
188
- catch {
189
- return undefined;
190
- }
191
- }
192
- function page(values, options) {
193
- return values.slice(options.offset, options.offset + options.limit);
194
- }
195
- function output(value, options, text, pagination) {
196
- process.stdout.write(options.json ? agentSuccessJson(options.command, value, pagination) : `${text}\n`);
197
- }
198
- function queryPagination(total, returned, options) {
199
- return agentPagination(options.offset, options.limit, returned, total);
200
- }
201
- function pct(value) {
202
- return `${value.toFixed(2)}%`;
203
- }
204
- function shellQuote(value) {
205
- return `'${value.replaceAll("'", `'\\''`)}'`;
206
- }
207
- function optionKey(option) {
208
- return [...new Set(option)].sort().join("\0");
209
- }
210
- function minimumTestObligations(report, candidates) {
211
- const tests = new Map(report.tests.map((test) => [test.id, test]));
212
- const testsByFile = new Map();
213
- for (const test of report.tests) {
214
- if (test.role !== "test" || !test.file || !candidates.has(test.id))
215
- continue;
216
- const ids = testsByFile.get(test.file) ?? [];
217
- ids.push(test.id);
218
- testsByFile.set(test.file, ids);
219
- }
220
- const evidenceChoices = (ids) => {
221
- const options = [];
222
- for (const id of ids) {
223
- const test = tests.get(id);
224
- if (!test)
225
- continue;
226
- if (test.role === "background")
227
- options.push([]);
228
- else if (test.role === "setup" && test.file)
229
- options.push(...(testsByFile.get(test.file) ?? []).map((candidate) => [candidate]));
230
- else if (candidates.has(id))
231
- options.push([id]);
232
- }
233
- return [...new Map(options.map((option) => [optionKey(option), option])).values()];
234
- };
235
- const obligations = [];
236
- const uniqueLines = new Map();
237
- for (const line of report.lines)
238
- uniqueLines.set(`${line.file}:${line.line}`, line);
239
- for (const [id, line] of uniqueLines)
240
- obligations.push({ id: `line:${id}`, metric: "lines", options: evidenceChoices(line.tests) });
241
- for (const point of report.points)
242
- obligations.push({
243
- id: `${point.meta.kind}:${point.meta.id}`,
244
- metric: point.meta.kind === "statement" ? "statements" : "functions",
245
- options: evidenceChoices(point.tests),
246
- });
247
- for (const branch of report.branches)
248
- for (const alternative of branch.alternatives)
249
- obligations.push({
250
- id: `branch:${branch.meta.id}:${alternative.id}`,
251
- metric: "branches",
252
- options: evidenceChoices(alternative.tests),
253
- });
254
- for (const decision of report.decisions) {
255
- for (let condition = 0; condition < decision.meta.conditions.length; condition += 1) {
256
- const options = [];
257
- for (let left = 0; left < decision.vectorObservations.length; left += 1) {
258
- for (let right = left + 1; right < decision.vectorObservations.length; right += 1) {
259
- const first = decision.vectorObservations[left];
260
- const second = decision.vectorObservations[right];
261
- if (!isIndependencePair(first.vector, second.vector, condition))
262
- continue;
263
- for (const firstChoice of evidenceChoices(first.tests))
264
- for (const secondChoice of evidenceChoices(second.tests))
265
- options.push([...new Set([...firstChoice, ...secondChoice])].sort());
266
- }
267
- }
268
- obligations.push({
269
- id: `mcdc:${decision.meta.id}:${condition}`,
270
- metric: "mcdc",
271
- options: [...new Map(options.map((option) => [optionKey(option), option])).values()],
272
- });
273
- }
274
- }
275
- return {
276
- obligations,
277
- expand(selected) {
278
- const expanded = new Set(selected);
279
- for (const test of report.tests) {
280
- if (test.role === "background")
281
- expanded.add(test.id);
282
- else if (test.role === "setup" &&
283
- test.file &&
284
- (testsByFile.get(test.file) ?? []).some((id) => selected.has(id)))
285
- expanded.add(test.id);
286
- }
287
- return expanded;
288
- },
289
- };
290
- }
291
- function obligationSatisfied(obligation, selected) {
292
- return obligation.options.some((option) => option.every((test) => selected.has(test)));
293
- }
294
- /** Exact branch-and-bound solver; MC/DC obligations retain their witness-pair structure. */
295
- export function minimumTestSet(report, target = 100, metric = "all", maxStates = 5_000) {
296
- const unattributed = report.tests.filter((test) => test.role === "background" &&
297
- (test.hits.length > 0 || test.decisions.some((decision) => decision.vectors.length > 0)));
298
- if (unattributed.length > 0) {
299
- 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.");
300
- }
301
- const candidateTests = report.tests
302
- .filter((test) => test.role === "test")
303
- .map((test) => test.id)
304
- .sort();
305
- const candidates = new Set(candidateTests);
306
- const model = minimumTestObligations(report, candidates);
307
- const metrics = metric === "all"
308
- ? ["lines", "statements", "functions", "branches", "mcdc"]
309
- : [metric];
310
- const obligations = model.obligations.filter((obligation) => metrics.includes(obligation.metric));
311
- const totals = new Map();
312
- for (const obligation of obligations)
313
- totals.set(obligation.metric, (totals.get(obligation.metric) ?? 0) + 1);
314
- const skipLimits = new Map();
315
- for (const selectedMetric of metrics) {
316
- const total = totals.get(selectedMetric) ?? 0;
317
- const required = Math.ceil((total * target) / 100);
318
- skipLimits.set(selectedMetric, total - required);
319
- }
320
- let best = new Set(candidateTests);
321
- const fullExpanded = model.expand(best);
322
- const fullSummary = coverageSummaryForTests(report, fullExpanded);
323
- const percentage = (selectedMetric, summary) => selectedMetric === "mcdc" ? summary.conditionCoveragePct : summary[selectedMetric].percentage;
324
- const impossible = metrics.find((selectedMetric) => percentage(selectedMetric, fullSummary) + 1e-9 < target);
325
- if (impossible)
326
- 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) } });
327
- let exploredStates = 0;
328
- const seen = new Set();
329
- const search = (selected, skipped, skippedByMetric) => {
330
- exploredStates += 1;
331
- if (exploredStates > maxStates) {
332
- throw new SupercovError("MINIMIZATION_COMPLEXITY_LIMIT", `Exact minimization exceeded its ${maxStates.toLocaleString()}-state safety budget. Narrow the test view with --kind or --runner, or request a different target.`, {
333
- details: {
334
- candidateTests: candidateTests.length,
335
- obligations: obligations.length,
336
- exploredStates,
337
- maxStates,
338
- target,
339
- metric,
340
- },
341
- });
342
- }
343
- if (selected.size >= best.size)
344
- return;
345
- const stateKey = `${[...selected].sort().join(",")}|${[...skipped].sort().join(",")}`;
346
- if (seen.has(stateKey))
347
- return;
348
- seen.add(stateKey);
349
- const unmet = obligations.filter((obligation) => !skipped.has(obligation.id) && !obligationSatisfied(obligation, selected));
350
- if (unmet.length === 0) {
351
- best = new Set(selected);
352
- return;
353
- }
354
- const obligation = unmet.sort((left, right) => {
355
- const feasible = (value) => value.options.filter((option) => option.some((test) => !selected.has(test))).length;
356
- return feasible(left) - feasible(right) || left.id.localeCompare(right.id);
357
- })[0];
358
- const additions = [...new Map(obligation.options
359
- .map((option) => option.filter((test) => !selected.has(test)))
360
- .filter((option) => option.length > 0)
361
- .map((option) => [optionKey(option), option])).values()].sort((left, right) => left.length - right.length || optionKey(left).localeCompare(optionKey(right)));
362
- for (const addition of additions) {
363
- if (selected.size + addition.length >= best.size)
364
- continue;
365
- search(new Set([...selected, ...addition]), skipped, skippedByMetric);
366
- }
367
- const skippedCount = skippedByMetric.get(obligation.metric) ?? 0;
368
- if (skippedCount < (skipLimits.get(obligation.metric) ?? 0)) {
369
- const nextSkipped = new Set(skipped);
370
- nextSkipped.add(obligation.id);
371
- const nextCounts = new Map(skippedByMetric);
372
- nextCounts.set(obligation.metric, skippedCount + 1);
373
- search(selected, nextSkipped, nextCounts);
374
- }
375
- };
376
- search(new Set(), new Set(), new Map());
377
- const expanded = model.expand(best);
378
- return {
379
- optimal: true,
380
- target,
381
- metric,
382
- selected: [...best].sort(),
383
- expanded: [...expanded].sort(),
384
- summary: coverageSummaryForTests(report, expanded),
385
- exploredStates,
386
- };
387
- }
388
- function coverageCommand(runId, options, child) {
389
- return [
390
- "npx supercov runs",
391
- shellQuote(runId),
392
- "coverage",
393
- child,
394
- options.filter !== "all" ? `--filter ${options.filter}` : undefined,
395
- options.kind ? `--kind ${shellQuote(options.kind)}` : undefined,
396
- options.runner ? `--runner ${shellQuote(options.runner)}` : undefined,
397
- options.metric !== "all" ? `--metric ${options.metric}` : undefined,
398
- ]
399
- .filter(Boolean)
400
- .join(" ");
401
- }
402
- function pageLabel(total, returned, options) {
403
- const start = total === 0 || returned === 0 ? 0 : options.offset + 1;
404
- const end = Math.min(options.offset + returned, total);
405
- return `showing ${start}-${end} of ${total}`;
406
- }
407
- function nextPageCommand(base, total, returned, options) {
408
- const nextOffset = options.offset + returned;
409
- if (returned === 0 || nextOffset >= total)
410
- return undefined;
411
- return `${base} --offset ${nextOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`;
412
- }
413
- function selectedTestIds(report, options) {
414
- if (!options.kind && !options.runner)
415
- return undefined;
416
- const selected = report.tests.filter((test) => (!options.kind || test.provenance.kind === options.kind) &&
417
- (!options.runner || test.provenance.runner === options.runner));
418
- if (selected.length === 0) {
419
- const filter = [
420
- options.kind ? `kind=${options.kind}` : undefined,
421
- options.runner ? `runner=${options.runner}` : undefined,
422
- ]
423
- .filter(Boolean)
424
- .join(", ");
425
- throw new SupercovError("TEST_FILTER_EMPTY", `No tests match ${filter}`, {
426
- details: { kind: options.kind, runner: options.runner },
427
- });
428
- }
429
- return new Set(selected.map((test) => test.id));
430
- }
431
- function includesSelectedTest(tests, selected) {
432
- return selected ? tests.some((test) => selected.has(test)) : tests.length > 0;
433
- }
434
- function otherCoverage(report, testIds, selected) {
435
- const tests = selected
436
- ? testIds
437
- .filter((id) => !selected.has(id))
438
- .map((id) => report.tests.find((test) => test.id === id))
439
- .filter((test) => Boolean(test))
440
- : [];
441
- return {
442
- coveredElsewhere: tests.length > 0,
443
- kinds: [...new Set(tests.map((test) => test.provenance.kind))].sort(),
444
- runners: [...new Set(tests.map((test) => test.provenance.runner))].sort(),
445
- tests: tests.map((test) => ({ id: test.id, name: test.name })),
446
- };
447
- }
448
- function filterDecision(decision, selected) {
449
- if (!selected)
450
- return decision;
451
- const vectorObservations = decision.vectorObservations
452
- .map((observation) => ({
453
- ...observation,
454
- tests: observation.tests.filter((test) => selected.has(test)),
455
- }))
456
- .filter((observation) => observation.tests.length > 0);
457
- const vectors = vectorObservations.map((observation) => observation.vector);
458
- const conditions = decision.meta.conditions.map((source, index) => {
459
- let witness;
460
- for (let left = 0; left < vectors.length && !witness; left += 1) {
461
- for (let right = left + 1; right < vectors.length; right += 1) {
462
- const first = vectors[left];
463
- const second = vectors[right];
464
- if (isIndependencePair(first, second, index)) {
465
- witness = [first, second];
466
- break;
467
- }
468
- }
469
- }
470
- const witnessTests = witness?.map((vector) => vectorObservations.find((observation) => observation.vector === vector)
471
- ?.tests ?? []);
472
- return {
473
- index,
474
- source,
475
- covered: Boolean(witness),
476
- ...(witness ? { witness } : {}),
477
- ...(witnessTests ? { witnessTests } : {}),
478
- };
479
- });
480
- return {
481
- ...decision,
482
- executed: vectors.length > 0,
483
- covered: conditions.every((condition) => condition.covered),
484
- vectors,
485
- vectorObservations,
486
- conditions,
487
- tests: decision.tests.filter((test) => selected.has(test)),
488
- };
489
- }
490
- function filterLabel(options) {
491
- return [
492
- options.filter !== "all" ? `${options.filter} attempts only` : undefined,
493
- options.kind ? `kind ${options.kind}` : undefined,
494
- options.runner ? `runner ${options.runner}` : undefined,
495
- ]
496
- .filter(Boolean)
497
- .join(", ");
498
- }
499
- function queryFilters(options) {
500
- return {
501
- outcome: options.filter,
502
- kind: options.kind ?? null,
503
- runner: options.runner ?? null,
504
- };
505
- }
506
- function attribution(report, selected) {
507
- const phases = selected
508
- ? report.phases.filter((phase) => selected.has(phase.test))
509
- : report.phases;
510
- return {
511
- browserExplicit: phases.reduce((sum, phase) => sum + phase.explicitBrowserEvents, 0),
512
- browserFallback: phases.reduce((sum, phase) => sum + phase.inferredBrowserEvents, 0),
513
- serverExplicit: phases.reduce((sum, phase) => sum + phase.explicitServerEvents, 0),
514
- serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
515
- };
516
- }
517
- export function coverageDiagnostics(report, selected) {
518
- const observed = attribution(report, selected);
519
- const diagnostics = [];
520
- // Assertion phases prove that the runner adapter observed the attempt, but
521
- // they do not prove that the test executed measured application source. A
522
- // contract/static-data assertion can legitimately have no coverage. Keep
523
- // this visible as an ambiguity warning; only explicit corrupt transport is
524
- // strong enough to claim evidence loss as an error.
525
- const emptyEvidenceTests = report.tests.filter((test) => (test.role ?? "test") === "test" &&
526
- (!selected || selected.has(test.id)) &&
527
- test.lines.length === 0 &&
528
- test.hits.length === 0 &&
529
- test.decisions.length === 0 &&
530
- report.phases.some((phase) => phase.test === test.id));
531
- if (emptyEvidenceTests.length > 0) {
532
- diagnostics.push({
533
- code: "TEST_EVIDENCE_MISSING",
534
- severity: "warning",
535
- message: `${emptyEvidenceTests.length} test(s) recorded assertion phases but attributed zero coverage evidence; ` +
536
- `this is valid for assertions over static or uninstrumented data, but may otherwise indicate missing probe transport. ` +
537
- `First: ${emptyEvidenceTests[0].name}`,
538
- });
539
- }
540
- if ((report.transport?.corruptRecords ?? 0) > 0) {
541
- diagnostics.push({
542
- code: "CORRUPT_EVIDENCE_RECORDS",
543
- severity: "error",
544
- message: `${report.transport.corruptRecords} malformed evidence record(s) in ` +
545
- `${report.transport.corruptFiles} file(s) were excluded; coverage is incomplete.`,
546
- });
547
- }
548
- if ((report.transport?.remoteLaunches ?? 0) > 0 &&
549
- (report.transport?.scopedServerRecords ?? 0) === 0 &&
550
- observed.serverExplicit === 0 &&
551
- observed.serverFallback === 0) {
552
- diagnostics.push({
553
- code: "REMOTE_SERVER_EVIDENCE_MISSING",
554
- severity: "warning",
555
- message: "Remote launches were supervised, but no server evidence returned. Coverage may describe only browser/test processes; inspect how the application server is launched.",
556
- });
557
- }
558
- return diagnostics;
559
- }
560
- function gapMetricValue(gap, metric) {
561
- if (metric === "all")
562
- return gap.score;
563
- if (metric === "lines")
564
- return gap.uncoveredLines;
565
- if (metric === "statements")
566
- return gap.uncoveredStatements;
567
- if (metric === "functions")
568
- return gap.uncoveredFunctions;
569
- if (metric === "branches")
570
- return gap.missingBranches;
571
- return gap.missingMcdcConditions;
572
- }
573
- function obligationMatchesMetric(obligation, metric) {
574
- if (metric === "all")
575
- return true;
576
- const kindByMetric = {
577
- lines: "line",
578
- statements: "statement",
579
- functions: "function",
580
- branches: "branch",
581
- mcdc: "mcdc",
582
- };
583
- return obligation.kind === kindByMetric[metric];
584
- }
585
- export function fileGaps(report, selected) {
586
- const files = new Map();
587
- const get = (file) => {
588
- const existing = files.get(file);
589
- if (existing)
590
- return existing;
591
- const created = {
592
- file,
593
- uncoveredLines: 0,
594
- uncoveredStatements: 0,
595
- uncoveredFunctions: 0,
596
- missingBranches: 0,
597
- missingMcdcConditions: 0,
598
- measurementLimitations: 0,
599
- limitationKinds: [],
600
- coveredByOtherTests: {
601
- lines: 0,
602
- statements: 0,
603
- functions: 0,
604
- branches: 0,
605
- mcdcConditions: 0,
606
- },
607
- uncoveredEverywhere: {
608
- lines: 0,
609
- statements: 0,
610
- functions: 0,
611
- branches: 0,
612
- mcdcConditions: 0,
613
- },
614
- score: 0,
615
- };
616
- files.set(file, created);
617
- return created;
618
- };
619
- const classify = (gap, dimension, coveredOverall) => {
620
- if (selected && coveredOverall)
621
- gap.coveredByOtherTests[dimension] += 1;
622
- else
623
- gap.uncoveredEverywhere[dimension] += 1;
624
- };
625
- for (const line of report.lines) {
626
- const gap = get(line.file);
627
- if (!includesSelectedTest(line.tests, selected)) {
628
- gap.uncoveredLines += 1;
629
- classify(gap, "lines", line.covered);
630
- }
631
- }
632
- for (const point of report.points) {
633
- const gap = get(point.meta.file);
634
- if (!includesSelectedTest(point.tests, selected)) {
635
- if (point.meta.kind === "function")
636
- gap.uncoveredFunctions += 1;
637
- else
638
- gap.uncoveredStatements += 1;
639
- classify(gap, point.meta.kind === "function" ? "functions" : "statements", point.covered);
640
- }
641
- }
642
- for (const branch of report.branches) {
643
- const gap = get(branch.meta.file);
644
- for (const alternative of branch.alternatives) {
645
- if (!includesSelectedTest(alternative.tests, selected)) {
646
- gap.missingBranches += 1;
647
- classify(gap, "branches", alternative.covered);
648
- }
649
- }
650
- }
651
- for (const decision of report.decisions) {
652
- const gap = get(decision.meta.file);
653
- const filteredConditions = filterDecision(decision, selected).conditions;
654
- for (const condition of filteredConditions) {
655
- if (!condition.covered) {
656
- gap.missingMcdcConditions += 1;
657
- classify(gap, "mcdcConditions", decision.conditions[condition.index]?.covered ?? false);
658
- }
659
- }
660
- }
661
- for (const limitation of report.limitations ?? []) {
662
- const gap = get(limitation.file);
663
- gap.measurementLimitations += 1;
664
- if (!gap.limitationKinds.includes(limitation.kind))
665
- gap.limitationKinds.push(limitation.kind);
666
- }
667
- for (const gap of files.values()) {
668
- gap.limitationKinds.sort();
669
- gap.score =
670
- gap.uncoveredLines +
671
- gap.uncoveredFunctions * 2 +
672
- gap.missingBranches * 2 +
673
- gap.missingMcdcConditions * 3 +
674
- gap.measurementLimitations * 3;
675
- }
676
- return [...files.values()].sort((left, right) => right.score - left.score || left.file.localeCompare(right.file));
677
- }
678
- function findFile(report, selector) {
679
- const files = [
680
- ...new Set([
681
- ...report.lines.map((line) => line.file),
682
- ...(report.limitations ?? []).map((limitation) => limitation.file),
683
- ]),
684
- ];
685
- if (files.includes(selector))
686
- return selector;
687
- const matches = files.filter((file) => file.includes(selector));
688
- if (matches.length === 1)
689
- return matches[0];
690
- if (matches.length === 0)
691
- throw new SupercovError("SOURCE_NOT_FOUND", `Source file not found: ${selector}`, {
692
- details: { selector },
693
- });
694
- throw new SupercovError("AMBIGUOUS_SELECTOR", `Ambiguous file selector: ${matches.join(", ")}`, {
695
- details: { selector, matches },
696
- });
697
- }
698
- export function coverageMeasurement(report) {
699
- const limitations = report.limitations ?? [];
700
- const evidenceCorruptions = report.transport?.corruptRecords ?? 0;
701
- const byKind = {
702
- "dynamic-code": 0,
703
- "semantic-safety": 0,
704
- "source-scope": 0,
705
- };
706
- for (const limitation of limitations)
707
- byKind[limitation.kind] += 1;
708
- return {
709
- complete: limitations.length === 0 && evidenceCorruptions === 0,
710
- limitations: limitations.length,
711
- evidenceCorruptions,
712
- // Every current limitation removes source from the measured denominator.
713
- blocking: limitations.length + evidenceCorruptions,
714
- files: new Set(limitations.map((limitation) => limitation.file)).size +
715
- (report.transport?.corruptFiles ?? 0),
716
- byKind,
717
- };
718
- }
719
- export function locationSelector(selector) {
720
- const match = /^(.*):(\d+):\d+$/.exec(selector) ?? /^(.*):(\d+)$/.exec(selector);
721
- if (!match)
722
- throw new SupercovError("INVALID_ARGUMENT", "Expected <source-file>:<line>", {
723
- details: { selector },
724
- });
725
- return { file: match[1], line: Number(match[2]) };
726
- }
727
- function vectorText(values, outcome) {
728
- return `${values.map((value) => (value === null ? "-" : value ? "T" : "F")).join("")} -> ${outcome ? "T" : "F"}`;
729
- }
730
- const helpText = `Agent-oriented local coverage queries:
731
- supercov runs [--limit N] [--json]
732
- supercov runs <run-id> coverage [--filter all|passed|failed] [--kind e2e] [--runner playwright] [--json]
733
- supercov runs <run-id> coverage kinds [--json]
734
- supercov runs <run-id> coverage runners [--json]
735
- supercov runs <run-id> coverage scope [--limit N] [--offset N] [--json]
736
- supercov runs <run-id> coverage files [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--limit N] [--offset N] [--json]
737
- supercov runs <run-id> coverage gaps [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
738
- supercov runs <run-id> coverage file <source-file> [--metric all|lines|statements|functions|branches|mcdc] [--group decision] [--sort location|missing] [--kind e2e] [--limit N] [--offset N] [--json]
739
- supercov runs <run-id> coverage decision <id|source-file:line> [--kind e2e] [--json]
740
- supercov runs <run-id> coverage covers <source-file:line> [--kind e2e] [--json]
741
- supercov runs <run-id> coverage test <id|name-fragment> [--kind e2e] [--limit N] [--json]
742
- supercov runs <run-id> coverage minimize [--target 0..100] [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--limit N] [--offset N] [--json]
743
- supercov diff <older-run> <newer-run> [--limit N] [--json]
744
- supercov merge <run-id> <run-id> [...]
745
- supercov prune [--keep N] [--dry-run]
746
- supercov clean [--keep N] [--dry-run]
747
-
748
- Use "latest" as <run-id> to query the newest local run.
749
-
750
- Reviewed MC/DC waivers (optional ${WAIVERS_FILE} at the project root):
751
- {"version":1,"waivers":[{"file":"src/x.ts","decision":"<id or source>","line":12,"condition":"<source or C2>","reason":"..."}]}
752
- Waived conditions stay uncovered in every raw total and are reported separately.
753
- Waive only conditions with no satisfiable independence pair. A condition that
754
- some platform or environment simply never runs here is a coverage gap, not an
755
- impossibility; keep it visible. Prefer omitting "line" (it breaks on edits
756
- above it and is then reported as unmatched).
757
-
758
- Create a run with:
759
- supercov -- <test command>`;
760
- function help(options) {
761
- if (options.json) {
762
- return output({
763
- usage: "supercov -- <test command>",
764
- runSelector: "Use latest as <run-id> to query the newest local run.",
765
- queryCommands: [
766
- "runs",
767
- "runs <run-id> coverage",
768
- "runs <run-id> coverage kinds",
769
- "runs <run-id> coverage runners",
770
- "runs <run-id> coverage scope",
771
- "runs <run-id> coverage files",
772
- "runs <run-id> coverage gaps",
773
- "runs <run-id> coverage file <source-file>",
774
- "runs <run-id> coverage decision <id|source-file:line>",
775
- "runs <run-id> coverage covers <source-file:line>",
776
- "runs <run-id> coverage test <id|name-fragment>",
777
- "runs <run-id> coverage minimize",
778
- "diff <older-run> <newer-run>",
779
- ],
780
- }, options, helpText);
781
- }
782
- console.log(helpText);
783
- }
784
- /** Resolve the instance-first coverage resource syntax. */
785
- export function resolveCoverageQueryInvocation(command, args) {
786
- if (command !== "runs")
787
- return { command, args };
788
- const runId = args[0];
789
- if (!runId || runId.startsWith("-")) {
790
- return { command, args };
791
- }
792
- if (args[1] !== "coverage") {
793
- // A positional run ID must never silently degrade to the run listing:
794
- // an agent that omits "coverage" would otherwise get unrelated output.
795
- throw new SupercovError("UNKNOWN_COMMAND", args[1] === undefined || args[1].startsWith("-")
796
- ? `Missing coverage query after run ${runId}. Expected: supercov runs <run-id> coverage [<query>]. Try supercov help.`
797
- : `Unknown runs query: ${args[1]}. Expected: supercov runs <run-id> coverage [<query>]. Try supercov help.`, { details: { run: runId, command: args[1] ?? null } });
798
- }
799
- const childToken = args[2];
800
- const hasChild = Boolean(childToken && !childToken.startsWith("-"));
801
- const child = hasChild ? childToken : "summary";
802
- const childArgs = args.slice(hasChild ? 3 : 2);
803
- const coverageCommands = new Set([
804
- "summary",
805
- "kinds",
806
- "runners",
807
- "scope",
808
- "files",
809
- "gaps",
810
- "file",
811
- "decision",
812
- "covers",
813
- "test",
814
- "minimize",
815
- ]);
816
- if (!coverageCommands.has(child)) {
817
- throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${child}. Try supercov help.`, { details: { command: child } });
818
- }
819
- return {
820
- command: child,
821
- args: ["--run", runId, ...childArgs],
822
- };
823
- }
824
- export async function runQueryCommand(command, args, root = process.cwd()) {
825
- const resolved = resolveCoverageQueryInvocation(command, args);
826
- command = resolved.command;
827
- const options = parseOptions(command, resolved.args);
828
- if (command === "help")
829
- return help(options);
830
- const currentIntegrity = currentProjectIntegrity(root);
831
- const waiverSource = readCoverageWaivers(root);
832
- if (command === "runs") {
833
- const availableRuns = discoverRuns(root);
834
- const runs = page(availableRuns, options).map((run) => {
835
- const cached = readStoredRunIndex(run);
836
- const report = cached ? filteredCoverage(cached, options) : undefined;
837
- return {
838
- id: run.id,
839
- generatedAt: report?.generatedAt ?? run.metadata?.startedAt,
840
- coverageIndexed: Boolean(report),
841
- lines: report?.summary.lines.percentage ?? null,
842
- branches: report?.summary.branches.percentage ?? null,
843
- mcdc: report?.summary.conditionCoveragePct ?? null,
844
- command: run.metadata?.command,
845
- durationMs: run.metadata?.durationMs,
846
- timings: run.metadata?.timings,
847
- testExitCode: run.metadata?.testExitCode,
848
- buildReused: run.metadata?.instrumentedBuildCache?.reused,
849
- rawEvidence: run.metadata?.rawEvidence,
850
- ...(currentIntegrity
851
- ? compareRunIntegrity(run.metadata?.integrity, currentIntegrity)
852
- : { stale: undefined, reasons: [] }),
853
- };
854
- });
855
- const runsBase = `npx supercov runs${options.filter !== "all" ? ` --filter ${options.filter}` : ""}`;
856
- const runsNext = nextPageCommand(runsBase, availableRuns.length, runs.length, options);
857
- return output({ filters: queryFilters(options), runs }, options, runs
858
- .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(", ")})` : ""}`)
859
- .join("\n") +
860
- `\n${pageLabel(availableRuns.length, runs.length, options)}` +
861
- (runsNext ? `\nnext page: ${runsNext}` : ""), queryPagination(availableRuns.length, runs.length, options));
862
- }
863
- if (command === "diff") {
864
- const [olderSelector, newerSelector] = options.positional;
865
- if (!olderSelector || !newerSelector)
866
- throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov diff <older-run> <newer-run>");
867
- const olderSelected = selectRun(root, olderSelector, currentIntegrity, options.json);
868
- const newerSelected = selectRun(root, newerSelector, currentIntegrity, options.json);
869
- const older = {
870
- ...olderSelected,
871
- report: filteredCoverage(olderSelected.report, options),
872
- };
873
- const newer = {
874
- ...newerSelected,
875
- report: filteredCoverage(newerSelected.report, options),
876
- };
877
- const key = (file, line) => `${file}:${line}`;
878
- const oldLines = new Set(older.report.lines
879
- .filter((line) => line.covered)
880
- .map((line) => key(line.file, line.line)));
881
- const newLines = new Set(newer.report.lines
882
- .filter((line) => line.covered)
883
- .map((line) => key(line.file, line.line)));
884
- const branchKeys = (report) => new Map(report.branches.flatMap((branch) => branch.alternatives
885
- .filter((alternative) => alternative.covered)
886
- .map((alternative) => [
887
- `${branch.meta.id}:${alternative.id}`,
888
- `${branch.meta.file}:${branch.meta.line} ${alternative.label}`,
889
- ])));
890
- const mcdcKeys = (report) => new Map(report.decisions.flatMap((decision) => decision.conditions
891
- .filter((condition) => condition.covered)
892
- .map((condition) => [
893
- `${decision.meta.id}:c${condition.index}`,
894
- `${decision.meta.file}:${decision.meta.line} C${condition.index + 1} ${condition.source}`,
895
- ])));
896
- const oldBranches = branchKeys(older.report);
897
- const newBranches = branchKeys(newer.report);
898
- const oldMcdc = mcdcKeys(older.report);
899
- const newMcdc = mcdcKeys(newer.report);
900
- const gainedLines = [...newLines]
901
- .filter((line) => !oldLines.has(line))
902
- .sort();
903
- const lostLines = [...oldLines]
904
- .filter((line) => !newLines.has(line))
905
- .sort();
906
- const gainedBranches = [...newBranches]
907
- .filter(([id]) => !oldBranches.has(id))
908
- .map(([, label]) => label)
909
- .sort();
910
- const lostBranches = [...oldBranches]
911
- .filter(([id]) => !newBranches.has(id))
912
- .map(([, label]) => label)
913
- .sort();
914
- const gainedMcdc = [...newMcdc]
915
- .filter(([id]) => !oldMcdc.has(id))
916
- .map(([, label]) => label)
917
- .sort();
918
- const lostMcdc = [...oldMcdc]
919
- .filter(([id]) => !newMcdc.has(id))
920
- .map(([, label]) => label)
921
- .sort();
922
- const result = {
923
- filters: queryFilters(options),
924
- older: older.run.id,
925
- newer: newer.run.id,
926
- delta: {
927
- lines: Number((newer.report.summary.lines.percentage -
928
- older.report.summary.lines.percentage).toFixed(2)),
929
- branches: Number((newer.report.summary.branches.percentage -
930
- older.report.summary.branches.percentage).toFixed(2)),
931
- mcdc: Number((newer.report.summary.conditionCoveragePct -
932
- older.report.summary.conditionCoveragePct).toFixed(2)),
933
- },
934
- gained: {
935
- lineCount: gainedLines.length,
936
- branchCount: gainedBranches.length,
937
- mcdcCount: gainedMcdc.length,
938
- lines: page(gainedLines, options),
939
- branches: page(gainedBranches, options),
940
- mcdc: page(gainedMcdc, options),
941
- },
942
- lost: {
943
- lineCount: lostLines.length,
944
- branchCount: lostBranches.length,
945
- mcdcCount: lostMcdc.length,
946
- lines: page(lostLines, options),
947
- branches: page(lostBranches, options),
948
- mcdc: page(lostMcdc, options),
949
- },
950
- };
951
- const diffTotal = Math.max(gainedLines.length, gainedBranches.length, gainedMcdc.length, lostLines.length, lostBranches.length, lostMcdc.length);
952
- const diffReturned = Math.max(result.gained.lines.length, result.gained.branches.length, result.gained.mcdc.length, result.lost.lines.length, result.lost.branches.length, result.lost.mcdc.length);
953
- const diffBase = [
954
- "npx supercov diff",
955
- shellQuote(older.run.id),
956
- shellQuote(newer.run.id),
957
- options.filter !== "all" ? `--filter ${options.filter}` : undefined,
958
- ]
959
- .filter(Boolean)
960
- .join(" ");
961
- const diffNext = nextPageCommand(diffBase, diffTotal, diffReturned, options);
962
- 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));
963
- }
964
- const selectedRun = selectRun(root, options.run, currentIntegrity, options.json);
965
- const run = selectedRun.run;
966
- const report = filteredCoverage(selectedRun.report, options);
967
- const selectedTestSet = selectedTestIds(report, options);
968
- const waiverEvaluation = waiverSource
969
- ? evaluateCoverageWaivers(report.decisions, waiverSource)
970
- : undefined;
971
- if (command === "summary") {
972
- const summary = selectedTestSet
973
- ? coverageSummaryForTests(report, selectedTestSet)
974
- : report.summary;
975
- const gaps = fileGaps(report, selectedTestSet).filter((gap) => gap.score > 0);
976
- const measurement = coverageMeasurement(report);
977
- const selectedTests = report.tests.filter((test) => !selectedTestSet || selectedTestSet.has(test.id));
978
- const testCount = selectedTests.filter((test) => (test.role ?? "test") === "test").length;
979
- const setupCount = selectedTests.filter((test) => test.role === "setup").length;
980
- const testOutcomes = Object.fromEntries(["passed", "failed", "flaky", "skipped", "timedOut", "interrupted", "unknown"].map((outcome) => [
981
- outcome,
982
- selectedTests.filter((test) => test.role === "test" && test.outcome === outcome).length,
983
- ]));
984
- const diagnostics = coverageDiagnostics(report, selectedTestSet);
985
- const result = {
986
- run: run.id,
987
- filters: queryFilters(options),
988
- generatedAt: report.generatedAt,
989
- valid: run.metadata?.testExitCode === 0,
990
- stale: report.integrity?.stale ?? false,
991
- staleReasons: report.integrity?.staleReasons ?? [],
992
- structurallyComplete: summary.coverageComplete && measurement.complete,
993
- complete: options.filter === "passed" &&
994
- run.metadata?.testExitCode === 0 &&
995
- !report.integrity?.stale &&
996
- summary.coverageComplete &&
997
- measurement.complete,
998
- coverage: summary,
999
- measurement,
1000
- ...(waiverEvaluation
1001
- ? {
1002
- waivers: {
1003
- file: WAIVERS_FILE,
1004
- entries: waiverEvaluation.waivers.length,
1005
- applied: waiverEvaluation.applied.length,
1006
- contradicted: waiverEvaluation.contradicted.map((match) => ({
1007
- file: match.file,
1008
- line: match.line,
1009
- condition: match.conditionSource,
1010
- reason: match.waiver.reason,
1011
- })),
1012
- unmatched: waiverEvaluation.unmatched,
1013
- mcdcExcludingWaived: {
1014
- covered: report.summary.coveredConditions,
1015
- total: report.summary.conditions -
1016
- waiverEvaluation.applied.length,
1017
- percentage: report.summary.conditions -
1018
- waiverEvaluation.applied.length >
1019
- 0
1020
- ? (report.summary.coveredConditions /
1021
- (report.summary.conditions -
1022
- waiverEvaluation.applied.length)) *
1023
- 100
1024
- : 100,
1025
- },
1026
- },
1027
- }
1028
- : {}),
1029
- coverageByKind: report.coverageByKind,
1030
- coverageByRunner: report.coverageByRunner,
1031
- attribution: attribution(report, selectedTestSet),
1032
- transport: report.transport,
1033
- diagnostics,
1034
- ...(!selectedTestSet
1035
- ? {
1036
- confidence: {
1037
- lines: Object.fromEntries(["unexecuted", "executed", "action", "asserted"].map((level) => [
1038
- level,
1039
- report.lines.filter((line) => line.confidence?.level === level).length,
1040
- ])),
1041
- assertionCoveredMcdcConditions: report.decisions.reduce((total, decision) => total +
1042
- decision.conditions.filter((condition) => condition.assertionCovered).length, 0),
1043
- },
1044
- }
1045
- : {}),
1046
- filesWithGaps: gaps.length,
1047
- filesWithCoverageGaps: gaps.filter((gap) => gap.uncoveredLines > 0 ||
1048
- gap.uncoveredStatements > 0 ||
1049
- gap.uncoveredFunctions > 0 ||
1050
- gap.missingBranches > 0 ||
1051
- gap.missingMcdcConditions > 0).length,
1052
- filesWithMeasurementLimitations: measurement.files,
1053
- tests: testCount,
1054
- setups: setupCount,
1055
- testOutcomes,
1056
- sourceScope: report.scope
1057
- ? {
1058
- mode: report.scope.mode,
1059
- roots: report.scope.roots,
1060
- included: report.scope.entries.filter((entry) => entry.status === "included").length,
1061
- excluded: report.scope.entries.filter((entry) => entry.status === "excluded").length,
1062
- ambiguous: report.scope.entries.filter((entry) => entry.status === "ambiguous").length,
1063
- }
1064
- : undefined,
1065
- };
1066
- 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)`}${waiverEvaluation ? `\nwaivers: ${waiverEvaluation.applied.length} applied, ${waiverEvaluation.contradicted.length} contradicted, ${waiverEvaluation.unmatched.length} unmatched; MC/DC excluding waived ${pct(report.summary.conditions - waiverEvaluation.applied.length > 0 ? (report.summary.coveredConditions / (report.summary.conditions - waiverEvaluation.applied.length)) * 100 : 100)} (${report.summary.coveredConditions}/${report.summary.conditions - waiverEvaluation.applied.length})${waiverEvaluation.contradicted.map((match) => `\n contradicted (condition is covered): ${match.file}:${match.line} ${match.conditionSource}`).join("")}${waiverEvaluation.unmatched.map((waiver) => `\n unmatched (no such condition): ${waiver.file}${waiver.line !== undefined ? `:${waiver.line}` : ""} ${waiver.condition}`).join("")}` : ""}${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`);
1067
- }
1068
- if (command === "minimize") {
1069
- const solverReport = selectedTestSet
1070
- ? {
1071
- ...report,
1072
- tests: report.tests.filter((test) => selectedTestSet.has(test.id)),
1073
- }
1074
- : report;
1075
- const minimized = minimumTestSet(solverReport, options.target, options.metric);
1076
- const selectedDetails = minimized.selected.map((id) => {
1077
- const test = report.tests.find((candidate) => candidate.id === id);
1078
- return {
1079
- id,
1080
- name: test.name,
1081
- file: test.file,
1082
- runner: test.provenance.runner,
1083
- kind: test.provenance.kind,
1084
- };
1085
- });
1086
- const selectedPage = page(selectedDetails, options);
1087
- const base = `${coverageCommand(run.id, options, "minimize")} --target ${options.target}`;
1088
- const next = nextPageCommand(base, selectedDetails.length, selectedPage.length, options);
1089
- return output({
1090
- run: run.id,
1091
- filters: queryFilters(options),
1092
- ...minimized,
1093
- selectedCount: selectedDetails.length,
1094
- totalCandidateTests: solverReport.tests.filter((test) => test.role === "test").length,
1095
- tests: selectedPage,
1096
- }, 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));
1097
- }
1098
- if (command === "scope") {
1099
- if (!report.scope)
1100
- throw new SupercovError("SCOPE_UNAVAILABLE", "This run does not contain a source-scope inventory.");
1101
- const limitationsByFile = new Map();
1102
- for (const limitation of report.limitations ?? []) {
1103
- const existing = limitationsByFile.get(limitation.file) ?? [];
1104
- existing.push(limitation);
1105
- limitationsByFile.set(limitation.file, existing);
1106
- }
1107
- const ordered = report.scope.entries.map((entry) => {
1108
- const limitations = limitationsByFile.get(entry.file) ?? [];
1109
- return {
1110
- ...entry,
1111
- measurementLimitations: limitations.length,
1112
- limitationKinds: [...new Set(limitations.map((item) => item.kind))].sort(),
1113
- };
1114
- }).sort((left, right) => {
1115
- const rank = { ambiguous: 0, included: 1, excluded: 2 };
1116
- return rank[left.status] - rank[right.status] || left.file.localeCompare(right.file);
1117
- });
1118
- const selectedEntries = page(ordered, options);
1119
- const base = coverageCommand(run.id, options, "scope");
1120
- const next = nextPageCommand(base, ordered.length, selectedEntries.length, options);
1121
- const counts = {
1122
- included: ordered.filter((entry) => entry.status === "included").length,
1123
- excluded: ordered.filter((entry) => entry.status === "excluded").length,
1124
- ambiguous: ordered.filter((entry) => entry.status === "ambiguous").length,
1125
- };
1126
- return output({
1127
- run: run.id,
1128
- filters: queryFilters(options),
1129
- mode: report.scope.mode,
1130
- roots: report.scope.roots,
1131
- counts,
1132
- measurement: coverageMeasurement(report),
1133
- entries: selectedEntries,
1134
- }, 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));
1135
- }
1136
- if (command === "kinds" || command === "runners") {
1137
- const dimension = command === "kinds" ? report.coverageByKind : report.coverageByRunner;
1138
- const selectedDimension = page(dimension, options);
1139
- const dimensionNext = nextPageCommand(coverageCommand(run.id, options, command), dimension.length, selectedDimension.length, options);
1140
- return output({
1141
- run: run.id,
1142
- filters: queryFilters(options),
1143
- [command]: selectedDimension,
1144
- }, options, selectedDimension
1145
- .map((entry) => {
1146
- const name = entry.kind ?? entry.runner ?? "unknown";
1147
- return `${name} ${entry.tests} test(s)${entry.setups ? ` + ${entry.setups} setup scope(s)` : ""} lines ${pct(entry.summary.lines.percentage)} branches ${pct(entry.summary.branches.percentage)} MC/DC ${pct(entry.summary.conditionCoveragePct)}`;
1148
- })
1149
- .join("\n") +
1150
- `\n${pageLabel(dimension.length, selectedDimension.length, options)}` +
1151
- (dimensionNext ? `\nnext page: ${dimensionNext}` : ""), queryPagination(dimension.length, selectedDimension.length, options));
1152
- }
1153
- if (command === "files" || command === "gaps") {
1154
- const files = fileGaps(report, selectedTestSet);
1155
- const all = files
1156
- .filter((gap) => command === "files" ||
1157
- gapMetricValue(gap, options.metric) > 0 ||
1158
- gap.measurementLimitations > 0)
1159
- .sort((left, right) => gapMetricValue(right, options.metric) - gapMetricValue(left, options.metric) ||
1160
- right.measurementLimitations - left.measurementLimitations ||
1161
- left.file.localeCompare(right.file));
1162
- const selectedFiles = page(all, options).map((gap) => ({
1163
- ...gap,
1164
- ...(waiverEvaluation
1165
- ? {
1166
- waivedMcdcConditions: waiverEvaluation.appliedByFile.get(gap.file) ?? 0,
1167
- }
1168
- : {}),
1169
- }));
1170
- const pageStart = all.length === 0 ? 0 : options.offset + 1;
1171
- const pageEnd = Math.min(options.offset + selectedFiles.length, all.length);
1172
- const nextOffset = options.offset + selectedFiles.length;
1173
- const nextCommand = nextOffset < all.length
1174
- ? `${coverageCommand(run.id, options, command)} --offset ${nextOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
1175
- : undefined;
1176
- return output({
1177
- run: run.id,
1178
- filters: queryFilters(options),
1179
- metric: options.metric,
1180
- [command]: selectedFiles,
1181
- }, options, selectedFiles
1182
- .map((gap) => {
1183
- const missing = gap.uncoveredLines +
1184
- gap.uncoveredStatements +
1185
- gap.uncoveredFunctions +
1186
- gap.missingBranches +
1187
- gap.missingMcdcConditions;
1188
- const status = missing === 0
1189
- ? "coverage complete"
1190
- : `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}${gap.waivedMcdcConditions ? ` (${gap.waivedMcdcConditions} waived)` : ""}`;
1191
- const limitations = gap.measurementLimitations
1192
- ? ` measurement limitations ${gap.measurementLimitations} (${gap.limitationKinds.join(", ")})`
1193
- : "";
1194
- 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)}]` : ""}`;
1195
- })
1196
- .join("\n") +
1197
- `\nshowing ${pageStart}-${pageEnd} of ${all.length}` +
1198
- (nextCommand ? `\nnext page: ${nextCommand}` : ""), queryPagination(all.length, selectedFiles.length, options));
1199
- }
1200
- if (command === "file") {
1201
- const selector = options.positional.join(" ");
1202
- if (!selector)
1203
- throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage file <source-file>");
1204
- const file = findFile(report, selector);
1205
- if (options.group === "decision") {
1206
- const decisionRows = report.decisions
1207
- .filter((decision) => decision.meta.file === file)
1208
- .map((decision) => {
1209
- const filtered = filterDecision(decision, selectedTestSet);
1210
- const waived = waiverEvaluation?.waivedByDecision.get(decision.meta.id);
1211
- const missing = filtered.conditions.filter((condition) => !condition.covered);
1212
- const waivedMissing = missing.filter((condition) => waived?.has(condition.index));
1213
- return {
1214
- id: decision.meta.id,
1215
- line: decision.meta.line,
1216
- column: decision.meta.column,
1217
- kind: decision.meta.kind,
1218
- conditions: filtered.conditions.length,
1219
- missingConditions: missing.length,
1220
- waivedConditions: waivedMissing.length,
1221
- source: decision.meta.source.replace(/\s+/g, " ").trim(),
1222
- };
1223
- });
1224
- const withMissing = decisionRows.filter((row) => row.missingConditions > 0);
1225
- const ordered = [...withMissing].sort((left, right) => options.sort === "missing"
1226
- ? right.missingConditions -
1227
- right.waivedConditions -
1228
- (left.missingConditions - left.waivedConditions) ||
1229
- right.missingConditions - left.missingConditions ||
1230
- left.line - right.line ||
1231
- left.column - right.column
1232
- : left.line - right.line ||
1233
- left.column - right.column ||
1234
- left.id.localeCompare(right.id));
1235
- const rows = page(ordered, options);
1236
- const totals = {
1237
- decisions: decisionRows.length,
1238
- decisionsWithMissingConditions: withMissing.length,
1239
- conditions: decisionRows.reduce((sum, row) => sum + row.conditions, 0),
1240
- missingConditions: decisionRows.reduce((sum, row) => sum + row.missingConditions, 0),
1241
- waivedConditions: decisionRows.reduce((sum, row) => sum + row.waivedConditions, 0),
1242
- };
1243
- const groupedBase = `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --group decision${options.sort !== "location" ? ` --sort ${options.sort}` : ""}`;
1244
- const groupedNext = nextPageCommand(groupedBase, ordered.length, rows.length, options);
1245
- const snippet = (source) => source.length > 96 ? `${source.slice(0, 95)}…` : source;
1246
- return output({
1247
- run: run.id,
1248
- filters: queryFilters(options),
1249
- file,
1250
- group: "decision",
1251
- sort: options.sort,
1252
- totals,
1253
- decisions: rows,
1254
- }, options, `${file} MC/DC by decision\ndecisions ${totals.decisions}, with missing conditions ${totals.decisionsWithMissingConditions}; conditions missing ${totals.missingConditions}/${totals.conditions}${totals.waivedConditions ? `, waived ${totals.waivedConditions}` : ""}\n${rows
1255
- .map((row) => `${row.line}:${row.column} [${row.id}] missing ${row.missingConditions}/${row.conditions}${row.waivedConditions ? ` (${row.waivedConditions} waived)` : ""} ${snippet(row.source)}`)
1256
- .join("\n")}\n${pageLabel(ordered.length, rows.length, options)} decisions with missing conditions${groupedNext ? `\nnext page: ${groupedNext}` : ""}`, queryPagination(ordered.length, rows.length, options));
1257
- }
1258
- const uncoveredLines = report.lines
1259
- .filter((line) => line.file === file &&
1260
- !includesSelectedTest(line.tests, selectedTestSet))
1261
- .map((line) => ({
1262
- kind: "line",
1263
- line: line.line,
1264
- otherCoverage: otherCoverage(report, line.tests, selectedTestSet),
1265
- }));
1266
- const functions = report.points
1267
- .filter((point) => point.meta.file === file &&
1268
- point.meta.kind === "function" &&
1269
- !includesSelectedTest(point.tests, selectedTestSet))
1270
- .map((point) => ({
1271
- kind: "function",
1272
- line: point.meta.line,
1273
- column: point.meta.column,
1274
- source: point.meta.label ?? point.meta.source,
1275
- otherCoverage: otherCoverage(report, point.tests, selectedTestSet),
1276
- }));
1277
- const statements = report.points
1278
- .filter((point) => point.meta.file === file &&
1279
- point.meta.kind === "statement" &&
1280
- !includesSelectedTest(point.tests, selectedTestSet))
1281
- .map((point) => ({
1282
- kind: "statement",
1283
- line: point.meta.line,
1284
- column: point.meta.column,
1285
- source: point.meta.label ?? point.meta.source,
1286
- otherCoverage: otherCoverage(report, point.tests, selectedTestSet),
1287
- }));
1288
- const branches = report.branches
1289
- .filter((branch) => branch.meta.file === file)
1290
- .flatMap((branch) => branch.alternatives
1291
- .filter((alternative) => !includesSelectedTest(alternative.tests, selectedTestSet))
1292
- .map((alternative) => ({
1293
- kind: "branch",
1294
- line: branch.meta.line,
1295
- column: branch.meta.column,
1296
- source: branch.meta.source,
1297
- missing: alternative.label,
1298
- otherCoverage: otherCoverage(report, alternative.tests, selectedTestSet),
1299
- })));
1300
- const mcdc = report.decisions
1301
- .filter((decision) => decision.meta.file === file)
1302
- .flatMap((decision) => filterDecision(decision, selectedTestSet)
1303
- .conditions.filter((condition) => !condition.covered)
1304
- .map((condition) => ({
1305
- kind: "mcdc",
1306
- id: decision.meta.id,
1307
- line: decision.meta.line,
1308
- column: decision.meta.column,
1309
- decision: decision.meta.source,
1310
- missingCondition: condition.source,
1311
- ...(waiverEvaluation?.waivedByDecision
1312
- .get(decision.meta.id)
1313
- ?.has(condition.index)
1314
- ? {
1315
- waived: true,
1316
- waiverReason: waiverEvaluation.waivedByDecision
1317
- .get(decision.meta.id)
1318
- .get(condition.index).reason,
1319
- }
1320
- : {}),
1321
- observedVectors: filterDecision(decision, selectedTestSet).vectorObservations.map((observation) => vectorText(observation.vector.values, observation.vector.outcome)),
1322
- otherCoverage: otherCoverage(report, (decision.conditions[condition.index]?.witnessTests ?? []).flat(), selectedTestSet),
1323
- })));
1324
- const obligations = [
1325
- ...uncoveredLines,
1326
- ...statements,
1327
- ...functions,
1328
- ...branches,
1329
- ...mcdc,
1330
- ].filter((obligation) => obligationMatchesMetric(obligation, options.metric)).sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
1331
- const allFileLimitations = (report.limitations ?? [])
1332
- .filter((limitation) => limitation.file === file)
1333
- .map((limitation) => ({
1334
- ...limitation,
1335
- blocking: true,
1336
- effect: "outside-measured-denominator",
1337
- }))
1338
- .sort((left, right) => left.line - right.line ||
1339
- left.column - right.column ||
1340
- left.id.localeCompare(right.id));
1341
- const allFileTests = report.tests
1342
- .filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
1343
- test.lines.some((line) => line.file === file))
1344
- .map((test) => ({
1345
- id: test.id,
1346
- name: test.name,
1347
- provenance: test.provenance,
1348
- }));
1349
- const tests = page(allFileTests, options);
1350
- const selected = page(obligations, options);
1351
- const limitations = page(allFileLimitations, options);
1352
- const filePageTotal = Math.max(obligations.length, allFileTests.length, allFileLimitations.length);
1353
- const filePageReturned = Math.max(selected.length, tests.length, limitations.length);
1354
- const nextFileOffset = options.offset + filePageReturned;
1355
- const nextFileCommand = filePageReturned > 0 && nextFileOffset < filePageTotal
1356
- ? `${coverageCommand(run.id, options, "file")} ${shellQuote(file)} --offset ${nextFileOffset}${options.limit !== 20 ? ` --limit ${options.limit}` : ""}`
1357
- : undefined;
1358
- const result = {
1359
- run: run.id,
1360
- filters: queryFilters(options),
1361
- file,
1362
- metric: options.metric,
1363
- counts: {
1364
- uncoveredLines: uncoveredLines.length,
1365
- uncoveredStatements: statements.length,
1366
- uncoveredFunctions: functions.length,
1367
- missingBranches: branches.length,
1368
- missingMcdcConditions: mcdc.length,
1369
- waivedMcdcConditions: mcdc.filter((item) => item.waived).length,
1370
- measurementLimitations: allFileLimitations.length,
1371
- },
1372
- tests,
1373
- totalTests: allFileTests.length,
1374
- totalObligations: obligations.length,
1375
- obligations: selected,
1376
- totalLimitations: allFileLimitations.length,
1377
- limitations,
1378
- };
1379
- 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
1380
- .map((item) => item.kind === "line"
1381
- ? `line ${item.line}: ${item.otherCoverage.coveredElsewhere ? `covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}` : "uncovered everywhere"}`
1382
- : item.kind === "statement"
1383
- ? `statement ${item.line}:${item.column}: ${item.source}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
1384
- : item.kind === "function"
1385
- ? `function ${item.line}:${item.column}: ${item.source}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
1386
- : item.kind === "branch"
1387
- ? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
1388
- : `MC/DC ${item.line}:${item.column} [${item.id}]: ${item.missingCondition}${item.waived ? " [waived]" : ""}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`)
1389
- .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));
1390
- }
1391
- if (command === "decision") {
1392
- const selector = options.positional[0];
1393
- if (!selector)
1394
- throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage decision <id|source-file:line>");
1395
- let matches = report.decisions.filter((decision) => decision.meta.id === selector);
1396
- if (matches.length === 0 && /:\d+(?::\d+)?$/.test(selector)) {
1397
- const location = locationSelector(selector);
1398
- matches = report.decisions.filter((decision) => decision.meta.file === location.file &&
1399
- decision.meta.line === location.line);
1400
- }
1401
- if (matches.length === 0)
1402
- throw new SupercovError("DECISION_NOT_FOUND", `Decision not found: ${selector}`, {
1403
- details: { selector },
1404
- });
1405
- if (matches.length > 1) {
1406
- const matchingDecisions = page(matches, options).map((decision) => ({
1407
- id: decision.meta.id,
1408
- file: decision.meta.file,
1409
- line: decision.meta.line,
1410
- column: decision.meta.column,
1411
- source: decision.meta.source,
1412
- }));
1413
- const matchesNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, matches.length, matchingDecisions.length, options);
1414
- return output({
1415
- run: run.id,
1416
- filters: queryFilters(options),
1417
- decisions: matchingDecisions,
1418
- }, 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));
1419
- }
1420
- matches = matches.map((decision) => filterDecision(decision, selectedTestSet));
1421
- const totalDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
1422
- matches = matches.map((decision) => {
1423
- const totals = {
1424
- conditions: decision.conditions.length,
1425
- vectorObservations: decision.vectorObservations.length,
1426
- tests: decision.tests.length,
1427
- };
1428
- const waived = waiverEvaluation?.waivedByDecision.get(decision.meta.id);
1429
- const vectorObservations = page(decision.vectorObservations, options);
1430
- return {
1431
- ...decision,
1432
- totals,
1433
- vectors: vectorObservations.map((observation) => observation.vector),
1434
- vectorObservations,
1435
- conditions: page(decision.conditions, options).map((condition) => !condition.covered && waived?.has(condition.index)
1436
- ? {
1437
- ...condition,
1438
- waived: true,
1439
- waiverReason: waived.get(condition.index).reason,
1440
- }
1441
- : condition),
1442
- tests: page(decision.tests, options),
1443
- };
1444
- });
1445
- const returnedDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
1446
- const decisionNext = nextPageCommand(`${coverageCommand(run.id, options, "decision")} ${shellQuote(selector)}`, totalDecisionEvidence, returnedDecisionEvidence, options);
1447
- const result = {
1448
- run: run.id,
1449
- filters: queryFilters(options),
1450
- paginationAppliesTo: "conditions, vectorObservations, and tests independently within each decision",
1451
- decisions: matches,
1452
- };
1453
- return output(result, options, matches
1454
- .map((decision) => `${decision.meta.id} ${decision.meta.file}:${decision.meta.line}:${decision.meta.column}\n${decision.meta.source}\n${decision.conditions
1455
- .map((condition) => `C${condition.index + 1} ${condition.covered ? "covered" : condition.waived ? "MISSING (waived)" : "MISSING"}${condition.assertionCovered ? " + asserted" : ""}: ${condition.source}${condition.waiverReason ? `\n waived: ${condition.waiverReason}` : ""}`)
1456
- .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"}`)
1457
- .join("\n\n") +
1458
- `\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
1459
- (decisionNext ? `\nnext page: ${decisionNext}` : ""), queryPagination(totalDecisionEvidence, returnedDecisionEvidence, options));
1460
- }
1461
- if (command === "covers") {
1462
- const selector = options.positional[0];
1463
- if (!selector)
1464
- throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage covers <source-file:line>");
1465
- const location = locationSelector(selector);
1466
- const line = report.lines.find((candidate) => candidate.file === location.file && candidate.line === location.line);
1467
- if (!line) {
1468
- // "Uncovered" would be a false claim here: nothing is measured on this
1469
- // exact line. Report what does anchor at it instead of a misleading no.
1470
- const anchored = [
1471
- ...report.decisions
1472
- .filter((decision) => decision.meta.file === location.file &&
1473
- decision.meta.line === location.line)
1474
- .map((decision) => ({
1475
- kind: "decision",
1476
- id: decision.meta.id,
1477
- column: decision.meta.column,
1478
- covered: decision.covered,
1479
- coveredConditions: decision.conditions.filter((condition) => condition.covered).length,
1480
- conditions: decision.conditions.length,
1481
- })),
1482
- ...report.branches
1483
- .filter((branch) => branch.meta.file === location.file &&
1484
- branch.meta.line === location.line)
1485
- .map((branch) => ({
1486
- kind: "branch",
1487
- id: branch.meta.id,
1488
- column: branch.meta.column,
1489
- covered: branch.alternatives.every((alternative) => alternative.covered),
1490
- })),
1491
- ...report.points
1492
- .filter((point) => point.meta.file === location.file &&
1493
- point.meta.line === location.line)
1494
- .map((point) => ({
1495
- kind: point.meta.kind,
1496
- id: point.meta.id,
1497
- column: point.meta.column,
1498
- covered: includesSelectedTest(point.tests, selectedTestSet),
1499
- })),
1500
- ].sort((left, right) => left.column - right.column);
1501
- const anchoredPage = page(anchored, options);
1502
- return output({
1503
- run: run.id,
1504
- filters: queryFilters(options),
1505
- location,
1506
- lineObligation: false,
1507
- anchored: anchoredPage,
1508
- totalAnchored: anchored.length,
1509
- }, options, `${location.file}:${location.line} has no line obligation${anchored.length === 0 ? "; nothing is measured at this exact line" : `; ${anchored.length} obligation(s) anchor here`}\n${anchoredPage
1510
- .map((obligation) => `${obligation.kind} ${location.line}:${obligation.column} [${obligation.id}] ${obligation.covered ? "covered" : "not fully covered"}${"conditions" in obligation ? ` (${obligation.coveredConditions}/${obligation.conditions} conditions)` : ""}`)
1511
- .join("\n")}${anchoredPage.length ? "\n" : ""}${pageLabel(anchored.length, anchoredPage.length, options)} anchored obligations`, queryPagination(anchored.length, anchoredPage.length, options));
1512
- }
1513
- const allTests = (line?.tests ?? [])
1514
- .filter((id) => !selectedTestSet || selectedTestSet.has(id))
1515
- .map((id) => {
1516
- const test = report.tests.find((candidate) => candidate.id === id);
1517
- return {
1518
- id,
1519
- name: test?.name ?? id,
1520
- provenance: test?.provenance,
1521
- };
1522
- });
1523
- const allPhases = (line?.phases ?? [])
1524
- .map((id) => report.phases.find((candidate) => candidate.id === id))
1525
- .filter((phase) => Boolean(phase) &&
1526
- (!selectedTestSet || selectedTestSet.has(phase.test)))
1527
- .map((phase) => ({
1528
- id: phase.id,
1529
- kind: phase.kind,
1530
- operation: phase.operation,
1531
- source: phase.source,
1532
- test: phase.test,
1533
- status: phase.status,
1534
- causedByPhaseId: phase.causedByPhaseId,
1535
- }));
1536
- const tests = page(allTests, options);
1537
- const phases = page(allPhases, options);
1538
- const coversTotal = Math.max(allTests.length, allPhases.length);
1539
- const coversReturned = Math.max(tests.length, phases.length);
1540
- const coversNext = nextPageCommand(`${coverageCommand(run.id, options, "covers")} ${shellQuote(selector)}`, coversTotal, coversReturned, options);
1541
- const result = {
1542
- run: run.id,
1543
- filters: queryFilters(options),
1544
- location,
1545
- covered: includesSelectedTest(line?.tests ?? [], selectedTestSet),
1546
- confidence: line?.confidence,
1547
- totalTests: allTests.length,
1548
- totalPhases: allPhases.length,
1549
- tests,
1550
- phases,
1551
- };
1552
- 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));
1553
- }
1554
- if (command === "test") {
1555
- const selector = options.positional.join(" ").toLowerCase();
1556
- if (!selector)
1557
- throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage test <id|name-fragment>");
1558
- const matches = report.tests.filter((test) => (!selectedTestSet || selectedTestSet.has(test.id)) &&
1559
- (test.id === selector || test.name.toLowerCase().includes(selector)));
1560
- if (matches.length === 0)
1561
- throw new SupercovError("TEST_NOT_FOUND", `Test not found: ${selector}`, {
1562
- details: { selector },
1563
- });
1564
- const testBase = `${coverageCommand(run.id, options, "test")} ${shellQuote(options.positional.join(" "))}`;
1565
- if (matches.length > 1) {
1566
- const matchingTests = page(matches, options).map((test) => ({
1567
- id: test.id,
1568
- name: test.name,
1569
- outcome: test.outcome,
1570
- provenance: test.provenance,
1571
- }));
1572
- const matchesNext = nextPageCommand(testBase, matches.length, matchingTests.length, options);
1573
- return output({
1574
- run: run.id,
1575
- filters: queryFilters(options),
1576
- tests: matchingTests,
1577
- }, 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));
1578
- }
1579
- const test = matches[0];
1580
- const pointById = new Map(report.points.map((point) => [point.meta.id, point.meta]));
1581
- const branchAlternativeById = new Map(report.branches.flatMap((branch) => branch.alternatives.map((alternative) => [
1582
- alternative.id,
1583
- {
1584
- ...branch.meta,
1585
- id: alternative.id,
1586
- alternative: alternative.label,
1587
- },
1588
- ])));
1589
- const allHitDetails = test.hits.map((id) => {
1590
- const point = pointById.get(id);
1591
- if (point)
1592
- return {
1593
- id: point.id,
1594
- obligation: point.kind,
1595
- file: point.file,
1596
- line: point.line,
1597
- column: point.column,
1598
- label: point.label,
1599
- };
1600
- const branch = branchAlternativeById.get(id);
1601
- if (branch)
1602
- return {
1603
- id: branch.id,
1604
- obligation: "branch",
1605
- branchKind: branch.kind,
1606
- file: branch.file,
1607
- line: branch.line,
1608
- column: branch.column,
1609
- alternative: branch.alternative,
1610
- };
1611
- return { id, obligation: "unknown" };
1612
- });
1613
- const decisionById = new Map(report.decisions.map((decision) => [decision.meta.id, decision.meta]));
1614
- const allPhases = report.phases
1615
- .filter((phase) => phase.test === test.id)
1616
- .map((phase) => ({
1617
- id: phase.id,
1618
- kind: phase.kind,
1619
- operation: phase.operation,
1620
- source: phase.source,
1621
- status: phase.status,
1622
- causedByPhaseId: phase.causedByPhaseId,
1623
- lines: phase.lines.length,
1624
- decisions: phase.decisions.reduce((sum, decision) => sum + decision.vectors.length, 0),
1625
- }));
1626
- const testTotal = Math.max(test.lines.length, test.hits.length, test.decisions.length, allPhases.length);
1627
- const selected = {
1628
- ...test,
1629
- lines: page(test.lines, options),
1630
- hits: page(test.hits, options),
1631
- hitDetails: page(allHitDetails, options),
1632
- decisions: page(test.decisions, options).map((decision) => ({
1633
- ...decision,
1634
- meta: decisionById.get(decision.id),
1635
- })),
1636
- phases: page(allPhases, options),
1637
- totals: {
1638
- lines: test.lines.length,
1639
- hits: test.hits.length,
1640
- decisions: test.decisions.length,
1641
- phases: allPhases.length,
1642
- },
1643
- };
1644
- const testReturned = Math.max(selected.lines.length, selected.hits.length, selected.decisions.length, selected.phases.length);
1645
- const testNext = nextPageCommand(testBase, testTotal, testReturned, options);
1646
- return output({
1647
- run: run.id,
1648
- filters: queryFilters(options),
1649
- paginationAppliesTo: "lines, hits/hitDetails, decisions, and phases independently within the test",
1650
- tests: [selected],
1651
- }, 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));
1652
- }
1653
- throw new SupercovError("UNKNOWN_COMMAND", `Unknown coverage query: ${command}. Try supercov help.`, { details: { command } });
1654
- }
1655
- export const coverageQueryCommands = new Set(["help", "runs", "diff"]);
1656
- //# sourceMappingURL=query.js.map