Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ function CustomNodeComponent({

const name = data.label ?? "unnamed";
const truncatedName =
name.length > 24 ? name.slice(0, 22) + "..." : name;
name.length > 60 ? name.slice(0, 58) + "..." : name;

return (
<div
className={`relative rounded-lg bg-elevated border border-border-subtle ${extraClass} min-w-[180px] max-w-[220px] overflow-hidden transition-[box-shadow,outline,opacity,filter] duration-200 cursor-pointer shadow-[0_2px_8px_rgba(0,0,0,0.3)]`}
className={`relative rounded-lg bg-elevated border border-border-subtle ${extraClass} min-w-[240px] max-w-[310px] overflow-hidden transition-[box-shadow,outline,opacity,filter] duration-200 cursor-pointer shadow-[0_2px_8px_rgba(0,0,0,0.3)]`}
onClick={() => data.onNodeClick?.(id)}
>
{/* Left color bar */}
Expand Down Expand Up @@ -168,11 +168,11 @@ function CustomNodeComponent({
</div>
</div>

<div className="text-sm font-heading text-text-primary truncate" title={data.label}>
<div className="text-sm font-heading text-text-primary line-clamp-2 break-words" title={data.label}>
{truncatedName}
</div>

<div className="text-[11px] text-text-secondary mt-1 line-clamp-2 leading-tight">
<div className="text-[11px] text-text-secondary mt-1 line-clamp-3 leading-tight">
{data.summary}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,36 @@ describe("deriveContainers — community fallback", () => {
}
}
const { containers } = deriveContainers(nodes, edges);
// A single folder covering the whole set is now kept as ONE folder
// container (named after the folder) instead of being split into
// anonymous Louvain communities.
expect(containers.length).toBe(1);
expect(containers[0].strategy).toBe("folder");
expect(containers[0].name).toBe("services");
});

it("names community clusters by member files when no folder signal exists", () => {
// Flat paths (no directories) force the community fallback.
const nodes = Array.from({ length: 10 }, (_, i) =>
node(`n${i}`, `n${i}.go`),
);
const edges: GraphEdge[] = [];
for (const i of [0, 1, 2, 3, 4]) {
for (const j of [0, 1, 2, 3, 4]) {
if (i !== j) edges.push({ source: `n${i}`, target: `n${j}`, type: "calls" } as GraphEdge);
}
}
for (const i of [5, 6, 7, 8, 9]) {
for (const j of [5, 6, 7, 8, 9]) {
if (i !== j) edges.push({ source: `n${i}`, target: `n${j}`, type: "calls" } as GraphEdge);
}
}
const { containers } = deriveContainers(nodes, edges);
expect(containers.length).toBeGreaterThanOrEqual(2);
for (const c of containers) {
expect(c.strategy).toBe("community");
expect(c.name).toMatch(/^Cluster [A-Z]$/);
// Member-derived label, e.g. "n0 · n1 · n2 +2" — not "Cluster A".
expect(c.name).toMatch(/·|\+/);
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ function shouldFallbackToCommunity(
rooted: string[],
totalNodes: number,
): boolean {
// A single folder covering the whole set is a meaningful unit (e.g. a
// Redux slice folder like src/store/meetingTypes) — keep it as ONE named
// container instead of splitting into anonymous Louvain communities.
if (groups.size === 1 && rooted.length === 0) return false;
const bucketCount = groups.size + (rooted.length > 0 ? 1 : 0);
if (bucketCount < MIN_BUCKET_COUNT) return true;
for (const ids of groups.values()) {
Expand Down Expand Up @@ -113,11 +117,33 @@ export function deriveContainers(
byCommunity.set(cid, arr);
}
const sorted = [...byCommunity.entries()].sort((a, b) => a[0] - b[0]);
// Name community clusters by their member files instead of "Cluster A/B/C".
const nodeById = new Map(nodes.map((n) => [n.id, n]));
const FILE_LEVEL = new Set([
"file", "config", "document", "service", "pipeline",
"table", "schema", "resource", "endpoint",
]);
const labelFor = (ids: string[]): string => {
let members = ids
.map((id) => nodeById.get(id))
.filter((n): n is GraphNode => Boolean(n));
const fileMembers = members.filter((m) => FILE_LEVEL.has(m.type));
if (fileMembers.length > 0) members = fileMembers;
const bases = [
...new Set(
members.map((m) =>
(m.name ?? m.id).replace(/\.(tsx?|jsx?|java|py|go|rb|cs|kt)$/i, ""),
),
),
];
const head = bases.slice(0, 3).join(" · ");
return bases.length > 3 ? `${head} +${bases.length - 3}` : head;
};
containers = sorted.map(([cid, ids], i) => ({
id: `container:cluster-${cid}`,
// A-Z for the first 26, then numeric. Avoids `String.fromCharCode(65+i)`
// wrapping into `[`, `\`, `]` ... once the cluster count exceeds 26.
name: i < 26 ? `Cluster ${String.fromCharCode(65 + i)}` : `Cluster ${i + 1}`,
name:
labelFor(ids) ||
(i < 26 ? `Cluster ${String.fromCharCode(65 + i)}` : `Cluster ${i + 1}`),
nodeIds: ids,
strategy: "community" as const,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import type { SimulationNodeDatum, SimulationLinkDatum } from "d3-force";
import type { Node, Edge } from "@xyflow/react";
import type { ElkInput } from "./elk-layout";

export const NODE_WIDTH = 280;
export const NODE_HEIGHT = 120;
export const NODE_WIDTH = 330;
export const NODE_HEIGHT = 150;
export const LAYER_CLUSTER_WIDTH = 320;
export const LAYER_CLUSTER_HEIGHT = 180;
export const PORTAL_NODE_WIDTH = 240;
Expand Down
Loading