@fortemi/graph 2026.7.3 → 2026.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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>;
@@ -151,7 +89,28 @@ interface LayoutOptions {
151
89
  communityStrength?: number;
152
90
  /** Keep every node center at least this many px from each canvas edge. */
153
91
  boundsPadding?: number;
92
+ /**
93
+ * Positions to hold FIXED during settlement (drag pins, issue #245). Pinned
94
+ * nodes stay put while the rest of the graph re-settles around them, giving an
95
+ * incremental re-layout when a node is moved. Keyed by node id.
96
+ */
97
+ pinned?: PositionMap;
98
+ /**
99
+ * Warm-start seed positions. Nodes present here begin the settlement from
100
+ * these coordinates instead of the closed-form ring, so a re-layout resumes
101
+ * from the current arrangement (smooth reshape) rather than re-seeding. Keyed
102
+ * by node id. `pinned` overrides this for pinned nodes.
103
+ */
104
+ initialPositions?: PositionMap;
154
105
  }
106
+ /** A node-id → position lookup, accepted as either a Map or a plain record. */
107
+ type PositionMap = Map<string, {
108
+ x: number;
109
+ y: number;
110
+ }> | Record<string, {
111
+ x: number;
112
+ y: number;
113
+ }>;
155
114
  /**
156
115
  * Deterministically position the nodes of a {@link CommunityGraph} in 2D space.
157
116
  *
@@ -254,87 +213,208 @@ declare function stringifyGraphSnapshot(graph: CommunityGraph, options?: Seriali
254
213
  */
255
214
  declare function deserializeGraphSnapshot(input: GraphSnapshot | string): CommunityGraph;
256
215
 
257
- /** A database handle accepted by the underlying repositories. */
258
- type GraphControllerDb = QueryExecutor & DatabaseClient;
259
- type GraphSourceMode = 'citations' | 'topics' | 'precomputed' | 'dynamic-search' | 'user-authored';
260
- interface GraphLayoutState {
261
- algorithm: GraphLayoutAlgorithm;
262
- pinSelectedNodes?: boolean;
263
- preserveViewport?: boolean;
264
- communitySpacing?: number;
216
+ /**
217
+ * Greyscale community ramp — warm-grey, darkest first, meant to sit on a light
218
+ * ("cream") background. Index 0 (darkest) is the largest community. Strictly
219
+ * greyscale so community tone reads as density, not category.
220
+ */
221
+ declare const GREYSCALE_COMMUNITY_RAMP: readonly ["#2B2824", "#43403A", "#585149", "#6E665A", "#837A6B", "#968C7C"];
222
+ /** A render-ready node: identity + label + degree-derived size + tone + optional baked position. */
223
+ interface RenderNode {
224
+ id: string;
225
+ label: string;
226
+ /** Degree-derived render size. */
227
+ size: number;
228
+ /** Community tone (see {@link CommunityPalette}). */
229
+ color: string;
230
+ /** Community rank: 0 = largest community, ascending; -1 when unassigned. */
231
+ communityRank: number;
232
+ /** Baked layout coordinate (present only when positions were supplied). */
233
+ x?: number;
234
+ y?: number;
235
+ }
236
+ /** A render-ready link (edge) referencing nodes by id. */
237
+ interface RenderLink {
238
+ source: string;
239
+ target: string;
240
+ weight: number;
241
+ kind?: string;
265
242
  }
266
- interface GraphTransitionState {
267
- fromMode?: GraphSourceMode;
268
- toMode: GraphSourceMode;
269
- reason: 'mode-change' | 'embedding-set-change' | 'community-source-change' | 'filter-change' | 'recompute';
270
- startedAt: string;
243
+ /** A render-ready graph: the shape every renderer tier consumes. */
244
+ interface RenderGraph {
245
+ nodes: RenderNode[];
246
+ links: RenderLink[];
247
+ /** Number of communities in the source graph. */
248
+ clusters: number;
271
249
  }
272
- interface GraphControllerStatus {
273
- loading: boolean;
274
- error: Error | null;
275
- freshness: 'fresh' | 'stale' | 'unknown' | null;
276
- cache: SimilarityGraphResult['cache'] | null;
250
+ /**
251
+ * Community coloring strategy for {@link mapCommunityGraph}:
252
+ * - `'community'` (default): hash the community id into the qualitative palette
253
+ * (matches the historical `GraphView` coloring).
254
+ * - `'greyscale'`: {@link GREYSCALE_COMMUNITY_RAMP} indexed by community rank
255
+ * (largest cluster = darkest).
256
+ * - a custom array: indexed by community rank.
257
+ */
258
+ type CommunityPalette = 'community' | 'greyscale' | readonly string[];
259
+ interface MapCommunityGraphOptions {
260
+ /** Resolve a human label for a node id. Defaults to the id itself. */
261
+ labelFor?: (id: string) => string;
262
+ /** Baked layout positions to stamp onto nodes (warm start). */
263
+ positions?: Map<string, {
264
+ x: number;
265
+ y: number;
266
+ }> | Record<string, {
267
+ x: number;
268
+ y: number;
269
+ }>;
270
+ /** Community coloring strategy. Default `'community'`. */
271
+ palette?: CommunityPalette;
272
+ /** Node-radius tuning passed to {@link nodeRadius} (default sizer). */
273
+ radius?: NodeRadiusOptions;
274
+ /** Override the degree → size mapping entirely. */
275
+ sizeFor?: (degree: number, id: string) => number;
277
276
  }
278
- type GraphSourceRef = SimilarityGraphResult['graphSource'] | {
279
- id: string;
280
- name: string;
281
- };
282
- /** The full, framework-agnostic controller state surfaced to subscribers. */
283
- interface GraphControllerState {
284
- mode: GraphSourceMode;
285
- graph: CommunityGraph$1 | null;
286
- graphSource?: GraphSourceRef;
287
- communitySource?: CommunitySourceDescriptor;
288
- embeddingSetSelector?: EmbeddingSetSelector;
289
- filters?: CommunityFilterDefinition;
290
- layout: GraphLayoutState;
291
- status: GraphControllerStatus;
292
- transition?: GraphTransitionState;
277
+ /**
278
+ * Rank communities by member count, largest first. Returns a map from community
279
+ * id to its rank (0 = largest). Ties break on community id for determinism.
280
+ */
281
+ declare function communityRanks(graph: CommunityGraph): Map<string, number>;
282
+ /**
283
+ * Map a {@link CommunityGraph} to a render-ready {@link RenderGraph}: labels,
284
+ * degree-derived node size, per-community tone (largest cluster = darkest for
285
+ * ramped palettes), and baked positions when supplied. Pure and deterministic —
286
+ * shared by the runtime loader and the build-time snapshot writer so both render
287
+ * identically.
288
+ */
289
+ declare function mapCommunityGraph(graph: CommunityGraph, options?: MapCommunityGraphOptions): RenderGraph;
290
+ interface BakeRenderGraphOptions extends MapCommunityGraphOptions {
291
+ /** Layout options for the one-shot build-time layout pass. */
292
+ layout?: LayoutOptions;
293
293
  }
294
- interface GraphControllerOptions {
295
- initialMode?: GraphSourceMode;
296
- initialEmbeddingSetSelector?: EmbeddingSetSelector;
297
- initialCommunitySourceId?: string;
298
- initialFilters?: CommunityFilterDefinition;
299
- layout?: Partial<GraphLayoutState>;
294
+ /**
295
+ * Build-time snapshot writer: run the layout once and emit a {@link RenderGraph}
296
+ * with baked x/y so hosts can warm-start (skip the layout pass) at runtime.
297
+ * Companion to `serializeGraphSnapshot` — this bakes render-ready positions
298
+ * rather than storing the raw graph + layout intent. Any `positions` passed in
299
+ * options are ignored in favor of the freshly computed layout.
300
+ */
301
+ declare function bakeRenderGraph(graph: CommunityGraph, options?: BakeRenderGraphOptions): RenderGraph;
302
+ /** Deterministic JSON for a render graph (stable key order for cache-friendly diffs). */
303
+ declare function stringifyRenderGraph(graph: RenderGraph): string;
304
+ /** Structural guard for a {@link RenderGraph} loaded from an untrusted source. */
305
+ declare function isRenderGraph(value: unknown): value is RenderGraph;
306
+ /** Whether every node in a render graph carries a baked position. */
307
+ declare function hasBakedPositions(graph: RenderGraph): boolean;
308
+ interface LoadRenderSnapshotOptions {
309
+ /** `fetch` implementation to use when `source` is a URL. Defaults to `globalThis.fetch`. */
310
+ fetchImpl?: typeof fetch;
311
+ /** Require baked x/y on every node; a snapshot without positions resolves to `null`. Default `true`. */
312
+ requirePositions?: boolean;
300
313
  }
301
- type GraphControllerListener = (state: GraphControllerState) => void;
302
314
  /**
303
- * Drives graph-source selection and loading independent of any UI framework.
304
- *
305
- * Construct with pre-built repositories (handy for tests) or via
306
- * {@link GraphController.fromDb}. Subscribe with {@link subscribe} and read the
307
- * current state with {@link getState}; call {@link start} once to trigger the
308
- * initial load. Setters update state and schedule a refresh, mirroring the
309
- * effect-driven behavior of the original React hook.
315
+ * Snapshot-first loader (issue #264). Load a precomputed, render-ready graph
316
+ * carrying baked positions — instant, no live `GraphController` build and no
317
+ * layout pass. Accepts a URL to fetch, an already-parsed {@link RenderGraph}, or
318
+ * a thunk. Returns `null` (never throws) when the snapshot is absent, malformed,
319
+ * or lacks baked positions, so callers fall back to a live build + `mapCommunityGraph`.
310
320
  */
311
- declare class GraphController {
312
- private readonly graphRepo;
313
- private readonly communityRepo;
314
- private readonly listeners;
315
- private communitySourceId;
316
- private state;
317
- /** Build a controller from a database handle (production path). */
318
- static fromDb(db: GraphControllerDb, options?: GraphControllerOptions): GraphController;
319
- constructor(graphRepo: GraphRepository, communityRepo: CommunitiesRepository, options?: GraphControllerOptions);
320
- getState(): GraphControllerState;
321
- subscribe(listener: GraphControllerListener): () => void;
322
- /** Run the initial load. Call once after construction (the hook calls this on mount). */
323
- start(): Promise<void>;
324
- private setState;
325
- private beginTransition;
326
- refresh(): Promise<void>;
327
- setMode(nextMode: GraphSourceMode): void;
328
- setEmbeddingSetSelector(selector: EmbeddingSetSelector): void;
329
- setCommunitySource(sourceId: string | null): void;
330
- /** Apply dynamic-search filters and switch into that mode (state only). */
331
- private applyFilters;
332
- setFilters(nextFilters: CommunityFilterDefinition): void;
333
- recompute(): Promise<void>;
334
- previewDynamicCommunity(nextFilters: CommunityFilterDefinition): Promise<void>;
335
- saveCurrentCommunity(input: CommunityCreateInput): Promise<CommunitySourceDescriptor>;
321
+ declare function loadRenderSnapshot(source: string | RenderGraph | (() => Promise<RenderGraph | null> | RenderGraph | null), options?: LoadRenderSnapshotOptions): Promise<RenderGraph | null>;
322
+
323
+ /** Node/edge visibility filter shared by every renderer tier. */
324
+ interface GraphControlFilters {
325
+ /** Keep only nodes in these communities. */
326
+ communityIds?: string[];
327
+ /** Keep only edges whose `kind` is in this set. */
328
+ edgeKinds?: string[];
329
+ /** Explicit node allow-list. */
330
+ nodeIds?: string[];
331
+ /** Hide nodes with fewer than this many connections (degree) in the visible set. */
332
+ minDegree?: number;
336
333
  }
334
+ /**
335
+ * The control surface common to all renderer tiers. A consumer coded against
336
+ * this contract can move a graph between the JS-only renderer, React
337
+ * `GraphView`, and future tiers by passing the same options object.
338
+ */
339
+ interface GraphControlContract {
340
+ /** Layout algorithm. Default `force`. */
341
+ algorithm?: GraphLayoutAlgorithm;
342
+ /** Visibility filter. */
343
+ filters?: GraphControlFilters;
344
+ /** Selected node id (controlled). */
345
+ selectedNodeId?: string | null;
346
+ /** Selection callback (click / keyboard). */
347
+ onSelectNode?: (nodeId: string) => void;
348
+ /** Navigation callback (double-click / Enter on a selected node). */
349
+ onNavigate?: (nodeId: string) => void;
350
+ /** Human label for a node (popup / a11y). Default: the node id. */
351
+ labelFor?: (nodeId: string) => string;
352
+ /** Community palette override. */
353
+ colors?: readonly string[];
354
+ /** Logical viewport size. */
355
+ width?: number;
356
+ height?: number;
357
+ }
358
+ /** One community-legend row: community id, its color, and member count. */
359
+ interface GraphLegendEntry {
360
+ communityId: string;
361
+ color: string;
362
+ count: number;
363
+ }
364
+ /**
365
+ * Apply the shared {@link GraphControlFilters} to `graph`. Composes the engine's
366
+ * community / edge-kind / node-allow-list filter with a `minDegree` pass
367
+ * (degree computed over the already-filtered set). Pure — input is not mutated.
368
+ */
369
+ declare function applyControlFilters(graph: CommunityGraph | null | undefined, filters?: GraphControlFilters): CommunityGraph;
370
+ /**
371
+ * Build community-legend rows for a graph — the shared data that any tier can
372
+ * render as a legend and use to drive the `communityIds` show/hide filter.
373
+ * Sorted by member count (largest first). Colors match `colorForCommunity`.
374
+ */
375
+ declare function communityLegend(graph: CommunityGraph | null | undefined, colors?: readonly string[]): GraphLegendEntry[];
376
+
377
+ /** Filter shape accepted by the renderer — the shared control-filter contract. */
378
+ type GraphRenderFilters = GraphControlFilters;
379
+ /**
380
+ * Options for {@link renderCommunityGraph}. Extends the shared
381
+ * {@link GraphControlContract} (algorithm, filters, selection, navigation,
382
+ * labels, colors, size — honored by every renderer tier) with renderer-specific
383
+ * knobs. The React `GraphView` honors the same contract, so the tiers are
384
+ * drop-in swappable.
385
+ */
386
+ interface GraphRenderOptions extends GraphControlContract {
387
+ /** Extra layout tuning forwarded to `layoutCommunityGraph`. */
388
+ layoutOptions?: Omit<LayoutOptions, 'algorithm' | 'width' | 'height'>;
389
+ /** Canvas background. Default `#fafafa`. */
390
+ background?: string;
391
+ /** Enable zoom/pan/hover interactions. Default `true`. */
392
+ interactive?: boolean;
393
+ /** Zoom clamp. Default 0.4 / 3. */
394
+ minScale?: number;
395
+ maxScale?: number;
396
+ }
397
+ /** Mutable subset applied via {@link GraphRenderHandle.update}. */
398
+ type GraphRenderUpdate = Partial<Pick<GraphRenderOptions, 'filters' | 'algorithm' | 'selectedNodeId' | 'labelFor'>> & {
399
+ graph?: CommunityGraph;
400
+ };
401
+ /** Live handle returned by {@link renderCommunityGraph}. */
402
+ interface GraphRenderHandle {
403
+ /** Apply new graph/filters/algorithm/selection without recreating the view. */
404
+ update(next: GraphRenderUpdate): void;
405
+ /** Select and center a node (search/focus). Pass `null` to clear selection. */
406
+ focus(nodeId: string | null): void;
407
+ /** Remove the view, its popup, and all listeners from the container. */
408
+ destroy(): void;
409
+ /** The mounted `<svg>` element. */
410
+ readonly element: SVGSVGElement;
411
+ }
412
+ /**
413
+ * Render `graph` into `container` as an interactive SVG community graph.
414
+ * Returns a {@link GraphRenderHandle}; call `destroy()` to tear it down.
415
+ */
416
+ declare function renderCommunityGraph(container: HTMLElement, graph: CommunityGraph, options?: GraphRenderOptions): GraphRenderHandle;
337
417
 
338
- declare const VERSION = "2026.7.3";
418
+ declare const VERSION = "2026.7.5";
339
419
 
340
- 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 NodeRadiusResolver, type PositionedCommunity, 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 };
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 };