From 41c9e66bd1fa90d467b972a35c40b9e10d57d50e Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Fri, 11 Sep 2026 19:59:00 -0400
Subject: [PATCH 1/2] Point every rung citation for alert_persistence_state at
V118, and pin that they do
#3282's rung renumbered from V117 to V118 when #3315 merged first, and five
comments across both SKUs still cite V117 - which is now the mute-rule
reload-beacon rung. Review found three of them on #3328; the other two are
in LiteAlertStateStore and PgAlertStateStore. The repo uses exactly these
citations to check Lite/Darling parity, so a wrong one sends the next person
to the wrong rung.
This was the last commit on #3328 and the merge landed one commit short of
it, so it comes back on its own.
The pin derives the number from StorageVersion.SchemaVersion rather than
comparing against a literal, so a future renumber reds it instead of leaving
silent copies. Each phrase carries its own SUBJECT, which is what makes a
citation attributable without parsing comments at all - and both earlier
spellings are why. Scoped to the FILE it flagged nine correct citations for
other rungs, which is a check someone turns off. Scoped to comment BLOCKS it
needed a hand-rolled line-prefix comment filter, which
CommentFilterAdoptionTests refuses without a stated bound - correctly, and
the bound I measured for it came back unreliable on its own terms. A phrase
naming both the table and the rung needs neither.
Reddened three ways before its pass was trusted: one citation reverted to
V117, a SchemaVersion bump that leaves all five behind, and a reworded
phrase - which has to fail loudly rather than silently matching nothing, so
the pattern is asserted to match at all before its captures are compared.
Assert.NotEmpty is load-bearing there: Regex.Escape escapes '{' and not '}',
which was verified by running it rather than assumed.
---
.../BuiltinAlertPersistenceRungTests.cs | 84 +++++++++++++++++++
.../PgAlertStateStore.cs | 2 +-
Lite/Database/DuckDbInitializer.cs | 2 +-
Lite/Database/Schema.cs | 2 +-
Lite/Services/DuckDbAlertHistoryStore.cs | 2 +-
Lite/Services/LiteAlertStateStore.cs | 2 +-
6 files changed, 89 insertions(+), 5 deletions(-)
diff --git a/Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs b/Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs
index e4e37380f..df63c1e2a 100644
--- a/Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs
+++ b/Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs
@@ -7,7 +7,11 @@
*/
using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
using System.Linq;
+using System.Text.RegularExpressions;
using System.Reflection;
using PerformanceMonitor.Alerting;
using PerformanceMonitor.Common;
@@ -322,6 +326,86 @@ private static string EvaluatePgCpuBody()
return body;
}
+ ///
+ /// Every comment that cites this rung's NUMBER cites the right one.
+ ///
+ /// A rung number in prose is a frozen claim, and this branch froze the wrong one: #3315 landed
+ /// V117 first, this rung renumbered to V118, and five comments across both SKUs kept saying V117 —
+ /// three of which review found and two of which it did not. The repo leans on exactly these citations
+ /// to check Lite/Darling parity, so a wrong one sends the next person to the wrong rung.
+ ///
+ /// The number is derived from rather than
+ /// compared against a literal, so a future renumber reds this instead of leaving silent copies.
+ ///
+ /// And each phrase carries its own SUBJECT, which is what makes a citation attributable
+ /// without parsing comments at all. Two earlier spellings of this pin are the reason it is shaped this
+ /// way. Scoped to the file, it flagged nine correct citations — those files cite V32, V40, V44, V50,
+ /// V60, V61, V80 and V81 for their own tables, all right. Scoped to comment BLOCKS instead, it needed a
+ /// hand-rolled line-prefix comment filter, which CommentFilterAdoptionTests correctly refuses
+ /// without a stated bound — and the bound I measured for it came back unreliable on its own terms. A
+ /// phrase that names both the table and the rung needs neither: it cannot match another rung's citation
+ /// because it does not describe another rung's subject.
+ ///
+ [Fact]
+ public void EveryCommentCitingThisRungsNumber_CitesTheRealOne()
+ {
+ var rung = StorageVersion.SchemaVersion;
+
+ /* Each entry names a file and the citation in it, with {0} where the rung goes. The subject words
+ are part of the phrase deliberately — see the remarks. A reword reds this, which is correct: the
+ prose and the pin are one claim, so the pin has to be edited with it. */
+ var citations = new (string Path, string Phrase)[]
+ {
+ (Path.Combine("Lite", "Database", "Schema.cs"),
+ "config.alert_persistence_state (PgMigrations V{0})"),
+ (Path.Combine("Lite", "Database", "DuckDbInitializer.cs"),
+ "persistence-gate state, porting Darling's V{0}"),
+ (Path.Combine("Lite", "Services", "DuckDbAlertHistoryStore.cs"),
+ "the Lite twin of Darling's V{0} table"),
+ (Path.Combine("Lite", "Services", "LiteAlertStateStore.cs"),
+ "Darling's V{0} table, so the shared engine's CPU gate"),
+ (Path.Combine("Darling", "PerformanceMonitor.Darling.Service", "PgAlertStateStore.cs"),
+ "persistence-gate record from the V{0}"),
+ };
+
+ foreach (var (relative, phrase) in citations)
+ {
+ var source = RepoFile.ReadRepoFileLf(relative);
+
+ /* EXACTLY once. Absent means the prose was reworded and this pin went stale with it; twice
+ means a copy was made that the next renumber would miss. */
+ var expected = string.Format(CultureInfo.InvariantCulture, phrase, rung);
+ Assert.Equal(1, CountOf(source, expected));
+
+ /* And the same phrase carrying ANY other rung number must not appear — the renumber case. The
+ regex is built from the phrase itself, so it cannot drift away from the string above. */
+ var pattern = Regex.Escape(phrase).Replace(@"V\{0}", @"V(\d+)", StringComparison.Ordinal);
+ var found = Regex.Matches(source, pattern, RegexOptions.CultureInvariant)
+ .Select(m => m.Groups[1].Value)
+ .ToArray();
+
+ /* The regex has to MATCH, or the assertion below is over an empty set and passes for the wrong
+ reason — the failure mode of building a pattern out of an escaped literal. */
+ Assert.NotEmpty(found);
+ Assert.All(found, n => Assert.Equal(rung.ToString(CultureInfo.InvariantCulture), n));
+ }
+ }
+
+ /// Non-overlapping occurrences of — IndexOf in a loop, because
+ /// there is no overload that counts and a Split would allocate the whole file per call.
+ private static int CountOf(string haystack, string needle)
+ {
+ var count = 0;
+ var at = haystack.IndexOf(needle, StringComparison.Ordinal);
+ while (at >= 0)
+ {
+ count++;
+ at = haystack.IndexOf(needle, at + needle.Length, StringComparison.Ordinal);
+ }
+
+ return count;
+ }
+
///
/// The README's alert catalog states the sample count, so the number is pinned to the constant rather
/// than left as a prose copy of it. A doc that says "3 samples" while the code says something else is
diff --git a/Darling/PerformanceMonitor.Darling.Service/PgAlertStateStore.cs b/Darling/PerformanceMonitor.Darling.Service/PgAlertStateStore.cs
index 27db57824..9f6bb33cc 100644
--- a/Darling/PerformanceMonitor.Darling.Service/PgAlertStateStore.cs
+++ b/Darling/PerformanceMonitor.Darling.Service/PgAlertStateStore.cs
@@ -381,7 +381,7 @@ a missed or duplicated alert. The alert itself has already been decided by the g
}
///
- /// #3282: loads one subject's built-in persistence-gate record from the V117
+ /// #3282: loads one subject's built-in persistence-gate record from the V118
/// config.alert_persistence_state table.
///
/// Returns null on failure, which the engine reads as "no memory" and arms the gate from zero —
diff --git a/Lite/Database/DuckDbInitializer.cs b/Lite/Database/DuckDbInitializer.cs
index d4222db0d..953805fdc 100644
--- a/Lite/Database/DuckDbInitializer.cs
+++ b/Lite/Database/DuckDbInitializer.cs
@@ -1551,7 +1551,7 @@ positional appender and old parquet are unaffected. */
if (fromVersion < 58)
{
- /* v58 (#3282): the built-in alert catalog's persistence-gate state, porting Darling's V117.
+ /* v58 (#3282): the built-in alert catalog's persistence-gate state, porting Darling's V118.
Before this no built-in alert required its condition to PERSIST — one sample over the bar
fired and the next sample under it resolved — so a momentary CPU spike was indistinguishable
from sustained saturation. New table only; fresh installs get it from
diff --git a/Lite/Database/Schema.cs b/Lite/Database/Schema.cs
index de0d1d0b6..a2df7438c 100644
--- a/Lite/Database/Schema.cs
+++ b/Lite/Database/Schema.cs
@@ -119,7 +119,7 @@ last_observed_at is not display data — it is what makes a row's staleness deci
fingerprint's NEXT incident would decay its already-counted mark to the new window count and report
the recurrence as nothing new. */
/* The BUILT-IN alert catalog's persistence-gate state (#3282), the twin of Darling's
- config.alert_persistence_state (PgMigrations V117) — same columns, same key. "How long must this
+ config.alert_persistence_state (PgMigrations V118) — same columns, same key. "How long must this
condition hold before it counts" for the gauge alerts: consecutive breaching samples so far,
consecutive clearing samples so far, and whether an incident is currently open.
diff --git a/Lite/Services/DuckDbAlertHistoryStore.cs b/Lite/Services/DuckDbAlertHistoryStore.cs
index 8224dba65..b244a3ab3 100644
--- a/Lite/Services/DuckDbAlertHistoryStore.cs
+++ b/Lite/Services/DuckDbAlertHistoryStore.cs
@@ -530,7 +530,7 @@ gate has already decided by this point. */
///
/// #3282: loads one subject's built-in persistence-gate record from config_alert_persistence_state
- /// — the Lite twin of Darling's V117 table. Returns null when there is no row, which the engine reads as
+ /// — the Lite twin of Darling's V118 table. Returns null when there is no row, which the engine reads as
/// "no memory" and arms the gate from zero.
///
/// Null on failure too, deliberately. A load failure cannot resurrect a firing bit it did
diff --git a/Lite/Services/LiteAlertStateStore.cs b/Lite/Services/LiteAlertStateStore.cs
index c06a746ab..6fbae59d6 100644
--- a/Lite/Services/LiteAlertStateStore.cs
+++ b/Lite/Services/LiteAlertStateStore.cs
@@ -181,7 +181,7 @@ public Task SaveIncidentOccurrencesAsync(
///
/// #3282: the built-in persistence-gate record, over config_alert_persistence_state — Lite's twin
- /// of Darling's V117 table, so the shared engine's CPU gate behaves the same on both SKUs. Wrapped in
+ /// of Darling's V118 table, so the shared engine's CPU gate behaves the same on both SKUs. Wrapped in
/// Task.Run like every other method here: DuckDB.NET's I/O is synchronous under its async facade
/// and the engine runs on the WPF dispatcher, so an unwrapped call is a UI hitch (#1202).
///
From 9f4f80ec167c50bdf09a55c316bb58d2022ecb03 Mon Sep 17 00:00:00 2001
From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com>
Date: Fri, 11 Sep 2026 20:24:34 -0400
Subject: [PATCH 2/2] Drop the unused using the pin added, and sort the rest
Review nit. System.Collections.Generic came in with the first spelling of
the pin, which used a List, and stayed behind when the phrase-based version
replaced it. The added lines also put System.Text.RegularExpressions ahead
of System.Reflection.
---
Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs b/Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs
index df63c1e2a..c051690f2 100644
--- a/Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs
+++ b/Darling/Darling.Tests/BuiltinAlertPersistenceRungTests.cs
@@ -7,12 +7,11 @@
*/
using System;
-using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
-using System.Text.RegularExpressions;
using System.Reflection;
+using System.Text.RegularExpressions;
using PerformanceMonitor.Alerting;
using PerformanceMonitor.Common;
using PerformanceMonitor.Darling.Storage;