fix(graph): address review — reachFrom direct edges + node a11y

- reachFrom now returns the direct outbound edges PLUS the recursive
  $graphLookup reaches, de-duped by edge id, with maxDepth to bound cycles.
  It was dropping edges straight off the start node. [review P2]
- GraphView SVG nodes are keyboard-accessible: role/tabIndex/aria-label +
  Enter/Space handler alongside onClick. [review P3]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
sb-iam
2026-06-27 17:56:47 -07:00
parent 546aeeed70
commit 32bff6a3ab
2 changed files with 27 additions and 2 deletions
+18 -2
View File
@@ -70,7 +70,7 @@ export async function reachFrom(podId: string, startNodeId: string): Promise<Rea
const db = await getDb(); const db = await getDb();
const rows = await db const rows = await db
.collection<GraphEdgeDoc>('graph_edges') .collection<GraphEdgeDoc>('graph_edges')
.aggregate<{ reaches: GraphEdgeDoc[] }>([ .aggregate<GraphEdgeDoc & { reaches: GraphEdgeDoc[] }>([
{ $match: { podId, source: startNodeId } }, { $match: { podId, source: startNodeId } },
{ {
$graphLookup: { $graphLookup: {
@@ -80,9 +80,25 @@ export async function reachFrom(podId: string, startNodeId: string): Promise<Rea
connectToField: 'source', connectToField: 'source',
as: 'reaches', as: 'reaches',
restrictSearchWithMatch: { podId }, restrictSearchWithMatch: { podId },
maxDepth: 6,
}, },
}, },
]) ])
.toArray(); .toArray();
return { start: startNodeId, reaches: rows.flatMap((r) => r.reaches) };
// Include the direct outbound edges (the $match rows) plus everything reachable
// from them, de-duped by edge id. Without the direct rows, edges straight off
// the start node go missing unless a cycle happens to re-discover them.
const seen = new Set<string>();
const reaches: GraphEdgeDoc[] = [];
for (const row of rows) {
const { reaches: recursive, ...direct } = row;
for (const edge of [direct as GraphEdgeDoc, ...(recursive ?? [])]) {
if (edge?.id && !seen.has(edge.id)) {
seen.add(edge.id);
reaches.push(edge);
}
}
}
return { start: startNodeId, reaches };
} }
+9
View File
@@ -245,7 +245,16 @@ export function GraphView({ podId, onClose }: { podId: string; onClose: () => vo
<g <g
key={n.id} key={n.id}
className={`pm-node ${dimNode(n.id) ? 'pm-dim' : ''}`} className={`pm-node ${dimNode(n.id) ? 'pm-dim' : ''}`}
role="button"
tabIndex={0}
aria-label={`${n.kind}: ${n.label}`}
onClick={() => setSelected((cur) => (cur === n.id ? null : n.id))} onClick={() => setSelected((cur) => (cur === n.id ? null : n.id))}
onKeyDown={(ev) => {
if (ev.key === 'Enter' || ev.key === ' ') {
ev.preventDefault();
setSelected((cur) => (cur === n.id ? null : n.id));
}
}}
> >
<NodeShape node={n} /> <NodeShape node={n} />
<text className="pm-lbl" x={n.x} y={n.y + 33} textAnchor="middle"> <text className="pm-lbl" x={n.x} y={n.y + 33} textAnchor="middle">