Skip to content

Fix the honesty trio: fake granted-memory zero, dropped physical-reads column, caveat-free RCSI advice - #3550

Merged
erikdarlingdata merged 3 commits into
devfrom
fix/3529-3531-honesty-trio
Sep 18, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
fix/3529-3531-honesty-trio

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 18, 2026 •

Copy link
Copy Markdown
Owner

Three small verified honesty defects from the 2026-09 brains-review campaign, plus two lead-approved extensions folded in during review.

#3529 — get_memory_trend published a hardcoded total_granted_mb = 0.0 on both SKUs

The payload promised granted-memory data and shipped a literal zero; an agent investigating RESOURCE_SEMAPHORE could read "granted was 0 all window" and rule out memory grants at exactly the wrong moment.

  • total_granted_mb is now an explicit null on both SKUs (identical shape), with an envelope-level granted_note naming get_memory_grants as the source of the grants series.
  • Both tool descriptions corrected: they no longer promise granted memory and point at get_memory_grants.
  • Pinned by: a Lite real-DuckDB payload test (null per point + note), the Darling gated-live test's extended payload assertions, and an ungated description pin per SKU.
  • The grants-series join (the issue's "complete" option) is deliberately out of scope — filed as get_memory_trend: join the grants series so total_granted_mb carries real data (complete fix for #3529) #3548.

#3530 — Lite QueryStore time-slice read TotalReads and TotalLogicalReads from the same ordinal

The SELECT computes total_reads (logical) at ordinal 4, total_writes at 5, and total_physical_reads at 6. The reader mapped both read fields to 4 — a deliberate alias for the logical aggregate, matching the QueryStats slicer — but never read ordinal 6, so the physical column was computed and dropped. TotalPhysicalReads now maps ordinal 6.

  • Pinned by a new test whose fixture rows carry pairwise-distinct values per I/O column (logical 110, writes 30, physical 50) — equal fixture values are exactly how the slip stayed invisible.

#3531 — LCK_M_IS advice handed out the RCSI ALTER with no caveats

The LCK_M_S twin closed with the brief-exclusive-lock timing note and the test-on-a-copy warning; LCK_M_IS recommended the identical ALTER calling RCSI "the strictly better mechanism" with neither. Both twins now carry the same three counter-objectives — the brief exclusive lock the ALTER takes, the tempdb version-store cost (phrased after FactRiskDisclosure's RCSI risk items, not duplicated from them), and the test-on-a-copy warning. Pinned by a shared advice-content test over both keys.

Extensions folded in per team-lead instruction

Both were initially left out under this PR's file boundary and filed as issues; the wave lead extended the boundary and directed they land here.

Tests

  • Lite.Tests (QueryStoreDedupReadTests + TrendEmptyParityToolTests): 21/21 pass, including the extended distinct-per-column pin over the slicer bucket AND the overlay timeline
  • Dashboard.Tests (FactAdviceCorrectnessTests + FactAdviceComposeTests): 49/49 pass
  • Darling.Tests (DarlingMcpTrendToolsSurfaceAndSqlTests + DarlingTrendEmptyTests): 35 pass, 1 skip (the gated _AgainstDevPostgres test; CI's darling-pg job runs it)
  • All touched test projects build with 0 warnings, 0 errors

Fixes #3529, fixes #3530, fixes #3531, fixes #3547, fixes #3549

🤖 Generated with Claude Code

https://claude.ai/code/session_01QMYFB4qv5tqGwMmdPsa4Zx

…he dropped physical-reads column, mirror the RCSI caveats

#3529: get_memory_trend shipped a hardcoded total_granted_mb = 0.0 on both
SKUs — an agent investigating memory-grant pressure read "granted was 0 all
window" and ruled out the real culprit. The field is now an explicit null,
the envelope carries a granted_note naming get_memory_grants as the grants
series' source, and both tool descriptions stop promising granted memory.
Pinned by payload tests (Lite real-DuckDB, Darling gated-live) and
per-SKU description pins.

#3530: Lite's QueryStore time-slice reader mapped TotalReads AND
TotalLogicalReads to ordinal 4 (the logical aggregate — a deliberate alias,
matching the QueryStats slicer) but never read ordinal 6, so the SELECT's
total_physical_reads was computed and dropped. TotalPhysicalReads now maps
ordinal 6, pinned by a fixture whose I/O columns carry pairwise-distinct
values — equal values are exactly how the slip stayed invisible.

#3531: the LCK_M_IS advice handed out the RCSI ALTER calling it "strictly
better" with none of the caveats its LCK_M_S twin carried. Both twins now
name the brief exclusive lock the ALTER takes, the tempdb version-store
cost (phrased after FactRiskDisclosure's RCSI risk items), and the
test-on-a-copy warning, pinned by a shared content test.

Fixes #3529, fixes #3530, fixes #3531

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMYFB4qv5tqGwMmdPsa4Zx
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 18, 2026 03:39
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown

Reviewed the diff against CONTRIBUTING.md's C#/Two-Store-Parity conventions, focusing on correctness, Lite/Darling parity, security, and perf.

No correctness, security, or Lite/Darling parity issues found in the actual changed code.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — verified the memory-trend null/granted_note change serializes correctly (no DefaultIgnoreCondition on JsonOptions), confirmed the QueryStore ordinal-6 fix matches the SQL and that Darling's equivalent reader already had it right (no parity drift), and confirmed FactAdvice.cs is shared so the RCSI caveat fix covers both SKUs.

…always-0 parity placeholder

Boundary extension approved by the wave lead: doc lines going stale because
of the #3529 change belong in the same PR. Darling/README.md's tool-catalog
line and DarlingTrendReader's MemoryTrendPoint doc comment now describe the
shipped behavior — explicit null plus a granted_note naming
get_memory_grants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMYFB4qv5tqGwMmdPsa4Zx

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — reviewed the memory-trend null/granted_note change (Lite + Darling wording is byte-identical), the QueryStore physical-reads ordinal fix (verified ordinal 6 in the SELECT is total_physical_reads, and confirmed Darling's equivalent slicer already read it correctly so no parity gap existed), and the LCK_M_S/LCK_M_IS advice-caveat parity (shared FactAdvice.cs, wording consistent with FactRiskDisclosure's existing tempdb version-store language). No correctness, parity, or security issues found; tests added are appropriately targeted (distinct fixture values per I/O column, both SKUs pinned).

…s and overlay both

#3547, folded in per the wave lead. The grid's sorting handler mapped
"AvgPhysicalReads" to the LOGICAL series under a physical-reads label — a
forced workaround while the reader dropped the physical column, which the
#3530 fix ended. The mapping now targets TotalPhysicalReads with the
bucket.Value case to match, and the selected-row overlay follows: the
timeline read carries avg_physical_io_reads through the same deduped CTE
(no new dedup site), the point record gains PhysicalReads, and the overlay
selector plots it — otherwise honest bars would have drawn under an
elapsed-ms overlay, a new mismatch replacing the old label lie.

The distinct-per-column test now covers the timeline's read path too, so a
logical/physical swap on either the bars' or the overlay's source goes red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMYFB4qv5tqGwMmdPsa4Zx
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown

Reviewed the diff in detail (Lite/Darling parity, the QueryStore ordinal fix, the RCSI advice caveats, and the memory-grant nulling). Everything in the diff itself checks out:

  • GetQueryStoreSlicerDataAsync's ordinal 6 → TotalPhysicalReads mapping matches the SELECT's actual column order.
  • The new QueryStoreItemTimelinePoint.PhysicalReads plumbing (SQL → record → grid overlay selector) is wired correctly end to end, and the new distinct-per-column test (SlicerBucket_MapsWritesAndPhysicalReads_ToTheirOwnColumns) would genuinely catch a logical/physical swap on either the bars or the overlay.
  • Lite and Darling's get_memory_trend descriptions, the granted_note envelope field, and the null-vs-0 payload change are word-for-word identical between SKUs — no parity drift there. PerformanceMonitor.Analysis (FactAdvice) is shared between Lite and Darling, so the RCSI caveat fix for LCK_M_S/LCK_M_IS covers both automatically.
  • McpHelpers.JsonOptions has no DefaultIgnoreCondition, so (double?)null correctly serializes to JSON null rather than being omitted — the tests asserting JsonValueKind.Null are checking real behavior.

One thing worth a look, not in this diff's blast radius but directly adjacent to it: Lite/Controls/ServerTab.Grids.cs's ProcedureStatsGrid_Sorting (around line 421) still has the exact bug this PR fixes for Query Store. It maps the TotalPhysicalReads column to the TotalReads (logical) metric under a "Total Physical Reads" label:

"TotalPhysicalReads" => ("TotalReads", "Total Physical Reads"),

And its data source, GetProcStatsSlicerDataAsync (Lite/Services/LocalDataService.QueryStats.cs ~line 824-869), computes total_physical_reads in the SELECT at ordinal 6 but never reads it into TotalPhysicalReads on the TimeSliceBucket — same drop-on-the-floor pattern as #3530. Interestingly, ComputeProcOverlayPoints (same file, ~line 117) already has a "TotalPhysReads" => h.DeltaPhysicalReads arm ready to go, so the overlay-selection plumbing is half-built and just needs the reader + sorting-handler fix to match what this PR just did for Query Store.

Not asking to block on this since it's outside the file regions this PR touches and wasn't called out as one of the deliberately-deferred items (unlike #3548), but given the PR's whole premise is "don't silently drop the physical-reads column or mislabel the series," it seems worth a fast follow-up issue so it doesn't get lost.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — verified the ordinal-6 physical-reads mapping against the actual SELECT column order, traced the new PhysicalReads field through the SQL → record → grid overlay selector, confirmed Lite/Darling get_memory_trend descriptions and granted_note payloads are word-for-word identical (shared FactAdvice covers the RCSI caveat fix for both SKUs automatically), and confirmed JsonSerializerOptions has no null-ignore so the null-payload tests reflect real behavior. Flagged one non-blocking adjacent issue (ProcedureStatsGrid_Sorting has the same dropped-physical-reads bug, untouched by this PR) as a PR comment for a follow-up.

@erikdarlingdata
erikdarlingdata merged commit 0c86d7a into dev Sep 18, 2026
10 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3529-3531-honesty-trio branch September 18, 2026 03:54
erikdarlingdata added a commit that referenced this pull request Sep 18, 2026
…rted

PR #3567's claude-review flagged that ViewerServerTab.Queries.cs — the
viewer's copy of Lite's sort-driven slicer metric mapping — still carried
both bug classes this PR fixes in Lite. The procedures grid's physical
sort mapped to the LOGICAL series with no physical value case, and the
Query Store grid had both the "Avg"-over-totals labels AND the un-ported
#3547 physical/logical swap. Both handlers now match Lite: TotalPhysReads
mapping, value case, honest Total labels.

No reader threading was needed anywhere — the shared slicer reader
(ReadQueryStatsSlicerAsync) has always mapped ordinal 6's
total_physical_reads, all three slicer SELECTs compute it, and the three
item-timeline reads already carry physical_reads into ItemTimelinePoint,
with ComputeOverlayPoints' TotalPhysReads arm sitting dormant — so only
the handlers were lying.

One genuine port gap beyond the switch arms: Lite's sorting handlers
re-run SelectionChanged after a metric change so a selected row's overlay
re-projects onto the new metric; the viewer's handlers never did, which
would have drawn the newly-honest physical bars under a stale-metric
overlay — the exact mismatch the #3550 commit warned about. All three
handlers gain the re-trigger.

Pins mirror Lite's: a source-text pin file over both fixed handlers plus
the re-trigger (the omission was per handler, so pinned per handler), the
slicer SQL shape theory now asserts the reads/writes/physical alias order
the shared reader's ordinals depend on, and the live slicer + dedup tests
gain distinct-per-column fixture values (Lite's 110/30/50 signature;
41x100/41x10 for the Query Store bucket and 40x100/40x10 for the timeline
point) — equal fixture values are exactly how a swap stays invisible.
Verified against a throwaway PG 18.4 + TimescaleDB cluster: 11/11 live,
48/48 ungated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMYFB4qv5tqGwMmdPsa4Zx
erikdarlingdata added a commit that referenced this pull request Sep 18, 2026
… Store slice-total labels say Total (#3567)

* Procedures slicer: physical sort plots the physical series, and slice totals stop calling themselves averages

Fixes #3556 — the #3547 bug's verified siblings, found during that fix and
kept out of its file boundary.

The procedures grid's physical-reads sort mapped to the LOGICAL aggregate
under a "Total Physical Reads" label, with no physical case in the value
switch — while the slicer's SELECT computed total_physical_reads at ordinal
6 and the reader dropped it on the floor, the #3530 shape exactly. The
reader now maps TotalPhysicalReads, and the handler mirrors the Query Store
fix: metric TotalPhysReads, the value-switch case, honest label. The
overlay side was already wired — ComputeProcOverlayPoints has carried a
dormant TotalPhysReads -> DeltaPhysicalReads arm all along, and the handler
re-triggers SelectionChanged on metric change, so bars and overlay go
physical together with no further threading.

The Query Store switch's two remaining dishonest labels — "Avg Reads" and
"Avg Writes" over execution-weighted slice TOTALS — now say Total, matching
the physical arm beside them and the query-stats grid's vocabulary.

Pinned three ways: a distinct-per-column reader test (110/30/50 — equal
fixture values are how the drop stayed invisible), and a source-text pin
over both sorting handlers' switch arms in the AvailabilityGroupsGridSortTests
style, since the handlers are private WPF event handlers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMYFB4qv5tqGwMmdPsa4Zx

* Darling viewer: port the #3547/#3556 grid fixes the review found unported

PR #3567's claude-review flagged that ViewerServerTab.Queries.cs — the
viewer's copy of Lite's sort-driven slicer metric mapping — still carried
both bug classes this PR fixes in Lite. The procedures grid's physical
sort mapped to the LOGICAL series with no physical value case, and the
Query Store grid had both the "Avg"-over-totals labels AND the un-ported
#3547 physical/logical swap. Both handlers now match Lite: TotalPhysReads
mapping, value case, honest Total labels.

No reader threading was needed anywhere — the shared slicer reader
(ReadQueryStatsSlicerAsync) has always mapped ordinal 6's
total_physical_reads, all three slicer SELECTs compute it, and the three
item-timeline reads already carry physical_reads into ItemTimelinePoint,
with ComputeOverlayPoints' TotalPhysReads arm sitting dormant — so only
the handlers were lying.

One genuine port gap beyond the switch arms: Lite's sorting handlers
re-run SelectionChanged after a metric change so a selected row's overlay
re-projects onto the new metric; the viewer's handlers never did, which
would have drawn the newly-honest physical bars under a stale-metric
overlay — the exact mismatch the #3550 commit warned about. All three
handlers gain the re-trigger.

Pins mirror Lite's: a source-text pin file over both fixed handlers plus
the re-trigger (the omission was per handler, so pinned per handler), the
slicer SQL shape theory now asserts the reads/writes/physical alias order
the shared reader's ordinals depend on, and the live slicer + dedup tests
gain distinct-per-column fixture values (Lite's 110/30/50 signature;
41x100/41x10 for the Query Store bucket and 40x100/40x10 for the timeline
point) — equal fixture values are exactly how a swap stays invisible.
Verified against a throwaway PG 18.4 + TimescaleDB cluster: 11/11 live,
48/48 ungated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QMYFB4qv5tqGwMmdPsa4Zx

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant