@fortemi/graph 2026.7.4 → 2026.7.7

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
 
@@ -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":";;;AAgFA,IAAM,cAAA,GAAmC;AAAA,EACvC,SAAA,EAAW,OAAA;AAAA,EACX,gBAAA,EAAkB,IAAA;AAAA,EAClB,gBAAA,EAAkB;AACpB,CAAA;AAaO,IAAM,eAAA,GAAN,MAAM,gBAAA,CAAgB;AAAA,EACV,SAAA;AAAA,EACA,aAAA;AAAA,EACA,SAAA,uBAAgB,GAAA,EAA6B;AAAA,EACtD,iBAAA;AAAA,EACA,KAAA;AAAA;AAAA,EAGR,OAAO,MAAA,CAAO,EAAA,EAAuB,OAAA,GAAkC,EAAC,EAAoB;AAC1F,IAAA,OAAO,IAAI,gBAAA,CAAgB,IAAI,eAAA,CAAgB,EAAE,GAAG,IAAI,qBAAA,CAAsB,EAAE,CAAA,EAAG,OAAO,CAAA;AAAA,EAC5F;AAAA,EAEA,WAAA,CACE,SAAA,EACA,aAAA,EACA,OAAA,GAAkC,EAAC,EACnC;AACA,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AACjB,IAAA,IAAA,CAAK,aAAA,GAAgB,aAAA;AACrB,IAAA,IAAA,CAAK,iBAAA,GAAoB,QAAQ,wBAAA,IAA4B,IAAA;AAC7D,IAAA,IAAA,CAAK,KAAA,GAAQ;AAAA,MACX,IAAA,EAAM,QAAQ,WAAA,IAAe,WAAA;AAAA,MAC7B,KAAA,EAAO,IAAA;AAAA,MACP,WAAA,EAAa,MAAA;AAAA,MACb,eAAA,EAAiB,MAAA;AAAA,MACjB,sBAAsB,OAAA,CAAQ,2BAAA;AAAA,MAC9B,SAAS,OAAA,CAAQ,cAAA;AAAA,MACjB,QAAQ,EAAE,GAAG,cAAA,EAAgB,GAAG,QAAQ,MAAA,EAAO;AAAA,MAC/C,MAAA,EAAQ,EAAE,OAAA,EAAS,KAAA,EAAO,OAAO,IAAA,EAAM,SAAA,EAAW,IAAA,EAAM,KAAA,EAAO,IAAA,EAAK;AAAA,MACpE,UAAA,EAAY;AAAA,KACd;AAAA,EACF;AAAA,EAEA,QAAA,GAAiC;AAC/B,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd;AAAA,EAEA,UAAU,QAAA,EAA+C;AACvD,IAAA,IAAA,CAAK,SAAA,CAAU,IAAI,QAAQ,CAAA;AAC3B,IAAA,OAAO,MAAM;AACX,MAAA,IAAA,CAAK,SAAA,CAAU,OAAO,QAAQ,CAAA;AAAA,IAChC,CAAA;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,KAAA,GAAuB;AAC3B,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEQ,SAAS,KAAA,EAA4C;AAC3D,IAAA,IAAA,CAAK,QAAQ,EAAE,GAAG,IAAA,CAAK,KAAA,EAAO,GAAG,KAAA,EAAM;AACvC,IAAA,KAAA,MAAW,QAAA,IAAY,IAAA,CAAK,SAAA,EAAW,QAAA,CAAS,KAAK,KAAK,CAAA;AAAA,EAC5D;AAAA,EAEQ,gBACN,MAAA,EACA,MAAA,EACA,QAAA,GAA4B,IAAA,CAAK,MAAM,IAAA,EACjC;AACN,IAAA,IAAA,CAAK,QAAA,CAAS,EAAE,UAAA,EAAY,EAAE,UAAU,MAAA,EAAQ,MAAA,EAAQ,SAAA,EAAA,iBAAW,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY,IAAK,CAAA;AAAA,EACjG;AAAA,EAEA,MAAM,OAAA,GAAyB;AAC7B,IAAA,IAAI;AACF,MAAA,IAAA,CAAK,QAAA,CAAS,EAAE,MAAA,EAAQ,EAAE,GAAG,IAAA,CAAK,KAAA,CAAM,MAAA,EAAQ,OAAA,EAAS,IAAA,EAAM,KAAA,EAAO,IAAA,IAAQ,CAAA;AAC9E,MAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,aAAA,CAAc,oBAAA,EAAqB;AAC9D,MAAA,MAAM,eAAA,GAAkB,IAAA,CAAK,iBAAA,GACzB,OAAA,CAAQ,IAAA,CAAK,CAAC,MAAA,KAAW,MAAA,CAAO,EAAA,KAAO,IAAA,CAAK,iBAAiB,CAAA,GAC7D,KAAA,CAAA;AACJ,MAAA,IAAA,CAAK,QAAA,CAAS,EAAE,eAAA,EAAiB,eAAA,EAAiB,CAAA;AAElD,MAAA,MAAM,IAAA,GAAO,KAAK,KAAA,CAAM,IAAA;AAExB,MAAA,IAAI,SAAS,WAAA,EAAa;AACxB,QAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,SAAA,CAAU,cAAA,EAAe;AACtD,QAAA,IAAA,CAAK,QAAA,CAAS;AAAA,UACZ,KAAA,EAAO,SAAA;AAAA,UACP,WAAA,EAAa,EAAE,EAAA,EAAI,WAAA,EAAa,MAAM,gBAAA,EAAiB;AAAA,UACvD,MAAA,EAAQ,EAAE,OAAA,EAAS,KAAA,EAAO,OAAO,IAAA,EAAM,SAAA,EAAW,OAAA,EAAS,KAAA,EAAO,IAAA;AAAK,SACxE,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,SAAS,QAAA,EAAU;AACrB,QAAA,MAAM,QAAA,GAAW,KAAK,KAAA,CAAM,oBAAA;AAC5B,QAAA,IAAI,CAAC,QAAA,EAAU,MAAM,IAAI,MAAM,gDAAgD,CAAA;AAC/E,QAAA,MAAM,SAAS,MAAM,IAAA,CAAK,UAAU,0BAAA,CAA2B,EAAE,UAAU,CAAA;AAC3E,QAAA,IAAA,CAAK,QAAA,CAAS;AAAA,UACZ,OAAO,MAAA,CAAO,KAAA;AAAA,UACd,aAAa,MAAA,CAAO,WAAA;AAAA,UACpB,MAAA,EAAQ,EAAE,OAAA,EAAS,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA,EAAW,MAAA,CAAO,SAAA,EAAW,KAAA,EAAO,MAAA,CAAO,KAAA;AAAM,SACzF,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,SAAS,aAAA,EAAe;AAC1B,QAAA,IAAI,CAAC,iBAAiB,aAAA,EAAe;AACnC,UAAA,MAAM,IAAI,MAAM,iEAAiE,CAAA;AAAA,QACnF;AACA,QAAA,MAAM,cAAc,MAAM,IAAA,CAAK,aAAA,CAAc,uBAAA,CAAwB,gBAAgB,EAAE,CAAA;AACvF,QAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,SAAA,CAAU,iBAAA;AAAA,UACrC,eAAA,CAAgB,aAAA;AAAA,UAChB,WAAA,CAAY,GAAA,CAAI,CAAC,UAAA,KAAe,WAAW,MAAM;AAAA,SACnD;AACA,QAAA,IAAA,CAAK,QAAA,CAAS;AAAA,UACZ,KAAA,EAAO,SAAA;AAAA,UACP,aAAa,EAAE,EAAA,EAAI,gBAAgB,aAAA,EAAe,IAAA,EAAM,gBAAgB,IAAA,EAAK;AAAA,UAC7E,MAAA,EAAQ,EAAE,OAAA,EAAS,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA,EAAW,eAAA,CAAgB,SAAA,IAAa,SAAA,EAAW,KAAA,EAAO,IAAA;AAAK,SACvG,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,SAAS,gBAAA,EAAkB;AAC7B,QAAA,MAAM,WAAA,GAAc,MAAM,IAAA,CAAK,aAAA,CAAc,wBAAwB,IAAA,CAAK,KAAA,CAAM,OAAA,IAAW,EAAE,CAAA;AAC7F,QAAA,IAAA,CAAK,QAAA,CAAS;AAAA,UACZ,KAAA,EAAO;AAAA,YACL,KAAA,EAAO,YAAY,GAAA,CAAI,CAAC,gBAAgB,EAAE,EAAA,EAAI,UAAA,CAAW,MAAA,EAAO,CAAE,CAAA;AAAA,YAClE,OAAO,EAAC;AAAA,YACR,WAAA,EAAa,CAAC,EAAE,EAAA,EAAI,iBAAA,EAAmB,KAAA,EAAO,WAAA,CAAY,GAAA,CAAI,CAAC,UAAA,KAAe,UAAA,CAAW,MAAM,GAAG;AAAA,WACpG;AAAA,UACA,WAAA,EAAa,EAAE,EAAA,EAAI,iBAAA,EAAmB,MAAM,iBAAA,EAAkB;AAAA,UAC9D,MAAA,EAAQ,EAAE,OAAA,EAAS,KAAA,EAAO,OAAO,IAAA,EAAM,SAAA,EAAW,SAAA,EAAW,KAAA,EAAO,IAAA;AAAK,SAC1E,CAAA;AACD,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,SAAS,eAAA,EAAiB;AAC5B,QAAA,IAAI,CAAC,eAAA,EAAiB,MAAM,IAAI,MAAM,wDAAwD,CAAA;AAC9F,QAAA,MAAM,cAAc,MAAM,IAAA,CAAK,aAAA,CAAc,uBAAA,CAAwB,gBAAgB,EAAE,CAAA;AACvF,QAAA,IAAA,CAAK,QAAA,CAAS;AAAA,UACZ,KAAA,EAAO;AAAA,YACL,KAAA,EAAO,YAAY,GAAA,CAAI,CAAC,gBAAgB,EAAE,EAAA,EAAI,UAAA,CAAW,MAAA,EAAO,CAAE,CAAA;AAAA,YAClE,OAAO,EAAC;AAAA,YACR,WAAA,EAAa,CAAC,EAAE,EAAA,EAAI,gBAAgB,EAAA,EAAI,KAAA,EAAO,WAAA,CAAY,GAAA,CAAI,CAAC,UAAA,KAAe,UAAA,CAAW,MAAM,GAAG;AAAA,WACrG;AAAA,UACA,WAAA,EAAa,EAAE,EAAA,EAAI,eAAA,CAAgB,iBAAiB,eAAA,CAAgB,EAAA,EAAI,IAAA,EAAM,eAAA,CAAgB,IAAA,EAAK;AAAA,UACnG,MAAA,EAAQ,EAAE,OAAA,EAAS,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA,EAAW,eAAA,CAAgB,SAAA,IAAa,SAAA,EAAW,KAAA,EAAO,IAAA;AAAK,SACvG,CAAA;AAAA,MACH;AAAA,IACF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,CAAA,GAAI,eAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,KAAA,CAAM,MAAA,CAAO,GAAG,CAAC,CAAA;AAC5D,MAAA,IAAA,CAAK,QAAA,CAAS,EAAE,MAAA,EAAQ,EAAE,GAAG,IAAA,CAAK,KAAA,CAAM,MAAA,EAAQ,OAAA,EAAS,KAAA,EAAO,KAAA,EAAO,CAAA,IAAK,CAAA;AAC5E,MAAA,MAAM,CAAA;AAAA,IACR;AAAA,EACF;AAAA,EAEA,QAAQ,QAAA,EAAiC;AACvC,IAAA,IAAA,CAAK,eAAA,CAAgB,UAAU,aAAa,CAAA;AAC5C,IAAA,IAAA,CAAK,QAAA,CAAS,EAAE,IAAA,EAAM,QAAA,EAAU,CAAA;AAChC,IAAA,KAAK,KAAK,OAAA,EAAQ;AAAA,EACpB;AAAA,EAEA,wBAAwB,QAAA,EAAsC;AAC5D,IAAA,IAAA,CAAK,eAAA,CAAgB,UAAU,sBAAsB,CAAA;AACrD,IAAA,IAAA,CAAK,SAAS,EAAE,oBAAA,EAAsB,QAAA,EAAU,IAAA,EAAM,UAAU,CAAA;AAChE,IAAA,KAAK,KAAK,OAAA,EAAQ;AAAA,EACpB;AAAA,EAEA,mBAAmB,QAAA,EAA+B;AAChD,IAAA,IAAA,CAAK,iBAAA,GAAoB,QAAA;AACzB,IAAA,IAAA,CAAK,eAAA,CAAgB,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,yBAAyB,CAAA;AAC/D,IAAA,KAAK,KAAK,OAAA,EAAQ;AAAA,EACpB;AAAA;AAAA,EAGQ,aAAa,WAAA,EAA8C;AACjE,IAAA,IAAA,CAAK,eAAA,CAAgB,kBAAkB,eAAe,CAAA;AACtD,IAAA,IAAA,CAAK,SAAS,EAAE,OAAA,EAAS,WAAA,EAAa,IAAA,EAAM,kBAAkB,CAAA;AAAA,EAChE;AAAA,EAEA,WAAW,WAAA,EAA8C;AACvD,IAAA,IAAA,CAAK,aAAa,WAAW,CAAA;AAC7B,IAAA,KAAK,KAAK,OAAA,EAAQ;AAAA,EACpB;AAAA,EAEA,MAAM,SAAA,GAA2B;AAC/B,IAAA,IAAA,CAAK,eAAA,CAAgB,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,WAAW,CAAA;AACjD,IAAA,IAAI,KAAK,KAAA,CAAM,IAAA,KAAS,QAAA,IAAY,IAAA,CAAK,MAAM,oBAAA,EAAsB;AACnE,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,SAAA,CAAU,0BAAA,CAA2B;AAAA,QAC7D,QAAA,EAAU,KAAK,KAAA,CAAM,oBAAA;AAAA,QACrB,MAAA,EAAQ;AAAA,OACT,CAAA;AACD,MAAA,IAAA,CAAK,QAAA,CAAS;AAAA,QACZ,OAAO,MAAA,CAAO,KAAA;AAAA,QACd,aAAa,MAAA,CAAO,WAAA;AAAA,QACpB,MAAA,EAAQ,EAAE,OAAA,EAAS,KAAA,EAAO,KAAA,EAAO,IAAA,EAAM,SAAA,EAAW,MAAA,CAAO,SAAA,EAAW,KAAA,EAAO,MAAA,CAAO,KAAA;AAAM,OACzF,CAAA;AACD,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAM,wBAAwB,WAAA,EAAuD;AACnF,IAAA,IAAA,CAAK,aAAa,WAAW,CAAA;AAC7B,IAAA,MAAM,KAAK,OAAA,EAAQ;AAAA,EACrB;AAAA,EAEA,MAAM,qBAAqB,KAAA,EAAiE;AAC1F,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,aAAA,CAAc,cAAc,KAAK,CAAA;AAC3D,IAAA,IAAA,CAAK,oBAAoB,MAAA,CAAO,EAAA;AAChC,IAAA,IAAA,CAAK,QAAA,CAAS,EAAE,eAAA,EAAiB,MAAA,EAAQ,CAAA;AACzC,IAAA,OAAO,MAAA;AAAA,EACT;AACF","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"]}
package/dist/index.d.ts CHANGED
@@ -1,68 +1,6 @@
1
- import { QueryExecutor, DatabaseClient, EmbeddingSetSelector, CommunityFilterDefinition, GraphRepository, CommunitiesRepository, CommunityGraph as CommunityGraph$1, SimilarityGraphResult, CommunitySourceDescriptor, 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
- }
1
+ import { C as CommunityGraph, G as GraphNode, a as GraphLayoutAlgorithm, P as PositionedGraph, b as PositionedGraphNode, c as GraphBounds, V as ViewportTransform } from './controller-m13D5PbF.js';
2
+ export { d as GraphCommunity, e as GraphControllerDb, f as GraphControllerListener, g as GraphControllerOptions, h as GraphControllerState, i as GraphControllerStatus, j as GraphEdge, k as GraphLayoutState, l as GraphSourceMode, m as GraphSourceRef, n as GraphTransitionState, o as PositionedCommunity } from './controller-m13D5PbF.js';
3
+ import '@fortemi/core';
66
4
 
67
5
  /** Count the (undirected) degree of every node, including isolated nodes (0). */
68
6
  declare function computeDegrees(graph: CommunityGraph): Map<string, number>;
@@ -477,87 +415,6 @@ interface GraphRenderHandle {
477
415
  */
478
416
  declare function renderCommunityGraph(container: HTMLElement, graph: CommunityGraph, options?: GraphRenderOptions): GraphRenderHandle;
479
417
 
480
- /** A database handle accepted by the underlying repositories. */
481
- type GraphControllerDb = QueryExecutor & DatabaseClient;
482
- type GraphSourceMode = 'citations' | 'topics' | 'precomputed' | 'dynamic-search' | 'user-authored';
483
- interface GraphLayoutState {
484
- algorithm: GraphLayoutAlgorithm;
485
- pinSelectedNodes?: boolean;
486
- preserveViewport?: boolean;
487
- communitySpacing?: number;
488
- }
489
- interface GraphTransitionState {
490
- fromMode?: GraphSourceMode;
491
- toMode: GraphSourceMode;
492
- reason: 'mode-change' | 'embedding-set-change' | 'community-source-change' | 'filter-change' | 'recompute';
493
- startedAt: string;
494
- }
495
- interface GraphControllerStatus {
496
- loading: boolean;
497
- error: Error | null;
498
- freshness: 'fresh' | 'stale' | 'unknown' | null;
499
- cache: SimilarityGraphResult['cache'] | null;
500
- }
501
- type GraphSourceRef = SimilarityGraphResult['graphSource'] | {
502
- id: string;
503
- name: string;
504
- };
505
- /** The full, framework-agnostic controller state surfaced to subscribers. */
506
- interface GraphControllerState {
507
- mode: GraphSourceMode;
508
- graph: CommunityGraph$1 | null;
509
- graphSource?: GraphSourceRef;
510
- communitySource?: CommunitySourceDescriptor;
511
- embeddingSetSelector?: EmbeddingSetSelector;
512
- filters?: CommunityFilterDefinition;
513
- layout: GraphLayoutState;
514
- status: GraphControllerStatus;
515
- transition?: GraphTransitionState;
516
- }
517
- interface GraphControllerOptions {
518
- initialMode?: GraphSourceMode;
519
- initialEmbeddingSetSelector?: EmbeddingSetSelector;
520
- initialCommunitySourceId?: string;
521
- initialFilters?: CommunityFilterDefinition;
522
- layout?: Partial<GraphLayoutState>;
523
- }
524
- type GraphControllerListener = (state: GraphControllerState) => void;
525
- /**
526
- * Drives graph-source selection and loading independent of any UI framework.
527
- *
528
- * Construct with pre-built repositories (handy for tests) or via
529
- * {@link GraphController.fromDb}. Subscribe with {@link subscribe} and read the
530
- * current state with {@link getState}; call {@link start} once to trigger the
531
- * initial load. Setters update state and schedule a refresh, mirroring the
532
- * effect-driven behavior of the original React hook.
533
- */
534
- declare class GraphController {
535
- private readonly graphRepo;
536
- private readonly communityRepo;
537
- private readonly listeners;
538
- private communitySourceId;
539
- private state;
540
- /** Build a controller from a database handle (production path). */
541
- static fromDb(db: GraphControllerDb, options?: GraphControllerOptions): GraphController;
542
- constructor(graphRepo: GraphRepository, communityRepo: CommunitiesRepository, options?: GraphControllerOptions);
543
- getState(): GraphControllerState;
544
- subscribe(listener: GraphControllerListener): () => void;
545
- /** Run the initial load. Call once after construction (the hook calls this on mount). */
546
- start(): Promise<void>;
547
- private setState;
548
- private beginTransition;
549
- refresh(): Promise<void>;
550
- setMode(nextMode: GraphSourceMode): void;
551
- setEmbeddingSetSelector(selector: EmbeddingSetSelector): void;
552
- setCommunitySource(sourceId: string | null): void;
553
- /** Apply dynamic-search filters and switch into that mode (state only). */
554
- private applyFilters;
555
- setFilters(nextFilters: CommunityFilterDefinition): void;
556
- recompute(): Promise<void>;
557
- previewDynamicCommunity(nextFilters: CommunityFilterDefinition): Promise<void>;
558
- saveCurrentCommunity(input: CommunityCreateInput): Promise<CommunitySourceDescriptor>;
559
- }
560
-
561
- declare const VERSION = "2026.7.4";
418
+ declare const VERSION = "2026.7.7";
562
419
 
563
- export { type BakeRenderGraphOptions, COMMUNITY_COLORS, type CommunityGraph, type CommunityPalette, type ExpandOptions, type FitOptions, GRAPH_SNAPSHOT_VERSION, GREYSCALE_COMMUNITY_RAMP, type GraphBounds, type GraphCommunity, type GraphControlContract, type GraphControlFilters, GraphController, type GraphControllerDb, type GraphControllerListener, type GraphControllerOptions, type GraphControllerState, type GraphControllerStatus, type GraphEdge, type GraphFilter, type GraphLayoutAlgorithm, type GraphLayoutState, type GraphLegendEntry, type GraphNode, type GraphRenderFilters, type GraphRenderHandle, type GraphRenderOptions, type GraphRenderUpdate, type GraphSnapshot, type GraphSourceMode, type GraphSourceRef, type GraphTransitionState, type LayoutOptions, type LoadRenderSnapshotOptions, type MapCommunityGraphOptions, type NodeRadiusOptions, type NodeRadiusResolver, type PositionMap, type PositionedCommunity, type PositionedGraph, type PositionedGraphNode, type RenderGraph, type RenderLink, type RenderNode, type SerializeSnapshotOptions, UNASSIGNED_COMMUNITY_COLOR, VERSION, type ViewportTransform, applyControlFilters, bakeRenderGraph, buildAdjacency, colorForCommunity, communityLegend, communityRanks, computeDegrees, computeGraphBounds, deserializeGraphSnapshot, expandNeighborhood, filterCommunityGraph, fitGraphToViewport, hasBakedPositions, isRenderGraph, layoutCommunityGraph, loadRenderSnapshot, mapCommunityGraph, neighborhoodSubgraph, neighborsOf, nodeRadius, renderCommunityGraph, serializeGraphSnapshot, stringifyGraphSnapshot, stringifyRenderGraph, subgraphForNodes };
420
+ export { type BakeRenderGraphOptions, COMMUNITY_COLORS, CommunityGraph, type CommunityPalette, type ExpandOptions, type FitOptions, GRAPH_SNAPSHOT_VERSION, GREYSCALE_COMMUNITY_RAMP, GraphBounds, type GraphControlContract, type GraphControlFilters, type GraphFilter, GraphLayoutAlgorithm, type GraphLegendEntry, GraphNode, type GraphRenderFilters, type GraphRenderHandle, type GraphRenderOptions, type GraphRenderUpdate, type GraphSnapshot, type LayoutOptions, type LoadRenderSnapshotOptions, type MapCommunityGraphOptions, type NodeRadiusOptions, type NodeRadiusResolver, type PositionMap, PositionedGraph, PositionedGraphNode, type RenderGraph, type RenderLink, type RenderNode, type SerializeSnapshotOptions, UNASSIGNED_COMMUNITY_COLOR, VERSION, ViewportTransform, applyControlFilters, bakeRenderGraph, buildAdjacency, colorForCommunity, communityLegend, communityRanks, computeDegrees, computeGraphBounds, deserializeGraphSnapshot, expandNeighborhood, filterCommunityGraph, fitGraphToViewport, hasBakedPositions, isRenderGraph, layoutCommunityGraph, loadRenderSnapshot, mapCommunityGraph, neighborhoodSubgraph, neighborsOf, nodeRadius, renderCommunityGraph, serializeGraphSnapshot, stringifyGraphSnapshot, stringifyRenderGraph, subgraphForNodes };