Repository navigation
mute_analysis_finding stops writing the hash as the path and stops registering the same mute twice, remove_server can remove a server that never connected, and eight MCP descriptions stop saying what the code does not do (partial #3653 A15/A16) - #3696
Merged
Merged
Conversation
…hain, remove_server resolves against the definitions it deletes from (partial #3653 A15/A16, behaviour bugs)
…tion awareness and names its PostgreSQL refusal, the CPU note names both source cadences, plan tools state the top_operators cut and basis and drop the CREATE INDEX paste, the AG tool surfaces its reader's traps, the duration-trend catalogue line says rates, and get_resource_semaphore orders rows the same way on both SKUs (partial #3653 A15/A16)
…t stacked above the mute pin's (DocCommentHygieneTests.NoMemberCarriesTwoStackedSummaryBlocks)
Closed
39 tasks done
erikdarlingdata
enabled auto-merge (squash)
September 19, 2026 06:26
7 of 12 tasks
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
… after the viewer stopped; and two sentences that #3695/#3696 made false (partial #3653) (#3700) DarlingTrendReader.QueryDurationTrendSql becomes an alias of DurationTrendRouting.QueryDurationTrendRawSql(withDatabaseFilter: false), so get_query_duration_trend's raw route reads query_stats.sample_interval_seconds three-state (0 = restart, unrated; NULL = no stored interval, LAG; n = measured) exactly as the viewer's chart has since #3695. Until now the tool kept its own LAG-only copy and published a confident 0.00 ms/sec for a restart collection the viewer skipped. ProcedureDurationTrendSql (MCP and viewer) become aliases of the builder's procedure output too - #3695's line-equality pin proved that a no-op. The must-move residual pin in DarlingMcpTrendToolsTests is retired into an identity pin over the declarations (the #3684 idiom) plus a live round-trip through the real reader and the tool over a planted NULL/NULL/0/120 series. get_query_duration_trend / get_procedure_duration_trend / get_query_store_duration_trend descriptions and the instructions row say what the reads do now: plan-cache raw points are rated over the stored interval (restart = null), Query Store rollup points over their bucket width (first bucket included), only raw Query Store points LAG-rated and null-first. get_cpu_utilization's Darling note ports Lite's #3696 correction verbatim (one RING_BUFFER_SCHEDULER_MONITOR record per minute; 15 s is Azure's sys.dm_db_resource_stats); the Lite pin becomes a both-SKU theory that also asserts the two notes byte-identical.
This was referenced Sep 19, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 20, 2026
…ption names its reader's two documented traps, the web catalogue's duration-trend line says rates and never percentiles, and server-tabs.js reads every Stamped tool by captured_at (partial #3653 A15/A16) (#3801) The six drifts item 17 still listed were all delivered before this branch (#3665, #3696, #3697); three of the fixed texts had nothing holding them. Pins only, no wire change: - DarlingMcpAgToolsSurfaceTests.Description_SurfacesTheReadersTwoDocumentedTraps anchors on DarlingAgReader's own bolded paragraph headers, then holds the description and the instructions paragraph to each trap's mechanism, consequence and tell (NO row for a server with no AGs / last non-empty snapshot / collection_time; lag and queue depth NOT banded / secondary_lag_seconds / is_suspended / 0 lag while suspended). - DarlingMcpTrendToolsSurfaceAndSqlTests.QueryDurationTrend_CatalogueLine_NamesTheRatesThePayloadCarries_NotPercentiles holds the catalogue line to ms/sec + executions/sec + 'not percentiles', SerializeTrend to the two rates it projects, and the whole catalogue to 'percentile' only ever negated. - ServerPageTabsTests.TheStampedReads_AreReadByTheirStamp_NeverTheRetiredKey derives the Stamped + SearchBound roster from McpLatestSnapshotStampTests.LatestTools, slices every stat/table/fanout call by bracket balance, and refuses key: "collection_time" in any descriptor array bound to one of them; the four named reads must be reached; SESSION_STATS must show captured_at. Each red against its defect, then green, in the Mac harness.
This was referenced Sep 20, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 20, 2026
… not a design" on a rule nobody made: missing_indexes[].create_statement is back on both SKUs and the Dashboard twin, every row carries the maintainer's one fixed caveat, the descriptions reframe from suppression to corroboration-with-caveats, and the engine's MISSING_INDEX card opens with the same sentence at the Information rung (Closes #3805) (#3820) #3696 (#3653 A15/A16) dropped create_statement from McpPlanAnalysisFormatter's missing_indexes[] rows, put a "hint, not a design" note in its place, rewrote seven descriptions to say "no CREATE INDEX text" and pinned the absence, citing a "no-missing-index-recs rule". The rule it acted on is CONTRIBUTING.md's bullet about sys.dm_db_missing_index_* — DMVs nothing in this tree reads — misapplied to the plan's own MissingIndexGroup, and to one surface only: the analysis engine's MISSING_INDEX finding kept rendering the very same parser statement as remediation_command. The maintainer: "i never made that rule? there are several things that recommend ddl, e.g. enabling rcsi in the analysis engine" — and then the spec: legitimate as corroboration when traced from a measured-slow query, never a defining characteristic of the engine, never delivered without the caveats (plan-cache-bounded uses, one operator's statement-scoped impact, the regression risk a new index carries). Formatter: create_statement = idx.CreateStatement back on every row; impact_basis stays; `note` becomes `caveat`, the maintainer's parity sentence verbatim as the public const McpPlanAnalysisFormatter.MissingIndexCaveat so every emitter reads one string. Descriptions (4 Darling + 3 Lite) and both instructions lines: the field named as the optimizer's suggested CREATE INDEX for this statement, corroboration for a statement already measured slow and never a diagnosis, every row carrying the fixed caveat. Engine: FactAdvice.MissingIndexCaveat (duplicated literal — the assembly references no project; pinned byte-identical) opens the MISSING_INDEX card on both the static and the composed path; the four measured findings' co-fire prose stops promising "those indexes cut the work"; FactScorer scores MISSING_INDEX at MissingIndexCorroborationSeverity 0.25 (the Information rung, below every standing misconfiguration, still rooting under its unchanged story_path_hash) instead of the 0.4 advisory base. CONTRIBUTING.md's bullet rewritten to the maintainer's position. No sys.dm_db_missing_index_* reader exists to label: every user_seeks reader is sys.dm_db_index_usage_stats. Pins: McpPlanAnalysisEnvelopeTests asserts the exact parser statement and the exact caveat on every group (two-group fixture), the sentence verbatim, formatter == engine, both advice paths, the rung; DarlingMcpPlanToolsTests + McpDescriptionTruthPinTests pin the corroboration fragments and the suppression's absence; Dashboard FactScorerTests reads the constant.
This was referenced Sep 23, 2026
Merged
erikdarlingdata
added a commit
that referenced
this pull request
Sep 24, 2026
…4117) * analyze_procedure_plan moves reading guidance off tools/list (#3898) Darling and Lite both serve a 608-char head (was 926/912) plus a get_tool_guide pointer; the moved detail (operator ranking fields, the missing-index caveat's exact wording) lands verbatim in each product's tail. The differing key parameter (sql_handle on Darling, plan_handle on Lite) is named in one head clause per D6. Re-points Lite.Tests/McpDescriptionTruthPinTests's shared plan-tools Description() helper from head-only to head+tail, since the fragments it pins are guardrails against the #3696 suppression regression coming back, not a rule that they must live in the head specifically. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QQh8LczP1HGLXQYAx4oTZC * analyze_procedure_plan: the head carries the plan-tool guardrails; the truth-pin helper stays head-only (#3898) The Lite truth pin reads the served head on purpose (#3898 D3), so this PR no longer re-points it to head plus tail. The head now carries the pinned missing-index and operator-cut fragments in 587 characters (served 618). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUU29PuGg9TUFBsACgZg2K --------- Co-authored-by: Claude Sonnet 5 <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.
Two MCP writes stop acting on lies, and eight descriptions stop telling them
This is the A15/A16 "Description drift + envelope vocabulary" item of #3653, PARTIAL: the two behaviour bugs and the description lies land here; the truncation/severity vocabulary collapse and the four
StampedUnderCollectionTimetools are deliberately NOT here (see "What this does NOT do"). #3653 stays open.The item is one shape twice over. A write that stored something other than what its name says and reported success either way (
mute_analysis_findingwrote the hash into the path column and registered a second row for a second identical mute;remove_serverresolved the name against a table it does not delete from, so a server that had never connected could not be removed by the tool that added it). And a set of sentences a caller trusts that the code beneath them did not honour — an edition-aware audit with no edition branch, a "15-second ring buffer" that writes once a minute, a silentTake(10)whose ranking basis switched between a stopwatch and a guess, a bareimpactbeside paste-ready DDL, an AG badge that stays green over a lagging secondary with nothing in the description saying so, a "percentiles" catalogue line over a rates payload, and one tool whose rows came back in two different orders on the two SKUs.The behaviour bugs
1.
mute_analysis_finding— idempotent per (scope, hash), and the registry row names the chain. Both SKUs. The MCP entry point knows only the hash, and the pre-fix code wroteStoryPath = story_path_hash— a row that claims to name a diagnostic chain and names a checksum. And a second mute of the same hash in the same scope inserted a second row and reportedregistered: trueagain; the readers were never confused (they fold by hash, the viewer unmutes the smallestmute_id), but the caller was.PgFindingStore.MuteStorySql/ LiteFindingStore.MuteStorySqlare now ONE guarded statement, byte-identical on PostgreSQL and DuckDB:INSERT … SELECT $1, $2, $3, COALESCE($4, (newest retained finding's story_path for the hash), $3), $5, $6 WHERE NOT EXISTS (same hash, same scope) RETURNING story_path. The path is the caller's when it holds one (the viewer does), else resolved fromanalysis_findingsby hash (the hash is a function of the path —InferenceEngine.ComputeHash— so any row carrying it names the same chain), else the hash itself as a placeholder, because the column isNOT NULLand relaxing it is a rung this PR does not take; the result reports that case asStoryPath: nullso the tool says "unknown" instead of echoing the checksum as a path. Scope isCOALESCE(server_id, 0) = COALESCE($2, 0), soNULLand the legacy0all-servers rows fold together the way every reader already treats them.RETURNINGdistinguishes a written row from a pre-existing one without a second read.The store method returns
MuteWriteResult(MuteRegistration Registration, string? StoryPath)—Registered/AlreadyMuted/Failed(Darling's store swallows-and-logs; Lite's throws, so it never returnsFailed). The tool payload gainsalready_muted,story_path, and a fourth status"already_muted"(registered false, nothing written, a different reason is NOT recorded — said in the note).MuteFindingAsyncon bothAnalysisServices returns the result; the Darling viewer's mute button compiles unchanged (it awaits and never read thebool).Why a guarded INSERT and not a unique index or the write lock: no unique key exists over (scope, hash) and adding one is a rung; two callers muting the same pattern in the same statement's flight can still both land, and that duplicate is exactly the pre-fix state the readers tolerate. Lite's
FindingStoreTests.TheWritePathsTakeAReadLockOnPurpose_AndSayWhypin now states this in words (the NOT EXISTS is a courtesy to the caller's envelope, not an invariant the lock has to defend), so the guard is never mistaken for a reason to take the write lock.2.
remove_server— resolves against the definitions it deletes from. Darling-only tool. The connected-servers registry (servers) is written by the worker on a server's FIRST successful connection. A server thatadd_serversdefined a minute ago with a wrong password, or on an unreachable host, had a definition and no registry row — and the tool, resolving against the registry, answerednot_foundfor the very server its sibling had just added; the only undo was psql. It now loadsconfig_monitored_servers(DefinitionsForRemovalSql: the #2218 identity columns, rebuilt into the storage name through the sameServerIdHelper.BuildStorageNamethe worker uses, plus anEXISTSagainstservers), applies #3615's exact-then-unique-partial rule unchanged (ResolveForRemovaland its six pins are untouched), and DELETEs by the definition's ownserver_id. Payload:matched_in: "config_monitored_servers",ever_connected(per candidate onambiguous, on the row onremoved),display_name, and a note saying whether history exists under the id. A name the registry resolves but no definition does — the darling.json-defined shape — returnsnot_foundwithmatched_in: "servers"and a message that names the real reason, instead of the old "registered but has no definition" guess. Nois_enabledfilter: a disabled definition is still one an operator may remove.The descriptions — before → after, one line each
audit_config(both SKUs): "accounting for edition (Standard vs Enterprise)" → the edition is reported for context and no check branches on it (MAXDOP is topology-based, the rest resource-based). Darling's adds: SQL Server only; a PostgreSQL target is refused withnot_collectedand pointed atget_pg_server_config,get_pg_logging_audit(newnext_toolsrow) and theCONFIG_PG_*facts. Instruction rows on both SKUs drop "Edition-aware".get_cpu_utilizationnote (Lite): "1-minute averages of 15-second ring buffer samples" → oneRING_BUFFER_SCHEDULER_MONITORrecord per minute on-prem / MI / RDS, onesys.dm_db_resource_statsrow per 15 s on Azure SQL DB,samples_in_bucketis the measured count. The Darling twin line (DarlingMcpDataTools.cs:94) is byte-identical and NOT changed here — that file belongs to the wwwroot lane tonight; it is a one-line port (see report).analyze_*_plan(sharedMcpPlanAnalysisFormatter, so both SKUs and the frozen Dashboard twin by construction): the silentTake(10)→ per statementoperators_cap: 10,operators_returned,total_operators,truncated(observed against the whole population, never inferred),operators_ranked_by: "actual_elapsed_ms"|"cost_percent (optimizer estimate)".missing_indexes[]: bareimpact→impact+impact_basis(the showplanMissingIndexGroup/@Impact: estimated, statement-scoped, not additive);create_statementremoved and replaced by the three column lists (already there) plus anotethat the optimizer's suggestion is a hint, not a design. The plan viewer and the alert remediation text still render the DMV's DDL for a human reading one plan; the MCP surface is the one an agent may act on unread. Descriptions of the four plan tools on both SKUs and both instruction tables say the new shape; Lite's "Missing indexes with CREATE statements" line is gone.get_ag_health(Darling; Lite has no twin): "Severities are computed server-side" → …and restate the DMVs' OWN verdicts only: lag and queue depth are NOT banded (a lagging async secondary under HEALTHY stays healthy — read the numbers), lag reads 0 while suspended (read it withis_suspended), and each group'scollection_timeis that server's newest AG collection — a server whose AGs were dropped keeps returning its last non-empty snapshot, so an oldcollection_timeis that case. Same two sentences added to the instruction paragraph.DarlingWebEndpoints.cscatalogue:get_query_duration_trend"Query-duration percentiles over time." → "Query elapsed ms/sec + executions/sec over time (per-interval rates, not percentiles)." (the payload has been rates since The duration-trend trio routes by retention tier and discloses its source like get_query_trend already does, so a 7-day request no longer returns 4 days labelled quiet (#3541 A2) #3590/The MCP trend reader's tier ladder and hourly SQL were pinned EQUAL to the Storage definitions #3666 moved them to, which fails only after two copies drift — they are now aliases; and the Query Store duration chart, the one Performance Trends chart left untitled, says what served it and where the rollup's floor cut the window's head (#3653) #3684; Lite has no web catalogue).get_resource_semaphore(Darling reader):ORDER BY resource_semaphore_id, pool_idon the latest-snapshot AND window reads →pool_id, resource_semaphore_id, Lite's order and the order the pool-lens reads in both SKUs already use. Both SKUs always took the same newest snapshot (theMAX(collection_time)probe never drifted); only the row order did.mute_analysis_finding/remove_serverdescriptions and instruction rows carry the new keys and statuses above.Claims in the item found already true or over-stated
audit_configalready refused a PostgreSQL target withnot_collected(landed in the PostgreSQL-target analysis engine: design and v1 build #3542 lane); this PR adds theget_pg_logging_auditpointer and makes the DESCRIPTION say so.no_config_dataremains the SQL Server no-facts answer.top_operatorsbasis switch existed; the cut did not publish itself. Themissing_indexesDDL and unlabeled impact were as described.get_resource_semaphore: both SKUs took the latest snapshot the same way; the drift was ORDER BY alone (two statements, not one).What this does NOT do
StampedUnderCollectionTimetools (get_database_sizes,get_running_jobs,get_server_properties,get_session_stats) — the wwwroot lane owns them withserver-tabs.js.DarlingMcpDataTools.cs:94(Darling's "15-second ring buffer" twin) — same lane's file; one-line port pending.analysis_muted, no change to how the viewer or the alert remediation render missing-index DDL, no change toResolveForRemovalor the read resolver.Blast radius
mute_analysis_findingpayload:+already_muted,+story_path,statusgains"already_muted";registeredis now FALSE for a repeat mute (it used to be true).PgFindingStore.MuteStoryAsync→Task<MuteWriteResult>(wasTask<bool>),storyPathnullable; LiteFindingStore.MuteStoryAsync→Task<MuteWriteResult>(wasTask).DarlingAnalysisService.MuteFindingAsync/ LiteAnalysisService.MuteFindingAsyncreturn the result. All existingawaitcallers compile unchanged.remove_serverpayload:+matched_in,+ever_connected,+display_name,+note;ambiguous.candidates[]gainsever_connected; anot_foundmay carrymatched_in: "servers"+candidates. A never-connected definition is now removable (the point).−create_statement,+impact_basis,+noteper missing index;+operators_ranked_by/operators_cap/operators_returned/total_operators/truncatedper statement.get_resource_semaphoreon Darling: same rows, pool-first order.Tests
Every new pin was EXECUTED here before commit — Lite in-process on DuckDB 1.5.5 (a throwaway
net10.0-windowsharness Compile-Including the classes, WindowsDesktop stripped from the runtimeconfig), Darling through aDarling.Tests-namednet10.0harness against a throwawaytimescale/timescaledb:2.28.1-pg18migrated to ladder 131 withDARLING_TEST_PGset. The one thing NOT executed here is theremove_serverhalf ofAddServers_…_ThenRemoveServer_AgainstDevPostgres, because its add half needs DPAPI; the same six shapes were executed through a harness-only rig test (not committed) and CI is the first execution of the committed form.New / extended, both compile-verified (0 warnings) and first executed in CI:
McpMuteReportsWhatItMatchedTests(+4): same hash twice →already_muted, one row, first reason kept; idempotence per scope incl. a planted legacyserver_id = 0row counting as global; path rule (caller's → retained finding's → hash placeholder reported as unknown); description tokens.FindingStoreTests.TheWritePathsTakeAReadLockOnPurpose_AndSayWhy: anchors moved to the new signature /ExecuteScalarAsync; pins the "not an invariant the lock has to defend" sentence at the mute site.McpDescriptionTruthPinTests(new, 3):audit_configno edition claim + no edition branch in the body + no "Edition-aware" in instructions;get_cpu_utilizationnote names both cadences; plan-tool descriptions name the cut and "no CREATE INDEX text".DarlingMcpToolsTests: live mute round-trip assertsstory_path= the planted chain on the wire and in the row, the second mute →already_mutedwith count 1, the unmatched hash →story_path: nullwith the hash placeholder in the row;+AuditConfig_Description_DoesNotClaimEditionAwareness_AndNamesThePostgresRefusal; the mute description pin gains the new tokens.DarlingMcpServerAdminToolsTests:DefinitionsForRemovalSql_…dialect pin; the description pin gains the definitions/ever_connected/matched_intokens; the live round-trip now registers ONLY the SQL host inserversand removes the never-connected Windows host and the never-connectedhost:AppDbdefinition, asserts three ambiguous candidates withever_connectedper row, and thematched_in: "servers"not_found on the darling.json shape.McpPlanAnalysisEnvelopeTests(new, 3): synthetic 12-RelOp estimated plan →operators_returned 10 / total_operators 13 / truncated true / ranked_by cost_percent, rows actually in that order; 3-RelOp plan → not truncated; missing index → labelled impact, three column lists, note, nocreate_statement, no DDL text anywhere in the row.DarlingMcpMemoryGrantToolsTests.TheSameToolName_OrdersTheSemaphoreRowsTheSameWay_OnBothSkus: both Darling consts and Lite's source hold one ORDER BY (pool first), with the drifted spelling excluded by a regex proven against literals (the DISTINCT ON subquery legitimately leads with the semaphore on both SKUs and is excluded by lookahead).Rosters checked:
AsOfWindowAnchorTests(noDateTime.UtcNowadded to any tool body),RepoFileAdoptionTests.s_lfReaders(the new readers are raw, single-line anchors),FindingStoreTestslock-site count unchanged at 7 (the path resolution lives inside the one statement),PgTargetMcpSurfaceTests.AuditConfig_AnswersNotCollectedForAPostgresTarget_BeforeItReadsAnyFact(all tokens still present).Partial #3653 — no
Closes.Which component(s) does this affect?
How was this tested?
Compile-only on macOS for the
net10.0-windowstest projects (0 warnings on Darling.Tests; Lite.Tests carries dev's one pre-existing xUnit2000 warning atLogTailOverlapThresholdPinTests.cs:95); Lite pins executed in-process on DuckDB (32/32), Darling pins + the live mute round-trip executed against a throwaway PG18/TimescaleDB rig (80/81 — the one red is the DPAPI-only add half of the admin live test, a macOS harness artifact). Rig and harnesses torn down. No SQL Server touched.Checklist
dotnet build -c Release)