diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 67d601abe..6ca23bd20 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -380,7 +380,10 @@ when the disabled candidate no longer counts toward monitored-system capacity. Patrol workflow on otherwise healthy fully verified states, and the disclosure copy must explicitly tell operators that findings and run history are the Patrol verification evidence while those supporting cards only add - explanation from the same governed payload family + explanation from the same governed payload family, and the Patrol-owned + helper `frontend-modern/src/features/patrol/patrolSupportingContextPresentation.ts` + must keep that transport-derived trust copy aligned across the workspace + disclosure rather than letting page shells invent local wording 9. Keep AI settings setup transport vendor-neutral: `/api/settings/ai/update` must accept provider credentials or base URLs without a baked vendor model ID, resolve the effective BYOK `model` through the canonical runtime diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 92768efac..7999df152 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -288,6 +288,7 @@ work extends shared components instead of creating new local variants. `frontend-modern/src/features/patrol/PatrolIntelligenceBanners.tsx`, `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts`, `frontend-modern/src/features/patrol/patrolInvestigationContextModel.ts`, + `frontend-modern/src/features/patrol/patrolSupportingContextPresentation.ts`, and `frontend-modern/src/components/patrol/PatrolStatusBar.tsx` must keep Patrol assessment, verification, and findings primary; surface recent changes, learned correlations, and policy coverage only as explicitly @@ -299,7 +300,10 @@ work extends shared components instead of creating new local variants. `frontend-modern/src/App.tsx` and `frontend-modern/src/AppLayout.tsx` must likewise expose `/patrol` as the canonical route and navigation target, keeping legacy `/ai` entry points as thin compatibility redirects rather - than a second Patrol-branded primary route. + than a second Patrol-branded primary route. The Patrol-owned supporting-context + presenter must also keep the disclosure toggle plus evidence-boundary copy + centralized instead of letting the workspace reintroduce inline shell-local + trust wording. 19. Keep the shared `system-ai` settings shell product-first. `frontend-modern/src/components/Settings/AISettings.tsx`, `frontend-modern/src/components/Settings/settingsHeaderMeta.ts`, @@ -1630,7 +1634,9 @@ explanation; healthy fully verified Patrol states must not advertise that supporting evidence as a peer workflow. When that disclosure expands, the workspace must explicitly label findings and run history as Patrol verification evidence and frame the supporting cards as explanatory context rather than as a -fresh Patrol result. +fresh Patrol result. `frontend-modern/src/features/patrol/patrolSupportingContextPresentation.ts` +must own that disclosure copy and toggle wording so the Patrol workspace does +not regress into inline shell-local trust language. Shared primitive consumers that split status-dot tone and status-text tone must now keep both values routed through the same exported presentation helper. diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 82154fffc..e04d98260 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -317,6 +317,10 @@ The same hierarchy applies to supporting context. Correlations, recent changes, and policy posture are secondary evidence for deeper investigation, so the supporting-context disclosure belongs beneath the primary findings/history workspace rather than inside the assessment card itself. +When that disclosure is expanded, the page must explicitly tell operators that +findings and run history are Patrol verification evidence, while recent +changes, learned correlations, and policy posture are explanatory context and +do not count as a fresh Patrol run. When Patrol is healthy and fully verified, that supporting-context disclosure should stay out of the main page flow instead of advertising a second parallel Patrol workflow with nothing active to explain. diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index c8fd4a9a8..1384e33f3 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -212,6 +212,12 @@ assembly branch. `Analysis`, `Analysis Reasoning`, and `Safe Summary` rather than reviving generic `AI` or `AI-Safe` branding inside the resource drawer or discovery shell. +14. Keep shared policy-posture framing on the unified-resource card owner. + `frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx` + may accept caller-owned subtitle or resource-count wording when Patrol or + another shared surface needs to explain how the same governed policy counts + should be read, but those framing lines must extend the shared card API + rather than spawning page-local policy summary shells. ## Forbidden Paths @@ -1403,7 +1409,10 @@ sensitivity, routing, and redaction labels and aggregate count summaries. That shared policy card also owns caller-supplied framing lines such as subtitle and resource-count wording, so Patrol or other shared surfaces may clarify whether the same governed counts read as policy-covered-resource -context without rebuilding their own policy-posture card shell. +context without rebuilding their own policy-posture card shell. New +shared-surface framing needs such as Patrol's `policy-covered resources` +count label or explanatory subtitle should extend that card API instead of +forking a second page-local policy summary. Future correlation or policy-posture wording changes should extend those unified-resource owners instead of drifting into page-local loops in AI, Patrol, or infrastructure surfaces. diff --git a/frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx b/frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx index ea5edfe95..2f3391d5b 100644 --- a/frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourcePolicySummary.tsx @@ -9,6 +9,8 @@ import { interface ResourcePolicySummaryProps { posture?: IntelligencePolicyPostureSummary | null; title?: string; + subtitle?: string; + resourceCountLabel?: string; class?: string; } @@ -28,7 +30,12 @@ export const ResourcePolicySummary: Component = (pro

{props.title ?? 'Data Governance'}

-

{value().total_resources} governed resources

+ + {(subtitle) =>

{subtitle()}

} +
+

+ {value().total_resources} {props.resourceCountLabel ?? 'governed resources'} +

diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourcePolicySummary.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourcePolicySummary.test.tsx index 20e26c07f..7b7eaefe5 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourcePolicySummary.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourcePolicySummary.test.tsx @@ -35,4 +35,23 @@ describe('ResourcePolicySummary', () => { expect(screen.getByText('Hostname 3')).toBeInTheDocument(); expect(screen.getByText('IP Address 1')).toBeInTheDocument(); }); + + it('supports caller-owned subtitle and resource count wording', () => { + render(() => ( + + )); + + expect(screen.getByText('Policy coverage')).toBeInTheDocument(); + expect(screen.getByText('Coverage posture for policy-covered resources.')).toBeInTheDocument(); + expect(screen.getByText('4 policy-covered resources')).toBeInTheDocument(); + }); }); diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx index a9f1209a8..1aa27838e 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceWorkspace.tsx @@ -7,6 +7,16 @@ import { formatTriggerReason } from '@/utils/patrolFormat'; import { ResourcePolicySummary } from '@/components/Infrastructure/ResourcePolicySummary'; import { ResourceCorrelationSummary } from '@/components/Infrastructure/ResourceCorrelationSummary'; import { ResourceChangeSummary } from '@/components/Infrastructure/ResourceChangeSummary'; +import { + getPatrolSupportingContextCorrelationSummary, + getPatrolSupportingContextToggleLabel, + PATROL_SUPPORTING_CONTEXT_CHANGE_SUBTITLE, + PATROL_SUPPORTING_CONTEXT_DESCRIPTION, + PATROL_SUPPORTING_CONTEXT_EVIDENCE_LABEL, + PATROL_SUPPORTING_CONTEXT_EVIDENCE_NOTE, + PATROL_SUPPORTING_CONTEXT_POLICY_SUBTITLE, + PATROL_SUPPORTING_CONTEXT_TITLE, +} from './patrolSupportingContextPresentation'; import type { PatrolIntelligenceState } from './usePatrolIntelligenceState'; export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceState }) { @@ -128,11 +138,10 @@ export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceSt

- Supporting context + {PATROL_SUPPORTING_CONTEXT_TITLE}

- Recent changes, learned correlations, and policy coverage that may explain - findings or incomplete verification. + {PATROL_SUPPORTING_CONTEXT_DESCRIPTION}

{state.investigationContextSummary()}

@@ -144,33 +153,49 @@ export function PatrolIntelligenceWorkspace(props: { state: PatrolIntelligenceSt onClick={() => state.setShowInvestigationContext((value) => !value)} class="inline-flex items-center rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover" > - {state.showInvestigationContext() ? 'Hide supporting context' : 'View supporting context'} + {getPatrolSupportingContextToggleLabel(state.showInvestigationContext())}
-
- 0}> - - +
+
+

+ {PATROL_SUPPORTING_CONTEXT_EVIDENCE_LABEL} +

+

{PATROL_SUPPORTING_CONTEXT_EVIDENCE_NOTE}

+
-
- 0}> - + 0}> + - +
+ 0}> + + + + +
diff --git a/frontend-modern/src/features/patrol/__tests__/patrolSupportingContextPresentation.test.ts b/frontend-modern/src/features/patrol/__tests__/patrolSupportingContextPresentation.test.ts new file mode 100644 index 000000000..d324f4bf6 --- /dev/null +++ b/frontend-modern/src/features/patrol/__tests__/patrolSupportingContextPresentation.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { + getPatrolSupportingContextCorrelationSummary, + getPatrolSupportingContextToggleLabel, + PATROL_SUPPORTING_CONTEXT_CHANGE_SUBTITLE, + PATROL_SUPPORTING_CONTEXT_DESCRIPTION, + PATROL_SUPPORTING_CONTEXT_EVIDENCE_LABEL, + PATROL_SUPPORTING_CONTEXT_EVIDENCE_NOTE, + PATROL_SUPPORTING_CONTEXT_POLICY_SUBTITLE, + PATROL_SUPPORTING_CONTEXT_TITLE, +} from '../patrolSupportingContextPresentation'; + +describe('patrolSupportingContextPresentation', () => { + it('exports the canonical supporting-context trust copy', () => { + expect(PATROL_SUPPORTING_CONTEXT_TITLE).toBe('Supporting context'); + expect(PATROL_SUPPORTING_CONTEXT_DESCRIPTION).toBe( + 'Recent changes, learned correlations, and policy coverage that may explain findings or incomplete verification.', + ); + expect(PATROL_SUPPORTING_CONTEXT_EVIDENCE_LABEL).toBe('How to read this'); + expect(PATROL_SUPPORTING_CONTEXT_EVIDENCE_NOTE).toBe( + 'Findings and run history are Patrol verification evidence. The cards below add explanatory context and do not count as a fresh full patrol.', + ); + expect(PATROL_SUPPORTING_CONTEXT_CHANGE_SUBTITLE).toBe( + 'Observed from the canonical timeline in the last 24 hours.', + ); + expect(PATROL_SUPPORTING_CONTEXT_POLICY_SUBTITLE).toBe( + 'Coverage posture for policy-covered resources.', + ); + }); + + it('builds canonical supporting-context action labels and correlation summaries', () => { + expect(getPatrolSupportingContextToggleLabel(false)).toBe('View supporting context'); + expect(getPatrolSupportingContextToggleLabel(true)).toBe('Hide supporting context'); + expect(getPatrolSupportingContextCorrelationSummary(2)).toBe( + '2 learned patterns · explanatory context', + ); + expect(getPatrolSupportingContextCorrelationSummary(1)).toBe( + '1 learned pattern · explanatory context', + ); + expect(getPatrolSupportingContextCorrelationSummary(Number.NaN)).toBe( + 'Learned pattern context', + ); + }); +}); diff --git a/frontend-modern/src/features/patrol/patrolSupportingContextPresentation.ts b/frontend-modern/src/features/patrol/patrolSupportingContextPresentation.ts new file mode 100644 index 000000000..d0a922f3a --- /dev/null +++ b/frontend-modern/src/features/patrol/patrolSupportingContextPresentation.ts @@ -0,0 +1,21 @@ +export const PATROL_SUPPORTING_CONTEXT_TITLE = 'Supporting context'; +export const PATROL_SUPPORTING_CONTEXT_DESCRIPTION = + 'Recent changes, learned correlations, and policy coverage that may explain findings or incomplete verification.'; +export const PATROL_SUPPORTING_CONTEXT_EVIDENCE_LABEL = 'How to read this'; +export const PATROL_SUPPORTING_CONTEXT_EVIDENCE_NOTE = + 'Findings and run history are Patrol verification evidence. The cards below add explanatory context and do not count as a fresh full patrol.'; +export const PATROL_SUPPORTING_CONTEXT_CHANGE_SUBTITLE = + 'Observed from the canonical timeline in the last 24 hours.'; +export const PATROL_SUPPORTING_CONTEXT_POLICY_SUBTITLE = + 'Coverage posture for policy-covered resources.'; + +export function getPatrolSupportingContextToggleLabel(expanded: boolean) { + return expanded ? 'Hide supporting context' : 'View supporting context'; +} + +export function getPatrolSupportingContextCorrelationSummary(count: number) { + if (!Number.isFinite(count) || count <= 0) { + return 'Learned pattern context'; + } + return `${count} learned pattern${count === 1 ? '' : 's'} · explanatory context`; +} diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx index e5bef981c..cfe675588 100644 --- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx +++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx @@ -428,6 +428,11 @@ describe('AIIntelligence entitlement gating', () => { predictions_count: 0, recent_changes_count: 0, recent_changes: [], + policy_posture: { + total_resources: 3, + sensitivity_counts: {}, + routing_counts: {}, + }, learning: { resources_with_knowledge: 0, total_notes: 0, @@ -483,7 +488,15 @@ describe('AIIntelligence entitlement gating', () => { expect(screen.getByRole('heading', { name: 'Learned correlations' })).toBeInTheDocument(); }); - expect(screen.getByText('2 total')).toBeInTheDocument(); + expect(screen.getByText('How to read this')).toBeInTheDocument(); + expect( + screen.getByText( + 'Findings and run history are Patrol verification evidence. The cards below add explanatory context and do not count as a fresh full patrol.', + ), + ).toBeInTheDocument(); + expect(screen.getByText('2 learned patterns · explanatory context')).toBeInTheDocument(); + expect(screen.getByText('Coverage posture for policy-covered resources.')).toBeInTheDocument(); + expect(screen.getByText('3 policy-covered resources')).toBeInTheDocument(); const storage2Link = screen.getByRole('link', { name: 'Open source resource Storage 2 in Infrastructure', });