Skip to content

MCP pages now say what bounded them: caps bind to the caller's limit, truncation is detected not inferred, and no page count is called a total (#3541 A3) - #3594

Merged
erikdarlingdata merged 2 commits into
devfrom
fix/3541-hidden-caps-honest-pages
Sep 18, 2026
Merged

erikdarlingdata merged 2 commits into
devfrom
fix/3541-hidden-caps-honest-pages

Conversation

@erikdarlingdata

Copy link
Copy Markdown
Owner

Six MCP pages now say what bounded them, so a limited page stops passing for a complete answer

Partial for #3541 — checklist item A3 "Hidden caps published as window totals". Does not close the issue.

Six tool groups on both SKUs carried a hidden reader cap: LIMIT 200 / 50 / 500 (or no bound at all) inside the SQL, under a tool that advertised limit, applied it with Take(limit) on top, and then published the capped row count under a total_* name. A reasoning agent holding only the JSON read a 200-row page of a 5,000-event window as the window. The "widen hours_back" advice in the empty branches could never help, because the cap was on rows, not time. #3287 fixed exactly this shape on get_collection_log and established the pattern; this PR applies that one pattern, as one dialect, to every tool the finding named:

tool group the hidden cap what else was wrong
get_blocking (Lite: get_blocked_process_reports) 200, both arms + merge #2159's "fingerprint runs over the whole window before limit" was quietly hollow past the newest 200 rows
get_deadlocks / get_deadlock_detail 50 detail filtered for a graph in C# after the capped fetch, so a run of graph-less rows at the newest end read as "no XML in the window"
get_alert_history caller's limit, but truncation unobservable a hidden dismissed = FALSE filter — acknowledged criticals vanished, unstated anywhere
get_long_query_completions 200 Darling kept the SLOWEST 200, Lite kept the NEWEST 200 then re-ranked — Lite could omit the window's slowest run entirely, under the same tool name
get_plan_corrections 200 newest per-cycle re-captures make 200 rows ≈ 16 hours whatever hours_back said
get_waiting_tasks / get_wait_stats 500 / unbounded / 50 under a limit up to 1,000 get_waiting_tasks shipped a bare envelope: server and rows, no window, no count, no bound

The one pattern

  1. The cap is the caller's limit, bound as a SQL parameter (LIMIT $4), never a literal. Every touched reader const is pinned to end in a parameterised LIMIT.
  2. Truncation is observed, not inferred. The tool fetches limit + 1 and sets truncated only when the extra row came back. count >= limit cannot tell a window holding exactly limit rows from a busier one, and every test here asserts the pair — limit = N-1 truncated, limit = N not — over N seeded rows.
  3. The page publishes its own bounds, under the names get_collection_log established: oldest_returned_<stamp> / newest_returned_<stamp>, plus order naming the ordering. Under newest-first the oldest stamp is the reach of the read; under get_long_query_completions' duration ranking the stamps bound the slowest runs and say nothing about reach — the description says which case each tool is.
  4. No page count is called total_*. total_events / total_deadlocks / total_alerts / total_completions / total_recommendations → *_returned.
  5. A filter that shapes the population is stated and measured. get_alert_history publishes dismissed_excluded and dismissed_excluded_count (a real COUNT(*) of what the default filter removed in the window), gains include_dismissed (appended after as_of, default false = the grid's own read), labels each row dismissed, and an all-dismissed window now says "N dismissed alert(s) were excluded — re-run with include_dismissed" instead of calling itself quiet.
  6. Where a dedupe ran over a page, it now runs over the window — bounded by a stated ceiling. get_blocking / get_deadlocks fetch up to FingerprintScanCeiling (2,000; lineage in the const's comment: a scan row carries the graph or both SQL texts, ten times the old cap, half of what the analysis pair-row readers fetch without XML) when a dedup_key is supplied, publish rows_examined / scan_truncated, and a no-match answer on a scan that ran out appends that fact and the remedy (anchor as_of at the alert time). Without a key the scan is the page plus its sentinel row.
  7. One truth for get_long_query_completions: a completions tool sorted by duration keeps the window's SLOWEST. Lite gained a duration-ranked SQL read (GetSlowestLongQueryCompletionsAsync, ORDER BY duration_microseconds DESC NULLS LAST) for the tool; the grid keeps its chronological read. Both descriptions now say "the window's limit SLOWEST, not its newest".
  8. Every touched description says what bounds the page — the word truncated appears in the tool description and in the limit parameter's description, on both SKUs, and the Instructions tables carry the same sentence.

get_deadlock_detail and get_blocked_process_xml moved their graph/XML predicate into the SQL (RecentDeadlocksWithGraphSql, BlockedProcessReportsWithXmlSql on Darling; graphOnly / xmlOnly on Lite's readers), so limit counts graphs rather than rows the tool would have discarded, and the XML tool reads the XE arm alone (a DMV snapshot never carries a report).

Why this shape, and what was rejected

  • Over-fetch by one rather than a second COUNT(*) for the row-level tools: a true windowed count is a second scan per call on the store's largest edge tables for a fact the caller only needs as a boolean. The count is run for the alert-history filter, because "2 dismissed rows were hidden" is the disclosure that makes the filter honest and it is a cheap indexed count. get_active_queries' unbounded total_snapshots beside shown was left alone — that is a real total, and its filter semantics are A13's item.
  • Fingerprint scan ceiling rather than unbounded — the scan carries every row's XML. 2,000 rather than 5,000 for that reason, stated in the const and in the payload when it bites.
  • The merged blocking read fetches the DMV arm at cap + XE rows in hand, because the merge drops one DMV row per (pair, minute) an XE row already covers and the DMV collector samples once per cycle; fetched at the bare cap, a surplus made of XE-covered rows would vanish in the merge and a full page would read as complete. Both SKUs, same reason, in the reader remarks.
  • include_dismissed rather than dropping the filter: dismissal is a Viewer operator's acknowledgement ("hide it from the grid"), which is the right default for a person at the grid and says nothing about whether the alert fired. The default stays the grid's; the payload now says so and measures it. On Lite, an alert dismissed after aging into the parquet archive is removed by the archive view itself and can be neither returned nor counted — the description says so.
  • Grid readers keep their caps. Lite's shared LocalDataService reads gained a trailing limit (defaulting to the grid's 50 / 200) so every WPF caller reads exactly what it always read; Darling's readers take cap and the Viewer has its own SQL.
  • No shared McpHelpers page helper. The one-dialect guarantee is held by census tests instead: PerformanceMonitor.Common/Mcp/McpHelpers.cs is outside this lane's file boundary. Reported to the coordinator as the natural follow-up if the contract item wants a helper.

Blast radius

  • total_events / total_deadlocks / total_alerts / total_completions / total_recommendations are gone from these six payloads (renamed). Swept the web viewer (wwwroot/js) and Lite: every consumer keys on the array (events, deadlocks, alerts, …), none on the totals. /api/read/* dispatch passes as_of by name, so the appended include_dismissed is invisible to it (it cannot yet be asked for; noted below).
  • DarlingAlertReader.GetAlertHistoryAsync keeps its signature (the triage endpoint calls it); the tool reads through the new GetAlertHistoryPageAsync. AlertHistoryReadRow / Lite AlertHistoryRow gain Dismissed.
  • The Lite grid's merged blocking read now lets up to 200 + XE rows DMV candidates into the merge before re-capping to 200 — strictly closer to "the newest 200 merged" than before.
  • Darling get_wait_stats: only the LIMIT 50 → $4 binding plus wait_types_returned / truncated (the twin port of Lite's named defect). The A7 page-scoped-percent item is untouched.
  • deprecated/Dashboard/Mcp carries the same total_events / total_alerts page counts, but its caps live in its own SQL-Server-side readers outside this lane's boundary; not touched, reported.

What it does NOT do

Test plan

New, compile-verified here (the test projects are net10.0-windows), first executed in CI:

  • Lite.Tests/McpPageContractTests.cs — runs the real tools against a real DuckDB: the boundary pair for every tool (limit = N-1 truncated / limit = N not), page bounds equal the seeded stamps, get_deadlock_detail / get_blocked_process_xml count graphs not rows, the merged blocking arms stay observable, get_alert_history states/measures/lifts the dismissed filter and its all-dismissed empty branch names it, get_long_query_completions returns the window's slowest (seeded as the OLDEST) at limit = 1 and is duration-ordered, get_waiting_tasks carries hours_back, get_wait_stats binds the cap; AssertPage refuses any total_* key; reflection pins truncated in every touched description and include_dismissed as an appended optional.
  • Darling/Darling.Tests/McpPageContractTests.cs — the census: every paged Darling read binds LIMIT $N and no literal; every paged tool BODY on both SKUs has no total_* = x.Count, no >= limit inference, and a Count > limit over a limit + 1 fetch; the same tool name emits the same page-contract keys on both SKUs (the naming-drift pair compared on bounds/order); descriptions on both SKUs say truncated/limit; the fingerprint readers name rows_examined / scan_truncated; discriminators witnessed against defect-shaped and fixed-shaped literals. Plus McpPageContractLivePostgresTests (gated on DARLING_TEST_PG) running the same boundary pairs against live Postgres.
  • Updated pins: DarlingMcpBlockingToolsTests (LIMIT $4, the two with-XML/graph consts share a body, scan ceiling bounds, live read asserts events_returned), DarlingMcpAlertToolsTests ((dismissed = FALSE OR $5), the count consts unbounded, include_dismissed contract, live read asserts the new fields), DarlingMcpSessionToolsTests / DarlingMcpDataToolsTests / DarlingMcpPlanCorrectionToolsTests (LIMIT $4, literal gone), PlanCorrectionFrameLiveTests (new cap arg).

Verified here: all four projects build with 0 warnings (Darling.Service, Lite with EnableWindowsTargeting, both test projects). The Darling SQL was exercised end-to-end against a throwaway timescale/timescaledb:2.28.1-pg18 container (migrated with PgMigrations, rows planted, every touched tool called through its real method): LIMIT $4 binds, the (dismissed = FALSE OR $5) bool parameter works on both scopes, the with-XML/graph consts page correctly, truncation flips exactly at the boundary, the dedup-key path reports rows_examined, the slowest-first page holds the window's slowest (the oldest row), and the all-dismissed window hits the new empty branch. Container torn down. The Darling census regexes were emulated against the actual sources and pass.

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; verified against PG18/Timescale for the Darling store and via the DuckDB-backed Lite tests)
  • I have not introduced any hardcoded credentials or server names

…ncation is observed, no page count is a total (#3541 A3)

Six MCP tool groups on both SKUs carried a HIDDEN reader cap -- LIMIT 200 / 50 /
500 under a tool that advertised `limit`, a Take(limit) on top, and the capped
count published under a total_* name. An agent that cannot see the code read a
200-row page of a 5,000-event window as the window, and the "widen hours_back"
advice could never help because the cap was on rows, not time.

get_blocking / get_blocked_process_reports (200), get_deadlocks (50),
get_alert_history (+ a hidden dismissed = FALSE filter), get_long_query_completions
(200; Darling kept the SLOWEST, Lite kept the NEWEST then re-ranked, so Lite could
omit the window's slowest run), get_plan_corrections (200 newest per-cycle
re-captures ~ 16h reach regardless of hours_back), get_waiting_tasks (500 / unbounded,
bare envelope) and get_wait_stats (50 under a limit up to 1,000).

ONE pattern, the one #3287 established for get_collection_log: the cap is the
caller's limit bound as a SQL parameter; the tool fetches limit + 1 and reads the
extra row as `truncated` rather than inferring it from count >= limit; the page
publishes oldest_returned_* / newest_returned_* and names its `order`; the page
count is *_returned, never total_*. get_deadlock_detail and get_blocked_process_xml
moved their graph/XML predicate into the SQL so limit counts graphs, not rows they
would have discarded.

get_blocking / get_deadlocks honour #2159's whole-window promise for dedup_key:
the fingerprint scan is bounded by a stated 2,000-row ceiling, the payload carries
rows_examined / scan_truncated, and a no-match answer on a scan that ran out says so.

get_alert_history states and MEASURES its filter: dismissed_excluded and
dismissed_excluded_count on every page, include_dismissed to lift it (each row then
labelled `dismissed`), and an all-dismissed window names the filter instead of
calling itself quiet. Lite's completions read is now duration-ranked in SQL, so
both SKUs serve the same population.

Descriptions on every touched tool say what bounds the page. Grids keep their caps
(the readers default to them). Census tests pin the dialect across both SKUs;
Lite's run against a real DuckDB, Darling's live half is gated on DARLING_TEST_PG.
@erikdarlingdata
erikdarlingdata enabled auto-merge (squash) September 18, 2026 15:36
…lare the new LF-reading pin

Two census pins from the first CI run. AsOfWindowAnchorTests requires every LocalDataService call that
CAN take asOfUtc to be given it by name, so the anchor's presence is visible to the scan rather than
inferred from position; the new GetSlowestLongQueryCompletionsAsync call passed it positionally.
RepoFileAdoptionTests holds the exact set of pins that read LF-normalised source; McpPageContractTests
is one (its Lite-description anchor spans the line break on get_plan_corrections), so it is declared.

@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 core implementation across both SKUs (Darling: DarlingBlockingReader/McpBlockingTools, DarlingAlertReader/McpAlertTools incl. the include_dismissed/dismissed-count logic, DarlingLongQueryReader/McpLongQueryTools, DarlingSessionReader/McpSessionTools, DarlingDataReader/McpDataTools, DarlingPlanCorrectionReader/McpPlanCorrectionTools; Lite: the corresponding LocalDataService..cs and McpTools.cs files) plus DarlingMcpInstructions.cs / McpInstructions.cs.

Specifically verified:

  • SQL parameter ordinal binding is correct on every touched query, including the two-branch (server-scoped vs. fleet-wide) get_alert_history reads where the $N position of limit and the new include_dismissed bool shifts between branches — traced the C# parameter-add order against both SQL consts and it lines up.
  • The limit + 1 over-fetch / Count > limit truncation pattern is applied consistently and correctly at every touched call site, including the two-stage fingerprint-scan-then-page flow in get_blocking/get_deadlocks/get_deadlock_detail (scan ceiling truncation computed before filtering, page truncation computed after).
  • The (dismissed = FALSE OR $N) filter, the dismissed_excluded_count measurement query, and the include_dismissed plumbing match field-for-field between Darling and Lite.
  • The new duration-ranked get_long_query_completions read (SQL Server NULLS LAST / DuckDB equivalent) produces the same population and ordering on both SKUs, fixing the described Lite-only gap.
  • Lite/Darling parity holds throughout the touched surface; the one remaining asymmetry I checked (Darling's dedup_key fingerprint scan has no Lite equivalent on get_blocked_process_reports/get_deadlocks) predates this PR and isn't part of its stated scope.
  • No literal LIMIT caps remain in live code paths (only in comments referencing the old defect); OPTION(RECOMPILE)-equivalent concerns don't apply since these are Postgres/DuckDB reads, not T-SQL collectors.

No correctness, security, or parity issues found.

@erikdarlingdata
erikdarlingdata merged commit 0516ac4 into dev Sep 18, 2026
9 of 11 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/3541-hidden-caps-honest-pages branch September 18, 2026 16:01
erikdarlingdata added a commit that referenced this pull request Sep 18, 2026
… counter, a partial name cannot delete the wrong server, an omitted field is never a clear, and a mute that matched nothing says so (#3541 A14) (#3615)

* MCP write tools report what happened: every server you add lands in a counter, a partial name cannot delete the wrong server, an omitted field is never a clear, and a mute that matched nothing says so (#3541 A14)

add_servers: the per-row status "collides" (#2280) was counted in no summary
counter, so a batch with a collided server summarised as failed: 0 and the
server went silently unmonitored. Every status now maps to exactly one counter
through CounterOfStatus (added / skipped / collided / failed), the envelope
carries requested, the four counters sum to it by construction, and the
aggregator throws on a status the map does not know. Statuses are constants.

remove_server: the read resolver's first-wins partial match deleted whichever
sibling sorted first. The write now honors an exact match, or a partial ONLY
when unique; an ambiguous name deletes nothing and returns status "ambiguous"
with the candidates named (matched_by exact|partial on success).

Instructions: the add_servers row still denied the Entra modes #3484 accepted;
it now names ServicePrincipal / ManagedIdentity as accepted and the interactive
trio as invalid, plus engine / port and the new counters.

update_custom_view / update_custom_alert_rule: one write vocabulary for the
optional description - omitted = unchanged, empty string = cleared - through a
single shared helper (ResolveOptionalText). The view tool used to write an
omitted description as NULL; neither tool offered a clear.

mute_analysis_finding (both SKUs): the response carries registered and
matched_now (stored findings in the mute's scope carrying the hash); status is
"muted" or "muted_unmatched", and Darling's swallowed INSERT failure now
surfaces as status "error" instead of "muted". The store gains a hash count read
(CountStoredFindingsAsync) on both FindingStores; PgFindingStore.MuteStoryAsync
returns whether the row landed. A blank hash is refused.

Rider (#3594 residual): /api/read/get_alert_history dispatches and advertises
include_dismissed.

Tests: counters census + sum, unknown-status throw, construction-site literal
census, ResolveForRemoval truth table, Entra parser-vs-table pin, description
vocabulary pins and cross-tool census, web catalog + dispatch pin, live
extensions for the collision batch / ambiguous removal / omitted-vs-cleared
description on both update tools / matched_now on Darling, and a new Lite DuckDB
class for the mute disclosure with a negative server id.

* Lite FindingStore lock-site census: name the seventh read-lock site (CountStoredFindingsAsync, the #3541 A14 matched_now read)

TheWritePathsTakeAReadLockOnPurpose_AndSayWhy pins the count of _duckDb.AcquireReadLock( sites so
a new lock site is a decision made against the #2455 reason rather than a tidy-up. The new count
read takes the READ lock (it is a read; the exclusion it needs is against maintenance only), so the
pin moves 6 → 7 with the site named beside it. First CI run's only failure.

* CountStoredFindingsAsync states its lock choice at the site (read lock, maintenance-only exclusion, #2455)
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)
erikdarlingdata added a commit that referenced this pull request Sep 18, 2026
… idiom (#3653 Dashboard mirrors) (#3658)

- get_blocking / get_deadlocks / get_alert_history: page counts stop posing as totals
  (*_returned, truncated, limit_applied_by_reader) - #3594's class
- Poison Wait: accumulated over the ten-minute window, graded on the shared
  PoisonWaitEvaluator bars, clear needs an observation - #3593's class
- mute_analysis_finding: registered / matched_now / muted_unmatched - #3615's class
- CollectorSeverity (0,0) online -> Unknown, '--' - #3635's class
- cntr_value_per_second recomputed as a fraction on the read - #3540 A11
- SqlServerAnomalyDetector lab-box markers replaced with what #3616 measured (comment-only)
erikdarlingdata added a commit that referenced this pull request Sep 19, 2026
…hared helper, the census sweeps the inference class instead of a roster, and four payload rules become facts and inventories (partial #3653)

get_pg_deadlocks, get_pg_plan_capture_readiness and get_pg_index_bloat published
truncated = rows.Count >= limit, the #3594 inference; the readiness and index tools
withheld their summaries on the strength of it for pages that were the whole set,
and get_pg_plans beside them Take(limit)'d a read already cut at limit and said
nothing. McpHelpers.BoundPage turns (fetched at limit + 1, limit) into (page,
truncated) in one place; the four tools are its first consumers and every count
downstream is still a count of the page.

McpPageContractTests missed the three because EveryPagedTool_ObservesTruncation
walks the ten tools #3541 A3 named and its #3653 sibling walks one file - a roster
gap, not a regex gap; the matcher catches both spellings and now witnesses the
object-initializer one. NoTool_InfersTruncationFromItsCap_OnEitherSku sweeps every
tool body on both SKUs with a population floor and fails on the three at dev.

McpPayloadContractCensusTests pins the four unpinned rules as far as a census can
honestly go: every catch returns through a shared shape (one ad-hoc site rostered)
and the two shared shapes - a sentence and a JSON envelope - are an inventory for
the vocabulary lane; every bounded parameter reaches a shared validator or one of
five inline days_back refusals, and nothing clamps a parameter; every total_*
assigned from a Count names a whole set (rostered), every latest-anchored reader
projects its anchor (two known exceptions); truncation-key dialects, severity-word
spellings and statement-terminal literal LIMITs are exact inventories. The
DarlingMcpPgPlanToolsTests pin that handed the builder two rows at limit 2 and
expected truncated encoded the inference as its own proof and becomes the boundary
pair.

Executed here through the built Darling.Tests.dll and against PostgreSQL 18.4 /
TimescaleDB 2.28.1; positive controls: the three files at dev state fail the sweep
by name, and BoundPage flipped to >= fails the live boundary, the builder pins and
the helper theory.
erikdarlingdata added a commit that referenced this pull request Sep 19, 2026
…hared helper, the census sweeps the inference class instead of a roster, and four payload rules become facts and inventories (partial #3653) (#3699)

get_pg_deadlocks, get_pg_plan_capture_readiness and get_pg_index_bloat published
truncated = rows.Count >= limit, the #3594 inference; the readiness and index tools
withheld their summaries on the strength of it for pages that were the whole set,
and get_pg_plans beside them Take(limit)'d a read already cut at limit and said
nothing. McpHelpers.BoundPage turns (fetched at limit + 1, limit) into (page,
truncated) in one place; the four tools are its first consumers and every count
downstream is still a count of the page.

McpPageContractTests missed the three because EveryPagedTool_ObservesTruncation
walks the ten tools #3541 A3 named and its #3653 sibling walks one file - a roster
gap, not a regex gap; the matcher catches both spellings and now witnesses the
object-initializer one. NoTool_InfersTruncationFromItsCap_OnEitherSku sweeps every
tool body on both SKUs with a population floor and fails on the three at dev.

McpPayloadContractCensusTests pins the four unpinned rules as far as a census can
honestly go: every catch returns through a shared shape (one ad-hoc site rostered)
and the two shared shapes - a sentence and a JSON envelope - are an inventory for
the vocabulary lane; every bounded parameter reaches a shared validator or one of
five inline days_back refusals, and nothing clamps a parameter; every total_*
assigned from a Count names a whole set (rostered), every latest-anchored reader
projects its anchor (two known exceptions); truncation-key dialects, severity-word
spellings and statement-terminal literal LIMITs are exact inventories. The
DarlingMcpPgPlanToolsTests pin that handed the builder two rows at limit 2 and
expected truncated encoded the inference as its own proof and becomes the boundary
pair.

Executed here through the built Darling.Tests.dll and against PostgreSQL 18.4 /
TimescaleDB 2.28.1; positive controls: the three files at dev state fail the sweep
by name, and BoundPage flipped to >= fails the live boundary, the builder pins and
the helper theory.
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