Skip to content

Finish #456's descent: score, override, display, and share proc-body statements - #486

Merged
erikdarlingdata merged 2 commits into
devfrom
fix/proc-descent-integration
Sep 3, 2026
Merged

Finish #456's descent: score, override, display, and share proc-body statements#486
erikdarlingdata merged 2 commits into
devfrom
fix/proc-descent-integration

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

What does this PR do?

Fixes the four analysis-integration findings from the 2026-09-02 adversarial review — #456 taught the parser and analyzer to descend into procedure/UDF bodies, but four consumers never followed:

  • BenefitScorer walked batch.Statements only: body-statement warnings got no MaxBenefitPercent and no wait-stat scoring, so the UI's benefit sort quietly buried every finding inside an EXEC plan. Now walks PlanStatements.EnumerateAll.
  • Severity overrides had the same blind spot — a user's override applied to an outer warning and silently not to the identical body warning. Same fix. (MarkLegacyWarnings was verified already-covered: it rides inside the analyzer's own descending loop.)
  • The desktop statements grid and MCP session counts were outer-only while advice saw everything — an EXEC plan showed one row and near-zero counts while Human Advice discussed warnings the UI couldn't display. The grid now lists body statements labeled by module (dbo.Proc > SELECT …) via a new EnumerateAllWithContainer traversal (defined so the plain and context-carrying walks are literally one walk), and selecting a body statement renders its operators — verified on the exec-proc fixture in the headless harness. Adjacent mirrors fixed in-pass: MCP counts come from the stored analysis summary, AllMissingIndexes descends, and the web viewer's statement-tab index now maps through EnumerateAll (clicking a body statement's tab used to render no tree).
  • The web share path was three depth-ceiling writers Stop Robot Advice from crashing the app on a deep plan (#430) #431 never listed: serialize, parse, and deserialize all ran at default MaxDepth 64, so a ~30-operator plan analyzed fine and then failed to Share (or shared and failed to open) with a misleading "object cycle" error. AnalysisJson gains Wire (deliberately not the WithoutNulls variants — existing DB shares stay readable) and Document options; the server mirrors the constant with a comment naming the source of truth, since it deliberately takes no Core reference.

How was this tested?

Six new tests (proc-body scoring/overrides, grid contents via the headless harness, and a share-envelope round-trip of a 100-operator chain that the default reader provably rejects). Full suite at dev tip + fix: 414 tests, 413 passed, 1 platform skip, 0 failed, on Windows.

Deploy note: server/PlanShare/Program.cs changed — PlanShare deploys by hand (scp + systemctl on the Hetzner box), and a redeploy was already owed for the range's dependency bump.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PvAv72Pwb8czsjDWsCCk7n

erikdarlingdata and others added 2 commits September 3, 2026 05:11
#456 taught the parser and analyzer to descend into stored procedure and
UDF bodies via the shared PlanStatements.EnumerateAll, but four consumers
kept walking batch.Statements and quietly saw a different plan:

- BenefitScorer.ScoreCancellable never visited body statements, so their
  warnings had no MaxBenefitPercent and no wait-stat scoring — the UI
  sorts unquantified warnings last, burying every finding of an EXEC plan.
- PlanAnalyzer.ApplySeverityOverrides skipped body statements, so a
  user's severity override silently did not apply inside a procedure.
  (MarkLegacyWarnings was checked and is fine: it runs per-statement from
  inside the analyzer's EnumerateAll loop.)
- The desktop statements grid and the MCP session registration showed one
  row (the EXEC's synthetic root) and near-zero counts while Human/Robot
  Advice discussed warnings the UI could not display or navigate to. The
  same mismatch lived in McpQueryStoreTools.CaptureSession, whose counts
  now come from the analysis summary stored on the same session, and in
  ParsedPlan.AllMissingIndexes, which feeds both registrations.
- The web viewer's statement tabs index result.Statements (EnumerateAll
  order) but ActiveStmtPlan mapped that index into the outer-only batch
  list, so clicking a body statement's tab rendered no operator tree.

Body statements need a visible home, so the traversal grew a context-
carrying form, EnumerateAllWithContainer, that pairs each statement with
the module path it lives in ("dbo.Proc", "dbo.Outer > dbo.Inner");
EnumerateAll is defined on top of it so the two cannot diverge. The grid
prefixes body rows with that path for display only — copy and
open-in-editor still hand out the statement text exactly as recorded.

Tests: body warnings carry benefit scores (fixture) and wait-stat
warnings (synthetic nested plan); severity overrides reach body
statements; container paths are asserted; and a headless-UI test drives
the real PlanViewerControl over the exec_stored_procedure fixture,
asserting the grid row count matches EnumerateAll, body rows carry the
module prefix, and selecting a body statement renders its operators.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PvAv72Pwb8czsjDWsCCk7n
#431 made AnalysisJson the one place that knows how deep a serialized
AnalysisResult goes (MaxDepth 1024 against the System.Text.Json default
of 64, two JSON levels per operator), precisely because inline options
keep getting rebuilt without it. Review found three more such sites, all
on the web share path, so a deep-but-realistic plan (~30 nested
operators) analyzed fine and then failed to Share — or shared and failed
to load — with an "object cycle" message pointing at the wrong cause:

- PlanShareService.ShareAsync serialized the upload envelope with
  default options;
- PlanShareService.LoadAsync parsed the share with default
  JsonDocumentOptions and deserialized the result at the default 64
  again;
- server/PlanShare's /api/share parsed the uploaded body with default
  JsonDocumentOptions, turning a legitimate deep upload into 400
  "Invalid JSON" before ttl_days was ever read.

AnalysisJson grows a Wire options set (default formatting, only the
ceiling raised — shares already in the database were written unindented
with nulls, and the fix is the ceiling, not a wire-format change) and a
Document counterpart for JsonDocument.Parse call sites. The web project
links AnalysisJson.cs the way it links the rest of Core's sources.

The server cannot reference PlanViewer.Core, so it mirrors the constant
as a literal with a comment naming AnalysisJson as the source of truth —
a shared constant only helps call sites that reference it; this one
cannot. The Core depth test now says so too, as the tripwire for anyone
changing the number.

Tests: the exact share envelope shape ({result, text, ttl_days} →
JsonDocument → GetRawText → Deserialize) round-trips a 100-operator
chain through the shared options, and the default reader is shown to
reject the same payload so the options are provably load-bearing. The
Web and server call sites themselves are out of this suite's reach
(Blazor WASM project not referenced; server references nothing), so they
are verified by inspection and the contract is pinned here.

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

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown

Reviewed the diff in full (server/PlanShare, PlanViewer.App control/MCP code, PlanViewer.Core traversal/scoring/JSON options, PlanViewer.Web share path, and the new/changed tests). This is a clean, well-targeted fix — no correctness, injection, or crash issues found.

Specifically verified:

  • PlanStatements.EnumerateAllWithContainer preserves the exact push/pop order of the original EnumerateAll (UDF plans before StoredProcPlan, both pushed post-yield), so EnumerateAll(IReadOnlyList<PlanStatement>) delegating to it is a true no-op for existing callers — confirmed by the pre-existing APlanWithNoProcedureEnumeratesUnchanged test still passing conceptually and the new BodyStatementsKnowTheirContainingModule test asserting statement-for-statement equality between the two enumerations.
  • BenefitScorer, ApplySeverityOverrides, ParsedPlan.AllMissingIndexes, the desktop grid/MCP registration, McpQueryStoreTools.CaptureSession, and Index.razor's ActiveStmtPlan now all walk the same PlanStatements.EnumerateAll/EnumerateAllWithContainer traversal as ResultMapper, so grid rows, MCP counts, and advice text can no longer disagree about which statements exist.
  • The PlanViewer.Web.csproj link addition for AnalysisJson.cs is correct and sufficient — its only dependencies are System.Text.Json and doc-comment crefs to PlanNode/OperatorResult, both already linked into that project, so no missing-include compile break.
  • The three depth-ceiling writers (PlanShareService.ShareAsync/LoadAsync, server/PlanShare/Program.cs) are now consistent with AnalysisJson.MaxDepth (1024) on both the JsonSerializerOptions and JsonDocumentOptions sides; the server intentionally mirrors the constant as a literal since it has no dependency on PlanViewer.Core, and a test (AnalysisJsonDepthTests) pins that constant as the tripwire for keeping them in sync.
  • Manually re-derived the new ProcedureBodyWaitStatsAreScoredAndSurfaced test's expected 40% benefit against BenefitScorer.ScoreWaitStats's serial/no-operator-profile branch (400ms/1000ms × 100) — the math and warning-type string ("Wait: " + WaitType) both check out.
  • server/PlanShare/Program.cs's only other JsonDocument.Parse call (the /api/event handler) legitimately keeps default MaxDepth, since it parses a small analytics payload, not a serialized AnalysisResult.

One non-issue worth flagging as an FYI rather than a defect: in McpQueryStoreTools.CaptureSession, HasActualStats changes from a hardcoded false to analysis.Summary.HasActualStats as a side effect of consolidating onto the analysis summary. That looks like it fixes a pre-existing wrong value (query-store sessions with actual stats previously always reported false to MCP clients) rather than introducing one, but it's outside what the PR description calls out, so worth a quick sanity check that it's intended.

@erikdarlingdata
erikdarlingdata merged commit c5626a4 into dev Sep 3, 2026
3 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/proc-descent-integration branch September 3, 2026 09:19
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