first
This commit is contained in:
@@ -0,0 +1,664 @@
|
||||
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]
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user