diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index c11f2c4ea..cadea5989 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -120,6 +120,12 @@ overall-health summary is degraded or not fully verified. The green healthy empty state belongs only to an actually healthy Patrol summary, while degraded coverage or paused-runtime states must surface the governing warning/error copy through `frontend-modern/src/utils/patrolEmptyStatePresentation.ts`. +That degraded empty-state copy must also interpret the finding state rather +than simply replaying the primary assessment sentence verbatim: when coverage +is incomplete, the findings panel should tell the operator that Patrol has not +surfaced active findings but that this is not a full all-clear, so the page +does not duplicate the summary prediction as if it were a second independent +status surface. The Patrol summary surface must follow that same hierarchy. The primary summary headline in `frontend-modern/src/features/patrol/PatrolIntelligenceSummary.tsx` should state Patrol's current assessment first, such as verified healthy, diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx index ea3400a89..15985b191 100644 --- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx +++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx @@ -818,6 +818,11 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.queryByText('No issues found')).not.toBeInTheDocument(); expect(screen.queryByText('Partial verification')).not.toBeInTheDocument(); + expect( + screen.getAllByText( + 'Patrol coverage is incomplete: recent activity was limited to scoped runs and ended with errors, so overall health is not fully verified.', + ), + ).toHaveLength(1); }); it('treats a selected zero-finding run as an empty snapshot and uses effective scope ids', async () => { diff --git a/frontend-modern/src/utils/__tests__/patrolEmptyStatePresentation.test.ts b/frontend-modern/src/utils/__tests__/patrolEmptyStatePresentation.test.ts index a55abac34..5a21e2b31 100644 --- a/frontend-modern/src/utils/__tests__/patrolEmptyStatePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/patrolEmptyStatePresentation.test.ts @@ -59,11 +59,38 @@ describe('patrolEmptyStatePresentation', () => { }), ).toEqual({ title: 'No active findings', - body: 'Patrol coverage is incomplete: recent activity was limited to scoped runs and ended with errors, so overall health is not fully verified.', + body: 'Patrol has not surfaced active findings, but coverage is incomplete, so this is not a full all-clear.', tone: 'warning', }); }); + it('uses an attention-focused empty state when patrol health is degraded for non-coverage reasons', () => { + expect( + getPatrolFindingsEmptyState({ + filter: 'active', + overallHealth: { + score: 45, + grade: 'D', + trend: 'declining', + factors: [ + { + name: 'Critical unresolved risk', + impact: -0.55, + description: 'Recent Patrol evidence indicates unresolved infrastructure risk.', + category: 'findings', + }, + ], + prediction: 'Critical infrastructure risk still requires attention.', + }, + runtimeState: 'active', + }), + ).toEqual({ + title: 'No active findings', + body: 'Patrol has not surfaced active findings, but the overall Patrol assessment still needs attention.', + tone: 'error', + }); + }); + it('returns the patrol runtime explanation when the runtime is blocked', () => { expect( getPatrolFindingsEmptyState({ diff --git a/frontend-modern/src/utils/patrolEmptyStatePresentation.ts b/frontend-modern/src/utils/patrolEmptyStatePresentation.ts index f8c0ed892..bdde8839b 100644 --- a/frontend-modern/src/utils/patrolEmptyStatePresentation.ts +++ b/frontend-modern/src/utils/patrolEmptyStatePresentation.ts @@ -60,6 +60,10 @@ export interface PatrolFindingsEmptyStateCopy { } const HEALTHY_PATROL_EMPTY_STATE_BODY = 'Your infrastructure looks healthy!'; +const DEGRADED_COVERAGE_EMPTY_STATE_BODY = + 'Patrol has not surfaced active findings, but coverage is incomplete, so this is not a full all-clear.'; +const DEGRADED_HEALTH_EMPTY_STATE_BODY = + 'Patrol has not surfaced active findings, but the overall Patrol assessment still needs attention.'; function getHealthDegradedTone(overallHealth: IntelligenceHealthScore): SemanticTone { return overallHealth.grade === 'D' || overallHealth.grade === 'F' ? 'error' : 'warning'; @@ -77,6 +81,14 @@ function shouldSuppressHealthyEmptyState(overallHealth: IntelligenceHealthScore return overallHealth.grade !== 'A'; } +function getDegradedPatrolEmptyStateBody(overallHealth: IntelligenceHealthScore): string { + if (overallHealth.factors.some((factor) => factor.category === 'coverage')) { + return DEGRADED_COVERAGE_EMPTY_STATE_BODY; + } + + return DEGRADED_HEALTH_EMPTY_STATE_BODY; +} + export function getPatrolFindingsEmptyState(args: { filter: FindingsFilter; overallHealth?: IntelligenceHealthScore; @@ -106,9 +118,7 @@ export function getPatrolFindingsEmptyState(args: { if (shouldSuppressHealthyEmptyState(args.overallHealth)) { return { title: 'No active findings', - body: - args.overallHealth?.prediction?.trim() || - 'Patrol has not surfaced active findings, but overall infrastructure health is not fully verified.', + body: getDegradedPatrolEmptyStateBody(args.overallHealth!), tone: getHealthDegradedTone(args.overallHealth!), }; }