From f4793b04392eada58866b4e2d1b528868cf9509b Mon Sep 17 00:00:00 2001 From: pitboss Date: Sun, 17 May 2026 07:41:28 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0022 (20260517T044708Z-e058) --- frontend/src/graph/adapters/surface.ts | 82 ++++++++++++ .../graph/components/SurfaceGraphCanvas.tsx | 123 ++++++++++++++++++ frontend/src/graph/layout/elk.ts | 8 ++ frontend/src/graph/layout/text.ts | 7 + frontend/src/graph/styles.ts | 104 ++++++++++++++- frontend/src/graph/types.ts | 2 +- frontend/src/pages/SurfacePage.tsx | 65 ++++++--- frontend/src/styles/global.css | 31 +++++ frontend/src/test/graph/nodeStyles.test.ts | 45 +++++++ .../src/test/graph/surfaceAdapter.test.ts | 110 ++++++++++++++++ frontend/tsconfig.tsbuildinfo | 2 +- 11 files changed, 559 insertions(+), 20 deletions(-) create mode 100644 frontend/src/graph/adapters/surface.ts create mode 100644 frontend/src/graph/components/SurfaceGraphCanvas.tsx create mode 100644 frontend/src/test/graph/surfaceAdapter.test.ts diff --git a/frontend/src/graph/adapters/surface.ts b/frontend/src/graph/adapters/surface.ts new file mode 100644 index 00000000..dc37d75c --- /dev/null +++ b/frontend/src/graph/adapters/surface.ts @@ -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 = { + 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 }, + })), + }; +} diff --git a/frontend/src/graph/components/SurfaceGraphCanvas.tsx b/frontend/src/graph/components/SurfaceGraphCanvas.tsx new file mode 100644 index 00000000..ea21e48c --- /dev/null +++ b/frontend/src/graph/components/SurfaceGraphCanvas.tsx @@ -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 ( +
Failed to compute the surface layout.
+ ); + } + + if (!graph) { + return
Preparing surface graph…
; + } + + const extras = ( + <> + + + + + + ); + + return ( + onSelectNode(Number(key))} + searchMatchKeys={matchKeys} + toolbarExtras={extras} + loading={isLoading} + /> + ); +} diff --git a/frontend/src/graph/layout/elk.ts b/frontend/src/graph/layout/elk.ts index 1ae2ce39..299d5a83 100644 --- a/frontend/src/graph/layout/elk.ts +++ b/frontend/src/graph/layout/elk.ts @@ -39,6 +39,14 @@ const PRESETS: Record = { padding: 32, edgeRouting: 'ORTHOGONAL', }, + surface: { + direction: 'RIGHT', + nodeSpacing: 44, + layerSpacing: 156, + edgeNodeSpacing: 28, + padding: 36, + edgeRouting: 'POLYLINE', + }, }; function measureNode( diff --git a/frontend/src/graph/layout/text.ts b/frontend/src/graph/layout/text.ts index 1339943b..0c94c610 100644 --- a/frontend/src/graph/layout/text.ts +++ b/frontend/src/graph/layout/text.ts @@ -31,6 +31,13 @@ const CONFIG: Record = { maxSecondaryLines: 2, maxSublabelLines: 1, }, + surface: { + primaryChars: 32, + secondaryChars: 32, + maxPrimaryLines: 2, + maxSecondaryLines: 2, + maxSublabelLines: 1, + }, }; function normalizeWhitespace(value: string): string { diff --git a/frontend/src/graph/styles.ts b/frontend/src/graph/styles.ts index 531718da..cf6fb31a 100644 --- a/frontend/src/graph/styles.ts +++ b/frontend/src/graph/styles.ts @@ -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: [] }; diff --git a/frontend/src/graph/types.ts b/frontend/src/graph/types.ts index 5869bed7..ecf1e049 100644 --- a/frontend/src/graph/types.ts +++ b/frontend/src/graph/types.ts @@ -1,4 +1,4 @@ -export type GraphViewKind = 'callgraph' | 'cfg'; +export type GraphViewKind = 'callgraph' | 'cfg' | 'surface'; export interface GraphPoint { x: number; diff --git a/frontend/src/pages/SurfacePage.tsx b/frontend/src/pages/SurfacePage.tsx index 97c8158d..1995d107 100644 --- a/frontend/src/pages/SurfacePage.tsx +++ b/frontend/src/pages/SurfacePage.tsx @@ -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(null); const [filter, setFilter] = useState('all'); const [query, setQuery] = useState(''); + const [viewMode, setViewMode] = useState('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'} /> +
+ + +
-
- {visible.length === 0 ? ( -

No nodes match.

- ) : ( - visible.map(({ node, index }) => ( - setSelected(index)} - /> - )) - )} -
+ {viewMode === 'list' ? ( +
+ {visible.length === 0 ? ( +

No nodes match.

+ ) : ( + visible.map(({ node, index }) => ( + setSelected(index)} + /> + )) + )} +
+ ) : ( +
+ setSelected(id)} + /> +
+ )} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 67bc6605..5a08df74 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -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; +} diff --git a/frontend/src/test/graph/nodeStyles.test.ts b/frontend/src/test/graph/nodeStyles.test.ts index 211c7f4e..77e23544 100644 --- a/frontend/src/test/graph/nodeStyles.test.ts +++ b/frontend/src/test/graph/nodeStyles.test.ts @@ -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([]); + }); }); diff --git a/frontend/src/test/graph/surfaceAdapter.test.ts b/frontend/src/test/graph/surfaceAdapter.test.ts new file mode 100644 index 00000000..45fc7566 --- /dev/null +++ b/frontend/src/test/graph/surfaceAdapter.test.ts @@ -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); + }); +}); diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 50416713..4995350f 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file