@fortemi/graph 2026.6.2 → 2026.6.3

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
@@ -1,26 +1,101 @@
1
+ <div align="center">
2
+
1
3
  # @fortemi/graph
2
4
 
3
- Framework-agnostic graph tooling for fortemi. Pure TypeScript helpers that turn
4
- a `CommunityGraph` into something you can render — layout, filtering, coloring,
5
- degree-based sizing, bounds/fit, neighborhood expansion, and static snapshot
6
- serialization. No React, no database, no runtime dependencies.
5
+ **Framework-agnostic graph projection helpers for rendering Fortemi community graphs anywhere**
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.
7
8
 
8
9
  ```bash
9
10
  pnpm add @fortemi/graph
10
11
  ```
11
12
 
12
- This package is an **add-on**, not a base layer. `@fortemi/core` remains the
13
- foundation and owns graph *production* (building similarity/link graphs from the
14
- PGlite store, detecting communities). `@fortemi/graph` sits on top and handles
15
- graph *projection* — the layout and rendering logic that used to live inside the
16
- React `GraphView`. Because it has zero dependencies and operates on plain
17
- `CommunityGraph` data, it can be mixed into:
13
+ [![npm version](https://img.shields.io/npm/v/@fortemi/graph/latest?label=npm&color=CB3837&logo=npm&style=flat-square)](https://www.npmjs.com/package/@fortemi/graph)
14
+ [![npm downloads](https://img.shields.io/npm/dm/@fortemi/graph?color=CB3837&logo=npm&style=flat-square)](https://www.npmjs.com/package/@fortemi/graph)
15
+ [![License: AGPL-3.0-only](https://img.shields.io/badge/License-AGPL--3.0--only-blue.svg?style=flat-square)](https://github.com/Fortemi/fortemi-react/blob/main/LICENSE)
16
+ [![Node Version](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen?style=flat-square&logo=node.js)](https://nodejs.org)
17
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue?style=flat-square&logo=typescript)](https://www.typescriptlang.org)
18
+ [![Built with aiwg](https://img.shields.io/npm/v/aiwg?label=built%20with%20aiwg&color=CB3837&logo=npm&style=flat-square)](https://www.npmjs.com/package/aiwg)
19
+
20
+ [**Install**](#installation) · [**Why Fortemi**](#why-fortemi-graph) · [**Quick Start**](#quick-start) · [**API**](#api) · [**Data Model**](#data-model) · [**Docs**](#documentation) · [**License**](#license)
21
+
22
+ </div>
23
+
24
+ ---
25
+
26
+ ## What @fortemi/graph Is
27
+
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
+
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.
31
+
32
+ ## Why Fortemi Graph
33
+
34
+ Rendering a knowledge graph usually means re-implementing layout math, community coloring, degree sizing, and viewport fitting in every host. Fortemi centralizes that logic in one package, so a React app and a static site stay visually aligned and share the exact same deterministic projection.
35
+
36
+ | Need | What Fortemi Graph provides |
37
+ |---|---|
38
+ | Deterministic layout | `layoutCommunityGraph` — closed-form `force`/`radial`/`community`/`manual` positions, no randomness |
39
+ | Visibility control | `filterCommunityGraph` — filter by community, edge kind, node allow-list, or predicate |
40
+ | Readable encoding | Degree-based node sizing and deterministic community color assignment |
41
+ | Viewport math | Bounding box plus a centered fit transform for SVG/canvas |
42
+ | Interaction | Neighborhood expansion and induced-subgraph helpers for selection |
43
+ | Portable views | Stable snapshot serialization for static, precomputed graphs |
44
+ | Zero lock-in | Pure TypeScript, no React, structurally compatible with `@fortemi/core` data |
45
+
46
+ ### What You Can Build
47
+
48
+ - Static documentation relationship maps rendered from a precomputed snapshot
49
+ - Custom React, Svelte, or Vue graph views that share Fortemi's projection logic
50
+ - Server- or build-time SVG generation of AIWG relationship graphs
51
+ - Selection and "expand neighborhood" interactions over a community graph
52
+ - Themed community visualizations using a custom color palette
53
+
54
+ ### Architecture at a Glance
55
+
56
+ `@fortemi/core` produces a `CommunityGraph` and detects communities. `@fortemi/graph` consumes that shape; it depends on `@fortemi/core` but never on React, and `@fortemi/core` never depends on it. `filterCommunityGraph` narrows what is shown, `layoutCommunityGraph` assigns deterministic coordinates, and the color/degree/bounds helpers turn the positioned graph into draw calls. `serializeGraphSnapshot` freezes a graph to portable JSON for hosts that render without recomputing it.
57
+
58
+ ## Installation
59
+
60
+ ```bash
61
+ pnpm add @fortemi/graph
62
+ # or
63
+ npm install @fortemi/graph
64
+ ```
65
+
66
+ No peer dependencies. Ships ESM with type declarations and works in browsers, Node, and bundlers.
67
+
68
+ ## Quick Start
18
69
 
19
- - `@fortemi/react` — `GraphView` uses these helpers internally.
20
- - a JS-only host (e.g. a static documentation site) that wants to render an AIWG
21
- relationship map without pulling in React or the PGlite core.
70
+ ```ts
71
+ import {
72
+ filterCommunityGraph,
73
+ layoutCommunityGraph,
74
+ colorForCommunity,
75
+ nodeRadius,
76
+ type CommunityGraph,
77
+ } from '@fortemi/graph'
78
+
79
+ const graph: CommunityGraph = {
80
+ nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
81
+ edges: [{ source: 'a', target: 'b', weight: 1, kind: 'similarity' }],
82
+ communities: [{ id: 'c1', nodes: ['a', 'b'] }],
83
+ }
22
84
 
23
- ## Data model
85
+ const visible = filterCommunityGraph(graph, { edgeKinds: ['similarity'] })
86
+ const { nodes, nodeIndex } = layoutCommunityGraph(visible, {
87
+ algorithm: 'community',
88
+ width: 800,
89
+ height: 600,
90
+ })
91
+
92
+ for (const node of nodes) {
93
+ // deterministic coordinates, degree-based radius, community color
94
+ console.log(node.id, node.x, node.y, nodeRadius(node.degree), colorForCommunity(node.communityId))
95
+ }
96
+ ```
97
+
98
+ ## Data Model
24
99
 
25
100
  ```ts
26
101
  interface GraphNode { id: string }
@@ -33,30 +108,32 @@ interface CommunityGraph {
33
108
  }
34
109
  ```
35
110
 
36
- These shapes are structurally identical to the ones `@fortemi/core` produces
37
- (`GraphRepository.buildLinkGraph()`, `buildSimilarityGraph()`, and
38
- `aiwgFortemiIndexToCommunityGraph()`), so a core graph drops straight into these
39
- helpers without conversion.
111
+ These shapes are structurally identical to the ones `@fortemi/core` produces (`GraphRepository.buildLinkGraph()`, `buildSimilarityGraph()`, and `aiwgFortemiIndexToCommunityGraph()`), so a core graph drops straight into these helpers without conversion.
40
112
 
41
113
  ## API
42
114
 
43
115
  | Helper | Purpose |
44
116
  |---|---|
45
- | `layoutCommunityGraph(graph, opts?)` | Deterministic 2D positions (`force`/`radial`/`community`/`manual`) + per-node degree and community |
117
+ | `layoutCommunityGraph(graph, opts?)` | Deterministic 2D positions (`force`/`radial`/`community`/`manual`) plus per-node degree and community |
46
118
  | `filterCommunityGraph(graph, filter?)` | Filter by community, edge kind, node allow-list, or predicate; drops emptied communities |
47
119
  | `computeDegrees(graph)` / `nodeRadius(degree, opts?)` | Undirected degree map and degree → render radius |
48
120
  | `colorForCommunity(id, palette?)` | Deterministic community → color (themeable palette) |
49
121
  | `computeGraphBounds(nodes)` / `fitGraphToViewport(bounds, viewport, opts?)` | Bounding box and a centered fit transform |
50
- | `neighborsOf` / `expandNeighborhood` / `subgraphForNodes` / `neighborhoodSubgraph` | Selection and BFS neighborhood expansion |
122
+ | `neighborsOf` / `expandNeighborhood` / `subgraphForNodes` / `neighborhoodSubgraph` / `buildAdjacency` | Selection and BFS neighborhood expansion |
51
123
  | `serializeGraphSnapshot` / `stringifyGraphSnapshot` / `deserializeGraphSnapshot` | Stable, reproducible static snapshots for JS-only hosts |
52
124
 
53
- All helpers are pure: they never mutate their inputs and (except for an optional
54
- snapshot timestamp) produce identical output for identical input.
125
+ All helpers are pure: they never mutate their inputs and (except for an optional snapshot timestamp) produce identical output for identical input.
55
126
 
56
- ## Produce a snapshot from `@fortemi/core` (build/server side)
127
+ ## What You Get
57
128
 
58
- The core package builds the graph; the graph package serializes it to a static
59
- file. Core does not depend on graph you compose them at the call site:
129
+ - Deterministic, dependency-free graph projection that renders identically across hosts
130
+ - One source of truth for layout, filtering, coloring, and sizing shared with `@fortemi/react`
131
+ - Portable snapshots so a static site can render an AIWG graph without React or PGlite
132
+ - Full TypeScript types, re-exported `CommunityGraph` model, ESM output
133
+
134
+ ## Produce a Snapshot from @fortemi/core
135
+
136
+ The core package builds the graph; the graph package serializes it to a static file. Core does not depend on graph — you compose them at the call site:
60
137
 
61
138
  ```ts
62
139
  import { GraphRepository } from '@fortemi/core'
@@ -70,10 +147,9 @@ writeFileSync(
70
147
  )
71
148
  ```
72
149
 
73
- ## Render in a vanilla JS host (no React)
150
+ ## Render in a Vanilla JS Host (no React)
74
151
 
75
- A static documentation site can fetch that snapshot and render its own SVG using
76
- only the projection helpers — no React, no PGlite:
152
+ A static documentation site can fetch that snapshot and render its own SVG using only the projection helpers — no React, no PGlite:
77
153
 
78
154
  ```js
79
155
  import {
@@ -127,17 +203,24 @@ document.getElementById('graph').innerHTML = `
127
203
  </svg>`
128
204
  ```
129
205
 
130
- The same helpers back `@fortemi/react`'s `GraphView`, so a React app and a
131
- static site stay visually aligned.
206
+ The same helpers back `@fortemi/react`'s `GraphView`, so a React app and a static site stay visually aligned.
132
207
 
133
208
  ## Determinism
134
209
 
135
- `layoutCommunityGraph`, `colorForCommunity`, `filterCommunityGraph`, and
136
- `serializeGraphSnapshot` are deterministic functions of their inputs. That makes
137
- them safe for build-time snapshot generation, content-addressable caching, and
138
- stable visual diffs. `serializeGraphSnapshot` sorts nodes, edges, and community
139
- members so equal graphs serialize byte-for-byte identically; pass `generatedAt`
140
- only when you want a timestamp in the output.
210
+ `layoutCommunityGraph`, `colorForCommunity`, `filterCommunityGraph`, and `serializeGraphSnapshot` are deterministic functions of their inputs. That makes them safe for build-time snapshot generation, content-addressable caching, and stable visual diffs. `serializeGraphSnapshot` sorts nodes, edges, and community members so equal graphs serialize byte-for-byte identically; pass `generatedAt` only when you want a timestamp in the output.
211
+
212
+ Community *detection* lives in `@fortemi/core` (the base layer); this package only projects graphs it is given.
213
+
214
+ ## Documentation
215
+
216
+ | Guide | Description |
217
+ |---|---|
218
+ | [API Reference](https://github.com/Fortemi/fortemi-react/blob/main/docs/api-reference.md) | Full `@fortemi/graph`, `@fortemi/core`, and `@fortemi/react` surface |
219
+ | [Package Architecture](https://github.com/Fortemi/fortemi-react/blob/main/docs/architecture/package-architecture.md) | Diagram and capability tables for each package and how they layer |
220
+
221
+ ## React Bindings
222
+
223
+ Using React? [`@fortemi/react`](https://www.npmjs.com/package/@fortemi/react) ships a `GraphView` component built on these helpers, plus hooks for building and caching graphs from a live archive.
141
224
 
142
225
  ## License
143
226
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { QueryExecutor, DatabaseClient, EmbeddingSetSelector, CommunityFilterDefinition, GraphRepository, CommunitiesRepository, CommunityGraph as CommunityGraph$1, SimilarityGraphResult, CommunitySourceDescriptor, CommunityCreateInput } from '@fortemi/core';
2
+
1
3
  interface GraphNode {
2
4
  id: string;
3
5
  }
@@ -216,6 +218,87 @@ declare function stringifyGraphSnapshot(graph: CommunityGraph, options?: Seriali
216
218
  */
217
219
  declare function deserializeGraphSnapshot(input: GraphSnapshot | string): CommunityGraph;
218
220
 
221
+ /** A database handle accepted by the underlying repositories. */
222
+ type GraphControllerDb = QueryExecutor & DatabaseClient;
223
+ type GraphSourceMode = 'citations' | 'topics' | 'precomputed' | 'dynamic-search' | 'user-authored';
224
+ interface GraphLayoutState {
225
+ algorithm: GraphLayoutAlgorithm;
226
+ pinSelectedNodes?: boolean;
227
+ preserveViewport?: boolean;
228
+ communitySpacing?: number;
229
+ }
230
+ interface GraphTransitionState {
231
+ fromMode?: GraphSourceMode;
232
+ toMode: GraphSourceMode;
233
+ reason: 'mode-change' | 'embedding-set-change' | 'community-source-change' | 'filter-change' | 'recompute';
234
+ startedAt: string;
235
+ }
236
+ interface GraphControllerStatus {
237
+ loading: boolean;
238
+ error: Error | null;
239
+ freshness: 'fresh' | 'stale' | 'unknown' | null;
240
+ cache: SimilarityGraphResult['cache'] | null;
241
+ }
242
+ type GraphSourceRef = SimilarityGraphResult['graphSource'] | {
243
+ id: string;
244
+ name: string;
245
+ };
246
+ /** The full, framework-agnostic controller state surfaced to subscribers. */
247
+ interface GraphControllerState {
248
+ mode: GraphSourceMode;
249
+ graph: CommunityGraph$1 | null;
250
+ graphSource?: GraphSourceRef;
251
+ communitySource?: CommunitySourceDescriptor;
252
+ embeddingSetSelector?: EmbeddingSetSelector;
253
+ filters?: CommunityFilterDefinition;
254
+ layout: GraphLayoutState;
255
+ status: GraphControllerStatus;
256
+ transition?: GraphTransitionState;
257
+ }
258
+ interface GraphControllerOptions {
259
+ initialMode?: GraphSourceMode;
260
+ initialEmbeddingSetSelector?: EmbeddingSetSelector;
261
+ initialCommunitySourceId?: string;
262
+ initialFilters?: CommunityFilterDefinition;
263
+ layout?: Partial<GraphLayoutState>;
264
+ }
265
+ type GraphControllerListener = (state: GraphControllerState) => void;
266
+ /**
267
+ * Drives graph-source selection and loading independent of any UI framework.
268
+ *
269
+ * Construct with pre-built repositories (handy for tests) or via
270
+ * {@link GraphController.fromDb}. Subscribe with {@link subscribe} and read the
271
+ * current state with {@link getState}; call {@link start} once to trigger the
272
+ * initial load. Setters update state and schedule a refresh, mirroring the
273
+ * effect-driven behavior of the original React hook.
274
+ */
275
+ declare class GraphController {
276
+ private readonly graphRepo;
277
+ private readonly communityRepo;
278
+ private readonly listeners;
279
+ private communitySourceId;
280
+ private state;
281
+ /** Build a controller from a database handle (production path). */
282
+ static fromDb(db: GraphControllerDb, options?: GraphControllerOptions): GraphController;
283
+ constructor(graphRepo: GraphRepository, communityRepo: CommunitiesRepository, options?: GraphControllerOptions);
284
+ getState(): GraphControllerState;
285
+ subscribe(listener: GraphControllerListener): () => void;
286
+ /** Run the initial load. Call once after construction (the hook calls this on mount). */
287
+ start(): Promise<void>;
288
+ private setState;
289
+ private beginTransition;
290
+ refresh(): Promise<void>;
291
+ setMode(nextMode: GraphSourceMode): void;
292
+ setEmbeddingSetSelector(selector: EmbeddingSetSelector): void;
293
+ setCommunitySource(sourceId: string | null): void;
294
+ /** Apply dynamic-search filters and switch into that mode (state only). */
295
+ private applyFilters;
296
+ setFilters(nextFilters: CommunityFilterDefinition): void;
297
+ recompute(): Promise<void>;
298
+ previewDynamicCommunity(nextFilters: CommunityFilterDefinition): Promise<void>;
299
+ saveCurrentCommunity(input: CommunityCreateInput): Promise<CommunitySourceDescriptor>;
300
+ }
301
+
219
302
  declare const VERSION = "2026.6.2";
220
303
 
221
- export { COMMUNITY_COLORS, type CommunityGraph, type ExpandOptions, type FitOptions, GRAPH_SNAPSHOT_VERSION, type GraphBounds, type GraphCommunity, type GraphEdge, type GraphFilter, type GraphLayoutAlgorithm, type GraphNode, type GraphSnapshot, type LayoutOptions, type NodeRadiusOptions, type PositionedGraph, type PositionedGraphNode, type SerializeSnapshotOptions, UNASSIGNED_COMMUNITY_COLOR, VERSION, type ViewportTransform, buildAdjacency, colorForCommunity, computeDegrees, computeGraphBounds, deserializeGraphSnapshot, expandNeighborhood, filterCommunityGraph, fitGraphToViewport, layoutCommunityGraph, neighborhoodSubgraph, neighborsOf, nodeRadius, serializeGraphSnapshot, stringifyGraphSnapshot, subgraphForNodes };
304
+ export { COMMUNITY_COLORS, type CommunityGraph, type ExpandOptions, type FitOptions, GRAPH_SNAPSHOT_VERSION, type GraphBounds, type GraphCommunity, GraphController, type GraphControllerDb, type GraphControllerListener, type GraphControllerOptions, type GraphControllerState, type GraphControllerStatus, type GraphEdge, type GraphFilter, type GraphLayoutAlgorithm, type GraphLayoutState, type GraphNode, type GraphSnapshot, type GraphSourceMode, type GraphSourceRef, type GraphTransitionState, type LayoutOptions, type NodeRadiusOptions, type PositionedGraph, type PositionedGraphNode, type SerializeSnapshotOptions, UNASSIGNED_COMMUNITY_COLOR, VERSION, type ViewportTransform, buildAdjacency, colorForCommunity, computeDegrees, computeGraphBounds, deserializeGraphSnapshot, expandNeighborhood, filterCommunityGraph, fitGraphToViewport, layoutCommunityGraph, neighborhoodSubgraph, neighborsOf, nodeRadius, serializeGraphSnapshot, stringifyGraphSnapshot, subgraphForNodes };
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { GraphRepository, CommunitiesRepository } from '@fortemi/core';
2
+
1
3
  // src/degree.ts
2
4
  function computeDegrees(graph) {
3
5
  const degree = /* @__PURE__ */ new Map();
@@ -251,10 +253,187 @@ function deserializeGraphSnapshot(input) {
251
253
  }
252
254
  return graph;
253
255
  }
256
+ var DEFAULT_LAYOUT = {
257
+ algorithm: "force",
258
+ preserveViewport: true,
259
+ communitySpacing: 1
260
+ };
261
+ var GraphController = class _GraphController {
262
+ graphRepo;
263
+ communityRepo;
264
+ listeners = /* @__PURE__ */ new Set();
265
+ communitySourceId;
266
+ state;
267
+ /** Build a controller from a database handle (production path). */
268
+ static fromDb(db, options = {}) {
269
+ return new _GraphController(new GraphRepository(db), new CommunitiesRepository(db), options);
270
+ }
271
+ constructor(graphRepo, communityRepo, options = {}) {
272
+ this.graphRepo = graphRepo;
273
+ this.communityRepo = communityRepo;
274
+ this.communitySourceId = options.initialCommunitySourceId ?? null;
275
+ this.state = {
276
+ mode: options.initialMode ?? "citations",
277
+ graph: null,
278
+ graphSource: void 0,
279
+ communitySource: void 0,
280
+ embeddingSetSelector: options.initialEmbeddingSetSelector,
281
+ filters: options.initialFilters,
282
+ layout: { ...DEFAULT_LAYOUT, ...options.layout },
283
+ status: { loading: false, error: null, freshness: null, cache: null },
284
+ transition: void 0
285
+ };
286
+ }
287
+ getState() {
288
+ return this.state;
289
+ }
290
+ subscribe(listener) {
291
+ this.listeners.add(listener);
292
+ return () => {
293
+ this.listeners.delete(listener);
294
+ };
295
+ }
296
+ /** Run the initial load. Call once after construction (the hook calls this on mount). */
297
+ async start() {
298
+ await this.refresh();
299
+ }
300
+ setState(patch) {
301
+ this.state = { ...this.state, ...patch };
302
+ for (const listener of this.listeners) listener(this.state);
303
+ }
304
+ beginTransition(toMode, reason, fromMode = this.state.mode) {
305
+ this.setState({ transition: { fromMode, toMode, reason, startedAt: (/* @__PURE__ */ new Date()).toISOString() } });
306
+ }
307
+ async refresh() {
308
+ try {
309
+ this.setState({ status: { ...this.state.status, loading: true, error: null } });
310
+ const sources = await this.communityRepo.listCommunitySources();
311
+ const activeCommunity = this.communitySourceId ? sources.find((source) => source.id === this.communitySourceId) : void 0;
312
+ this.setState({ communitySource: activeCommunity });
313
+ const mode = this.state.mode;
314
+ if (mode === "citations") {
315
+ const nextGraph = await this.graphRepo.buildLinkGraph();
316
+ this.setState({
317
+ graph: nextGraph,
318
+ graphSource: { id: "citations", name: "Citation graph" },
319
+ status: { loading: false, error: null, freshness: "fresh", cache: null }
320
+ });
321
+ return;
322
+ }
323
+ if (mode === "topics") {
324
+ const selector = this.state.embeddingSetSelector;
325
+ if (!selector) throw new Error("topics mode requires an embedding-set selector");
326
+ const result = await this.graphRepo.buildOrLoadSimilarityGraph({ selector });
327
+ this.setState({
328
+ graph: result.graph,
329
+ graphSource: result.graphSource,
330
+ status: { loading: false, error: null, freshness: result.freshness, cache: result.cache }
331
+ });
332
+ return;
333
+ }
334
+ if (mode === "precomputed") {
335
+ if (!activeCommunity?.graphSourceId) {
336
+ throw new Error("precomputed mode requires a community source with graphSourceId");
337
+ }
338
+ const assignments = await this.communityRepo.getCommunityAssignments(activeCommunity.id);
339
+ const nextGraph = await this.graphRepo.loadGraphArtifact(
340
+ activeCommunity.graphSourceId,
341
+ assignments.map((assignment) => assignment.noteId)
342
+ );
343
+ this.setState({
344
+ graph: nextGraph,
345
+ graphSource: { id: activeCommunity.graphSourceId, name: activeCommunity.name },
346
+ status: { loading: false, error: null, freshness: activeCommunity.freshness ?? "unknown", cache: null }
347
+ });
348
+ return;
349
+ }
350
+ if (mode === "dynamic-search") {
351
+ const assignments = await this.communityRepo.previewDynamicCommunity(this.state.filters ?? {});
352
+ this.setState({
353
+ graph: {
354
+ nodes: assignments.map((assignment) => ({ id: assignment.noteId })),
355
+ edges: [],
356
+ communities: [{ id: "dynamic-preview", nodes: assignments.map((assignment) => assignment.noteId) }]
357
+ },
358
+ graphSource: { id: "dynamic-preview", name: "Dynamic preview" },
359
+ status: { loading: false, error: null, freshness: "unknown", cache: null }
360
+ });
361
+ return;
362
+ }
363
+ if (mode === "user-authored") {
364
+ if (!activeCommunity) throw new Error("user-authored mode requires an active community source");
365
+ const assignments = await this.communityRepo.getCommunityAssignments(activeCommunity.id);
366
+ this.setState({
367
+ graph: {
368
+ nodes: assignments.map((assignment) => ({ id: assignment.noteId })),
369
+ edges: [],
370
+ communities: [{ id: activeCommunity.id, nodes: assignments.map((assignment) => assignment.noteId) }]
371
+ },
372
+ graphSource: { id: activeCommunity.graphSourceId ?? activeCommunity.id, name: activeCommunity.name },
373
+ status: { loading: false, error: null, freshness: activeCommunity.freshness ?? "unknown", cache: null }
374
+ });
375
+ }
376
+ } catch (err) {
377
+ const e = err instanceof Error ? err : new Error(String(err));
378
+ this.setState({ status: { ...this.state.status, loading: false, error: e } });
379
+ throw e;
380
+ }
381
+ }
382
+ setMode(nextMode) {
383
+ this.beginTransition(nextMode, "mode-change");
384
+ this.setState({ mode: nextMode });
385
+ void this.refresh();
386
+ }
387
+ setEmbeddingSetSelector(selector) {
388
+ this.beginTransition("topics", "embedding-set-change");
389
+ this.setState({ embeddingSetSelector: selector, mode: "topics" });
390
+ void this.refresh();
391
+ }
392
+ setCommunitySource(sourceId) {
393
+ this.communitySourceId = sourceId;
394
+ this.beginTransition(this.state.mode, "community-source-change");
395
+ void this.refresh();
396
+ }
397
+ /** Apply dynamic-search filters and switch into that mode (state only). */
398
+ applyFilters(nextFilters) {
399
+ this.beginTransition("dynamic-search", "filter-change");
400
+ this.setState({ filters: nextFilters, mode: "dynamic-search" });
401
+ }
402
+ setFilters(nextFilters) {
403
+ this.applyFilters(nextFilters);
404
+ void this.refresh();
405
+ }
406
+ async recompute() {
407
+ this.beginTransition(this.state.mode, "recompute");
408
+ if (this.state.mode === "topics" && this.state.embeddingSetSelector) {
409
+ const result = await this.graphRepo.buildOrLoadSimilarityGraph({
410
+ selector: this.state.embeddingSetSelector,
411
+ source: "live-only"
412
+ });
413
+ this.setState({
414
+ graph: result.graph,
415
+ graphSource: result.graphSource,
416
+ status: { loading: false, error: null, freshness: result.freshness, cache: result.cache }
417
+ });
418
+ return;
419
+ }
420
+ await this.refresh();
421
+ }
422
+ async previewDynamicCommunity(nextFilters) {
423
+ this.applyFilters(nextFilters);
424
+ await this.refresh();
425
+ }
426
+ async saveCurrentCommunity(input) {
427
+ const source = await this.communityRepo.saveCommunity(input);
428
+ this.communitySourceId = source.id;
429
+ this.setState({ communitySource: source });
430
+ return source;
431
+ }
432
+ };
254
433
 
255
434
  // src/index.ts
256
435
  var VERSION = "2026.6.2";
257
436
 
258
- export { COMMUNITY_COLORS, GRAPH_SNAPSHOT_VERSION, UNASSIGNED_COMMUNITY_COLOR, VERSION, buildAdjacency, colorForCommunity, computeDegrees, computeGraphBounds, deserializeGraphSnapshot, expandNeighborhood, filterCommunityGraph, fitGraphToViewport, layoutCommunityGraph, neighborhoodSubgraph, neighborsOf, nodeRadius, serializeGraphSnapshot, stringifyGraphSnapshot, subgraphForNodes };
437
+ export { COMMUNITY_COLORS, GRAPH_SNAPSHOT_VERSION, GraphController, UNASSIGNED_COMMUNITY_COLOR, VERSION, buildAdjacency, colorForCommunity, computeDegrees, computeGraphBounds, deserializeGraphSnapshot, expandNeighborhood, filterCommunityGraph, fitGraphToViewport, layoutCommunityGraph, neighborhoodSubgraph, neighborsOf, nodeRadius, serializeGraphSnapshot, stringifyGraphSnapshot, subgraphForNodes };
259
438
  //# sourceMappingURL=index.js.map
260
439
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/degree.ts","../src/color.ts","../src/filter.ts","../src/layout.ts","../src/bounds.ts","../src/neighborhood.ts","../src/serialize.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { CommunityGraph } from './types.js'\n\n/** Count the (undirected) degree of every node, including isolated nodes (0). */\nexport function computeDegrees(graph: CommunityGraph): Map<string, number> {\n const degree = new Map<string, number>()\n for (const node of graph.nodes) degree.set(node.id, 0)\n for (const edge of graph.edges) {\n degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1)\n degree.set(edge.target, (degree.get(edge.target) ?? 0) + 1)\n }\n return degree\n}\n\nexport interface NodeRadiusOptions {\n /** Radius of a degree-0 node. */\n base?: number\n /** Pixels added per unit of degree. */\n perDegree?: number\n /** Hard floor on the radius. */\n min?: number\n /** Hard ceiling on the radius. */\n max?: number\n}\n\n/**\n * Map a node degree to a render radius. Defaults match the values historically\n * used by the React `GraphView`: `clamp(5 + degree * 1.5, 5, 16)`.\n */\nexport function nodeRadius(degree: number, options: NodeRadiusOptions = {}): number {\n const base = options.base ?? 5\n const perDegree = options.perDegree ?? 1.5\n const min = options.min ?? 5\n const max = options.max ?? 16\n return Math.max(min, Math.min(max, base + degree * perDegree))\n}\n","/** Default qualitative palette for community coloring (8 hues). */\nexport const COMMUNITY_COLORS = [\n '#2f6fbb',\n '#d97706',\n '#218838',\n '#7c3aed',\n '#c2410c',\n '#0f766e',\n '#be185d',\n '#4b5563',\n] as const\n\n/** Color used for nodes that are not assigned to any community. */\nexport const UNASSIGNED_COMMUNITY_COLOR = '#64748b'\n\n/**\n * Deterministically assign a color to a community id by hashing the id into the\n * palette. The same id always maps to the same color across runs and hosts.\n * Pass a custom `palette` to theme the output. Matches the historical\n * `GraphView` color assignment.\n */\nexport function colorForCommunity(\n communityId: string | undefined,\n palette: readonly string[] = COMMUNITY_COLORS,\n unassignedColor: string = UNASSIGNED_COMMUNITY_COLOR,\n): string {\n if (!communityId) return unassignedColor\n if (palette.length === 0) return unassignedColor\n let hash = 0\n for (const char of communityId) hash = (hash * 31 + char.charCodeAt(0)) | 0\n return palette[Math.abs(hash) % palette.length]\n}\n","import type { CommunityGraph, GraphNode } from './types.js'\n\nexport interface GraphFilter {\n /** Keep only nodes belonging to these communities (by community id). */\n communityIds?: string[]\n /** Keep only edges whose `kind` is in this set. */\n edgeKinds?: string[]\n /** Explicit allow-list of node ids. Useful for privacy / visibility filtering. */\n nodeIds?: string[]\n /**\n * Arbitrary per-node predicate evaluated against the (allow-listed) nodes.\n * Returning `false` removes the node and any edge touching it. Hosts whose\n * nodes carry extra metadata (privacy class, type, etc.) can filter on it here.\n */\n nodePredicate?: (node: GraphNode) => boolean\n}\n\n/**\n * Produce a new {@link CommunityGraph} containing only the nodes, edges, and\n * community memberships permitted by `filter`. Pure — the input is not mutated.\n *\n * Filtering precedence:\n * 1. `communityIds` (if non-empty) restricts the candidate node set to members\n * of those communities; otherwise `nodeIds` (if given) is the candidate set,\n * else all nodes.\n * 2. `nodePredicate` further narrows the candidate set.\n * 3. Edges survive only when both endpoints survive and `edgeKinds` permits.\n * 4. Empty communities are dropped.\n */\nexport function filterCommunityGraph(\n graph: CommunityGraph | null | undefined,\n filter?: GraphFilter,\n): CommunityGraph {\n if (!graph) return { nodes: [], edges: [], communities: [] }\n\n const nodeIds = new Set(filter?.nodeIds ?? graph.nodes.map((node) => node.id))\n if (filter?.communityIds?.length) {\n const allowedCommunities = new Set(filter.communityIds)\n nodeIds.clear()\n for (const community of graph.communities) {\n if (allowedCommunities.has(community.id)) {\n for (const nodeId of community.nodes) nodeIds.add(nodeId)\n }\n }\n }\n\n const predicate = filter?.nodePredicate\n if (predicate) {\n for (const node of graph.nodes) {\n if (nodeIds.has(node.id) && !predicate(node)) nodeIds.delete(node.id)\n }\n }\n\n const edgeKinds = filter?.edgeKinds ? new Set(filter.edgeKinds) : null\n const nodes = graph.nodes.filter((node) => nodeIds.has(node.id))\n const edges = graph.edges.filter((edge) => (\n nodeIds.has(edge.source)\n && nodeIds.has(edge.target)\n && (!edgeKinds || edgeKinds.has(edge.kind ?? ''))\n ))\n const communities = graph.communities\n .map((community) => ({ ...community, nodes: community.nodes.filter((nodeId) => nodeIds.has(nodeId)) }))\n .filter((community) => community.nodes.length > 0)\n return { nodes, edges, communities }\n}\n","import { computeDegrees } from './degree.js'\nimport type {\n CommunityGraph,\n GraphLayoutAlgorithm,\n PositionedGraph,\n PositionedGraphNode,\n} from './types.js'\n\nexport interface LayoutOptions {\n algorithm?: GraphLayoutAlgorithm\n width?: number\n height?: number\n}\n\nconst DEFAULT_WIDTH = 760\nconst DEFAULT_HEIGHT = 460\n\n/**\n * Deterministically position the nodes of a {@link CommunityGraph} in 2D space.\n *\n * The layout is a closed-form function of (graph, algorithm, width, height) with\n * no randomness or iteration, so the same input always yields the same\n * coordinates — safe for snapshot rendering and server-side generation. Each\n * node is also annotated with its degree and resolved community.\n *\n * Algorithms:\n * - `force` radial ring with a mild degree-based jitter (default)\n * - `radial` plain radial ring at the full layout radius\n * - `community` nodes orbit their community's anchor on the ring\n * - `manual` same ring as `radial`; intended as a base for host-driven pinning\n */\nexport function layoutCommunityGraph(\n graph: CommunityGraph,\n options: LayoutOptions = {},\n): PositionedGraph {\n const algorithm = options.algorithm ?? 'force'\n const width = options.width ?? DEFAULT_WIDTH\n const height = options.height ?? DEFAULT_HEIGHT\n\n const degree = computeDegrees(graph)\n\n const communityByNode = new Map<string, string>()\n for (const community of graph.communities) {\n for (const nodeId of community.nodes) {\n if (!communityByNode.has(nodeId)) communityByNode.set(nodeId, community.id)\n }\n }\n\n const centerX = width / 2\n const centerY = height / 2\n const radius = Math.max(40, Math.min(width, height) * 0.38)\n\n const nodes = graph.nodes.map((node, index): PositionedGraphNode => {\n const angle = (Math.PI * 2 * index) / Math.max(1, graph.nodes.length)\n const communityIndex = graph.communities.findIndex(\n (community) => community.id === communityByNode.get(node.id),\n )\n const communityAngle = (Math.PI * 2 * Math.max(0, communityIndex)) / Math.max(1, graph.communities.length)\n const communityRadius = algorithm === 'community' ? radius * 0.55 : radius\n const localRadius = algorithm === 'force'\n ? radius * (0.7 + ((degree.get(node.id) ?? 0) % 4) * 0.08)\n : radius\n const x = algorithm === 'community'\n ? centerX + Math.cos(communityAngle) * communityRadius + Math.cos(angle) * 46\n : centerX + Math.cos(angle) * localRadius\n const y = algorithm === 'community'\n ? centerY + Math.sin(communityAngle) * communityRadius + Math.sin(angle) * 46\n : centerY + Math.sin(angle) * (algorithm === 'radial' ? radius : localRadius * 0.72)\n return {\n ...node,\n x,\n y,\n degree: degree.get(node.id) ?? 0,\n communityId: communityByNode.get(node.id),\n }\n })\n\n return {\n nodes,\n edges: graph.edges,\n nodeIndex: new Map(nodes.map((node) => [node.id, node])),\n }\n}\n","import type { GraphBounds, PositionedGraphNode, ViewportTransform } from './types.js'\n\nconst EMPTY_BOUNDS: GraphBounds = {\n minX: 0,\n minY: 0,\n maxX: 0,\n maxY: 0,\n width: 0,\n height: 0,\n centerX: 0,\n centerY: 0,\n}\n\n/**\n * Compute the axis-aligned bounding box around a set of positioned nodes.\n * Returns an all-zero box for an empty input.\n */\nexport function computeGraphBounds(\n nodes: ReadonlyArray<Pick<PositionedGraphNode, 'x' | 'y'>>,\n): GraphBounds {\n if (nodes.length === 0) return { ...EMPTY_BOUNDS }\n let minX = Infinity\n let minY = Infinity\n let maxX = -Infinity\n let maxY = -Infinity\n for (const node of nodes) {\n if (node.x < minX) minX = node.x\n if (node.y < minY) minY = node.y\n if (node.x > maxX) maxX = node.x\n if (node.y > maxY) maxY = node.y\n }\n return {\n minX,\n minY,\n maxX,\n maxY,\n width: maxX - minX,\n height: maxY - minY,\n centerX: (minX + maxX) / 2,\n centerY: (minY + maxY) / 2,\n }\n}\n\nexport interface FitOptions {\n /** Uniform padding (in viewport units) to leave around the graph. */\n padding?: number\n /** Clamp the computed scale to this minimum. */\n minScale?: number\n /** Clamp the computed scale to this maximum. */\n maxScale?: number\n}\n\n/**\n * Compute a {@link ViewportTransform} (uniform scale + translation) that fits\n * `bounds` centered within a `viewport` of the given width/height. Apply the\n * result as `translate(offsetX, offsetY) scale(scale)` in SVG/canvas space.\n */\nexport function fitGraphToViewport(\n bounds: GraphBounds,\n viewport: { width: number; height: number },\n options: FitOptions = {},\n): ViewportTransform {\n const padding = options.padding ?? 0\n const minScale = options.minScale ?? 0\n const maxScale = options.maxScale ?? Infinity\n\n const availableWidth = Math.max(0, viewport.width - padding * 2)\n const availableHeight = Math.max(0, viewport.height - padding * 2)\n\n let scale = 1\n if (bounds.width > 0 || bounds.height > 0) {\n const scaleX = bounds.width > 0 ? availableWidth / bounds.width : Infinity\n const scaleY = bounds.height > 0 ? availableHeight / bounds.height : Infinity\n scale = Math.min(scaleX, scaleY)\n }\n if (!Number.isFinite(scale) || scale <= 0) scale = 1\n scale = Math.max(minScale, Math.min(maxScale, scale))\n\n const offsetX = viewport.width / 2 - bounds.centerX * scale\n const offsetY = viewport.height / 2 - bounds.centerY * scale\n return { scale, offsetX, offsetY }\n}\n","import { filterCommunityGraph } from './filter.js'\nimport type { CommunityGraph } from './types.js'\n\n/** Build an undirected adjacency map from a graph's edges. */\nexport function buildAdjacency(graph: CommunityGraph): Map<string, Set<string>> {\n const adjacency = new Map<string, Set<string>>()\n const ensure = (id: string) => {\n let set = adjacency.get(id)\n if (!set) {\n set = new Set<string>()\n adjacency.set(id, set)\n }\n return set\n }\n for (const node of graph.nodes) ensure(node.id)\n for (const edge of graph.edges) {\n ensure(edge.source).add(edge.target)\n ensure(edge.target).add(edge.source)\n }\n return adjacency\n}\n\n/** Return the set of node ids directly adjacent to `nodeId` (excludes itself). */\nexport function neighborsOf(graph: CommunityGraph, nodeId: string): Set<string> {\n return new Set(buildAdjacency(graph).get(nodeId) ?? [])\n}\n\nexport interface ExpandOptions {\n /** How many edge-hops to traverse from the seeds. Default 1. */\n depth?: number\n /** Include the seed ids in the result. Default true. */\n includeSeeds?: boolean\n}\n\n/**\n * Breadth-first expansion of one or more seed nodes out to `depth` hops.\n * Returns the set of reached node ids (seeds included by default).\n */\nexport function expandNeighborhood(\n graph: CommunityGraph,\n seeds: Iterable<string>,\n options: ExpandOptions = {},\n): Set<string> {\n const depth = options.depth ?? 1\n const includeSeeds = options.includeSeeds ?? true\n const adjacency = buildAdjacency(graph)\n\n const seen = new Set<string>()\n let frontier = new Set<string>()\n for (const seed of seeds) {\n if (adjacency.has(seed) || depth === 0) {\n seen.add(seed)\n frontier.add(seed)\n }\n }\n\n for (let hop = 0; hop < depth; hop++) {\n const next = new Set<string>()\n for (const id of frontier) {\n for (const neighbor of adjacency.get(id) ?? []) {\n if (!seen.has(neighbor)) {\n seen.add(neighbor)\n next.add(neighbor)\n }\n }\n }\n if (next.size === 0) break\n frontier = next\n }\n\n if (!includeSeeds) {\n for (const seed of seeds) seen.delete(seed)\n }\n return seen\n}\n\n/**\n * Extract the induced subgraph over `nodeIds` — a {@link CommunityGraph}\n * containing only those nodes, the edges between them, and the trimmed\n * community memberships. Thin wrapper over {@link filterCommunityGraph}.\n */\nexport function subgraphForNodes(graph: CommunityGraph, nodeIds: Iterable<string>): CommunityGraph {\n return filterCommunityGraph(graph, { nodeIds: Array.from(nodeIds) })\n}\n\n/**\n * Convenience: the induced subgraph of a seed node plus its `depth`-hop\n * neighborhood. Useful for \"expand selection\" interactions.\n */\nexport function neighborhoodSubgraph(\n graph: CommunityGraph,\n seeds: Iterable<string>,\n options: ExpandOptions = {},\n): CommunityGraph {\n return subgraphForNodes(graph, expandNeighborhood(graph, seeds, options))\n}\n","import type { CommunityGraph, GraphLayoutAlgorithm } from './types.js'\n\n/** Current on-disk version for {@link GraphSnapshot}. */\nexport const GRAPH_SNAPSHOT_VERSION = 1\n\n/**\n * A self-contained, serializable representation of a community graph plus the\n * layout intent used to render it. Designed to be written to a static JSON file\n * and consumed by a JS-only host (e.g. a documentation site) without recomputing\n * the graph.\n */\nexport interface GraphSnapshot {\n version: number\n graph: CommunityGraph\n layout?: {\n algorithm: GraphLayoutAlgorithm\n width: number\n height: number\n }\n generatedAt?: string\n}\n\nexport interface SerializeSnapshotOptions {\n layout?: GraphSnapshot['layout']\n /** ISO timestamp to stamp into the snapshot. Omit for reproducible output. */\n generatedAt?: string\n}\n\nfunction sortedGraph(graph: CommunityGraph): CommunityGraph {\n return {\n nodes: [...graph.nodes].sort((a, b) => a.id.localeCompare(b.id)),\n edges: [...graph.edges].sort((a, b) => (\n a.source.localeCompare(b.source)\n || a.target.localeCompare(b.target)\n || (a.kind ?? '').localeCompare(b.kind ?? '')\n )),\n communities: [...graph.communities]\n .map((community) => ({ ...community, nodes: [...community.nodes].sort() }))\n .sort((a, b) => a.id.localeCompare(b.id)),\n }\n}\n\n/**\n * Build a deterministic {@link GraphSnapshot} from a graph. Nodes, edges, and\n * community members are sorted so that equal graphs serialize identically\n * (stable diffs, content-addressable caching).\n */\nexport function serializeGraphSnapshot(\n graph: CommunityGraph,\n options: SerializeSnapshotOptions = {},\n): GraphSnapshot {\n const snapshot: GraphSnapshot = {\n version: GRAPH_SNAPSHOT_VERSION,\n graph: sortedGraph(graph),\n }\n if (options.layout) snapshot.layout = options.layout\n if (options.generatedAt) snapshot.generatedAt = options.generatedAt\n return snapshot\n}\n\n/** Stable JSON string for a graph snapshot. */\nexport function stringifyGraphSnapshot(\n graph: CommunityGraph,\n options: SerializeSnapshotOptions = {},\n): string {\n return JSON.stringify(serializeGraphSnapshot(graph, options))\n}\n\n/**\n * Validate and unwrap a {@link GraphSnapshot} (or its JSON string) back into a\n * {@link CommunityGraph}. Throws on shape/version mismatch.\n */\nexport function deserializeGraphSnapshot(input: GraphSnapshot | string): CommunityGraph {\n const snapshot: unknown = typeof input === 'string' ? JSON.parse(input) : input\n if (!snapshot || typeof snapshot !== 'object') {\n throw new Error('invalid graph snapshot: not an object')\n }\n const candidate = snapshot as Partial<GraphSnapshot>\n if (candidate.version !== GRAPH_SNAPSHOT_VERSION) {\n throw new Error(`unsupported graph snapshot version: ${String(candidate.version)}`)\n }\n const graph = candidate.graph\n if (\n !graph\n || !Array.isArray(graph.nodes)\n || !Array.isArray(graph.edges)\n || !Array.isArray(graph.communities)\n ) {\n throw new Error('invalid graph snapshot: missing graph nodes/edges/communities')\n }\n return graph\n}\n","// @fortemi/graph — framework-agnostic graph presentation/projection helpers.\n//\n// This is a standalone add-on with zero runtime dependencies. It layers on top\n// of any host that produces `CommunityGraph`-shaped data (e.g. @fortemi/core's\n// GraphRepository / AIWG index export) and gives React and JS-only hosts the\n// shared projection, layout, filtering, coloring, sizing, bounds, neighborhood,\n// and snapshot logic needed to render their own SVG/canvas views.\n//\n// Community *detection* intentionally lives in @fortemi/core (the base layer);\n// this package only renders/projects graphs it is given.\n\nexport const VERSION = '2026.6.2'\n\nexport type {\n GraphNode,\n GraphEdge,\n GraphCommunity,\n CommunityGraph,\n GraphLayoutAlgorithm,\n PositionedGraphNode,\n PositionedGraph,\n GraphBounds,\n ViewportTransform,\n} from './types.js'\n\nexport { computeDegrees, nodeRadius } from './degree.js'\nexport type { NodeRadiusOptions } from './degree.js'\n\nexport { COMMUNITY_COLORS, UNASSIGNED_COMMUNITY_COLOR, colorForCommunity } from './color.js'\n\nexport { filterCommunityGraph } from './filter.js'\nexport type { GraphFilter } from './filter.js'\n\nexport { layoutCommunityGraph } from './layout.js'\nexport type { LayoutOptions } from './layout.js'\n\nexport { computeGraphBounds, fitGraphToViewport } from './bounds.js'\nexport type { FitOptions } from './bounds.js'\n\nexport {\n buildAdjacency,\n neighborsOf,\n expandNeighborhood,\n subgraphForNodes,\n neighborhoodSubgraph,\n} from './neighborhood.js'\nexport type { ExpandOptions } from './neighborhood.js'\n\nexport {\n GRAPH_SNAPSHOT_VERSION,\n serializeGraphSnapshot,\n stringifyGraphSnapshot,\n deserializeGraphSnapshot,\n} from './serialize.js'\nexport type { GraphSnapshot, SerializeSnapshotOptions } from './serialize.js'\n"]}
1
+ {"version":3,"sources":["../src/degree.ts","../src/color.ts","../src/filter.ts","../src/layout.ts","../src/bounds.ts","../src/neighborhood.ts","../src/serialize.ts","../src/controller.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { CommunityGraph } from './types.js'\n\n/** Count the (undirected) degree of every node, including isolated nodes (0). */\nexport function computeDegrees(graph: CommunityGraph): Map<string, number> {\n const degree = new Map<string, number>()\n for (const node of graph.nodes) degree.set(node.id, 0)\n for (const edge of graph.edges) {\n degree.set(edge.source, (degree.get(edge.source) ?? 0) + 1)\n degree.set(edge.target, (degree.get(edge.target) ?? 0) + 1)\n }\n return degree\n}\n\nexport interface NodeRadiusOptions {\n /** Radius of a degree-0 node. */\n base?: number\n /** Pixels added per unit of degree. */\n perDegree?: number\n /** Hard floor on the radius. */\n min?: number\n /** Hard ceiling on the radius. */\n max?: number\n}\n\n/**\n * Map a node degree to a render radius. Defaults match the values historically\n * used by the React `GraphView`: `clamp(5 + degree * 1.5, 5, 16)`.\n */\nexport function nodeRadius(degree: number, options: NodeRadiusOptions = {}): number {\n const base = options.base ?? 5\n const perDegree = options.perDegree ?? 1.5\n const min = options.min ?? 5\n const max = options.max ?? 16\n return Math.max(min, Math.min(max, base + degree * perDegree))\n}\n","/** Default qualitative palette for community coloring (8 hues). */\nexport const COMMUNITY_COLORS = [\n '#2f6fbb',\n '#d97706',\n '#218838',\n '#7c3aed',\n '#c2410c',\n '#0f766e',\n '#be185d',\n '#4b5563',\n] as const\n\n/** Color used for nodes that are not assigned to any community. */\nexport const UNASSIGNED_COMMUNITY_COLOR = '#64748b'\n\n/**\n * Deterministically assign a color to a community id by hashing the id into the\n * palette. The same id always maps to the same color across runs and hosts.\n * Pass a custom `palette` to theme the output. Matches the historical\n * `GraphView` color assignment.\n */\nexport function colorForCommunity(\n communityId: string | undefined,\n palette: readonly string[] = COMMUNITY_COLORS,\n unassignedColor: string = UNASSIGNED_COMMUNITY_COLOR,\n): string {\n if (!communityId) return unassignedColor\n if (palette.length === 0) return unassignedColor\n let hash = 0\n for (const char of communityId) hash = (hash * 31 + char.charCodeAt(0)) | 0\n return palette[Math.abs(hash) % palette.length]\n}\n","import type { CommunityGraph, GraphNode } from './types.js'\n\nexport interface GraphFilter {\n /** Keep only nodes belonging to these communities (by community id). */\n communityIds?: string[]\n /** Keep only edges whose `kind` is in this set. */\n edgeKinds?: string[]\n /** Explicit allow-list of node ids. Useful for privacy / visibility filtering. */\n nodeIds?: string[]\n /**\n * Arbitrary per-node predicate evaluated against the (allow-listed) nodes.\n * Returning `false` removes the node and any edge touching it. Hosts whose\n * nodes carry extra metadata (privacy class, type, etc.) can filter on it here.\n */\n nodePredicate?: (node: GraphNode) => boolean\n}\n\n/**\n * Produce a new {@link CommunityGraph} containing only the nodes, edges, and\n * community memberships permitted by `filter`. Pure — the input is not mutated.\n *\n * Filtering precedence:\n * 1. `communityIds` (if non-empty) restricts the candidate node set to members\n * of those communities; otherwise `nodeIds` (if given) is the candidate set,\n * else all nodes.\n * 2. `nodePredicate` further narrows the candidate set.\n * 3. Edges survive only when both endpoints survive and `edgeKinds` permits.\n * 4. Empty communities are dropped.\n */\nexport function filterCommunityGraph(\n graph: CommunityGraph | null | undefined,\n filter?: GraphFilter,\n): CommunityGraph {\n if (!graph) return { nodes: [], edges: [], communities: [] }\n\n const nodeIds = new Set(filter?.nodeIds ?? graph.nodes.map((node) => node.id))\n if (filter?.communityIds?.length) {\n const allowedCommunities = new Set(filter.communityIds)\n nodeIds.clear()\n for (const community of graph.communities) {\n if (allowedCommunities.has(community.id)) {\n for (const nodeId of community.nodes) nodeIds.add(nodeId)\n }\n }\n }\n\n const predicate = filter?.nodePredicate\n if (predicate) {\n for (const node of graph.nodes) {\n if (nodeIds.has(node.id) && !predicate(node)) nodeIds.delete(node.id)\n }\n }\n\n const edgeKinds = filter?.edgeKinds ? new Set(filter.edgeKinds) : null\n const nodes = graph.nodes.filter((node) => nodeIds.has(node.id))\n const edges = graph.edges.filter((edge) => (\n nodeIds.has(edge.source)\n && nodeIds.has(edge.target)\n && (!edgeKinds || edgeKinds.has(edge.kind ?? ''))\n ))\n const communities = graph.communities\n .map((community) => ({ ...community, nodes: community.nodes.filter((nodeId) => nodeIds.has(nodeId)) }))\n .filter((community) => community.nodes.length > 0)\n return { nodes, edges, communities }\n}\n","import { computeDegrees } from './degree.js'\nimport type {\n CommunityGraph,\n GraphLayoutAlgorithm,\n PositionedGraph,\n PositionedGraphNode,\n} from './types.js'\n\nexport interface LayoutOptions {\n algorithm?: GraphLayoutAlgorithm\n width?: number\n height?: number\n}\n\nconst DEFAULT_WIDTH = 760\nconst DEFAULT_HEIGHT = 460\n\n/**\n * Deterministically position the nodes of a {@link CommunityGraph} in 2D space.\n *\n * The layout is a closed-form function of (graph, algorithm, width, height) with\n * no randomness or iteration, so the same input always yields the same\n * coordinates — safe for snapshot rendering and server-side generation. Each\n * node is also annotated with its degree and resolved community.\n *\n * Algorithms:\n * - `force` radial ring with a mild degree-based jitter (default)\n * - `radial` plain radial ring at the full layout radius\n * - `community` nodes orbit their community's anchor on the ring\n * - `manual` same ring as `radial`; intended as a base for host-driven pinning\n */\nexport function layoutCommunityGraph(\n graph: CommunityGraph,\n options: LayoutOptions = {},\n): PositionedGraph {\n const algorithm = options.algorithm ?? 'force'\n const width = options.width ?? DEFAULT_WIDTH\n const height = options.height ?? DEFAULT_HEIGHT\n\n const degree = computeDegrees(graph)\n\n const communityByNode = new Map<string, string>()\n for (const community of graph.communities) {\n for (const nodeId of community.nodes) {\n if (!communityByNode.has(nodeId)) communityByNode.set(nodeId, community.id)\n }\n }\n\n const centerX = width / 2\n const centerY = height / 2\n const radius = Math.max(40, Math.min(width, height) * 0.38)\n\n const nodes = graph.nodes.map((node, index): PositionedGraphNode => {\n const angle = (Math.PI * 2 * index) / Math.max(1, graph.nodes.length)\n const communityIndex = graph.communities.findIndex(\n (community) => community.id === communityByNode.get(node.id),\n )\n const communityAngle = (Math.PI * 2 * Math.max(0, communityIndex)) / Math.max(1, graph.communities.length)\n const communityRadius = algorithm === 'community' ? radius * 0.55 : radius\n const localRadius = algorithm === 'force'\n ? radius * (0.7 + ((degree.get(node.id) ?? 0) % 4) * 0.08)\n : radius\n const x = algorithm === 'community'\n ? centerX + Math.cos(communityAngle) * communityRadius + Math.cos(angle) * 46\n : centerX + Math.cos(angle) * localRadius\n const y = algorithm === 'community'\n ? centerY + Math.sin(communityAngle) * communityRadius + Math.sin(angle) * 46\n : centerY + Math.sin(angle) * (algorithm === 'radial' ? radius : localRadius * 0.72)\n return {\n ...node,\n x,\n y,\n degree: degree.get(node.id) ?? 0,\n communityId: communityByNode.get(node.id),\n }\n })\n\n return {\n nodes,\n edges: graph.edges,\n nodeIndex: new Map(nodes.map((node) => [node.id, node])),\n }\n}\n","import type { GraphBounds, PositionedGraphNode, ViewportTransform } from './types.js'\n\nconst EMPTY_BOUNDS: GraphBounds = {\n minX: 0,\n minY: 0,\n maxX: 0,\n maxY: 0,\n width: 0,\n height: 0,\n centerX: 0,\n centerY: 0,\n}\n\n/**\n * Compute the axis-aligned bounding box around a set of positioned nodes.\n * Returns an all-zero box for an empty input.\n */\nexport function computeGraphBounds(\n nodes: ReadonlyArray<Pick<PositionedGraphNode, 'x' | 'y'>>,\n): GraphBounds {\n if (nodes.length === 0) return { ...EMPTY_BOUNDS }\n let minX = Infinity\n let minY = Infinity\n let maxX = -Infinity\n let maxY = -Infinity\n for (const node of nodes) {\n if (node.x < minX) minX = node.x\n if (node.y < minY) minY = node.y\n if (node.x > maxX) maxX = node.x\n if (node.y > maxY) maxY = node.y\n }\n return {\n minX,\n minY,\n maxX,\n maxY,\n width: maxX - minX,\n height: maxY - minY,\n centerX: (minX + maxX) / 2,\n centerY: (minY + maxY) / 2,\n }\n}\n\nexport interface FitOptions {\n /** Uniform padding (in viewport units) to leave around the graph. */\n padding?: number\n /** Clamp the computed scale to this minimum. */\n minScale?: number\n /** Clamp the computed scale to this maximum. */\n maxScale?: number\n}\n\n/**\n * Compute a {@link ViewportTransform} (uniform scale + translation) that fits\n * `bounds` centered within a `viewport` of the given width/height. Apply the\n * result as `translate(offsetX, offsetY) scale(scale)` in SVG/canvas space.\n */\nexport function fitGraphToViewport(\n bounds: GraphBounds,\n viewport: { width: number; height: number },\n options: FitOptions = {},\n): ViewportTransform {\n const padding = options.padding ?? 0\n const minScale = options.minScale ?? 0\n const maxScale = options.maxScale ?? Infinity\n\n const availableWidth = Math.max(0, viewport.width - padding * 2)\n const availableHeight = Math.max(0, viewport.height - padding * 2)\n\n let scale = 1\n if (bounds.width > 0 || bounds.height > 0) {\n const scaleX = bounds.width > 0 ? availableWidth / bounds.width : Infinity\n const scaleY = bounds.height > 0 ? availableHeight / bounds.height : Infinity\n scale = Math.min(scaleX, scaleY)\n }\n if (!Number.isFinite(scale) || scale <= 0) scale = 1\n scale = Math.max(minScale, Math.min(maxScale, scale))\n\n const offsetX = viewport.width / 2 - bounds.centerX * scale\n const offsetY = viewport.height / 2 - bounds.centerY * scale\n return { scale, offsetX, offsetY }\n}\n","import { filterCommunityGraph } from './filter.js'\nimport type { CommunityGraph } from './types.js'\n\n/** Build an undirected adjacency map from a graph's edges. */\nexport function buildAdjacency(graph: CommunityGraph): Map<string, Set<string>> {\n const adjacency = new Map<string, Set<string>>()\n const ensure = (id: string) => {\n let set = adjacency.get(id)\n if (!set) {\n set = new Set<string>()\n adjacency.set(id, set)\n }\n return set\n }\n for (const node of graph.nodes) ensure(node.id)\n for (const edge of graph.edges) {\n ensure(edge.source).add(edge.target)\n ensure(edge.target).add(edge.source)\n }\n return adjacency\n}\n\n/** Return the set of node ids directly adjacent to `nodeId` (excludes itself). */\nexport function neighborsOf(graph: CommunityGraph, nodeId: string): Set<string> {\n return new Set(buildAdjacency(graph).get(nodeId) ?? [])\n}\n\nexport interface ExpandOptions {\n /** How many edge-hops to traverse from the seeds. Default 1. */\n depth?: number\n /** Include the seed ids in the result. Default true. */\n includeSeeds?: boolean\n}\n\n/**\n * Breadth-first expansion of one or more seed nodes out to `depth` hops.\n * Returns the set of reached node ids (seeds included by default).\n */\nexport function expandNeighborhood(\n graph: CommunityGraph,\n seeds: Iterable<string>,\n options: ExpandOptions = {},\n): Set<string> {\n const depth = options.depth ?? 1\n const includeSeeds = options.includeSeeds ?? true\n const adjacency = buildAdjacency(graph)\n\n const seen = new Set<string>()\n let frontier = new Set<string>()\n for (const seed of seeds) {\n if (adjacency.has(seed) || depth === 0) {\n seen.add(seed)\n frontier.add(seed)\n }\n }\n\n for (let hop = 0; hop < depth; hop++) {\n const next = new Set<string>()\n for (const id of frontier) {\n for (const neighbor of adjacency.get(id) ?? []) {\n if (!seen.has(neighbor)) {\n seen.add(neighbor)\n next.add(neighbor)\n }\n }\n }\n if (next.size === 0) break\n frontier = next\n }\n\n if (!includeSeeds) {\n for (const seed of seeds) seen.delete(seed)\n }\n return seen\n}\n\n/**\n * Extract the induced subgraph over `nodeIds` — a {@link CommunityGraph}\n * containing only those nodes, the edges between them, and the trimmed\n * community memberships. Thin wrapper over {@link filterCommunityGraph}.\n */\nexport function subgraphForNodes(graph: CommunityGraph, nodeIds: Iterable<string>): CommunityGraph {\n return filterCommunityGraph(graph, { nodeIds: Array.from(nodeIds) })\n}\n\n/**\n * Convenience: the induced subgraph of a seed node plus its `depth`-hop\n * neighborhood. Useful for \"expand selection\" interactions.\n */\nexport function neighborhoodSubgraph(\n graph: CommunityGraph,\n seeds: Iterable<string>,\n options: ExpandOptions = {},\n): CommunityGraph {\n return subgraphForNodes(graph, expandNeighborhood(graph, seeds, options))\n}\n","import type { CommunityGraph, GraphLayoutAlgorithm } from './types.js'\n\n/** Current on-disk version for {@link GraphSnapshot}. */\nexport const GRAPH_SNAPSHOT_VERSION = 1\n\n/**\n * A self-contained, serializable representation of a community graph plus the\n * layout intent used to render it. Designed to be written to a static JSON file\n * and consumed by a JS-only host (e.g. a documentation site) without recomputing\n * the graph.\n */\nexport interface GraphSnapshot {\n version: number\n graph: CommunityGraph\n layout?: {\n algorithm: GraphLayoutAlgorithm\n width: number\n height: number\n }\n generatedAt?: string\n}\n\nexport interface SerializeSnapshotOptions {\n layout?: GraphSnapshot['layout']\n /** ISO timestamp to stamp into the snapshot. Omit for reproducible output. */\n generatedAt?: string\n}\n\nfunction sortedGraph(graph: CommunityGraph): CommunityGraph {\n return {\n nodes: [...graph.nodes].sort((a, b) => a.id.localeCompare(b.id)),\n edges: [...graph.edges].sort((a, b) => (\n a.source.localeCompare(b.source)\n || a.target.localeCompare(b.target)\n || (a.kind ?? '').localeCompare(b.kind ?? '')\n )),\n communities: [...graph.communities]\n .map((community) => ({ ...community, nodes: [...community.nodes].sort() }))\n .sort((a, b) => a.id.localeCompare(b.id)),\n }\n}\n\n/**\n * Build a deterministic {@link GraphSnapshot} from a graph. Nodes, edges, and\n * community members are sorted so that equal graphs serialize identically\n * (stable diffs, content-addressable caching).\n */\nexport function serializeGraphSnapshot(\n graph: CommunityGraph,\n options: SerializeSnapshotOptions = {},\n): GraphSnapshot {\n const snapshot: GraphSnapshot = {\n version: GRAPH_SNAPSHOT_VERSION,\n graph: sortedGraph(graph),\n }\n if (options.layout) snapshot.layout = options.layout\n if (options.generatedAt) snapshot.generatedAt = options.generatedAt\n return snapshot\n}\n\n/** Stable JSON string for a graph snapshot. */\nexport function stringifyGraphSnapshot(\n graph: CommunityGraph,\n options: SerializeSnapshotOptions = {},\n): string {\n return JSON.stringify(serializeGraphSnapshot(graph, options))\n}\n\n/**\n * Validate and unwrap a {@link GraphSnapshot} (or its JSON string) back into a\n * {@link CommunityGraph}. Throws on shape/version mismatch.\n */\nexport function deserializeGraphSnapshot(input: GraphSnapshot | string): CommunityGraph {\n const snapshot: unknown = typeof input === 'string' ? JSON.parse(input) : input\n if (!snapshot || typeof snapshot !== 'object') {\n throw new Error('invalid graph snapshot: not an object')\n }\n const candidate = snapshot as Partial<GraphSnapshot>\n if (candidate.version !== GRAPH_SNAPSHOT_VERSION) {\n throw new Error(`unsupported graph snapshot version: ${String(candidate.version)}`)\n }\n const graph = candidate.graph\n if (\n !graph\n || !Array.isArray(graph.nodes)\n || !Array.isArray(graph.edges)\n || !Array.isArray(graph.communities)\n ) {\n throw new Error('invalid graph snapshot: missing graph nodes/edges/communities')\n }\n return graph\n}\n","// 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","// @fortemi/graph — framework-agnostic graph presentation/projection helpers\n// plus the graph-source controller.\n//\n// Depends on @fortemi/core (the base data layer) and is consumed by\n// @fortemi/react and JS-only hosts. Dependency direction is the linear chain:\n// pglite ← @fortemi/core ← @fortemi/graph ← @fortemi/react. @fortemi/core never\n// imports @fortemi/graph.\n//\n// Pure projection helpers (layout, filtering, coloring, sizing, bounds,\n// neighborhood, snapshot) give React and JS-only hosts the shared logic to\n// render their own SVG/canvas views. `GraphController` adds the framework-\n// agnostic graph-source state machine (mode selection + load dispatch) on top\n// of @fortemi/core's repositories.\n//\n// Community *detection* intentionally lives in @fortemi/core (the base layer);\n// this package only renders/projects graphs it is given and orchestrates which\n// source to load.\n\nexport const VERSION = '2026.6.2'\n\nexport type {\n GraphNode,\n GraphEdge,\n GraphCommunity,\n CommunityGraph,\n GraphLayoutAlgorithm,\n PositionedGraphNode,\n PositionedGraph,\n GraphBounds,\n ViewportTransform,\n} from './types.js'\n\nexport { computeDegrees, nodeRadius } from './degree.js'\nexport type { NodeRadiusOptions } from './degree.js'\n\nexport { COMMUNITY_COLORS, UNASSIGNED_COMMUNITY_COLOR, colorForCommunity } from './color.js'\n\nexport { filterCommunityGraph } from './filter.js'\nexport type { GraphFilter } from './filter.js'\n\nexport { layoutCommunityGraph } from './layout.js'\nexport type { LayoutOptions } from './layout.js'\n\nexport { computeGraphBounds, fitGraphToViewport } from './bounds.js'\nexport type { FitOptions } from './bounds.js'\n\nexport {\n buildAdjacency,\n neighborsOf,\n expandNeighborhood,\n subgraphForNodes,\n neighborhoodSubgraph,\n} from './neighborhood.js'\nexport type { ExpandOptions } from './neighborhood.js'\n\nexport {\n GRAPH_SNAPSHOT_VERSION,\n serializeGraphSnapshot,\n stringifyGraphSnapshot,\n deserializeGraphSnapshot,\n} from './serialize.js'\nexport type { GraphSnapshot, SerializeSnapshotOptions } from './serialize.js'\n\nexport { GraphController } from './controller.js'\nexport type {\n GraphControllerDb,\n GraphSourceMode,\n GraphLayoutState,\n GraphTransitionState,\n GraphControllerStatus,\n GraphSourceRef,\n GraphControllerState,\n GraphControllerOptions,\n GraphControllerListener,\n} from './controller.js'\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fortemi/graph",
3
- "version": "2026.6.2",
4
- "description": "Framework-agnostic graph tooling for fortemi: pure TypeScript helpers for community-graph layout, filtering, community detection, color assignment, degree sizing, bounds/fit, neighborhood expansion, and static snapshot serialization. No React, no database — shared by @fortemi/core, @fortemi/react, and JS-only hosts.",
3
+ "version": "2026.6.3",
4
+ "description": "Framework-agnostic graph tooling for fortemi: pure TypeScript helpers for community-graph layout, filtering, color assignment, degree sizing, bounds/fit, neighborhood expansion, and static snapshot serialization, plus a framework-agnostic GraphController for graph-source selection. No React. Depends on @fortemi/core; consumed by @fortemi/react and JS-only hosts.",
5
5
  "keywords": [
6
6
  "fortemi",
7
7
  "graph",
@@ -42,16 +42,18 @@
42
42
  "README.md",
43
43
  "LICENSE"
44
44
  ],
45
- "scripts": {
46
- "build": "tsup",
47
- "typecheck": "tsc --noEmit",
48
- "test": "vitest run",
49
- "test:watch": "vitest",
50
- "prepack": "pnpm build"
45
+ "dependencies": {
46
+ "@fortemi/core": "2026.6.3"
51
47
  },
52
48
  "devDependencies": {
53
49
  "tsup": "^8.3.5",
54
50
  "typescript": "^5.8.0",
55
51
  "vitest": "^4.1.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest"
56
58
  }
57
- }
59
+ }