Skip to content

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

Merged
erikdarlingdata merged 2 commits into
devfrom
fix/3541-page-scoped-percents
Sep 18, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
fix/3541-page-scoped-percents

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

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 as pct_of_total_*, and emit the sum itself as total_*. At limit = 3 the 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 under total_exec_time_ms / total_wait_time_ms / total_samples / total_cpu_ms / total_reads as if it were the window's figure.

get_pg_top_queries compounded it: its reader carried LIMIT 50 as a literal (the A3 shape, under a limit the tool accepts up to 1,000) and the tool applied Take(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_stats in the same family had already refused this: its cache ratio is spelled cache_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 after GROUP BY / HAVING and before ORDER 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 (ViewerPostgresTabsTests maps every row property to a display column) and invite a reader to sum it.

Each tool then, in the A3 dialect:

  • fetches limit + 1 and observes truncation (var truncated = page.Rows.Count > limit;), publishes *_returned (queries_returned, wait_events_returned, waits_returned; get_pg_io_stats keeps combination_count, which the web I/O tile reads by key and which is a page count — the description says so and truncated sits beside it), truncated, and order;
  • publishes the window's figure under the existing total_* key — the name now true — and the page's own sum as returned_* (returned_exec_time_ms, returned_wait_time_ms, returned_samples, returned_cpu_ms, returned_reads / returned_read_time_ms);
  • states the headline once as returned_pct_of_total (I/O: one per denominator) — the honest form of "the top N account for X%";
  • keeps the per-row pct_of_total_* / pct_of_samples names, now dividing by the window total;
  • says in its description, in one sentence an agent will read: "SHARES ARE OF THE WINDOW, NOT OF THE PAGE: <share>'s denominator is <total>, the WHOLE window's figure … so a three-row page does not sum to 100%", names the returned_* key, and repeats the bound on the limit parameter. The note on every payload carries the same sentence.

*_of_returned remains 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 with PgMigrations, three series per table seeded at 600 / 300 / 100, every tool called through its real method at limit = 1 and limit = 3). Four came back 60 / 30 / 10. get_pg_kernel_stats came back 60 / 10 / 30.

DarlingPgKernelStatsReader ordered by 3 + 4 DESC, meant as "ordinal 3 plus ordinal 4" — user CPU plus system CPU. PostgreSQL reads a bare integer in ORDER BY as an output-column ordinal, but 3 + 4 is an expression, so it is the constant 7, and a constant sort key sorts nothing: EXPLAIN on 18 shows ORDER BY 1 + 2 DESC, x planning as Sort 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 a limit was 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 differenced layer where they get names, and the outer query sorts by user_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 no ORDER BY <int> <op> <int> survives in any of the five reads) and by execution (the live test seeds the hottest kernel series with the middle query_id and asserts it leads the limit = 1 page).

Why this shape, and what was rejected

  • Window aggregate over a second COUNT/SUM statement. 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_returned would 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.
  • Keeping the total_* key names rather than window_total_*. The name promised a total; it now delivers one. The per-row pct_of_total_time ↔ top-level total_exec_time_ms pairing 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 the note on every payload.
  • limit + 1 over-fetch for truncated rather than COUNT(*) 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.
  • Page record rather than a widened row record. See above — the Viewer's projection census would have demanded a display column for a fact that is not about a row.
  • Viewer keeps its unpaged reads. GetPgTopQueriesAsync / GetPgWaitStatsAsync / … keep their signatures and return .Rows of the page read; the top-queries one binds PgTopQueriesGridRowCap = 50, the literal it always carried, so the grid reads exactly what it read. Darling/PerformanceMonitor.Darling.Viewer is not touched.
  • get_pg_database_stats is not touched, deliberately. Its total_temp_files / total_temp_bytes / total_deadlocks are the same page-summed shape, but they sit beside limit_reached, a note that says the totals cover only the databases returned, a database_count the 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 in wwwroot, 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

  • Payload keys added on the five tools: 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_* and pct_of_total_* / pct_of_samples keep their names and change meaning from page to window — which is what the names said.
  • get_pg_top_queries now honours limit past 50 (was silently capped) and answers with a window denominator (was the top-50's).
  • get_pg_kernel_stats is ranked by CPU (was by (database_name, query_id)); the Viewer's kernel grid inherits this.
  • Reader API: five new Get*PageAsync methods and five page records; the existing list-returning methods keep their signatures. BuildTopQueriesJson / BuildIoJson take a page and a limit; three new Build*Json split-outs (BuildWaitStatsJson, BuildWaitSamplingJson, BuildKernelStatsJson) so the wire shape is assertable without a store, the precedent the statement and I/O tools set.
  • The PgIoSql select list grows by two columns (ordinals 19, 20 in the positional reader); Pg18IoBytesTests' count pin moves 19 → 21 with the reason.
  • Swept wwwroot/js: every consumer keys on the arrays or on names that survive. /api/read/* dispatch is unchanged.

What it does NOT do

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): at limit = 1 the row's share is 60 not 100, total_* is the window's 1,000, returned_* is 600, returned_pct_of_total is 60, truncated is true; at limit = 3 shares 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_timing off nulls the read-time totals and keeps the read counts; exactly-limit is not truncated and limit + 1 is.
  • McpPageContractTests (extended) — the A7 census: every percent tool's description carries the denominator sentence, the share key, the total key, the returned_* key and "does not sum to 100%", and its limit parameter says the shares stay of the whole window; every percent read carries an OVER () AS window_total_* column above a parameterised LIMIT and 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 by page.WindowTotal*, observes truncation off the page record over a limit + 1 fetch, and feeds no .Sum( local into a share or a total_* key; a negative sweep over every tool body on both SKUs that takes limit/top (82 bodies; population control ≥ 60) finds no share divided by a page sum and no page sum under a total_* 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 shipped totalTimeMs shape, the rounded wait form, the multi-line sampling form) and the fixed shape.
  • McpPageContractLivePostgresTests.PercentTools_ShareTheWindowNotThePage_AgainstDevPostgres (gated on DARLING_TEST_PG) — the SQL itself: five tables seeded (two snapshots where the read differences), every tool called at limit = 1 and limit = 3 through its real method, the same pair asserted, and on the three per-query reads the limit = 1 row's queryid asserted to be the hottest series — seeded with the middle id so id-order fails it.
  • Updated pins: DarlingMcpPgIoToolsTests (page-shaped BuildIoJson, totals as the rows' own sums so every existing assertion keeps its meaning), PgInt64IdentityWireShapeTests (page-shaped BuildTopQueriesJson), Pg18IoBytesTests (select-list count 19 → 21).

Verified here: all four projects build with 0 warnings (Darling.Service; Lite, Darling.Tests, Lite.Tests with EnableWindowsTargeting). Every changed statement was executed against a throwaway timescale/timescaledb:2.28.1-pg18 on Docker context desktop-linux through the real tool methods: window totals bind above LIMIT $4, shares come back 60 at limit = 1 and 60/30/10 at limit = 3 on 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 kernel limit = 1 page holds the hottest series after the sort fix (and held the coolest before it). The ORDER BY 3 + 4 finding was confirmed with EXPLAIN on the same server. The in-process assertions in DarlingMcpPgPercentDenominatorTests were 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 named get_pg_database_stats totals as the only total hits. Container torn down.

Which component(s) does this affect?

  • Lite
  • Darling
  • Lite Tests
  • Darling Tests
  • SQL collection scripts
  • Documentation
  • Full Dashboard (deprecated)
  • CLI Installer (deprecated)

Checklist

  • I have read the contributing guide
  • My code builds with zero warnings (dotnet build -c Debug)
  • I have tested my changes against at least one SQL Server version (store-side change on the PostgreSQL tool family; verified against PG18/Timescale 2.28.1 for the Darling store)
  • I have not introduced any hardcoded credentials or server names

…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.
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 18, 2026 16:55
@claude

claude Bot commented Sep 18, 2026

Copy link
Copy Markdown

Reviewed the diff in full: all 5 changed PostgreSQL readers (DarlingPgStatementReader, DarlingPgWaitReader, DarlingPgWaitSamplingReader, DarlingPgKernelStatsReader, DarlingPgIoReader) and all 5 changed MCP tool files, plus the new/extended test suites.

Checked specifically:

  • Column ordinal correctness: traced every reader.GetXxx(n) against its SQL SELECT list position in each reader — all match, including the newly appended window_total_* columns (e.g. DarlingPgIoReader ordinals 19/20, matching the Pg18IoBytesTests count bump from 19 → 21).
  • Window-aggregate SQL semantics: SUM(SUM(x)) OVER () nesting is valid Postgres (the outer is a window function over an already-grouped result, not a nested aggregate call), evaluated after GROUP BY/HAVING and before ORDER BY/LIMIT — so it correctly sums the whole window rather than the page.
  • The kernel-stats ranking fix: ORDER BY 3 + 4 DESC → constant-folds to ORDER BY 7, which Postgres drops as a no-op sort key. The fix (differenced CTE naming the CASE expressions, then ORDER BY user_ms + system_ms DESC) is correct and well-covered by both the in-process shape test and the live-Postgres test that seeds the hot series at a non-monotonic query_id.
  • Truncation/paging: limit + 1 fetch, page.Rows.Count > limit truncation check, rows[0] indexing in BuildIoJson — safe because McpHelpers.ValidateTop rejects limit <= 0 before any of this runs, so rows is never empty when indexed and limit + 1 can't overflow int (MaxTop = 1000).
  • Parameter binding: every reader binds via AddWithValue with DateTimeKind.Unspecified per the naive-UTC convention in CONTRIBUTING.md — no injection surface, no timezone-drift regression.
  • Lite/Darling parity: confirmed Lite/Mcp/ has no PostgreSQL-specific tool files, so the PR's claim that no Lite changes are needed holds — this is a Darling-only surface.
  • Web frontend: the total_reads / pct_of_total_* / combination_count keys wwwroot/js reads by name are unchanged, so the semantic shift (page-sum → window-sum) doesn't break existing tile bindings, per the PR description.

No correctness, security, or parity issues found.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — verified ordinal correctness across all 5 changed readers, window-aggregate SQL semantics, the kernel-stats ORDER BY fix, limit/truncation edge cases, parameter binding, and Lite/Darling parity. No substantive issues found.

…e of status: get_pg_io_stats carries status=io_activity on data (#3541 A7)

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@erikdarlingdata
erikdarlingdata merged commit 52788ae into dev Sep 18, 2026
8 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3541-page-scoped-percents branch September 18, 2026 17:16
erikdarlingdata added a commit that referenced this pull request Sep 18, 2026
…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.
erikdarlingdata added a commit that referenced this pull request Sep 18, 2026
…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.
erikdarlingdata added a commit that referenced this pull request Sep 18, 2026
…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.
erikdarlingdata added a commit that referenced this pull request Sep 18, 2026
…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)
This was referenced Sep 18, 2026
erikdarlingdata added a commit that referenced this pull request Sep 19, 2026
…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)
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