Add read-only Repl plan exploration pilot#388
Conversation
erikdarlingdata
left a comment
There was a problem hiding this comment.
Thanks for this — it's a carefully built pilot, and the App MCP delegation was done with more care than these refactors usually get. I verified the details rather than trusting the description: analyze_plan/get_plan_summary delegate to literally the same ResultMapper.Map call; the truncation suffixes/lengths and top-validation ranges match McpHelpers exactly; the expensive-operator ranking is value-equivalent to the old raw-PlanNode path; both existing Register call sites pass keys equal to SessionId, so the new mismatch guard can't fire; the Web project's per-file linked Core sources are unaffected; and the read-only claim holds — nothing in the new surface writes. Nice work.
Requesting changes on three concrete items, plus dependency questions I'd like settled before merge.
The dependency
Repl/Repl.Mcp/Repl.Testing are pinned to 0.11.0-dev.181 — a dev-channel prerelease of your own pre-1.0 framework (last stable is 0.10.0, and dev.189 is already out). The packages restore fine from nuget.org, and the CLI isn't shipped in releases today, so the blast radius is contributor experience rather than installers — but before I take the pin:
- What's the rough timeline for a stable 0.11 with the MCP bits, and are the APIs used here expected to survive it?
- The NuGet packages declare no license metadata (the repo is MIT) — can you add
PackageLicenseExpressionto the packages so consumers aren't looking at "no license" on nuget.org?
Required
-
Don't let a malformed
planview.config.jsonsilently kill the desktop app's MCP server. Insrc/PlanViewer.App/Mcp/McpHostService.cs,new PlanOperations(_sessionManager)eagerly callsConfigLoader.Load(), which readsplanview.config.jsonfrom CWD or the user's home and throws on malformed JSON. That lands in the catch at the bottom ofExecuteAsync, which onlyDebug.WriteLines — so the entire MCP host silently fails to start over a config file the App path never even uses (_configis only consumed byOpenAsync, which no App tool calls). PassAnalyzerConfig.Defaultexplicitly there, or make the config load lazy. -
Disclose and pin the MCP content changes. "Preserving tool names and JSON envelopes" is true for shapes, but three outputs change content:
get_plan_warningsnow includes operator-level warnings. The old code only flattenedStatements[].Warnings, whichResultMapperpopulates with statement-level warnings only — so the old tool underreported and disagreed withget_plan_summarytotals. That's a fix, but an undisclosed one, and it changes an AI-client-facing contract.get_expensive_operators.object_namenow emitsFullObjectName ?? ObjectName(three-part names) because it flows throughResultMapper.MapNode(ResultMapper.cs:251), where the old code emitted bareObjectName.- An invalid
severityargument now returns an error string (via the newArgumentException) instead of the old "No {severity} warnings found in this plan."
Please note these in the PR description and add an App-side test pinning the new
get_plan_warningscontent — which leads to: -
docs/repl-pilot.mdclaims characterization tests that aren't in the PR. "Existing MCP JSON shape characterization before delegation refactor" — no such test is committed; per the PR body that check was a manual SHA comparison. Either commit the characterization test (it's exactly the test that would have caught item 2) or reword the doc to say the verification was manual.
Suggestions (non-blocking)
PlanOperations.OpenAsyncre-implements parse → analyze → score, which already exists asPlanAnalysisRunner.Analyze— but that helper lives in the Cli project where Core can't reach it. Consider moving the runner to Core and sharing it, so this path can't diverge fromanalyzewhen the pipeline grows a step. While there: surfaceplan.ParseErrorin theInvalidDataExceptionmessage instead of the generic "Could not parse any statements" (the legacy path has the same gap; cheap to fix here).- The CLI stdio server and the desktop app's HTTP MCP server are both named
PerformanceStudio. A client configured with both sees two identically named servers with different tool sets — name the CLI oneplanview. - Session IDs derive from filenames, so
list.sqlplan/open.sqlplanproduce IDs that collide with the literalplan list/plan openroutes and become unaddressable. Reserve those words inCreateBaseSessionId. PlanReplModule:warnings/operatorswrapArgumentExceptionviaExecute(), butsummary/missing-indexes/listare bare — a session evicted between context validation and the handler throws a rawKeyNotFoundException. Wrap them uniformly.- README: worth calling out that bare
planviewnow blocks on stdin (scripts that shelled out toplanviewexpecting help text will hang), and that root--helpdoesn't mentionplan/open/repl/mcpat all — a one-line pointer in the legacy root description would help discoverability. McpSmokeTestsspawns a childdotnetprocess with no explicit timeout — a hung MCP handshake hangs CI rather than failing. Consider a linkedCancellationTokenSourcewith a ceiling.- Test placement: the routing/REPL/MCP tests make
PlanViewer.Core.Testsreference the Cli project. Fine for the pilot; aPlanViewer.Cli.Testsproject would keep the boundary honest. - Nits:
(char)34→\"inPlanReplTests;plan.Batches.SelectMany(...).Count() == 0→!....Any(), and the emptiness check could run beforeAnalyze/Score;InMemoryPlanCatalogis the only unsealed new class;Register(session)skips theSessionIdvalidation thatTryRegisterhas.
🤖 Review drafted with Claude Code
|
Thanks for the detailed review. I pushed the follow-up as two commits:
Dependency questions
Required changes
Suggestions addressed
I kept the two non-blocking structural suggestions (moving the analysis runner into Core and splitting CLI tests into a separate test project) out of this already-large pilot follow-up. Verification
When convenient, could you please take another look? |
|
Thanks for the thorough follow-up — this addressed the review properly. I verified rather than skimmed:
One regression to fix, and three optional notes. Fix: the README screenshot link is now broken. Line 60 references Optional:
CI is approved and running on this head. 🤖 Review drafted with Claude Code |
|
@erikdarlingdata Update pushed in This includes the README link correction and 12-hex session ID suffixes you requested, plus the follow-up fixes from independent review:
Validation on this head:
I am still running the final independent review panel against the updated head, so this is a progress update rather than a claim that the PR is merge-ready. I will follow up with the final panel result and any remaining fixes. |
|
@erikdarlingdata Thanks — I applied the requested convergence in
Validation on this exact SHA:
I am freezing this PR here as requested; any unrelated hardening findings will go to separate follow-up PRs. |
erikdarlingdata
left a comment
There was a problem hiding this comment.
Re-reviewed the full delta since d8f90c3 (+2,502/−452 across 37 files). Both items I asked for are fixed — README image link reverted, 12-hex session suffixes — and several things in this round are genuinely excellent: HistoricalCliContractTests running the real CLI and pinning the compact-output SHA is exactly the characterization I asked for originally; the Query Store capture contract test comparing all four raw tools against a graph-backed session is thorough; the bare-planview routing revert is the right call and consistently documented; and the legacy analyze path staying entirely outside the new preflight/budget machinery is the right separation.
That said, this round introduces three regressions that need fixing, and I want to talk about scope.
Required fixes
-
The Ordinal path-containment switch breaks
plan_openon Windows and default macOS volumes.McpPlanPathPolicynow compares containment withStringComparison.Ordinalon every platform, andResolveDirectoryEntryrequires the advertised root's per-segment casing to match on-disk casing exactly. Two concrete failures on case-insensitive filesystems:- Clients commonly advertise lowercased paths (VS Code famously sends
file:///c:/...). A root advertised asc:\plansagainst on-diskC:\Plansgets dropped inResolveDirectoryEntry→ all roots dropped → every open denied. - Even with per-segment casing matching, the drive letter alone kills it:
Directory.EnumerateDirectories("c:\")preserves the lowercasec:\prefix, so the resolved root isc:\plans\..., whileGetFinalPathNameByHandlereturns canonicalC:\Plans\...— the OrdinalStartsWithfails and the opened stream is discarded. Same class of failure on macOS:F_GETPATHreturns on-disk casing, non-Windows roots aren't casing-canonicalized at all, and APFS defaults to case-insensitive. - The no-roots CWD fallback can also hard-fail:
ResolveExistingDirectory(Directory.GetCurrentDirectory())throws (uncaught) when the shell's CWD casing differs from disk (cd c:\github\...in cmd).
None of this is exercised by CI, which is ubuntu-only. The threat being defended against — case-sensitive sibling directories on NTFS — requires a per-directory
fsutilopt-in that essentially nobody enables, and it can be defended without breaking everyone: canonicalize both sides through the kernel (you already resolve the opened file's final path — resolve each root directory the same way, e.g. open a directory handle andGetFinalPathNameByHandle/realpathit) and then Ordinal-compare the two kernel-final strings. Alternatively, revert toOrdinalIgnoreCaseon Windows/macOS and document the fsutil edge as out of scope. Either restores function; as written, the feature is broken for the majority of real clients on the two OSes this product ships desktop builds for. - Clients commonly advertise lowercased paths (VS Code famously sends
-
The fail-fast query cap is a regression for the desktop app's MCP server. Pre-PR, the App served any number of concurrent read tools; now
AcquireQuerySlotisWait(0)fail-fast with 4 slots shared per catalog, applied toanalyze_plan,get_plan_summary,get_plan_warnings,get_missing_indexes, andget_expensive_operators. AI clients routinely issue parallel tool calls — Claude fanning outanalyze_planacross five sessions now gets errors where it used to get answers. These are pure CPU-bound reads on a single-user local machine; please make the slots queue (a realWaitAsync(cancellationToken), optionally with a bounded wait) rather than fail fast — or exempt the desktop App path from query slots entirely and keep them CLI-side. Same question for the 2-slot open gate: fail-fast turns parallelplan_opencalls into retry churn for the client. -
Eager
ResultMapper.Mapon the desktop file-open path is pure cost.PlanViewerControlnow builds the full analysis DTO tree at registration time for every opened plan — on the load path, whether or not any MCP tool is ever called — roughly doubling retained memory for large plans (parser graph for the UI plus a full snapshot). Thesession.Analysis ?? ResultMapper.Map(...)fallback already handles a null snapshot lazily, which is exactly the historical behavior. The eager capture is justified for Query Store sessions (the graph is released there — that trade makes sense); for UI file sessions, please passAnalysis = null(or make it lazy) and keep map-on-demand.
Simplification request
The compatibility layer has crossed into invention. PlanViewer.App is an executable with no external consumers, but this round adds: a second App-local PlanSession/PlanSessionSummary pair with ToCore/FromCore converters and an implicit conversion operator; resurrected "historical overloads" on McpPlanTools that nothing calls (each constructing a fresh PlanOperations); reflection-based signature tests; and a test pinning the Register(key, session) key/payload-mismatch semantics — a wart no caller exercises. Meanwhile InMemoryPlanCatalog, which last round gained a test asserting it stays unsealed, is now internal sealed and that test is deleted — which rather proves these contracts are being invented per round. Please delete the dead overloads, the implicit operator, and the reflection tests, and keep one session type unless something real consumes the second. Complexity that defends no actual consumer is just future maintenance.
Scope
This is the third self-review wave, the PR has roughly quadrupled from the original pilot, and your comment says the panel is still running. The core of the pilot — REPL, one-shot, MCP surface, shared catalog/operations — was solid two rounds ago and is what this PR is for. Each hardening wave since has touched progressively more shipping surface (parser, analyzer, scorer, Query Store tools, the desktop load path) and this one introduced the regressions above. Please land the three fixes plus the simplification and then freeze this PR — no further panel-driven hardening waves here. If the panel surfaces more items, bring them as small follow-up PRs where each can be judged on its own.
CI is approved on the current head; note it only builds ubuntu, so item 1 will not be caught by checks — it needs the fix, not a green run, to be considered resolved.
🤖 Review drafted with Claude Code
|
@erikdarlingdata Thanks. This review appears to have been drafted against the pre-convergence tree and then submitted after The requested items are already resolved on this exact head:
That convergence commit is a net simplification ( Could you please re-review the current tree or dismiss this stale changes-request state? I am keeping the PR frozen as requested. |
|
You're right about the sequencing, and that one's on me: the request-changes review was drafted against I've now verified the current head directly rather than taking the summary on faith:
I also checked the parts of I've dismissed both stale changes-requested reviews and approved CI on this head. No outstanding code requests from review; final disposition is a maintainer decision from here. Thanks for holding the freeze. 🤖 Review drafted with Claude Code |
erikdarlingdata
left a comment
There was a problem hiding this comment.
First, an apology: I owe you one for the last round. I posted a request-changes review against a head you had already fixed — the review was drafted against dc03dc1b and submitted more than an hour after 1372214 landed, without re-checking the tree. That cost you a rebuttal you shouldn't have had to write and left a stale gate on the PR. That was my process failure, not yours, and I'm sorry for the churn.
With that owned: I've now done a final full-pass read of the complete PR as it stands (1372214, +4,794/−366 across 46 files), including every file from the last two commits I had previously only spot-checked. Findings:
- All four regression fixes are verified in the final code, not just the summary: case-insensitive containment on Windows/macOS with ordinal Linux (and the platform-comparison logic unit-tested), queued cancellable open slots,
enforceQueryAdmission: falseresolving to a true no-op for the desktop host, and lazy analysis mapping on the desktop file-open path. - The simplification is real: the App-local session/summary duplicates, converters, implicit operator, dead overloads, and reflection tests are gone. One
Core.Models.PlanSession(now carryingRawPlanXml/DatabaseName) serves UI, Query Store, CLI, and MCP, andPlanSessionManageris back to a straightforward catalog. - The Query Store path improved beyond what was asked:
AdmitSnapshotgives QS sessions bounded admission with per-planload_errorcontext instead of the old silent catch-all, and the four-tool exact-equality contract test still passes against gutted graphs. - The remaining unreviewed churn (
TextFormatter,ShowPlanParser.Costs, QS cancellation threading) is output-preserving — every emitted line unchanged — and the committedanalyze --compactSHA-256 characterization test pins the legacy contract for good.
For the record, two accepted behavior changes to be aware of: Query Store bulk loads are now bounded by the shared 32-session / 64 MiB budget (previously unbounded; overflow reports a clear load_error instead of loading), and the get_query_store_top / missing-index envelopes gained additive load_error / columns_truncated fields. Both are documented and reasonable.
CI is green on this head with 187/187 tests. Approving. Thanks for the quality of the convergence work, for holding the freeze, and for the patience with my sequencing mistake — any further hardening findings from your panel are welcome as separate follow-up PRs.
🤖 Review drafted with Claude Code
Context: unsolicited exploratory proposal
This is an unsolicited exploratory contribution proposing that Performance Studio add Repl. It was not requested, sponsored, or pre-approved by the Performance Studio maintainers, and the proposing contributor is also involved in Repl's development.
The goal is to provide a concrete prototype for evaluation—not to imply that the project has selected Repl, committed to this direction, or placed it on its roadmap. There is no expectation that this proposal be accepted; declining or closing it is a completely valid outcome if the dependency or direction does not fit the project.
What does this PR do?
Adds an experimental Repl surface for loading and exploring SQL Server execution plans without replacing the existing
System.CommandLineCLI.Repl,Repl.Mcp, andRepl.Testingto the stable0.11.0releasePlanOperationsin Coreplanviewobject_name, and numeric actual-stat defaultsanalyze,query-store,credential, and their existingSystem.CommandLinebehavior unchangedSecurity and resource boundaries
The generated MCP server is stricter than local CLI file access:
plan_openis confined to native MCP client roots after canonical path and symbolic-link resolutionopenandcloseare correctly treated as catalog-mutating tools, not read-only resourcesplan_closeprovides explicit evictionRead-only here means no plan file, database, Query Store, or credential is mutated.
open/closeonly mutate the process-local session catalog.Which component(s) does this affect?
Maintainer-review follow-up
AnalyzerConfig.Default, so an unrelated malformedplanview.config.jsoncannot prevent startup.planview.openandlistare reserved when deriving session IDs.planviewwaits on stdin.Repl dependency status
0.11.0is the current stable Repl release. The APIs used by this pilot are exercised by the one-shot, persistent-session, and real MCP tests.The Repl repository is MIT licensed, but I confirmed that the
0.11.0NuGet packages still omit license metadata. AddingPackageLicenseExpressionrequires a subsequent Repl package publication; this PR documents the issue but does not claim to fix metadata in an external package.How was this tested?
Platform: Linux, .NET SDK 10.0.301.
Plan fixtures:
row_goal_plan.sqlplan)top_above_scan_plan.sqlplan)Checks:
dotnet build PlanViewer.sln -c Release --nologosucceeds with 0 errors; the only warnings are pre-existing Avalonia/package warnings (upstream/devreports 8 under the same build)dotnet test PlanViewer.sln -c Release --no-build --nologo— 147/147 tests passanalyzeJSON is byte-for-byte identical toupstream/devforrow_goal_plan.sqlplan(SHA-2560c609fed8e250d9366eb9a6cd5eaf40b661ee30d7ba2546bd7726960592e9d87)dotnet format ... --verify-no-changessucceedsgit diff --checksucceedsdotnet list PlanViewer.sln package --vulnerable --include-transitivereports no vulnerable packagesChecklist
dotnet build -c Debug) — the branch adds no warnings; the same 8 warnings reproduce on unmodifiedupstream/devdotnet test)Scope
This is intentionally a plan-read-only pilot. It does not migrate Query Store or credential commands, mutate plan files or databases, or replace the desktop application's existing HTTP MCP host.