Skip to content

Drain the review-bot comments left undispositioned across #1824-#1885 - #1895

Merged
erikdarlingdata merged 12 commits into
devfrom
fix/review-bot-audit-residue
Jul 31, 2026
Merged

erikdarlingdata merged 12 commits into
devfrom
fix/review-bot-audit-residue

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Jul 31, 2026 •

Copy link
Copy Markdown
Owner

Ten inline review-bot comments across #1824-#1885 were never explicitly dispositioned. Each was verified against origin/dev before being touched - all ten were still live; none had been fixed by a later PR. Eight are fixed here, two are filed.

Disposition

# PR Item Outcome
1 #1824 Get-CimInstance catch swallows a failed service-account lookup Fixed
2 #1826 Docs still recommending SQLAgentReaderRole Fixed (4 spots, one more than reported)
3 #1829 WorkArea is primary-monitor-only Filed: #1891
4 #1834 AlertValueParser truncates grouped numbers Fixed
5 #1835 Duplicate InternalsVisibleTo Lite.Tests Fixed
6a #1840 Lite's AlertBlockingWaitSecondsBox missing from 3 sibling call sites Fixed + drift guard
6b #1840 Lite/Darling MCP alert key mismatch Fixed (breaking)
7 #1842 DataRootMigration cannot tell a migrated artifact from a stray one Fixed
8 #1853 Window filter vs bucket key mismatch Filed: #1892
9 #1880 bare bare typo Fixed
10 #1885 ReachesFurtherBack branch untested Fixed

What each fix does

#1824 - the installer stops guessing which account it is hardening for. The service-account lookup exists so an upgrade cannot strip the grant of a domain account or gMSA an operator re-homed the service to. Its catch was silent, so a WMI failure fell back to the virtual account and reproduced exactly that lockout with nothing on screen. sc.exe qc is now tried as a non-WMI second opinion (a broken WMI repository alone should not derail an upgrade), and if the account still cannot be resolved the paths split: fresh install warns and proceeds (sc create just set that account, so the default is provably right), upgrade stops with an actionable error rather than ACLing a principal it had to guess.

#1826 - four stale SQLAgentReaderRole recommendations. The bot named three; a repo sweep found a fourth that matters more than the other three combined - the deprecated Dashboard's least-privilege section still hands out ALTER ROLE [SQLAgentReaderRole] ADD MEMBER in copy-pasteable SQL, i.e. instructions #1823 proved do not work. All four corrected, plus the doc comments in FailedJobsQuery, DarlingWorker, AlertHealthResult and DatabaseService.NocHealth that described the role as the thing whose absence causes the skip.

#1834 - grouped numbers. Survey first: no CurrentValueText producer formats with grouping today, so the bot's "doesn't fire yet" holds. But AlertContextBuilders - alert code one file over - fills its detail fields with :N0, so "nobody groups" is a habit, not a guarantee, and the de-DE case is worse than the en-US one because there the group separator IS .. Chose to handle it rather than document a non-goal: the failure mode is a plausible-looking wrong number, which is strictly harder to notice than the : 0 this parser replaced.

The handling is strict on purpose. .NET's own AllowThousands does not validate group placement - double.Parse("1,2,3", en-US) returns 123 - so handing the token to TryParse and trusting it would fuse any comma-joined list. A group is accepted only when the separator is followed by exactly three digits and then a non-digit, so "1,234" reads as 1234 while "1,5", "55,66" and "1,2,3" stop at the first run exactly as before.

#1840a - and a second instance of the same miss. The three sibling call sites are wired to match Darling. Building the drift guard for them surfaced AlertLongRunningQueryMaxResultsBox, wired into neither RestoreAlertDefaultsButton_Click nor UpdateAlertControlStates in Lite and into both in Darling - the identical defect one control over, fixed alongside. Both apps are now pinned by AlertSettingsControlWiringTests: every Alert*Box a settings window LOADS must also be gated and reset. Load and save are impossible to forget (the box does nothing without them); the other two fail silently, which is why this is the pair worth pinning.

#1840b - MCP keys. Breaking for clients reading Lite's payload. blocking.threshold_count becomes count_threshold, deadlocks.threshold becomes count_threshold, and top-level notifications_enabled becomes alerts_enabled. Darling's spelling wins as instructed. The third rename was not in the bot's comment and is the one worth flagging: Darling spells notifications_enabled something else entirely (the analysis section's own toggle), so Lite's name was not merely different but collided - an agent reading that key got two different meanings depending on which app answered. Fixing one key and leaving that would have shipped a known second mismatch. The #1839 CHANGELOG entry, which documented the old threshold_count, is corrected in place - same release, so it must not contradict the code.

#1842 - the biggest one. The bot's argument holds and the trigger is a single transient lock. If monitor.duckdb failed to move once, the app opened DuckDB at the new root and a fresh empty database appeared there the same session; from the next launch on HasCompleteStore read as done and the real store was never looked at again. The config\ variant is likelier still: startup runs ConfigSeeder regardless of whether the migration failed, so a failed config\ move leaves a seeded stand-in permanently occupying the target while the user's real settings.json waits in the install directory for Setup.exe to delete it.

Note that the empty-directory/zero-byte refinement alone does not fix either case - a fresh DuckDB file is not zero bytes and a seeded config directory is not empty. So it is implemented as the narrow half plus a durable one:

  • Empty directory or zero-byte file at the target: recognized as a placeholder, deleted, real artifact moves over it.
  • Anything with content: the live copy is untouched, and the legacy copy is moved out of the install directory into recovered-from-install-dir\ under the data root, where Setup.exe cannot reach it.
  • Same treatment when the new root is already complete: quarantined rather than left with a "delete by hand" note, and quarantined rather than merged, so a stale archive\ from an older install cannot quietly become the live one.

Deliberately not attempted: swapping a bigger legacy store in for a live one. Size is no evidence of which one the user wants. The decision stays theirs - the data just stops sitting somewhere deletable while they make it.

Testing

dotnet build PerformanceMonitor.sln -c Debug -t:Rebuild - Build succeeded, 0 Warning(s), 0 Error(s).

Suite Result
Lite.Tests 1876 passed, 0 failed
Darling.Tests 3793 passed, 0 failed, 212 skipped
Dashboard.Tests 768 passed, 0 failed

Installer.Tests deliberately not run.

install-darling.ps1 validated with the PowerShell language parser - parses clean.

Red-watch. Every new pin was mutated to confirm it is not vacuous, and each mutation turned only its own test red:

Mutation Red
Drop ReachesFurtherBack from PreferDayGrainDaily QueryStore_NeitherDailyCoversTheWindow_DeeperDayGrainStillWins
Disable the group-separator arm 6 grouped cases (malformed-group and no-number cases stayed green)
Restore the old kept short-circuit in Migrate the 3 placeholder/rescue tests
Remove AlertBlockingWaitSecondsBox.IsEnabled from Lite the wiring guard's Lite case only - Darling's stayed green

Review rounds on this PR

The bot found six more, all against code this PR introduced. Every one is fixed here, each verified against the source before acting, each red-watched.

Finding Fix
Nothing pinned Lite's renamed MCP keys (Darling has DarlingMcpAlertToolsTests) McpAlertSettingsKeyTests - new names present AND old ones absent, since re-adding the old key beside the new one is the likelier accident
The "moved it to the quarantine" log fired even when every rescue failed Each arm gated on its own list; the all-failed case says so
DATA-MOVED.txt listed only what went live, omitting quarantined artifacts Separate headings per destination
The marker was built from one run's lists, so a two-launch retry dropped the earlier launch's names Read off the filesystem at write time - no state carried between runs. Also names what is STILL in the folder, so it can be written mid-migration instead of only on a clean run
IsGroup bounded digits after a separator but not the leading run, so "1234,567" fused into 1234567 Leading run must be a valid first group (1-3 digits). A new fusion this PR introduced - pre-PR that input returned 1234
The per-item loop's early return preceded the summary log; the outer catch omitted Rescued Logging now precedes every return; Rescued populated and counted toward PartiallyMigrated

Two of the six are worth calling out because I did not simply take the reviewer's word:

  • On the agent_datetime grant, the review was right that it belongs to a collector this SKU does not have - and checking the T-SQL myself turned up the opposite error it did not mention: 51_collect_running_jobs.sql reads msdb.dbo.syssessions, which my grant block omitted. Following the recipe as written would have left the collector failing on the one table nobody listed. A later round then caught sp_stop_job (44_hung_job_monitor.sql, @stop_hung_job defaults to 1), documented as a conditional grant with the @stop_hung_job = 0 alternative, since it is job control rather than monitoring.
  • On the early return, the review said the user gets "no indication". That is overstated - TryRescue logs its own per-item failure either way. What was actually skipped is the summary line that names the whole set and says what to do about it. My first version of that test asserted only that something was logged and passed with the bug still in place; it asserts the summary specifically now.

Filed, not fixed

🤖 Generated with Claude Code

erikdarlingdata and others added 2 commits July 30, 2026 23:05
Ten inline review comments on this campaign's PRs were never explicitly
answered. Each was verified against dev first; all ten were still live.

Fixed here:

- install-darling.ps1 swallowed a failed service-account lookup and fell
  back to the virtual account. On an UPGRADE that silently re-ACLs
  darling.json around the wrong principal, which is the lockout the
  lookup exists to prevent. sc.exe qc is now a non-WMI second opinion,
  and an unresolvable account fails loudly on the upgrade path while a
  fresh install (where the default is provably right) only warns.
- Four remaining docs still recommended or legitimized SQLAgentReaderRole
  after #1823 disproved it, including copy-pasteable SQL in the
  deprecated Dashboard's least-privilege script.
- AlertValueParser truncated grouped numbers to the first digit group
  ("1,234" -> 1; de-DE "1.234" likewise). Whole groups are now read,
  strictly: a comma-joined list must not fuse, which .NET's own
  AllowThousands does not guarantee.
- Duplicate InternalsVisibleTo Lite.Tests in PerformanceMonitor.Ui.csproj.
- Lite's AlertBlockingWaitSecondsBox was missing from all three sibling
  call sites Darling got. Writing the drift guard for it found a second
  live instance, AlertLongRunningQueryMaxResultsBox, fixed alongside.
- Lite's MCP get_alert_settings keys now match Darling's (count_threshold,
  alerts_enabled). Breaking for MCP clients; noted in the CHANGELOG.
- DataRootMigration could not tell a migrated artifact from a placeholder
  recreated at the target after a failed move, stranding real data in the
  folder Setup.exe deletes. Empty targets are now replaced; populated
  ones keep the live copy and the legacy one is quarantined out of the
  install directory.
- "bare bare" typo in BlockedProcessReportCollector.
- ComposeSourceRouterTests gained the missing ReachesFurtherBack case.
- Two adjacent nits: the fully-qualified AlertValueParser call sites in
  both history stores.

Filed rather than fixed, both needing a design decision:

- #1891 - Add Server dialogs clamp to the primary monitor's work area.
- #1892 - Query Store time-slice filters on collection_time but buckets
  on interval_start_time_utc.

Every new pin was red-watched: mutating each production change turns its
own test red and nothing else's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Lite/Mcp/McpAlertTools.cs
/* alerts_enabled, not notifications_enabled: Darling reports the master switch under this
name, and it spells notifications_enabled something else entirely (the analysis
sub-object's own toggle), so the old Lite name was not merely different — it collided. */
alerts_enabled = App.AlertsEnabled,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is documented as a breaking change for MCP clients (notifications_enabled→alerts_enabled, threshold_count/threshold→count_threshold), and Darling's equivalent (DarlingMcpAlertTools) has a dedicated DarlingMcpAlertToolsTests.cs asserting its JSON shape. There's no equivalent test file for Lite's McpAlertTools.GetAlertSettings — nothing pins the new key names here, so a future edit could silently reintroduce the old spelling (or a new one-off) with no test catching it. Worth adding at least a smoke test asserting the top-level and nested key names Lite now emits, mirroring DarlingMcpAlertToolsTests.

@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review summary

Reviewed the diff for PR #1895 (drains 10 outstanding review-bot comments across #1824–#1885). Overall this is careful, well-tested work — each fix ships with a targeted regression test, and the mutation/"red-watch" testing described in the PR body is genuine (verified the ReachesFurtherBack day-grain test and the AlertSettingsControlWiringTests drift guard both exercise the code paths they claim to).

Correctness spot-checks (no issues found):

  • AlertValueParser.IsGroup grouped-number parsing: traced through all the new en-US/de-DE test cases by hand ("1,234", "1,5", "1,2,3", "1.234,5 ms", "87.5%") — the strict "whole group only" rule is implemented correctly and matches the test expectations.
  • DataRootMigration.Migrate's new placeholder-clear / rescue-to-quarantine logic: walked through the HasCompleteStore branch, the per-artifact collision branch, and the marker-writing conditions. The design (quarantine the lot rather than merge partial artifacts) is sound and matches its own doc comments and tests.
  • install-darling.ps1's sc.exe qc fallback and the fresh-vs-upgrade branch split: the here-string Fail block terminates correctly, $existing is scoped correctly across the create/upgrade decision and the later account-resolution check, and the fresh-install path correctly falls back to the just-created virtual account.
  • Verified Lite/Darling parity for the actual fixes: Darling's SettingsWindow.xaml.cs already had AlertBlockingWaitSecondsBox/AlertLongRunningQueryMaxResultsBox wired into all four call sites (load/save/gate/reset), confirming Lite was the one drifting, not the reverse. Confirmed Darling's DarlingMcpAlertTools.cs already used count_threshold/wait_threshold_seconds/alerts_enabled — Lite's rename matches Darling's existing spelling exactly, not a new third spelling.
  • InternalsVisibleTo dedup and the PerformanceMonitor.Notifications.AlertValueParser → AlertValueParser simplification are safe (both files already had the using PerformanceMonitor.Notifications; directive).

One gap flagged inline: Lite's McpAlertTools.GetAlertSettings breaking key rename (notifications_enabled→alerts_enabled, threshold/threshold_count→count_threshold) has no regression test, unlike Darling's equivalent which is covered by DarlingMcpAlertToolsTests.cs. Nothing pins the new JSON shape on the Lite side.

No security, missing-index-DMV, or other parity issues found beyond the one noted above.

erikdarlingdata and others added 2 commits July 30, 2026 23:25
Review feedback on this PR: the three MCP keys renamed here are documented
as breaking, and Darling's DarlingMcpAlertToolsTests has asserted its JSON
shape all along, but nothing held Lite's. A rename with nothing holding it
is a rename that comes back.

Asserts the new spellings present AND the old ones absent - presence alone
would pass just as happily if someone re-added the old key beside the new
one, which is the likelier accident, and two keys meaning the same thing is
its own bug for an MCP client. Runtime rather than source-parsing, since
JsonOptions' naming policy means the C# identifier is not automatically the
wire key.

Red-watched: reverting alerts_enabled and count_threshold turns exactly the
two matching tests red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread Lite/Services/DataRootMigration.cs Outdated
Comment on lines +170 to +174
log($"Data root '{newRoot}' is already populated and is the one in use, so the older copy in " +
$"'{legacyRoot}' cannot go live. Moved it to '{Path.Combine(newRoot, RescuedDirName)}' so " +
$"the next Setup.exe cannot delete it ({string.Join(", ", rescued)}). If the data you " +
"expected to see is in there rather than in the live store, close Lite and swap it in by " +
"hand; otherwise the folder is safe to delete.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Misleading log message when every rescue attempt in this branch fails.

This log(...) call fires unconditionally after the foreach loop, regardless of whether any items actually landed in rescued. If TryRescue fails for every pending item (e.g. all the rescue targets already exist from an earlier partial run, or the quarantine dir can't be created), rescued is empty and this message still says:

"...Moved it to '...' so the next Setup.exe cannot delete it ()."

— with an empty parenthetical — directly followed by the kept.Count > 0 block saying "Could not move out of '{legacyRoot}': ...". The two messages contradict each other in exactly the scenario this rescue mechanism exists to make legible to the user (their data is about to be deleted by the next Setup.exe run).

Compare with the equivalent branch further down (~line 245), which correctly gates the "rescued" log behind if (rescued.Count > 0). This block should do the same, e.g.:

if (rescued.Count > 0)
{
    log($"Data root '{newRoot}' is already populated and is the one in use, so the older copy in " +
        $"'{legacyRoot}' cannot go live. Moved it to '{Path.Combine(newRoot, RescuedDirName)}' so " +
        $"the next Setup.exe cannot delete it ({string.Join(", ", rescued)}). If the data you " +
        "expected to see is in there rather than in the live store, close Lite and swap it in by " +
        "hand; otherwise the folder is safe to delete.");
}

No test currently exercises the all-rescue-fails path, so this wasn't caught by the new Migrate_QuarantinesTheStaleCopy_... test (which only covers the all-succeed case).

Comment thread Lite/Services/DataRootMigration.cs Outdated
launch and writes the marker then, and a `kept` artifact is still sitting in there. */
if (failed.Count == 0 && kept.Count == 0)
{
TryWriteMarker(legacyRoot, newRoot, moved, log);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor: the final DATA-MOVED.txt signpost (written here via TryWriteMarker(legacyRoot, newRoot, moved, log)) only lists moved items, not rescued ones. In a mixed run (e.g. monitor.duckdb moves normally while config\ collides and gets quarantined via TryRescue), the persistent marker file will say only monitor.duckdb was moved and omit config entirely — even though config also left the legacy root and now lives under newRoot\recovered-from-install-dir\. The console log() call above (line ~245) does mention it at runtime, but that's not persisted anywhere for someone who checks the marker file later instead of the install-time log. Not data-loss (nothing here is destructive), just an incomplete signpost — might be worth including rescued in the marker text too, or noting the quarantine folder's existence in it.

@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review summary

Went through the diff file-by-file and cross-checked the Lite/Darling parity claims made in the PR description against both apps' actual source (not just the changelog prose).

Verified as correct

  • AlertValueParser grouped-number handling — traced ParseOrDefault/IsGroup by hand against every new test case (en-US multi-group, malformed-group rejection, de-DE where . groups and , decimals) and the logic holds, including the boundary case where a 4th digit follows what looks like a 3-digit group ("1,2345" correctly stops at "1").
  • Lite/Darling MCP key parity (#1840b) — confirmed Darling/.../DarlingMcpAlertTools.cs already emits alerts_enabled / count_threshold (both blocking and deadlocks), so Lite's rename in Lite/Mcp/McpAlertTools.cs genuinely brings it in line rather than just matching prose in the changelog. Also confirmed Lite has no update_alert_settings write tool, so there's no read/write key mismatch to worry about post-rename.
  • Alert settings box wiring (#1840a) — confirmed Darling's SettingsWindow.xaml.cs already wires AlertBlockingWaitSecondsBox and AlertLongRunningQueryMaxResultsBox into both UpdateAlertControlStates and RestoreAlertDefaultsButton_Click (with matching defaults, "0" and "5"), so Lite's fix is a genuine parity catch-up and not new drift.
  • install-darling.ps1 service-account resolution — the fresh-vs-upgrade branching is correct: $existing is captured before the create/upgrade branch, so the elseif ($existing) hard-fail only triggers on upgrades, and fresh installs correctly fall through to the warn-and-continue branch. The sc.exe qc fallback parsing is reasonable given the acknowledged localization caveat.
  • Namespace-qualifier cleanups in PgAlertHistoryStore.cs/DuckDbAlertHistoryStore.cs, the duplicate InternalsVisibleTo removal, and the various doc/comment corrections (SQLAgentReaderRole, "bare bare") all check out against current source.

Issues found (posted inline)

  1. Lite/Services/DataRootMigration.cs (~line 170) — In the "new root already complete" branch, the log message claiming data was quarantined fires unconditionally, even when every TryRescue call in the preceding loop fails (rescued is empty). That produces a contradictory pair of log lines (“Moved it to ... ()” immediately followed by “Could not move out of ...”) in exactly the scenario this mechanism exists to make legible — untested by the new Migrate_QuarantinesTheStaleCopy_... test, which only covers the all-succeed path.
  2. Same file (~line 263), minor — The final DATA-MOVED.txt marker is written with moved only, so a mixed run (some artifacts moved normally, others rescued into quarantine due to a collision) leaves the persistent signpost silently omitting the rescued ones. Runtime log() output mentions it, but the file someone reads later doesn't.

Not flagged

No missing-index DMV suggestions per your instructions, and no other Lite/Darling parity gaps found — the ten dispositions in the PR description all matched what's actually in the diff and in the current source for the app said to already be correct.

erikdarlingdata and others added 3 commits July 30, 2026 23:57
Both are mine, both from the #1842 rescue path added earlier in this branch.

The "moved it to the quarantine" log fired unconditionally after the rescue
loop, so a run where EVERY rescue failed still claimed the move - naming an
empty list and pointing at an empty folder. This log is the only channel
that early, and an operator who reads it and finds nothing there stops
looking, with their data still in the folder Setup.exe deletes. Each arm is
now gated on its own list, and the all-failed case says so plainly.

The DATA-MOVED.txt marker listed only `moved`, so a mixed run - store
migrates, config collides and is quarantined - left a permanent signpost
naming the store and omitting config entirely. Someone reads that file
precisely because they are hunting one artifact by name, so the omission
reads as proof it was never touched. Rescued names now appear under their
own heading with the quarantine path, and the already-populated branch
(where nothing goes live) writes them there rather than mislabeling them
as moved.

Red-watched: restoring the unconditional log and the moved-only marker
turns exactly the two new tests red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review: #1895 — Drain the review-bot comments left undispositioned

This is a well-scoped, well-tested PR: 23 files, mostly small targeted fixes plus solid new test coverage (AlertSettingsControlWiringTests, McpAlertSettingsKeyTests, plus the DataRootMigration and AlertValueParser expansions). I spot-checked the disposition table against the actual code rather than just the PR description:

Verified correct:

  • MCP key parity (#1840b): Confirmed Darling's DarlingMcpAlertTools.cs already emits alerts_enabled, blocking.count_threshold, deadlocks.count_threshold — Lite's McpAlertTools.cs now matches exactly. Lite has no update_alert_settings tool today, so there is no round-trip/write-path mismatch from this rename.
  • Settings-window wiring drift guard (#1840a): Confirmed both Lite/Windows/SettingsWindow.xaml.cs and Darling/.../SettingsWindow.xaml.cs now wire AlertBlockingWaitSecondsBox and AlertLongRunningQueryMaxResultsBox into load/save/gate/reset symmetrically. The drift-guard test's anchors (LoadAlertSettings/SeedAlertControlsFrom, UpdateAlertControlStates, RestoreAlertDefaultsButton_Click) exist verbatim in both files, so it is not testing nothing.
  • msdb grant docs (Least-privilege monitoring grants: document the verified set, harden three paths (#1823) #1826): The new GRANT SELECT/GRANT EXECUTE script in deprecated/Dashboard/README.md lines up with what the actual collectors read (RunningJobsCollector.cs/AgentStatusCollector.cs use sysjobactivity/agent_datetime; FailedJobsQuery.cs uses sysjobs/sysjobhistory).
  • install-darling.ps1 (Darling: survive service accounts that are not the virtual account (#1823) #1824): The fresh-vs-upgrade split and the sc.exe qc fallback read correctly — an upgrade with an unresolvable logon account Fails (exits) before the ACL-hardening step runs, so it cannot silently ACL the wrong principal. Good.

Two findings below (one narrow correctness gap, one signpost-completeness gap), both posted inline. Neither blocks — they are refinements to already-good defensive code.

No SQL Server missing-index recommendations here as requested — this PR does not touch that surface anyway.

Comment thread Lite/Services/DataRootMigration.cs Outdated
Comment on lines 269 to 277
/* Only claim the move in the marker once nothing is still behind — a partial run retries next
launch and writes the marker then. */
if (failed.Count == 0)
launch and writes the marker then, and a `kept` artifact is still sitting in there. Rescued
names go in too, under their own destination: they also left this folder, and a marker that
named only `moved` would tell someone hunting for their settings.json that it was never
touched. */
if (failed.Count == 0 && kept.Count == 0)
{
TryWriteMarker(legacyRoot, newRoot, moved, log);
TryWriteMarker(legacyRoot, newRoot, moved, rescued, log);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correctness (signpost completeness across multiple launches): moved/rescued/kept are local to a single Migrate() call, and the marker is only written when kept.Count == 0 (same gating exists in the AlreadyMigrated branch above at ~190-195). That's correct for a single run, but across multiple runs it can silently drop history from the marker:

  1. Launch 1: artifact A rescues successfully, artifact B's rescue fails (kept = [B]). Per the "claiming a move that didn't happen is worse than saying nothing" rule, the marker is (rightly) not written this run.
  2. Launch 2: A is no longer in legacyRoot (it already moved), so pending only contains B. B now succeeds. This run's local rescued = [B], and since kept.Count == 0 now, TryWriteMarker fires — but with a list that only contains B.
  3. Once written, the marker is never revisited (a later launch finds pending.Count == 0 and returns NothingToMigrate before reaching any marker logic).

End state: A really did move to RescuedDirName and is safe, but DATA-MOVED.txt never mentions it — only B. That directly undercuts the rationale given a few lines up ("a marker naming only the artifacts that went live would read... as proof it was never touched at all") and in the TryWriteMarker doc comment, just for a multi-launch retry instead of a single-launch partial failure. Worth accumulating moved/rescued names that leave the legacy root even on runs where the marker isn't written yet (e.g., append to a running list persisted via a sidecar, or just always write/update the marker with whatever has left so far, rather than gating the entire marker on kept.Count == 0).

Comment on lines +87 to +91
if (!seenSeparator && IsGroup(span, end, groupSeparator))
{
end += groupSeparator.Length + GroupSize;
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Correctness (minor/edge-case): IsGroup validates that a group following a separator is exactly 3 digits, but nothing bounds the length of the leading digit run before the first separator is seen — the digit-consuming branch a few lines up (char.IsAsciiDigit(span[end])) will happily consume 4+ digits before ever reaching a separator.

That means a malformed leading run followed by one well-formed group is accepted as a single continuous number, even though it isn't valid grouped formatting: "1234,567" (en-US) — leading run "1234" (4 digits, not ≤3), then ,567 which does pass IsGroup (exactly 3 digits, then end-of-string) — so the whole token "1234,567" goes to double.TryParse with AllowThousands, which (per the doc comment on IsGroup, citing double.Parse("1,2,3", en-US) == 123) doesn't validate placement either, and returns 1234567.

Before this PR, that same input would have stopped at the first non-decimal separator and returned 1234 — so this is a new (if narrow) way to fuse two adjacent numbers, which is exactly what the PR's own stated goal ("a comma-joined list must not fuse into one number") is trying to prevent. It's not covered by the new test cases, which only exercise malformed trailing groups ("1,2,3", "1,2345"), not a malformed leading run followed by a valid group.

Given no current producer emits this shape, practical impact is low today — but since the whole point of IsGroup is documented strictness, it's probably worth bounding the leading run to 1–3 digits too (track how many digits were consumed before the first separator and reject if it's not 1–3).

erikdarlingdata and others added 2 commits July 31, 2026 00:27
The marker file was assembled from the calling run's moved/rescued lists,
which are local to one Migrate() call. Launch 1 rescues A and fails on B,
so it correctly writes nothing; launch 2 sees only B pending, succeeds, and
writes a marker naming B alone - with A safe in the quarantine and never
mentioned again, because a later launch finds nothing pending and returns
before any marker logic. The lists are now read off the filesystem at write
time, which has no such hole and carries no state between runs, and the
marker names what is STILL in the folder too - so it can be written as soon
as anything leaves instead of waiting for a clean run.

AlertValueParser bounded the digits AFTER a group separator to exactly three
but never the leading run before the first one, so "1234,567" fused into
1234567 - a NEW way to join two adjacent numbers, which is the exact thing
this PR's strictness exists to prevent, and worse than the 1234 it returned
before grouping was handled at all. The leading run must now be a valid
first group (1-3 digits). Only the first separator needs it; every later run
is exactly three by construction. Plain decimals like "1234.56" are
untouched - the bound is on groups, not on digits.

Red-watched: reverting either turns exactly its own tests red.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread deprecated/Dashboard/README.md Outdated
GRANT SELECT ON dbo.sysjobs TO [SQLServerPerfMon];
GRANT SELECT ON dbo.sysjobactivity TO [SQLServerPerfMon];
GRANT SELECT ON dbo.sysjobhistory TO [SQLServerPerfMon];
GRANT EXECUTE ON dbo.agent_datetime TO [SQLServerPerfMon];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit: agent_datetime isn't actually referenced by any of the Full Edition's T-SQL collectors (install/*.sql) — it's only used by PerformanceMonitor.Collectors/AgentStatusCollector.cs, which is Lite/Darling-only (not wired into the deprecated Dashboard). sysjobs, sysjobactivity, and sysjobhistory are the three actually read by install/51_collect_running_jobs.sql / 44_hung_job_monitor.sql, matching that file's own dependency comment exactly. Harmless to grant (trivial EXECUTE on a system scalar function), but since this section's whole point is precision over the old blanket SQLAgentReaderRole recommendation, this one grant doesn't belong in the "least privilege" recipe for this SKU.

@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review

Went through the full diff (23 files, +945/-88) against the ten dispositions the PR body lists, cross-checking each claim against the actual current source rather than just the PR description. Summary: this holds up well — I traced the trickiest piece of new logic end-to-end and didn't find a defect, and the Lite/Darling parity claims check out against the live code on both sides.

AlertValueParser grouped-number parsing (PerformanceMonitor.Notifications/AlertValueParser.cs) — hand-traced every InlineData case in the new tests plus a few not covered (multi-group chains like "1,234,567", signed "-1,024") through IsGroup/ParseOrDefault character-by-character. The leading-run bound (end - digitsStart is >= 1 and <= GroupSize) correctly prevents a 4+ digit run before the first separator from fusing ("1234,567" stays 1234, doesn't become 1234567), and the seenGroup bypass after the first accepted group is safe because IsGroup re-validates the 3-digit+non-digit boundary itself each time. No bug found.

DataRootMigration.cs rescue/quarantine logic — traced the HasCompleteStore branch, TryClearPlaceholder (empty-dir/zero-byte detection), and TryRescue (collision-safe move into recovered-from-install-dir\) for the interrupted-migration and multi-launch cases. The marker-file rewrite reading OurArtifactsIn() off the filesystem rather than the current call's local moved/rescued lists is the right call for the cross-launch case described in the doc comment — a second launch that only sees the leftover artifact would otherwise silently drop the first launch's rescued item from the marker. No data-loss path found; every failure mode leaves data live or in a discoverable spot, never silently stranded.

Lite/Darling parity — verified directly against Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpAlertTools.cs: Darling already emits alerts_enabled, blocking.count_threshold, and deadlocks.count_threshold, exactly what Lite's McpAlertTools.cs now matches. Lite has no update_alert_settings MCP tool at all (read-only), so there's no round-trip/write-path to also update — the rename is self-contained. Also verified AlertBlockingWaitSecondsBox and AlertLongRunningQueryMaxResultsBox are now wired into LoadAlertSettings/SeedAlertControlsFrom, UpdateAlertControlStates, and RestoreAlertDefaultsButton_Click in both Lite/Windows/SettingsWindow.xaml.cs and Darling/PerformanceMonitor.Darling.Viewer/SettingsWindow.xaml.cs — no drift left between the two.

install-darling.ps1 — the fresh-vs-upgrade split on account-resolution failure (warn-and-proceed vs. Fail and stop) is gated correctly on $existing, which is set before the sc.exe qc fallback runs. $serviceName is a hardcoded constant, not external input, so no injection concern in the Get-CimInstance -Filter/sc.exe qc calls.

One minor doc nit, left inline: the corrected deprecated/Dashboard/README.md grant block adds GRANT EXECUTE ON dbo.agent_datetime, but that function isn't referenced by any Full Edition T-SQL collector — only by the Lite/Darling-only AgentStatusCollector.cs. Harmless to grant, but slightly undercuts the "precise minimal grants" point of this fix.

Nothing else stood out — no SQL injection surface (no T-SQL changed in this PR), no secrets handling issues, no performance regressions in the changed paths. The CHANGELOG entries I spot-checked against the code match what actually shipped.

Review feedback on this PR. Replacing SQLAgentReaderRole with direct table
grants, I copied the recipe from the Lite/Darling README rather than from
this SKU's own collectors. Verified against the T-SQL: agent_datetime is
never referenced by install/*.sql at all - it is AgentStatusCollector.cs,
which the Dashboard does not even reference - so it does not belong in a
least-privilege recipe whose whole point is precision.

The check also turned up the opposite error the review did not mention:
51_collect_running_jobs.sql reads msdb.dbo.syssessions, which my grant
block omitted, so following it would have left the collector failing on
the one table nobody listed. The four granted now are exactly the four
those two scripts read.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment on lines +104 to +115
/* Direct table grants, deliberately NOT SQLAgentReaderRole: that role gates the sp_help_job*
procedures, which this product never calls, and grants NO SELECT on the tables the running
jobs collector actually reads - with only the role, those reads fail with error 229 (#1823).
These four are exactly what 51_collect_running_jobs.sql and 44_hung_job_monitor.sql read.
Lite and Darling need a wider set (syscategories, sysjobschedules, and EXECUTE on
agent_datetime) for collectors this edition does not have - see the main README. */
USE [msdb];
CREATE USER [SQLServerPerfMon] FOR LOGIN [SQLServerPerfMon];
ALTER ROLE [SQLAgentReaderRole] ADD MEMBER [SQLServerPerfMon];
GRANT SELECT ON dbo.sysjobs TO [SQLServerPerfMon];
GRANT SELECT ON dbo.sysjobactivity TO [SQLServerPerfMon];
GRANT SELECT ON dbo.sysjobhistory TO [SQLServerPerfMon];
GRANT SELECT ON dbo.syssessions TO [SQLServerPerfMon];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This grant block is still incomplete for 44_hung_job_monitor.sql. That script's @stop_hung_job parameter defaults to 1, and when a hung job is detected it runs EXECUTE msdb.dbo.sp_stop_job (install/44_hung_job_monitor.sql:195-196) — a job-control EXECUTE, not a table SELECT. None of the four GRANT SELECTs here cover it, and it's not listed under "Optional" either.

Under this exact least-privilege recipe, the default hung-job auto-stop will fail its permission check every time it fires — caught and logged to collection_log rather than crashing, but silently non-functional despite an operator following the doc precisely. This is the same class of gap the commit message for this change says it went and verified against the T-SQL (which is how syssessions got added) — sp_stop_job just wasn't the miss that pass caught.

Comment thread Lite/Services/DataRootMigration.cs Outdated
}

if (moved.Count == 0 && failed.Count == 0)
if (moved.Count == 0 && rescued.Count == 0 && failed.Count == 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This early return can silently strand data with zero log output — the exact failure mode this PR otherwise guards against.

If every pending artifact collides with real content at the target and TryRescue also fails for all of them (e.g. recovered-from-install-dir\ already has an entry with that name from an earlier partial run, or the rescue move throws), then moved, rescued, and failed all stay empty while kept is not. This condition is true, so the method returns AlreadyMigrated immediately — skipping the moved > 0 / rescued > 0 / kept > 0 log blocks a few lines below and skipping TryWriteMarker entirely.

Per this method's own doc comment, "log is the only channel out" this early — so the user gets no indication that real data (e.g. config/settings.json) is still sitting in the folder Setup.exe will delete on the next upgrade.

This is exactly the scenario the PR added a dedicated log + test for in the sibling HasCompleteStore(newRoot) branch above it (Migrate_EveryRescueFails_SaysSoInsteadOfClaimingAMove), but this per-item-loop path has no equivalent coverage. Reproduces with a single pending item that collides with real (non-placeholder) content and whose rescue also fails.

Status = failed.Count > 0 ? DataRootMigrationStatus.PartiallyMigrated : DataRootMigrationStatus.Migrated,
Moved = moved,
Kept = kept,
Rescued = rescued,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor inconsistency: the outer catch (Exception ex) block a few lines below this method (not itself touched by this PR, so it doesn't show in the diff) builds its DataRootMigrationResult with Moved, Kept, Failed but no Rescued = rescued. If an artifact was already moved into recovered-from-install-dir\ before a later exception aborts the run, the result reports Rescued as empty even though the rescue actually happened — unlike this return (and the AlreadyMigrated one above), which both now populate Rescued. App.xaml.cs doesn't currently read result.Rescued, so it's not user-visible today, but it's a real gap in the contract this PR just introduced.

@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review summary

This PR dispositions ten review-bot comments left open across #1824-#1885: eight fixed here, two filed as new issues (#1891, #1892). It's unusually disciplined about it — each fix is red-watched (a targeted mutation confirmed to turn only its own test red), and three of the five commits are the author fixing bugs a self-review found in this PR's own earlier commits (the marker-log claiming a move that didn't happen, the marker not surviving multiple launches, the grouped-number parser fusing "1234,567", and the deprecated Dashboard's grant list missing syssessions).

Scope checked: correctness/edge-cases, Lite/Darling parity, security (the install-darling.ps1 account-resolution and file-ACL changes), and the AlertValueParser grouping logic. No missing-index recommendations anywhere, as instructed.

What holds up well

  • AlertValueParser's group-separator parsing (PerformanceMonitor.Notifications/AlertValueParser.cs) is genuinely tricky (culture-dependent group vs. decimal separators, AllowThousands not validating placement) and the strictness is correctly implemented, including the second-pass fix for the leading-run bound ("1234,567" no longer fuses into 1234567). Traced through by hand against the new test cases; didn't find a counterexample.
  • MCP key parity: verified Lite/Mcp/McpAlertTools.cs's new alerts_enabled / count_threshold / wait_threshold_seconds against Darling/PerformanceMonitor.Darling.Service/Mcp/DarlingMcpAlertTools.cs directly — they match exactly, and Lite.Tests/McpAlertSettingsKeyTests.cs pins both the new names and the absence of the old ones.
  • Alert settings control wiring parity: confirmed both AlertBlockingWaitSecondsBox and AlertLongRunningQueryMaxResultsBox are now wired into all four call sites in both Lite/Windows/SettingsWindow.xaml.cs and Darling/PerformanceMonitor.Darling.Viewer/SettingsWindow.xaml.cs (Darling already had them; the new AlertSettingsControlWiringTests.cs drift guard is a good addition).
  • install-darling.ps1: the sc.exe qc fallback and the fresh-install-warns/upgrade-stops split are logically sound; $serviceName is a fixed constant so there's no injection surface in the -Filter string or the sc.exe calls.
  • ComposeSourceRouterTests's new ReachesFurtherBack case: worked through the floor/coverage arithmetic by hand — it exercises the intended branch in PreferDayGrainDaily correctly.

Issues found (posted inline)

  1. Lite/Services/DataRootMigration.cs:239 — a real silent-data-loss path: when every pending artifact collides with real content and every rescue also fails, the method returns AlreadyMigrated with zero log output and no marker, even though real data is left in the folder Setup.exe deletes. This is the same failure mode the PR explicitly guards against (and tests) in the sibling HasCompleteStore branch just above it — this one has no equivalent test.
  2. Lite/Services/DataRootMigration.cs:283 (minor) — the outer catch block's result omits Rescued, so a Migrate call that rescues something and then throws under-reports what happened. Not currently user-visible since the only caller doesn't read Rescued, but it's an inconsistent contract.
  3. deprecated/Dashboard/README.md:104-115 — the least-privilege grant block this commit just finished correcting is still incomplete: 44_hung_job_monitor.sql defaults @stop_hung_job = 1 and EXECUTEs msdb.dbo.sp_stop_job, which none of the four GRANT SELECTs cover. Caught and logged rather than crashing, but the documented recipe leaves the default auto-stop feature silently non-functional.

Nothing else stood out as a correctness, parity, or security issue in the diff.

erikdarlingdata and others added 2 commits July 31, 2026 00:57
…stop_job

All three verified against the code before acting; one of the three claims
turned out to be overstated and the fix is scoped to what is actually true.

The per-item loop's early return ran before the summary log blocks, so a run
where every artifact collided AND every rescue then failed returned without
the line telling the operator these are still in the folder Setup.exe
deletes. NOT total silence as the review had it - TryRescue logs its own
per-item failure either way - but the actionable summary, the only line that
names the whole set and says what to do, was skipped. Logging now precedes
every return. The first version of the test asserted only that SOMETHING was
logged and passed with the bug still in place; it asserts the summary line
specifically now, and red-watches correctly.

The outer catch built its result without Rescued, so an artifact already
quarantined before a later throw was reported as not rescued. It also could
not reach PartiallyMigrated on rescues alone. Both corrected - a rescue is
progress exactly like a move.

44_hung_job_monitor.sql runs EXECUTE msdb.dbo.sp_stop_job when it finds a
hung job, and @stop_hung_job defaults to 1, so following the least-privilege
recipe as written left the auto-stop failing its permission check on every
fire. Documented as a conditional grant with the alternative (@stop_hung_job
= 0) spelled out, since it is job CONTROL rather than monitoring and not
everyone wants it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

claude Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review

Went through the full diff (23 files) plus the current state of the touched code paths. This is a disciplined audit PR — each of the 8 fixed items is scoped tightly to its issue, backed by tests that were mutation-checked (confirmed via the "red-watch" table), and the two deferred items are filed rather than rushed. Nothing here reads as scope creep.

Verified correct by manual trace (not just trusting the new tests):

  • AlertValueParser.IsGroup/ParseOrDefault — walked the grouping/decimal state machine by hand against several of the new test cases ("1,234,567 reads", "1.234,5 ms" under de-DE, "1234,567" non-fusion, negative + grouped "-1,024"). The strictness invariant (a group must be preceded by a valid leading run and followed by exactly 3 digits + non-digit) holds in every case I traced.
  • DataRootMigration.Migrate — traced the HasCompleteStore / TryClearPlaceholder / TryRescue control flow, including the "every rescue fails" and "quarantine name already taken" branches. Logging happens before every return path, so the "silence strands data" failure mode the review called out is actually closed.
  • install-darling.ps1 account resolution — confirmed the upgrade path bails (Fail, service already stopped with binPath already updated, so no half-applied state) before ever reaching the DACL-hardening section, and the fresh-install path can only warn because sc create just set the very account being assumed.
  • Lite/Darling parity: confirmed Darling's SettingsWindow.xaml.cs already wires AlertBlockingWaitSecondsBox and AlertLongRunningQueryMaxResultsBox into all 4 call sites (load/save/gate/reset) as claimed, and confirmed DarlingMcpAlertTools.cs already emits count_threshold/alerts_enabled — so Lite's MCP rename is genuinely closing a gap rather than introducing one. Also confirmed no other internal Lite consumer (UI, settings round-trip) still reads the old notifications_enabled/threshold_count keys — the rename is clean.
  • The duplicate InternalsVisibleTo Include="Lite.Tests" removal and the bare bare → bare typo fix are exactly what they claim to be.

One finding — a leftover instance of the exact bug this PR fixes:

deprecated/Dashboard/Services/DatabaseService.NocHealth.cs line 1097 (not touched by this PR's diff, so I couldn't leave an inline suggestion on it) still reads:

/// SQLAgentReaderRole access raises a catchable SqlException (916/229/297/300) that returns

This is the XML summary doc comment on GetRecentlyFailedJobsAsync in the same file. Twenty lines below it, the catch block for that same method was already corrected (presumably by #1823) to say SQLAgentReaderRole is deliberately NOT the named remedy: it gates the sp_help_job* interface only. So the method's catch block and its own doc comment now contradict each other in place.

This PR's #1826 fix explicitly frames itself as "a repo-wide sweep for the same shape" that "found a fourth [instance] that matters more than the other three combined." Given that framing, this is a fifth instance in the same file as an instance the PR already fixed (the GetCollectionStoppedAsync comment a few dozen lines away was corrected in this PR). Small, but worth closing in the same pass rather than leaving it for a ninth review-bot comment later — suggested fix:

/// the server-local RunDateTime note). Runs at alert-check time — failure outcomes are not
/// part of the collected running_jobs snapshot. Degrades gracefully: a login that cannot
/// SELECT the msdb job tables raises a catchable SqlException (916/229/297/300) that returns
/// an empty list rather than failing the alert cycle. Azure SQL DB (no Agent) is skipped by
/// the caller.

(Lower-confidence, optional: deprecated/Dashboard.Tests/CollectionStoppedDetectionTests.cs line 83 has a similar comment — // msdb unreadable (Azure / RDS / no SQLAgentReaderRole) — for the collection-stopped check, which per this PR's own Darling/README.md change is gated on HAS_DBACCESS('msdb') and has nothing to do with role membership. Weaker case since it's describing a real-world cause-of-restriction rather than asserting the role is a valid remedy, so I'd leave it to your judgment.)

No correctness bugs, no Lite/Darling drift, and no security issues found beyond the one doc-comment leftover above. Didn't flag any missing-index DMV suggestions per your standing preference.

@erikdarlingdata
erikdarlingdata merged commit c20dbd2 into dev Jul 31, 2026
5 checks passed
@erikdarlingdata
erikdarlingdata deleted the fix/review-bot-audit-residue branch July 31, 2026 05:06
erikdarlingdata added a commit that referenced this pull request Jul 31, 2026
Fix the three #1895 residue items: per-monitor dialog clamp, Query Store window edges, cpu.mode vocabulary
ianwalkeruk pushed a commit to ianwalkeruk/PerformanceMonitor that referenced this pull request Jul 31, 2026
erikdarlingdata#1892, erikdarlingdata#1911

erikdarlingdata#1891 - the Add Server dialogs clamped against SystemParameters.WorkArea,
which always reports the PRIMARY monitor. On a secondary monitor shorter
than the primary that reproduced the very off-screen-footer bug erikdarlingdata#1828/erikdarlingdata#1829
fixed. The clamp and the height cap now resolve the actual monitor via
MonitorFromWindow + GetMonitorInfo, converted from physical pixels to WPF
units through the window's own presentation source - an unconverted work
area on a 150%-scaled screen would be a worse bug than the one being fixed.
Re-evaluated on SourceInitialized and LocationChanged, because the cap it
replaced was a one-shot XAML binding read once at load, so a dialog dragged
to a shorter screen kept the taller screen's cap forever. Every path that
cannot get a real answer falls back to the old behaviour, so single-monitor
is unchanged. Win32 rather than WinForms: neither app references WinForms
and one lookup is not worth a second UI framework. Shared between both apps
so the twins cannot drift.

erikdarlingdata#1892 - the Query Store slicer and duration trend filtered the window on
collection_time while keying the bars on the interval start. Once erikdarlingdata#1841 made
those different instants the chart disagreed with its own range at both
edges: an interval starting before the range but collected inside it drew a
bar dated before the range began, and the range's final interval - still
open when the range ended - was dropped entirely. Both reads now window on
the expression they place on, in both apps.

A bare collection_time floor stays and is NOT a filter: query_store_stats is
a hypertable partitioned on that column and Lite's view unions the parquet
archive, so without it Postgres decompresses every chunk and DuckDB cannot
skip row groups. An interval is always collected after it starts, so the
floor is provably implied and can never decide membership - the same shape
as the drill-down collector's last_execution_time bound. Deliberately no
upper twin: collection_time can exceed the interval start without bound if
the collector was down when the interval closed, so a ceiling would silently
drop exactly the rows a restarted collector just caught up on.

erikdarlingdata#1911 - Lite's MCP get_alert_settings did not report cpu.mode at all.
Reporting the raw enum name would have traded erikdarlingdata#1895's key-level mismatch for
a value-level one, since Darling emits "sql"/"total" and validates writes
against exactly those. Darling's vocabulary wins as the older public
surface; the cross-app literals are pinned by reading Darling's own source,
because asserting Lite's constants alone would keep passing the day Darling
changed.

Verified live against PostgreSQL 18.4 + TimescaleDB 2.28.1 (4002 passed, 10
skipped - the gated live suite genuinely ran). Every new pin red-watched,
including the live one. The erikdarlingdata#1902 ratchet correctly caught the new live test
using the old teardown idiom; converted to LiveStoreCleanup rather than
raising the ceiling.

Filed rather than folded in: erikdarlingdata#1921 (the item-timeline overlay still plots at
collection_time while the bars it annotates are keyed on the interval start -
a product decision about what the overlay is for) and erikdarlingdata#1922 (the live
fixture keeps using a connection that CREATE EXTENSION timescaledb killed,
masking the real cause).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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