[pitboss/grind] deferred session-0022 (20260517T044708Z-e058)

This commit is contained in:
pitboss 2026-05-17 07:41:28 -05:00
parent 01eb67e1f9
commit f4793b0439
11 changed files with 559 additions and 20 deletions

View file

@ -0,0 +1,82 @@
import type { SurfaceEdge, SurfaceMap, SurfaceNode } from '@/api/types';
import type { GraphModel } from '../types';
const MAX_LABEL = 44;
const MAX_DETAIL = 48;
function truncate(value: string, max: number): string {
return value.length > max ? `${value.slice(0, max - 1)}` : value;
}
export const SURFACE_NODE_KIND: Record<SurfaceNode['node'], string> = {
entry_point: 'EntryPoint',
data_store: 'DataStore',
external_service: 'ExternalService',
dangerous_local: 'DangerousLocal',
};
function nodeTitle(node: SurfaceNode): string {
switch (node.node) {
case 'entry_point':
return `${node.method} ${node.route}`;
case 'data_store':
return `${node.kind}: ${node.label}`;
case 'external_service':
return `${node.kind}: ${node.label}`;
case 'dangerous_local':
return node.function_name;
}
}
function nodeDetail(node: SurfaceNode): string {
switch (node.node) {
case 'entry_point':
return `${node.framework} · ${node.handler_name}`;
case 'data_store':
return 'data store';
case 'external_service':
return 'external service';
case 'dangerous_local':
return `cap=0x${node.cap_bits.toString(16)}`;
}
}
function nodeLocation(node: SurfaceNode): { file: string; line: number } {
if (node.node === 'entry_point') return node.handler_location;
return node.location;
}
export function adaptSurfaceMap(data: SurfaceMap): GraphModel {
return {
kind: 'surface',
nodes: data.nodes.map((node, index) => {
const loc = nodeLocation(node);
const title = nodeTitle(node);
const detail = nodeDetail(node);
const searchText = [title, detail, loc.file].join(' ').toLowerCase();
const authBadge =
node.node === 'entry_point' && node.auth_required ? ['auth'] : undefined;
return {
key: String(index),
rawId: index,
label: truncate(title, MAX_LABEL),
kind: SURFACE_NODE_KIND[node.node],
detail: truncate(detail, MAX_DETAIL),
line: loc.line,
badges: authBadge,
metadata: {
surfaceKind: node.node,
node,
searchText,
},
};
}),
edges: data.edges.map((edge: SurfaceEdge, index) => ({
key: `surface:${edge.from}:${edge.to}:${edge.kind}:${index}`,
source: String(edge.from),
target: String(edge.to),
kind: edge.kind,
metadata: { ...edge },
})),
};
}

View file

@ -0,0 +1,123 @@
import { useMemo, useState } from 'react';
import type { SurfaceMap } from '@/api/types';
import { adaptSurfaceMap } from '../adapters/surface';
import { useElkLayout } from '../hooks/useElkLayout';
import {
collectSearchMatches,
extractNeighborhoodSubgraph,
} from '../reduction/neighborhood';
import { SigmaGraph } from '../rendering/sigma/SigmaGraph';
interface SurfaceGraphCanvasProps {
data: SurfaceMap;
selectedNodeId: number | null;
onSelectNode: (id: number) => void;
}
export function SurfaceGraphCanvas({
data,
selectedNodeId,
onSelectNode,
}: SurfaceGraphCanvasProps) {
const [searchQuery, setSearchQuery] = useState('');
const [neighborhoodOnly, setNeighborhoodOnly] = useState(false);
const [radius, setRadius] = useState(2);
const fullGraph = useMemo(() => adaptSurfaceMap(data), [data]);
const selectedNodeKey =
selectedNodeId == null ? null : String(selectedNodeId);
const matches = useMemo(
() => collectSearchMatches(fullGraph, searchQuery, 60),
[fullGraph, searchQuery],
);
const matchKeys = useMemo(
() => new Set(matches.map((node) => node.key)),
[matches],
);
const visibleGraph = useMemo(() => {
if (!neighborhoodOnly || !selectedNodeKey) return fullGraph;
return extractNeighborhoodSubgraph(fullGraph, selectedNodeKey, radius);
}, [fullGraph, neighborhoodOnly, radius, selectedNodeKey]);
const { graph, isLoading, error } = useElkLayout(visibleGraph);
if (error) {
return (
<div className="error-state">Failed to compute the surface layout.</div>
);
}
if (!graph) {
return <div className="loading">Preparing surface graph</div>;
}
const extras = (
<>
<label className="graph-toolbar-field">
<span>Search</span>
<input
className="graph-toolbar-input"
type="search"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder="Route, label, or path"
/>
</label>
<label className="graph-toolbar-field">
<span>Match</span>
<select
className="graph-toolbar-select"
value={selectedNodeKey ?? ''}
onChange={(event) => {
const next = event.target.value;
if (!next) return;
onSelectNode(Number(next));
}}
>
<option value="">Select</option>
{matches.map((match) => (
<option key={match.key} value={match.key}>
{match.label}
</option>
))}
</select>
</label>
<label className="graph-toolbar-check">
<input
type="checkbox"
checked={neighborhoodOnly}
onChange={(event) => setNeighborhoodOnly(event.target.checked)}
/>
<span>Neighbors only</span>
</label>
<label className="graph-toolbar-field graph-toolbar-field-compact">
<span>Radius</span>
<input
className="graph-toolbar-range"
type="range"
min="1"
max="4"
step="1"
value={radius}
disabled={!neighborhoodOnly}
onChange={(event) => setRadius(Number(event.target.value))}
/>
<strong>{radius}</strong>
</label>
</>
);
return (
<SigmaGraph
graph={graph}
viewKind="surface"
selectedNodeKey={selectedNodeKey}
onNodeClick={(key) => onSelectNode(Number(key))}
searchMatchKeys={matchKeys}
toolbarExtras={extras}
loading={isLoading}
/>
);
}

View file

@ -39,6 +39,14 @@ const PRESETS: Record<GraphViewKind, ElkLayoutPreset> = {
padding: 32,
edgeRouting: 'ORTHOGONAL',
},
surface: {
direction: 'RIGHT',
nodeSpacing: 44,
layerSpacing: 156,
edgeNodeSpacing: 28,
padding: 36,
edgeRouting: 'POLYLINE',
},
};
function measureNode(

View file

@ -31,6 +31,13 @@ const CONFIG: Record<GraphViewKind, TextLayoutConfig> = {
maxSecondaryLines: 2,
maxSublabelLines: 1,
},
surface: {
primaryChars: 32,
secondaryChars: 32,
maxPrimaryLines: 2,
maxSecondaryLines: 2,
maxSublabelLines: 1,
},
};
function normalizeWhitespace(value: string): string {

View file

@ -195,6 +195,94 @@ function cfgNodeStyle(
}
}
function surfaceNodeStyle(
type: string,
palette: GraphThemePalette,
): NodeStyle {
switch (type) {
case 'EntryPoint':
return {
fill: palette.success,
stroke: withAlpha(palette.success, 0.85),
textFill: '#ffffff',
secondaryFill: withAlpha('#ffffff', 0.78),
shape: 'double',
strokeWidth: 1.8,
accentFill: palette.accent,
neighborFill: withAlpha(palette.success, 0.75),
};
case 'DataStore':
return {
fill: palette.warning,
stroke: withAlpha(palette.warning, 0.85),
textFill: '#ffffff',
secondaryFill: withAlpha('#ffffff', 0.8),
shape: 'rect',
strokeWidth: 1.5,
accentFill: palette.accent,
neighborFill: withAlpha(palette.warning, 0.76),
};
case 'ExternalService':
return {
fill: palette.accent,
stroke: withAlpha(palette.accent, 0.82),
textFill: '#ffffff',
secondaryFill: withAlpha('#ffffff', 0.8),
shape: 'rect',
strokeWidth: 1.5,
accentFill: palette.accent,
neighborFill: palette.accentSoft,
};
case 'DangerousLocal':
return {
fill: palette.danger,
stroke: withAlpha(palette.danger, 0.86),
textFill: '#ffffff',
secondaryFill: withAlpha('#ffffff', 0.8),
shape: 'terminal',
strokeWidth: 1.7,
accentFill: palette.accent,
neighborFill: withAlpha(palette.danger, 0.75),
};
default:
return {
fill: withAlpha(palette.neutral, 0.92),
stroke: withAlpha(palette.neutral, 0.8),
textFill: '#ffffff',
secondaryFill: withAlpha('#ffffff', 0.78),
shape: 'rect',
strokeWidth: 1.2,
accentFill: palette.accent,
neighborFill: withAlpha(palette.neutralSoft, 0.88),
};
}
}
function surfaceEdgeStyle(type: string, palette: GraphThemePalette): EdgeStyle {
switch (type) {
case 'calls':
return { color: withAlpha(palette.textSecondary, 0.78), width: 1.4, dash: [] };
case 'reads_from':
return { color: palette.success, width: 1.5, dash: [] };
case 'writes_to':
return { color: palette.warning, width: 1.6, dash: [] };
case 'talks_to':
return { color: palette.accent, width: 1.4, dash: [] };
case 'reaches':
return { color: palette.danger, width: 1.7, dash: [] };
case 'triggers':
return { color: palette.success, width: 1.5, dash: [4, 3] };
case 'auth_required_on':
return { color: palette.textTertiary, width: 1.3, dash: [2, 4] };
default:
return {
color: withAlpha(palette.textTertiary, 0.78),
width: 1.3,
dash: [],
};
}
}
function callGraphNodeStyle(
palette: GraphThemePalette,
metadata?: GraphMetadata,
@ -221,9 +309,15 @@ export function getNodeStyle(
metadata?: GraphMetadata,
palette = FALLBACK_PALETTE,
): NodeStyle {
return graphKind === 'callgraph'
? callGraphNodeStyle(palette, metadata)
: cfgNodeStyle(type, palette, metadata);
switch (graphKind) {
case 'callgraph':
return callGraphNodeStyle(palette, metadata);
case 'surface':
return surfaceNodeStyle(type, palette);
case 'cfg':
default:
return cfgNodeStyle(type, palette, metadata);
}
}
export function getEdgeStyle(
@ -239,6 +333,10 @@ export function getEdgeStyle(
};
}
if (graphKind === 'surface') {
return surfaceEdgeStyle(type, palette);
}
switch (type) {
case 'True':
return { color: palette.success, width: 1.8, dash: [] };

View file

@ -1,4 +1,4 @@
export type GraphViewKind = 'callgraph' | 'cfg';
export type GraphViewKind = 'callgraph' | 'cfg' | 'surface';
export interface GraphPoint {
x: number;

View file

@ -4,6 +4,7 @@ import { LoadingState } from '../components/ui/LoadingState';
import { ErrorState } from '../components/ui/ErrorState';
import { EmptyState } from '../components/ui/EmptyState';
import { usePageTitle } from '../hooks/usePageTitle';
import { SurfaceGraphCanvas } from '../graph/components/SurfaceGraphCanvas';
import type {
SurfaceEdge,
SurfaceEdgeKind,
@ -182,6 +183,7 @@ function NeighborList({
}
type NodeKindFilter = 'all' | SurfaceNode['node'];
type SurfaceViewMode = 'list' | 'graph';
export function SurfacePage() {
usePageTitle('Surface');
@ -189,6 +191,7 @@ export function SurfacePage() {
const [selected, setSelected] = useState<number | null>(null);
const [filter, setFilter] = useState<NodeKindFilter>('all');
const [query, setQuery] = useState('');
const [viewMode, setViewMode] = useState<SurfaceViewMode>('list');
const visible = useMemo(() => {
if (!data) return [] as Array<{ node: SurfaceNode; index: number }>;
@ -233,11 +236,13 @@ export function SurfacePage() {
placeholder="Filter by name, label, or path"
onChange={(e) => setQuery(e.target.value)}
className="surface-filter-input"
disabled={viewMode === 'graph'}
/>
<select
value={filter}
onChange={(e) => setFilter(e.target.value as NodeKindFilter)}
className="surface-filter-select"
disabled={viewMode === 'graph'}
>
<option value="all">All node kinds</option>
<option value="entry_point">Entry points</option>
@ -245,23 +250,53 @@ export function SurfacePage() {
<option value="external_service">External services</option>
<option value="dangerous_local">Dangerous locals</option>
</select>
<div className="surface-view-toggle" role="tablist" aria-label="Surface view">
<button
type="button"
role="tab"
aria-selected={viewMode === 'list'}
className={`surface-view-toggle-button${viewMode === 'list' ? ' selected' : ''}`}
onClick={() => setViewMode('list')}
>
List
</button>
<button
type="button"
role="tab"
aria-selected={viewMode === 'graph'}
className={`surface-view-toggle-button${viewMode === 'graph' ? ' selected' : ''}`}
onClick={() => setViewMode('graph')}
>
Graph
</button>
</div>
</div>
<div className="surface-grid">
<div className="surface-node-list">
{visible.length === 0 ? (
<p className="surface-node-list-empty">No nodes match.</p>
) : (
visible.map(({ node, index }) => (
<NodeCard
key={index}
node={node}
index={index}
selected={selected === index}
onClick={() => setSelected(index)}
/>
))
)}
</div>
{viewMode === 'list' ? (
<div className="surface-node-list">
{visible.length === 0 ? (
<p className="surface-node-list-empty">No nodes match.</p>
) : (
visible.map(({ node, index }) => (
<NodeCard
key={index}
node={node}
index={index}
selected={selected === index}
onClick={() => setSelected(index)}
/>
))
)}
</div>
) : (
<div className="surface-graph-frame">
<SurfaceGraphCanvas
data={data}
selectedNodeId={selected}
onSelectNode={(id) => setSelected(id)}
/>
</div>
)}
<aside className="surface-sidebar">
<NeighborList map={data} index={selected} />
</aside>

View file

@ -8912,3 +8912,34 @@ input[type='checkbox'] {
font-size: var(--text-2xs);
color: var(--text-tertiary);
}
.surface-view-toggle {
display: inline-flex;
border: 1px solid var(--border);
border-radius: var(--radius-1);
overflow: hidden;
background: var(--surface-1);
}
.surface-view-toggle-button {
padding: var(--space-2) var(--space-3);
background: transparent;
border: 0;
color: var(--text-secondary);
cursor: pointer;
font-size: var(--text-xs);
}
.surface-view-toggle-button:not(:last-child) {
border-right: 1px solid var(--border);
}
.surface-view-toggle-button.selected {
background: var(--surface-2);
color: var(--text-primary);
font-weight: 600;
}
.surface-graph-frame {
position: relative;
min-height: 70vh;
border: 1px solid var(--border);
border-radius: var(--radius-2);
background: var(--surface-1);
overflow: hidden;
}

View file

@ -49,6 +49,29 @@ describe('getNodeStyle', () => {
const s = getNodeStyle('Call', 'callgraph', { isRecursive: true });
expect(s.fill).toBe('#5a5042');
});
it('returns a double shape for surface entry-point nodes', () => {
const s = getNodeStyle('EntryPoint', 'surface');
expect(s.shape).toBe('double');
expect(s.fill).toBe('#1c5c38');
});
it('returns a terminal shape for surface dangerous-local nodes', () => {
const s = getNodeStyle('DangerousLocal', 'surface');
expect(s.shape).toBe('terminal');
expect(s.fill).toBe('#9d2f25');
});
it('returns a warning fill for surface data-store nodes', () => {
const s = getNodeStyle('DataStore', 'surface');
expect(s.fill).toBe('#8c6310');
expect(s.shape).toBe('rect');
});
it('returns an accent fill for surface external-service nodes', () => {
const s = getNodeStyle('ExternalService', 'surface');
expect(s.fill).toBe('#0b3d2a');
});
});
describe('getEdgeStyle', () => {
@ -90,4 +113,26 @@ describe('getEdgeStyle', () => {
const s = getEdgeStyle('Call', 'callgraph');
expect(s.dash).toEqual([]);
});
it('returns a dashed style for surface auth_required_on edges', () => {
const s = getEdgeStyle('auth_required_on', 'surface');
expect(s.dash).toEqual([2, 4]);
});
it('returns a solid danger color for surface reaches edges', () => {
const s = getEdgeStyle('reaches', 'surface');
expect(s.color).toBe('#9d2f25');
expect(s.dash).toEqual([]);
});
it('returns a dashed success style for surface triggers edges', () => {
const s = getEdgeStyle('triggers', 'surface');
expect(s.dash).toEqual([4, 3]);
});
it('returns a fallback style for unknown surface edge kinds', () => {
const s = getEdgeStyle('mystery', 'surface');
expect(s.color).toContain('rgba');
expect(s.dash).toEqual([]);
});
});

View file

@ -0,0 +1,110 @@
import { describe, expect, it } from 'vitest';
import { adaptSurfaceMap, SURFACE_NODE_KIND } from '@/graph/adapters/surface';
import type { SurfaceMap } from '@/api/types';
const SAMPLE: SurfaceMap = {
nodes: [
{
node: 'entry_point',
location: { file: 'app.py', line: 10, col: 0 },
framework: 'flask',
method: 'POST',
route: '/api/run',
handler_name: 'run',
handler_location: { file: 'app.py', line: 12, col: 2 },
auth_required: false,
},
{
node: 'data_store',
location: { file: 'db.py', line: 40, col: 0 },
kind: 'sql',
label: 'orders',
},
{
node: 'external_service',
location: { file: 'client.py', line: 5, col: 0 },
kind: 'http_api',
label: 'github.com',
},
{
node: 'dangerous_local',
location: { file: 'app.py', line: 24, col: 4 },
function_name: 'run',
cap_bits: 0x400,
},
],
edges: [
{ from: 0, to: 3, kind: 'calls' },
{ from: 3, to: 1, kind: 'writes_to' },
{ from: 0, to: 2, kind: 'talks_to' },
],
};
describe('adaptSurfaceMap', () => {
it('produces a surface-kind GraphModel', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.kind).toBe('surface');
expect(model.nodes).toHaveLength(4);
expect(model.edges).toHaveLength(3);
});
it('keys nodes by index so SurfaceEdge.from/to map directly', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes.map((n) => n.key)).toEqual(['0', '1', '2', '3']);
expect(model.edges[0]?.source).toBe('0');
expect(model.edges[0]?.target).toBe('3');
});
it('maps each SurfaceNode kind to a distinct style discriminator', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes[0]?.kind).toBe(SURFACE_NODE_KIND.entry_point);
expect(model.nodes[1]?.kind).toBe(SURFACE_NODE_KIND.data_store);
expect(model.nodes[2]?.kind).toBe(SURFACE_NODE_KIND.external_service);
expect(model.nodes[3]?.kind).toBe(SURFACE_NODE_KIND.dangerous_local);
});
it('builds entry-point labels from method and route', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes[0]?.label).toBe('POST /api/run');
expect(model.nodes[0]?.detail).toBe('flask · run');
});
it('renders dangerous_local cap_bits as hex in detail', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes[3]?.detail).toBe('cap=0x400');
});
it('uses handler_location for entry_point line, location for others', () => {
const model = adaptSurfaceMap(SAMPLE);
expect(model.nodes[0]?.line).toBe(12);
expect(model.nodes[1]?.line).toBe(40);
});
it('emits an auth badge only for entry_points marked auth_required', () => {
const protectedEntry = adaptSurfaceMap({
nodes: [
{
...SAMPLE.nodes[0],
node: 'entry_point',
auth_required: true,
} as SurfaceMap['nodes'][0],
],
edges: [],
});
expect(protectedEntry.nodes[0]?.badges).toEqual(['auth']);
const openEntry = adaptSurfaceMap(SAMPLE);
expect(openEntry.nodes[0]?.badges).toBeUndefined();
});
it('produces unique edge keys even for parallel edges of the same kind', () => {
const parallel: SurfaceMap = {
nodes: SAMPLE.nodes,
edges: [
{ from: 0, to: 1, kind: 'calls' },
{ from: 0, to: 1, kind: 'calls' },
],
};
const model = adaptSurfaceMap(parallel);
expect(model.edges[0]?.key).not.toBe(model.edges[1]?.key);
});
});

View file

@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/queryclient.ts","./src/api/types.ts","./src/api/mutations/baseline.ts","./src/api/mutations/config.ts","./src/api/mutations/rules.ts","./src/api/mutations/scans.ts","./src/api/mutations/triage.ts","./src/api/queries/config.ts","./src/api/queries/debug.ts","./src/api/queries/explorer.ts","./src/api/queries/findings.ts","./src/api/queries/health.ts","./src/api/queries/overview.ts","./src/api/queries/rules.ts","./src/api/queries/scans.ts","./src/api/queries/surface.ts","./src/api/queries/triage.ts","./src/components/copymarkdownbutton.tsx","./src/components/verdictbadge.tsx","./src/components/charts/horizontalbarchart.tsx","./src/components/charts/linechart.tsx","./src/components/data-display/codeviewer.tsx","./src/components/data-display/filetree.tsx","./src/components/explorer/analysisworkspace.tsx","./src/components/icons/icons.tsx","./src/components/layout/applayout.tsx","./src/components/layout/headerbar.tsx","./src/components/layout/sidebar.tsx","./src/components/overview/overviewwidgets.tsx","./src/components/ui/commandpalette.tsx","./src/components/ui/dropdown.tsx","./src/components/ui/emptystate.tsx","./src/components/ui/errorstate.tsx","./src/components/ui/loadingstate.tsx","./src/components/ui/modal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/shortcutshelp.tsx","./src/components/ui/statcard.tsx","./src/components/ui/toaster.tsx","./src/contexts/ssecontext.tsx","./src/contexts/themecontext.tsx","./src/contexts/toastcontext.tsx","./src/graph/styles.ts","./src/graph/types.ts","./src/graph/adapters/callgraph.ts","./src/graph/adapters/cfg.ts","./src/graph/components/callgraphcanvas.tsx","./src/graph/components/cfggraphcanvas.tsx","./src/graph/components/graphtoolbar.tsx","./src/graph/hooks/useelklayout.ts","./src/graph/layout/elk.ts","./src/graph/layout/text.ts","./src/graph/reduction/cfgcompaction.ts","./src/graph/reduction/neighborhood.ts","./src/graph/rendering/sigma/sigmagraph.tsx","./src/graph/rendering/sigma/buildgraph.ts","./src/graph/rendering/sigma/edgeoverlay.ts","./src/hooks/usechordnavigation.ts","./src/hooks/usedebounce.ts","./src/hooks/usefiletree.ts","./src/hooks/usefindingsurlstate.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/usepagetitle.ts","./src/hooks/usepersistedstate.ts","./src/modals/codeviewermodal.tsx","./src/modals/newscanmodal.tsx","./src/pages/configpage.tsx","./src/pages/explorerpage.tsx","./src/pages/findingdetailpage.tsx","./src/pages/findingspage.tsx","./src/pages/overviewpage.tsx","./src/pages/rulespage.tsx","./src/pages/scancomparepage.tsx","./src/pages/scandetailpage.tsx","./src/pages/scanspage.tsx","./src/pages/surfacepage.tsx","./src/pages/triagepage.tsx","./src/pages/debug/abstractinterppage.tsx","./src/pages/debug/authanalysispage.tsx","./src/pages/debug/callgraphpage.tsx","./src/pages/debug/cfgviewerpage.tsx","./src/pages/debug/debuglayout.tsx","./src/pages/debug/functionselector.tsx","./src/pages/debug/pointerviewerpage.tsx","./src/pages/debug/ssaviewerpage.tsx","./src/pages/debug/summaryexplorerpage.tsx","./src/pages/debug/symexpage.tsx","./src/pages/debug/taintviewerpage.tsx","./src/pages/debug/typefactspage.tsx","./src/test/setup.ts","./src/test/api/client.test.ts","./src/test/components/pagination.test.tsx","./src/test/components/statcard.test.tsx","./src/test/components/dynamicverdictsection.test.tsx","./src/test/components/statecomponents.test.tsx","./src/test/components/verdictbadge.test.tsx","./src/test/graph/cfgadapter.test.ts","./src/test/graph/compactgraph.test.ts","./src/test/graph/nodestyles.test.ts","./src/test/hooks/usedebounce.test.ts","./src/test/modals/newscanmodal.test.tsx","./src/test/utils/findingmarkdown.test.ts","./src/test/utils/formatdate.test.ts","./src/test/utils/syntaxhighlight.test.ts","./src/test/utils/truncpath.test.ts","./src/utils/findingmarkdown.ts","./src/utils/formatdate.ts","./src/utils/parsenote.ts","./src/utils/syntaxhighlight.ts","./src/utils/truncpath.ts"],"version":"6.0.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/api/queryclient.ts","./src/api/types.ts","./src/api/mutations/baseline.ts","./src/api/mutations/config.ts","./src/api/mutations/rules.ts","./src/api/mutations/scans.ts","./src/api/mutations/triage.ts","./src/api/queries/config.ts","./src/api/queries/debug.ts","./src/api/queries/explorer.ts","./src/api/queries/findings.ts","./src/api/queries/health.ts","./src/api/queries/overview.ts","./src/api/queries/rules.ts","./src/api/queries/scans.ts","./src/api/queries/surface.ts","./src/api/queries/triage.ts","./src/components/copymarkdownbutton.tsx","./src/components/verdictbadge.tsx","./src/components/charts/horizontalbarchart.tsx","./src/components/charts/linechart.tsx","./src/components/data-display/codeviewer.tsx","./src/components/data-display/filetree.tsx","./src/components/explorer/analysisworkspace.tsx","./src/components/icons/icons.tsx","./src/components/layout/applayout.tsx","./src/components/layout/headerbar.tsx","./src/components/layout/sidebar.tsx","./src/components/overview/overviewwidgets.tsx","./src/components/ui/commandpalette.tsx","./src/components/ui/dropdown.tsx","./src/components/ui/emptystate.tsx","./src/components/ui/errorstate.tsx","./src/components/ui/loadingstate.tsx","./src/components/ui/modal.tsx","./src/components/ui/pagination.tsx","./src/components/ui/shortcutshelp.tsx","./src/components/ui/statcard.tsx","./src/components/ui/toaster.tsx","./src/contexts/ssecontext.tsx","./src/contexts/themecontext.tsx","./src/contexts/toastcontext.tsx","./src/graph/styles.ts","./src/graph/types.ts","./src/graph/adapters/callgraph.ts","./src/graph/adapters/cfg.ts","./src/graph/adapters/surface.ts","./src/graph/components/callgraphcanvas.tsx","./src/graph/components/cfggraphcanvas.tsx","./src/graph/components/graphtoolbar.tsx","./src/graph/components/surfacegraphcanvas.tsx","./src/graph/hooks/useelklayout.ts","./src/graph/layout/elk.ts","./src/graph/layout/text.ts","./src/graph/reduction/cfgcompaction.ts","./src/graph/reduction/neighborhood.ts","./src/graph/rendering/sigma/sigmagraph.tsx","./src/graph/rendering/sigma/buildgraph.ts","./src/graph/rendering/sigma/edgeoverlay.ts","./src/hooks/usechordnavigation.ts","./src/hooks/usedebounce.ts","./src/hooks/usefiletree.ts","./src/hooks/usefindingsurlstate.ts","./src/hooks/usekeyboardshortcuts.ts","./src/hooks/usepagetitle.ts","./src/hooks/usepersistedstate.ts","./src/modals/codeviewermodal.tsx","./src/modals/newscanmodal.tsx","./src/pages/configpage.tsx","./src/pages/explorerpage.tsx","./src/pages/findingdetailpage.tsx","./src/pages/findingspage.tsx","./src/pages/overviewpage.tsx","./src/pages/rulespage.tsx","./src/pages/scancomparepage.tsx","./src/pages/scandetailpage.tsx","./src/pages/scanspage.tsx","./src/pages/surfacepage.tsx","./src/pages/triagepage.tsx","./src/pages/debug/abstractinterppage.tsx","./src/pages/debug/authanalysispage.tsx","./src/pages/debug/callgraphpage.tsx","./src/pages/debug/cfgviewerpage.tsx","./src/pages/debug/debuglayout.tsx","./src/pages/debug/functionselector.tsx","./src/pages/debug/pointerviewerpage.tsx","./src/pages/debug/ssaviewerpage.tsx","./src/pages/debug/summaryexplorerpage.tsx","./src/pages/debug/symexpage.tsx","./src/pages/debug/taintviewerpage.tsx","./src/pages/debug/typefactspage.tsx","./src/test/setup.ts","./src/test/api/client.test.ts","./src/test/components/pagination.test.tsx","./src/test/components/statcard.test.tsx","./src/test/components/dynamicverdictsection.test.tsx","./src/test/components/statecomponents.test.tsx","./src/test/components/verdictbadge.test.tsx","./src/test/graph/cfgadapter.test.ts","./src/test/graph/compactgraph.test.ts","./src/test/graph/nodestyles.test.ts","./src/test/graph/surfaceadapter.test.ts","./src/test/hooks/usedebounce.test.ts","./src/test/modals/newscanmodal.test.tsx","./src/test/utils/findingmarkdown.test.ts","./src/test/utils/formatdate.test.ts","./src/test/utils/syntaxhighlight.test.ts","./src/test/utils/truncpath.test.ts","./src/utils/findingmarkdown.ts","./src/utils/formatdate.ts","./src/utils/parsenote.ts","./src/utils/syntaxhighlight.ts","./src/utils/truncpath.ts"],"version":"6.0.3"}