[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;