Files
AutopilotMonitor/rules
Oliver Kieselbach dff5ba65f4 fix(rules): sunset = delete, with cross-tenant orphan RuleState GC
Session 080edee9 follow-up — the customer screenshots showed ANALYZE-CORR-001
("Slow Network") and ANALYZE-APP-009 ("App Download Stalled") still firing
on a clean re-analyze, despite my previous fix shipping them as
enabled:false in the JSON. Investigation via direct DB query:

- AnalyzeRules/global rows for both: Enabled=false ✓ (version-bump landed)
- RuleStates/{tenant} rows for both: Enabled=true ✗ (orphan overrides
  from the user toggling them in the portal UI on 2026-03-09)

[AnalyzeRuleService.cs:60](src/Backend/AutopilotMonitor.Functions/Services/AnalyzeRuleService.cs#L60)
overlays per-tenant RuleState on top of the global Enabled, so the orphans
kept resurrecting the rules. No reseed path was cleaning them up.

The right semantic is "sunset = remove from catalog", not "ship as
enabled:false and hope nobody overrides". Three coordinated changes:

1. Both sunset rules deleted from the rules/analyze/ source tree (they
   never had a working condition shape — CORR-001 didn't actually check
   download speeds, APP-009 matched the V2 termination handler's
   post-mortem-synthetic download_progress:failed event).

2. New IRuleRepository.DeleteRuleStatesForRuleIdAcrossTenantsAsync
   cross-partition cleanup primitive — scans RuleStates by RowKey, deletes
   matching rows across every tenant partition.

3. Both reseed paths now do orphan-state GC:
   - AnalyzeRuleService.EnsureBuiltInRulesSeededAsync (on-startup):
     detects rules that exist in DB as built-in but aren't in
     BuiltInAnalyzeRules.GetAll() anymore → deletes the global row +
     fans out the per-tenant RuleState GC.
   - AnalyzeRuleService.ReseedBuiltInRulesAsync (delete+reimport): same
     diff-based detection on the pre-delete snapshot.
   - ReseedFromGitHubFunction.ReseedAnalyzeAsync (admin "Reseed from
     GitHub" button — the path the user actually clicks): mirrored
     identical logic so every reseed flavor leaves the same DB invariant.

4. ReseedBuiltInRulesAsync return shape extended to
   (deleted, written, orphanStatesGcd) so the admin UI can surface the
   cleanup count to operators.

Coverage:
- RuleEngineDetectionFailureTests: the two version-bump assertions for
  CORR-001/APP-009 are replaced with `DoesNotContain` against the
  built-in catalog (sunset semantic).
- AnalyzeRuleServiceSunsetTests (new, 3 tests): pins EnsureSeed orphan-GC
  invariant, asserts live-rule states are NOT touched, and pins the
  ReseedBuiltIn (deleted, written, orphanStatesGcd) tuple shape.

After deploy + a single GetAllRulesForTenantAsync call (any analyze run
triggers it), the orphan RuleStates for sunset rules are automatically
cleaned. The user's tenant 5ca2b350-… will see CORR-001 + APP-009
disappear from the Rules UI on the next analyze cycle.

Backend 2073/2073.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 16:31:32 +02:00
..
2026-03-17 22:43:17 +01:00