Clarify patrol supporting context evidence

This commit is contained in:
rcourtman
2026-04-15 11:03:18 +01:00
parent d0b28ef994
commit 41ac0c1ad2
10 changed files with 180 additions and 28 deletions
@@ -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
@@ -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.
@@ -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.
@@ -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.
@@ -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<ResourcePolicySummaryProps> = (pro
<h3 class="text-sm font-semibold text-base-content">
{props.title ?? 'Data Governance'}
</h3>
<p class="mt-1 text-xs text-muted">{value().total_resources} governed resources</p>
<Show when={props.subtitle}>
{(subtitle) => <p class="mt-1 text-xs text-muted">{subtitle()}</p>}
</Show>
<p class="mt-1 text-xs text-muted">
{value().total_resources} {props.resourceCountLabel ?? 'governed resources'}
</p>
</div>
</div>
@@ -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(() => (
<ResourcePolicySummary
title="Policy coverage"
subtitle="Coverage posture for policy-covered resources."
resourceCountLabel="policy-covered resources"
posture={{
total_resources: 4,
sensitivity_counts: {},
routing_counts: {},
}}
/>
));
expect(screen.getByText('Policy coverage')).toBeInTheDocument();
expect(screen.getByText('Coverage posture for policy-covered resources.')).toBeInTheDocument();
expect(screen.getByText('4 policy-covered resources')).toBeInTheDocument();
});
});
@@ -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
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted">
Supporting context
{PATROL_SUPPORTING_CONTEXT_TITLE}
</p>
<p class="mt-1 text-sm text-muted">
Recent changes, learned correlations, and policy coverage that may explain
findings or incomplete verification.
{PATROL_SUPPORTING_CONTEXT_DESCRIPTION}
</p>
<Show when={state.investigationContextSummary()}>
<p class="mt-1 text-xs text-base-content">{state.investigationContextSummary()}</p>
@@ -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())}
</button>
</div>
<Show when={state.showInvestigationContext()}>
<div class="mt-4 grid gap-4 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<Show when={state.recentChangeCount() > 0}>
<ResourceChangeSummary
class="space-y-0"
title="Recent changes"
subtitle="Last 24 hours"
changes={state.intelligenceSummary()?.recent_changes}
maxChanges={3}
compact
/>
</Show>
<div class="mt-4 space-y-4">
<div class="rounded-md border border-border-subtle bg-base px-3 py-2">
<p class="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted">
{PATROL_SUPPORTING_CONTEXT_EVIDENCE_LABEL}
</p>
<p class="mt-1 text-xs text-muted">{PATROL_SUPPORTING_CONTEXT_EVIDENCE_NOTE}</p>
</div>
<div class="space-y-4">
<Show when={state.correlations().length > 0}>
<ResourceCorrelationSummary
title="Learned correlations"
correlations={state.correlations()}
summaryText={`${state.correlationTotal()} total`}
<div class="grid gap-4 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
<Show when={state.recentChangeCount() > 0}>
<ResourceChangeSummary
class="space-y-0"
title="Recent changes"
subtitle={PATROL_SUPPORTING_CONTEXT_CHANGE_SUBTITLE}
changes={state.intelligenceSummary()?.recent_changes}
maxChanges={3}
compact
/>
</Show>
<ResourcePolicySummary posture={state.policyPosture()} title="Policy coverage" />
<div class="space-y-4">
<Show when={state.correlations().length > 0}>
<ResourceCorrelationSummary
title="Learned correlations"
correlations={state.correlations()}
summaryText={getPatrolSupportingContextCorrelationSummary(
state.correlationTotal(),
)}
/>
</Show>
<ResourcePolicySummary
posture={state.policyPosture()}
title="Policy coverage"
subtitle={PATROL_SUPPORTING_CONTEXT_POLICY_SUBTITLE}
resourceCountLabel="policy-covered resources"
/>
</div>
</div>
</div>
</Show>
@@ -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',
);
});
});
@@ -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`;
}
@@ -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',
});