@kiyeonjeon21/datacontext 0.3.2 → 0.4.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.
Files changed (52) hide show
  1. package/dist/adapters/sqlite.d.ts.map +1 -1
  2. package/dist/adapters/sqlite.js +13 -0
  3. package/dist/adapters/sqlite.js.map +1 -1
  4. package/dist/api/server.d.ts.map +1 -1
  5. package/dist/api/server.js +115 -0
  6. package/dist/api/server.js.map +1 -1
  7. package/dist/cli/index.js +58 -14
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/core/context-service.d.ts +63 -0
  10. package/dist/core/context-service.d.ts.map +1 -1
  11. package/dist/core/context-service.js +66 -0
  12. package/dist/core/context-service.js.map +1 -1
  13. package/dist/core/harvester.d.ts +57 -5
  14. package/dist/core/harvester.d.ts.map +1 -1
  15. package/dist/core/harvester.js +86 -6
  16. package/dist/core/harvester.js.map +1 -1
  17. package/dist/core/types.d.ts +21 -5
  18. package/dist/core/types.d.ts.map +1 -1
  19. package/dist/index.d.ts +2 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +9 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/knowledge/store.d.ts +186 -3
  24. package/dist/knowledge/store.d.ts.map +1 -1
  25. package/dist/knowledge/store.js +389 -5
  26. package/dist/knowledge/store.js.map +1 -1
  27. package/dist/knowledge/types.d.ts +252 -4
  28. package/dist/knowledge/types.d.ts.map +1 -1
  29. package/dist/knowledge/types.js +138 -1
  30. package/dist/knowledge/types.js.map +1 -1
  31. package/dist/mcp/tools.d.ts.map +1 -1
  32. package/dist/mcp/tools.js +231 -3
  33. package/dist/mcp/tools.js.map +1 -1
  34. package/docs/KNOWLEDGE_GRAPH.md +540 -0
  35. package/docs/KNOWLEDGE_TYPES.md +261 -0
  36. package/docs/MULTI_DB_ARCHITECTURE.md +319 -0
  37. package/package.json +1 -1
  38. package/scripts/create-sqlite-testdb.sh +75 -0
  39. package/scripts/test-databases.sh +324 -0
  40. package/sqlite:./test-sqlite.db +0 -0
  41. package/src/adapters/sqlite.ts +16 -0
  42. package/src/api/server.ts +134 -0
  43. package/src/cli/index.ts +57 -16
  44. package/src/core/context-service.ts +70 -0
  45. package/src/core/harvester.ts +120 -8
  46. package/src/core/types.ts +21 -5
  47. package/src/index.ts +19 -1
  48. package/src/knowledge/store.ts +480 -6
  49. package/src/knowledge/types.ts +321 -4
  50. package/src/mcp/tools.ts +273 -3
  51. package/test-sqlite.db +0 -0
  52. package/tests/knowledge-store.test.ts +130 -0
@@ -1,6 +1,44 @@
1
1
  /**
2
2
  * Knowledge Store
3
- * Local JSON-based storage for table descriptions, query examples, and business rules
3
+ *
4
+ * Local JSON-based storage for the Knowledge Graph, including:
5
+ * - Table descriptions (nodes)
6
+ * - Table relationships (edges)
7
+ * - Query examples, business rules, and terms (annotations)
8
+ *
9
+ * ## Architecture
10
+ *
11
+ * The Knowledge Store implements a lightweight graph model:
12
+ *
13
+ * ```
14
+ * ┌─────────────────────────────────────────────────────────────────┐
15
+ * │ Knowledge Store │
16
+ * │ │
17
+ * │ Nodes: │
18
+ * │ - TableDescription (with embedded columns) │
19
+ * │ │
20
+ * │ Edges: │
21
+ * │ - TableRelationship (from → to with join condition) │
22
+ * │ │
23
+ * │ Annotations: │
24
+ * │ - QueryExample, BusinessRule, BusinessTerm │
25
+ * │ │
26
+ * └─────────────────────────────────────────────────────────────────┘
27
+ * ```
28
+ *
29
+ * ## Graph Traversal
30
+ *
31
+ * Provides methods for:
32
+ * - Finding related tables (1-hop)
33
+ * - Finding shortest join path (multi-hop BFS)
34
+ * - Detecting relationship types
35
+ *
36
+ * ## Future: SQLite Migration
37
+ *
38
+ * For larger graphs, consider migrating to SQLite embedded storage
39
+ * for better indexing and query performance.
40
+ *
41
+ * @module knowledge/store
4
42
  */
5
43
 
6
44
  import { readFile, writeFile, mkdir } from 'fs/promises';
@@ -10,16 +48,27 @@ import { homedir } from 'os';
10
48
  import type {
11
49
  KnowledgeData,
12
50
  TableDescription,
51
+ TableRelationship,
52
+ TableRef,
13
53
  QueryExample,
14
54
  BusinessRule,
15
55
  BusinessTerm,
16
56
  KnowledgeEntry,
17
57
  ColumnDescription,
18
58
  TermCategory,
59
+ RelationshipType,
60
+ RelationshipCardinality,
61
+ } from './types.js';
62
+ import {
63
+ generateId,
64
+ createKnowledgeMeta,
65
+ createTableRef,
66
+ tableRefToString,
67
+ tableRefsEqual,
19
68
  } from './types.js';
20
- import { generateId, createKnowledgeMeta } from './types.js';
21
69
 
22
- const KNOWLEDGE_VERSION = '1.0.0';
70
+ /** Current knowledge format version */
71
+ const KNOWLEDGE_VERSION = '2.0.0';
23
72
 
24
73
  export class KnowledgeStore {
25
74
  private dataPath: string;
@@ -42,7 +91,9 @@ export class KnowledgeStore {
42
91
  }
43
92
 
44
93
  /**
45
- * Load knowledge data from disk
94
+ * Load knowledge data from disk.
95
+ *
96
+ * Includes automatic migration for older data formats.
46
97
  */
47
98
  async load(): Promise<void> {
48
99
  if (existsSync(this.dataPath)) {
@@ -50,10 +101,18 @@ export class KnowledgeStore {
50
101
  const content = await readFile(this.dataPath, 'utf-8');
51
102
  const parsed = JSON.parse(content) as KnowledgeData;
52
103
 
53
- // Migration: ensure businessTerms array exists (for old data files)
104
+ // Migration: ensure arrays exist (for old data files)
54
105
  if (!parsed.businessTerms) {
55
106
  parsed.businessTerms = [];
56
107
  }
108
+ if (!parsed.tableRelationships) {
109
+ parsed.tableRelationships = [];
110
+ }
111
+
112
+ // Update version if migrating
113
+ if (parsed.version !== KNOWLEDGE_VERSION) {
114
+ parsed.version = KNOWLEDGE_VERSION;
115
+ }
57
116
 
58
117
  this.data = parsed;
59
118
  } catch (error) {
@@ -224,6 +283,345 @@ export class KnowledgeStore {
224
283
  await this.save();
225
284
  }
226
285
 
286
+ // ============ Table Relationships (Graph Edges) ============
287
+
288
+ /**
289
+ * Get all table relationships.
290
+ *
291
+ * @returns All relationships in the knowledge store
292
+ */
293
+ getRelationships(): TableRelationship[] {
294
+ return this.data?.tableRelationships ?? [];
295
+ }
296
+
297
+ /**
298
+ * Get tables related to a given table (1-hop traversal).
299
+ *
300
+ * Returns all relationships where the given table is either
301
+ * the source (from) or target (to).
302
+ *
303
+ * @param tableName - Table name to find relationships for
304
+ * @param schema - Schema name (defaults to 'public')
305
+ * @returns Array of relationships involving this table
306
+ *
307
+ * @example
308
+ * ```typescript
309
+ * const related = store.getRelatedTables('orders', 'public');
310
+ * // Returns relationships like:
311
+ * // - orders → users (FK: orders.user_id → users.id)
312
+ * // - orders → products (FK: orders.product_id → products.id)
313
+ * // - order_items → orders (FK: order_items.order_id → orders.id)
314
+ * ```
315
+ */
316
+ getRelatedTables(tableName: string, schema: string = 'public'): TableRelationship[] {
317
+ return this.data?.tableRelationships.filter(
318
+ rel => (rel.from.table === tableName && rel.from.schema === schema) ||
319
+ (rel.to.table === tableName && rel.to.schema === schema)
320
+ ) ?? [];
321
+ }
322
+
323
+ /**
324
+ * Get outgoing relationships from a table (where table is the source).
325
+ *
326
+ * @param tableName - Source table name
327
+ * @param schema - Schema name
328
+ * @returns Relationships where this table is the 'from' side
329
+ */
330
+ getOutgoingRelationships(tableName: string, schema: string = 'public'): TableRelationship[] {
331
+ return this.data?.tableRelationships.filter(
332
+ rel => rel.from.table === tableName && rel.from.schema === schema
333
+ ) ?? [];
334
+ }
335
+
336
+ /**
337
+ * Get incoming relationships to a table (where table is the target).
338
+ *
339
+ * @param tableName - Target table name
340
+ * @param schema - Schema name
341
+ * @returns Relationships where this table is the 'to' side
342
+ */
343
+ getIncomingRelationships(tableName: string, schema: string = 'public'): TableRelationship[] {
344
+ return this.data?.tableRelationships.filter(
345
+ rel => rel.to.table === tableName && rel.to.schema === schema
346
+ ) ?? [];
347
+ }
348
+
349
+ /**
350
+ * Find the shortest join path between two tables.
351
+ *
352
+ * Uses BFS (Breadth-First Search) to find the shortest path
353
+ * through the relationship graph.
354
+ *
355
+ * @param fromTable - Starting table name
356
+ * @param toTable - Destination table name
357
+ * @param schema - Schema name (defaults to 'public')
358
+ * @returns Array of relationships forming the path, or empty if no path exists
359
+ *
360
+ * @example
361
+ * ```typescript
362
+ * const path = store.findJoinPath('order_items', 'customers');
363
+ * // Returns path like:
364
+ * // [
365
+ * // { from: 'order_items', to: 'orders', joinCondition: '...' },
366
+ * // { from: 'orders', to: 'customers', joinCondition: '...' }
367
+ * // ]
368
+ * ```
369
+ */
370
+ findJoinPath(
371
+ fromTable: string,
372
+ toTable: string,
373
+ schema: string = 'public'
374
+ ): TableRelationship[] {
375
+ if (fromTable === toTable) return [];
376
+
377
+ const visited = new Set<string>();
378
+ const queue: Array<{ table: string; path: TableRelationship[] }> = [
379
+ { table: fromTable, path: [] }
380
+ ];
381
+
382
+ while (queue.length > 0) {
383
+ const { table, path } = queue.shift()!;
384
+ const tableKey = `${schema}.${table}`;
385
+
386
+ if (visited.has(tableKey)) continue;
387
+ visited.add(tableKey);
388
+
389
+ // Get all relationships for current table
390
+ const relationships = this.getRelatedTables(table, schema);
391
+
392
+ for (const rel of relationships) {
393
+ // Determine the next table in the path
394
+ const isFromCurrent = rel.from.table === table && rel.from.schema === schema;
395
+ const nextTable = isFromCurrent ? rel.to.table : rel.from.table;
396
+ const nextSchema = isFromCurrent ? rel.to.schema : rel.from.schema;
397
+
398
+ // Skip if we've already visited
399
+ if (visited.has(`${nextSchema}.${nextTable}`)) continue;
400
+
401
+ const newPath = [...path, rel];
402
+
403
+ // Found the destination
404
+ if (nextTable === toTable && nextSchema === schema) {
405
+ return newPath;
406
+ }
407
+
408
+ // Add to queue for further exploration
409
+ queue.push({ table: nextTable, path: newPath });
410
+ }
411
+ }
412
+
413
+ // No path found
414
+ return [];
415
+ }
416
+
417
+ /**
418
+ * Get the relationship between two specific tables (if it exists).
419
+ *
420
+ * @param fromTable - Source table
421
+ * @param toTable - Target table
422
+ * @param schema - Schema name
423
+ * @returns Relationship if found, undefined otherwise
424
+ */
425
+ getRelationshipBetween(
426
+ fromTable: string,
427
+ toTable: string,
428
+ schema: string = 'public'
429
+ ): TableRelationship | undefined {
430
+ return this.data?.tableRelationships.find(
431
+ rel =>
432
+ (rel.from.table === fromTable && rel.to.table === toTable &&
433
+ rel.from.schema === schema && rel.to.schema === schema) ||
434
+ (rel.from.table === toTable && rel.to.table === fromTable &&
435
+ rel.from.schema === schema && rel.to.schema === schema)
436
+ );
437
+ }
438
+
439
+ /**
440
+ * Add a table relationship.
441
+ *
442
+ * @param relationship - Complete relationship object
443
+ * @returns The added relationship
444
+ */
445
+ async addRelationship(relationship: TableRelationship): Promise<TableRelationship> {
446
+ if (!this.data) {
447
+ throw new Error('Knowledge store not loaded');
448
+ }
449
+
450
+ // Initialize if needed (migration)
451
+ if (!this.data.tableRelationships) {
452
+ this.data.tableRelationships = [];
453
+ }
454
+
455
+ // Check for duplicates (same from/to tables)
456
+ const existingIndex = this.data.tableRelationships.findIndex(
457
+ r => tableRefsEqual(r.from, relationship.from) &&
458
+ tableRefsEqual(r.to, relationship.to)
459
+ );
460
+
461
+ if (existingIndex >= 0) {
462
+ // Update existing
463
+ this.data.tableRelationships[existingIndex] = {
464
+ ...this.data.tableRelationships[existingIndex],
465
+ ...relationship,
466
+ updatedAt: new Date().toISOString(),
467
+ };
468
+ await this.save();
469
+ return this.data.tableRelationships[existingIndex];
470
+ }
471
+
472
+ // Add new
473
+ this.data.tableRelationships.push(relationship);
474
+ await this.save();
475
+ return relationship;
476
+ }
477
+
478
+ /**
479
+ * Add a relationship from FK information (convenience method).
480
+ *
481
+ * @param params - FK parameters
482
+ * @returns The added relationship
483
+ *
484
+ * @example
485
+ * ```typescript
486
+ * await store.addRelationshipFromFK({
487
+ * fromTable: 'orders',
488
+ * fromColumn: 'user_id',
489
+ * toTable: 'users',
490
+ * toColumn: 'id',
491
+ * });
492
+ * ```
493
+ */
494
+ async addRelationshipFromFK(params: {
495
+ fromTable: string;
496
+ fromSchema?: string;
497
+ fromColumn: string;
498
+ toTable: string;
499
+ toSchema?: string;
500
+ toColumn: string;
501
+ constraintName?: string;
502
+ }): Promise<TableRelationship> {
503
+ if (!this.data) {
504
+ throw new Error('Knowledge store not loaded');
505
+ }
506
+
507
+ const fromSchema = params.fromSchema ?? 'public';
508
+ const toSchema = params.toSchema ?? 'public';
509
+
510
+ const relationship: TableRelationship = {
511
+ ...createKnowledgeMeta('auto', this.data.schemaHash),
512
+ type: 'table_relationship',
513
+ from: createTableRef(params.fromTable, fromSchema),
514
+ to: createTableRef(params.toTable, toSchema),
515
+ relationshipType: 'foreign_key',
516
+ joinCondition: `${params.fromTable}.${params.fromColumn} = ${params.toTable}.${params.toColumn}`,
517
+ cardinality: 'many-to-one',
518
+ fromColumns: [params.fromColumn],
519
+ toColumns: [params.toColumn],
520
+ isPreferred: true,
521
+ constraintName: params.constraintName,
522
+ };
523
+
524
+ return this.addRelationship(relationship);
525
+ }
526
+
527
+ /**
528
+ * Add multiple relationships at once (batch operation).
529
+ *
530
+ * @param relationships - Array of relationships to add
531
+ * @returns Number of relationships added
532
+ */
533
+ async addRelationships(relationships: TableRelationship[]): Promise<number> {
534
+ if (!this.data) {
535
+ throw new Error('Knowledge store not loaded');
536
+ }
537
+
538
+ if (!this.data.tableRelationships) {
539
+ this.data.tableRelationships = [];
540
+ }
541
+
542
+ let addedCount = 0;
543
+
544
+ for (const rel of relationships) {
545
+ const existingIndex = this.data.tableRelationships.findIndex(
546
+ r => tableRefsEqual(r.from, rel.from) && tableRefsEqual(r.to, rel.to)
547
+ );
548
+
549
+ if (existingIndex >= 0) {
550
+ // Update existing
551
+ this.data.tableRelationships[existingIndex] = {
552
+ ...this.data.tableRelationships[existingIndex],
553
+ ...rel,
554
+ updatedAt: new Date().toISOString(),
555
+ };
556
+ } else {
557
+ // Add new
558
+ this.data.tableRelationships.push(rel);
559
+ addedCount++;
560
+ }
561
+ }
562
+
563
+ await this.save();
564
+ return addedCount;
565
+ }
566
+
567
+ /**
568
+ * Delete a relationship by ID.
569
+ *
570
+ * @param id - Relationship ID to delete
571
+ */
572
+ async deleteRelationship(id: string): Promise<void> {
573
+ if (!this.data) {
574
+ throw new Error('Knowledge store not loaded');
575
+ }
576
+
577
+ const index = this.data.tableRelationships.findIndex(r => r.id === id);
578
+ if (index >= 0) {
579
+ this.data.tableRelationships.splice(index, 1);
580
+ await this.save();
581
+ }
582
+ }
583
+
584
+ /**
585
+ * Get a summary of the relationship graph.
586
+ *
587
+ * @returns Graph statistics
588
+ */
589
+ getGraphSummary(): {
590
+ nodeCount: number;
591
+ edgeCount: number;
592
+ tablesWithRelationships: number;
593
+ isolatedTables: number;
594
+ relationshipsByType: Record<RelationshipType, number>;
595
+ } {
596
+ const relationships = this.data?.tableRelationships ?? [];
597
+ const tables = this.data?.tableDescriptions ?? [];
598
+
599
+ // Find unique tables in relationships
600
+ const tablesInRelationships = new Set<string>();
601
+ for (const rel of relationships) {
602
+ tablesInRelationships.add(tableRefToString(rel.from));
603
+ tablesInRelationships.add(tableRefToString(rel.to));
604
+ }
605
+
606
+ // Count by type
607
+ const byType: Record<RelationshipType, number> = {
608
+ 'foreign_key': 0,
609
+ 'implicit_join': 0,
610
+ 'manual': 0,
611
+ };
612
+ for (const rel of relationships) {
613
+ byType[rel.relationshipType]++;
614
+ }
615
+
616
+ return {
617
+ nodeCount: tables.length,
618
+ edgeCount: relationships.length,
619
+ tablesWithRelationships: tablesInRelationships.size,
620
+ isolatedTables: tables.length - Math.min(tablesInRelationships.size, tables.length),
621
+ relationshipsByType: byType,
622
+ };
623
+ }
624
+
227
625
  // ============ Query Examples ============
228
626
 
229
627
  /**
@@ -542,6 +940,12 @@ export class KnowledgeStore {
542
940
  }
543
941
  }
544
942
 
943
+ for (const rel of this.data.tableRelationships ?? []) {
944
+ if (rel.schemaHash !== currentSchemaHash) {
945
+ outdated.push(rel);
946
+ }
947
+ }
948
+
545
949
  return outdated;
546
950
  }
547
951
 
@@ -656,6 +1060,7 @@ export class KnowledgeStore {
656
1060
  schemaHash: '',
657
1061
  lastSyncAt: new Date().toISOString(),
658
1062
  tableDescriptions: [],
1063
+ tableRelationships: [],
659
1064
  queryExamples: [],
660
1065
  businessRules: [],
661
1066
  businessTerms: [],
@@ -672,12 +1077,19 @@ export class KnowledgeStore {
672
1077
  description?: string;
673
1078
  columns: Array<{ name: string; description?: string }>;
674
1079
  }>;
1080
+ relationships: TableRelationship[];
675
1081
  queryExamples: QueryExample[];
676
1082
  businessRules: BusinessRule[];
677
1083
  businessTerms: BusinessTerm[];
678
1084
  } {
679
1085
  if (!this.data) {
680
- return { tables: [], queryExamples: [], businessRules: [], businessTerms: [] };
1086
+ return {
1087
+ tables: [],
1088
+ relationships: [],
1089
+ queryExamples: [],
1090
+ businessRules: [],
1091
+ businessTerms: []
1092
+ };
681
1093
  }
682
1094
 
683
1095
  return {
@@ -687,11 +1099,73 @@ export class KnowledgeStore {
687
1099
  description: td.description,
688
1100
  columns: td.columns,
689
1101
  })),
1102
+ relationships: this.data.tableRelationships ?? [],
690
1103
  queryExamples: this.data.queryExamples,
691
1104
  businessRules: this.data.businessRules,
692
1105
  businessTerms: this.data.businessTerms ?? [],
693
1106
  };
694
1107
  }
1108
+
1109
+ /**
1110
+ * Build context including relationships for AI.
1111
+ *
1112
+ * Enhanced version that includes table relationship information
1113
+ * to help AI understand how tables connect.
1114
+ *
1115
+ * @param tables - Tables to include
1116
+ * @param userQuery - Optional user query for term matching
1117
+ * @param schema - Optional schema (if not provided, searches all schemas)
1118
+ * @returns Formatted context string
1119
+ */
1120
+ buildContextWithRelationships(tables: string[], userQuery?: string, schema?: string): string {
1121
+ let context = this.buildContext(tables, userQuery);
1122
+
1123
+ // Add relationship information
1124
+ // If no schema specified, find relationships across all schemas
1125
+ const relevantRelationships: TableRelationship[] = [];
1126
+ for (const table of tables) {
1127
+ // Search relationships that involve this table (in any schema if not specified)
1128
+ const allRelationships = this.data?.tableRelationships ?? [];
1129
+ const rels = schema
1130
+ ? this.getRelatedTables(table, schema)
1131
+ : allRelationships.filter(
1132
+ rel => rel.from.table === table || rel.to.table === table
1133
+ );
1134
+ for (const rel of rels) {
1135
+ // Avoid duplicates
1136
+ if (!relevantRelationships.find(r => r.id === rel.id)) {
1137
+ relevantRelationships.push(rel);
1138
+ }
1139
+ }
1140
+ }
1141
+
1142
+ if (relevantRelationships.length > 0) {
1143
+ const parts: string[] = [
1144
+ '',
1145
+ '## Table Relationships',
1146
+ 'Use these relationships when joining tables:',
1147
+ '',
1148
+ ];
1149
+
1150
+ for (const rel of relevantRelationships) {
1151
+ const fromStr = `${rel.from.schema}.${rel.from.table}`;
1152
+ const toStr = `${rel.to.schema}.${rel.to.table}`;
1153
+ parts.push(`- **${fromStr}** → **${toStr}**`);
1154
+ parts.push(` Join: \`${rel.joinCondition}\``);
1155
+ if (rel.cardinality) {
1156
+ parts.push(` Cardinality: ${rel.cardinality}`);
1157
+ }
1158
+ if (rel.notes) {
1159
+ parts.push(` Note: ${rel.notes}`);
1160
+ }
1161
+ parts.push('');
1162
+ }
1163
+
1164
+ context += '\n' + parts.join('\n');
1165
+ }
1166
+
1167
+ return context;
1168
+ }
695
1169
  }
696
1170
 
697
1171
  /**