Skip to content

Stacked <summary> doc comments: six on dev, invisible to every automated check #1745

Description

@erikdarlingdata

Surfaced while fixing two instances of this in #1739/#1744. Filing the class rather than the two, because the two are already fixed and the other six are not.

The defect class

A member ends up with two <summary> elements stacked, usually because an edit added a new summary above an old one instead of replacing it:

/// <summary>
/// Base service for collecting performance data from remote SQL Servers.
/// Partial class - individual collectors are in separate files.
/// </summary>
/// <summary>
/// Tracks the health state of an individual collector.
/// </summary>
public class CollectorHealthEntry

XML documentation takes the LAST summary. So IntelliSense, generated docs, and every analyzer render the correct text. The build is clean, the tests pass, nothing warns. The only reader who is misled is a human reading the file top-down — and the first thing they read is the wrong description, attached to the wrong member.

That is what makes this worth a guard rather than a cleanup: it is invisible to every automated check we currently run, and re-reading does not find it. In #1739 the same region was read at least three times by two people over several hours without either of us noticing the duplicate; a regex found it in one pass.

Verified inventory on dev (6 sites)

Confirmed by walking every .cs (excluding obj/bin) on dev at f54485ee:

Darling\PerformanceMonitor.Darling.Storage\TimescaleSupport.cs:370
Darling\PerformanceMonitor.Darling.Viewer\MainWindow.xaml.cs:769
Darling\PerformanceMonitor.Darling.Viewer\ViewerDataService.FinOps.Inventory.cs:131
Lite\Services\LocalDataService.QueryStore.cs:419
Lite\Services\RemoteCollectorService.cs:31
deprecated\Dashboard.Tests\ThemeParityTests.cs:195

Four projects, all pre-existing, none introduced by #1706.

Two of them are actively misleading, not benign duplicates

  • RemoteCollectorService.cs — the file's old service-level header ("Base service for collecting performance data from remote SQL Servers") was never removed when a DTO was added at the top of the file, so CollectorHealthEntry, a small health-tracking class, now carries the description of the whole service.
  • TimescaleSupport.cs — a summary describing the daily hierarchical CAGG sits directly above one describing the per-database rollup (Darling viewer: built-in tabs silently lose history past 4 days - CAGG read-routing only covers Custom Views #1661). Two different objects; a top-down reader gets the daily-CAGG explanation attached to the per-database member. Both summaries are long and detailed, which makes the wrong one more convincing, not less.

I have not audited the other four.

Proposed guard: one regex

The repo already has the idiom for pinning things no behavioral test can see — HostHeaderGuardTests (middleware ordering, #1648) and DarlingStoreUpgradeTests (the store-upgrade wiring pins, #1739). This one is cheaper than both, because it needs no fixture wiring and can walk the source tree directly:

</summary>\s*\r?\n\s*///\s*<summary>

One assertion over the .cs files, milliseconds to run, and it would have caught both of tonight's and all six of these. Probably the best guard-per-defect ratio of anything we have added.

Suggested order

  1. Add the pin, with the six known sites either fixed first or explicitly allow-listed so it goes green.
  2. Fix the six (each is a delete of the stale block; the surviving summary must be checked to confirm nothing unique was in the deleted one — in Remove a second stale stacked summary, on QuiesceTimescaleServerOptions #1744 the deleted block was a true duplicate, but that is worth verifying per site rather than assuming).

Not urgent and nothing is broken at runtime. Worth doing before someone quotes the wrong summary in a code review or a doc.

🤖 Generated with Claude Code

Activity

  1. added a commit that references this issue on Jul 27, 2026
  2. erikdarlingdata commented on Jul 27, 2026

    @erikdarlingdata
    OwnerAuthor

    Fixed and pinned in #1751, merged to dev at 6fa21e8c (all four checks green). Closing manually rather than by keyword — GitHub only auto-closes on a merge to the default branch, and this repo merges to dev.

    Two corrections to the issue as filed, both found during the audit.

    It was eight, not six. The inventory here was taken from dev before #1744 merged. Re-scanning dev at f54485ee found two more — DarlingWorker.cs and DarlingSelfAlertEvaluator.cs, the two files #1739's alert-surfacing fix touched. The class recurred inside the PR sequence that was fixing two instances of it, and shipped. That is a better argument for the guard than anything in the original filing.

    The defect is displacement, not duplication — which inverts the fix. An edit inserts a new member and its summary above an existing summary, stranding that summary on the wrong member. There are two casualties each time, not one: the new member reads with a description of something else, and the member the orphaned block actually described is left with no documentation at all. RunCollectionLoopAsync and EvaluateCompressionJobsAsync were both undocumented on dev for exactly this reason.

    So the obvious remedy is wrong almost every time: seven of the eight needed the orphaned block moved back to its real member; only one was a superseded duplicate safe to delete. A blind "remove the extra summary" sweep would have destroyed real documentation at seven of eight sites. The guard's failure message now says so outright, so the next person to hit it does not reach for the obvious wrong fix.

    Per-site disposition (all eight, at f54485ee)

    # Site Orphaned block describes Member left undocumented Verdict
    1 DarlingWorker.cs:582 post-bootstrap store/collection loop RunCollectionLoopAsync:605 MOVE — introduced by #1739/#1744
    2 DarlingSelfAlertEvaluator.cs:1127 isolating compression-job entry point EvaluateCompressionJobsAsync:1235 MOVE — introduced by #1739/#1744
    3 TimescaleSupport.cs:369 query_stats DAILY hierarchical CAGG CreateQueryStatsDailySql:422 MOVE
    4 MainWindow.xaml.cs:768 visible-tab refresh + overlap guard RefreshVisibleAsync:786 MOVE
    5 ViewerDataService.FinOps.Inventory.cs:130 Server Inventory base rows ServerInventorySql:147 MOVE
    6 ThemeParityTests.cs:194 repo-root walk-up FindRepoRoot:198 MOVE
    7 RemoteCollectorService.cs:30 the base collector service class class RemoteCollectorService:99 MOVE
    8 LocalDataService.QueryStore.cs:418 superseded per-query history text none — same member DELETE

    Site 8 is the only genuine duplicate: both summaries describe GetQueryStoreHistoryAsync, the doc was rewritten from "a specific query" to "ALL plans of a query", and the new text was added below the old instead of replacing it. Verified there is no per-plan sibling and that the preceding method kept its own docs, so nothing was displaced.

    Every MOVE was verified before being touched: locate the member the orphaned prose actually describes, confirm that member currently has no doc comment, then move the block to it. Nothing was deleted on the assumption it was redundant.

    The guard

    Darling/Darling.Tests/DocCommentHygieneTests.cs walks every .cs outside bin/obj from the repo root and fails on </summary> immediately followed by <summary> — the regex proposed here.

    It fails rather than skips when it cannot locate the tree. A Skip would mean the rule silently stops being enforced the day the output layout changes, which is the same shape as the defect it guards.

    Verified in both directions, since a pin nobody has tried to break is not evidence:

    green on the fixed tree                Passed! - Failed: 0, Passed: 1
    stacked block re-introduced in Lite    [FAIL] Lite\Services\RemoteCollectorService.cs:29
    reverted                               Passed! - Failed: 0, Passed: 1
    

    That red is a mutation in Lite caught by a test living in Darling.Tests, which is the cross-project reach this rule needs.

    Coverage limit, documented in the test rather than assumed: CI path filters are per-project, so it runs on any pull request tripping the darling or core filter and on every nightly and release build — but a change touching only Lite or Installer will not run it, and would be caught on the next nightly instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions