665 lines
25 KiB
TypeScript
665 lines
25 KiB
TypeScript
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
buildCommunityAggregationMarkers,
|
|
summarizeExcludedGraphObject,
|
|
summarizeGraphCommunity,
|
|
summarizeGraphGlobal,
|
|
summarizeGraphNode,
|
|
summarizeGraphSearchResults,
|
|
summarizeUnavailableGraphObject
|
|
} from "../src/summary";
|
|
import {
|
|
createGraphFacadeFromRenderer,
|
|
createGraphFacadeRouteManager,
|
|
type GraphFacadeRenderer,
|
|
type GraphFacadeRouteRendererFactoryInput,
|
|
type GraphFacadeState
|
|
} from "../src/facade";
|
|
import type { GraphAggregationMarker, GraphData, GraphSummaryCommand, GraphSummaryObjectRef, GraphTypeFilters, PinMap, SelectionInput, ThemeId } from "../src/types";
|
|
|
|
describe("graph summary contract", () => {
|
|
it("preserves node identity, selection, search hits, Pin hints, relations, and aggregation markers", () => {
|
|
const pins: PinMap = {
|
|
"wiki/alpha/a.md": { x: 12, y: 34, coordinateSpace: "world" }
|
|
};
|
|
const markers: GraphAggregationMarker[] = [
|
|
{
|
|
id: "agg-alpha",
|
|
label: "Alpha overflow",
|
|
communityId: "alpha",
|
|
nodeIds: ["a", "b"],
|
|
selectedNodeIds: ["a"],
|
|
searchResultIds: ["a"],
|
|
pinnedNodeIds: ["a"],
|
|
totalCount: 2
|
|
}
|
|
];
|
|
|
|
const summary = summarizeGraphNode(graphFixture(), "a", {
|
|
selection: { kind: "node", id: "a" },
|
|
searchResultIds: ["a", "d"],
|
|
pins,
|
|
aggregationMarkers: markers
|
|
});
|
|
|
|
assert.equal(summary.kind, "node-summary");
|
|
assert.equal(summary.nodeId, "a");
|
|
assert.equal(summary.object.nodeId, "a");
|
|
assert.equal(summary.communityId, "alpha");
|
|
assert.equal(summary.searchHit, true);
|
|
assert.deepEqual(summary.pinHint, {
|
|
nodeId: "a",
|
|
wikiPath: "wiki/alpha/a.md",
|
|
pinned: true,
|
|
position: { x: 12, y: 34, coordinateSpace: "world" }
|
|
});
|
|
assert.deepEqual(summary.selection.selectedNodeIds, ["a"]);
|
|
assert.deepEqual(summary.selection.selectedCommunityIds, ["alpha"]);
|
|
assert.equal(summary.selection.containsCurrentObject, true);
|
|
assert.deepEqual(summary.aggregationMarkers.map((marker) => marker.id), ["agg-alpha"]);
|
|
assert.deepEqual(summary.strongestRelations.map((relation) => relation.edgeId), ["a-c", "a-b"]);
|
|
assert.deepEqual(summary.bridgeRelations.map((relation) => relation.edgeId), ["a-c"]);
|
|
});
|
|
|
|
it("keeps open-detail/read distinct from enter-community overview", () => {
|
|
const node = summarizeGraphNode(graphFixture(), "a");
|
|
const community = summarizeGraphCommunity(graphFixture(), "alpha");
|
|
|
|
assert.equal(node.kind, "node-summary");
|
|
assert.equal(community.kind, "community-summary");
|
|
assert.deepEqual(commandKinds(node.commands), ["open-detail-read", "select-neighbors", "set-fixed-position", "enter-community"]);
|
|
assert.deepEqual(commandKinds(community.commands), ["enter-community"]);
|
|
|
|
const openDetail = node.commands.find((command) => command.kind === "open-detail-read");
|
|
const selectNeighbors = node.commands.find((command) => command.kind === "select-neighbors");
|
|
const enterCommunity = node.commands.find((command) => command.kind === "enter-community");
|
|
|
|
assert.deepEqual(openDetail, {
|
|
kind: "open-detail-read",
|
|
nodeId: "a",
|
|
path: "wiki/alpha/a.md",
|
|
label: "打开详情"
|
|
});
|
|
assert.deepEqual(selectNeighbors, {
|
|
kind: "select-neighbors",
|
|
nodeId: "a",
|
|
label: "+邻居"
|
|
});
|
|
assert.deepEqual(enterCommunity, {
|
|
kind: "enter-community",
|
|
communityId: "alpha",
|
|
label: "进入社区"
|
|
});
|
|
});
|
|
|
|
it("models fixed and unfixed position as explicit commands instead of node double-click behavior", () => {
|
|
const unpinned = summarizeGraphNode(graphFixture(), "b");
|
|
const pinned = summarizeGraphNode(graphFixture(), "b", {
|
|
pins: {
|
|
"wiki/alpha/b.md": { x: 4, y: 8, coordinateSpace: "world" }
|
|
}
|
|
});
|
|
|
|
assert.equal(unpinned.kind, "node-summary");
|
|
assert.equal(pinned.kind, "node-summary");
|
|
|
|
assert.deepEqual(fixedPositionCommands(unpinned.commands), [
|
|
{
|
|
kind: "set-fixed-position",
|
|
mode: "fix",
|
|
nodeId: "b",
|
|
wikiPath: "wiki/alpha/b.md",
|
|
label: "固定位置"
|
|
}
|
|
]);
|
|
assert.deepEqual(fixedPositionCommands(pinned.commands), [
|
|
{
|
|
kind: "set-fixed-position",
|
|
mode: "unfix",
|
|
nodeId: "b",
|
|
wikiPath: "wiki/alpha/b.md",
|
|
label: "取消固定位置"
|
|
}
|
|
]);
|
|
assert.equal(commandKinds(pinned.commands).includes("node-double-click"), false);
|
|
});
|
|
|
|
it("preserves community ids, core nodes, search result ids, Pin hints, and selection state", () => {
|
|
const summary = summarizeGraphCommunity(graphFixture(), "alpha", {
|
|
selection: { kind: "community", id: "alpha" },
|
|
searchResultIds: ["b", "d"],
|
|
pins: {
|
|
"wiki/alpha/a.md": { x: 1, y: 2, coordinateSpace: "world" }
|
|
},
|
|
aggregationMarkers: [
|
|
{ id: "agg-alpha", communityId: "alpha", nodeIds: ["a", "b"], selectedNodeIds: ["a"], searchResultIds: ["b"] }
|
|
]
|
|
});
|
|
|
|
assert.equal(summary.kind, "community-summary");
|
|
assert.equal(summary.communityId, "alpha");
|
|
assert.equal(summary.object.communityId, "alpha");
|
|
assert.deepEqual(summary.coreNodeIds, ["a", "b"]);
|
|
assert.deepEqual(summary.searchResultIds, ["b"]);
|
|
assert.deepEqual(summary.pinHints.map((hint) => hint.nodeId), ["a"]);
|
|
assert.deepEqual(summary.selection.selectedNodeIds, ["a", "b"]);
|
|
assert.deepEqual(summary.selection.selectedCommunityIds, ["alpha"]);
|
|
assert.equal(summary.selection.containsCurrentObject, true);
|
|
assert.deepEqual(summary.aggregationMarkers.map((marker) => marker.id), ["agg-alpha"]);
|
|
});
|
|
|
|
it("lets legacy node-only community data enter real communities", () => {
|
|
const data = graphFixture();
|
|
data.learning = data.learning ? { ...data.learning, communities: [] } : undefined;
|
|
|
|
const summary = summarizeGraphCommunity(data, "alpha");
|
|
|
|
assert.equal(summary.kind, "community-summary");
|
|
assert.equal(summary.canEnterCommunity, true);
|
|
assert.deepEqual(summary.commands.map((command) => command.kind), ["enter-community"]);
|
|
});
|
|
|
|
it("summarizes the ungrouped virtual community as a community payload", () => {
|
|
const data = graphFixtureWithUngroupedNodes();
|
|
const summary = summarizeGraphCommunity(data, "_none", {
|
|
selection: { kind: "community", id: "_none" },
|
|
searchResultIds: ["loose-a"]
|
|
});
|
|
|
|
assert.equal(summary.kind, "community-summary");
|
|
assert.equal(summary.communityId, "_none");
|
|
assert.equal(summary.label, "未分组");
|
|
assert.equal(summary.nodeCount, 2);
|
|
assert.deepEqual(summary.facts, {
|
|
pageCount: 2,
|
|
internalLinkCount: 0,
|
|
communityCount: 1,
|
|
isolatedCount: 2
|
|
});
|
|
assert.equal(summary.structureState, "ungrouped");
|
|
assert.equal(summary.canEnterCommunity, false);
|
|
assert.equal(summary.description, "这些页面暂未形成明确社区。你可以让 agent 探索它们之间是否存在潜在关系。");
|
|
assert.deepEqual(summary.searchResultIds, ["loose-a"]);
|
|
assert.deepEqual(summary.selection.selectedNodeIds, ["loose-a", "loose-b"]);
|
|
assert.deepEqual(summary.selection.selectedCommunityIds, ["_none"]);
|
|
assert.deepEqual(summary.commands.map((command) => command.kind), []);
|
|
assert.deepEqual(summary.coreNodes.map((node) => node.nodeId), ["loose-a", "loose-b"]);
|
|
assert.deepEqual(summary.coreNodes.map((node) => node.label), ["Loose A", "Loose B"]);
|
|
});
|
|
|
|
it("counts internal links inside the ungrouped virtual community", () => {
|
|
const data = graphFixtureWithLinkedUngroupedNodes();
|
|
const summary = summarizeGraphCommunity(data, "_none", {
|
|
selection: { kind: "community", id: "_none" }
|
|
});
|
|
|
|
assert.equal(summary.kind, "community-summary");
|
|
assert.deepEqual(summary.facts, {
|
|
pageCount: 2,
|
|
internalLinkCount: 1,
|
|
communityCount: 1,
|
|
isolatedCount: 0
|
|
});
|
|
assert.equal(summary.structureState, "ungrouped");
|
|
});
|
|
|
|
it("classifies loose communities when the community has a single node", () => {
|
|
const data = graphFixtureWithSingleNodeCommunity();
|
|
const summary = summarizeGraphCommunity(data, "gamma", {
|
|
selection: { kind: "community", id: "gamma" }
|
|
});
|
|
|
|
assert.equal(summary.structureState, "loose");
|
|
assert.equal(summary.description, "这组页面结构还比较松散。你可以先找知识缺口,也可以继续探索潜在关系。");
|
|
});
|
|
|
|
it("classifies a real community as loose when it has no internal links", () => {
|
|
const data = graphFixtureWithEmptyCommunityGamma();
|
|
const summary = summarizeGraphCommunity(data, "gamma", {
|
|
selection: { kind: "community", id: "gamma" }
|
|
});
|
|
|
|
assert.equal(summary.structureState, "loose");
|
|
assert.equal(summary.facts.internalLinkCount, 0);
|
|
assert.equal(summary.facts.isolatedCount, 0);
|
|
});
|
|
|
|
it("treats the isolated-count threshold boundary between clear and loose", () => {
|
|
// nodeCount = 6, floor(nodeCount/2) = 3.
|
|
// clear variant: exactly 3 isolated nodes (=== floor) -> clear.
|
|
const clear = summarizeGraphCommunity(graphFixtureWithIsolatedBoundary(false), "gamma", {
|
|
selection: { kind: "community", id: "gamma" }
|
|
});
|
|
assert.equal(clear.facts.pageCount, 6);
|
|
assert.equal(clear.facts.isolatedCount, 3);
|
|
assert.equal(clear.structureState, "clear");
|
|
|
|
// loose variant: 4 isolated nodes (=== floor + 1) -> loose.
|
|
const loose = summarizeGraphCommunity(graphFixtureWithIsolatedBoundary(true), "gamma", {
|
|
selection: { kind: "community", id: "gamma" }
|
|
});
|
|
assert.equal(loose.facts.pageCount, 6);
|
|
assert.equal(loose.facts.isolatedCount, 4);
|
|
assert.equal(loose.structureState, "loose");
|
|
});
|
|
|
|
it("builds community aggregation markers for large communities with pinned-node metadata", () => {
|
|
const markers = buildCommunityAggregationMarkers(graphFixture(), {
|
|
minCommunitySize: 2,
|
|
pins: {
|
|
"wiki/alpha/a.md": { x: 3, y: 4, coordinateSpace: "world" }
|
|
}
|
|
});
|
|
|
|
assert.deepEqual(markers.map((marker) => marker.id), ["community-container:alpha", "community-container:beta"]);
|
|
assert.deepEqual(markers[0], {
|
|
id: "community-container:alpha",
|
|
label: "Alpha",
|
|
communityId: "alpha",
|
|
nodeIds: ["a", "b"],
|
|
pinnedNodeIds: ["a"],
|
|
totalCount: 2
|
|
});
|
|
});
|
|
|
|
it("preserves global, search, excluded, and unavailable payload identity fields", () => {
|
|
const pins: PinMap = {
|
|
"wiki/alpha/a.md": { x: 1, y: 1, coordinateSpace: "world" },
|
|
"wiki/beta/c.md": { x: 2, y: 2, coordinateSpace: "world" }
|
|
};
|
|
const aggregationMarkers: GraphAggregationMarker[] = [
|
|
{ id: "agg-search", communityId: "beta", nodeIds: ["c", "d"], searchResultIds: ["d"], pinnedNodeIds: ["c"] }
|
|
];
|
|
const global = summarizeGraphGlobal(graphFixture(), {
|
|
selection: { kind: "node", id: "c" },
|
|
searchResultIds: ["d"],
|
|
pins,
|
|
aggregationMarkers
|
|
});
|
|
const search = summarizeGraphSearchResults(graphFixture(), "delta", ["d", "missing"], {
|
|
selection: { kind: "node", id: "d" },
|
|
pins,
|
|
aggregationMarkers
|
|
});
|
|
const excluded = summarizeExcludedGraphObject(
|
|
graphFixture(),
|
|
{ kind: "aggregation", aggregationId: "agg-search", communityId: "beta", nodeIds: ["c", "d"] },
|
|
"aggregation",
|
|
{ selection: { kind: "node", id: "c" }, pins, searchResultIds: ["d"], aggregationMarkers }
|
|
);
|
|
const unavailable = summarizeUnavailableGraphObject(
|
|
graphFixture(),
|
|
{ kind: "node", nodeId: "missing" },
|
|
"missing-node",
|
|
{ selection: { kind: "node", id: "c" }, searchResultIds: ["missing"] }
|
|
);
|
|
|
|
assert.deepEqual(global.coreNodeIds, ["a", "c", "b", "d"]);
|
|
assert.deepEqual(global.searchResultIds, ["d"]);
|
|
assert.deepEqual(global.pinHints.map((hint) => hint.nodeId), ["a", "c"]);
|
|
assert.deepEqual(global.selection.selectedNodeIds, ["c"]);
|
|
assert.deepEqual(global.aggregationMarkers.map((marker) => marker.id), ["agg-search"]);
|
|
|
|
assert.deepEqual(search.searchResultIds, ["d", "missing"]);
|
|
assert.deepEqual(search.visibleResultIds, ["d"]);
|
|
assert.deepEqual(search.unavailableResultIds, ["missing"]);
|
|
assert.deepEqual(search.commands.map((command) => command.kind), ["show-this-object"]);
|
|
assert.deepEqual(search.aggregationMarkers.map((marker) => marker.id), ["agg-search"]);
|
|
|
|
assert.equal(excluded.object.kind, "aggregation");
|
|
assert.equal(excluded.object.aggregationId, "agg-search");
|
|
assert.equal(excluded.reason, "aggregation");
|
|
assert.deepEqual(excluded.searchResultIds, ["d"]);
|
|
assert.deepEqual(excluded.pinHints.map((hint) => hint.nodeId), ["c"]);
|
|
assert.deepEqual(commandKinds(excluded.commands), ["show-this-object", "clear-temporary-object-display"]);
|
|
|
|
assert.equal(unavailable.kind, "unavailable-object");
|
|
assert.equal(unavailable.object.kind, "node");
|
|
assert.equal(unavailable.object.nodeId, "missing");
|
|
assert.equal(unavailable.reason, "missing-node");
|
|
assert.deepEqual(unavailable.searchResultIds, ["missing"]);
|
|
assert.deepEqual(unavailable.selection.selectedNodeIds, ["c"]);
|
|
});
|
|
|
|
it("facade summaries inherit current route selection, search, pins, aggregation, and temporary object state", () => {
|
|
const data = graphFixture();
|
|
const renderer = createFakeRenderer();
|
|
const state: GraphFacadeState = {
|
|
data,
|
|
pins: {
|
|
"wiki/beta/d.md": { x: 8, y: 9, coordinateSpace: "world" }
|
|
},
|
|
selection: { kind: "node", id: "d" },
|
|
searchQuery: "delta",
|
|
searchResultIds: ["d"],
|
|
typeFilters: { topic: true, entity: true, source: false },
|
|
aggregationMarkers: [
|
|
{ id: "agg-beta", communityId: "beta", nodeIds: ["c", "d"], selectedNodeIds: ["d"], searchResultIds: ["d"], pinnedNodeIds: ["d"] }
|
|
],
|
|
temporaryObject: { kind: "node", nodeId: "d" }
|
|
};
|
|
const engine = createGraphFacadeFromRenderer({ dataset: {} }, renderer, { data, theme: "shan-shui" }, state);
|
|
|
|
engine.setTypeFilters({ topic: true, entity: true, source: false });
|
|
const global = engine.summarizeGlobal();
|
|
const excluded = engine.summarizeExcludedObject({ kind: "node", nodeId: "d" }, "filter");
|
|
|
|
assert.deepEqual(global.selection.selectedNodeIds, ["d"]);
|
|
assert.deepEqual(global.searchResultIds, ["d"]);
|
|
assert.deepEqual(global.pinHints.map((hint) => hint.nodeId), ["d"]);
|
|
assert.deepEqual(global.aggregationMarkers.map((marker) => marker.id), ["agg-beta"]);
|
|
assert.equal(excluded.reason, "filter");
|
|
assert.deepEqual(excluded.selection.selectedNodeIds, ["d"]);
|
|
assert.deepEqual(excluded.searchResultIds, ["d"]);
|
|
assert.deepEqual(excluded.pinHints.map((hint) => hint.nodeId), ["d"]);
|
|
assert.deepEqual(commandKinds(excluded.commands), ["show-this-object", "clear-temporary-object-display"]);
|
|
|
|
engine.clearSelection();
|
|
assert.deepEqual(engine.summarizeGlobal().selection.selectedNodeIds, []);
|
|
});
|
|
|
|
it("summarizes large ungrouped communities without repeated bridge-node scans", () => {
|
|
const bridgeNodes = Array.from({ length: 400 }, (_, index) => ({
|
|
id: `n${index}`,
|
|
label: `Node ${index}`,
|
|
community: null,
|
|
connected_communities: [],
|
|
community_count: 0
|
|
}));
|
|
const data: GraphData = {
|
|
meta: {
|
|
build_date: "2026-06-20T00:00:00.000Z",
|
|
wiki_title: "Large ungrouped",
|
|
total_nodes: 400,
|
|
total_edges: 0
|
|
},
|
|
nodes: bridgeNodes.map((node, index) => ({
|
|
id: node.id,
|
|
label: node.label,
|
|
type: "topic",
|
|
community: null,
|
|
source_path: `wiki/${node.id}.md`,
|
|
score: index % 7,
|
|
weight: index % 5
|
|
})),
|
|
edges: [],
|
|
insights: {
|
|
surprising_connections: [],
|
|
isolated_nodes: [],
|
|
bridge_nodes: bridgeNodes,
|
|
sparse_communities: [],
|
|
meta: {
|
|
degraded: false,
|
|
node_count: 400,
|
|
edge_count: 0,
|
|
max_insight_nodes: 400,
|
|
max_insight_edges: 0
|
|
}
|
|
}
|
|
};
|
|
let bridgeNodeReads = 0;
|
|
Object.defineProperty(data.insights, "bridge_nodes", {
|
|
configurable: true,
|
|
get() {
|
|
bridgeNodeReads += 1;
|
|
return bridgeNodes;
|
|
}
|
|
});
|
|
|
|
const summary = summarizeGraphCommunity(data, "_none");
|
|
|
|
assert.equal(summary.kind, "community-summary");
|
|
assert.equal(summary.coreNodeIds.length, 5);
|
|
assert.equal(bridgeNodeReads, 1);
|
|
});
|
|
|
|
it("route manager carries search query and temporary object state when switching renderers", () => {
|
|
const data = graphFixture();
|
|
const state: GraphFacadeState = {
|
|
data,
|
|
pins: {},
|
|
theme: "shan-shui",
|
|
focus: null,
|
|
typeFilters: {},
|
|
aggregationMarkers: [],
|
|
selection: { kind: "node", id: "c" },
|
|
searchQuery: "",
|
|
searchResultIds: [],
|
|
temporaryObject: null
|
|
};
|
|
const sigmaInputs: GraphFacadeRouteRendererFactoryInput[] = [];
|
|
const communityInputs: GraphFacadeRouteRendererFactoryInput[] = [];
|
|
const manager = createGraphFacadeRouteManager({ dataset: {} } as unknown as HTMLElement, {
|
|
state,
|
|
factories: {
|
|
createSigmaGlobal: (input) => {
|
|
sigmaInputs.push(input);
|
|
return createFakeRenderer();
|
|
},
|
|
createDomSvgCommunity: (input) => {
|
|
communityInputs.push(input);
|
|
return createFakeRenderer();
|
|
},
|
|
createDomSvgSmallFallback: () => createFakeRenderer()
|
|
}
|
|
});
|
|
|
|
sigmaInputs[0].options.callbacks.onVisibilityStateChange?.({
|
|
searchQuery: "delta",
|
|
searchResultIds: ["d"],
|
|
typeFilters: { topic: true, entity: true, source: false },
|
|
temporaryObject: { kind: "node", nodeId: "d" }
|
|
});
|
|
manager.focusCommunity("beta");
|
|
|
|
assert.equal(communityInputs[0].options.searchQuery, "delta");
|
|
assert.deepEqual(communityInputs[0].options.searchResultIds, ["d"]);
|
|
assert.deepEqual(communityInputs[0].options.temporaryObject, { kind: "node", nodeId: "d" });
|
|
assert.deepEqual(communityInputs[0].options.selection, { kind: "node", id: "c" });
|
|
assert.deepEqual(communityInputs[0].options.typeFilters, { topic: true, entity: true, source: false });
|
|
});
|
|
});
|
|
|
|
function commandKinds(commands: GraphSummaryCommand[]): string[] {
|
|
return commands.map((command) => command.kind);
|
|
}
|
|
|
|
function fixedPositionCommands(commands: GraphSummaryCommand[]): GraphSummaryCommand[] {
|
|
return commands.filter((command) => command.kind === "set-fixed-position");
|
|
}
|
|
|
|
function createFakeRenderer(): GraphFacadeRenderer & { calls: unknown[][] } {
|
|
const calls: unknown[][] = [];
|
|
return {
|
|
calls,
|
|
async applyDiff(diff, options) {
|
|
calls.push(["applyDiff", diff, options]);
|
|
},
|
|
isDragging() {
|
|
return false;
|
|
},
|
|
setData(data: GraphData, pins?: PinMap) {
|
|
calls.push(["setData", data, pins]);
|
|
},
|
|
setAggregationMarkers(markers: GraphAggregationMarker[]) {
|
|
calls.push(["setAggregationMarkers", markers]);
|
|
},
|
|
focusNode(path: string) {
|
|
calls.push(["focusNode", path]);
|
|
},
|
|
focusCommunity(id: string) {
|
|
calls.push(["focusCommunity", id]);
|
|
},
|
|
setTypeFilters(filters: GraphTypeFilters) {
|
|
calls.push(["setTypeFilters", filters]);
|
|
},
|
|
showTemporaryObject(object: GraphSummaryObjectRef) {
|
|
calls.push(["showTemporaryObject", object]);
|
|
},
|
|
clearTemporaryObjectDisplay() {
|
|
calls.push(["clearTemporaryObjectDisplay"]);
|
|
},
|
|
resetView() {
|
|
calls.push(["resetView"]);
|
|
},
|
|
select(selection: SelectionInput) {
|
|
calls.push(["select", selection]);
|
|
},
|
|
previewNode(id: string | null) {
|
|
calls.push(["previewNode", id]);
|
|
},
|
|
clearSelection() {
|
|
calls.push(["clearSelection"]);
|
|
},
|
|
clearInteraction() {
|
|
calls.push(["clearInteraction"]);
|
|
},
|
|
setNodeFixed(id: string, mode: "fix" | "unfix") {
|
|
calls.push(["setNodeFixed", id, mode]);
|
|
return true;
|
|
},
|
|
setTheme(theme: ThemeId) {
|
|
calls.push(["setTheme", theme]);
|
|
},
|
|
setPins(pins: PinMap) {
|
|
calls.push(["setPins", pins]);
|
|
},
|
|
resetLayout() {
|
|
calls.push(["resetLayout"]);
|
|
},
|
|
destroy() {
|
|
calls.push(["destroy"]);
|
|
}
|
|
};
|
|
}
|
|
|
|
function graphFixture(): GraphData {
|
|
return {
|
|
meta: {
|
|
build_date: "2026-06-18T00:00:00.000Z",
|
|
wiki_title: "Summary contract graph",
|
|
total_nodes: 4,
|
|
total_edges: 3
|
|
},
|
|
nodes: [
|
|
{ id: "a", label: "Alpha hub", type: "topic", community: "alpha", source_path: "wiki/alpha/a.md", score: 3 },
|
|
{ id: "b", label: "Alpha leaf", type: "entity", community: "alpha", source_path: "wiki/alpha/b.md", weight: 2 },
|
|
{ id: "c", label: "Beta bridge", type: "topic", community: "beta", source_path: "wiki/beta/c.md", weight: 1 },
|
|
{ id: "d", label: "Beta detail", type: "source", community: "beta", source_path: "wiki/beta/d.md" }
|
|
],
|
|
edges: [
|
|
{ id: "a-b", from: "a", to: "b", type: "EXTRACTED", relation_type: "实现", weight: 0.6 },
|
|
{ id: "a-c", from: "a", to: "c", type: "INFERRED", relation_type: "依赖", weight: 0.9 },
|
|
{ id: "c-d", from: "c", to: "d", type: "EXTRACTED", relation_type: "衍生", weight: 0.8 }
|
|
],
|
|
insights: {
|
|
surprising_connections: [],
|
|
isolated_nodes: [],
|
|
bridge_nodes: [{ id: "c", label: "Beta bridge", community: "beta", connected_communities: ["alpha"], community_count: 2 }],
|
|
sparse_communities: [],
|
|
meta: { degraded: false, node_count: 4, edge_count: 3, max_insight_nodes: 10, max_insight_edges: 10 }
|
|
},
|
|
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", "c", "d"], degraded: false }
|
|
},
|
|
communities: [
|
|
{ id: "alpha", label: "Alpha", node_count: 2, color_index: 0, members: ["a", "b"] },
|
|
{ id: "beta", label: "Beta", node_count: 2, color_index: 1, members: ["c", "d"] }
|
|
]
|
|
}
|
|
};
|
|
}
|
|
|
|
function graphFixtureWithUngroupedNodes(): GraphData {
|
|
const base = graphFixture();
|
|
return {
|
|
...base,
|
|
meta: {
|
|
...base.meta,
|
|
total_nodes: base.meta.total_nodes + 2
|
|
},
|
|
nodes: [
|
|
...base.nodes,
|
|
{ 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 }
|
|
]
|
|
};
|
|
}
|
|
|
|
function graphFixtureWithLinkedUngroupedNodes(): GraphData {
|
|
const base = graphFixtureWithUngroupedNodes();
|
|
return {
|
|
...base,
|
|
meta: {
|
|
...base.meta,
|
|
total_edges: base.meta.total_edges + 1
|
|
},
|
|
edges: [
|
|
...base.edges,
|
|
{ id: "loose-a-loose-b", from: "loose-a", to: "loose-b", type: "INFERRED", relation_type: "潜在关联", weight: 0.6 }
|
|
]
|
|
};
|
|
}
|
|
|
|
function graphFixtureWithSingleNodeCommunity(): GraphData {
|
|
const base = graphFixture();
|
|
return {
|
|
...base,
|
|
nodes: [
|
|
...base.nodes,
|
|
{ id: "g1", label: "Gamma only", type: "entity", community: "gamma", source_path: "wiki/gamma/g1.md" }
|
|
]
|
|
};
|
|
}
|
|
|
|
function graphFixtureWithEmptyCommunityGamma(): GraphData {
|
|
const base = graphFixture();
|
|
return {
|
|
...base,
|
|
nodes: [
|
|
...base.nodes,
|
|
{ id: "g1", label: "Gamma one", type: "entity", community: "gamma", source_path: "wiki/gamma/g1.md" },
|
|
{ id: "g2", label: "Gamma two", type: "entity", community: "gamma", source_path: "wiki/gamma/g2.md" }
|
|
],
|
|
edges: [
|
|
...base.edges,
|
|
{ id: "g1-a", from: "g1", to: "a", type: "INFERRED", relation_type: "潜在关联", weight: 0.5 },
|
|
{ id: "g2-a", from: "g2", to: "a", type: "INFERRED", relation_type: "潜在关联", weight: 0.5 }
|
|
]
|
|
};
|
|
}
|
|
|
|
function graphFixtureWithIsolatedBoundary(loose: boolean): GraphData {
|
|
const base = graphFixture();
|
|
const linked = loose
|
|
? [{ id: "g1", label: "Gamma hub", type: "topic", community: "gamma", source_path: "wiki/gamma/g1.md" },
|
|
{ id: "g2", label: "Gamma leaf", type: "entity", community: "gamma", source_path: "wiki/gamma/g2.md" }]
|
|
: [{ id: "g1", label: "Gamma one", type: "topic", community: "gamma", source_path: "wiki/gamma/g1.md" },
|
|
{ id: "g2", label: "Gamma two", type: "entity", community: "gamma", source_path: "wiki/gamma/g2.md" },
|
|
{ id: "g3", label: "Gamma three", type: "entity", community: "gamma", source_path: "wiki/gamma/g3.md" }];
|
|
const edges = loose
|
|
? [{ id: "g1-g2", from: "g1", to: "g2", type: "EXTRACTED", relation_type: "实现", weight: 0.7 }]
|
|
: [{ id: "g1-g2", from: "g1", to: "g2", type: "EXTRACTED", relation_type: "实现", weight: 0.7 },
|
|
{ id: "g2-g3", from: "g2", to: "g3", type: "EXTRACTED", relation_type: "实现", weight: 0.7 }];
|
|
const isolated = loose
|
|
? [{ id: "i1", label: "Isolated one", type: "entity", community: "gamma", source_path: "wiki/gamma/i1.md" },
|
|
{ id: "i2", label: "Isolated two", type: "entity", community: "gamma", source_path: "wiki/gamma/i2.md" },
|
|
{ id: "i3", label: "Isolated three", type: "entity", community: "gamma", source_path: "wiki/gamma/i3.md" },
|
|
{ id: "i4", label: "Isolated four", type: "entity", community: "gamma", source_path: "wiki/gamma/i4.md" }]
|
|
: [{ id: "i1", label: "Isolated one", type: "entity", community: "gamma", source_path: "wiki/gamma/i1.md" },
|
|
{ id: "i2", label: "Isolated two", type: "entity", community: "gamma", source_path: "wiki/gamma/i2.md" },
|
|
{ id: "i3", label: "Isolated three", type: "entity", community: "gamma", source_path: "wiki/gamma/i3.md" }];
|
|
return {
|
|
...base,
|
|
nodes: [...base.nodes, ...linked, ...isolated],
|
|
edges: [...base.edges, ...edges]
|
|
};
|
|
}
|