Files
llm-wiki/packages/graph-engine/test/render-model.test.ts
T
2026-07-12 21:26:08 +08:00

1078 lines
46 KiB
TypeScript

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
buildRenderableGraph,
edgeOpacity,
edgeRelationClass,
edgeStrokeWidth,
edgeVisualOpacity,
edgeVisualStrokeWidth,
evaluateCommunityQuality,
GRAPH_COMMUNITY_FOCUS_BUDGETS,
GRAPH_RENDER_BUDGETS,
makeEdgePath,
nodeDisplayModeForDensity,
screenEffectiveDensityMode
} from "../src/render";
import { UNGROUPED_COMMUNITY_ID, UNGROUPED_COMMUNITY_LABEL } from "../src/types";
import type { GraphData } from "../src/types";
function sampleGraph(): GraphData {
return {
meta: {
build_date: "2026-06-12T00:00:00.000Z",
wiki_title: "Stage 4 Demo",
total_nodes: 4,
total_edges: 3
},
nodes: [
{ id: "topic", label: "主题", type: "topic", community: "c1", source_path: "wiki/topic.md", weight: 80, x: 20, y: 30 },
{ id: "entity", label: "实体", type: "entity", community: "c1", source_path: "wiki/entity.md", weight: 50 },
{ id: "source", label: "来源", type: "source", community: "c2", source_path: "wiki/source.md", weight: 40, x: 70, y: 60 },
{ id: "island", label: "孤岛", type: "entity", community: "c3", source_path: "wiki/island.md", weight: 10 }
],
edges: [
{ id: "e1", from: "topic", to: "entity", type: "EXTRACTED", confidence: "EXTRACTED", relation_type: "实现", weight: 1 },
{ id: "e2", from: "topic", to: "source", type: "INFERRED", confidence: "INFERRED", relation_type: "对比", weight: 0.5 },
{ id: "missing", from: "topic", to: "missing", type: "UNVERIFIED", weight: 0.1 }
],
learning: {
version: 1,
entry: { recommended_start_node_id: "topic", recommended_start_reason: "community_hub", default_mode: "global" },
views: {
path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: false, degraded: true },
global: { enabled: true, node_ids: ["topic", "entity", "source", "island"], degraded: false }
},
communities: [
{ id: "c1", label: "核心", node_count: 2, color_index: 0, recommended_start_node_id: "topic" },
{ id: "c2", label: "来源", node_count: 1, color_index: 1 },
{ id: "c3", label: "孤岛", node_count: 1, color_index: 2 }
]
}
};
}
function outlierCommunityGraph(): GraphData {
const nodes = [
{ id: "core-a", label: "Core A", type: "entity", community: "c1", source_path: "wiki/core-a.md", weight: 70, x: 20, y: 40 },
{ id: "core-b", label: "Core B", type: "entity", community: "c1", source_path: "wiki/core-b.md", weight: 65, x: 22, y: 42 },
{ id: "core-c", label: "Core C", type: "topic", community: "c1", source_path: "wiki/core-c.md", weight: 80, x: 24, y: 39 },
{ id: "core-d", label: "Core D", type: "source", community: "c1", source_path: "wiki/core-d.md", weight: 55, x: 26, y: 41 },
{ id: "outlier", label: "Outlier", type: "entity", community: "c1", source_path: "wiki/outlier.md", weight: 35, x: 92, y: 78 }
];
return {
meta: {
build_date: "2026-06-12T00:00:00.000Z",
wiki_title: "Outlier Fixture",
total_nodes: nodes.length,
total_edges: 0
},
nodes,
edges: [],
learning: {
version: 1,
entry: { recommended_start_node_id: "core-c", recommended_start_reason: "community_hub", default_mode: "global" },
views: {
path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: false, degraded: true },
global: { enabled: true, node_ids: nodes.map((node) => node.id), degraded: false }
},
communities: [
{ id: "c1", label: "Cluster", node_count: nodes.length, color_index: 0, recommended_start_node_id: "core-c" }
]
}
};
}
function budgetGraph(nodeCount: number, edgeCount: number): GraphData {
const nodes = Array.from({ length: nodeCount }, (_, index) => ({
id: `n${index}`,
label: `Budget node ${index}`,
type: index % 7 === 0 ? "topic" : index % 11 === 0 ? "source" : "entity",
community: "c1",
source_path: `wiki/budget/n${index}.md`,
weight: 100 - (index % 83),
x: (index * 37) % 100,
y: (index * 53) % 100
}));
const edges: NonNullable<GraphData["edges"]> = [];
for (let sourceIndex = 0; sourceIndex < nodeCount && edges.length < edgeCount; sourceIndex += 1) {
for (let targetIndex = sourceIndex + 1; targetIndex < nodeCount && edges.length < edgeCount; targetIndex += 1) {
edges.push({
id: `e${edges.length}`,
from: `n${sourceIndex}`,
to: `n${targetIndex}`,
type: "EXTRACTED",
confidence: "EXTRACTED",
relation_type: edges.length % 3 === 0 ? "实现" : "依赖",
weight: (edges.length % 10) / 10
});
}
}
return {
meta: {
build_date: "2026-06-18T00:00:00.000Z",
wiki_title: "Budget Fixture",
total_nodes: nodes.length,
total_edges: edges.length
},
nodes,
edges,
learning: {
version: 1,
entry: { recommended_start_node_id: "n0", recommended_start_reason: "budget_fixture", default_mode: "global" },
views: {
path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: false, degraded: true },
global: { enabled: true, node_ids: nodes.map((node) => node.id), degraded: false }
},
communities: [
{ id: "c1", label: "Budget Community", node_count: nodes.length, color_index: 0, recommended_start_node_id: "n0" }
]
}
};
}
function densePointMapGraph(): GraphData {
const nodeCount = 2000;
const edgeTarget = 3996;
const communityCount = 16;
const nodes = Array.from({ length: nodeCount }, (_, index) => ({
id: `dense-${index}`,
label: `Dense node ${index}`,
type: index % 17 === 0 ? "topic" : index % 29 === 0 ? "source" : "entity",
community: `dense-community-${index % communityCount}`,
source_path: `wiki/dense/dense-${index}.md`,
weight: 100 - (index % 97),
x: (index * 37) % 100,
y: (index * 53) % 100
}));
const edges: NonNullable<GraphData["edges"]> = [
{
id: "dense-selected-weak",
from: "dense-1999",
to: "dense-1",
type: "INFERRED",
confidence: "INFERRED",
relation_type: "依赖",
weight: 0
},
{
id: "dense-selected-strong",
from: "dense-1999",
to: "dense-2",
type: "EXTRACTED",
confidence: "EXTRACTED",
relation_type: "实现",
weight: 1
}
];
for (let index = 0; edges.length < edgeTarget; index += 1) {
const source = index % nodeCount;
const target = (source + 1 + (index % 113)) % nodeCount;
if (source === target) continue;
edges.push({
id: `dense-edge-${index}`,
from: `dense-${source}`,
to: `dense-${target}`,
type: index % 5 === 0 ? "INFERRED" : "EXTRACTED",
confidence: index % 5 === 0 ? "INFERRED" : "EXTRACTED",
relation_type: index % 7 === 0 ? "对比" : "依赖",
weight: (index % 11) / 10
});
}
return {
meta: {
build_date: "2026-06-21T00:00:00.000Z",
wiki_title: "Dense Point Map Fixture",
total_nodes: nodes.length,
total_edges: edges.length
},
nodes,
edges,
learning: {
version: 1,
entry: { recommended_start_node_id: "dense-0", recommended_start_reason: "dense_fixture", default_mode: "global" },
views: {
path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: false, degraded: true },
global: { enabled: true, node_ids: nodes.map((node) => node.id), degraded: false }
},
communities: Array.from({ length: communityCount }, (_, index) => ({
id: `dense-community-${index}`,
label: `Dense Community ${index}`,
node_count: nodes.filter((node) => node.community === `dense-community-${index}`).length,
color_index: index,
recommended_start_node_id: index === 0 ? "dense-0" : null
}))
}
};
}
function manyTinyCommunitiesGraph(): GraphData {
const nodes = Array.from({ length: 10 }, (_, index) => ({
id: `tiny-${index}`,
label: `Tiny node ${index}`,
type: "entity",
community: `tiny-${index}`,
source_path: `wiki/tiny/${index}.md`,
weight: 20 + index,
x: (index * 11) % 100,
y: (index * 17) % 100
}));
return {
meta: {
build_date: "2026-06-18T00:00:00.000Z",
wiki_title: "Many Tiny Communities",
total_nodes: nodes.length,
total_edges: 0
},
nodes,
edges: [],
learning: {
version: 1,
entry: { recommended_start_node_id: "tiny-0", recommended_start_reason: "fixture", default_mode: "global" },
views: {
path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: false, degraded: true },
global: { enabled: true, node_ids: nodes.map((node) => node.id), degraded: false }
},
communities: nodes.map((node, index) => ({
id: String(node.community),
label: `Specific topic ${index}`,
node_count: 1,
color_index: index
}))
}
};
}
function oversizedWeakCommunityGraph(): GraphData {
const nodes = Array.from({ length: 120 }, (_, index) => ({
id: `blob-${index}`,
label: `Blob node ${index}`,
type: index % 9 === 0 ? "topic" : "entity",
community: "community",
source_path: `wiki/blob/${index}.md`,
weight: 60 - (index % 30),
x: (index * 19) % 100,
y: (index * 23) % 100
}));
return {
meta: {
build_date: "2026-06-18T00:00:00.000Z",
wiki_title: "Oversized Weak Community",
total_nodes: nodes.length,
total_edges: 0
},
nodes,
edges: [],
learning: {
version: 1,
entry: { recommended_start_node_id: "blob-0", recommended_start_reason: "fixture", default_mode: "global" },
views: {
path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: true, degraded: true },
global: { enabled: true, node_ids: nodes.map((node) => node.id), degraded: false }
},
communities: [
{ id: "community", label: "community", node_count: nodes.length, color_index: 0, is_weak: true }
]
}
};
}
function mixedCrossCommunityGraph(): GraphData {
const nodes = Array.from({ length: 12 }, (_, index) => ({
id: `mixed-${index}`,
label: `Mixed node ${index}`,
type: "entity",
community: index < 6 ? "left" : "right",
source_path: `wiki/mixed/${index}.md`,
weight: 30,
x: (index * 13) % 100,
y: (index * 29) % 100
}));
const edges = Array.from({ length: 8 }, (_, index) => ({
id: `mixed-edge-${index}`,
from: `mixed-${index % 6}`,
to: `mixed-${6 + (index % 6)}`,
type: "INFERRED",
confidence: "INFERRED",
relation_type: "依赖",
weight: 0.5
}));
return {
meta: {
build_date: "2026-06-18T00:00:00.000Z",
wiki_title: "Mixed Cross Community",
total_nodes: nodes.length,
total_edges: edges.length
},
nodes,
edges,
learning: {
version: 1,
entry: { recommended_start_node_id: "mixed-0", recommended_start_reason: "fixture", default_mode: "global" },
views: {
path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: true, degraded: true },
global: { enabled: true, node_ids: nodes.map((node) => node.id), degraded: false }
},
communities: [
{ id: "left", label: "left", node_count: 6, color_index: 0, is_weak: true },
{ id: "right", label: "right", node_count: 6, color_index: 1, is_weak: true }
]
}
};
}
describe("buildRenderableGraph", () => {
it("maps graph data to static renderable nodes, edges, communities, and minimap points", () => {
const graph = buildRenderableGraph(sampleGraph(), { theme: "shan-shui" });
assert.equal(graph.counts.totalNodes, 4);
assert.equal(graph.nodes.length, 4);
assert.equal(graph.edges.length, 2);
assert.equal(graph.communities.length, 3);
assert.equal(graph.densityMode, "card");
assert.equal(graph.nodes.find((node) => node.id === "topic")?.visualRole, "index-slip");
assert.equal(graph.edges[0].type, "extracted");
assert.equal(graph.edges[0].confidence, "extracted");
assert.equal(graph.edges[0].relationType, "实现");
assert.equal(graph.edges[0].relationClass, "relation-implementation");
assert.equal(graph.edges[1].confidence, "inferred");
assert.equal(graph.edges[1].relationType, "对比");
assert.equal(graph.edges[1].relationClass, "relation-contrast");
assert.match(graph.edges[0].path, /^M \d+ \d+ Q /);
assert.equal(graph.minimap.path, "M8 40 C34 20 54 36 76 22 C98 8 118 24 150 12");
assert.equal(graph.minimap.nodes.length, 4);
});
it("uses low-weight global edges and fuller focused relation edges", () => {
const global = buildRenderableGraph(sampleGraph(), { theme: "shan-shui" });
const focused = buildRenderableGraph(sampleGraph(), {
theme: "shan-shui",
focus: { kind: "community", id: "c1" }
});
assert.equal(global.edges.find((edge) => edge.id === "e1")?.relationClass, "relation-implementation");
assert.equal(global.edges.find((edge) => edge.id === "e2")?.relationClass, "relation-contrast");
assert.equal(focused.edges[0].relationClass, "relation-implementation");
assert.ok(focused.edges[0].strokeWidth > global.edges[0].strokeWidth, "focused relation edge should render with a fuller stroke");
assert.ok(focused.edges[0].opacity > global.edges[0].opacity, "focused relation edge should render with higher opacity");
});
it("uses pins as the cold-start coordinates when a node source path matches", () => {
const graph = buildRenderableGraph(sampleGraph(), {
theme: "shan-shui",
pins: {
"wiki/entity.md": { x: 800, y: 340, coordinateSpace: "world" }
}
});
const pinned = graph.nodes.find((node) => node.id === "entity");
assert.ok(pinned);
assert.equal(pinned.x, 80);
assert.equal(pinned.y, 50);
assert.deepEqual(pinned.point, { x: 800, y: 340 });
});
it("keeps explicit world pins readable even when coordinates look like old percentages", () => {
const graph = buildRenderableGraph(sampleGraph(), {
theme: "shan-shui",
pins: {
"wiki/entity.md": { x: 80, y: -42.5, coordinateSpace: "world" }
}
});
const pinned = graph.nodes.find((node) => node.id === "entity");
assert.ok(pinned);
assert.deepEqual(pinned.point, { x: 80, y: -42.5 });
assert.ok(graph.worldBounds.minY < 0);
});
it("continues to read migrated old percent pins with an explicit coordinate-space marker", () => {
const graph = buildRenderableGraph(sampleGraph(), {
theme: "shan-shui",
pins: {
"wiki/entity.md": { x: 80, y: 50, coordinateSpace: "legacy-percent" }
}
});
const pinned = graph.nodes.find((node) => node.id === "entity");
assert.ok(pinned);
assert.deepEqual(pinned.point, { x: 800, y: 340 });
});
it("infers wiki-relative source paths for graph data without source_path", () => {
const data = sampleGraph();
data.nodes = data.nodes.map(({ source_path: _sourcePath, ...node }) => node);
const graph = buildRenderableGraph(data, {
theme: "shan-shui",
pins: {
"wiki/topics/topic.md": { x: 900, y: 408, coordinateSpace: "world" }
}
});
const topic = graph.nodes.find((node) => node.id === "topic");
assert.ok(topic);
assert.equal(topic.sourcePath, "wiki/topics/topic.md");
assert.deepEqual(topic.point, { x: 900, y: 408 });
});
it("marks selected nodes and preserves cinnabar visual role", () => {
const graph = buildRenderableGraph(sampleGraph(), {
theme: "mo-ye",
selection: { kind: "node", id: "source" }
});
const selected = graph.nodes.find((node) => node.id === "source");
assert.ok(selected);
assert.equal(selected.selected, true);
assert.equal(selected.visualRole, "cinnabar-note");
assert.equal(graph.minimap.nodes.find((node) => node.id === "source")?.selected, true);
});
it("marks shift-style multi-node selections", () => {
const graph = buildRenderableGraph(sampleGraph(), {
theme: "shan-shui",
selection: { kind: "nodes", ids: ["topic", "source"] }
});
const selected = graph.nodes.filter((node) => node.selected).map((node) => node.id);
assert.deepEqual(selected, ["topic", "source"]);
assert.notEqual(graph.nodes.find((node) => node.id === "topic")?.displayMode, "card");
assert.equal(graph.nodes.find((node) => node.id === "source")?.visualRole, "cinnabar-note");
assert.equal(graph.minimap.nodes.filter((node) => node.selected).length, 2);
});
it("enforces zero full cards in global view even for selected, search, and pinned nodes", () => {
const data = budgetGraph(80, 1200);
const graph = buildRenderableGraph(data, {
theme: "shan-shui",
selection: { kind: "node", id: "n79" },
searchResultIds: data.nodes.map((node) => node.id),
pins: {
"wiki/budget/n79.md": { x: 900, y: 500, coordinateSpace: "world" }
}
});
assert.equal(graph.budget.view, "global");
assert.equal(graph.budget.limits.maxCards, GRAPH_RENDER_BUDGETS.global.maxCards);
assert.equal(graph.budget.usage.maxCards, 0);
assert.equal(graph.nodes.filter((node) => node.displayMode === "card").length, 0);
assert.ok(graph.nodes.find((node) => node.id === "n79")?.labelVisible, "selected and pinned node should be promoted into labels");
});
it("keeps global labels, edges, and interaction updates within budget and reports overflow", () => {
const data = budgetGraph(200, 1200);
const graph = buildRenderableGraph(data, {
theme: "shan-shui",
searchResultIds: data.nodes.map((node) => node.id)
});
assert.ok(graph.budget.usage.maxLabels <= GRAPH_RENDER_BUDGETS.global.maxLabels);
assert.ok(graph.budget.usage.maxVisibleEdges <= GRAPH_RENDER_BUDGETS.global.maxVisibleEdges);
assert.ok(graph.budget.usage.maxInteractionUpdates <= GRAPH_RENDER_BUDGETS.global.maxInteractionUpdates);
assert.equal(graph.nodes.filter((node) => node.labelVisible).length, GRAPH_RENDER_BUDGETS.global.maxLabels);
assert.equal(graph.edges.length, GRAPH_RENDER_BUDGETS.global.maxVisibleEdges);
assert.ok(graph.overflow.labels.hidden > 0);
assert.ok(graph.overflow.edges.hidden > 0);
assert.ok(graph.overflow.interactionUpdates.hidden > 0);
assert.equal(graph.overflow.labels.total, 200);
assert.equal(graph.overflow.edges.total, 1200);
});
it("keeps interaction-time detail updates inside budget while preserving anchors", () => {
const data = budgetGraph(200, 1200);
const graph = buildRenderableGraph(data, {
theme: "shan-shui",
selection: { kind: "node", id: "n199" },
searchResultIds: ["n198", "n199"],
pins: {
"wiki/budget/n197.md": { x: 850, y: 500, coordinateSpace: "world" }
}
});
assert.ok(graph.interaction.updatedObjects <= GRAPH_RENDER_BUDGETS.global.maxInteractionUpdates);
assert.ok(graph.interaction.updateCandidates < graph.overflow.interactionUpdates.total);
assert.ok(graph.interaction.edgesVisibleDuringInteraction <= graph.budget.usage.maxVisibleEdges);
assert.ok(graph.interaction.preservedNodeIds.includes("n199"), "selected node should stay traceable");
assert.ok(graph.interaction.preservedNodeIds.includes("n198"), "searched node should stay traceable");
assert.ok(graph.interaction.preservedNodeIds.includes("n197"), "pinned node should stay traceable");
assert.ok(graph.interaction.preservedNodeIds.some((id) => graph.importance.stableCoreNodeIds.includes(id)), "stable core anchors should stay traceable");
});
it("keeps focused community nodes as a lightweight map without full cards", () => {
const data = budgetGraph(80, 1200);
const graph = buildRenderableGraph(data, {
theme: "shan-shui",
focus: { kind: "community", id: "c1" },
searchResultIds: data.nodes.map((node) => node.id),
pins: {
"wiki/budget/n79.md": { x: 900, y: 500, coordinateSpace: "world" }
}
});
assert.equal(graph.budget.view, "community");
assert.equal(graph.nodes.length, 80);
assert.equal(graph.budget.limits.maxCards, 0);
assert.equal(graph.budget.usage.maxCards, 0);
assert.equal(graph.nodes.filter((node) => node.displayMode === "card").length, 0);
assert.ok(graph.nodes.find((node) => node.id === "n79")?.labelVisible, "selected/search/pinned context should still be eligible for a label");
assert.ok(graph.nodes.filter((node) => node.labelVisible).length <= graph.budget.limits.maxLabels);
assert.equal(graph.overflow.cards.total, 0);
assert.equal(graph.overflow.cards.hidden, 0);
const firstNode = graph.nodes[0];
assert.ok(firstNode.communityMapDotSize >= 9, `community map dot should keep a floor size, got ${firstNode.communityMapDotSize}`);
assert.ok(firstNode.communityMapDotSize <= 24, `community map dot should respect the importance ceiling, got ${firstNode.communityMapDotSize}`);
assert.ok(["left", "right", "top", "bottom"].includes(firstNode.communityMapLabelSide));
assert.equal(typeof firstNode.communityMapRelationLabel, "boolean");
});
it("uses the small community band as a lightweight map with sparse labels", () => {
const graph = buildRenderableGraph(budgetGraph(24, 120), {
theme: "shan-shui",
focus: { kind: "community", id: "c1" }
});
assert.equal(graph.communityFocus?.sizeBand, "small");
assert.equal(graph.communityFocus?.representation, "cards-and-labels");
assert.equal(graph.communityFocus?.completePresence, "nodes");
assert.equal(graph.nodes.length, 24);
assert.equal(graph.nodes.filter((node) => node.displayMode === "card").length, 0);
assert.ok(graph.nodes.filter((node) => node.labelVisible).length <= GRAPH_COMMUNITY_FOCUS_BUDGETS.small.maxLabels);
assert.ok(graph.nodes.some((node) => node.displayMode === "point" || node.displayMode === "overview"), "ordinary nodes should remain point-like");
});
it("uses the medium community band with all nodes present and no full cards", () => {
const graph = buildRenderableGraph(budgetGraph(120, 600), {
theme: "shan-shui",
focus: { kind: "community", id: "c1" },
searchResultIds: Array.from({ length: 120 }, (_, index) => `n${index}`)
});
const cardCount = graph.nodes.filter((node) => node.displayMode === "card").length;
const pointCount = graph.nodes.filter((node) => node.displayMode === "point" || node.displayMode === "overview").length;
assert.equal(graph.communityFocus?.sizeBand, "medium");
assert.equal(graph.communityFocus?.representation, "points-with-cards");
assert.equal(graph.nodes.length, 120);
assert.equal(cardCount, 0);
assert.ok(pointCount > 0);
assert.ok(graph.nodes.filter((node) => node.labelVisible).length <= GRAPH_COMMUNITY_FOCUS_BUDGETS.medium.maxLabels);
});
it("uses the large community band as a lightweight outline map with strict label caps", () => {
const graph = buildRenderableGraph(budgetGraph(800, 1400), {
theme: "shan-shui",
focus: { kind: "community", id: "c1" },
searchResultIds: Array.from({ length: 800 }, (_, index) => `n${index}`)
});
assert.equal(graph.communityFocus?.sizeBand, "large");
assert.equal(graph.communityFocus?.representation, "outline-with-caps");
assert.equal(graph.communityFocus?.completePresence, "outline");
assert.equal(graph.nodes.length, 800);
assert.equal(graph.nodes.filter((node) => node.displayMode === "card").length, 0);
assert.equal(graph.nodes.filter((node) => node.labelVisible).length, GRAPH_COMMUNITY_FOCUS_BUDGETS.large.maxLabels);
assert.ok(graph.edges.length > 0);
assert.ok(graph.edges.length <= GRAPH_COMMUNITY_FOCUS_BUDGETS.large.maxVisibleEdges);
});
it("uses the oversized community band as an internal-map entry without rendering every member as a card", () => {
const graph = buildRenderableGraph(budgetGraph(3000, 1200), {
theme: "shan-shui",
focus: { kind: "community", id: "c1" },
searchResultIds: Array.from({ length: 3000 }, (_, index) => `n${index}`)
});
assert.equal(graph.communityFocus?.sizeBand, "oversized");
assert.equal(graph.communityFocus?.representation, "internal-map-entry");
assert.equal(graph.communityFocus?.completePresence, "internal-map");
assert.equal(graph.nodes.length, GRAPH_COMMUNITY_FOCUS_BUDGETS.oversized.maxVisibleNodes);
assert.equal(graph.nodes.filter((node) => node.displayMode === "card").length, 0);
assert.equal(graph.nodes.filter((node) => node.labelVisible).length, GRAPH_COMMUNITY_FOCUS_BUDGETS.oversized.maxLabels);
assert.ok(graph.overflow.nodes.hidden > 0);
});
it("keeps stable core anchors unchanged when search boosts change", () => {
const data = budgetGraph(200, 1200);
const baseline = buildRenderableGraph(data, { theme: "shan-shui" });
const searched = buildRenderableGraph(data, {
theme: "shan-shui",
searchResultIds: data.nodes.slice(120).map((node) => node.id)
});
assert.deepEqual(searched.importance.stableCoreNodeIds, baseline.importance.stableCoreNodeIds);
assert.deepEqual(searched.importance.stableSkeletonEdgeIds, baseline.importance.stableSkeletonEdgeIds);
assert.ok(searched.importance.temporaryBoostNodeIds.includes("n199"));
assert.equal(searched.nodes.find((node) => node.id === "n199")?.coreAnchor, baseline.nodes.find((node) => node.id === "n199")?.coreAnchor);
});
it("lets search and selection boost visibility without rewriting stable core identity", () => {
const data = budgetGraph(200, 1200);
const baseline = buildRenderableGraph(data, { theme: "shan-shui" });
const boosted = buildRenderableGraph(data, {
theme: "shan-shui",
selection: { kind: "node", id: "n199" },
searchResultIds: ["n199"]
});
const node = boosted.nodes.find((item) => item.id === "n199");
assert.ok(node);
assert.deepEqual(boosted.importance.stableCoreNodeIds, baseline.importance.stableCoreNodeIds);
assert.equal(node.labelVisible, true);
assert.ok(node.temporaryBoost > 0);
assert.equal(node.coreAnchor, baseline.nodes.find((item) => item.id === "n199")?.coreAnchor);
});
it("keeps many search hits, many pins, and a pressured selected object inside budget caps", () => {
const data = budgetGraph(200, 1200);
const pins = Object.fromEntries(
data.nodes.slice(80).map((node) => [node.source_path || `wiki/budget/${node.id}.md`, { x: 700, y: 420, coordinateSpace: "world" as const }])
);
const graph = buildRenderableGraph(data, {
theme: "shan-shui",
selection: { kind: "node", id: "n199" },
searchResultIds: data.nodes.map((node) => node.id),
pins
});
assert.ok(graph.importance.temporaryBoostNodeIds.length > GRAPH_RENDER_BUDGETS.global.maxLabels);
assert.ok(graph.budget.usage.maxLabels <= GRAPH_RENDER_BUDGETS.global.maxLabels);
assert.ok(graph.budget.usage.maxVisibleEdges <= GRAPH_RENDER_BUDGETS.global.maxVisibleEdges);
assert.ok(graph.budget.usage.maxInteractionUpdates <= GRAPH_RENDER_BUDGETS.global.maxInteractionUpdates);
assert.equal(graph.nodes.find((node) => node.id === "n199")?.labelVisible, true);
assert.ok(graph.overflow.labels.hidden > 0);
});
it("keeps crowded accepted global graphs as sparse point maps without aggregation", () => {
const data = densePointMapGraph();
const graph = buildRenderableGraph(data, {
theme: "shan-shui",
selection: { kind: "node", id: "dense-1999" },
searchResultIds: ["dense-20", "dense-120", "dense-1220"],
pins: {
"wiki/dense/dense-40.md": { x: 860, y: 500, coordinateSpace: "world" },
"wiki/dense/dense-1440.md": { x: 920, y: 540, coordinateSpace: "world" }
},
aggregationMarkers: [
{
id: "dense-aggregation",
label: "Dense aggregation should stay hidden",
communityId: "dense-community-0",
nodeIds: data.nodes.slice(0, 200).map((node) => node.id),
totalCount: 200
}
]
});
const selected = graph.nodes.find((node) => node.id === "dense-1999");
const searched = ["dense-20", "dense-120", "dense-1220"].map((id) => graph.nodes.find((node) => node.id === id));
const pinned = ["dense-40", "dense-1440"].map((id) => graph.nodes.find((node) => node.id === id));
const ordinaryNodes = graph.nodes.filter((node) => !node.selected && node.temporaryBoost === 0 && !node.coreAnchor);
const weakEdge = graph.edges.find((edge) => edge.id === "dense-selected-weak");
const strongEdge = graph.edges.find((edge) => edge.id === "dense-selected-strong");
assert.equal(graph.counts.visibleNodes, 2000);
assert.equal(graph.counts.totalEdges, 3996);
assert.equal(graph.budget.view, "global");
assert.equal(graph.densityMode, "overview");
assert.equal(graph.aggregationContainers.length, 0);
assert.equal(graph.budget.usage.maxCards, 0);
assert.equal(graph.nodes.filter((node) => node.displayMode === "card").length, 0);
assert.ok(ordinaryNodes.length > 1500);
assert.ok(ordinaryNodes.every((node) => node.displayMode === "overview"), "ordinary dense nodes should stay in overview point mode");
assert.ok(graph.budget.usage.maxLabels <= GRAPH_RENDER_BUDGETS.global.maxLabels);
assert.ok(graph.nodes.filter((node) => node.labelVisible).length <= GRAPH_RENDER_BUDGETS.global.maxLabels);
assert.ok(graph.nodes.filter((node) => node.labelVisible).length / graph.nodes.length <= 0.03);
assert.ok(graph.budget.usage.maxVisibleEdges <= GRAPH_RENDER_BUDGETS.global.maxVisibleEdges);
assert.ok(graph.budget.usage.maxInteractionUpdates <= GRAPH_RENDER_BUDGETS.global.maxInteractionUpdates);
assert.ok(graph.interaction.updatedObjects <= GRAPH_RENDER_BUDGETS.global.maxInteractionUpdates);
assert.ok(selected);
assert.equal(selected.selected, true);
assert.equal(selected.labelVisible, true);
assert.ok(searched.every(Boolean));
assert.ok(searched.every((node) => node?.labelVisible === true));
assert.ok(pinned.every(Boolean));
assert.ok(pinned.every((node) => node?.labelVisible === true));
assert.ok(graph.importance.stableCoreNodeIds.every((id) => graph.nodes.some((node) => node.id === id)), "core anchors should stay visible");
assert.ok(weakEdge);
assert.ok(strongEdge);
assert.ok(weakEdge.opacity < strongEdge.opacity, "weak dense edges should be faded behind strong edges");
assert.ok(weakEdge.strokeWidth < strongEdge.strokeWidth, "weak dense edges should be thinner than strong edges");
});
it("does not produce visible aggregation containers in the normal render model", () => {
const data = budgetGraph(20, 40);
const pins = {
"wiki/budget/n3.md": { x: 700, y: 420, coordinateSpace: "world" as const }
};
const graph = buildRenderableGraph(data, {
theme: "shan-shui",
selection: { kind: "node", id: "n2" },
searchResultIds: ["n1", "n4"],
pins,
aggregationMarkers: [
{
id: "agg-c1",
label: "Budget container",
communityId: "c1",
nodeIds: ["n1", "n2", "n3", "n4"],
totalCount: 12
}
]
});
assert.deepEqual(graph.aggregationContainers, []);
});
it("marks moderate community quality without auxiliary organization modes", () => {
const graph = buildRenderableGraph(manyTinyCommunitiesGraph(), { theme: "shan-shui" });
assert.equal(graph.communityQuality.level, "moderate");
assert.equal(graph.communityQuality.boundaryCertainty, "reduced");
assert.equal(graph.communityQuality.warning, "moderate-community-quality");
assert.deepEqual(graph.communityQuality.signals.map((signal) => signal.id), ["many-tiny-communities"]);
assert.deepEqual(graph.communityQuality.auxiliaryViews, []);
});
it("lowers boundary certainty and exposes only core connectivity for poor community quality", () => {
const graph = buildRenderableGraph(oversizedWeakCommunityGraph(), { theme: "shan-shui" });
assert.equal(graph.communityQuality.level, "poor");
assert.equal(graph.communityQuality.boundaryCertainty, "low");
assert.deepEqual(graph.communityQuality.auxiliaryViews, [
{ id: "core-structure-connectivity", label: "核心结构 / 连通性" }
]);
assert.deepEqual(
graph.communityQuality.signals.map((signal) => signal.id),
["oversized-community", "weak-community-labels", "abnormal-community-count"]
);
assert.ok(graph.communities.every((community) => community.boundaryCertainty === "low"));
assert.deepEqual(graph.communityQuality.auxiliaryViews.map((view) => view.id), ["core-structure-connectivity"]);
assert.equal(graph.communityQuality.auxiliaryViews.some((view) => /type|source|time/i.test(view.id)), false);
});
it("detects mixed cross-community edges as an explicit quality signal", () => {
const quality = evaluateCommunityQuality(mixedCrossCommunityGraph());
assert.equal(quality.level, "poor");
assert.ok(quality.signals.some((signal) => signal.id === "mixed-cross-community-edges"));
assert.deepEqual(quality.auxiliaryViews.map((view) => view.id), ["core-structure-connectivity"]);
});
it("enters a community focus view by hiding nodes outside the selected community", () => {
const graph = buildRenderableGraph(sampleGraph(), {
theme: "shan-shui",
focus: { kind: "community", id: "c1" }
});
assert.deepEqual(graph.nodes.map((node) => node.id), ["topic", "entity"]);
assert.deepEqual(graph.edges.map((edge) => edge.id), ["e1"]);
assert.equal(graph.counts.visibleNodes, 2);
assert.equal(graph.counts.totalNodes, 4);
assert.equal(graph.focus?.kind, "community");
assert.equal(graph.focus?.id, "c1");
assert.deepEqual(
communityWashStates(graph),
[["c1", true], ["c2", false], ["c3", false]]
);
});
it("filters visible nodes by graph node type and stacks with community focus", () => {
const graph = buildRenderableGraph(sampleGraph(), {
theme: "shan-shui",
focus: { kind: "community", id: "c1" },
typeFilters: {
entity: true,
topic: false,
source: true
}
});
assert.deepEqual(graph.nodes.map((node) => node.id), ["entity"]);
assert.deepEqual(graph.edges, []);
assert.equal(graph.counts.visibleNodes, 1);
assert.equal(graph.typeFilters.topic, false);
});
it("preserves community focus and type filters when positions are reapplied", () => {
const graph = buildRenderableGraph(sampleGraph(), {
theme: "shan-shui",
focus: { kind: "community", id: "c1" },
typeFilters: {
entity: true,
topic: false,
source: true
},
positions: {
topic: { x: 500, y: 500 },
entity: { x: 420, y: 320 },
source: { x: 900, y: 120 },
island: { x: 120, y: 120 }
}
});
assert.deepEqual(graph.nodes.map((node) => node.id), ["entity"]);
assert.deepEqual(graph.edges, []);
assert.equal(graph.focus?.id, "c1");
assert.equal(graph.typeFilters.topic, false);
assert.deepEqual(
communityWashStates(graph),
[["c1", true], ["c2", false], ["c3", false]]
);
});
it("keeps community wash around the member cluster instead of chasing an outlier", () => {
const graph = buildRenderableGraph(outlierCommunityGraph(), { theme: "shan-shui" });
const community = graph.communities.find((item) => item.id === "c1");
assert.ok(community?.wash);
assert.equal(community.nodeCount, 5);
assert.ok(community.wash.cx > 300, `wash center should respond toward the outlier, got ${community.wash.cx}`);
assert.ok(community.wash.cx < 430, `wash center should stay near the clustered members, got ${community.wash.cx}`);
assert.equal(community.wash.rx, 190);
assert.equal(community.wash.ry, 142.8);
});
it("keeps community membership stable when a pinned member is outside the wash cap", () => {
const graph = buildRenderableGraph(outlierCommunityGraph(), {
theme: "shan-shui",
pins: {
"wiki/outlier.md": { x: 980, y: 650 }
}
});
const community = graph.communities.find((item) => item.id === "c1");
const outlier = graph.nodes.find((node) => node.id === "outlier");
assert.ok(community?.wash);
assert.ok(outlier);
assert.equal(outlier.community, "c1");
assert.equal(community.nodeCount, 5);
assert.equal(community.wash.rx, 190);
assert.equal(community.wash.ry, 142.8);
assert.ok(outlier.point.x > community.wash.cx + community.wash.rx, "pinned member may sit outside the capped visual wash");
});
it("lets a dragged core member reshape the wash within caps without changing membership", () => {
const before = buildRenderableGraph(outlierCommunityGraph(), { theme: "shan-shui" });
const after = buildRenderableGraph(outlierCommunityGraph(), {
theme: "shan-shui",
positions: {
"core-a": { x: 980, y: 650 }
}
});
const beforeCommunity = before.communities.find((item) => item.id === "c1");
const afterCommunity = after.communities.find((item) => item.id === "c1");
const dragged = after.nodes.find((node) => node.id === "core-a");
assert.ok(beforeCommunity?.wash);
assert.ok(afterCommunity?.wash);
assert.ok(dragged);
assert.equal(dragged.community, "c1");
assert.equal(afterCommunity.nodeCount, beforeCommunity.nodeCount);
assert.notEqual(afterCommunity.wash.cx, beforeCommunity.wash.cx);
assert.ok(afterCommunity.wash.cx > beforeCommunity.wash.cx, "wash should move toward the dragged member");
assert.equal(afterCommunity.wash.rx, 190);
assert.equal(afterCommunity.wash.ry, 142.8);
});
it("preserves community focus after a member is dragged beyond the wash cap", () => {
const graph = buildRenderableGraph(outlierCommunityGraph(), {
theme: "shan-shui",
focus: { kind: "community", id: "c1" },
positions: {
outlier: { x: 980, y: 650 }
}
});
const community = graph.communities.find((item) => item.id === "c1");
assert.deepEqual(graph.nodes.map((node) => node.id).sort(), ["core-a", "core-b", "core-c", "core-d", "outlier"]);
assert.ok(community?.wash);
assert.equal(graph.focus?.id, "c1");
assert.equal(community.wash.rx, 190);
assert.equal(community.wash.ry, 142.8);
});
it("preserves live drag positions outside the old default world", () => {
const graph = buildRenderableGraph(outlierCommunityGraph(), {
theme: "shan-shui",
positions: {
outlier: { x: 1240, y: 816 }
}
});
const outlier = graph.nodes.find((node) => node.id === "outlier");
assert.ok(outlier);
assert.deepEqual(outlier.point, { x: 1240, y: 816 });
assert.equal(outlier.x, 93.939);
assert.equal(outlier.y, 91.071);
assert.equal(graph.worldBounds.maxX, 1320);
assert.equal(graph.worldBounds.maxY, 896);
assert.equal(outlier.community, "c1");
});
it("preserves pinned positions outside the old default world by expanding render bounds", () => {
const graph = buildRenderableGraph(outlierCommunityGraph(), {
theme: "shan-shui",
pins: {
"wiki/outlier.md": { x: 1240, y: 816, coordinateSpace: "world" }
}
});
const outlier = graph.nodes.find((node) => node.id === "outlier");
assert.ok(outlier);
assert.deepEqual(outlier.point, { x: 1240, y: 816 });
assert.equal(outlier.x, 93.939);
assert.equal(outlier.y, 91.071);
assert.equal(graph.worldBounds.width, 1320);
assert.equal(graph.worldBounds.height, 896);
});
it("does not let expanded render bounds enlarge the community wash cap", () => {
const graph = buildRenderableGraph(outlierCommunityGraph(), {
theme: "shan-shui",
positions: {
outlier: { x: 5000, y: 3400 }
}
});
const community = graph.communities.find((item) => item.id === "c1");
const outlier = graph.nodes.find((node) => node.id === "outlier");
assert.ok(community?.wash);
assert.ok(outlier);
assert.ok(graph.worldBounds.width > 5000, `world bounds should expand to include the outlier, got ${graph.worldBounds.width}`);
assert.ok(graph.worldBounds.height > 3400, `world bounds should expand to include the outlier, got ${graph.worldBounds.height}`);
assert.equal(community.wash.rx, 190);
assert.equal(community.wash.ry, 142.8);
assert.ok(outlier.point.x > community.wash.cx + community.wash.rx, "the node can sit outside the capped visual wash");
assert.equal(outlier.community, "c1");
});
it("normalizes ungrouped nodes into a selectable render community", () => {
const graph = buildRenderableGraph(graphFixtureWithUngroupedNodes(), {
selection: { kind: "community", id: UNGROUPED_COMMUNITY_ID }
});
const ungrouped = graph.communities.find((community) => community.id === UNGROUPED_COMMUNITY_ID);
assert.equal(ungrouped?.label, UNGROUPED_COMMUNITY_LABEL);
assert.ok(ungrouped?.wash, "ungrouped community should be clickable when its nodes are visible");
assert.deepEqual(
graph.nodes.filter((node) => node.community === UNGROUPED_COMMUNITY_ID && node.selected).map((node) => node.id),
["loose-a", "loose-b"]
);
});
});
function communityWashStates(graph: ReturnType<typeof buildRenderableGraph>): Array<[string, boolean]> {
return graph.communities
.map((community): [string, boolean] => [community.id, Boolean(community.wash)])
.sort(([left], [right]) => left.localeCompare(right));
}
describe("screen-effective density", () => {
it("uses viewport scale to choose the effective density mode", () => {
assert.equal(screenEffectiveDensityMode(120, 1), "compact-card");
assert.equal(screenEffectiveDensityMode(120, 2), "card");
assert.equal(screenEffectiveDensityMode(120, 0.5), "point-plus-focus");
assert.equal(screenEffectiveDensityMode(30, 0.5), "compact-card");
});
it("maps effective density to node display without changing selected cards", () => {
const node = {
selected: false,
labelVisible: false,
visualRole: "map-pin" as const
};
const labeledNode = {
selected: false,
labelVisible: true,
visualRole: "map-pin" as const
};
const selectedNode = {
selected: true,
labelVisible: false,
visualRole: "map-pin" as const
};
assert.equal(nodeDisplayModeForDensity(node, "card"), "card");
assert.equal(nodeDisplayModeForDensity(node, "compact-card"), "compact-card");
assert.equal(nodeDisplayModeForDensity(node, "point-plus-focus"), "point");
assert.equal(nodeDisplayModeForDensity(labeledNode, "point-plus-focus"), "compact-card");
assert.equal(nodeDisplayModeForDensity(selectedNode, "overview"), "card");
});
});
describe("edge drawing helpers", () => {
it("keeps graph-wash stroke strength bounds", () => {
assert.equal(edgeStrokeWidth({ weight: 0 }), 1.1);
assert.equal(edgeStrokeWidth({ weight: 1 }), 2.9);
assert.equal(edgeOpacity({ weight: 0 }), 0.32);
assert.equal(edgeOpacity({ weight: 1 }), 0.76);
});
it("maps relation type to a separate visual class from confidence", () => {
assert.equal(edgeRelationClass("实现"), "relation-implementation");
assert.equal(edgeRelationClass("依赖"), "relation-dependency");
assert.equal(edgeRelationClass("衍生"), "relation-derivation");
assert.equal(edgeRelationClass("对比"), "relation-contrast");
assert.equal(edgeRelationClass("矛盾"), "relation-conflict");
assert.equal(edgeRelationClass("未知"), "relation-dependency");
});
it("keeps global relation edges subdued and focused edges fuller", () => {
assert.equal(edgeVisualStrokeWidth({ weight: 1 }, false), 1.7);
assert.equal(edgeVisualStrokeWidth({ weight: 1 }, true), 2.9);
assert.equal(edgeVisualOpacity({ weight: 1 }, false), 0.42);
assert.equal(edgeVisualOpacity({ weight: 1 }, true), 0.76);
});
it("builds a curved path from atlas node coordinates", () => {
const path = makeEdgePath(
{ id: "a", label: "A", type: "entity", kind: "概念", community: "c1", x: 10, y: 20 },
{ id: "b", label: "B", type: "entity", kind: "概念", community: "c1", x: 60, y: 70 },
{ weight: 0.5 }
);
assert.equal(path, "M 100 136 Q 274 284 600 476");
});
});
function graphFixtureWithUngroupedNodes(): GraphData {
return {
meta: {
build_date: "2026-06-18T00:00:00.000Z",
wiki_title: "Ungrouped render graph",
total_nodes: 4,
total_edges: 1
},
nodes: [
{ id: "a", label: "Alpha", type: "topic", community: "alpha", source_path: "wiki/alpha/a.md", weight: 2 },
{ id: "b", label: "Beta", type: "entity", community: "alpha", source_path: "wiki/alpha/b.md" },
{ id: "loose-a", label: "Loose A", type: "topic", community: null, source_path: "wiki/loose/a.md", score: 2 },
{ id: "loose-b", label: "Loose B", type: "entity", source_path: "wiki/loose/b.md", weight: 1 }
],
edges: [
{ id: "a-b", from: "a", to: "b", type: "EXTRACTED", relation_type: "实现", weight: 0.6 }
],
learning: {
version: 1,
entry: { recommended_start_node_id: "a", recommended_start_reason: "hub", default_mode: "global" },
views: {
path: { enabled: false, start_node_id: null, node_ids: [], degraded: true },
community: { enabled: false, community_id: null, label: null, node_ids: [], is_weak: false, degraded: true },
global: { enabled: true, node_ids: ["a", "b"], degraded: false }
},
communities: [
{ id: "alpha", label: "Alpha", node_count: 2, color_index: 0, members: ["a", "b"] }
]
}
};
}