@ltm-blueverse/alpha-semantic-hub 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1674 -0
  3. package/explorer/README.md +282 -0
  4. package/explorer/src/App.css +0 -0
  5. package/explorer/src/App.tsx +2035 -0
  6. package/explorer/src/ErrorBoundary.tsx +112 -0
  7. package/explorer/src/ExploreWorkspaceTabs.tsx +29 -0
  8. package/explorer/src/assets/hero.png +0 -0
  9. package/explorer/src/assets/react.svg +1 -0
  10. package/explorer/src/assets/vite.svg +1 -0
  11. package/explorer/src/explorerCapabilities.ts +24 -0
  12. package/explorer/src/index.css +71 -0
  13. package/explorer/src/main.tsx +7 -0
  14. package/explorer/src/store/edgePairKeys.d.ts +2 -0
  15. package/explorer/src/store/edgePairKeys.js +7 -0
  16. package/explorer/src/store/graphStore.ts +183 -0
  17. package/explorer/src/store/registryStore.ts +78 -0
  18. package/explorer/src/types.d.ts +16 -0
  19. package/explorer/src/ui/primitives.css +414 -0
  20. package/explorer/src/ui/primitives.tsx +170 -0
  21. package/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx +279 -0
  22. package/explorer/src/workspaces/DiffMergeWorkspace/DiffMergeWorkspace.tsx +121 -0
  23. package/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx +454 -0
  24. package/explorer/src/workspaces/EnrichWorkspace/RegistryTab.tsx +285 -0
  25. package/explorer/src/workspaces/GraphWorkspace/GraphCanvas.tsx +2673 -0
  26. package/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +771 -0
  27. package/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx +410 -0
  28. package/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +3584 -0
  29. package/explorer/src/workspaces/GraphWorkspace/MarkdownContentViewer.tsx +705 -0
  30. package/explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx +55 -0
  31. package/explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx +193 -0
  32. package/explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts +29 -0
  33. package/explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts +15 -0
  34. package/explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts +25 -0
  35. package/explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts +15 -0
  36. package/explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts +52 -0
  37. package/explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts +36 -0
  38. package/explorer/src/workspaces/GraphWorkspace/behaviors/types.ts +41 -0
  39. package/explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts +29 -0
  40. package/explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts +779 -0
  41. package/explorer/src/workspaces/GraphWorkspace/graphColorLegend.ts +36 -0
  42. package/explorer/src/workspaces/GraphWorkspace/graphConfig.ts +102 -0
  43. package/explorer/src/workspaces/GraphWorkspace/graphEntityShape.ts +34 -0
  44. package/explorer/src/workspaces/GraphWorkspace/graphLoading.ts +84 -0
  45. package/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts +775 -0
  46. package/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +2753 -0
  47. package/explorer/src/workspaces/GraphWorkspace/graphStructureLayer.ts +335 -0
  48. package/explorer/src/workspaces/GraphWorkspace/graphTheme.ts +1078 -0
  49. package/explorer/src/workspaces/GraphWorkspace/graphViewCopy.ts +41 -0
  50. package/explorer/src/workspaces/GraphWorkspace/localGraphTransition.ts +9 -0
  51. package/explorer/src/workspaces/GraphWorkspace/markdownEditorState.ts +114 -0
  52. package/explorer/src/workspaces/GraphWorkspace/markdownResourceClient.ts +103 -0
  53. package/explorer/src/workspaces/GraphWorkspace/markdownUrlSafety.ts +29 -0
  54. package/explorer/src/workspaces/GraphWorkspace/nodeMarkdownSync.ts +81 -0
  55. package/explorer/src/workspaces/GraphWorkspace/pluginRegistryPredicates.ts +24 -0
  56. package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx +353 -0
  57. package/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx +522 -0
  58. package/explorer/src/workspaces/GraphWorkspace/plugins/index.ts +16 -0
  59. package/explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx +120 -0
  60. package/explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx +231 -0
  61. package/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx +142 -0
  62. package/explorer/src/workspaces/GraphWorkspace/plugins/types.ts +106 -0
  63. package/explorer/src/workspaces/GraphWorkspace/realtimeGraphAttributes.ts +50 -0
  64. package/explorer/src/workspaces/GraphWorkspace/scene.ts +79 -0
  65. package/explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts +421 -0
  66. package/explorer/src/workspaces/GraphWorkspace/smallGraphLayout.ts +135 -0
  67. package/explorer/src/workspaces/GraphWorkspace/temporalLifecyclePredicates.ts +31 -0
  68. package/explorer/src/workspaces/GraphWorkspace/temporalScrubberBounds.ts +46 -0
  69. package/explorer/src/workspaces/GraphWorkspace/temporalSnapshotGuards.ts +113 -0
  70. package/explorer/src/workspaces/GraphWorkspace/types.ts +395 -0
  71. package/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts +746 -0
  72. package/explorer/src/workspaces/GraphWorkspace/useMarkdownEditor.ts +177 -0
  73. package/explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx +196 -0
  74. package/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx +177 -0
  75. package/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx +291 -0
  76. package/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx +346 -0
  77. package/explorer/src/workspaces/MemoryWorkspace.tsx +464 -0
  78. package/explorer/src/workspaces/OntologyWorkspace/AlignmentsTab.tsx +437 -0
  79. package/explorer/src/workspaces/OntologyWorkspace/HealthTab.tsx +214 -0
  80. package/explorer/src/workspaces/OntologyWorkspace/OntologyEditor.tsx +770 -0
  81. package/explorer/src/workspaces/OntologyWorkspace/OntologyLoader.tsx +913 -0
  82. package/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx +930 -0
  83. package/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx +590 -0
  84. package/explorer/src/workspaces/OntologyWorkspace/ProposalReview.tsx +429 -0
  85. package/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx +696 -0
  86. package/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx +363 -0
  87. package/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx +546 -0
  88. package/explorer/src/workspaces/OntologyWorkspace/api.ts +164 -0
  89. package/explorer/src/workspaces/OntologyWorkspace/index.tsx +101 -0
  90. package/explorer/src/workspaces/OntologyWorkspace/ontologyEditorModel.ts +87 -0
  91. package/explorer/src/workspaces/OntologyWorkspace/ontologyUrlState.ts +94 -0
  92. package/explorer/src/workspaces/OntologyWorkspace/types.ts +119 -0
  93. package/explorer/src/workspaces/ReasoningWorkspace.tsx +243 -0
  94. package/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx +271 -0
  95. package/explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx +97 -0
  96. package/explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx +95 -0
  97. package/explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx +116 -0
  98. package/explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx +112 -0
  99. package/explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx +70 -0
  100. package/explorer/src/workspaces/VocabularyWorkspace/queries.ts +52 -0
  101. package/explorer/src/workspaces/VocabularyWorkspace/types.ts +24 -0
  102. package/package.json +31 -0
  103. package/plugins/.claude-plugin/README.md +262 -0
  104. package/plugins/.claude-plugin/marketplace.json +20 -0
  105. package/plugins/.claude-plugin/plugin.json +33 -0
  106. package/plugins/.cline-plugin/README.md +32 -0
  107. package/plugins/.cline-plugin/marketplace.json +17 -0
  108. package/plugins/.cline-plugin/plugin.json +35 -0
  109. package/plugins/.codex-plugin/marketplace.json +21 -0
  110. package/plugins/.codex-plugin/plugin.json +35 -0
  111. package/plugins/.continue-plugin/README.md +34 -0
  112. package/plugins/.continue-plugin/marketplace.json +17 -0
  113. package/plugins/.continue-plugin/plugin.json +33 -0
  114. package/plugins/.cursor-plugin/marketplace.json +18 -0
  115. package/plugins/.cursor-plugin/plugin.json +32 -0
  116. package/plugins/.openclaw-plugin/README.md +62 -0
  117. package/plugins/.openclaw-plugin/marketplace.json +18 -0
  118. package/plugins/.openclaw-plugin/plugin.json +46 -0
  119. package/plugins/.pi-plugin/README.md +35 -0
  120. package/plugins/.vscode-plugin/README.md +48 -0
  121. package/plugins/.vscode-plugin/marketplace.json +17 -0
  122. package/plugins/.vscode-plugin/plugin.json +35 -0
  123. package/plugins/.windsurf-plugin/README.md +39 -0
  124. package/plugins/.windsurf-plugin/marketplace.json +17 -0
  125. package/plugins/.windsurf-plugin/plugin.json +35 -0
  126. package/plugins/agents/decision-advisor.md +126 -0
  127. package/plugins/agents/explainability.md +129 -0
  128. package/plugins/agents/kg-assistant.md +73 -0
  129. package/plugins/hooks/hooks.json +12 -0
  130. package/plugins/skills/causal/SKILL.md +74 -0
  131. package/plugins/skills/change/SKILL.md +80 -0
  132. package/plugins/skills/decision/SKILL.md +197 -0
  133. package/plugins/skills/deduplicate/SKILL.md +41 -0
  134. package/plugins/skills/embed/SKILL.md +230 -0
  135. package/plugins/skills/explain/SKILL.md +44 -0
  136. package/plugins/skills/export/SKILL.md +67 -0
  137. package/plugins/skills/extract/SKILL.md +93 -0
  138. package/plugins/skills/ingest/SKILL.md +38 -0
  139. package/plugins/skills/ontology/SKILL.md +89 -0
  140. package/plugins/skills/policy/SKILL.md +66 -0
  141. package/plugins/skills/provenance/SKILL.md +70 -0
  142. package/plugins/skills/query/SKILL.md +84 -0
  143. package/plugins/skills/reason/SKILL.md +201 -0
  144. package/plugins/skills/temporal/SKILL.md +164 -0
  145. package/plugins/skills/validate/SKILL.md +228 -0
  146. package/plugins/skills/visualize/SKILL.md +249 -0
@@ -0,0 +1,746 @@
1
+ import { useQuery, useQueryClient } from "@tanstack/react-query";
2
+ import { batchMergeEdges, batchMergeNodes, clearGraph } from "../../store/graphStore";
3
+ import type { EdgeAttributes, NodeAttributes } from "../../store/graphStore";
4
+ import { curveGroupForPair, pairRegistryKey } from "../../store/edgePairKeys.js";
5
+ import {
6
+ GRAPH_THEME,
7
+ clamp,
8
+ darkenHex,
9
+ hashString,
10
+ withAlpha,
11
+ type GraphBadgeKind,
12
+ type GraphEdgeVariant,
13
+ type GraphEntityShapeVariant,
14
+ type GraphLabelVisibilityPolicy,
15
+ type GraphNodeShapeVariant,
16
+ } from "./graphTheme";
17
+ import { classifyEntityShape } from "./graphEntityShape";
18
+ import { createGraphLoadProgress } from "./graphLoading";
19
+ import {
20
+ buildSmallGraphSeedPositions,
21
+ resolveGraphLayoutDecision,
22
+ resolveNodeLayoutPosition,
23
+ } from "./smallGraphLayout";
24
+ import type { GraphLoadProgress, GraphLoadSummary } from "./types";
25
+
26
+ const SEMANTIC_COLOR_FIELDS = [
27
+ "community",
28
+ "cluster",
29
+ "module",
30
+ "group",
31
+ "category",
32
+ "domain",
33
+ "layer",
34
+ "source",
35
+ "nodeType",
36
+ ] as const;
37
+
38
+ const PROVENANCE_KEYS = ["source", "source_url", "pmid", "pmids", "evidence", "provenance", "confidence"] as const;
39
+
40
+ function getSemanticFieldValue(attributes: NodeAttributes, field: (typeof SEMANTIC_COLOR_FIELDS)[number]): string | null {
41
+ if (field === "nodeType") {
42
+ const value = attributes.nodeType;
43
+ return typeof value === "string" && value.trim() ? value : null;
44
+ }
45
+
46
+ const value = attributes.properties?.[field];
47
+ return typeof value === "string" && value.trim() ? value : null;
48
+ }
49
+
50
+ function normalizedEntropy(counts: number[], total: number): number {
51
+ if (counts.length <= 1 || total <= 0) {
52
+ return 0;
53
+ }
54
+
55
+ let entropy = 0;
56
+ for (const count of counts) {
57
+ const probability = count / total;
58
+ entropy -= probability * Math.log(probability);
59
+ }
60
+
61
+ return entropy / Math.log(counts.length);
62
+ }
63
+
64
+ function chooseColorAccessor(
65
+ nodes: Array<{ id: string; attributes: NodeAttributes }>,
66
+ ): (nodeId: string, attributes: NodeAttributes) => string {
67
+ let bestField: (typeof SEMANTIC_COLOR_FIELDS)[number] | null = null;
68
+ let bestScore = 0;
69
+
70
+ for (const field of SEMANTIC_COLOR_FIELDS) {
71
+ const counts = new Map<string, number>();
72
+ let covered = 0;
73
+
74
+ for (const node of nodes) {
75
+ const value = getSemanticFieldValue(node.attributes, field);
76
+ if (!value) {
77
+ continue;
78
+ }
79
+ covered += 1;
80
+ counts.set(value, (counts.get(value) ?? 0) + 1);
81
+ }
82
+
83
+ const uniqueCount = counts.size;
84
+ if (covered === 0 || uniqueCount <= 1) {
85
+ continue;
86
+ }
87
+
88
+ const countValues = [...counts.values()];
89
+ const coverage = covered / nodes.length;
90
+ const dominantRatio = Math.max(...countValues) / covered;
91
+ const entropy = normalizedEntropy(countValues, covered);
92
+ const diversity = Math.min(uniqueCount, GRAPH_THEME.palette.semantic.length) / GRAPH_THEME.palette.semantic.length;
93
+ const score = entropy * 0.65 + diversity * 0.2 + coverage * 0.15;
94
+
95
+ const isInformative =
96
+ coverage >= 0.45 &&
97
+ entropy >= 0.45 &&
98
+ dominantRatio <= 0.88;
99
+
100
+ if (!isInformative) {
101
+ continue;
102
+ }
103
+
104
+ if (score > bestScore) {
105
+ bestField = field;
106
+ bestScore = score;
107
+ }
108
+ }
109
+
110
+ if (bestField) {
111
+ return (_nodeId: string, attributes: NodeAttributes) =>
112
+ getSemanticFieldValue(attributes, bestField) ?? structuralColorKey(_nodeId, attributes);
113
+ }
114
+
115
+ return (nodeId: string, attributes: NodeAttributes) => structuralColorKey(nodeId, attributes);
116
+ }
117
+
118
+ function structuralColorKey(nodeId: string, attributes: NodeAttributes): string {
119
+ const shard = hashString(nodeId) % GRAPH_THEME.palette.semantic.length;
120
+ return `${attributes.nodeType || "entity"}:${shard}`;
121
+ }
122
+
123
+ function readFiniteCoordinate(value: unknown): number | null {
124
+ const numeric = Number(value);
125
+ return Number.isFinite(numeric) ? numeric : null;
126
+ }
127
+
128
+ function seededUnit(value: string): number {
129
+ return (hashString(value) % 10000) / 10000;
130
+ }
131
+
132
+ function buildClusterSeedPositions(
133
+ nodes: Array<{
134
+ id: string;
135
+ semanticGroup: string;
136
+ priority: number;
137
+ }>,
138
+ ): Map<string, { x: number; y: number }> {
139
+ const grouped = new Map<string, Array<{ id: string; priority: number }>>();
140
+ nodes.forEach((node) => {
141
+ const entry = grouped.get(node.semanticGroup);
142
+ if (entry) {
143
+ entry.push({ id: node.id, priority: node.priority });
144
+ } else {
145
+ grouped.set(node.semanticGroup, [{ id: node.id, priority: node.priority }]);
146
+ }
147
+ });
148
+
149
+ const groupEntries = [...grouped.entries()]
150
+ .sort((left, right) => {
151
+ const countDelta = right[1].length - left[1].length;
152
+ if (countDelta !== 0) {
153
+ return countDelta;
154
+ }
155
+ return left[0].localeCompare(right[0]);
156
+ });
157
+
158
+ const groupCenters = new Map<string, { x: number; y: number; spread: number }>();
159
+ groupEntries.forEach(([group, members], index) => {
160
+ const angle = index * 2.399963229728653;
161
+ const radius = 170 + Math.sqrt(index + 1) * 195;
162
+ const spread = 72 + Math.sqrt(members.length) * 16;
163
+ groupCenters.set(group, {
164
+ x: Math.cos(angle) * radius * 1.14,
165
+ y: Math.sin(angle) * radius * 0.84,
166
+ spread,
167
+ });
168
+ });
169
+
170
+ const seeded = new Map<string, { x: number; y: number }>();
171
+ groupEntries.forEach(([group, members]) => {
172
+ const center = groupCenters.get(group);
173
+ if (!center) {
174
+ return;
175
+ }
176
+
177
+ members
178
+ .sort((left, right) => {
179
+ if (right.priority !== left.priority) {
180
+ return right.priority - left.priority;
181
+ }
182
+ return left.id.localeCompare(right.id);
183
+ })
184
+ .forEach((member, index) => {
185
+ const angle = index * 2.399963229728653 + seededUnit(`${group}:${member.id}:angle`) * 0.72;
186
+ const radial = Math.sqrt((index + 0.5) / Math.max(members.length, 1)) * center.spread;
187
+ const jitterX = (seededUnit(`${member.id}:jx`) - 0.5) * center.spread * 0.22;
188
+ const jitterY = (seededUnit(`${member.id}:jy`) - 0.5) * center.spread * 0.18;
189
+ seeded.set(member.id, {
190
+ x: center.x + Math.cos(angle) * radial + jitterX,
191
+ y: center.y + Math.sin(angle) * radial * 0.86 + jitterY,
192
+ });
193
+ });
194
+ });
195
+
196
+ return seeded;
197
+ }
198
+
199
+ function getProvenanceCount(properties: Record<string, unknown>): number {
200
+ return PROVENANCE_KEYS.reduce(
201
+ (count, key) => (properties[key] !== undefined && properties[key] !== null ? count + 1 : count),
202
+ 0,
203
+ );
204
+ }
205
+
206
+ function resolveEntityShape(attributes: NodeAttributes, semanticGroup: string): GraphEntityShapeVariant {
207
+ return classifyEntityShape(
208
+ attributes.nodeType,
209
+ semanticGroup,
210
+ attributes.content,
211
+ attributes.properties as Record<string, unknown> | undefined,
212
+ );
213
+ }
214
+
215
+ function resolveNodeVariantMetadata(
216
+ baseColor: string,
217
+ sizeRatio: number,
218
+ hasTemporalBounds: boolean,
219
+ provenanceCount: number,
220
+ ): Pick<
221
+ NodeAttributes,
222
+ "nodeVariant" | "nodeShapeVariant" | "badgeKind" | "badgeCount" | "ringColor" | "haloColor" | "labelVisibilityPolicy"
223
+ > {
224
+ let nodeShapeVariant: GraphNodeShapeVariant = "default";
225
+ let badgeKind: GraphBadgeKind | undefined;
226
+ let badgeCount: number | undefined;
227
+
228
+ if (hasTemporalBounds) {
229
+ nodeShapeVariant = "temporal";
230
+ badgeKind = "temporal";
231
+ } else if (provenanceCount > 0) {
232
+ nodeShapeVariant = "provenance";
233
+ badgeKind = "provenance";
234
+ badgeCount = provenanceCount;
235
+ }
236
+
237
+ let labelVisibilityPolicy: GraphLabelVisibilityPolicy = "none";
238
+ if (sizeRatio >= 0.86) {
239
+ labelVisibilityPolicy = "always";
240
+ } else if (badgeKind) {
241
+ labelVisibilityPolicy = "local";
242
+ } else if (sizeRatio >= 0.56) {
243
+ labelVisibilityPolicy = "priority";
244
+ }
245
+
246
+ return {
247
+ nodeVariant: nodeShapeVariant,
248
+ nodeShapeVariant,
249
+ badgeKind,
250
+ badgeCount,
251
+ ringColor: GRAPH_THEME.nodes.selectedRing.color,
252
+ haloColor: withAlpha(baseColor, 0.38),
253
+ labelVisibilityPolicy,
254
+ };
255
+ }
256
+
257
+ function resolveEdgeVariantMetadata(
258
+ edge: ApiEdge,
259
+ sourcePriority: number,
260
+ targetPriority: number,
261
+ isBidirectional: boolean,
262
+ ): Pick<
263
+ EdgeAttributes,
264
+ "edgeVariant" | "arrowVisibilityPolicy" | "relationshipStrength" | "isParallelPair" | "parallelIndex" | "parallelCount"
265
+ > {
266
+ const relationshipStrength = clamp(
267
+ 0.12,
268
+ 0.18 + Math.log(Math.max(Number(edge.weight) || 1, 1)) / Math.log(12),
269
+ 1,
270
+ );
271
+
272
+ let edgeVariant: GraphEdgeVariant = "line";
273
+ if (isBidirectional) {
274
+ edgeVariant = "bidirectionalCurve";
275
+ } else if (Math.max(sourcePriority, targetPriority, relationshipStrength) >= 0.58) {
276
+ edgeVariant = "directional";
277
+ }
278
+
279
+ return {
280
+ edgeVariant,
281
+ arrowVisibilityPolicy: edgeVariant === "line" ? "hidden" : "contextual",
282
+ relationshipStrength,
283
+ isParallelPair: false,
284
+ parallelIndex: 0,
285
+ parallelCount: 1,
286
+ };
287
+ }
288
+
289
+ interface ApiNode {
290
+ id: string;
291
+ type: string;
292
+ content: string;
293
+ properties: Record<string, unknown>;
294
+ valid_from?: string | null;
295
+ valid_until?: string | null;
296
+ }
297
+
298
+ interface ApiEdge {
299
+ id: string;
300
+ familyId: string;
301
+ source: string;
302
+ target: string;
303
+ type: string;
304
+ weight: number;
305
+ properties: Record<string, unknown>;
306
+ }
307
+
308
+ interface NodeListResponse {
309
+ nodes: ApiNode[];
310
+ total: number;
311
+ skip: number;
312
+ limit: number;
313
+ next_cursor?: string | null;
314
+ }
315
+
316
+ interface EdgeListResponse {
317
+ edges: ApiEdge[];
318
+ total: number;
319
+ skip: number;
320
+ limit: number;
321
+ next_cursor?: string | null;
322
+ }
323
+
324
+ const PAGE_LIMIT = 1000;
325
+
326
+ /** Surface the server's `detail` message (e.g. auth/setup guidance) on non-OK responses. */
327
+ async function fetchErrorDetail(response: Response): Promise<string> {
328
+ try {
329
+ const body: unknown = await response.json();
330
+ const detail = (body as { detail?: unknown } | null)?.detail;
331
+ if (typeof detail === "string" && detail.trim()) {
332
+ return ` — ${detail.trim()}`;
333
+ }
334
+ } catch {
335
+ // Non-JSON or unreadable body: fall back to the status-only message.
336
+ }
337
+ return "";
338
+ }
339
+
340
+ async function fetchAllNodes(
341
+ signal: AbortSignal,
342
+ onProgress?: (progress: GraphLoadProgress) => void,
343
+ ): Promise<ApiNode[]> {
344
+ let cursor: string | null = null;
345
+ const collected: ApiNode[] = [];
346
+ let total: number | null = null;
347
+
348
+ while (true) {
349
+ const url = new URL("/api/graph/nodes", window.location.origin);
350
+ url.searchParams.set("limit", String(PAGE_LIMIT));
351
+ if (cursor) {
352
+ url.searchParams.set("cursor", cursor);
353
+ }
354
+
355
+ const response = await fetch(url.toString(), { signal });
356
+ if (!response.ok) {
357
+ throw new Error(`Fetch failed: ${response.status}${await fetchErrorDetail(response)}`);
358
+ }
359
+
360
+ const data: NodeListResponse = await response.json();
361
+ if (!data.nodes?.length) {
362
+ break;
363
+ }
364
+
365
+ total = data.total ?? total;
366
+ collected.push(...data.nodes);
367
+ onProgress?.(createGraphLoadProgress({
368
+ phase: "fetching_nodes",
369
+ progressKind: total ? "determinate" : "indeterminate",
370
+ loaded: collected.length,
371
+ total,
372
+ nodesLoaded: collected.length,
373
+ nodesTotal: total,
374
+ edgesLoaded: 0,
375
+ edgesTotal: null,
376
+ message: total
377
+ ? `Loading nodes ${collected.length.toLocaleString()} of ${total.toLocaleString()}`
378
+ : `Loading nodes ${collected.length.toLocaleString()}`,
379
+ }));
380
+
381
+ if (!data.next_cursor) {
382
+ break;
383
+ }
384
+ cursor = data.next_cursor;
385
+ await yieldToMain();
386
+ }
387
+
388
+ return collected;
389
+ }
390
+
391
+ async function fetchAllEdges(
392
+ signal: AbortSignal,
393
+ nodeIds: Set<string>,
394
+ nodeProgress: { loaded: number; total: number | null },
395
+ onProgress?: (progress: GraphLoadProgress) => void,
396
+ ): Promise<ApiEdge[]> {
397
+ let cursor: string | null = null;
398
+ const collected: ApiEdge[] = [];
399
+ const seenEdgeIds = new Set<string>();
400
+ let total: number | null = null;
401
+ let warnedOverTotal = false;
402
+
403
+ while (true) {
404
+ const url = new URL("/api/graph/edges", window.location.origin);
405
+ url.searchParams.set("limit", String(PAGE_LIMIT));
406
+ if (cursor) {
407
+ url.searchParams.set("cursor", cursor);
408
+ }
409
+
410
+ const response = await fetch(url.toString(), { signal });
411
+ if (!response.ok) {
412
+ throw new Error(`Fetch failed: ${response.status}${await fetchErrorDetail(response)}`);
413
+ }
414
+
415
+ const data: EdgeListResponse = await response.json();
416
+ if (!data.edges?.length) {
417
+ break;
418
+ }
419
+
420
+ total = data.total ?? total;
421
+ const validEdges = data.edges.filter((edge) => {
422
+ if (!nodeIds.has(edge.source) || !nodeIds.has(edge.target)) {
423
+ return false;
424
+ }
425
+ if (seenEdgeIds.has(edge.id)) {
426
+ return false;
427
+ }
428
+ seenEdgeIds.add(edge.id);
429
+ return true;
430
+ });
431
+ collected.push(...validEdges);
432
+ const safeLoaded = total ? Math.min(seenEdgeIds.size, total) : seenEdgeIds.size;
433
+ if (!warnedOverTotal && total !== null && seenEdgeIds.size > total) {
434
+ warnedOverTotal = true;
435
+ console.warn("[graph-load] edge pagination returned more unique edge ids than total", {
436
+ uniqueEdgesLoaded: seenEdgeIds.size,
437
+ total,
438
+ });
439
+ }
440
+ onProgress?.(createGraphLoadProgress({
441
+ phase: "fetching_edges",
442
+ progressKind: total ? "determinate" : "indeterminate",
443
+ loaded: safeLoaded,
444
+ total,
445
+ nodesLoaded: nodeProgress.loaded,
446
+ nodesTotal: nodeProgress.total,
447
+ edgesLoaded: safeLoaded,
448
+ edgesTotal: total,
449
+ message: total
450
+ ? `Loading edges ${safeLoaded.toLocaleString()} of ${total.toLocaleString()}`
451
+ : `Loading edges ${safeLoaded.toLocaleString()}`,
452
+ }));
453
+
454
+ if (!data.next_cursor) {
455
+ break;
456
+ }
457
+ cursor = data.next_cursor;
458
+ await yieldToMain();
459
+ }
460
+
461
+ return collected;
462
+ }
463
+
464
+ function yieldToMain(): Promise<void> {
465
+ if ("scheduler" in window && typeof (window as Window & { scheduler?: { yield?: () => Promise<void> } }).scheduler?.yield === "function") {
466
+ return (window as Window & { scheduler: { yield: () => Promise<void> } }).scheduler.yield();
467
+ }
468
+ return new Promise((resolve) => setTimeout(resolve, 0));
469
+ }
470
+
471
+ interface UseLoadGraphOptions {
472
+ enabled?: boolean;
473
+ onGraphReady?: (summary: GraphLoadSummary) => void;
474
+ onProgress?: (progress: GraphLoadProgress) => void;
475
+ }
476
+
477
+ export function useLoadGraph(options: UseLoadGraphOptions = {}) {
478
+ const { enabled = true, onGraphReady, onProgress } = options;
479
+
480
+ return useQuery<GraphLoadSummary>({
481
+ queryKey: ["graph", "full-load"],
482
+ enabled,
483
+ staleTime: Infinity,
484
+ retry: 0,
485
+ queryFn: async ({ signal }): Promise<GraphLoadSummary> => {
486
+ const startedAt = performance.now();
487
+ onProgress?.(createGraphLoadProgress({
488
+ phase: "bootstrapping",
489
+ progressKind: "indeterminate",
490
+ nodesLoaded: 0,
491
+ nodesTotal: null,
492
+ edgesLoaded: 0,
493
+ edgesTotal: null,
494
+ message: "Preparing graph session",
495
+ }));
496
+
497
+ const fetchedNodes = await fetchAllNodes(signal, onProgress);
498
+ const nodeIds = new Set(fetchedNodes.map((node) => node.id));
499
+ const fetchedEdges = await fetchAllEdges(
500
+ signal,
501
+ nodeIds,
502
+ { loaded: fetchedNodes.length, total: fetchedNodes.length },
503
+ onProgress,
504
+ );
505
+
506
+ const degreeByNode = new Map<string, number>();
507
+ for (const nodeId of nodeIds) {
508
+ degreeByNode.set(nodeId, 0);
509
+ }
510
+ for (const edge of fetchedEdges) {
511
+ degreeByNode.set(edge.source, (degreeByNode.get(edge.source) ?? 0) + 1);
512
+ degreeByNode.set(edge.target, (degreeByNode.get(edge.target) ?? 0) + 1);
513
+ }
514
+
515
+ const maxDegree = Math.max(...degreeByNode.values(), 1);
516
+ const draftAttributes = fetchedNodes.map((node) => ({
517
+ id: node.id,
518
+ attributes: {
519
+ label: node.content || node.id,
520
+ x: 0,
521
+ y: 0,
522
+ nodeType: node.type,
523
+ content: node.content,
524
+ valid_from: node.valid_from,
525
+ valid_until: node.valid_until,
526
+ properties: node.properties,
527
+ } as NodeAttributes,
528
+ }));
529
+
530
+ onProgress?.(createGraphLoadProgress({
531
+ phase: "computing_styling",
532
+ progressKind: "indeterminate",
533
+ nodesLoaded: fetchedNodes.length,
534
+ nodesTotal: fetchedNodes.length,
535
+ edgesLoaded: fetchedEdges.length,
536
+ edgesTotal: fetchedEdges.length,
537
+ message: "Applying semantic color, sizing, and structural styling",
538
+ }));
539
+
540
+ const colorAccessor = chooseColorAccessor(draftAttributes);
541
+ const nodePriorityById = new Map<string, number>();
542
+
543
+ for (const nodeId of nodeIds) {
544
+ const degree = degreeByNode.get(nodeId) ?? 0;
545
+ const sizeRatio = Math.log(degree + 1) / Math.log(maxDegree + 1);
546
+ nodePriorityById.set(nodeId, sizeRatio);
547
+ }
548
+
549
+ const semanticKeyByNodeId = new Map<string, string>();
550
+ draftAttributes.forEach(({ id, attributes }) => {
551
+ semanticKeyByNodeId.set(id, colorAccessor(id, attributes));
552
+ });
553
+
554
+ const providedCoordinateCount = fetchedNodes.reduce((count, node) => {
555
+ const properties = node.properties as Record<string, unknown>;
556
+ return readFiniteCoordinate(properties?.x) !== null && readFiniteCoordinate(properties?.y) !== null
557
+ ? count + 1
558
+ : count;
559
+ }, 0);
560
+ const coordinateCoverage = fetchedNodes.length > 0 ? providedCoordinateCount / fetchedNodes.length : 0;
561
+ const {
562
+ useProvidedCoordinates,
563
+ useSmallGraphLayout,
564
+ layoutReady,
565
+ } = resolveGraphLayoutDecision(fetchedNodes.length, coordinateCoverage);
566
+ const seededPositions = useProvidedCoordinates
567
+ ? null
568
+ : useSmallGraphLayout
569
+ ? buildSmallGraphSeedPositions(
570
+ fetchedNodes.map((node) => node.id),
571
+ fetchedEdges,
572
+ )
573
+ : buildClusterSeedPositions(
574
+ draftAttributes.map(({ id, attributes }) => ({
575
+ id,
576
+ semanticGroup: semanticKeyByNodeId.get(id) ?? structuralColorKey(id, attributes),
577
+ priority: nodePriorityById.get(id) ?? 0,
578
+ })),
579
+ );
580
+
581
+ const nodesToMerge = draftAttributes.map(({ id, attributes }) => {
582
+ const semanticGroup = semanticKeyByNodeId.get(id) ?? colorAccessor(id, attributes);
583
+ const colorIndex = hashString(semanticGroup) % GRAPH_THEME.palette.semantic.length;
584
+ const baseColor = GRAPH_THEME.palette.semantic[colorIndex];
585
+ const sizeRatio = nodePriorityById.get(id) ?? 0;
586
+ const dynamicSize = useSmallGraphLayout
587
+ ? clamp(5.2, 5.2 + 6.6 * sizeRatio, 11.8)
588
+ : clamp(1.8, 1.8 + 8.8 * sizeRatio, 11.8);
589
+ const hasTemporalBounds = Boolean(attributes.valid_from || attributes.valid_until);
590
+ const provenanceCount = getProvenanceCount(attributes.properties ?? {});
591
+ const properties = attributes.properties as Record<string, unknown>;
592
+ const entityShape = resolveEntityShape(attributes, semanticGroup);
593
+ const providedX = readFiniteCoordinate(properties?.x);
594
+ const providedY = readFiniteCoordinate(properties?.y);
595
+ const seededPosition = seededPositions?.get(id);
596
+ const { x, y } = resolveNodeLayoutPosition(
597
+ { useProvidedCoordinates, useSmallGraphLayout, layoutReady },
598
+ { x: providedX, y: providedY },
599
+ seededPosition,
600
+ );
601
+ return {
602
+ id,
603
+ attributes: {
604
+ ...attributes,
605
+ x,
606
+ y,
607
+ semanticGroup,
608
+ color: baseColor,
609
+ baseColor,
610
+ mutedColor: withAlpha(baseColor, GRAPH_THEME.nodes.mutedAlpha),
611
+ glowColor: withAlpha(baseColor, 0.24),
612
+ size: dynamicSize,
613
+ baseSize: dynamicSize,
614
+ visualPriority: sizeRatio,
615
+ labelPriority: sizeRatio,
616
+ strokeColor: darkenHex(baseColor, 112),
617
+ borderColor: darkenHex(baseColor, 112),
618
+ borderSize: 0.72,
619
+ entityShape,
620
+ ...resolveNodeVariantMetadata(baseColor, sizeRatio, hasTemporalBounds, provenanceCount),
621
+ ...(useSmallGraphLayout ? { labelVisibilityPolicy: "always" as const } : {}),
622
+ } as NodeAttributes,
623
+ };
624
+ });
625
+
626
+ const edgeKeys = new Set(fetchedEdges.map((edge) => pairRegistryKey(edge.source, edge.target)));
627
+ const parallelCounts = new Map<string, number>();
628
+ const familyCounts = new Map<string, number>();
629
+ fetchedEdges.forEach((edge) => {
630
+ const pairKey = pairRegistryKey(edge.source, edge.target);
631
+ parallelCounts.set(pairKey, (parallelCounts.get(pairKey) ?? 0) + 1);
632
+ familyCounts.set(edge.familyId, (familyCounts.get(edge.familyId) ?? 0) + 1);
633
+ });
634
+ const parallelOffsets = new Map<string, number>();
635
+
636
+ const edgesToMerge = fetchedEdges.map((edge) => {
637
+ const sourcePriority = nodePriorityById.get(edge.source) ?? 0;
638
+ const targetPriority = nodePriorityById.get(edge.target) ?? 0;
639
+ const isBidirectional = edgeKeys.has(pairRegistryKey(edge.target, edge.source));
640
+ const pairKey = pairRegistryKey(edge.source, edge.target);
641
+ const parallelIndex = parallelOffsets.get(pairKey) ?? 0;
642
+ parallelOffsets.set(pairKey, parallelIndex + 1);
643
+ const parallelCount = parallelCounts.get(pairKey) ?? 1;
644
+ const normalizedWeight = clamp(0, Math.log1p(Math.max(Number(edge.weight) || 1, 1)) / 6, 1);
645
+ const edgeVisualPriority = clamp(
646
+ 0,
647
+ Math.sqrt(Math.max(sourcePriority, 0) * Math.max(targetPriority, 0)) * 0.72 + normalizedWeight * 0.28,
648
+ 1,
649
+ );
650
+
651
+ return {
652
+ id: edge.id,
653
+ familyId: edge.familyId,
654
+ source: edge.source,
655
+ target: edge.target,
656
+ attributes: {
657
+ edgeId: edge.id,
658
+ familyId: edge.familyId,
659
+ sourceId: edge.source,
660
+ targetId: edge.target,
661
+ weight: edge.weight,
662
+ edgeType: edge.type,
663
+ properties: edge.properties,
664
+ size: clamp(0.18, 0.22 + Math.sqrt(Math.max(Number(edge.weight) || 1, 1)) * 0.2, 0.88),
665
+ baseSize: clamp(0.18, 0.22 + Math.sqrt(Math.max(Number(edge.weight) || 1, 1)) * 0.2, 0.88),
666
+ color: GRAPH_THEME.palette.muted.edgeStructure,
667
+ baseColor: GRAPH_THEME.palette.muted.edgeStructure,
668
+ mutedColor: GRAPH_THEME.palette.muted.edgeOverview,
669
+ visualPriority: edgeVisualPriority,
670
+ isBidirectional,
671
+ edgeFamily: isBidirectional ? "bidirectional" : "line",
672
+ curveGroup: curveGroupForPair(edge.source, edge.target),
673
+ type: "line",
674
+ isParallelPair: parallelCount > 1,
675
+ parallelIndex,
676
+ parallelCount,
677
+ familySize: familyCounts.get(edge.familyId) ?? 1,
678
+ isSmallGraph: useSmallGraphLayout,
679
+ ...resolveEdgeVariantMetadata(edge, sourcePriority, targetPriority, isBidirectional),
680
+ } as EdgeAttributes,
681
+ };
682
+ });
683
+
684
+ onProgress?.(createGraphLoadProgress({
685
+ phase: "hydrating_scene",
686
+ progressKind: "indeterminate",
687
+ nodesLoaded: nodesToMerge.length,
688
+ nodesTotal: nodesToMerge.length,
689
+ edgesLoaded: edgesToMerge.length,
690
+ edgesTotal: edgesToMerge.length,
691
+ message: "Preparing renderer and hydrating graph scene",
692
+ }));
693
+
694
+ try {
695
+ clearGraph();
696
+ } catch (error) {
697
+ console.error("[graph-load] clearGraph failed", error);
698
+ throw error;
699
+ }
700
+
701
+ try {
702
+ batchMergeNodes(nodesToMerge);
703
+ } catch (error) {
704
+ console.error("[graph-load] batchMergeNodes failed", error);
705
+ throw error;
706
+ }
707
+
708
+ try {
709
+ batchMergeEdges(edgesToMerge);
710
+ } catch (error) {
711
+ console.error("[graph-load] batchMergeEdges failed", error);
712
+ throw error;
713
+ }
714
+
715
+ const summary = {
716
+ nodeCount: nodesToMerge.length,
717
+ edgeCount: edgesToMerge.length,
718
+ loadTimeMs: Math.round(performance.now() - startedAt),
719
+ hasCoordinates: useProvidedCoordinates,
720
+ layoutSource: useProvidedCoordinates ? "provided" : "runtime",
721
+ layoutReady,
722
+ } satisfies GraphLoadSummary;
723
+
724
+ onProgress?.(createGraphLoadProgress({
725
+ phase: summary.layoutReady ? "ready" : "stabilizing_layout",
726
+ progressKind: "indeterminate",
727
+ nodesLoaded: summary.nodeCount,
728
+ nodesTotal: summary.nodeCount,
729
+ edgesLoaded: summary.edgeCount,
730
+ edgesTotal: summary.edgeCount,
731
+ message: summary.layoutReady ? "Graph ready" : "Settling runtime layout",
732
+ showGraphBehind: !summary.layoutReady,
733
+ layoutSource: summary.layoutSource,
734
+ layoutState: summary.layoutReady ? "interactive" : "bootstrapping",
735
+ }));
736
+
737
+ onGraphReady?.(summary);
738
+ return summary;
739
+ },
740
+ });
741
+ }
742
+
743
+ export function useReloadGraph() {
744
+ const queryClient = useQueryClient();
745
+ return () => queryClient.invalidateQueries({ queryKey: ["graph", "full-load"] });
746
+ }