fix(coverage): dedupe Cobertura line and branch arithmetic (#441, #478) - #538
Merged
drmoisan merged 3 commits intoAug 11, 2026
Conversation
Phase 6 of the approved plan requires the four follow-up candidates to be filed through the MCP promotion lifecycle. The executing atomic-executor session did not expose new_potential_bug_entry / potential_to_issue, so it correctly took the plan's sanctioned POSTING BLOCKED branch and left AC-20 unchecked rather than fabricating issue numbers. The orchestrator session does expose those tools, so the branch is resolved here instead of deferred: - #529 package-level line-rate / branch-rate never recomputed after filtering - #530 merged Cobertura class retains only the primary class's <methods> - #531 Invoke-MSTestWithCoverage.ps1 discovery lacks a \.claude\ exclusion - #532 agent memory records an incorrect Cobertura dedup generalization Re-verified that none of the four is fixed by this change: the diff still touches exactly two source files and all four follow-up targets are byte-identical to edf3d34. Also commits the plan's permitted checklist residual (P7-T23). AC-20 checked off in spec.md; 20/20 acceptance criteria delivered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Feature review of bug/cobertura-coverage-arithmetic-441 returned blocking_count = 0: zero FAIL and zero blocking-PARTIAL findings, with all 20 acceptance criteria independently verified PASS. The remediation loop was not entered. Five non-blocking findings were recorded. The two Minor ones are dispositioned by filing follow-up issues through the promotion lifecycle rather than by widening a bugfix whose plan is 85/85 complete: - #537 NF-1: the max(hits) update assignment at Helpers.ps1:220 is exercised by no test, so the dedup rule is pinned only for first-entry-wins ordering. New-code coverage is 39/40 = 97.50% against a >= 90% floor, so no gate fails. - #536 NF-2: the bundled run_poshqc_test coverage capture writes artifacts/pester/powershell-coverage.xml with zero covered lines repo-wide. Verified independently: LINE covered 0 / missed 16075 across all 1227 JaCoCo counters, contradicted by the direct-Pester capture at the same head. The branch changes no coverage-capture tooling, so the defect is pre-existing. The policy audit's row 2.5 FAIL is a verdict on that invalid measurement, not on this branch's coverage. All change-scope coverage rows pass: changed file 90.59%, new code 97.50%, no regression on changed lines. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1, which counted every source line twice and emitted a per-fileline-rateblended from two different denominators.Get-CoberturaCoverageSummarynow counts each source line exactly once per class, deduplicating by line number withmax(hits). Branch counters are deduplicated on the same basis.dotnet-coveragedocument, the corrected code now reproduces that document's own root attributes exactly — 79957 / 56124 / 23109 / 13472, where it previously reportedlines-valid161086.Get-CoberturaClassLineSummaryplus six regression fixtures; four were demonstrated failing against the unmodified script before any production change, per the repository's bugfix workflow.CLAUDE.mdor.claude/rules/.Why
Two compounding defects lived in the same rate-recomputation path.
Issue #441 — descendant-axis double count.
Get-CoberturaCoverageSummaryselected over the XPath descendant axis.//lines/line. In the Cobertura documents this pipeline produces, each<class>carries its<line>nodes twice — once nested under each<method>, and once again as a class-level rollup. The descendant axis matched both sets, so every line was counted twice. The inflation is not uniform across assemblies, because duplication is not uniform across classes.Issue #478 — blended merge denominator.
Merge-CoberturaClassesByFilenameunioned the class-level<lines>of same-filename classes correctly, but never merged the corresponding<methods>subtrees. It then recomputedline-rateover the same descendant axis, which saw the correct union plus only the primary class's method-level lines. The emitted per-fileline-ratematched neither denominator.Root-cause analysis identified a single defective selection rather than the two sites named in the issue text. The line numbers cited in the GitHub issues (
:98,:167) are function declaration lines, not selections. The verified situation was:Helpers.ps1:122$cls.SelectNodes('.//lines/line')Helpers.ps1:219$classNode.SelectNodes('./lines/line')Helpers.ps1:270-273Get-CoberturaCoverageSummary:122defect reached the merged per-class rateInvoke-MSTestWithCoverage.ps1Editing
:219instead of:122would have destroyed the correct union and left both defects in place.Every coverage gate and every committed coverage baseline in the repository consumes the affected figure, and epic #136 gates each of its fifteen children on a per-file line rate.
What Changed
Core fix —
scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1(+132/-~34, 357 → 455 lines)Get-CoberturaClassLineSummary: takes a single[System.Xml.XmlElement]$ClassNode, performs no I/O, mutates nothing in the source document, and returnsLineMap,TotalLines,CoveredLines,TotalBranches,CoveredBranches. Deduplicates by line number withmax(hits)and resolvescondition-coverageper distinct line.Get-CoberturaCoverageSummarynow delegates to the helper instead of selecting over.//lines/line. Zero occurrences of.//lines/lineremain in the file.$classSummaryXmlsynthetic-document delegation inMerge-CoberturaClassesByFilenameis replaced by a direct helper call. Zero occurrences of$classSummaryXmlremain.cmpbefore and after formatting).Tests —
tests/scripts/vscode/Invoke-MSTestWithCoverage.Helpers.Tests.ps1(+246/-0, 222 → 468 lines)Six new
Itblocks appended inside the existingDescribe, plus direct unit tests for the new helper. Zero deletions — no pre-existing block was modified.lines-valid3,lines-covered2)branches-valid2,branches-covered1)line-rate='0.6', five ascending linesmax(hits)dedup across two constructors<methods>but no class-level<lines>)<methods>, per-linehitspreserved)Every fixture uses an inline single-quoted here-string, creates no file on disk, uses no mock, and passes
-ProjectNamesexplicitly for determinism.Docs and evidence
spec.mdacceptance criteria checked off (20/20) and the plan checklist completed (85/85 tasks).docs/features/active/2026-08-10-cobertura-coverage-arithmetic-441/evidence/, plus the three review artifacts (policy-audit,code-review,feature-audit).docs/features/potential/promoted/corresponding to the follow-up issues filed below.Architecture / How It Fits Together
ConvertTo-KoverageCoberturaXmlis the pipeline entry point. It filters.Testpackages, merges same-filename classes, and rewrites coverage attributes. Control flow after this change:Both call sites now share one pure, separately unit-tested counting function, so the root summary and the merged per-class rate can no longer disagree.
Get-CoberturaClassLineSummaryis the seam: it is host-neutral, does no I/O, and is exercised directly by unit tests covering all threecondition-coverageprecedence branches and the zero-input boundary.Verification
Completed
Toolchain (PowerShell), single clean pass with no restart:
mcp__drm-copilot__run_poshqc_format—ok:true, 0 files changed (SHA-256 identical before and after for both in-scope files).mcp__drm-copilot__run_poshqc_analyzeplus directInvoke-ScriptAnalyzerper file — 0 new findings. The one pre-existingPSUseSingularNounsfinding onGet-CoberturaLineConditionCoveragePartspersists and moved line 146 → 140; the baseline is keyed on(ScriptName, RuleName, Severity, Message)withLinerecorded as an observation only, so a line-number move on an otherwise-identical finding is not a new finding.Invoke-Pester— 19 passed / 0 failed (8 pre-existing + 6 fixtures + 5 helper unit tests).Arithmetic verification:
lines-validlines-coveredbranches-validbranches-coveredlines-validline-rateFail-before evidence: F1-F4 failed against the unmodified script with
FailedCount4 /PassedCount10, reporting 6/4, 4/2,'0.75'and 3/2 respectively. Reproduced independently during review.Coverage of the changed production file: LINE 146/165 = 88.48% → 183/202 = 90.59%. New code (
Get-CoberturaClassLineSummary) is 39/40 = 97.50% against the>= 90%new-code floor.Feature review returned 0 blocking findings (0 FAIL, 0 blocking-PARTIAL); all 20 acceptance criteria independently verified PASS. Five non-blocking findings were recorded and dispositioned.
Recommended
Backward Compatibility / Migration Notes
Get-CoberturaClassLineSummaryis additive.git diff --name-only edf3d34c -- CLAUDE.md .claude/rules coverage.configreturns empty.-424sample is 85.0317% against the uniform 85% floor — a margin of 0.03 pp. This is recorded as a fact for sibling feature Bug: conflicting-coverage-thresholds-across-policy-docs #494 (coverage threshold policy reconciliation, wave 2), which owns threshold decisions. This PR deliberately does not act on it.Risks and Mitigations
:217-268as byte-identical, verified bycmpboth before and after formatting<methods>(name, signature)pairs, so naive merging breaks per-method consumers. Fixture F6 pins the current behavior; filed as #530Review Guide
Suggested order:
scripts/vscode/Invoke-MSTestWithCoverage.Helpers.ps1— the new helper and the two rewritten call sites. This is the whole substantive change.tests/scripts/vscode/Invoke-MSTestWithCoverage.Helpers.Tests.ps1— fixtures F1-F6 and the helper unit tests. 0 deletions, so the diff is purely additive.docs/features/active/2026-08-10-cobertura-coverage-arithmetic-441/spec.md— the 20 acceptance criteria.evidence/regression-testing/fail-before-f1-f4.*.mdandevidence/qa-gates/postchange-generator-parity.*.md— the fail-before and oracle-parity proofs.policy-audit,code-review,feature-audit— the review verdicts.The remaining ~45 files are evidence artifacts, promoted potential-bug entries, and agent-memory notes. They are additive and mechanically uninteresting.
Note for reviewers:
policy-audit.2026-08-10T23-35.mdrow 2.5 records a FAIL againstartifacts/pester/powershell-coverage.xml, which reports zero covered lines repo-wide. That is a verdict on an invalid measurement by the bundled capture tool, not on this branch — independently confirmed asLINE covered 0 / missed 16075across all 1227 JaCoCo counters at a head whose suite passes. Filed as #536 and dispositioned non-blocking.Follow-ups
Six issues filed through the promotion lifecycle. None is fixed by this PR.
line-rate/branch-rateare never recomputed after package filtering and class merging, leaving stale values consumed byscripts/temp-extract-coverage.ps1:47.<methods>, so its methods do not account for all of its class-level lines.Invoke-MSTestWithCoverage.ps1test-assembly discovery lacks a\.claude\exclusion and picks up stale sibling-worktree assemblies.run_poshqc_testcoverage capture records zero covered lines repo-wide (review finding NF-2).max(hits)update assignment is exercised by no test, so the dedup rule is pinned only for first-entry-wins ordering (review finding NF-1).Threshold reconciliation for the corrected figures is owned by sibling feature #494 in wave 2.
GitHub Auto-close
Note: this PR targets the epic integration branch
epic/build-ci-coverage-gate-fidelity-integration, not the default branch, so GitHub will not fire these keywords on merge. They record intent for the epic's eventual integration-to-main pull request.