@gettymade/roux 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { DirectedGraph } from 'graphology';
1
2
  import { Database } from 'better-sqlite3';
2
3
 
3
4
  interface SourceRef {
@@ -17,6 +18,13 @@ interface Node {
17
18
  properties: Record<string, unknown>;
18
19
  sourceRef?: SourceRef;
19
20
  }
21
+ /** Caller-settable fields for updateNode. No id (immutable), no outgoingLinks (derived from content). */
22
+ interface NodeUpdates {
23
+ title?: string;
24
+ content?: string;
25
+ tags?: string[];
26
+ properties?: Record<string, unknown>;
27
+ }
20
28
  interface NodeWithContext extends Node {
21
29
  /** Populated when depth > 0 */
22
30
  neighbors?: Node[];
@@ -41,7 +49,7 @@ interface Edge {
41
49
  properties?: Record<string, unknown>;
42
50
  }
43
51
 
44
- type Metric = 'pagerank' | 'in_degree' | 'out_degree';
52
+ type Metric = 'in_degree' | 'out_degree';
45
53
  interface ListFilter {
46
54
  /** Filter by tag (case-insensitive) */
47
55
  tag?: string;
@@ -96,10 +104,24 @@ interface LinkInfo {
96
104
  id: string;
97
105
  title: string;
98
106
  }
107
+ /** Base fields all providers must implement. */
108
+ interface ProviderBase {
109
+ /** Unique identifier for this provider instance. Must be non-empty. */
110
+ readonly id: string;
111
+ }
112
+ /**
113
+ * Optional lifecycle hooks for providers.
114
+ * - onRegister: Called after registration with GraphCore. Errors propagate to caller.
115
+ * - onUnregister: Called before provider is replaced or GraphCore is destroyed. Best-effort, errors logged.
116
+ */
117
+ interface ProviderLifecycle {
118
+ onRegister?(): Promise<void>;
119
+ onUnregister?(): Promise<void>;
120
+ }
99
121
  /** Data persistence and graph operations. Required provider. */
100
- interface StoreProvider {
122
+ interface Store extends ProviderBase, ProviderLifecycle {
101
123
  createNode(node: Node): Promise<void>;
102
- updateNode(id: string, updates: Partial<Node>): Promise<void>;
124
+ updateNode(id: string, updates: NodeUpdates): Promise<void>;
103
125
  deleteNode(id: string): Promise<void>;
104
126
  getNode(id: string): Promise<Node | null>;
105
127
  getNodes(ids: string[]): Promise<Node[]>;
@@ -108,15 +130,15 @@ interface StoreProvider {
108
130
  getHubs(metric: Metric, limit: number): Promise<Array<[string, number]>>;
109
131
  storeEmbedding(id: string, vector: number[], model: string): Promise<void>;
110
132
  searchByVector(vector: number[], limit: number): Promise<VectorSearchResult[]>;
111
- searchByTags(tags: string[], mode: TagMode): Promise<Node[]>;
133
+ searchByTags(tags: string[], mode: TagMode, limit?: number): Promise<Node[]>;
112
134
  getRandomNode(tags?: string[]): Promise<Node | null>;
113
135
  resolveTitles(ids: string[]): Promise<Map<string, string>>;
114
136
  listNodes(filter: ListFilter, options?: ListOptions): Promise<ListNodesResult>;
115
137
  resolveNodes(names: string[], options?: ResolveOptions): Promise<ResolveResult[]>;
116
138
  nodesExist(ids: string[]): Promise<Map<string, boolean>>;
117
139
  }
118
- /** Stateless vector generation. Storage handled by StoreProvider. */
119
- interface EmbeddingProvider {
140
+ /** Stateless vector generation. Storage handled by Store. */
141
+ interface Embedding extends ProviderBase, ProviderLifecycle {
120
142
  embed(text: string): Promise<number[]>;
121
143
  embedBatch(texts: string[]): Promise<number[][]>;
122
144
  /** For storage allocation */
@@ -124,14 +146,34 @@ interface EmbeddingProvider {
124
146
  modelId(): string;
125
147
  }
126
148
  /** Pluggable vector storage and similarity search. */
127
- interface VectorProvider {
149
+ interface VectorIndex {
128
150
  store(id: string, vector: number[], model: string): Promise<void>;
129
151
  search(vector: number[], limit: number): Promise<VectorSearchResult[]>;
130
152
  delete(id: string): Promise<void>;
131
153
  getModel(id: string): Promise<string | null>;
132
154
  hasEmbedding(id: string): boolean;
133
155
  }
134
- declare function isVectorProvider(value: unknown): value is VectorProvider;
156
+ declare function isVectorIndex(value: unknown): value is VectorIndex;
157
+
158
+ type PropertyType = 'string' | 'number' | 'boolean' | 'function' | 'object' | 'array';
159
+ interface PropertySchema {
160
+ type: PropertyType;
161
+ optional?: boolean;
162
+ /** For 'string' type, require non-empty */
163
+ nonEmpty?: boolean;
164
+ }
165
+ type Schema = Record<string, PropertySchema>;
166
+ /**
167
+ * Create a type guard function from a schema definition.
168
+ *
169
+ * @example
170
+ * const isUser = createGuard<User>({
171
+ * id: { type: 'string', nonEmpty: true },
172
+ * name: { type: 'string' },
173
+ * age: { type: 'number', optional: true },
174
+ * });
175
+ */
176
+ declare function createGuard<T>(schema: Schema): (value: unknown) => value is T;
135
177
 
136
178
  interface SearchOptions {
137
179
  /** Default: 10 */
@@ -142,12 +184,13 @@ interface SearchOptions {
142
184
  }
143
185
  /** Orchestration hub. Zero functionality without providers. */
144
186
  interface GraphCore {
145
- registerStore(provider: StoreProvider): void;
146
- registerEmbedding(provider: EmbeddingProvider): void;
187
+ registerStore(provider: Store): Promise<void>;
188
+ registerEmbedding(provider: Embedding): Promise<void>;
189
+ destroy(): Promise<void>;
147
190
  search(query: string, options?: SearchOptions): Promise<Node[]>;
148
191
  getNode(id: string, depth?: number): Promise<NodeWithContext | null>;
149
192
  createNode(node: Partial<Node>): Promise<Node>;
150
- updateNode(id: string, updates: Partial<Node>): Promise<Node>;
193
+ updateNode(id: string, updates: NodeUpdates): Promise<Node>;
151
194
  deleteNode(id: string): Promise<boolean>;
152
195
  getNeighbors(id: string, options: NeighborOptions): Promise<Node[]>;
153
196
  findPath(source: string, target: string): Promise<string[] | null>;
@@ -171,9 +214,14 @@ interface CacheConfig {
171
214
  }
172
215
  type ModelChangeBehavior = 'lazy' | 'eager';
173
216
  interface SystemConfig {
174
- /** Embedding regeneration strategy */
175
217
  onModelChange: ModelChangeBehavior;
176
218
  }
219
+ type FilenameSeparator = 'space' | 'dash';
220
+ type TitleCasing = 'title' | 'sentence' | 'as-is';
221
+ interface NamingConventions {
222
+ filename: FilenameSeparator;
223
+ title: TitleCasing;
224
+ }
177
225
  interface DocStoreConfig {
178
226
  type: 'docstore';
179
227
  }
@@ -209,7 +257,6 @@ type LLMConfig = OllamaLLMConfig | OpenAILLMConfig;
209
257
  type StoreConfig = DocStoreConfig;
210
258
  interface ProvidersConfig {
211
259
  store: StoreConfig;
212
- /** Defaults to local */
213
260
  embedding?: EmbeddingConfig;
214
261
  llm?: LLMConfig;
215
262
  }
@@ -217,6 +264,7 @@ interface RouxConfig {
217
264
  source?: SourceConfig;
218
265
  cache?: CacheConfig;
219
266
  system?: SystemConfig;
267
+ naming?: NamingConventions;
220
268
  providers: ProvidersConfig;
221
269
  }
222
270
  declare const DEFAULT_CONFIG: Required<Pick<RouxConfig, 'source' | 'cache' | 'system'>> & {
@@ -228,14 +276,15 @@ declare const DEFAULT_CONFIG: Required<Pick<RouxConfig, 'source' | 'cache' | 'sy
228
276
  declare class GraphCoreImpl implements GraphCore {
229
277
  private store;
230
278
  private embedding;
231
- registerStore(provider: StoreProvider): void;
232
- registerEmbedding(provider: EmbeddingProvider): void;
279
+ registerStore(provider: Store): Promise<void>;
280
+ registerEmbedding(provider: Embedding): Promise<void>;
281
+ destroy(): Promise<void>;
233
282
  private requireStore;
234
283
  private requireEmbedding;
235
284
  search(query: string, options?: SearchOptions): Promise<Node[]>;
236
285
  getNode(id: string, depth?: number): Promise<NodeWithContext | null>;
237
286
  createNode(partial: Partial<Node>): Promise<Node>;
238
- updateNode(id: string, updates: Partial<Node>): Promise<Node>;
287
+ updateNode(id: string, updates: NodeUpdates): Promise<Node>;
239
288
  deleteNode(id: string): Promise<boolean>;
240
289
  getNeighbors(id: string, options: NeighborOptions): Promise<Node[]>;
241
290
  findPath(source: string, target: string): Promise<string[] | null>;
@@ -244,80 +293,255 @@ declare class GraphCoreImpl implements GraphCore {
244
293
  getRandomNode(tags?: string[]): Promise<Node | null>;
245
294
  listNodes(filter: ListFilter, options?: ListOptions): Promise<ListNodesResult>;
246
295
  resolveNodes(names: string[], options?: ResolveOptions): Promise<ResolveResult[]>;
247
- private cosineSimilarity;
248
- static fromConfig(config: RouxConfig): GraphCoreImpl;
296
+ static fromConfig(config: RouxConfig): Promise<GraphCoreImpl>;
249
297
  }
250
298
 
251
- declare class DocStore implements StoreProvider {
252
- private cache;
253
- private sourceRoot;
299
+ declare class GraphManager {
254
300
  private graph;
255
- private vectorProvider;
256
- private ownsVectorProvider;
301
+ /** Build graph and return centrality metrics. Caller stores as needed. */
302
+ build(nodes: Node[]): Map<string, CentralityMetrics>;
303
+ /** Throws GraphNotReadyError if not built. Returns graph for query use. */
304
+ assertReady(): DirectedGraph;
305
+ isReady(): boolean;
306
+ getNeighborIds(id: string, options: NeighborOptions): string[];
307
+ findPath(source: string, target: string): string[] | null;
308
+ getHubs(metric: Metric, limit: number): Array<[string, number]>;
309
+ }
310
+
311
+ interface StoreProviderOptions {
312
+ vectorIndex?: VectorIndex;
313
+ }
314
+ declare abstract class StoreProvider {
315
+ protected readonly graphManager: GraphManager;
316
+ protected readonly vectorIndex: VectorIndex | null;
317
+ constructor(options?: StoreProviderOptions);
318
+ protected abstract loadAllNodes(): Promise<Node[]>;
319
+ protected abstract getNodesByIds(ids: string[]): Promise<Node[]>;
320
+ abstract createNode(node: Node): Promise<void>;
321
+ abstract updateNode(id: string, updates: NodeUpdates): Promise<void>;
322
+ abstract deleteNode(id: string): Promise<void>;
323
+ abstract getNode(id: string): Promise<Node | null>;
324
+ abstract getNodes(ids: string[]): Promise<Node[]>;
325
+ abstract close(): void;
326
+ getNeighbors(id: string, options: NeighborOptions): Promise<Node[]>;
327
+ findPath(source: string, target: string): Promise<string[] | null>;
328
+ getHubs(metric: Metric, limit: number): Promise<Array<[string, number]>>;
329
+ storeEmbedding(id: string, vector: number[], model: string): Promise<void>;
330
+ searchByVector(vector: number[], limit: number): Promise<VectorSearchResult[]>;
331
+ getRandomNode(tags?: string[]): Promise<Node | null>;
332
+ searchByTags(tags: string[], mode: TagMode, limit?: number): Promise<Node[]>;
333
+ listNodes(filter: ListFilter, options?: ListOptions): Promise<ListNodesResult>;
334
+ nodesExist(ids: string[]): Promise<Map<string, boolean>>;
335
+ resolveTitles(ids: string[]): Promise<Map<string, string>>;
336
+ resolveNodes(names: string[], options?: ResolveOptions): Promise<ResolveResult[]>;
337
+ protected syncGraph(): Promise<void>;
338
+ protected onCentralityComputed(_centrality: Map<string, CentralityMetrics>): void;
339
+ }
340
+
341
+ /**
342
+ * FileWatcher - Pure file system event emitter
343
+ *
344
+ * Responsibilities:
345
+ * - Wraps chokidar
346
+ * - Filters (.md only, excluded dirs)
347
+ * - Coalesces events
348
+ * - Debounces
349
+ * - Emits batched events via callback
350
+ */
351
+
352
+ type FileEventType = 'add' | 'change' | 'unlink';
353
+ interface FileWatcherOptions {
354
+ root: string;
355
+ /** File extensions to watch (e.g., new Set(['.md', '.markdown'])). Required. */
356
+ extensions: ReadonlySet<string>;
357
+ debounceMs?: number;
358
+ /** Called after debounce with coalesced events. Exceptions (sync or async) are
359
+ * logged and swallowed; watcher continues operating. */
360
+ onBatch: (events: Map<string, FileEventType>) => void | Promise<void>;
361
+ }
362
+ declare class FileWatcher {
363
+ private readonly root;
364
+ private readonly extensions;
365
+ private readonly debounceMs;
366
+ private readonly onBatch;
257
367
  private watcher;
258
368
  private debounceTimer;
259
369
  private pendingChanges;
370
+ private isPaused;
371
+ constructor(options: FileWatcherOptions);
372
+ start(): Promise<void>;
373
+ stop(): void;
374
+ isWatching(): boolean;
375
+ pause(): void;
376
+ resume(): void;
377
+ flush(): void;
378
+ private queueChange;
379
+ }
380
+
381
+ /**
382
+ * FormatReader plugin types
383
+ *
384
+ * Extracted to break circular dependency between reader-registry and readers.
385
+ */
386
+
387
+ /**
388
+ * Result of parsing a file through ReaderRegistry
389
+ */
390
+ interface ParseResult {
391
+ node: Node;
392
+ /** True if the file needs a stable frontmatter ID written */
393
+ needsIdWrite: boolean;
394
+ }
395
+ /**
396
+ * Context provided to readers during parsing
397
+ */
398
+ interface FileContext {
399
+ /** Full absolute path to the file */
400
+ absolutePath: string;
401
+ /** Path relative to source root (becomes node ID) */
402
+ relativePath: string;
403
+ /** File extension including dot (e.g., '.md') */
404
+ extension: string;
405
+ /** File modification time */
406
+ mtime: Date;
407
+ }
408
+ /**
409
+ * Interface for format-specific file readers
410
+ */
411
+ interface FormatReader {
412
+ /** Extensions this reader handles (e.g., ['.md', '.markdown']) */
413
+ readonly extensions: string[];
414
+ /** Parse file content into a Node */
415
+ parse(content: string, context: FileContext): Node;
416
+ }
417
+
418
+ /**
419
+ * FormatReader plugin architecture
420
+ *
421
+ * Provides a registry for file format readers, enabling multi-format support
422
+ * in DocStore while keeping format-specific logic isolated.
423
+ */
424
+
425
+ /**
426
+ * Registry for FormatReader implementations
427
+ */
428
+ declare class ReaderRegistry {
429
+ private readers;
430
+ /**
431
+ * Register a reader for its declared extensions.
432
+ * Throws if any extension is already registered (atomic - no partial registration).
433
+ */
434
+ register(reader: FormatReader): void;
435
+ /**
436
+ * Get reader for an extension, or null if none registered.
437
+ * Case-insensitive.
438
+ */
439
+ getReader(extension: string): FormatReader | null;
440
+ /**
441
+ * Get all registered extensions
442
+ */
443
+ getExtensions(): ReadonlySet<string>;
444
+ /**
445
+ * Check if an extension has a registered reader.
446
+ * Case-insensitive.
447
+ */
448
+ hasReader(extension: string): boolean;
449
+ /**
450
+ * Parse content using the appropriate reader for the file's extension.
451
+ * Validates frontmatter ID and signals if writeback is needed.
452
+ * Throws if no reader is registered for the extension.
453
+ *
454
+ * Note: Does NOT generate new IDs here - that happens in Phase 3's writeback.
455
+ * Files without valid frontmatter IDs keep their path-based ID for now,
456
+ * with needsIdWrite: true signaling that an ID should be generated and written.
457
+ */
458
+ parse(content: string, context: FileContext): ParseResult;
459
+ }
460
+
461
+ interface DocStoreOptions {
462
+ sourceRoot: string;
463
+ cacheDir: string;
464
+ id?: string;
465
+ vectorIndex?: VectorIndex;
466
+ registry?: ReaderRegistry;
467
+ /** Optional FileWatcher instance. If provided, DocStore uses it instead of creating one. */
468
+ fileWatcher?: FileWatcher;
469
+ }
470
+ declare class DocStore extends StoreProvider {
471
+ readonly id: string;
472
+ private cache;
473
+ private sourceRoot;
474
+ private ownsVectorIndex;
475
+ private registry;
476
+ private fileWatcher;
260
477
  private onChangeCallback;
261
- constructor(sourceRoot: string, cacheDir: string, vectorProvider?: VectorProvider);
478
+ constructor(options: DocStoreOptions);
262
479
  sync(): Promise<void>;
263
480
  createNode(node: Node): Promise<void>;
264
- updateNode(id: string, updates: Partial<Node>): Promise<void>;
481
+ updateNode(id: string, updates: NodeUpdates): Promise<void>;
265
482
  deleteNode(id: string): Promise<void>;
266
483
  getNode(id: string): Promise<Node | null>;
267
484
  getNodes(ids: string[]): Promise<Node[]>;
268
485
  getAllNodeIds(): Promise<string[]>;
269
- searchByTags(tags: string[], mode: TagMode): Promise<Node[]>;
270
- getRandomNode(tags?: string[]): Promise<Node | null>;
486
+ searchByTags(tags: string[], mode: TagMode, limit?: number): Promise<Node[]>;
271
487
  resolveTitles(ids: string[]): Promise<Map<string, string>>;
272
488
  listNodes(filter: ListFilter, options?: ListOptions): Promise<ListNodesResult>;
273
489
  resolveNodes(names: string[], options?: ResolveOptions): Promise<ResolveResult[]>;
274
490
  nodesExist(ids: string[]): Promise<Map<string, boolean>>;
275
- getNeighbors(id: string, options: NeighborOptions): Promise<Node[]>;
276
- findPath(source: string, target: string): Promise<string[] | null>;
277
- getHubs(metric: Metric, limit: number): Promise<Array<[string, number]>>;
278
- storeEmbedding(id: string, vector: number[], model: string): Promise<void>;
279
- searchByVector(vector: number[], limit: number): Promise<VectorSearchResult[]>;
280
491
  hasEmbedding(id: string): boolean;
281
492
  close(): void;
493
+ onRegister(): Promise<void>;
494
+ onUnregister(): Promise<void>;
282
495
  startWatching(onChange?: (changedIds: string[]) => void): Promise<void>;
283
496
  stopWatching(): void;
284
497
  isWatching(): boolean;
285
- private queueChange;
286
- private processQueue;
287
- private buildFilenameIndex;
288
- private resolveOutgoingLinks;
289
- private ensureGraph;
290
- private rebuildGraph;
291
- private static readonly EXCLUDED_DIRS;
292
- private collectMarkdownFiles;
293
- private getFileMtime;
294
- private fileToNode;
498
+ private handleWatcherBatch;
499
+ private resolveAllLinks;
500
+ getNeighbors(id: string, options: {
501
+ direction: 'in' | 'out' | 'both';
502
+ limit?: number;
503
+ }): Promise<Node[]>;
504
+ findPath(source: string, target: string): Promise<string[] | null>;
505
+ getHubs(metric: 'in_degree' | 'out_degree', limit: number): Promise<Array<[string, number]>>;
506
+ protected loadAllNodes(): Promise<Node[]>;
507
+ protected getNodesByIds(ids: string[]): Promise<Node[]>;
508
+ protected onCentralityComputed(centrality: Map<string, CentralityMetrics>): void;
295
509
  /**
296
- * Normalize a wiki-link target to an ID.
297
- * - If it has a file extension, normalize as-is
298
- * - If no extension, add .md
299
- * - Lowercase, forward slashes
510
+ * Parse a file and optionally write a generated ID back if missing.
511
+ * Returns the node (with stable ID) and whether a write occurred.
300
512
  */
301
- private normalizeWikiLink;
302
- private hasFileExtension;
303
- private validatePathWithinSource;
513
+ private parseAndMaybeWriteId;
514
+ /**
515
+ * Write a generated ID back to file's frontmatter.
516
+ * Returns false if file was modified since originalMtime (race condition).
517
+ */
518
+ private writeIdBack;
304
519
  }
305
520
 
306
- declare class TransformersEmbeddingProvider implements EmbeddingProvider {
521
+ interface TransformersEmbeddingOptions {
522
+ model?: string;
523
+ dimensions?: number;
524
+ id?: string;
525
+ }
526
+ declare class TransformersEmbedding implements Embedding {
527
+ readonly id: string;
307
528
  private model;
308
529
  private dims;
309
530
  private pipe;
310
- constructor(model?: string, dimensions?: number);
531
+ constructor(options?: TransformersEmbeddingOptions);
311
532
  private getPipeline;
312
533
  embed(text: string): Promise<number[]>;
313
534
  embedBatch(texts: string[]): Promise<number[][]>;
314
535
  dimensions(): number;
315
536
  modelId(): string;
537
+ onRegister(): Promise<void>;
538
+ onUnregister(): Promise<void>;
316
539
  }
317
540
 
318
- declare class SqliteVectorProvider implements VectorProvider {
541
+ declare class SqliteVectorIndex implements VectorIndex {
319
542
  private db;
320
543
  private ownsDb;
544
+ private modelMismatchWarned;
321
545
  constructor(pathOrDb: string | Database);
322
546
  private init;
323
547
  store(id: string, vector: number[], model: string): Promise<void>;
@@ -334,6 +558,6 @@ declare class SqliteVectorProvider implements VectorProvider {
334
558
  close(): void;
335
559
  }
336
560
 
337
- declare const VERSION = "0.1.0";
561
+ declare const VERSION = "0.1.3";
338
562
 
339
- export { type CacheConfig, type CentralityMetrics, DEFAULT_CONFIG, type Direction, DocStore, type DocStoreConfig, type Edge, type EmbeddingConfig, type EmbeddingProvider, type GraphCore, GraphCoreImpl, type LLMConfig, type LinkInfo, type ListFilter, type ListOptions, type LocalEmbeddingConfig, type Metric, type ModelChangeBehavior, type NeighborOptions, type Node, type NodeSummary, type NodeWithContext, type OllamaEmbeddingConfig, type OllamaLLMConfig, type OpenAIEmbeddingConfig, type OpenAILLMConfig, type ProvidersConfig, type ResolveOptions, type ResolveResult, type ResolveStrategy, type RouxConfig, type SearchOptions, type SourceConfig, type SourceRef, SqliteVectorProvider, type StoreConfig, type StoreProvider, type SystemConfig, type TagMode, TransformersEmbeddingProvider, VERSION, type VectorProvider, type VectorSearchResult, isNode, isSourceRef, isVectorProvider };
563
+ export { type CacheConfig, type CentralityMetrics, DEFAULT_CONFIG, type Direction, DocStore, type DocStoreConfig, type Edge, type Embedding, type EmbeddingConfig, type GraphCore, GraphCoreImpl, type LLMConfig, type LinkInfo, type ListFilter, type ListOptions, type LocalEmbeddingConfig, type Metric, type ModelChangeBehavior, type NeighborOptions, type Node, type NodeSummary, type NodeUpdates, type NodeWithContext, type OllamaEmbeddingConfig, type OllamaLLMConfig, type OpenAIEmbeddingConfig, type OpenAILLMConfig, type PropertySchema, type PropertyType, type ProvidersConfig, type ResolveOptions, type ResolveResult, type ResolveStrategy, type RouxConfig, type Schema, type SearchOptions, type SourceConfig, type SourceRef, SqliteVectorIndex, type Store, type StoreConfig, StoreProvider, type StoreProviderOptions, type SystemConfig, type TagMode, TransformersEmbedding, VERSION, type VectorIndex, type VectorSearchResult, createGuard, isNode, isSourceRef, isVectorIndex };