Repository navigation
Blocking Wait Time fires on one snapshot at 3× the bar or on 3 consecutive collections, not on every snapshot; Long-Running Query gets an opt-out knob seeded from the production read — job-step program prefix, the two NT AUTHORITY logins — applied ahead of the row cap and counted by arm (#3653 A5, Q4 + Q5) - #3734
Merged
Merged
Conversation
… snapshot at 3x the bar still fires at once, anything less holds for 3 consecutive collections through the shared AlertPersistenceGate (#3653 A5, Q4) The Blocking Wait Time alert fired on the FIRST fresh blocking snapshot whose summed wait_time_ms reached the configured bar and resolved on the first one under it. Measured on one production store class (43 servers): 102 episodes, 97 of them a SINGLE snapshot at the collector's ~72 s cadence - including the p99, 1,509 s of summed blocked wait across 145 sessions, gone by the next collection - so a plain K-consecutive gate (the tempdb shape from #3692) would have dropped exactly the episodes an operator most needs to hear about. Erik ruled Q4 on #3653: fire on a single snapshot at N = 3x the configured threshold; otherwise K = 3 consecutive collections through the shared AlertPersistenceGate. Both arms ride ONE gate: the bar handed to ObservePersistenceAsync is one sample when the snapshot alone clears the multiple and BlockingWaitBreachSamples otherwise, so a 3x fire opens the incident on the same persisted record the consecutive arm would have opened it on and the clear that follows is the same clear. The fire prepends a gate item whose `Fired By` field carries the arm token (single_snapshot_3x / consecutive_k3) so the card, the history row and get_alert_history's context_json can say why. What makes this arm unlike tempdb's: dmv_blocking_snapshot writes rows only while something is blocked, so a quiet cycle writes nothing and "the latest snapshot" simply stops advancing. Two breaching snapshots with a quiet cycle between them are two episodes, so the arm restarts a partial streak when a breaching snapshot lands more than 1.5 collector cadences after the last one counted (CurrentBlockingWaitResult now carries the cadence both adapters already resolved for the freshness verdict). An OPEN incident is not restarted by a short gap. A STALE snapshot is a clear here (the #1812 rule this arm always had - it is the collector saying the blocking ended), observed per sweep; a NULL read freezes the gate (the #3692 rule); disabling the gate is not a clear (the CPU arm's rule). The in-memory _activeBlockingWaitAlert flag is gone; the persisted Firing bit under the alert's own metric name in the existing (server_id, metric_name) table is the one "incident open" fact (no rung), seeded at startup with the cooldown stamped so a restart does not re-announce. Lite and Darling both route this alert through the shared engine, so both SKUs get the gate. Long-Running Query (Q5) is a separate commit. Partial #3653 - item 6 (Q4). Does not close it.
…ied in the read ahead of the row cap, and the card counts what it removed (#3653 A5, Q5) - engine + Lite; Darling's store home is a rung away Measured on one production store class: 191 distinct sessions over the 30-minute Long-Running Query bar in 7 days, the p90 of them seen in 6,192 snapshots. These are permanent background requests - brokers, replication readers, agent loops, pollers - that are over any duration threshold forever. No persistence gate can help (they persist by definition); coverage is the fix, and which programs and logins are the background on an estate is per-operator knowledge, so it is a knob. Erik ruled Q5 on #3653: the knob is allowed; the "what are they" measurement that would seed it is the monitor seat's read. IAlertEngineSettings gains LongRunningQueryExcludedProgramNames and LongRunningQueryExcludedLogins. The rule is spelled ONCE in the new LongRunningQueryExclusions (Alerting): case-insensitive, whole-value, a trailing * matches any suffix, % and _ literal, null matches nothing, a bare * refused; From/Normalize give a Settings-window string and an MCP array one meaning. An excluded session is NOT EVALUATED - not read into the decision, not counted, not fingerprinted - which is the opposite of a mute rule, and is why excluding a paging program resolves the open incident instead of silencing it. Applied IN THE READ, before LIMIT, on both SKUs. The read is longest- elapsed-first capped at maxResults (5), and the sessions this knob is for are the longest-running on the server by construction, so a post- read drop (the excludedDatabases shape) would let five permanent requests fill the cap on every sweep and hide the real query behind them - the alert made blind by the knob meant to make it accurate. Both reads gain a candidates CTE that flags each over-threshold row with the shared ILIKE ... ESCAPE predicate (BuildSqlPredicate, operands from $5), keep the unflagged rows under the cap, and return the flagged count as an uncorrelated scalar over the same CTE: one statement, one snapshot. IAlertReadAdapter.GetLongRunningQueriesAsync returns LongRunningQueryReadResult(Sessions, ExcludedCount); login_name is now projected. The High CPU card's maintenance probe passes None: a backup run by an excluded service login is still what is burning the CPU. The card appends an "Excluded Count" item - only when the knob is set - so an operator can see the knob working; a setting whose only effect is a page NOT arriving is one nobody can verify. Lite: two settings.json arrays (documented in settings.sample.json), two boxes on the Settings window's Alerts tab, AppAlertEngineSettings forwards them. Darling: DarlingAlertSettings returns EMPTY for both, with the reason in the comment - the store row is authoritative for every alert knob (ApplyToConfig replaces config.Alerts wholesale), so a darling.json-only member would read as configured and be dead; the honest home is two text[] columns on config_alert_settings, which is a migration rung this lane may not write (cross-seat fence). The MCP twins publish the knob on neither SKU yet for the same reason (McpAlertSettingsKeyTests pins the two payloads to one shape). The Darling store plumbing follows as its own commit, gated on that rung. Partial #3653 - item 6 (Q5). Does not close it.
…d: program names match by PREFIX, logins exactly, the two lists default to the SQL Agent job-step prefix and the two NT AUTHORITY service logins, and the card counts what each arm removed in sessions (#3653 A5, Q5 addendum) The 7-day read of one large production store (monitor seat, authorised under Q5) split the long-running population into four classes: (1) SQL Agent job-step programs - program_name prefix "SQLAgent - TSQL JobStep", ~460 sessions/week across 11 single-server jobs, medians 35-62 min; (2) the NT AUTHORITY\SYSTEM / NT AUTHORITY\NETWORK SERVICE logins - the permanent multi-DAY background (~70 sessions across 42 servers, medians 4.8-8.6 days, 300K+ snapshots, CDC-capture shaped); (3) the application's admin login - carries the job wave but ALSO real ad-hoc long-runners, so excluding it would blind the alert to what it is for; (4) named humans - 3 sessions/week, the page's whole purpose. That read changes the knob's shape from the previous commit: - Program names match by PREFIX (case-insensitive), because job-step names embed the job id; logins match EXACTLY (a prefix would let svc swallow svc_owner). The trailing-* grammar is gone: an entry is the literal prefix / value, % and _ escaped before LIKE, * a character. The member is LongRunningQueryExcludedProgramNamePrefixes (settings.json alert_long_running_query_excluded_program_name_prefixes) so the name says the rule. - SEEDED DEFAULTS: LongRunningQueryExclusions.DefaultProgramNamePrefixes = ["SQLAgent - TSQL JobStep"], DefaultLogins = ["NT AUTHORITY\SYSTEM", "NT AUTHORITY\NETWORK SERVICE"]. Lite: the App statics initialise to the seeds and the settings.json reader replaces a list only when its key is PRESENT - so a fresh install / a file without the key evaluates with the seeds, and an operator who empties a box (the window always writes both keys) has cleared that arm for good; Restore Defaults puts the seeds back, not empty boxes. Darling: DarlingAlertSettings returns the seeds - not an empty knob - until the V135 rung gives the knob its store home, so both SKUs evaluate the same population from day one. - excluded_count SPLITS: both reads return excluded_by_program_prefix and excluded_by_login as two scalars over the candidates CTE, counted in SESSIONS (COUNT(DISTINCT session_id) - inside one collection of one server the (server_id, session_id, tran_start_time) identity collapses to session_id; a MARS session with two request rows is one session), the login count taken AND NOT the program flag so a session matching both arms counts once, under the prefix. The card's knob item shows Excluded Count, Excluded By Program Prefix, Excluded By Login and the entries. - Docs: IAlertEngineSettings, the Lite Settings window, settings.sample .json, README.md and Darling/README.md each state the four classes, the two seeds with their grounding, and why the admin login is absent. - Lite's Settings boxes are AlertLrqExcluded*Box so the #1840 wiring census sees them; they are gated on the master switch and reset to the seeds. The census's collection exception names the two new statics. Executed: the Darling read against a live PostgreSQL 18 store (the E2E's four-class snapshot: prefix arm 2, login arm 1 despite two rows, both arms once; mutation to COUNT(*) without AND NOT fails 3 != 1); the exact Lite DuckDB SQL text on DuckDB 1.5.5 (same fixture, plus the %/_ escaping and the literal-* cases); 347 Darling.Tests in the mactest harness. Partial #3653 - item 6 (Q5). Does not close it.
This was referenced Sep 19, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
… adds two text[] columns to config_alert_settings whose DEFAULT is the production read's seeds, wired through the service seed/read, the Viewer's Settings window and select/upsert, the viewer probe's top arm, and both SKUs' get_alert_settings / update_alert_settings twins (#3653 A5, Q5) (#3736) #3734 gave both SKUs the knob - program_name PREFIXES and exact logins whose sessions the Long-Running Query alert does not evaluate, seeded from the 7-day production read (SQLAgent - TSQL JobStep; NT AUTHORITY\ SYSTEM and NT AUTHORITY\NETWORK SERVICE) - editable on Lite, and on Darling the seeds and nothing else, because a Darling alert knob has no rung-free home: the store row is authoritative (ApplyToConfig replaces config.Alerts wholesale), so a darling.json-only member is dead, and McpAlertSettingsKeyTests pins both SKUs' get_alert_settings to one shape, so neither could publish it until Darling could store it. This is that store home, one rung after #3730's V134. V135: long_running_query_excluded_program_name_prefixes and long_running_query_excluded_logins, text[] NOT NULL, DEFAULT the two seeded arrays - the one knob rung on this row that ships a non-empty default, so a pre-rung row reads as exactly what DarlingAlertSettings returned before the rung and what Lite ships, and the two SKUs and the two sides of the rung evaluate one population. An operator's cleared list is stored as an explicit '{}' and honoured: present-and-empty is a decision, and nothing re-seeds it. AlertsConfig and the Viewer's AlertSettingsRow initialise to the same seeds (pinned equal to the rung's DEFAULT and to LongRunningQueryExclusions.Default*). No reload beacon (V17's trigger), no GRANT (table-level), IF NOT EXISTS. Plumbing, all at the appended ordinals: StoreConfigProvider seeds them as $69/$70 and reads them at 67-68 (a column selected but not read resets the knob on every worker start); the Viewer's select/upsert bind them as $68/$69 and read 67-68, its Settings window gets two boxes (AlertLrqExcluded*Box, so the #1840 wiring census sees them: gated on the master switch, Restore Defaults puts the SEEDS back); the viewer probe gains the V135 sentinel as the top arm (V134's top-rung claims move here, as V134 moved V133's); DarlingAlertReader reads 67-68 for get_alert_settings; update_alert_settings accepts long_running_query.excluded_program_name_prefixes / .excluded_logins through the shared normaliser (empty array = cleared arm); Lite's get_alert_settings publishes the same two keys. Every description states the two rules, the four classes, the seeds' grounding and why the admin login is not a default. TimeHonestyRungLivePostgresTests' simulated climb drops V135's columns and stamp too and expects two rungs: the applier skips anything at or below MAX(version), so a "one rung short" store must be short of the TOP, which this rung now is. Executed on this Mac: the real Darling.Tests.dll (WindowsDesktop stripped) against PostgreSQL 18 in Docker - 402/402 across the rung, probe, seed/ read, viewer bind, MCP and engine classes (the seed -> read scratch round-trip carries a cleared login list back empty and a padded prefix list back normalised; the store's column DEFAULTs read back from the catalogue as the seeds); full suite 11,451 with 191 platform reds (PresentationFramework / SqlClient ConfigurationManager / Windows paths), the same baseline as before this change. Partial #3653 - item 6 (Q5). Does not close it.
This was referenced Sep 19, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 19, 2026
… PRs across the #3653, #3542 and #3691 waves, #3704, #3710, #3712 and #3735, spliced once (#3751) Sixty-five PRs merged to dev since #3672 (the first splice) across two seats - the coordinator's #3653 brains-review residue lanes and the wave-2 worker's #3542 / #3691 PostgreSQL-target engine lanes - and by rule none touched this file; each returned its entry to a buffer. This is the one splice. Text-only; one file; nothing outside ## [Unreleased] and the definition block moves. [Unreleased] gains a ### Changed between ### Added and ### Fixed (Keep-a-Changelog order) for the nine entries whose subject is a designed behaviour that now behaves differently - routing (#3712 confidence chooses the channel), gating (#3692 tempdb persistence, #3734 blocking/LRQ gates, #3724 peak-and-mean), the wire shape (#3719 one error envelope), the review guard's drift arm (#3673), lineage metadata (#3723) and two grading changes (#3693, #3747). Twenty-two new capabilities append to ### Added (the sixteen PostgreSQL-target fact families and rungs, CONFIG_CHANGED, the recurrence label, the V134/V135 columns, the review traps file, the measurement census); thirty-two defects append to ### Fixed. Order within each section is ascending PR number, as #3672 did. Every entry is one line and verbatim from its buffer (trailing whitespace only). Four PR pairs fold into one entry as the buffers wrote them (#3738+#3746, #3683+#3686, #3681+#3685, #3726+#3729); #3695 carries three entries. #3673 has no issue and cites none, like the file's ninety-six other ref-less entries. Five definitions added ([#3691], [#3704], [#3710], [#3712], [#3735]) at the end of the [Unreleased] run, which stays one ascending run (3517..3735). No archive touched; the split was not run. tools/changelog/changelog_archive.py verify: all PASS (1,483 bold entries, floor 1,329; 1,363 distinct refs resolve; 1,372 definitions; CRLF throughout; 409,263 bytes under the ceiling). Client-hygiene grep over the file: zero hits.
This was referenced Sep 20, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 20, 2026
…n both SQL Server SKUs: the shared list becomes the candidates CTE's third flag through the knob's predicate builder, its count rides the card beside the knob's, and the two adapters' post-read Where blocks go (partial #3742) (#3772) Every SQL Server Long-Running Query read was ORDER BY total_elapsed_time_ms DESC LIMIT maxResults and THEN, in C#, dropped the rows whose database is on excludedDatabases — the page cut before the exclusion, so an excluded database's sessions consumed the cap and the alert came back short or empty while un-excluded long-running sessions existed. #3734 refused exactly this shape for the opt-out knob and named the database list as the contrast; that contrast described the defect. The list now rides LongRunningQueryExclusions.BuildSqlPredicates as a third arm (exact, case-insensitive, one ILIKE … ESCAPE term per entry — the login arm's rule, the text both stores already execute for the knob), operands appended after the logins so no ordinal the knob bound moves. Both reads project it as excluded_by_database, drop it in the outer WHERE with the knob's two, and count it AND NOT both knob flags — the database arm is last, so a session the knob would have removed anyway is the knob's and the three counts sum to the sessions removed. LongRunningQueryReadResult carries ExcludedByDatabase; the card gets its own "Excluded By Database" item, rendered only when the list is set (it is empty by default). The three-argument overload is unchanged, so #3770's PostgreSQL-target call sites are byte-identical; that read still filters after its cap and is #3743's follow-through. Executed here: 36 LongRunningQueryExclusionsTests, 176 AlertEngineTests, the live DarlingAlertReadAdapterTests E2E against PostgreSQL 18 (six excluded- database sessions ahead of one real one at cap 5 → a page of five matches; mutation with the old WHERE fails on [86, 85, 84, 83, 82]), the exact Lite DuckDB text on DuckDB 1.5.5 with the same fixture (same mutation, same red), and the census pins. Lite.Tests twin pins are CI-first (WPF-hosted).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mechanism
Blocking Wait Time (Q4). The alert fired on the FIRST fresh
dmv_blocking_snapshotssnapshot whose summedwait_time_msreachedBlockingWaitSecondsThresholdand resolved on the first one under it. It now sits behind the sharedAlertPersistenceGateas a disjunction: one fresh snapshot at or above 3× the bar fires at once; anything between the bar and that multiple must hold for 3 consecutive collections. Both arms ride ONE gate — the bar handed toObservePersistenceAsyncis one sample when the snapshot clears the multiple andBlockingWaitBreachSamplesotherwise — so a 3× fire opens the incident on the same persisted record the consecutive arm would, and the clear that follows is the same clear. The fire prepends a gate item whoseFired Byfield carriessingle_snapshot_3x/consecutive_k3.What makes this arm unlike tempdb's: the collector writes rows only while something is blocked, so a quiet cycle writes nothing. Two breaching snapshots with a quiet cycle between them are two episodes; the arm restarts a partial streak when a breaching snapshot lands more than 1.5 collector cadences after the last one counted (
CurrentBlockingWaitResult.CadenceMinutes, the number both adapters already resolved for freshness). An OPEN incident is not restarted by a short gap. A STALE snapshot is a clear (the arm's #1812 rule — it is the collector saying the blocking ended), observed per sweep; a NULL read freezes the gate; disabling the gate is not a clear. The in-memory active flag is gone; the persistedFiringbit under the alert's own metric name in the existing(server_id, metric_name)table is the one "incident open" fact — no rung.Long-Running Query opt-out knob (Q5). New
IAlertEngineSettingsmembersLongRunningQueryExcludedProgramNamePrefixes/LongRunningQueryExcludedLogins. The rule is spelled once inLongRunningQueryExclusions: a program name matches by prefix (case-insensitive — job-step names embed the job id, so only a prefix can name the class), a login matches exactly (case-insensitive — a prefix would letsvcswallowsvc_owner); no wildcard grammar (*is a character,%/_are escaped beforeLIKE), a null session value matches nothing, blanks are dropped on the way in. An excluded session is NOT EVALUATED — not read into the decision, not counted, not fingerprinted — the opposite of a mute, so excluding a paging program resolves the open incident. Applied IN THE READ ahead ofLIMITon both SKUs (acandidatesCTE flags each over-threshold row once per arm with the sharedILIKE … ESCAPE '\'predicates, the outer query keeps the unflagged rows under the cap and returns the two counts as scalars over the same CTE), because the sessions the knob names are the longest-running by construction and a post-read filter would let them fill the cap and blind the alert.Seeded from the production read. The 7-day Long-Running Query population on one large production store fell into four classes:
program_nameprefixSQLAgent - TSQL JobStep): ~460 sessions a week across 11 single-server jobs, medians 35–62 min → default-excluded by program-name prefix. Anything of theirs that should still page is Name the Agent job on the Long-Running Query card: nightly maintenance should not read as a mystery twelve times #3497's job-naming annotation, not this knob.NT AUTHORITY\SYSTEM/NT AUTHORITY\NETWORK SERVICElogins: the permanent multi-DAY background (~70 sessions across 42 servers, medians 4.8–8.6 days, 300K+ snapshots, CDC-capture shaped) → default-excluded by exact login.So the two lists ship as
["SQLAgent - TSQL JobStep"]and["NT AUTHORITY\SYSTEM", "NT AUTHORITY\NETWORK SERVICE"](LongRunningQueryExclusions.DefaultProgramNamePrefixes/DefaultLogins). They are defaults, not constants: Lite'sAppstatics initialise to the seeds and the settings.json reader replaces a list only when its key is PRESENT — a fresh install or a file without the key evaluates with the seeds; an operator who empties a box (the Settings window always writes both keys) has cleared that arm for good; "Restore Defaults" restores the seeds. Darling'sDarlingAlertSettingsreturns the seeds — not an empty knob — until the knob's store home lands, so both SKUs evaluate the same population from day one.The card's receipt, split by arm. The Long-Running Query card gains a knob item —
Excluded Count,Excluded By Program Prefix,Excluded By Login, and the entries — so an operator can see the knob working and which default did the work. Counts are sessions (COUNT(DISTINCT session_id)inside the one snapshot the read looks at; the production read's(server_id, session_id, tran_start_time)identity collapses tosession_idinside one collection of one server, and a MARS session with two request rows is one session), and a session matching both arms counts once, under the program prefix (the login count is takenAND NOTthe program flag), so the two counts sum to the sessions removed.Evidence (one production store class, 43 servers; one large production store for the knob's defaults)
query_snapshots/v_query_snapshots, the rows the arm has always read).collect.long_query_completionsis not consulted anywhere in the LRQ alert's path, and no LRQ alert description cites it.Why this shape
AlertPersistenceGate's header asks for one mechanism.excludedDatabasespost-read shape — the cap problem above.What it does NOT do
Blocking Detected(the count gate) — untouched.text[]columns onconfig_alert_settings, a migration rung (V135), and one un-landed rung rides at a time (cpu_utilization stores each sample's UTC instant beside its server-local stamp and server_properties stores the engine's time-zone id beside its offset (Darling V134 / Lite v63), so UTC-window readers stop deriving an offset that is an hour wrong across DST (#3653 item 13, Q7 + Q8) #3730's V134 is in flight). PR-B carries the rung,StoreConfigProviderseed/read, the Viewer'sSettingsWindowboxes andViewerDataService.AlertSettings,DarlingAlertReader, and theget_alert_settings/update_alert_settingstwins on both SKUs — neither SKU publishes the knob over MCP in this PR becauseMcpAlertSettingsKeyTestsderives Lite's payload shape from Darling's source, so the two must move together. Darling evaluates with the seeded defaults meanwhile.DarlingWorker.EvaluatePgLongRunningQueryAsync) — stated in the Darling README.long_query_completionscollector (default-off byCollectorScheduleDefaults); the fleet-wide empty table is reported separately.Tests
AlertEngineTests: 19 blocking-wait pins (3× fires at once and says so; 1.5× silent for two, fires on the third asconsecutive_k3; a streak one short that triples fires assingle_snapshot_3x; samecollection_time×9 = one observation; quiet-collection gap restarts; 72 s adjacent stays consecutive; short gap holds an open incident; no cadence degrades to freshness-window counting; stale resolves once and writes nothing more; stale from cold writes nothing; stale resets a partial streak; null freezes an open incident; disable is not a clear; suppression advances the gate; restart resumes / does not re-announce; constants pinned to the ruling; Darling README row pinned to the constants). 8 LRQ pins (knob travels normalised; the seeded defaults remove the four-class fixture's background and keep the admin login's ad-hoc query and the human, counts 2/2 with the job-step-as-SYSTEM session counted once; excluded session not evaluated and the card counts it by arm; login exact vs program prefix,*literal; excluding everything resolves; empty knob = no item; knob that removed nothing says 0/0/0; CPU probe ignores the knob).LongRunningQueryExclusionsTests(rewritten): prefix / exact truth tables,Classifycounts-once rule, the seeds pinned to the four classes (and to NOT containing an admin login), normaliser, prefix vs exactLIKEoperands, both-arm SQL withFALSEfor an empty arm and operand ordinals, both reads' source pins (two flags,COUNT(DISTINCT session_id),AND NOT, no trace of the single-flag shape), both hosts' seeding and Lite's absent-vs-present rule.DarlingAlertReadAdapterTestslive E2E (Darling PostgreSQL tests): a four-class snapshot — job step under the admin login, job step running as SYSTEM, NETWORK SERVICE with two request rows, the human — read withNone(all rows, 0/0), withDefaults(human only, 2/1), and with the login default cleared (NETWORK SERVICE evaluated again, 2/0).DarlingAlertTuningKnobsTests: Darling's seam returns the seeds.LiteAlertForwardingTests: Lite forwards both lists.AlertSettingsControlWiringTests: the two newAlertLrqExcluded*Boxes are loaded, gated, reset and persisted (collection exception named).%/_escaping and literal-*cases (10/10). Mutation:COUNT(*)withoutAND NOTfails the live E2E 3 ≠ 1. Red-first: 15 blocking pins red against the pre-gate arm; 3 LRQ pins red with the knob unwired. Lite.Tests: CI is first execution (WPF-hosted).Partial #3653 — item 6 (Q4, Q5 — Darling store home + MCP twins follow as V135)