supercov 0.0.8 → 0.0.11

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 (117) hide show
  1. package/README.md +14 -1
  2. package/bin/native.js +87 -0
  3. package/bin/supercov.js +20 -3
  4. package/dist/agentJson.d.ts +2 -1
  5. package/dist/agentJson.d.ts.map +1 -1
  6. package/dist/agentJson.js +18 -0
  7. package/dist/agentJson.js.map +1 -1
  8. package/dist/analyze.d.ts.map +1 -1
  9. package/dist/analyze.js +9 -5
  10. package/dist/analyze.js.map +1 -1
  11. package/dist/cli.js +126 -23
  12. package/dist/cli.js.map +1 -1
  13. package/dist/directInstrumenter.d.ts.map +1 -1
  14. package/dist/directInstrumenter.js +39 -19
  15. package/dist/directInstrumenter.js.map +1 -1
  16. package/dist/engineEvidence.d.ts +9 -0
  17. package/dist/engineEvidence.d.ts.map +1 -0
  18. package/dist/engineEvidence.js +31 -0
  19. package/dist/engineEvidence.js.map +1 -0
  20. package/dist/engineInstrumenter.d.ts +16 -0
  21. package/dist/engineInstrumenter.d.ts.map +1 -0
  22. package/dist/engineInstrumenter.js +50 -0
  23. package/dist/engineInstrumenter.js.map +1 -0
  24. package/dist/engineProcess.d.ts +11 -0
  25. package/dist/engineProcess.d.ts.map +1 -0
  26. package/dist/engineProcess.js +59 -0
  27. package/dist/engineProcess.js.map +1 -0
  28. package/dist/evidenceArchive.d.ts +1 -0
  29. package/dist/evidenceArchive.d.ts.map +1 -1
  30. package/dist/evidenceArchive.js +59 -13
  31. package/dist/evidenceArchive.js.map +1 -1
  32. package/dist/instrumenter.d.ts +8 -1
  33. package/dist/instrumenter.d.ts.map +1 -1
  34. package/dist/instrumenter.js +585 -113
  35. package/dist/instrumenter.js.map +1 -1
  36. package/dist/integrity.d.ts.map +1 -1
  37. package/dist/integrity.js +20 -1
  38. package/dist/integrity.js.map +1 -1
  39. package/dist/launchSupervisor.d.ts.map +1 -1
  40. package/dist/launchSupervisor.js +3 -0
  41. package/dist/launchSupervisor.js.map +1 -1
  42. package/dist/merge.d.ts.map +1 -1
  43. package/dist/merge.js +9 -6
  44. package/dist/merge.js.map +1 -1
  45. package/dist/nodeAssert.d.ts +31 -0
  46. package/dist/nodeAssert.d.ts.map +1 -0
  47. package/dist/nodeAssert.js +30 -0
  48. package/dist/nodeAssert.js.map +1 -0
  49. package/dist/nodeAssertAdapter.d.ts +6 -0
  50. package/dist/nodeAssertAdapter.d.ts.map +1 -0
  51. package/dist/nodeAssertAdapter.js +94 -0
  52. package/dist/nodeAssertAdapter.js.map +1 -0
  53. package/dist/nodeAssertStrict.d.ts +31 -0
  54. package/dist/nodeAssertStrict.d.ts.map +1 -0
  55. package/dist/nodeAssertStrict.js +28 -0
  56. package/dist/nodeAssertStrict.js.map +1 -0
  57. package/dist/nodeAssertionInstrumenter.d.ts +8 -0
  58. package/dist/nodeAssertionInstrumenter.d.ts.map +1 -0
  59. package/dist/nodeAssertionInstrumenter.js +271 -0
  60. package/dist/nodeAssertionInstrumenter.js.map +1 -0
  61. package/dist/nodeTest.d.ts.map +1 -1
  62. package/dist/nodeTest.js +14 -5
  63. package/dist/nodeTest.js.map +1 -1
  64. package/dist/playwright.d.ts.map +1 -1
  65. package/dist/playwright.js +177 -89
  66. package/dist/playwright.js.map +1 -1
  67. package/dist/processDiagnostics.d.ts +34 -0
  68. package/dist/processDiagnostics.d.ts.map +1 -0
  69. package/dist/processDiagnostics.js +155 -0
  70. package/dist/processDiagnostics.js.map +1 -0
  71. package/dist/project.d.ts +1 -0
  72. package/dist/project.d.ts.map +1 -1
  73. package/dist/project.js +39 -4
  74. package/dist/project.js.map +1 -1
  75. package/dist/query.d.ts +11 -0
  76. package/dist/query.d.ts.map +1 -1
  77. package/dist/query.js +224 -11
  78. package/dist/query.js.map +1 -1
  79. package/dist/register.mjs +97 -4
  80. package/dist/register.mjs.map +1 -1
  81. package/dist/resolve-loader.d.mts.map +1 -1
  82. package/dist/resolve-loader.mjs +16 -2
  83. package/dist/resolve-loader.mjs.map +1 -1
  84. package/dist/runAnalysis.d.ts.map +1 -1
  85. package/dist/runAnalysis.js +3 -1
  86. package/dist/runAnalysis.js.map +1 -1
  87. package/dist/runnerEvidence.d.ts +3 -3
  88. package/dist/runnerEvidence.d.ts.map +1 -1
  89. package/dist/runnerEvidence.js +7 -4
  90. package/dist/runnerEvidence.js.map +1 -1
  91. package/dist/runtime.d.ts +77 -4
  92. package/dist/runtime.d.ts.map +1 -1
  93. package/dist/runtime.js +395 -26
  94. package/dist/runtime.js.map +1 -1
  95. package/dist/runtimeIsolation.d.ts +3 -0
  96. package/dist/runtimeIsolation.d.ts.map +1 -0
  97. package/dist/runtimeIsolation.js +9 -0
  98. package/dist/runtimeIsolation.js.map +1 -0
  99. package/dist/sourceDiscovery.d.ts.map +1 -1
  100. package/dist/sourceDiscovery.js +7 -2
  101. package/dist/sourceDiscovery.js.map +1 -1
  102. package/dist/transport.js +2 -2
  103. package/dist/transport.js.map +1 -1
  104. package/dist/vitePlugin.d.ts.map +1 -1
  105. package/dist/vitePlugin.js +21 -7
  106. package/dist/vitePlugin.js.map +1 -1
  107. package/dist/vitest.js +38 -7
  108. package/dist/vitest.js.map +1 -1
  109. package/dist/waivers.d.ts +52 -0
  110. package/dist/waivers.d.ts.map +1 -0
  111. package/dist/waivers.js +121 -0
  112. package/dist/waivers.js.map +1 -0
  113. package/dist/workspace.d.ts +36 -0
  114. package/dist/workspace.d.ts.map +1 -1
  115. package/dist/workspace.js +216 -37
  116. package/dist/workspace.js.map +1 -1
  117. package/package.json +29 -3
package/dist/query.js CHANGED
@@ -5,6 +5,7 @@ import { coverageSummaryForTests, isIndependencePair } from "./analyze.js";
5
5
  import { EVIDENCE_ARCHIVE_SCHEMA_VERSION } from "./evidenceArchive.js";
6
6
  import { analyzeCoverageArchiveCached, readCoverageQueryIndex, } from "./queryCache.js";
7
7
  import { compareRunIntegrity, createRunIntegrity } from "./integrity.js";
8
+ import { evaluateCoverageWaivers, readCoverageWaivers, WAIVERS_FILE, } from "./waivers.js";
8
9
  import { discoverCoverageProject } from "./project.js";
9
10
  import { agentPagination, agentSuccessJson, SupercovError, } from "./agentJson.js";
10
11
  function parseOptions(command, args) {
@@ -18,6 +19,8 @@ function parseOptions(command, args) {
18
19
  filter: "all",
19
20
  target: 100,
20
21
  metric: "all",
22
+ group: "none",
23
+ sort: "location",
21
24
  positional: [],
22
25
  };
23
26
  for (let index = 0; index < args.length; index += 1) {
@@ -72,6 +75,20 @@ function parseOptions(command, args) {
72
75
  throw new SupercovError("INVALID_ARGUMENT", "--offset must be a non-negative integer");
73
76
  options.offset = offset;
74
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
+ }
75
92
  else if (value.startsWith("--"))
76
93
  throw new SupercovError("INVALID_ARGUMENT", `Unknown option: ${value}`, {
77
94
  details: { option: value },
@@ -79,6 +96,10 @@ function parseOptions(command, args) {
79
96
  else
80
97
  options.positional.push(value);
81
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");
82
103
  return options;
83
104
  }
84
105
  function filteredCoverage(report, options) {
@@ -493,9 +514,29 @@ function attribution(report, selected) {
493
514
  serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
494
515
  };
495
516
  }
496
- function coverageDiagnostics(report, selected) {
517
+ export function coverageDiagnostics(report, selected) {
497
518
  const observed = attribution(report, selected);
498
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
+ }
499
540
  if ((report.transport?.corruptRecords ?? 0) > 0) {
500
541
  diagnostics.push({
501
542
  code: "CORRUPT_EVIDENCE_RECORDS",
@@ -675,8 +716,8 @@ export function coverageMeasurement(report) {
675
716
  byKind,
676
717
  };
677
718
  }
678
- function locationSelector(selector) {
679
- const match = /^(.*):(\d+)(?::\d+)?$/.exec(selector);
719
+ export function locationSelector(selector) {
720
+ const match = /^(.*):(\d+):\d+$/.exec(selector) ?? /^(.*):(\d+)$/.exec(selector);
680
721
  if (!match)
681
722
  throw new SupercovError("INVALID_ARGUMENT", "Expected <source-file>:<line>", {
682
723
  details: { selector },
@@ -694,7 +735,7 @@ const helpText = `Agent-oriented local coverage queries:
694
735
  supercov runs <run-id> coverage scope [--limit N] [--offset N] [--json]
695
736
  supercov runs <run-id> coverage files [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--limit N] [--offset N] [--json]
696
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]
697
- supercov runs <run-id> coverage file <source-file> [--metric all|lines|statements|functions|branches|mcdc] [--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]
698
739
  supercov runs <run-id> coverage decision <id|source-file:line> [--kind e2e] [--json]
699
740
  supercov runs <run-id> coverage covers <source-file:line> [--kind e2e] [--json]
700
741
  supercov runs <run-id> coverage test <id|name-fragment> [--kind e2e] [--limit N] [--json]
@@ -706,6 +747,14 @@ const helpText = `Agent-oriented local coverage queries:
706
747
 
707
748
  Use "latest" as <run-id> to query the newest local run.
708
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
+
709
758
  Create a run with:
710
759
  supercov -- <test command>`;
711
760
  function help(options) {
@@ -737,9 +786,16 @@ export function resolveCoverageQueryInvocation(command, args) {
737
786
  if (command !== "runs")
738
787
  return { command, args };
739
788
  const runId = args[0];
740
- if (!runId || runId.startsWith("-") || args[1] !== "coverage") {
789
+ if (!runId || runId.startsWith("-")) {
741
790
  return { command, args };
742
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
+ }
743
799
  const childToken = args[2];
744
800
  const hasChild = Boolean(childToken && !childToken.startsWith("-"));
745
801
  const child = hasChild ? childToken : "summary";
@@ -772,6 +828,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
772
828
  if (command === "help")
773
829
  return help(options);
774
830
  const currentIntegrity = currentProjectIntegrity(root);
831
+ const waiverSource = readCoverageWaivers(root);
775
832
  if (command === "runs") {
776
833
  const availableRuns = discoverRuns(root);
777
834
  const runs = page(availableRuns, options).map((run) => {
@@ -908,6 +965,9 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
908
965
  const run = selectedRun.run;
909
966
  const report = filteredCoverage(selectedRun.report, options);
910
967
  const selectedTestSet = selectedTestIds(report, options);
968
+ const waiverEvaluation = waiverSource
969
+ ? evaluateCoverageWaivers(report.decisions, waiverSource)
970
+ : undefined;
911
971
  if (command === "summary") {
912
972
  const summary = selectedTestSet
913
973
  ? coverageSummaryForTests(report, selectedTestSet)
@@ -937,6 +997,35 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
937
997
  measurement.complete,
938
998
  coverage: summary,
939
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
+ : {}),
940
1029
  coverageByKind: report.coverageByKind,
941
1030
  coverageByRunner: report.coverageByRunner,
942
1031
  attribution: attribution(report, selectedTestSet),
@@ -974,7 +1063,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
974
1063
  }
975
1064
  : undefined,
976
1065
  };
977
- 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`);
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`);
978
1067
  }
979
1068
  if (command === "minimize") {
980
1069
  const solverReport = selectedTestSet
@@ -1070,7 +1159,14 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1070
1159
  .sort((left, right) => gapMetricValue(right, options.metric) - gapMetricValue(left, options.metric) ||
1071
1160
  right.measurementLimitations - left.measurementLimitations ||
1072
1161
  left.file.localeCompare(right.file));
1073
- const selectedFiles = page(all, options);
1162
+ const selectedFiles = page(all, options).map((gap) => ({
1163
+ ...gap,
1164
+ ...(waiverEvaluation
1165
+ ? {
1166
+ waivedMcdcConditions: waiverEvaluation.appliedByFile.get(gap.file) ?? 0,
1167
+ }
1168
+ : {}),
1169
+ }));
1074
1170
  const pageStart = all.length === 0 ? 0 : options.offset + 1;
1075
1171
  const pageEnd = Math.min(options.offset + selectedFiles.length, all.length);
1076
1172
  const nextOffset = options.offset + selectedFiles.length;
@@ -1091,7 +1187,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1091
1187
  gap.missingMcdcConditions;
1092
1188
  const status = missing === 0
1093
1189
  ? "coverage complete"
1094
- : `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}`;
1190
+ : `missing: lines ${gap.uncoveredLines} stmts ${gap.uncoveredStatements} funcs ${gap.uncoveredFunctions} branches ${gap.missingBranches} MC/DC ${gap.missingMcdcConditions}${gap.waivedMcdcConditions ? ` (${gap.waivedMcdcConditions} waived)` : ""}`;
1095
1191
  const limitations = gap.measurementLimitations
1096
1192
  ? ` measurement limitations ${gap.measurementLimitations} (${gap.limitationKinds.join(", ")})`
1097
1193
  : "";
@@ -1106,6 +1202,59 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1106
1202
  if (!selector)
1107
1203
  throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage file <source-file>");
1108
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
+ }
1109
1258
  const uncoveredLines = report.lines
1110
1259
  .filter((line) => line.file === file &&
1111
1260
  !includesSelectedTest(line.tests, selectedTestSet))
@@ -1159,6 +1308,16 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1159
1308
  column: decision.meta.column,
1160
1309
  decision: decision.meta.source,
1161
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
+ : {}),
1162
1321
  observedVectors: filterDecision(decision, selectedTestSet).vectorObservations.map((observation) => vectorText(observation.vector.values, observation.vector.outcome)),
1163
1322
  otherCoverage: otherCoverage(report, (decision.conditions[condition.index]?.witnessTests ?? []).flat(), selectedTestSet),
1164
1323
  })));
@@ -1207,6 +1366,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1207
1366
  uncoveredFunctions: functions.length,
1208
1367
  missingBranches: branches.length,
1209
1368
  missingMcdcConditions: mcdc.length,
1369
+ waivedMcdcConditions: mcdc.filter((item) => item.waived).length,
1210
1370
  measurementLimitations: allFileLimitations.length,
1211
1371
  },
1212
1372
  tests,
@@ -1225,7 +1385,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1225
1385
  ? `function ${item.line}:${item.column}: ${item.source}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
1226
1386
  : item.kind === "branch"
1227
1387
  ? `branch ${item.line}:${item.column}: missing ${item.missing}${item.otherCoverage.coveredElsewhere ? ` [covered only by ${item.otherCoverage.kinds.join(", ")}/${item.otherCoverage.runners.join(", ")}]` : ""}`
1228
- : `MC/DC ${item.line}:${item.column} [${item.id}]: ${item.missingCondition}${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(", ")}]` : ""}`)
1229
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));
1230
1390
  }
1231
1391
  if (command === "decision") {
@@ -1265,13 +1425,20 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1265
1425
  vectorObservations: decision.vectorObservations.length,
1266
1426
  tests: decision.tests.length,
1267
1427
  };
1428
+ const waived = waiverEvaluation?.waivedByDecision.get(decision.meta.id);
1268
1429
  const vectorObservations = page(decision.vectorObservations, options);
1269
1430
  return {
1270
1431
  ...decision,
1271
1432
  totals,
1272
1433
  vectors: vectorObservations.map((observation) => observation.vector),
1273
1434
  vectorObservations,
1274
- conditions: page(decision.conditions, options),
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),
1275
1442
  tests: page(decision.tests, options),
1276
1443
  };
1277
1444
  });
@@ -1285,7 +1452,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1285
1452
  };
1286
1453
  return output(result, options, matches
1287
1454
  .map((decision) => `${decision.meta.id} ${decision.meta.file}:${decision.meta.line}:${decision.meta.column}\n${decision.meta.source}\n${decision.conditions
1288
- .map((condition) => `C${condition.index + 1} ${condition.covered ? "covered" : "MISSING"}${condition.assertionCovered ? " + asserted" : ""}: ${condition.source}`)
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}` : ""}`)
1289
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"}`)
1290
1457
  .join("\n\n") +
1291
1458
  `\n${pageLabel(totalDecisionEvidence, returnedDecisionEvidence, options)} conditions/vectors/tests per decision` +
@@ -1297,6 +1464,52 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1297
1464
  throw new SupercovError("INVALID_ARGUMENT", "Usage: supercov runs <run-id> coverage covers <source-file:line>");
1298
1465
  const location = locationSelector(selector);
1299
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
+ }
1300
1513
  const allTests = (line?.tests ?? [])
1301
1514
  .filter((id) => !selectedTestSet || selectedTestSet.has(id))
1302
1515
  .map((id) => {