@fortemi/graph 2026.7.3 → 2026.7.5

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.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  **Framework-agnostic graph projection helpers for rendering Fortemi community graphs anywhere**
6
6
 
7
- Turn a `CommunityGraph` into something you can draw — deterministic layout, filtering, community coloring, degree-based sizing, bounds/fit, neighborhood expansion, and static snapshot serialization — plus a framework-agnostic `GraphController` for graph-source selection. Framework-agnostic (no React); depends on `@fortemi/core`. The pure projection helpers stay database-free and tree-shakeable for JS-only hosts, and run entirely client-side — no network, no database.
7
+ Turn a `CommunityGraph` into something you can draw — deterministic layout, filtering, community coloring, degree-based sizing, bounds/fit, neighborhood expansion, and static snapshot serialization. Framework-agnostic (no React). The root graph helpers stay database-free and tree-shakeable for JS-only hosts, and run entirely client-side — no network, no database. Database-backed graph-source selection is available from `@fortemi/graph/controller`.
8
8
 
9
9
  ```bash
10
10
  pnpm add @fortemi/graph
@@ -27,7 +27,7 @@ pnpm add @fortemi/graph
27
27
 
28
28
  `@fortemi/graph` is the framework-agnostic projection layer for Fortemi relationship graphs. It takes a plain `CommunityGraph` — the shape produced by `@fortemi/core`'s `GraphRepository` and AIWG index export — and provides the pure, deterministic helpers needed to lay it out, filter it, color it, size it, fit it to a viewport, and render it as SVG or canvas.
29
29
 
30
- It is an **add-on, not a base layer**. `@fortemi/core` remains the foundation and owns graph *production* (similarity and link graphs built from the PGlite store) and community *detection*. `@fortemi/graph` sits on top and owns graph *projection* and *source orchestration* (`GraphController`). It depends on `@fortemi/core` for the controller and shared graph types, but `@fortemi/core` never depends on it the chain is `@electric-sql/pglite` `@fortemi/core` `@fortemi/graph` `@fortemi/react`. The projection helpers operate on portable data and reach no database, so they power `@fortemi/react`'s `GraphView` and a JS-only host — a static documentation site, for example — with no React. Only `GraphController` touches the PGlite-backed repositories.
30
+ It is an **add-on, not a base layer**. `@fortemi/core` remains the foundation and owns graph *production* (similarity and link graphs built from the PGlite store) and community *detection*. The `@fortemi/graph` root entry owns graph *projection* and has no runtime dependency on `@fortemi/core`. The optional `@fortemi/graph/controller` subpath owns live graph-source orchestration (`GraphController`) and is the only graph entry that touches the PGlite-backed repositories. `@fortemi/core` never depends on `@fortemi/graph`.
31
31
 
32
32
  ## Why Fortemi Graph
33
33
 
@@ -121,6 +121,9 @@ These shapes are structurally identical to the ones `@fortemi/core` produces (`G
121
121
  | `computeGraphBounds(nodes)` / `fitGraphToViewport(bounds, viewport, opts?)` | Bounding box and a centered fit transform |
122
122
  | `neighborsOf` / `expandNeighborhood` / `subgraphForNodes` / `neighborhoodSubgraph` / `buildAdjacency` | Selection and BFS neighborhood expansion |
123
123
  | `serializeGraphSnapshot` / `stringifyGraphSnapshot` / `deserializeGraphSnapshot` | Stable, reproducible static snapshots for JS-only hosts |
124
+ | `mapCommunityGraph(graph, opts?)` | `CommunityGraph` → render-ready `RenderGraph` (labels, degree size, per-community tone, baked positions) shared by every renderer tier |
125
+ | `bakeRenderGraph(graph, opts?)` / `stringifyRenderGraph` | Build-time writer: run layout once, emit a deterministic baked-position snapshot |
126
+ | `loadRenderSnapshot(source, opts?)` | Snapshot-first warm start: load a precomputed baked-position graph (URL/object/thunk); `null` → fall back to a live build |
124
127
 
125
128
  ### Layout options (`force`)
126
129
 
@@ -166,9 +169,60 @@ writeFileSync(
166
169
  )
167
170
  ```
168
171
 
172
+ ## Choosing a Renderer
173
+
174
+ The same `CommunityGraph` can be drawn by more than one renderer *tier*. Every tier honors the shared **`GraphControlContract`** — `algorithm`, `filters` (community / edge-kind / node allow-list / `minDegree`), `selectedNodeId` + `onSelectNode`, `onNavigate`, `labelFor`, `colors` — so switching tiers is mechanical: pass the same options object.
175
+
176
+ | Tier | Import | Deps | Best for |
177
+ |---|---|---|---|
178
+ | **JS-only SVG** | `renderCommunityGraph` (`@fortemi/graph`) | none | Static sites, SSR/snapshot pipelines, no-framework hosts |
179
+ | **React (static)** | `GraphView` (`@fortemi/react`) | React | React apps wanting the component + hooks ecosystem |
180
+ | **Interactive 2D** | `SigmaGraphView` (`@fortemi/react/graph-2d`) | `sigma` + `graphology` (optional, lazy) | Live force settling, camera focus, LOD labels |
181
+ | **3D** | `ForceGraph3DView` (`@fortemi/react/graph-3d`) | `react-force-graph-3d` (optional, lazy) | Orbitable 3D force graph, 2D/3D toggle |
182
+
183
+ `communityLegend(graph)` returns `{ communityId, color, count }[]` — the shared data any tier can render as a legend and use to drive the `communityIds` show/hide filter. `applyControlFilters(graph, filters)` is the one filter function every tier runs, so visibility is identical across tiers.
184
+
169
185
  ## Render in a Vanilla JS Host (no React)
170
186
 
171
- A static documentation site can fetch that snapshot and render its own SVG using only the projection helpers — no React, no PGlite:
187
+ `renderCommunityGraph` is a batteries-included SVG renderer built on the engine zoom/pan, hover-neighborhood highlighting, click + keyboard selection, navigation, a labeled popup, and `focus(id)` search with no React and no canvas library. It stays pixel-comparable to `@fortemi/react`'s `GraphView`:
188
+
189
+ ```js
190
+ import { renderCommunityGraph, deserializeGraphSnapshot } from '@fortemi/graph'
191
+
192
+ const graph = deserializeGraphSnapshot(await (await fetch('./graph-snapshot.json')).json())
193
+
194
+ const view = renderCommunityGraph(document.getElementById('graph'), graph, {
195
+ algorithm: 'community',
196
+ filters: { edgeKinds: ['similarity', 'link'], minDegree: 1 },
197
+ labelFor: (id) => graph.nodes.find((n) => n.id === id)?.id ?? id,
198
+ onSelectNode: (id) => console.log('selected', id),
199
+ onNavigate: (id) => location.assign(`#/note/${id}`),
200
+ })
201
+
202
+ view.update({ filters: { communityIds: ['c1'] } }) // reactive filter change
203
+ view.focus('note-42') // search / focus a node
204
+ // view.destroy() // tear down
205
+ ```
206
+
207
+ The **React tier** takes the same contract fields, so a consumer can offer both and switch with no rewiring:
208
+
209
+ ```tsx
210
+ import { GraphView } from '@fortemi/react'
211
+
212
+ <GraphView
213
+ graph={graph}
214
+ layout={{ algorithm: 'community' }}
215
+ filters={{ edgeKinds: ['similarity', 'link'], minDegree: 1 }}
216
+ selectedNodeId={selected}
217
+ onSelectNode={setSelected}
218
+ onNavigate={(id) => navigate(`/note/${id}`)}
219
+ labelFor={(id) => titleOf(id)}
220
+ />
221
+ ```
222
+
223
+ ### Roll your own projection
224
+
225
+ If you need a fully custom view, the projection helpers are exposed directly — `renderCommunityGraph` is just a consumer of them — so you can emit your own SVG/canvas with no PGlite:
172
226
 
173
227
  ```js
174
228
  import {
@@ -0,0 +1,148 @@
1
+ import { QueryExecutor, DatabaseClient, CommunityGraph as CommunityGraph$1, SimilarityGraphResult, CommunitySourceDescriptor, EmbeddingSetSelector, CommunityFilterDefinition, GraphRepository, CommunitiesRepository, CommunityCreateInput } from '@fortemi/core';
2
+
3
+ interface GraphNode {
4
+ id: string;
5
+ }
6
+ interface GraphEdge {
7
+ source: string;
8
+ target: string;
9
+ weight: number;
10
+ kind?: string;
11
+ }
12
+ interface GraphCommunity {
13
+ id: string;
14
+ nodes: string[];
15
+ }
16
+ interface CommunityGraph {
17
+ nodes: GraphNode[];
18
+ edges: GraphEdge[];
19
+ communities: GraphCommunity[];
20
+ }
21
+ /** Deterministic layout algorithms understood by {@link layoutCommunityGraph}. */
22
+ type GraphLayoutAlgorithm = 'force' | 'radial' | 'community' | 'manual';
23
+ /** A node with computed 2D coordinates, render radius, degree, and community. */
24
+ interface PositionedGraphNode extends GraphNode {
25
+ x: number;
26
+ y: number;
27
+ /** Stable render radius, derived from degree/weight by default. */
28
+ r: number;
29
+ degree: number;
30
+ communityId?: string;
31
+ }
32
+ /** A community with a computed centroid over its positioned member nodes. */
33
+ interface PositionedCommunity {
34
+ id: string;
35
+ x: number;
36
+ y: number;
37
+ /** Number of member nodes that contributed to the centroid. */
38
+ size: number;
39
+ }
40
+ /** Result of laying out a {@link CommunityGraph} into 2D space. */
41
+ interface PositionedGraph {
42
+ nodes: PositionedGraphNode[];
43
+ edges: GraphEdge[];
44
+ /** Lookup from node id to its positioned node. */
45
+ nodeIndex: Map<string, PositionedGraphNode>;
46
+ /** Community centroids over the final positions (empty when none). */
47
+ communities: PositionedCommunity[];
48
+ }
49
+ /** Axis-aligned bounding box around a set of positioned nodes. */
50
+ interface GraphBounds {
51
+ minX: number;
52
+ minY: number;
53
+ maxX: number;
54
+ maxY: number;
55
+ width: number;
56
+ height: number;
57
+ centerX: number;
58
+ centerY: number;
59
+ }
60
+ /** A viewport transform that fits a {@link GraphBounds} into a target rect. */
61
+ interface ViewportTransform {
62
+ scale: number;
63
+ offsetX: number;
64
+ offsetY: number;
65
+ }
66
+
67
+ /** A database handle accepted by the underlying repositories. */
68
+ type GraphControllerDb = QueryExecutor & DatabaseClient;
69
+ type GraphSourceMode = 'citations' | 'topics' | 'precomputed' | 'dynamic-search' | 'user-authored';
70
+ interface GraphLayoutState {
71
+ algorithm: GraphLayoutAlgorithm;
72
+ pinSelectedNodes?: boolean;
73
+ preserveViewport?: boolean;
74
+ communitySpacing?: number;
75
+ }
76
+ interface GraphTransitionState {
77
+ fromMode?: GraphSourceMode;
78
+ toMode: GraphSourceMode;
79
+ reason: 'mode-change' | 'embedding-set-change' | 'community-source-change' | 'filter-change' | 'recompute';
80
+ startedAt: string;
81
+ }
82
+ interface GraphControllerStatus {
83
+ loading: boolean;
84
+ error: Error | null;
85
+ freshness: 'fresh' | 'stale' | 'unknown' | null;
86
+ cache: SimilarityGraphResult['cache'] | null;
87
+ }
88
+ type GraphSourceRef = SimilarityGraphResult['graphSource'] | {
89
+ id: string;
90
+ name: string;
91
+ };
92
+ /** The full, framework-agnostic controller state surfaced to subscribers. */
93
+ interface GraphControllerState {
94
+ mode: GraphSourceMode;
95
+ graph: CommunityGraph$1 | null;
96
+ graphSource?: GraphSourceRef;
97
+ communitySource?: CommunitySourceDescriptor;
98
+ embeddingSetSelector?: EmbeddingSetSelector;
99
+ filters?: CommunityFilterDefinition;
100
+ layout: GraphLayoutState;
101
+ status: GraphControllerStatus;
102
+ transition?: GraphTransitionState;
103
+ }
104
+ interface GraphControllerOptions {
105
+ initialMode?: GraphSourceMode;
106
+ initialEmbeddingSetSelector?: EmbeddingSetSelector;
107
+ initialCommunitySourceId?: string;
108
+ initialFilters?: CommunityFilterDefinition;
109
+ layout?: Partial<GraphLayoutState>;
110
+ }
111
+ type GraphControllerListener = (state: GraphControllerState) => void;
112
+ /**
113
+ * Drives graph-source selection and loading independent of any UI framework.
114
+ *
115
+ * Construct with pre-built repositories (handy for tests) or via
116
+ * {@link GraphController.fromDb}. Subscribe with {@link subscribe} and read the
117
+ * current state with {@link getState}; call {@link start} once to trigger the
118
+ * initial load. Setters update state and schedule a refresh, mirroring the
119
+ * effect-driven behavior of the original React hook.
120
+ */
121
+ declare class GraphController {
122
+ private readonly graphRepo;
123
+ private readonly communityRepo;
124
+ private readonly listeners;
125
+ private communitySourceId;
126
+ private state;
127
+ /** Build a controller from a database handle (production path). */
128
+ static fromDb(db: GraphControllerDb, options?: GraphControllerOptions): GraphController;
129
+ constructor(graphRepo: GraphRepository, communityRepo: CommunitiesRepository, options?: GraphControllerOptions);
130
+ getState(): GraphControllerState;
131
+ subscribe(listener: GraphControllerListener): () => void;
132
+ /** Run the initial load. Call once after construction (the hook calls this on mount). */
133
+ start(): Promise<void>;
134
+ private setState;
135
+ private beginTransition;
136
+ refresh(): Promise<void>;
137
+ setMode(nextMode: GraphSourceMode): void;
138
+ setEmbeddingSetSelector(selector: EmbeddingSetSelector): void;
139
+ setCommunitySource(sourceId: string | null): void;
140
+ /** Apply dynamic-search filters and switch into that mode (state only). */
141
+ private applyFilters;
142
+ setFilters(nextFilters: CommunityFilterDefinition): void;
143
+ recompute(): Promise<void>;
144
+ previewDynamicCommunity(nextFilters: CommunityFilterDefinition): Promise<void>;
145
+ saveCurrentCommunity(input: CommunityCreateInput): Promise<CommunitySourceDescriptor>;
146
+ }
147
+
148
+ export { type CommunityGraph as C, type GraphNode as G, type PositionedGraph as P, type ViewportTransform as V, type GraphLayoutAlgorithm as a, type PositionedGraphNode as b, type GraphBounds as c, type GraphCommunity as d, type GraphControllerDb as e, type GraphControllerListener as f, type GraphControllerOptions as g, type GraphControllerState as h, type GraphControllerStatus as i, type GraphEdge as j, type GraphLayoutState as k, type GraphSourceMode as l, type GraphSourceRef as m, type GraphTransitionState as n, type PositionedCommunity as o, GraphController as p };
@@ -0,0 +1,2 @@
1
+ import '@fortemi/core';
2
+ export { p as GraphController, e as GraphControllerDb, f as GraphControllerListener, g as GraphControllerOptions, h as GraphControllerState, i as GraphControllerStatus, k as GraphLayoutState, l as GraphSourceMode, m as GraphSourceRef, n as GraphTransitionState } from './controller-m13D5PbF.js';
@@ -0,0 +1,184 @@
1
+ import { GraphRepository, CommunitiesRepository } from '@fortemi/core';
2
+
3
+ // src/controller.ts
4
+ var DEFAULT_LAYOUT = {
5
+ algorithm: "force",
6
+ preserveViewport: true,
7
+ communitySpacing: 1
8
+ };
9
+ var GraphController = class _GraphController {
10
+ graphRepo;
11
+ communityRepo;
12
+ listeners = /* @__PURE__ */ new Set();
13
+ communitySourceId;
14
+ state;
15
+ /** Build a controller from a database handle (production path). */
16
+ static fromDb(db, options = {}) {
17
+ return new _GraphController(new GraphRepository(db), new CommunitiesRepository(db), options);
18
+ }
19
+ constructor(graphRepo, communityRepo, options = {}) {
20
+ this.graphRepo = graphRepo;
21
+ this.communityRepo = communityRepo;
22
+ this.communitySourceId = options.initialCommunitySourceId ?? null;
23
+ this.state = {
24
+ mode: options.initialMode ?? "citations",
25
+ graph: null,
26
+ graphSource: void 0,
27
+ communitySource: void 0,
28
+ embeddingSetSelector: options.initialEmbeddingSetSelector,
29
+ filters: options.initialFilters,
30
+ layout: { ...DEFAULT_LAYOUT, ...options.layout },
31
+ status: { loading: false, error: null, freshness: null, cache: null },
32
+ transition: void 0
33
+ };
34
+ }
35
+ getState() {
36
+ return this.state;
37
+ }
38
+ subscribe(listener) {
39
+ this.listeners.add(listener);
40
+ return () => {
41
+ this.listeners.delete(listener);
42
+ };
43
+ }
44
+ /** Run the initial load. Call once after construction (the hook calls this on mount). */
45
+ async start() {
46
+ await this.refresh();
47
+ }
48
+ setState(patch) {
49
+ this.state = { ...this.state, ...patch };
50
+ for (const listener of this.listeners) listener(this.state);
51
+ }
52
+ beginTransition(toMode, reason, fromMode = this.state.mode) {
53
+ this.setState({ transition: { fromMode, toMode, reason, startedAt: (/* @__PURE__ */ new Date()).toISOString() } });
54
+ }
55
+ async refresh() {
56
+ try {
57
+ this.setState({ status: { ...this.state.status, loading: true, error: null } });
58
+ const sources = await this.communityRepo.listCommunitySources();
59
+ const activeCommunity = this.communitySourceId ? sources.find((source) => source.id === this.communitySourceId) : void 0;
60
+ this.setState({ communitySource: activeCommunity });
61
+ const mode = this.state.mode;
62
+ if (mode === "citations") {
63
+ const nextGraph = await this.graphRepo.buildLinkGraph();
64
+ this.setState({
65
+ graph: nextGraph,
66
+ graphSource: { id: "citations", name: "Citation graph" },
67
+ status: { loading: false, error: null, freshness: "fresh", cache: null }
68
+ });
69
+ return;
70
+ }
71
+ if (mode === "topics") {
72
+ const selector = this.state.embeddingSetSelector;
73
+ if (!selector) throw new Error("topics mode requires an embedding-set selector");
74
+ const result = await this.graphRepo.buildOrLoadSimilarityGraph({ selector });
75
+ this.setState({
76
+ graph: result.graph,
77
+ graphSource: result.graphSource,
78
+ status: { loading: false, error: null, freshness: result.freshness, cache: result.cache }
79
+ });
80
+ return;
81
+ }
82
+ if (mode === "precomputed") {
83
+ if (!activeCommunity?.graphSourceId) {
84
+ throw new Error("precomputed mode requires a community source with graphSourceId");
85
+ }
86
+ const assignments = await this.communityRepo.getCommunityAssignments(activeCommunity.id);
87
+ const nextGraph = await this.graphRepo.loadGraphArtifact(
88
+ activeCommunity.graphSourceId,
89
+ assignments.map((assignment) => assignment.noteId)
90
+ );
91
+ this.setState({
92
+ graph: nextGraph,
93
+ graphSource: { id: activeCommunity.graphSourceId, name: activeCommunity.name },
94
+ status: { loading: false, error: null, freshness: activeCommunity.freshness ?? "unknown", cache: null }
95
+ });
96
+ return;
97
+ }
98
+ if (mode === "dynamic-search") {
99
+ const assignments = await this.communityRepo.previewDynamicCommunity(this.state.filters ?? {});
100
+ this.setState({
101
+ graph: {
102
+ nodes: assignments.map((assignment) => ({ id: assignment.noteId })),
103
+ edges: [],
104
+ communities: [{ id: "dynamic-preview", nodes: assignments.map((assignment) => assignment.noteId) }]
105
+ },
106
+ graphSource: { id: "dynamic-preview", name: "Dynamic preview" },
107
+ status: { loading: false, error: null, freshness: "unknown", cache: null }
108
+ });
109
+ return;
110
+ }
111
+ if (mode === "user-authored") {
112
+ if (!activeCommunity) throw new Error("user-authored mode requires an active community source");
113
+ const assignments = await this.communityRepo.getCommunityAssignments(activeCommunity.id);
114
+ this.setState({
115
+ graph: {
116
+ nodes: assignments.map((assignment) => ({ id: assignment.noteId })),
117
+ edges: [],
118
+ communities: [{ id: activeCommunity.id, nodes: assignments.map((assignment) => assignment.noteId) }]
119
+ },
120
+ graphSource: { id: activeCommunity.graphSourceId ?? activeCommunity.id, name: activeCommunity.name },
121
+ status: { loading: false, error: null, freshness: activeCommunity.freshness ?? "unknown", cache: null }
122
+ });
123
+ }
124
+ } catch (err) {
125
+ const e = err instanceof Error ? err : new Error(String(err));
126
+ this.setState({ status: { ...this.state.status, loading: false, error: e } });
127
+ throw e;
128
+ }
129
+ }
130
+ setMode(nextMode) {
131
+ this.beginTransition(nextMode, "mode-change");
132
+ this.setState({ mode: nextMode });
133
+ void this.refresh();
134
+ }
135
+ setEmbeddingSetSelector(selector) {
136
+ this.beginTransition("topics", "embedding-set-change");
137
+ this.setState({ embeddingSetSelector: selector, mode: "topics" });
138
+ void this.refresh();
139
+ }
140
+ setCommunitySource(sourceId) {
141
+ this.communitySourceId = sourceId;
142
+ this.beginTransition(this.state.mode, "community-source-change");
143
+ void this.refresh();
144
+ }
145
+ /** Apply dynamic-search filters and switch into that mode (state only). */
146
+ applyFilters(nextFilters) {
147
+ this.beginTransition("dynamic-search", "filter-change");
148
+ this.setState({ filters: nextFilters, mode: "dynamic-search" });
149
+ }
150
+ setFilters(nextFilters) {
151
+ this.applyFilters(nextFilters);
152
+ void this.refresh();
153
+ }
154
+ async recompute() {
155
+ this.beginTransition(this.state.mode, "recompute");
156
+ if (this.state.mode === "topics" && this.state.embeddingSetSelector) {
157
+ const result = await this.graphRepo.buildOrLoadSimilarityGraph({
158
+ selector: this.state.embeddingSetSelector,
159
+ source: "live-only"
160
+ });
161
+ this.setState({
162
+ graph: result.graph,
163
+ graphSource: result.graphSource,
164
+ status: { loading: false, error: null, freshness: result.freshness, cache: result.cache }
165
+ });
166
+ return;
167
+ }
168
+ await this.refresh();
169
+ }
170
+ async previewDynamicCommunity(nextFilters) {
171
+ this.applyFilters(nextFilters);
172
+ await this.refresh();
173
+ }
174
+ async saveCurrentCommunity(input) {
175
+ const source = await this.communityRepo.saveCommunity(input);
176
+ this.communitySourceId = source.id;
177
+ this.setState({ communitySource: source });
178
+ return source;
179
+ }
180
+ };
181
+
182
+ export { GraphController };
183
+ //# sourceMappingURL=controller.js.map
184
+ //# sourceMappingURL=controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/controller.ts"],"names":[],"mappings":"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","file":"controller.js","sourcesContent":["// Framework-agnostic graph-source controller.\n//\n// Owns the graph \"source mode\" state machine (citations | topics | precomputed\n// | dynamic-search | user-authored), transition tracking, and the mode→fetch\n// dispatch. The capability previously lived inside the React `useGraphController`\n// hook; it now lives here so JS-only hosts can drive the same logic without\n// React. The React hook is a thin `useSyncExternalStore` adapter over this class.\n//\n// This module depends on @fortemi/core for the repositories and shared types.\n// @fortemi/core never depends on @fortemi/graph — the dependency direction is\n// the linear chain: pglite ← @fortemi/core ← @fortemi/graph ← @fortemi/react.\n\nimport {\n CommunitiesRepository,\n GraphRepository,\n type CommunityCreateInput,\n type CommunityFilterDefinition,\n type CommunityGraph,\n type CommunitySourceDescriptor,\n type DatabaseClient,\n type EmbeddingSetSelector,\n type QueryExecutor,\n type SimilarityGraphResult,\n} from '@fortemi/core'\nimport type { GraphLayoutAlgorithm } from './types.js'\n\n/** A database handle accepted by the underlying repositories. */\nexport type GraphControllerDb = QueryExecutor & DatabaseClient\n\nexport type GraphSourceMode =\n | 'citations'\n | 'topics'\n | 'precomputed'\n | 'dynamic-search'\n | 'user-authored'\n\nexport interface GraphLayoutState {\n algorithm: GraphLayoutAlgorithm\n pinSelectedNodes?: boolean\n preserveViewport?: boolean\n communitySpacing?: number\n}\n\nexport interface GraphTransitionState {\n fromMode?: GraphSourceMode\n toMode: GraphSourceMode\n reason: 'mode-change' | 'embedding-set-change' | 'community-source-change' | 'filter-change' | 'recompute'\n startedAt: string\n}\n\nexport interface GraphControllerStatus {\n loading: boolean\n error: Error | null\n freshness: 'fresh' | 'stale' | 'unknown' | null\n cache: SimilarityGraphResult['cache'] | null\n}\n\nexport type GraphSourceRef = SimilarityGraphResult['graphSource'] | { id: string; name: string }\n\n/** The full, framework-agnostic controller state surfaced to subscribers. */\nexport interface GraphControllerState {\n mode: GraphSourceMode\n graph: CommunityGraph | null\n graphSource?: GraphSourceRef\n communitySource?: CommunitySourceDescriptor\n embeddingSetSelector?: EmbeddingSetSelector\n filters?: CommunityFilterDefinition\n layout: GraphLayoutState\n status: GraphControllerStatus\n transition?: GraphTransitionState\n}\n\nexport interface GraphControllerOptions {\n initialMode?: GraphSourceMode\n initialEmbeddingSetSelector?: EmbeddingSetSelector\n initialCommunitySourceId?: string\n initialFilters?: CommunityFilterDefinition\n layout?: Partial<GraphLayoutState>\n}\n\nconst DEFAULT_LAYOUT: GraphLayoutState = {\n algorithm: 'force',\n preserveViewport: true,\n communitySpacing: 1,\n}\n\nexport type GraphControllerListener = (state: GraphControllerState) => void\n\n/**\n * Drives graph-source selection and loading independent of any UI framework.\n *\n * Construct with pre-built repositories (handy for tests) or via\n * {@link GraphController.fromDb}. Subscribe with {@link subscribe} and read the\n * current state with {@link getState}; call {@link start} once to trigger the\n * initial load. Setters update state and schedule a refresh, mirroring the\n * effect-driven behavior of the original React hook.\n */\nexport class GraphController {\n private readonly graphRepo: GraphRepository\n private readonly communityRepo: CommunitiesRepository\n private readonly listeners = new Set<GraphControllerListener>()\n private communitySourceId: string | null\n private state: GraphControllerState\n\n /** Build a controller from a database handle (production path). */\n static fromDb(db: GraphControllerDb, options: GraphControllerOptions = {}): GraphController {\n return new GraphController(new GraphRepository(db), new CommunitiesRepository(db), options)\n }\n\n constructor(\n graphRepo: GraphRepository,\n communityRepo: CommunitiesRepository,\n options: GraphControllerOptions = {},\n ) {\n this.graphRepo = graphRepo\n this.communityRepo = communityRepo\n this.communitySourceId = options.initialCommunitySourceId ?? null\n this.state = {\n mode: options.initialMode ?? 'citations',\n graph: null,\n graphSource: undefined,\n communitySource: undefined,\n embeddingSetSelector: options.initialEmbeddingSetSelector,\n filters: options.initialFilters,\n layout: { ...DEFAULT_LAYOUT, ...options.layout },\n status: { loading: false, error: null, freshness: null, cache: null },\n transition: undefined,\n }\n }\n\n getState(): GraphControllerState {\n return this.state\n }\n\n subscribe(listener: GraphControllerListener): () => void {\n this.listeners.add(listener)\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n /** Run the initial load. Call once after construction (the hook calls this on mount). */\n async start(): Promise<void> {\n await this.refresh()\n }\n\n private setState(patch: Partial<GraphControllerState>): void {\n this.state = { ...this.state, ...patch }\n for (const listener of this.listeners) listener(this.state)\n }\n\n private beginTransition(\n toMode: GraphSourceMode,\n reason: GraphTransitionState['reason'],\n fromMode: GraphSourceMode = this.state.mode,\n ): void {\n this.setState({ transition: { fromMode, toMode, reason, startedAt: new Date().toISOString() } })\n }\n\n async refresh(): Promise<void> {\n try {\n this.setState({ status: { ...this.state.status, loading: true, error: null } })\n const sources = await this.communityRepo.listCommunitySources()\n const activeCommunity = this.communitySourceId\n ? sources.find((source) => source.id === this.communitySourceId)\n : undefined\n this.setState({ communitySource: activeCommunity })\n\n const mode = this.state.mode\n\n if (mode === 'citations') {\n const nextGraph = await this.graphRepo.buildLinkGraph()\n this.setState({\n graph: nextGraph,\n graphSource: { id: 'citations', name: 'Citation graph' },\n status: { loading: false, error: null, freshness: 'fresh', cache: null },\n })\n return\n }\n\n if (mode === 'topics') {\n const selector = this.state.embeddingSetSelector\n if (!selector) throw new Error('topics mode requires an embedding-set selector')\n const result = await this.graphRepo.buildOrLoadSimilarityGraph({ selector })\n this.setState({\n graph: result.graph,\n graphSource: result.graphSource,\n status: { loading: false, error: null, freshness: result.freshness, cache: result.cache },\n })\n return\n }\n\n if (mode === 'precomputed') {\n if (!activeCommunity?.graphSourceId) {\n throw new Error('precomputed mode requires a community source with graphSourceId')\n }\n const assignments = await this.communityRepo.getCommunityAssignments(activeCommunity.id)\n const nextGraph = await this.graphRepo.loadGraphArtifact(\n activeCommunity.graphSourceId,\n assignments.map((assignment) => assignment.noteId),\n )\n this.setState({\n graph: nextGraph,\n graphSource: { id: activeCommunity.graphSourceId, name: activeCommunity.name },\n status: { loading: false, error: null, freshness: activeCommunity.freshness ?? 'unknown', cache: null },\n })\n return\n }\n\n if (mode === 'dynamic-search') {\n const assignments = await this.communityRepo.previewDynamicCommunity(this.state.filters ?? {})\n this.setState({\n graph: {\n nodes: assignments.map((assignment) => ({ id: assignment.noteId })),\n edges: [],\n communities: [{ id: 'dynamic-preview', nodes: assignments.map((assignment) => assignment.noteId) }],\n },\n graphSource: { id: 'dynamic-preview', name: 'Dynamic preview' },\n status: { loading: false, error: null, freshness: 'unknown', cache: null },\n })\n return\n }\n\n if (mode === 'user-authored') {\n if (!activeCommunity) throw new Error('user-authored mode requires an active community source')\n const assignments = await this.communityRepo.getCommunityAssignments(activeCommunity.id)\n this.setState({\n graph: {\n nodes: assignments.map((assignment) => ({ id: assignment.noteId })),\n edges: [],\n communities: [{ id: activeCommunity.id, nodes: assignments.map((assignment) => assignment.noteId) }],\n },\n graphSource: { id: activeCommunity.graphSourceId ?? activeCommunity.id, name: activeCommunity.name },\n status: { loading: false, error: null, freshness: activeCommunity.freshness ?? 'unknown', cache: null },\n })\n }\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err))\n this.setState({ status: { ...this.state.status, loading: false, error: e } })\n throw e\n }\n }\n\n setMode(nextMode: GraphSourceMode): void {\n this.beginTransition(nextMode, 'mode-change')\n this.setState({ mode: nextMode })\n void this.refresh()\n }\n\n setEmbeddingSetSelector(selector: EmbeddingSetSelector): void {\n this.beginTransition('topics', 'embedding-set-change')\n this.setState({ embeddingSetSelector: selector, mode: 'topics' })\n void this.refresh()\n }\n\n setCommunitySource(sourceId: string | null): void {\n this.communitySourceId = sourceId\n this.beginTransition(this.state.mode, 'community-source-change')\n void this.refresh()\n }\n\n /** Apply dynamic-search filters and switch into that mode (state only). */\n private applyFilters(nextFilters: CommunityFilterDefinition): void {\n this.beginTransition('dynamic-search', 'filter-change')\n this.setState({ filters: nextFilters, mode: 'dynamic-search' })\n }\n\n setFilters(nextFilters: CommunityFilterDefinition): void {\n this.applyFilters(nextFilters)\n void this.refresh()\n }\n\n async recompute(): Promise<void> {\n this.beginTransition(this.state.mode, 'recompute')\n if (this.state.mode === 'topics' && this.state.embeddingSetSelector) {\n const result = await this.graphRepo.buildOrLoadSimilarityGraph({\n selector: this.state.embeddingSetSelector,\n source: 'live-only',\n })\n this.setState({\n graph: result.graph,\n graphSource: result.graphSource,\n status: { loading: false, error: null, freshness: result.freshness, cache: result.cache },\n })\n return\n }\n await this.refresh()\n }\n\n async previewDynamicCommunity(nextFilters: CommunityFilterDefinition): Promise<void> {\n this.applyFilters(nextFilters)\n await this.refresh()\n }\n\n async saveCurrentCommunity(input: CommunityCreateInput): Promise<CommunitySourceDescriptor> {\n const source = await this.communityRepo.saveCommunity(input)\n this.communitySourceId = source.id\n this.setState({ communitySource: source })\n return source\n }\n}\n"]}