mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-23 11:46:28 +00:00
Add FindingsStore.GetTrustSummary snapshot for trust metrics
Adds FindingsTrustSummary struct and GetTrustSummary() method that walks the in-memory findings store and returns a snapshot of how currently-tracked findings have resolved: tracked, currently-active, resolved (auto-resolved subset), fix-verified vs fix-failed, dismissed broken out by reason (noise / expected / later), suppressed, and regressed-at-least-once. This is the data layer for the user's "trust metrics" arc — concrete counts an operator can use to answer "do I trust Patrol?" Operators see fix-verified and dismissed-as-noise grow over time as Patrol's analysis gets sharper. The summary is intentionally a snapshot, not lifetime totals; once findings are cleaned up they no longer contribute, so the AutoResolved counter (which includes both Resolve(auto=true) and UpdateInvestigationOutcome(fix_verified) paths) is a current-state distribution, not a historical aggregate. The struct doc string is explicit about this so downstream surfaces do not misframe it. Adds a unit test covering each bucket (active, auto-resolved, fix-verified, fix-failed, dismissed-as-noise, dismissed-as-expected, regressed) with a fixture that exercises the real lifecycle methods rather than mutating the store directly. Updates the ai-runtime contract to pin the snapshot semantics and the AutoResolved-path union.
This commit is contained in:
@@ -288,6 +288,18 @@ runtime cost control, and shared AI transport surfaces.
|
||||
and to leave `impact` empty rather than fabricate one when the
|
||||
consequence is genuinely unknown; the runtime must not synthesize a
|
||||
default in that case.
|
||||
`FindingsStore.GetTrustSummary` returns a snapshot of how currently
|
||||
tracked findings have resolved (tracked, currently-active, resolved,
|
||||
auto-resolved, fix-verified, fix-failed, dismissed-as-noise,
|
||||
dismissed-as-expected, dismissed-as-later, suppressed,
|
||||
regressed-at-least-once). It is the data layer for trust metrics on
|
||||
operator surfaces. The summary is intentionally a snapshot of the
|
||||
in-memory store, not lifetime totals; once findings are cleaned up
|
||||
they no longer contribute. Downstream surfaces must frame the
|
||||
counts as current-state distribution rather than historical
|
||||
aggregates, and the AutoResolved bucket includes both the
|
||||
`Resolve(auto=true)` path and the
|
||||
`UpdateInvestigationOutcome(fix_verified)` path.
|
||||
Findings carry a `previous_resolved_fix_summary` field as
|
||||
operational memory across regressions: when a finding that had a
|
||||
resolved investigation with a proposed fix is re-detected,
|
||||
|
||||
@@ -1737,11 +1737,77 @@ type FindingsSummary struct {
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// FindingsTrustSummary is a snapshot of how the currently-tracked findings
|
||||
// have resolved over time. It answers the user-facing "do I trust Patrol?"
|
||||
// question with concrete counts: how many were auto-resolved, how many were
|
||||
// fix-verified, how many were dismissed as noise versus expected behavior.
|
||||
//
|
||||
// This is a snapshot of the in-memory store; it does not include historical
|
||||
// findings that have been cleaned up. Treat the counts as "current state
|
||||
// distribution of tracked findings," not lifetime totals.
|
||||
type FindingsTrustSummary struct {
|
||||
Tracked int `json:"tracked"`
|
||||
CurrentlyActive int `json:"currently_active"`
|
||||
Resolved int `json:"resolved"`
|
||||
AutoResolved int `json:"auto_resolved"`
|
||||
FixVerified int `json:"fix_verified"`
|
||||
FixFailed int `json:"fix_failed"`
|
||||
DismissedAsNoise int `json:"dismissed_as_noise"` // dismissed_reason=not_an_issue
|
||||
DismissedAsExpected int `json:"dismissed_as_expected"` // dismissed_reason=expected_behavior
|
||||
DismissedAsLater int `json:"dismissed_as_later"` // dismissed_reason=will_fix_later
|
||||
Suppressed int `json:"suppressed"`
|
||||
RegressedAtLeastOnce int `json:"regressed_at_least_once"`
|
||||
}
|
||||
|
||||
// HasIssues returns true if there are any warning or critical findings
|
||||
func (s FindingsSummary) HasIssues() bool {
|
||||
return s.Critical > 0 || s.Warning > 0
|
||||
}
|
||||
|
||||
// GetTrustSummary returns a snapshot of how currently-tracked findings have
|
||||
// resolved. See FindingsTrustSummary for caveats on snapshot vs lifetime
|
||||
// totals.
|
||||
func (s *FindingsStore) GetTrustSummary() FindingsTrustSummary {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
var summary FindingsTrustSummary
|
||||
for _, f := range s.findings {
|
||||
summary.Tracked++
|
||||
if f.IsActive() {
|
||||
summary.CurrentlyActive++
|
||||
}
|
||||
if f.ResolvedAt != nil {
|
||||
summary.Resolved++
|
||||
if f.AutoResolved {
|
||||
summary.AutoResolved++
|
||||
}
|
||||
}
|
||||
if f.Suppressed {
|
||||
summary.Suppressed++
|
||||
}
|
||||
if f.RegressionCount > 0 {
|
||||
summary.RegressedAtLeastOnce++
|
||||
}
|
||||
switch f.InvestigationOutcome {
|
||||
case string(aicontracts.OutcomeFixVerified):
|
||||
summary.FixVerified++
|
||||
case string(aicontracts.OutcomeFixFailed),
|
||||
string(aicontracts.OutcomeFixVerificationFailed):
|
||||
summary.FixFailed++
|
||||
}
|
||||
switch f.DismissedReason {
|
||||
case "not_an_issue":
|
||||
summary.DismissedAsNoise++
|
||||
case "expected_behavior":
|
||||
summary.DismissedAsExpected++
|
||||
case "will_fix_later":
|
||||
summary.DismissedAsLater++
|
||||
}
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
// IsHealthy returns true if there are no watch, warning, or critical findings
|
||||
func (s FindingsSummary) IsHealthy() bool {
|
||||
return s.Critical == 0 && s.Warning == 0 && s.Watch == 0
|
||||
|
||||
@@ -555,6 +555,91 @@ func TestFindingsStore_Add_UpdateExisting(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindingsStore_GetTrustSummary covers the snapshot counts that answer
|
||||
// the operator-facing "do I trust Patrol?" question. It verifies the bucket
|
||||
// boundaries for each finding outcome the summary tracks.
|
||||
func TestFindingsStore_GetTrustSummary(t *testing.T) {
|
||||
store := NewFindingsStore()
|
||||
|
||||
// Active finding (no resolution, no dismissal)
|
||||
store.Add(&Finding{
|
||||
ID: "f-active", ResourceID: "r1", Severity: FindingSeverityWarning,
|
||||
Category: FindingCategoryReliability, Title: "Active",
|
||||
})
|
||||
|
||||
// Auto-resolved finding (resolved + auto_resolved=true)
|
||||
store.Add(&Finding{
|
||||
ID: "f-auto", ResourceID: "r2", Severity: FindingSeverityWarning,
|
||||
Category: FindingCategoryReliability, Title: "Auto",
|
||||
})
|
||||
store.Resolve("f-auto", true)
|
||||
|
||||
// Fix-verified finding (investigation_outcome = fix_verified)
|
||||
store.Add(&Finding{
|
||||
ID: "f-verified", ResourceID: "r3", Severity: FindingSeverityWarning,
|
||||
Category: FindingCategoryReliability, Title: "Verified",
|
||||
})
|
||||
store.UpdateInvestigationOutcome("f-verified", string(aicontracts.OutcomeFixVerified))
|
||||
|
||||
// Fix-failed finding
|
||||
store.Add(&Finding{
|
||||
ID: "f-failed", ResourceID: "r4", Severity: FindingSeverityWarning,
|
||||
Category: FindingCategoryReliability, Title: "Failed",
|
||||
})
|
||||
store.UpdateInvestigationOutcome("f-failed", string(aicontracts.OutcomeFixFailed))
|
||||
|
||||
// Dismissed as noise
|
||||
store.Add(&Finding{
|
||||
ID: "f-noise", ResourceID: "r5", Severity: FindingSeverityWarning,
|
||||
Category: FindingCategoryReliability, Title: "Noise",
|
||||
})
|
||||
store.Dismiss("f-noise", "not_an_issue", "")
|
||||
|
||||
// Dismissed as expected
|
||||
store.Add(&Finding{
|
||||
ID: "f-expected", ResourceID: "r6", Severity: FindingSeverityWarning,
|
||||
Category: FindingCategoryReliability, Title: "Expected",
|
||||
})
|
||||
store.Dismiss("f-expected", "expected_behavior", "Maintenance window")
|
||||
|
||||
// Regressed at least once: setup via the regression branch
|
||||
store.Add(&Finding{
|
||||
ID: "f-regressed", ResourceID: "r7", Severity: FindingSeverityWarning,
|
||||
Category: FindingCategoryReliability, Title: "Regressed",
|
||||
})
|
||||
store.Resolve("f-regressed", true)
|
||||
store.Add(&Finding{
|
||||
ID: "f-regressed", ResourceID: "r7", Severity: FindingSeverityWarning,
|
||||
Category: FindingCategoryReliability, Title: "Regressed",
|
||||
})
|
||||
|
||||
got := store.GetTrustSummary()
|
||||
if got.Tracked != 7 {
|
||||
t.Errorf("Tracked = %d, want 7", got.Tracked)
|
||||
}
|
||||
// AutoResolved counts findings that were resolved without operator action.
|
||||
// Both Resolve(auto=true) and UpdateInvestigationOutcome(fix_verified) set
|
||||
// the AutoResolved flag, so f-auto and f-verified both contribute.
|
||||
if got.AutoResolved != 2 {
|
||||
t.Errorf("AutoResolved = %d, want 2", got.AutoResolved)
|
||||
}
|
||||
if got.FixVerified != 1 {
|
||||
t.Errorf("FixVerified = %d, want 1", got.FixVerified)
|
||||
}
|
||||
if got.FixFailed != 1 {
|
||||
t.Errorf("FixFailed = %d, want 1", got.FixFailed)
|
||||
}
|
||||
if got.DismissedAsNoise != 1 {
|
||||
t.Errorf("DismissedAsNoise = %d, want 1", got.DismissedAsNoise)
|
||||
}
|
||||
if got.DismissedAsExpected != 1 {
|
||||
t.Errorf("DismissedAsExpected = %d, want 1", got.DismissedAsExpected)
|
||||
}
|
||||
if got.RegressedAtLeastOnce != 1 {
|
||||
t.Errorf("RegressedAtLeastOnce = %d, want 1", got.RegressedAtLeastOnce)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFindingsStore_Add_CapturesPreviousResolvedFixOnRegression covers the
|
||||
// regression branch: when a finding that had a resolved investigation with a
|
||||
// proposed fix is re-detected, the prior fix description must be preserved on
|
||||
|
||||
Reference in New Issue
Block a user