supercov 0.0.6 → 0.0.8

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 (51) hide show
  1. package/README.md +13 -0
  2. package/dist/agentJson.d.ts +1 -1
  3. package/dist/agentJson.d.ts.map +1 -1
  4. package/dist/agentJson.js.map +1 -1
  5. package/dist/cli.js +49 -9
  6. package/dist/cli.js.map +1 -1
  7. package/dist/directInstrumenter.d.ts +1 -1
  8. package/dist/directInstrumenter.d.ts.map +1 -1
  9. package/dist/directInstrumenter.js +30 -6
  10. package/dist/directInstrumenter.js.map +1 -1
  11. package/dist/integrity.d.ts.map +1 -1
  12. package/dist/integrity.js +14 -0
  13. package/dist/integrity.js.map +1 -1
  14. package/dist/launchSupervisor.d.ts +8 -0
  15. package/dist/launchSupervisor.d.ts.map +1 -1
  16. package/dist/launchSupervisor.js +53 -2
  17. package/dist/launchSupervisor.js.map +1 -1
  18. package/dist/nodeTest.d.ts.map +1 -1
  19. package/dist/nodeTest.js +5 -1
  20. package/dist/nodeTest.js.map +1 -1
  21. package/dist/playwright.d.ts.map +1 -1
  22. package/dist/playwright.js +8 -18
  23. package/dist/playwright.js.map +1 -1
  24. package/dist/project.d.ts +1 -0
  25. package/dist/project.d.ts.map +1 -1
  26. package/dist/project.js +4 -0
  27. package/dist/project.js.map +1 -1
  28. package/dist/query.d.ts +3 -2
  29. package/dist/query.d.ts.map +1 -1
  30. package/dist/query.js +133 -12
  31. package/dist/query.js.map +1 -1
  32. package/dist/resolve-loader.d.mts.map +1 -1
  33. package/dist/resolve-loader.mjs +12 -0
  34. package/dist/resolve-loader.mjs.map +1 -1
  35. package/dist/runAnalysis.d.ts.map +1 -1
  36. package/dist/runAnalysis.js +52 -13
  37. package/dist/runAnalysis.js.map +1 -1
  38. package/dist/runnerEvidence.d.ts +1 -1
  39. package/dist/runnerEvidence.d.ts.map +1 -1
  40. package/dist/runnerEvidence.js +2 -2
  41. package/dist/runnerEvidence.js.map +1 -1
  42. package/dist/runtime.d.ts +15 -0
  43. package/dist/runtime.d.ts.map +1 -1
  44. package/dist/runtime.js +633 -534
  45. package/dist/runtime.js.map +1 -1
  46. package/dist/sourceDiscovery.d.ts.map +1 -1
  47. package/dist/sourceDiscovery.js +11 -1
  48. package/dist/sourceDiscovery.js.map +1 -1
  49. package/dist/types.d.ts +10 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/package.json +2 -2
package/dist/query.js CHANGED
@@ -271,7 +271,7 @@ function obligationSatisfied(obligation, selected) {
271
271
  return obligation.options.some((option) => option.every((test) => selected.has(test)));
272
272
  }
273
273
  /** Exact branch-and-bound solver; MC/DC obligations retain their witness-pair structure. */
274
- export function minimumTestSet(report, target = 100, metric = "all") {
274
+ export function minimumTestSet(report, target = 100, metric = "all", maxStates = 5_000) {
275
275
  const unattributed = report.tests.filter((test) => test.role === "background" &&
276
276
  (test.hits.length > 0 || test.decisions.some((decision) => decision.vectors.length > 0)));
277
277
  if (unattributed.length > 0) {
@@ -307,6 +307,18 @@ export function minimumTestSet(report, target = 100, metric = "all") {
307
307
  const seen = new Set();
308
308
  const search = (selected, skipped, skippedByMetric) => {
309
309
  exploredStates += 1;
310
+ if (exploredStates > maxStates) {
311
+ 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.`, {
312
+ details: {
313
+ candidateTests: candidateTests.length,
314
+ obligations: obligations.length,
315
+ exploredStates,
316
+ maxStates,
317
+ target,
318
+ metric,
319
+ },
320
+ });
321
+ }
310
322
  if (selected.size >= best.size)
311
323
  return;
312
324
  const stateKey = `${[...selected].sort().join(",")}|${[...skipped].sort().join(",")}`;
@@ -361,6 +373,7 @@ function coverageCommand(runId, options, child) {
361
373
  options.filter !== "all" ? `--filter ${options.filter}` : undefined,
362
374
  options.kind ? `--kind ${shellQuote(options.kind)}` : undefined,
363
375
  options.runner ? `--runner ${shellQuote(options.runner)}` : undefined,
376
+ options.metric !== "all" ? `--metric ${options.metric}` : undefined,
364
377
  ]
365
378
  .filter(Boolean)
366
379
  .join(" ");
@@ -480,6 +493,54 @@ function attribution(report, selected) {
480
493
  serverFallback: phases.reduce((sum, phase) => sum + phase.inferredServerEvents, 0),
481
494
  };
482
495
  }
496
+ function coverageDiagnostics(report, selected) {
497
+ const observed = attribution(report, selected);
498
+ const diagnostics = [];
499
+ if ((report.transport?.corruptRecords ?? 0) > 0) {
500
+ diagnostics.push({
501
+ code: "CORRUPT_EVIDENCE_RECORDS",
502
+ severity: "error",
503
+ message: `${report.transport.corruptRecords} malformed evidence record(s) in ` +
504
+ `${report.transport.corruptFiles} file(s) were excluded; coverage is incomplete.`,
505
+ });
506
+ }
507
+ if ((report.transport?.remoteLaunches ?? 0) > 0 &&
508
+ (report.transport?.scopedServerRecords ?? 0) === 0 &&
509
+ observed.serverExplicit === 0 &&
510
+ observed.serverFallback === 0) {
511
+ diagnostics.push({
512
+ code: "REMOTE_SERVER_EVIDENCE_MISSING",
513
+ severity: "warning",
514
+ message: "Remote launches were supervised, but no server evidence returned. Coverage may describe only browser/test processes; inspect how the application server is launched.",
515
+ });
516
+ }
517
+ return diagnostics;
518
+ }
519
+ function gapMetricValue(gap, metric) {
520
+ if (metric === "all")
521
+ return gap.score;
522
+ if (metric === "lines")
523
+ return gap.uncoveredLines;
524
+ if (metric === "statements")
525
+ return gap.uncoveredStatements;
526
+ if (metric === "functions")
527
+ return gap.uncoveredFunctions;
528
+ if (metric === "branches")
529
+ return gap.missingBranches;
530
+ return gap.missingMcdcConditions;
531
+ }
532
+ function obligationMatchesMetric(obligation, metric) {
533
+ if (metric === "all")
534
+ return true;
535
+ const kindByMetric = {
536
+ lines: "line",
537
+ statements: "statement",
538
+ functions: "function",
539
+ branches: "branch",
540
+ mcdc: "mcdc",
541
+ };
542
+ return obligation.kind === kindByMetric[metric];
543
+ }
483
544
  export function fileGaps(report, selected) {
484
545
  const files = new Map();
485
546
  const get = (file) => {
@@ -595,6 +656,7 @@ function findFile(report, selector) {
595
656
  }
596
657
  export function coverageMeasurement(report) {
597
658
  const limitations = report.limitations ?? [];
659
+ const evidenceCorruptions = report.transport?.corruptRecords ?? 0;
598
660
  const byKind = {
599
661
  "dynamic-code": 0,
600
662
  "semantic-safety": 0,
@@ -603,11 +665,13 @@ export function coverageMeasurement(report) {
603
665
  for (const limitation of limitations)
604
666
  byKind[limitation.kind] += 1;
605
667
  return {
606
- complete: limitations.length === 0,
668
+ complete: limitations.length === 0 && evidenceCorruptions === 0,
607
669
  limitations: limitations.length,
670
+ evidenceCorruptions,
608
671
  // Every current limitation removes source from the measured denominator.
609
- blocking: limitations.length,
610
- files: new Set(limitations.map((limitation) => limitation.file)).size,
672
+ blocking: limitations.length + evidenceCorruptions,
673
+ files: new Set(limitations.map((limitation) => limitation.file)).size +
674
+ (report.transport?.corruptFiles ?? 0),
611
675
  byKind,
612
676
  };
613
677
  }
@@ -628,9 +692,9 @@ const helpText = `Agent-oriented local coverage queries:
628
692
  supercov runs <run-id> coverage kinds [--json]
629
693
  supercov runs <run-id> coverage runners [--json]
630
694
  supercov runs <run-id> coverage scope [--limit N] [--offset N] [--json]
631
- supercov runs <run-id> coverage files [--filter all|passed|failed] [--limit N] [--offset N] [--json]
632
- supercov runs <run-id> coverage gaps [--filter all|passed|failed] [--kind e2e] [--limit N] [--offset N] [--json]
633
- supercov runs <run-id> coverage file <source-file> [--kind e2e] [--limit N] [--offset N] [--json]
695
+ supercov runs <run-id> coverage files [--metric all|lines|statements|functions|branches|mcdc] [--filter all|passed|failed] [--limit N] [--offset N] [--json]
696
+ 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]
634
698
  supercov runs <run-id> coverage decision <id|source-file:line> [--kind e2e] [--json]
635
699
  supercov runs <run-id> coverage covers <source-file:line> [--kind e2e] [--json]
636
700
  supercov runs <run-id> coverage test <id|name-fragment> [--kind e2e] [--limit N] [--json]
@@ -857,6 +921,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
857
921
  outcome,
858
922
  selectedTests.filter((test) => test.role === "test" && test.outcome === outcome).length,
859
923
  ]));
924
+ const diagnostics = coverageDiagnostics(report, selectedTestSet);
860
925
  const result = {
861
926
  run: run.id,
862
927
  filters: queryFilters(options),
@@ -875,6 +940,8 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
875
940
  coverageByKind: report.coverageByKind,
876
941
  coverageByRunner: report.coverageByRunner,
877
942
  attribution: attribution(report, selectedTestSet),
943
+ transport: report.transport,
944
+ diagnostics,
878
945
  ...(!selectedTestSet
879
946
  ? {
880
947
  confidence: {
@@ -907,7 +974,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
907
974
  }
908
975
  : undefined,
909
976
  };
910
- return output(result, options, `run ${run.id}${filterLabel(options) ? ` (${filterLabel(options)})` : ""}${run.metadata?.testExitCode !== 0 ? ` [INVALID: test exit ${run.metadata?.testExitCode ?? "unknown"}]` : ""}${report.integrity?.stale ? ` [STALE: ${(report.integrity.staleReasons ?? []).join(", ")}]` : ""}\nlines ${pct(summary.lines.percentage)} (${summary.lines.covered}/${summary.lines.total})\nbranches ${pct(summary.branches.percentage)} (${summary.branches.covered}/${summary.branches.total})\nMC/DC ${pct(summary.conditionCoveragePct)} (${summary.coveredConditions}/${summary.conditions})\nmeasurement: ${measurement.complete ? "complete" : `incomplete — ${measurement.blocking} blocking limitation(s) in ${measurement.files} file(s)`}${!selectedTestSet ? `\nconfidence: ${report.lines.filter((line) => line.confidence?.level === "asserted").length} asserted lines, ${report.lines.filter((line) => line.confidence?.level === "action").length} action-linked, ${report.lines.filter((line) => line.confidence?.level === "executed").length} execution-only; ${report.decisions.reduce((total, decision) => total + decision.conditions.filter((condition) => condition.assertionCovered).length, 0)} assertion-linked MC/DC conditions` : ""}\n${testCount} test(s)${setupCount ? ` + ${setupCount} setup scope(s)` : ""}; outcomes ${Object.entries(testOutcomes).filter(([, count]) => count > 0).map(([outcome, count]) => `${outcome}=${count}`).join(", ") || "none"}; ${gaps.length} file(s) have unresolved coverage or measurement gaps`);
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`);
911
978
  }
912
979
  if (command === "minimize") {
913
980
  const solverReport = selectedTestSet
@@ -928,7 +995,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
928
995
  };
929
996
  });
930
997
  const selectedPage = page(selectedDetails, options);
931
- const base = `${coverageCommand(run.id, options, "minimize")} --target ${options.target}${options.metric !== "all" ? ` --metric ${options.metric}` : ""}`;
998
+ const base = `${coverageCommand(run.id, options, "minimize")} --target ${options.target}`;
932
999
  const next = nextPageCommand(base, selectedDetails.length, selectedPage.length, options);
933
1000
  return output({
934
1001
  run: run.id,
@@ -996,7 +1063,13 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
996
1063
  }
997
1064
  if (command === "files" || command === "gaps") {
998
1065
  const files = fileGaps(report, selectedTestSet);
999
- const all = command === "gaps" ? files.filter((gap) => gap.score > 0) : files;
1066
+ const all = files
1067
+ .filter((gap) => command === "files" ||
1068
+ gapMetricValue(gap, options.metric) > 0 ||
1069
+ gap.measurementLimitations > 0)
1070
+ .sort((left, right) => gapMetricValue(right, options.metric) - gapMetricValue(left, options.metric) ||
1071
+ right.measurementLimitations - left.measurementLimitations ||
1072
+ left.file.localeCompare(right.file));
1000
1073
  const selectedFiles = page(all, options);
1001
1074
  const pageStart = all.length === 0 ? 0 : options.offset + 1;
1002
1075
  const pageEnd = Math.min(options.offset + selectedFiles.length, all.length);
@@ -1007,6 +1080,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1007
1080
  return output({
1008
1081
  run: run.id,
1009
1082
  filters: queryFilters(options),
1083
+ metric: options.metric,
1010
1084
  [command]: selectedFiles,
1011
1085
  }, options, selectedFiles
1012
1086
  .map((gap) => {
@@ -1094,7 +1168,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1094
1168
  ...functions,
1095
1169
  ...branches,
1096
1170
  ...mcdc,
1097
- ].sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
1171
+ ].filter((obligation) => obligationMatchesMetric(obligation, options.metric)).sort((left, right) => left.line - right.line || left.kind.localeCompare(right.kind));
1098
1172
  const allFileLimitations = (report.limitations ?? [])
1099
1173
  .filter((limitation) => limitation.file === file)
1100
1174
  .map((limitation) => ({
@@ -1126,6 +1200,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1126
1200
  run: run.id,
1127
1201
  filters: queryFilters(options),
1128
1202
  file,
1203
+ metric: options.metric,
1129
1204
  counts: {
1130
1205
  uncoveredLines: uncoveredLines.length,
1131
1206
  uncoveredStatements: statements.length,
@@ -1185,9 +1260,15 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1185
1260
  matches = matches.map((decision) => filterDecision(decision, selectedTestSet));
1186
1261
  const totalDecisionEvidence = Math.max(0, ...matches.map((decision) => Math.max(decision.vectorObservations.length, decision.conditions.length, decision.tests.length)));
1187
1262
  matches = matches.map((decision) => {
1263
+ const totals = {
1264
+ conditions: decision.conditions.length,
1265
+ vectorObservations: decision.vectorObservations.length,
1266
+ tests: decision.tests.length,
1267
+ };
1188
1268
  const vectorObservations = page(decision.vectorObservations, options);
1189
1269
  return {
1190
1270
  ...decision,
1271
+ totals,
1191
1272
  vectors: vectorObservations.map((observation) => observation.vector),
1192
1273
  vectorObservations,
1193
1274
  conditions: page(decision.conditions, options),
@@ -1199,6 +1280,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1199
1280
  const result = {
1200
1281
  run: run.id,
1201
1282
  filters: queryFilters(options),
1283
+ paginationAppliesTo: "conditions, vectorObservations, and tests independently within each decision",
1202
1284
  decisions: matches,
1203
1285
  };
1204
1286
  return output(result, options, matches
@@ -1282,6 +1364,40 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1282
1364
  }, 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));
1283
1365
  }
1284
1366
  const test = matches[0];
1367
+ const pointById = new Map(report.points.map((point) => [point.meta.id, point.meta]));
1368
+ const branchAlternativeById = new Map(report.branches.flatMap((branch) => branch.alternatives.map((alternative) => [
1369
+ alternative.id,
1370
+ {
1371
+ ...branch.meta,
1372
+ id: alternative.id,
1373
+ alternative: alternative.label,
1374
+ },
1375
+ ])));
1376
+ const allHitDetails = test.hits.map((id) => {
1377
+ const point = pointById.get(id);
1378
+ if (point)
1379
+ return {
1380
+ id: point.id,
1381
+ obligation: point.kind,
1382
+ file: point.file,
1383
+ line: point.line,
1384
+ column: point.column,
1385
+ label: point.label,
1386
+ };
1387
+ const branch = branchAlternativeById.get(id);
1388
+ if (branch)
1389
+ return {
1390
+ id: branch.id,
1391
+ obligation: "branch",
1392
+ branchKind: branch.kind,
1393
+ file: branch.file,
1394
+ line: branch.line,
1395
+ column: branch.column,
1396
+ alternative: branch.alternative,
1397
+ };
1398
+ return { id, obligation: "unknown" };
1399
+ });
1400
+ const decisionById = new Map(report.decisions.map((decision) => [decision.meta.id, decision.meta]));
1285
1401
  const allPhases = report.phases
1286
1402
  .filter((phase) => phase.test === test.id)
1287
1403
  .map((phase) => ({
@@ -1299,7 +1415,11 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1299
1415
  ...test,
1300
1416
  lines: page(test.lines, options),
1301
1417
  hits: page(test.hits, options),
1302
- decisions: page(test.decisions, options),
1418
+ hitDetails: page(allHitDetails, options),
1419
+ decisions: page(test.decisions, options).map((decision) => ({
1420
+ ...decision,
1421
+ meta: decisionById.get(decision.id),
1422
+ })),
1303
1423
  phases: page(allPhases, options),
1304
1424
  totals: {
1305
1425
  lines: test.lines.length,
@@ -1313,6 +1433,7 @@ export async function runQueryCommand(command, args, root = process.cwd()) {
1313
1433
  return output({
1314
1434
  run: run.id,
1315
1435
  filters: queryFilters(options),
1436
+ paginationAppliesTo: "lines, hits/hitDetails, decisions, and phases independently within the test",
1316
1437
  tests: [selected],
1317
1438
  }, 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));
1318
1439
  }