Repository navigation
MCP percents name their denominator: shares are of the window or say they are of the page, so a three-row page stops summing to 100% of everything (#3541 A7) - #3613
Conversation
…they are of the page, so a three-row page stops summing to 100% of everything (#3541 A7) Five PostgreSQL MCP tools - get_pg_top_queries, get_pg_wait_stats, get_pg_wait_sampling, get_pg_kernel_stats, get_pg_io_stats - divided every row by the sum of the rows they had fetched and published it as pct_of_total_*, so a limit=3 page summed to 100% of "total" by construction. get_pg_top_queries compounded it with a literal LIMIT 50 under a Take(limit). Each reader now carries the window's total on the SAME statement as its rows (SUM(...) OVER () over the grouped result, before LIMIT), returned on a page record; the tools fetch limit + 1, observe truncation, publish the window total as total_*, the page's own sum as returned_*, and the ratio once as returned_pct_of_total; every description says which denominator its shares use. The Viewer's unpaged reads keep their signatures and read exactly what they read. Found while executing the statements against live PostgreSQL 18: DarlingPgKernelStatsReader ordered by `3 + 4 DESC`, an expression the planner folds to a constant and drops, so "ranked by CPU" was never true and a limit page held the alphabetically-first series. Restructured to sort by the named differenced columns. Partial for #3541 (A7). Census extended in McpPageContractTests (both SKUs' paged tool bodies swept), live twin extended, new DarlingMcpPgPercentDenominatorTests pins the arithmetic per tool.
|
Reviewed the diff in full: all 5 changed PostgreSQL readers ( Checked specifically:
No correctness, security, or parity issues found. |
…e of status: get_pg_io_stats carries status=io_activity on data (#3541 A7)
There was a problem hiding this comment.
LGTM — reviewed the five reader/tool pairs (statement, wait, wait-sampling, kernel-stats, I/O): verified each SQL statement's window-total column (SUM(...) OVER ()) is computed post-GROUP BY/HAVING and pre-LIMIT so it can't drift from or be shrunk by the page cap, cross-checked every positional-reader ordinal against the actual SELECT list (including the IO reader's 19/20 and the kernel reader's 9), confirmed limit is bounded by McpHelpers.ValidateTop before the limit + 1 fetch (no overflow risk), and confirmed the sentinel row is excluded from every sentinel-adjacent computation (timing inference, reset flag) per the tests. The kernel ORDER BY 3 + 4 → constant-folds-to-7 finding is correct Postgres behavior and the fix (named columns via a differenced CTE, sorting by user_ms + system_ms) is sound. Lite/Darling parity is correctly scoped: Lite has no PostgreSQL tools, and the PR's own negative census sweeps tool bodies on both SKUs for the same anti-pattern. No SQL injection surface (all params bound), no secrets, no missing-index suggestions. Nothing substantive found.
There was a problem hiding this comment.
LGTM — reviewed the five reader/tool pairs (statement, wait, wait-sampling, kernel-stats, I/O): verified each SQL statement's window-total column (SUM(...) OVER ()) is computed post-GROUP BY/HAVING and pre-LIMIT so it can't drift from or be shrunk by the page cap, cross-checked every positional-reader ordinal against the actual SELECT list (including the IO reader's 19/20 and the kernel reader's 9), confirmed limit is bounded by McpHelpers.ValidateTop before the limit + 1 fetch (no overflow risk), and confirmed the sentinel row is excluded from every sentinel-adjacent computation (timing inference, reset flag) per the tests. The kernel ORDER BY 3 + 4 → constant-folds-to-7 finding is correct Postgres behavior and the fix (named columns via a differenced CTE, sorting by user_ms + system_ms) is sound. Lite/Darling parity is correctly scoped: Lite has no PostgreSQL tools, and the PR's own negative census sweeps tool bodies on both SKUs for the same anti-pattern. No SQL injection surface (all params bound), no secrets, no missing-index suggestions. Nothing substantive found.
…ors, connections and lock waits land in pg_log_events off the same tail and the same RDS API the deadlock and plan readers use, redacted with the plan parser's own patterns (#3601) The server log is the engine's primary event record and Darling read exactly two shapes out of it. This makes the shared part of those two readers explicit — PgServerLogTail is the one spelling of the pg_read_file tailer both siblings now splice byte-for-byte, PgLogEntryAssembler is the prefix/zone/ companion-line assembly done once — and adds the classifier on top: IPgLogFamilyParser is the seam, three families ship with parsers (error, connection, lock_wait), three are recognised and stored under their own name for #3602/#3603 to structure (temp_file, autovacuum, checkpoint). One cursor, every parser on every line, one identity per entry (raw_line_hash) for the overlapping tail to dedupe on. V129 creates collect.pg_log_events exactly as the generator emits it (one index; the family index is a separate rung for V104's reason). Both transports: PgLogEventsCollector (self-hosted; the body crosses the wire so no family's recogniser is spelled twice) and RdsLogEventIngestor (its own RdsLogSource, marker committed after the write). get_pg_log_events reads it with the #3594/#3613 page contract. Retention 30 d, operator-tunable. Censuses moved: 28 PG collectors, 70 collector tables, 71 hypertables (workflows 73/84), 34 get_pg_* tools, 152 tools.
…ors, connections and lock waits land in pg_log_events off the same tail and the same RDS API the deadlock and plan readers use, redacted with the plan parser's own patterns (#3601) The server log is the engine's primary event record and Darling read exactly two shapes out of it. This makes the shared part of those two readers explicit — PgServerLogTail is the one spelling of the pg_read_file tailer both siblings now splice byte-for-byte, PgLogEntryAssembler is the prefix/zone/ companion-line assembly done once — and adds the classifier on top: IPgLogFamilyParser is the seam, three families ship with parsers (error, connection, lock_wait), three are recognised and stored under their own name for #3602/#3603 to structure (temp_file, autovacuum, checkpoint). One cursor, every parser on every line, one identity per entry (raw_line_hash) for the overlapping tail to dedupe on. V129 creates collect.pg_log_events exactly as the generator emits it (one index; the family index is a separate rung for V104's reason). Both transports: PgLogEventsCollector (self-hosted; the body crosses the wire so no family's recogniser is spelled twice) and RdsLogEventIngestor (its own RdsLogSource, marker committed after the write). get_pg_log_events reads it with the #3594/#3613 page contract. Retention 30 d, operator-tunable. Censuses moved: 28 PG collectors, 70 collector tables, 71 hypertables (workflows 73/84), 34 get_pg_* tools, 152 tools.
…ors, connections and lock waits land in pg_log_events off the same tail and the same RDS API the deadlock and plan readers use, redacted with the plan parser's own patterns (#3601) The server log is the engine's primary event record and Darling read exactly two shapes out of it. This makes the shared part of those two readers explicit — PgServerLogTail is the one spelling of the pg_read_file tailer both siblings now splice byte-for-byte, PgLogEntryAssembler is the prefix/zone/ companion-line assembly done once — and adds the classifier on top: IPgLogFamilyParser is the seam, three families ship with parsers (error, connection, lock_wait), three are recognised and stored under their own name for #3602/#3603 to structure (temp_file, autovacuum, checkpoint). One cursor, every parser on every line, one identity per entry (raw_line_hash) for the overlapping tail to dedupe on. V129 creates collect.pg_log_events exactly as the generator emits it (one index; the family index is a separate rung for V104's reason). Both transports: PgLogEventsCollector (self-hosted; the body crosses the wire so no family's recogniser is spelled twice) and RdsLogEventIngestor (its own RdsLogSource, marker committed after the write). get_pg_log_events reads it with the #3594/#3613 page contract. Retention 30 d, operator-tunable. Censuses moved: 28 PG collectors, 70 collector tables, 71 hypertables (workflows 73/84), 34 get_pg_* tools, 152 tools.
…ors, connections and lock waits land in pg_log_events off the same tail and RDS API the deadlock and plan readers use, redacted with the plan parser's own patterns (#3601) (#3646) * PostgreSQL's log is read by a classifier now, not by two regexes: errors, connections and lock waits land in pg_log_events off the same tail and the same RDS API the deadlock and plan readers use, redacted with the plan parser's own patterns (#3601) The server log is the engine's primary event record and Darling read exactly two shapes out of it. This makes the shared part of those two readers explicit — PgServerLogTail is the one spelling of the pg_read_file tailer both siblings now splice byte-for-byte, PgLogEntryAssembler is the prefix/zone/ companion-line assembly done once — and adds the classifier on top: IPgLogFamilyParser is the seam, three families ship with parsers (error, connection, lock_wait), three are recognised and stored under their own name for #3602/#3603 to structure (temp_file, autovacuum, checkpoint). One cursor, every parser on every line, one identity per entry (raw_line_hash) for the overlapping tail to dedupe on. V129 creates collect.pg_log_events exactly as the generator emits it (one index; the family index is a separate rung for V104's reason). Both transports: PgLogEventsCollector (self-hosted; the body crosses the wire so no family's recogniser is spelled twice) and RdsLogEventIngestor (its own RdsLogSource, marker committed after the write). get_pg_log_events reads it with the #3594/#3613 page contract. Retention 30 d, operator-tunable. Censuses moved: 28 PG collectors, 70 collector tables, 71 hypertables (workflows 73/84), 34 get_pg_* tools, 152 tools. * Review + census: the key-tuple redaction runs to the tuple's true close (a value carrying ')' or ')=(' no longer leaks past it), the exclusion shape keeps both key names; the new COPY writer joins the phase/deadline rosters, the reader sets the MCP-read deadline, the README's derived counts move to 28/70/71 (#3601) * Rebase over #3607: the get_pg_* census is 35 with get_pg_logging_audit beside get_pg_log_events (instructions 153/67/thirty-five, runbook 35) * PgLogEventsLivePostgresTests joins the live-postgres collection: it writes the shared store, so it serializes against the other live classes (hygiene pin) * Both live tests tear down through LiveStoreCleanup on their own connection (#1902 ratchet), not on the body's * PgLogFamilies.IsKnown refuses the reserved 'other' word the read's error message already excludes, so family=other is a rejection rather than a silent zero (review note) * Root README edition table and llms.txt say 153 Darling tools (the cross-app inventory pins read them) * Double quotes are not a safe signal: a double-quoted run is kept only after an identifier noun, the ': "…"' / 'at or near "…"' value shapes and 'Failing row contains (…)' go whole, and the pins use the shapes PostgreSQL actually writes (review) * CONTEXT is stored, redacted like detail (V129 gains the column the lock-wait doc already promised), and a double-quoted run after bare whitespace or a newline is evaluated by the allowlist rather than skipped (review)
…le, Opus full tier (#3710) (#3718) * REVIEW-TRAPS.md: the repository's recurring defect shapes, one line each with the PR that bit (#3710) Seven layers (SQL, collectors and deltas, analysis, alerting and notifications, storage and MCP payloads, tests and censuses, workflows and CI, repository mechanics), fifty-odd lines, every one citing the pull request or issue that verified it: the >= limit truncation inference (#3594/#3679/#3699), ORDER BY 3 + 4 folding to a constant (#3613), MAX(max_dop) across plans (#3648), integer division in cntr_value_per_second (#3653 Q9), per-collection alert firing against a shorter cooldown (#3628/#3636), restart zeros in deltas and baselines (#3595/#3698/#3705/#3708), ScoreAll skipping amplifiers on a zero base and the non-virtual GetActiveEdges (#3688), the PgTargetFactKeys censuses (#3688), pg_total_relation_size vs the heap and the two TimescaleDB views that hide materializations and history (#3610/#3585), the -infinity crash backoff (#3629), the Slack block budget and the surrogate-pair cut (#3612/#3618/#3625/#3644), LCK_M_SCH_M folding into LCK so no SCH_M fact exists (#3709), one wait profile per run (#3709), and the CRLF round-trip. Each citation was read before it was written down; a wrong citation is worse than none. The review workflow reads this file before the PR body and must say, per trap the diff is near, whether it applies here and why. * Claude review: blind read before the body, callers rule, structured verdict with a ledger, and an Opus/Sonnet tier by changed path (#3710) The prompt runs in two phases. Phase 1 forbids gh pr view / git log / git show until the reviewer has read the diff, every changed region in context, the callers of every changed public symbol (Grep, both SKUs and both test projects) and .github/REVIEW-TRAPS.md, and written its own account. Phase 2 reads the body and grades every claim VERIFIED / UNVERIFIED / REFUTED, then reports the DIVERGENCE between the two accounts. The verdict is a fixed block -- Claims, Riskiest lines with the pinning test or UNPINNED, Traps near this diff, Divergence, Reviewer checklist -- ending in one machine-readable ledger line, posted through gh pr review --body-file - with a quoted heredoc (no Write tool; the allowlist is unchanged and still read-only). The #3650 verdict check now fetches the newest verdict in the window by id and fails the job when the body lacks the "## Verdict:" header or the ledger, or when the ledger's verdict disagrees with the review's API state (a "CHANGES REQUESTED" body submitted with --comment is the shape the guard would wave through). The guard's own arm is untouched and still keys on the state. A dorny/paths-filter@v4 step (the repo's pin style) routes changes under the shared libraries, Darling Storage/Analysis/Service, Lite Services/Mcp/Database, install/ and every .sql file to the full tier -- claude-opus-5[1m], 60 turns -- and everything else to claude-sonnet-5 at 30 turns; one decision step emits tier/model/max_turns, one review step consumes them (two steps would double the "review posted" accounting the guard does), and the prompt carries the tier and model into the ledger. A filter that dies degrades to FULL, not light. Step name "Claude review" and the ALWAYS POST sentinel are unchanged: both are read by claude-review-guard.yml. * Review tier: Alerting and Notifications read at the full tier (#3710)
MCP percents name their denominator, so a three-row page stops summing to 100% of everything
Partial for #3541 — checklist item A7 "Page-scoped percents/totals". Does not close the issue.
Five PostgreSQL MCP tools —
get_pg_top_queries,get_pg_wait_stats,get_pg_wait_sampling,get_pg_kernel_stats,get_pg_io_stats— published a share of the page under a name that promised a share of the window. Each did the same thing: fetch the rows,rows.Sum(...), divide every row by that, emit it aspct_of_total_*, and emit the sum itself astotal_*. Atlimit = 3the three shares summed to 100% by construction, and a reasoning agent holding only the JSON read "these three query shapes are 100% of execution time" off a server running four hundred. The same page sum sat undertotal_exec_time_ms/total_wait_time_ms/total_samples/total_cpu_ms/total_readsas if it were the window's figure.get_pg_top_queriescompounded it: its reader carriedLIMIT 50as a literal (the A3 shape, under alimitthe tool accepts up to 1,000) and the tool appliedTake(limit)on top, so its "total" was the top-50's sum whatever the caller asked for — a denominator that was neither the page nor the window.get_pg_database_statsin the same family had already refused this: its cache ratio is spelledcache_hit_pct_of_returned, with a comment forbidding a name that claims more than the number structurally has. That comment is the house rule this PR applies across the rest of the family — choosing the other honest arm, because here the true denominator is free.The one rule
A share's denominator is the window's figure, computed on the same statement as the rows. Every touched reader gains a window aggregate over its grouped result —
SUM(SUM(delta_total_exec_time_ms)) OVER (),SUM(SUM(delta_wait_time_us)) OVER () / 1000.0,SUM(<the differencing CASE>) OVER (),SUM(user_ms + system_ms) OVER (),SUM(coalesce(SUM(d_reads), 0)) OVER ()— which PostgreSQL evaluates afterGROUP BY/HAVINGand beforeORDER BY/LIMIT. So it sums every series the window holds, not the rows the cap admits; it cannot drift from the rows because it is the same statement; and it costs one pass over a result the query has already grouped and is about to sort. No second read, no unbounded scan: the aggregate runs on exactly the bounded window the read already walks.The reader returns a page record (
PgTopQueriesPage,PgWaitStatsPage,PgWaitSamplingPage,PgKernelStatsPage,PgIoPage) — the rows plus the window total(s) — rather than putting the total on each row: it is a fact about the window, a per-row copy would widen the Viewer's projection (ViewerPostgresTabsTestsmaps every row property to a display column) and invite a reader to sum it.Each tool then, in the A3 dialect:
limit + 1and observes truncation (var truncated = page.Rows.Count > limit;), publishes*_returned(queries_returned,wait_events_returned,waits_returned;get_pg_io_statskeepscombination_count, which the web I/O tile reads by key and which is a page count — the description says so andtruncatedsits beside it),truncated, andorder;total_*key — the name now true — and the page's own sum asreturned_*(returned_exec_time_ms,returned_wait_time_ms,returned_samples,returned_cpu_ms,returned_reads/returned_read_time_ms);returned_pct_of_total(I/O: one per denominator) — the honest form of "the top N account for X%";pct_of_total_*/pct_of_samplesnames, now dividing by the window total;<share>'s denominator is<total>, the WHOLE window's figure … so a three-row page does not sum to 100%", names thereturned_*key, and repeats the bound on thelimitparameter. Thenoteon every payload carries the same sentence.*_of_returnedremains the one other spelling, for a share that genuinely is of the page and says so.What executing the SQL found
The Darling statements were run end-to-end against a throwaway
timescale/timescaledb:2.28.1-pg18(migrated withPgMigrations, three series per table seeded at 600 / 300 / 100, every tool called through its real method atlimit = 1andlimit = 3). Four came back 60 / 30 / 10.get_pg_kernel_statscame back 60 / 10 / 30.DarlingPgKernelStatsReaderordered by3 + 4 DESC, meant as "ordinal 3 plus ordinal 4" — user CPU plus system CPU. PostgreSQL reads a bare integer inORDER BYas an output-column ordinal, but3 + 4is an expression, so it is the constant 7, and a constant sort key sorts nothing:EXPLAINon 18 showsORDER BY 1 + 2 DESC, xplanning asSort Key: x. The rows came back in the tiebreak's order,(database_name, query_id). So "Rows are ranked by total CPU" — in the tool description, the reader's class doc, and the web grid's premise — was never true, and every page this read served under alimitwas the alphabetically-first series rather than the hottest. Roughly a dozen string assertions on this SQL passed throughout; a statement that runs without error and returns plausible rows is invisible to them.Fixed in the same reader (the file this lane owns and whose page-contract sentence — "the rows are the heaviest, so the ones past the cap burned less" — depends on it): the differencing CASEs move into a
differencedlayer where they get names, and the outer query sorts byuser_ms + system_ms DESC. That also lets the window aggregate reference them by name instead of repeating both CASEs. The Viewer's kernel grid, which runs the same SQL, is ranked by CPU for the first time. Pinned by shape (TheKernelRead_RanksByCpu_NotByAConstant: the sort key names the columns, and noORDER BY <int> <op> <int>survives in any of the five reads) and by execution (the live test seeds the hottest kernel series with the middlequery_idand asserts it leads thelimit = 1page).Why this shape, and what was rejected
COUNT/SUMstatement. A second statement is a second scan of the same bounded window and a value that can drift from the rows (a collection landing between the two).OVER ()on the grouped result is one pass and cannot drift. Where the true total is this cheap, renaming to_of_returnedwould have been the worse arm of the house rule — it keeps a number nobody wants (the page's sum, as a share of itself) and drops the one everybody does.total_*key names rather thanwindow_total_*. The name promised a total; it now delivers one. The per-rowpct_of_total_time↔ top-leveltotal_exec_time_mspairing is the one an agent already reads, and the web tiles (PG_IO_SUMMARY_STATS, the PG grids'% of …columns) key on these names — under the old code a tile labelled "Reads" showed the top-20's sum; it now shows the window's. The disclosure lives in the description and thenoteon every payload.limit + 1over-fetch fortruncatedrather thanCOUNT(*) OVER (). The census dialect A3 established is the sentinel row; one dialect, not two. The window total is a different fact (a sum, not a count) and travels separately.GetPgTopQueriesAsync/GetPgWaitStatsAsync/ … keep their signatures and return.Rowsof the page read; the top-queries one bindsPgTopQueriesGridRowCap = 50, the literal it always carried, so the grid reads exactly what it read.Darling/PerformanceMonitor.Darling.Vieweris not touched.get_pg_database_statsis not touched, deliberately. Itstotal_temp_files/total_temp_bytes/total_deadlocksare the same page-summed shape, but they sit besidelimit_reached, a note that says the totals cover only the databases returned, adatabase_countthe web tile labels "Databases returned", and a share already spelled_of_returned— honest by disclosure. Moving it to the window idiom means relabelling those tile figures inwwwroot, outside this lane's boundary. The new cross-SKU census names those three keys as its one stated allowance, with a control that fails the day the allowance is no longer used, so it is carried loudly rather than silently. Reported to the coordinator.Blast radius
queries_returned/wait_events_returned/waits_returned,truncated,order,returned_exec_time_ms/returned_wait_time_ms/returned_samples/returned_cpu_ms/returned_reads/returned_read_time_ms,returned_pct_of_total(I/O:returned_pct_of_total_reads/returned_pct_of_total_read_time). None removed, none renamed.total_*andpct_of_total_*/pct_of_sampleskeep their names and change meaning from page to window — which is what the names said.get_pg_top_queriesnow honourslimitpast 50 (was silently capped) and answers with a window denominator (was the top-50's).get_pg_kernel_statsis ranked by CPU (was by(database_name, query_id)); the Viewer's kernel grid inherits this.Get*PageAsyncmethods and five page records; the existing list-returning methods keep their signatures.BuildTopQueriesJson/BuildIoJsontake a page and alimit; three newBuild*Jsonsplit-outs (BuildWaitStatsJson,BuildWaitSamplingJson,BuildKernelStatsJson) so the wire shape is assertable without a store, the precedent the statement and I/O tools set.PgIoSqlselect list grows by two columns (ordinals 19, 20 in the positional reader);Pg18IoBytesTests' count pin moves 19 → 21 with the reason.wwwroot/js: every consumer keys on the arrays or on names that survive./api/read/*dispatch is unchanged.What it does NOT do
DarlingTrendReader/DarlingMcpTrendTools, 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's files) — theexecution_count-as-rate and barevalueitems in the consistency table sit there.get_pg_database_stats(see above).get_top_queries_by_cpu's attribution already divides the page's CPU by the measured box CPU, Report unattributed CPU on the top-queries reads — ranked-query CPU vs the box's measured CPU-seconds #2320).deprecated/Dashboardchange.Test plan
New, compile-verified here (the test projects are
net10.0-windows), first executed in CI:Darling/Darling.Tests/DarlingMcpPgPercentDenominatorTests.cs— the arithmetic through each of the five projections with no store, every case a pair (cut page vs whole page over one 600/300/100 window, because the whole page is where old and new arithmetic agree): atlimit = 1the row's share is 60 not 100,total_*is the window's 1,000,returned_*is 600,returned_pct_of_totalis 60,truncatedis true; atlimit = 3shares are 60/30/10 and page sum equals total. Plus: the projection divides by the page's window total and never by a sum of the rows (a page handed a total larger than its rows reports 25, not 100); the sentinel row decides nothing the page reports (a reset or a non-zero time only on the over-fetched row);track_io_timingoff nulls the read-time totals and keeps the read counts; exactly-limitis not truncated andlimit + 1is.McpPageContractTests(extended) — the A7 census: every percent tool's description carries the denominator sentence, the share key, the total key, thereturned_*key and "does not sum to 100%", and itslimitparameter says the shares stay of the whole window; every percent read carries anOVER () AS window_total_*column above a parameterisedLIMITand no literal cap; the kernel read sorts by the named columns and no read orders by an integer expression; the sampling read's two copies of the differencing CASE are identical modulo whitespace; every percent tool body divides bypage.WindowTotal*, observes truncation off the page record over alimit + 1fetch, and feeds no.Sum(local into a share or atotal_*key; a negative sweep over every tool body on both SKUs that takeslimit/top(82 bodies; population control ≥ 60) finds no share divided by a page sum and no page sum under atotal_*name outside the one stated allowance, with a control that the allowance set equals the set actually used; discriminators witnessed against the defect verbatim (the shippedtotalTimeMsshape, the rounded wait form, the multi-line sampling form) and the fixed shape.McpPageContractLivePostgresTests.PercentTools_ShareTheWindowNotThePage_AgainstDevPostgres(gated onDARLING_TEST_PG) — the SQL itself: five tables seeded (two snapshots where the read differences), every tool called atlimit = 1andlimit = 3through its real method, the same pair asserted, and on the three per-query reads thelimit = 1row'squeryidasserted to be the hottest series — seeded with the middle id so id-order fails it.DarlingMcpPgIoToolsTests(page-shapedBuildIoJson, totals as the rows' own sums so every existing assertion keeps its meaning),PgInt64IdentityWireShapeTests(page-shapedBuildTopQueriesJson),Pg18IoBytesTests(select-list count 19 → 21).Verified here: all four projects build with 0 warnings (
Darling.Service;Lite,Darling.Tests,Lite.TestswithEnableWindowsTargeting). Every changed statement was executed against a throwawaytimescale/timescaledb:2.28.1-pg18on Docker contextdesktop-linuxthrough the real tool methods: window totals bind aboveLIMIT $4, shares come back 60 atlimit = 1and 60/30/10 atlimit = 3on all five, truncation flips at the boundary, the empty paths still answer (OVER ()over zero rows), the Viewer's unpaged read still returns its 50, and the kernellimit = 1page holds the hottest series after the sort fix (and held the coolest before it). TheORDER BY 3 + 4finding was confirmed withEXPLAINon the same server. The in-process assertions inDarlingMcpPgPercentDenominatorTestswere run through the built assemblies by reflection with the exact fixtures the tests use (60 / 25 / null / 900 all as asserted). The census regexes were emulated against the actual sources on both SKUs: 82 paged bodies, zero share hits, the three namedget_pg_database_statstotals as the only total hits. Container torn down.Which component(s) does this affect?
Checklist
dotnet build -c Debug)