Skip to content

get_query_store_regressions moves reading guidance off tools/list (#3898) - #4109

Merged
erikdarlingdata merged 3 commits into
devfrom
feature/3898-content-qsregressions
Sep 24, 2026
Merged

erikdarlingdata merged 3 commits into
devfrom
feature/3898-content-qsregressions

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Refs #3898.

Why

Darling's tools/list served 1,187 characters for get_query_store_regressions. Lite served the same
identical text. Under #3898, that reading guidance moves off the tool list. It goes into a reading guide that
get_tool_guide(tools[], topics[]) serves on demand, so a client no longer pays for it before the first
question. Nothing is deleted.

What changes

Converts the one twin pair (get_query_store_regressions, identical on both products) to the two-tier
McpToolGuide description. Each product now serves a terse head under the 620-character budget for
tools/list, and the full original prose becomes the get_tool_guide tail. The heads are byte-identical on
both products (D6 lockstep).

  • Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpQueryStoreRegressionTools.cs
  • Lite/Mcp/McpQueryTools.cs
  • New head pins: Darling/Darling.Tests/McpToolGuideHeads.QsRegressions.cs, Lite.Tests/McpToolGuideHeads.QsRegressions.cs
  • Budget lines: Darling/Darling.Tests/McpToolsListBudget/DarlingMcpQueryStoreRegressionTools.txt, Lite.Tests/McpToolsListBudget/McpQueryTools.txt
  • Re-pointed pin: Darling/Darling.Tests/McpZeroIsAMeasurementTests.cs (see "Re-pointed pins" below)

No topics were needed. Nothing in this tool's guidance repeats across another tool in this lane.

Before/after served characters

Tool Darling before Darling after Lite before Lite after
get_query_store_regressions 1,187 616 1,187 616

Corrections

None. The original prose matched the tool body on both products. I checked this against
DarlingQueryStoreRegressionReader.cs's SQL: the CPU-only 25% gate, the ORDER BY additional_duration_ms DESC
ranking key, and the DurationRegressionPercent is null severity condition.

D4 removals

None. The served description carried no issue references and no anecdotes. Only the doc comments above the
tool, which are never served, mention #2484 and #3541.

Re-pointed pins

McpZeroIsAMeasurementTests.TheRegressionTool_SaysWhyAPercentIsNull_AndDoesNotBandAMissingRatio used to assert
that the whole (then-unsplit) description contained undefined_percents and null percent never sorts as 0.
Both phrases moved to the tail, so I re-pointed the test in two parts:

  • Against the head, it now asserts the same guardrail in the head's own words. The two phrases are null, not 0%, when their baseline is 0 and additional_duration_ms is the ranking key.
  • against the tail (new TailOf helper, mirroring the file's existing DescriptionOf), it asserts the two
    literal phrases still ride there.

Reason: the field name undefined_percents and the literal ranking-key sentence are implementation detail.
They do not fit in the 620-character head beside this tool's three other guardrails. Those are the CPU gate, the
contrast with get_query_store_top, and the three status words that a zero-row answer can carry. An earlier
lane used the same pattern for the health-parser pins, narrowed to source_observed alone, so this follows
precedent already in the repository.

Status routes

Both products route a zero-row answer through the same four checks, in this order:

Status Darling Lite Condition
not_collected DarlingMcpQueryStoreRegressionTools.cs:165 McpQueryTools.cs:461 No rows, no Query Store capture before or inside the window, and this engine cannot run the query_store collector.
unavailable DarlingMcpQueryStoreRegressionTools.cs:167 McpQueryTools.cs:463 No rows and no Query Store capture before or inside the window, on an engine that can run it.
unavailable DarlingMcpQueryStoreRegressionTools.cs:174 McpQueryTools.cs:470 No rows, and every capture falls inside the window, so there is no baseline.
empty DarlingMcpQueryStoreRegressionTools.cs:181 McpQueryTools.cs:477 No rows, a baseline exists, and nothing was collected inside the window.
empty DarlingMcpQueryStoreRegressionTools.cs:186 McpQueryTools.cs:482 No rows, and both sides were collected. No query's average CPU is more than 25% worse. This is the all-clear.
error DarlingMcpQueryStoreRegressionTools.cs:124 McpQueryTools.cs:421 The read threw, through McpHelpers.FormatError.

D9 traps

  • Q: get_query_store_regressions and get_query_store_top both rank queries. Which one finds the query that got worse since yesterday?
    Correct: get_query_store_regressions. get_query_store_top ranks what is expensive now, and the most expensive query is usually the one that always was.
    Prevents it: "get_query_store_top ranks EXPENSIVE, this ranks CHANGED."

  • Q: A row shows duration_regression_percent: null. Does that mean duration held steady?
    Correct: No. Null means the baseline duration was 0, so there is no denominator. It does not mean nothing changed. It can be the largest possible regression.
    Prevents it: "duration_regression_percent and io_regression_percent are null, not 0%, when their baseline is 0"

  • Q: severity is null on a row. Does that mean the row is low severity?
    Correct: No. severity is null exactly when duration_regression_percent is null, because there is no duration ratio to band. It is not a "LOW" verdict.
    Prevents it: "severity is null with the former"

  • Q: I want the worst regression first. Is cpu_regression_percent the field to sort the rows by?
    Correct: No. Sort by additional_duration_ms, the tool's own ranking key. A percent can be null while this key always exists.
    Prevents it: "additional_duration_ms is the ranking key."

  • Q: The tool returns zero regressions with status empty. Is that always good news?
    Correct: Not always. It can also mean a baseline exists, but nothing was collected in the recent window. That is a collection gap, not a clean bill of health.
    Prevents it: "empty: no regression (all clear), or a baseline with nothing yet in the window."

  • Q: status is "unavailable". Can the head alone tell "never collected" apart from "collected, but all of it newer than the window"?
    Correct: No, it does not distinguish them: unavailable covers both, because both mean no baseline exists yet.
    Prevents it: "unavailable: no baseline exists yet."

  • Q: status is "not_collected". Is that a reason to go check whether the Query Store collector crashed?
    Correct: No. not_collected means this server's engine cannot run Query Store at all. That is permanent, not an outage to chase.
    Prevents it: "not_collected: this server's engine cannot run Query Store."

  • Q: With hours_back=24 and no baseline found, was the baseline just the 24 hours before that?
    Correct: No. The baseline is every capture ever collected before the start of the recent window, not a fixed lookback.
    Prevents it: "recent window (hours_back, ending at as_of) vs baseline, every capture before it."

  • Q: A row has io_regression_percent at 400% but cpu_regression_percent at 10%. Is this row returned at all?
    Correct: No. Only average CPU regressing over 25% gates a row into the result. A large I/O or duration regression alone does not qualify it.
    Prevents it: "Gated: average CPU regressed over 25%."

  • Q: Raising hours_back from 24 to 72 only changes the recent window, right?
    Correct: No. Baseline is everything before the recent window starts, so widening hours_back grows the recent window and shrinks the baseline at the same time.
    Prevents it: "recent window (hours_back, ending at as_of) vs baseline, every capture before it."

  • Q: severity reads "HIGH" on a row further down the list than one with no severity at all. Is the list sorted by severity?
    Correct: No. The list is sorted by additional_duration_ms. severity is a separate, duration-only band that can be null even on a row near the top.
    Prevents it: "additional_duration_ms is the ranking key." plus "severity is null with the former"

  • Q: io_regression_percent is null on a row that also has a numeric duration_regression_percent. Does that change what severity means for the row?
    Correct: No. severity follows duration_regression_percent only. Whether io_regression_percent is null never touches it.
    Prevents it: "severity is null with the former"

  • Q: (added by the coordinator) A call has as_of set to a day last month. It answers status empty,
    because nothing was collected inside that past window. Can the same call fill in if you wait an hour?
    Correct: No. The window is in the past, and nothing was collected in it.
    Prevents it: "empty: no regression (all clear), or a baseline with nothing yet in the window."

  • Q: (added by the coordinator) A call with as_of set to a day last month answers status unavailable. Can a
    different hours_back change the answer?
    Correct: Yes. The baseline is every capture before the window, so a shorter window can give it one.
    Prevents it: "unavailable: no baseline exists yet."

Test plan

  • dotnet build Darling/Darling.Tests/Darling.Tests.csproj -c Debug - Build succeeded, 0 Warning(s), 0 Error(s).
  • dotnet build Lite.Tests/Lite.Tests.csproj -c Debug - Build succeeded, 0 Warning(s), 0 Error(s).
  • Targeted, Darling: McpToolGuideTests, McpToolsListBudgetTests, McpZeroIsAMeasurementTests,
    McpToolGuideHeadsQsRegressionsTests, DarlingQueryStoreRegressionsTests, McpPayloadContractCensusTests -
    Total: 68, Failed: 0.
  • Targeted, Lite: McpToolGuideTests, McpToolsListBudgetTests, McpToolGuideHeadsQsRegressionsTests,
    McpMissMessageParityPinTests, QueryStoreRegressionsToolTests - Total: 83, Failed: 0.
  • Red-watch: broke the not_collected head sentence, saw McpToolGuideHeadsQsRegressionsTests fail,
    restored it, and saw it pass again.
  • python plans/3898-d9/dropcheck.py origin/dev HEAD get_query_store_regressions - missing=0 on both products.
  • git merge origin/dev - clean, no conflicts. It pulled in the unrelated PR get_resource_semaphore: add interval_seconds beside sample_interval_seconds (#3653 item 17) #4103 memory-grant changes.
  • Full suite, Darling (Darling.Tests.exe, no rig, live-PG classes skip): Total: 13,321, Failed: 2, Skipped: 636.
    Both failures are in DarlingInstallLocationTests (Windows ACL / install-tree-lock tests). They are the known pair from Install scripts: the service account gets Read & Execute on the install root and Modify only where it writes (#4052) #4090.
    They fail on any non-elevated local run, and CI runs elevated. This PR touches no install-tree file.
  • Full suite, Lite (Lite.Tests.exe): Total: 5,263, Failed: 0, Skipped: 0.
  • Plain-English checker on this PR body: body_4109.md: 29 hit(s), doc mode. The remaining hits are 28 D9 labels and the attribution emoji,
    which are allowed.

Coordinator verification

  • I merged origin/dev (74489820) into the branch. The dropped-sentence check shows missing=0 on both products. No parameter description changed.
  • I checked every head fact against the code on both products. The recent window runs from as_of - hours_back to as_of, and the baseline is every capture before the window starts. The CPU gate is > 25 in the WHERE clause. Rows sort by additional_duration_ms DESC. The duration and I/O percents divide by NULLIF(baseline, 0). The CPU percent cannot arrive null, because the gate drops a NULL comparison, so naming only the duration and I/O percents is exact. severity is null when the duration percent is null. Every head fact is correct. Head facts fixed: 0.
  • The re-pointed McpZeroIsAMeasurementTests pin: the head still carries both halves of the guardrail. null, not 0%, when their baseline is 0 says a null percent is undefined and is not zero. additional_duration_ms is the ranking key says a null percent never enters the sort. The two literal phrases stay pinned in the tail. D9 Q2, Q4 and Q11 show that a reader gets this right.
  • The lane's body had no Status routes table. I added one from the code.
  • D9 round 1: a fresh haiku reader answered the 12 lane questions and 2 of mine (the last two above). It saw only the served head and parameter text. Misreads: 0. Q5, Q6, Q8 and Q12 came back as "can't tell", and the reasoning in Q5 and Q6 matched the correct answers. My two questions test the word "yet" in the status clauses against a past window. The reader answered both correctly, so I left that wording alone.
  • PR body: I fixed 4 plain-English hits (long sentences). I replaced the guess about the two DarlingInstallLocationTests failures with their known cause. I added the checker line and the attribution footer.
  • After the merge, both builds have 0 warnings. Targeted classes: Darling 271 tests and Lite 165 tests, 0 failed. I did not run the local full suites again, by the coordinator's ruling. The lane ran them before the merge, and CI runs them before the auto-merge.

Handoff

Nothing is deferred. The lane's one twin pair is fully converted, green, and pinned. No topics were needed for
this lane. The two DarlingInstallLocationTests failures are a known
local test bug from #4090. A separate fix is in progress.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

erikdarlingdata and others added 3 commits September 23, 2026 22:22
)

Converts the D6 twin pair (Darling and Lite) to the two-tier
McpToolGuide description: a terse head under the 620-char budget for
tools/list, and the full original prose as the get_tool_guide tail.
Re-points McpZeroIsAMeasurementTests' null-percent pin from the whole
literal to the head's own guardrail sentence, with the field-name-level
detail (undefined_percents, "null percent never sorts as 0") pinned in
the tail instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC
@erikdarlingdata
erikdarlingdata marked this pull request as ready for review September 24, 2026 02:38
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 24, 2026 02:38
@erikdarlingdata
erikdarlingdata merged commit 5c4c19f into dev Sep 24, 2026
16 of 18 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/3898-content-qsregressions branch September 24, 2026 02:51
erikdarlingdata added a commit that referenced this pull request Sep 24, 2026
* Add 48 buffered CHANGELOG entries in one splice

Adds 48 entries and 48 link refs (#4057, #4077, #4079, #4081, #4082, #4083, #4084, #4085, #4086, #4087, #4088, #4089, #4090, #4091, #4092, #4093, #4095, #4096, #4099, #4100, #4101, #4103, #4105, #4106, #4107, #4108, #4109, #4110, #4111, #4113, #4114, #4115, #4116, #4117, #4118, #4119, #4120, #4121, #4122, #4123, #4124, #4125, #4126, #4127, #4131, #4133, #4136, #4141). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran.

Entries found under a bare section line (none unless the old script ran again) move into the matching ### section, below the new entries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC

* Changelog splice: add #4137's entry, which merged before the splice but was missed

#4137 (pg_plan_capture reads csvlog) merged at 05:57Z with a CHANGELOG entry section in its
body. It was neither spliced nor listed as needing no entry. Its entry goes under Fixed, right
after #4136's, and #4136's last sentence now points to it instead of saying plan capture is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K

---------

Co-authored-by: Claude Opus 5.5 (1M context) <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