canary-test-cli 6.8.0 → 6.8.1

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.
@@ -612,48 +612,56 @@ function readReportText(reportPath) {
612
612
  * blocks).
613
613
  */
614
614
  export function resolveFromReport(units, reportPath) {
615
- const { index } = readReportIndex(reportPath);
615
+ const { index, absence } = readReportIndex(reportPath);
616
616
  if (index === null)
617
617
  return null;
618
- return matchUnitsToIndex(units, index);
618
+ return matchUnitsToIndex(units, index, absence);
619
619
  }
620
620
  /** Read + parse a coverage report, reporting each step's outcome separately. */
621
621
  function readReportIndex(reportPath) {
622
+ const unusable = (found) => ({
623
+ found,
624
+ index: null,
625
+ absence: 'not-coverable',
626
+ });
622
627
  if (!existsSync(reportPath))
623
- return { found: false, index: null };
628
+ return unusable(false);
624
629
  const text = readReportText(reportPath);
625
630
  // Present but unreadable/non-UTF-8 counts as found-and-unusable, not absent.
626
631
  if (text === null)
627
- return { found: true, index: null };
632
+ return unusable(true);
628
633
  const name = basename(reportPath).toLowerCase();
629
634
  let index;
635
+ let absence;
630
636
  if (name.endsWith('.json')) {
631
637
  let parsed;
632
638
  try {
633
639
  parsed = JSON.parse(text);
634
640
  }
635
641
  catch {
636
- return { found: true, index: null };
642
+ return unusable(true);
637
643
  }
638
644
  index = parseCoverageJson(parsed);
645
+ absence = 'uncovered';
639
646
  }
640
647
  else if (name.endsWith('.info') || name.includes('lcov')) {
641
648
  index = parseLcov(text);
649
+ absence = 'not-coverable';
642
650
  }
643
651
  else if (name.endsWith('.xml')) {
644
652
  index = parseCobertura(text);
653
+ absence = 'not-coverable';
645
654
  }
646
655
  else {
647
656
  // Unrecognized format → fall through to a lower fidelity tier.
648
- return { found: true, index: null };
657
+ return unusable(true);
649
658
  }
650
- if (index === null || Object.keys(index).length === 0) {
651
- return { found: true, index: null };
652
- }
653
- return { found: true, index };
659
+ if (index === null || Object.keys(index).length === 0)
660
+ return unusable(true);
661
+ return { found: true, index, absence };
654
662
  }
655
663
  /** Resolve every unit the report index can speak to (COVERAGE_VERIFIED). */
656
- function matchUnitsToIndex(units, index) {
664
+ function matchUnitsToIndex(units, index, absence) {
657
665
  const results = [];
658
666
  for (const unit of units) {
659
667
  const hits = matchHits(unit.path, index);
@@ -665,17 +673,30 @@ function matchUnitsToIndex(units, index) {
665
673
  continue;
666
674
  }
667
675
  const added = expandRanges(unit.added_ranges);
668
- const uncovered = added.filter((ln) => (hits[ln] ?? 0) <= 0);
676
+ // The per-line form of the check above (#655): under a format that
677
+ // enumerates instrumented lines, a changed line with no record could not
678
+ // have been executed and is scored by neither side.
679
+ const coverable = absence === 'not-coverable' ? added.filter((ln) => ln in hits) : added;
680
+ if (coverable.length === 0) {
681
+ // Every changed line is non-coverable, so this report has nothing to say
682
+ // about the unit. An abstention — never a clean pass, never a finding.
683
+ // Falls through to the graph/heuristic tier exactly as an absent path does.
684
+ continue;
685
+ }
686
+ const uncovered = coverable.filter((ln) => (hits[ln] ?? 0) <= 0);
669
687
  const covered = uncovered.length === 0;
688
+ // State the denominator: "all covered" over 20 changed lines and over the 3
689
+ // of them that were coverable are very different claims (#508).
670
690
  const evidence = covered
671
- ? `lines ${rangesStr(unit.added_ranges)}: all covered`
672
- : `lines ${rangesStr(unit.added_ranges)}: ${uncovered.length} uncovered`;
691
+ ? `lines ${rangesStr(unit.added_ranges)}: all ${coverable.length} coverable line(s) covered`
692
+ : `lines ${rangesStr(unit.added_ranges)}: ${uncovered.length} of ${coverable.length} coverable line(s) uncovered`;
673
693
  results.push(makeResult({
674
694
  unit,
675
695
  covered,
676
696
  fidelity: Fidelity.CoverageVerified,
677
697
  evidence,
678
698
  uncovered_lines: uncovered,
699
+ coverable_lines: coverable.length,
679
700
  }));
680
701
  }
681
702
  return results;
@@ -1399,7 +1420,9 @@ export function resolveCoverageWithInput(units, options = {}) {
1399
1420
  coverage.parsed = read.index !== null;
1400
1421
  coverage.filesInReport =
1401
1422
  read.index === null ? 0 : Object.keys(read.index).length;
1402
- const report = read.index === null ? null : matchUnitsToIndex(remaining, read.index);
1423
+ const report = read.index === null
1424
+ ? null
1425
+ : matchUnitsToIndex(remaining, read.index, read.absence);
1403
1426
  // An empty array (no unit matched the report) is falsy-equivalent in the
1404
1427
  // Python `if report:` guard — fall through rather than lock in nothing.
1405
1428
  if (report !== null && report.length > 0) {
@@ -565,6 +565,22 @@ const HIGH_UNCOVERED_SHARE = 0.5;
565
565
  function addedLineCount(ranges) {
566
566
  return ranges.reduce((total, [start, end]) => total + (end - start + 1), 0);
567
567
  }
568
+ /**
569
+ * The fraction of what the report could speak to that came back unhit (#655).
570
+ *
571
+ * The denominator is the unit's **coverable** lines — added lines overstate it,
572
+ * because on a new file that is largely imports, types and blanks most changed
573
+ * lines were never instrumented at all, and dividing by them drives every share
574
+ * toward zero. Where the count is unknown (the graph and heuristic tiers, which
575
+ * are not graded by share anyway) it falls back to the added-line count, so
576
+ * grades predating that field are unchanged. An unknown denominator yields a
577
+ * full share rather than a low one — an absent measurement must never read as a
578
+ * good score (ADR 0010).
579
+ */
580
+ function uncoveredShare(result, uncovered) {
581
+ const denominator = result.coverable_lines ?? addedLineCount(result.unit.added_ranges);
582
+ return denominator > 0 ? uncovered / denominator : 1;
583
+ }
568
584
  /**
569
585
  * Severity for an uncovered **coverage-verified** result (#553).
570
586
  *
@@ -589,8 +605,7 @@ function coverageVerifiedSeverity(result) {
589
605
  const uncovered = result.uncovered_lines?.length ?? 0;
590
606
  if (uncovered === 0)
591
607
  return Severity.HIGH;
592
- const added = addedLineCount(result.unit.added_ranges);
593
- const share = added > 0 ? uncovered / added : 1;
608
+ const share = uncoveredShare(result, uncovered);
594
609
  if (uncovered >= CRITICAL_UNCOVERED_LINES &&
595
610
  share >= CRITICAL_UNCOVERED_SHARE)
596
611
  return Severity.CRITICAL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canary-test-cli",
3
- "version": "6.8.0",
3
+ "version": "6.8.1",
4
4
  "description": "Canary — AI-powered test automation agent",
5
5
  "license": "MIT",
6
6
  "repository": {