@grafema/core 0.2.11 → 0.2.12-beta

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 (62) hide show
  1. package/dist/Orchestrator.d.ts +13 -0
  2. package/dist/Orchestrator.d.ts.map +1 -1
  3. package/dist/Orchestrator.js +84 -2
  4. package/dist/Orchestrator.js.map +1 -1
  5. package/dist/ParallelAnalysisRunner.d.ts.map +1 -1
  6. package/dist/ParallelAnalysisRunner.js +28 -41
  7. package/dist/ParallelAnalysisRunner.js.map +1 -1
  8. package/dist/PhaseRunner.d.ts +2 -0
  9. package/dist/PhaseRunner.d.ts.map +1 -1
  10. package/dist/PhaseRunner.js +5 -1
  11. package/dist/PhaseRunner.js.map +1 -1
  12. package/dist/core/IncrementalReanalyzer.d.ts +3 -3
  13. package/dist/core/IncrementalReanalyzer.d.ts.map +1 -1
  14. package/dist/core/IncrementalReanalyzer.js +12 -12
  15. package/dist/core/IncrementalReanalyzer.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +2 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/plugins/analysis/JSASTAnalyzer.d.ts.map +1 -1
  21. package/dist/plugins/analysis/JSASTAnalyzer.js +20 -4
  22. package/dist/plugins/analysis/JSASTAnalyzer.js.map +1 -1
  23. package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
  24. package/dist/plugins/analysis/ast/GraphBuilder.js +8 -0
  25. package/dist/plugins/analysis/ast/GraphBuilder.js.map +1 -1
  26. package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.d.ts.map +1 -1
  27. package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.js +4 -2
  28. package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.js.map +1 -1
  29. package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.d.ts.map +1 -1
  30. package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.js +2 -1
  31. package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.js.map +1 -1
  32. package/dist/plugins/analysis/ast/types.d.ts +3 -0
  33. package/dist/plugins/analysis/ast/types.d.ts.map +1 -1
  34. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts.map +1 -1
  35. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js +6 -2
  36. package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js.map +1 -1
  37. package/dist/plugins/enrichment/ImportExportLinker.d.ts.map +1 -1
  38. package/dist/plugins/enrichment/ImportExportLinker.js +15 -5
  39. package/dist/plugins/enrichment/ImportExportLinker.js.map +1 -1
  40. package/dist/storage/backends/RFDBServerBackend.d.ts +21 -7
  41. package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
  42. package/dist/storage/backends/RFDBServerBackend.js +48 -48
  43. package/dist/storage/backends/RFDBServerBackend.js.map +1 -1
  44. package/dist/utils/startRfdbServer.d.ts +44 -0
  45. package/dist/utils/startRfdbServer.d.ts.map +1 -0
  46. package/dist/utils/startRfdbServer.js +79 -0
  47. package/dist/utils/startRfdbServer.js.map +1 -0
  48. package/package.json +3 -3
  49. package/src/Orchestrator.ts +91 -2
  50. package/src/ParallelAnalysisRunner.ts +30 -48
  51. package/src/PhaseRunner.ts +7 -1
  52. package/src/core/IncrementalReanalyzer.ts +15 -15
  53. package/src/index.ts +4 -0
  54. package/src/plugins/analysis/JSASTAnalyzer.ts +27 -4
  55. package/src/plugins/analysis/ast/GraphBuilder.ts +9 -0
  56. package/src/plugins/analysis/ast/builders/ModuleRuntimeBuilder.ts +4 -2
  57. package/src/plugins/analysis/ast/handlers/NewExpressionHandler.ts +2 -1
  58. package/src/plugins/analysis/ast/types.ts +3 -0
  59. package/src/plugins/analysis/ast/visitors/ImportExportVisitor.ts +8 -2
  60. package/src/plugins/enrichment/ImportExportLinker.ts +15 -5
  61. package/src/storage/backends/RFDBServerBackend.ts +52 -60
  62. package/src/utils/startRfdbServer.ts +126 -0
@@ -9,17 +9,17 @@
9
9
  * - Barrier: wait for all tasks before returning
10
10
  */
11
11
 
12
- import { existsSync, unlinkSync } from 'fs';
12
+ import { existsSync } from 'fs';
13
13
  import { join, dirname } from 'path';
14
- import { fileURLToPath } from 'url';
15
14
  import type { ChildProcess } from 'child_process';
16
- import { spawn, execSync } from 'child_process';
17
15
  import { setTimeout as sleep } from 'timers/promises';
18
16
  import { AnalysisQueue } from './core/AnalysisQueue.js';
19
17
  import type { Plugin } from './plugins/Plugin.js';
20
18
  import type { GraphBackend, Logger } from '@grafema/types';
21
19
  import type { ProgressCallback } from './PhaseRunner.js';
22
20
  import type { ParallelConfig, DiscoveryManifest } from './OrchestratorTypes.js';
21
+ import { findRfdbBinary } from './utils/findRfdbBinary.js';
22
+ import { startRfdbServer } from './utils/startRfdbServer.js';
23
23
 
24
24
  export class ParallelAnalysisRunner {
25
25
  private analysisQueue: AnalysisQueue | null = null;
@@ -43,10 +43,9 @@ export class ParallelAnalysisRunner {
43
43
  * - Barrier waits for all tasks before ENRICHMENT phase
44
44
  */
45
45
  async run(manifest: DiscoveryManifest): Promise<void> {
46
- const socketPath = this.parallelConfig.socketPath || '/tmp/rfdb.sock';
47
- const maxWorkers = this.parallelConfig.maxWorkers || null;
48
-
49
46
  const mainDbPath = (this.graph as unknown as { dbPath?: string }).dbPath || join(manifest.projectPath, '.grafema', 'graph.rfdb');
47
+ const socketPath = this.parallelConfig.socketPath || join(dirname(mainDbPath), 'rfdb.sock');
48
+ const maxWorkers = this.parallelConfig.maxWorkers || null;
50
49
 
51
50
  this.logger.debug('Starting queue-based parallel analysis', { database: mainDbPath });
52
51
 
@@ -67,6 +66,20 @@ export class ParallelAnalysisRunner {
67
66
  await this.analysisQueue.start();
68
67
 
69
68
  let moduleCount = 0;
69
+ let completedCount = 0;
70
+
71
+ this.analysisQueue.on('taskCompleted', ({ file, stats, duration }: { file: string; stats?: { nodes?: number }; duration: number }) => {
72
+ completedCount++;
73
+ this.onProgress({
74
+ phase: 'analysis',
75
+ currentPlugin: 'AnalysisQueue',
76
+ message: `${file.split('/').pop()} (${stats?.nodes || 0} nodes, ${duration}ms)`,
77
+ totalFiles: moduleCount,
78
+ processedFiles: completedCount,
79
+ currentService: file,
80
+ });
81
+ });
82
+
70
83
  for await (const node of this.graph.queryNodes({ type: 'MODULE' })) {
71
84
  if (!node.file?.match(/\.(js|jsx|ts|tsx|mjs|cjs)$/)) continue;
72
85
 
@@ -81,14 +94,6 @@ export class ParallelAnalysisRunner {
81
94
 
82
95
  this.logger.debug('Queued modules for analysis', { count: moduleCount });
83
96
 
84
- this.analysisQueue.on('taskCompleted', ({ file, stats, duration }: { file: string; stats?: { nodes?: number }; duration: number }) => {
85
- this.onProgress({
86
- phase: 'analysis',
87
- currentPlugin: 'AnalysisQueue',
88
- message: `${file.split('/').pop()} (${stats?.nodes || 0} nodes, ${duration}ms)`,
89
- });
90
- });
91
-
92
97
  this.analysisQueue.on('taskFailed', ({ file, error }: { file: string; error: string }) => {
93
98
  this.logger.error('Analysis failed', { file, error });
94
99
  });
@@ -111,6 +116,7 @@ export class ParallelAnalysisRunner {
111
116
  * Start RFDB server process (or connect to existing one).
112
117
  */
113
118
  private async startRfdbServer(socketPath: string, dbPath: string): Promise<void> {
119
+ // Check if server is already running
114
120
  if (existsSync(socketPath)) {
115
121
  try {
116
122
  const { RFDBClient } = await import('@grafema/rfdb-client');
@@ -123,49 +129,25 @@ export class ParallelAnalysisRunner {
123
129
  this._serverWasExternal = true;
124
130
  return;
125
131
  } catch {
126
- this.logger.debug('Stale socket found, removing');
127
- unlinkSync(socketPath);
132
+ this.logger.debug('Stale socket found, will be removed by startRfdbServer');
128
133
  }
129
134
  }
130
135
 
131
- const projectRoot = join(dirname(fileURLToPath(import.meta.url)), '../..');
132
- const serverBinary = join(projectRoot, 'packages/rfdb-server/target/release/rfdb-server');
133
- const debugBinary = join(projectRoot, 'packages/rfdb-server/target/debug/rfdb-server');
134
-
135
- let binaryPath = existsSync(serverBinary) ? serverBinary : debugBinary;
136
-
137
- if (!existsSync(binaryPath)) {
138
- this.logger.debug('RFDB server binary not found, building', { path: binaryPath });
139
- execSync('cargo build --bin rfdb-server', {
140
- cwd: join(projectRoot, 'packages/rfdb-server'),
141
- stdio: 'inherit',
142
- });
143
- binaryPath = debugBinary;
136
+ const binaryPath = findRfdbBinary();
137
+ if (!binaryPath) {
138
+ throw new Error('RFDB server binary not found');
144
139
  }
145
140
 
146
141
  this.logger.debug('Starting RFDB server', { binary: binaryPath, database: dbPath });
147
- this.rfdbServerProcess = spawn(binaryPath, [dbPath, '--socket', socketPath], {
148
- stdio: ['ignore', 'pipe', 'pipe'],
142
+ this.rfdbServerProcess = await startRfdbServer({
143
+ dbPath,
144
+ socketPath,
145
+ binaryPath,
146
+ waitTimeoutMs: 3000,
147
+ logger: { debug: (m: string) => this.logger.debug(m) },
149
148
  });
150
149
  this._serverWasExternal = false;
151
150
 
152
- this.rfdbServerProcess.stderr?.on('data', (data: Buffer) => {
153
- const msg = data.toString().trim();
154
- if (!msg.includes('FLUSH') && !msg.includes('WRITER')) {
155
- this.logger.debug('rfdb-server', { message: msg });
156
- }
157
- });
158
-
159
- let attempts = 0;
160
- while (!existsSync(socketPath) && attempts < 30) {
161
- await sleep(100);
162
- attempts++;
163
- }
164
-
165
- if (!existsSync(socketPath)) {
166
- throw new Error('RFDB server failed to start');
167
- }
168
-
169
151
  this.logger.debug('RFDB server started', { socketPath });
170
152
  }
171
153
 
@@ -50,6 +50,8 @@ export interface PhaseRunnerDeps {
50
50
  resourceRegistry: ResourceRegistry;
51
51
  configServices?: ServiceDefinition[];
52
52
  routing?: RoutingRule[];
53
+ /** When true, commitBatch defers index rebuild for bulk load (REG-487). */
54
+ deferIndexing?: boolean;
53
55
  }
54
56
 
55
57
  export class PhaseRunner {
@@ -92,7 +94,9 @@ export class PhaseRunner {
92
94
  graph.beginBatch();
93
95
  try {
94
96
  const result = await plugin.execute(pluginContext);
95
- const delta = await graph.commitBatch(tags);
97
+ const deferIndex = pluginContext.deferIndexing ?? false;
98
+ const protectedTypes = phaseName === 'ANALYSIS' ? ['MODULE'] : undefined;
99
+ const delta = await graph.commitBatch(tags, deferIndex, protectedTypes);
96
100
  return { result, delta };
97
101
  } catch (error) {
98
102
  graph.abortBatch();
@@ -121,6 +125,8 @@ export class PhaseRunner {
121
125
  rootPrefix: (baseContext as { rootPrefix?: string }).rootPrefix,
122
126
  // REG-256: Pass resource registry for inter-plugin communication
123
127
  resources: resourceRegistry,
128
+ // REG-487: Pass deferIndexing flag for bulk load optimization
129
+ deferIndexing: this.deps.deferIndexing ?? false,
124
130
  };
125
131
 
126
132
  // REG-256: Ensure config is available with routing and services for all plugins
@@ -25,9 +25,9 @@ export interface ReanalysisOptions {
25
25
 
26
26
  export interface ReanalysisProgress {
27
27
  phase: 'clearing' | 'indexing' | 'analysis' | 'enrichment';
28
- current: number;
29
- total: number;
30
- currentFile?: string;
28
+ processedFiles: number;
29
+ totalFiles: number;
30
+ currentService?: string;
31
31
  }
32
32
 
33
33
  export interface ReanalysisResult {
@@ -78,9 +78,9 @@ export class IncrementalReanalyzer {
78
78
  if (options.onProgress) {
79
79
  options.onProgress({
80
80
  phase: 'clearing',
81
- current: i + 1,
82
- total: staleModules.length,
83
- currentFile: module.file
81
+ processedFiles: i + 1,
82
+ totalFiles: staleModules.length,
83
+ currentService: module.file
84
84
  });
85
85
  }
86
86
  const cleared = await clearFileNodesIfNeeded(this.graph, module.file, touchedFiles);
@@ -98,9 +98,9 @@ export class IncrementalReanalyzer {
98
98
  if (options.onProgress) {
99
99
  options.onProgress({
100
100
  phase: 'indexing',
101
- current: i + 1,
102
- total: modifiedModules.length,
103
- currentFile: module.file
101
+ processedFiles: i + 1,
102
+ totalFiles: modifiedModules.length,
103
+ currentService: module.file
104
104
  });
105
105
  }
106
106
 
@@ -127,9 +127,9 @@ export class IncrementalReanalyzer {
127
127
  if (options.onProgress) {
128
128
  options.onProgress({
129
129
  phase: 'analysis',
130
- current: i + 1,
131
- total: modulesToAnalyze.length,
132
- currentFile: module.file
130
+ processedFiles: i + 1,
131
+ totalFiles: modulesToAnalyze.length,
132
+ currentService: module.file
133
133
  });
134
134
  }
135
135
 
@@ -150,7 +150,7 @@ export class IncrementalReanalyzer {
150
150
  // STEP 4: Re-run enrichment plugins
151
151
  if (!options.skipEnrichment && modulesToAnalyze.length > 0) {
152
152
  if (options.onProgress) {
153
- options.onProgress({ phase: 'enrichment', current: 0, total: 2 });
153
+ options.onProgress({ phase: 'enrichment', processedFiles: 0, totalFiles: 2 });
154
154
  }
155
155
 
156
156
  const pluginContext: PluginContext = {
@@ -169,7 +169,7 @@ export class IncrementalReanalyzer {
169
169
  }
170
170
 
171
171
  if (options.onProgress) {
172
- options.onProgress({ phase: 'enrichment', current: 1, total: 2 });
172
+ options.onProgress({ phase: 'enrichment', processedFiles: 1, totalFiles: 2 });
173
173
  }
174
174
 
175
175
  const importExportLinker = new ImportExportLinker();
@@ -182,7 +182,7 @@ export class IncrementalReanalyzer {
182
182
  }
183
183
 
184
184
  if (options.onProgress) {
185
- options.onProgress({ phase: 'enrichment', current: 2, total: 2 });
185
+ options.onProgress({ phase: 'enrichment', processedFiles: 2, totalFiles: 2 });
186
186
  }
187
187
  }
188
188
 
package/src/index.ts CHANGED
@@ -129,6 +129,10 @@ export { calculateFileHash, calculateFileHashAsync, calculateContentHash } from
129
129
  export { findRfdbBinary, getBinaryNotFoundMessage, getPlatformDir } from './utils/findRfdbBinary.js';
130
130
  export type { FindBinaryOptions } from './utils/findRfdbBinary.js';
131
131
 
132
+ // RFDB server lifecycle (RFD-40)
133
+ export { startRfdbServer } from './utils/startRfdbServer.js';
134
+ export type { StartRfdbServerOptions } from './utils/startRfdbServer.js';
135
+
132
136
  // Module resolution utilities (REG-320)
133
137
  export {
134
138
  resolveModulePath,
@@ -383,14 +383,21 @@ export class JSASTAnalyzer extends Plugin {
383
383
  const queue = new PriorityQueue();
384
384
  const pool = new WorkerPool(context.workerCount || 10);
385
385
 
386
+ const deferIndex = context.deferIndexing ?? false;
387
+
386
388
  pool.registerHandler('ANALYZE_MODULE', async (task) => {
387
389
  // Per-module batch: commit after each module to avoid buffering the entire
388
390
  // graph in memory. Prevents connection timeouts on large codebases.
391
+ // REG-487: Pass deferIndex to skip per-commit index rebuild during bulk load.
389
392
  if (graph.beginBatch && graph.commitBatch) {
390
393
  graph.beginBatch();
391
394
  try {
392
395
  const result = await this.analyzeModule(task.data.module, graph, projectPath);
393
- await graph.commitBatch(['JSASTAnalyzer', 'ANALYSIS', task.data.module.file]);
396
+ await graph.commitBatch(
397
+ ['JSASTAnalyzer', 'ANALYSIS', task.data.module.file],
398
+ deferIndex,
399
+ ['MODULE'],
400
+ );
394
401
  return result;
395
402
  } catch (err) {
396
403
  if (graph.abortBatch) graph.abortBatch();
@@ -422,7 +429,8 @@ export class JSASTAnalyzer extends Plugin {
422
429
  currentPlugin: 'JSASTAnalyzer',
423
430
  message: `Analyzing ${currentFile} (${completed}/${modulesToAnalyze.length})`,
424
431
  totalFiles: modulesToAnalyze.length,
425
- processedFiles: completed
432
+ processedFiles: completed,
433
+ currentService: currentFile
426
434
  });
427
435
  }
428
436
  }, 500);
@@ -443,6 +451,14 @@ export class JSASTAnalyzer extends Plugin {
443
451
 
444
452
  clearInterval(progressInterval);
445
453
 
454
+ // REG-487: Rebuild indexes after all deferred commits.
455
+ // This runs inside JSASTAnalyzer.execute() so downstream ANALYSIS plugins
456
+ // (which depend on JSASTAnalyzer) see rebuilt indexes.
457
+ if (deferIndex && graph.rebuildIndexes) {
458
+ logger.info('Rebuilding indexes after deferred bulk load...');
459
+ await graph.rebuildIndexes();
460
+ }
461
+
446
462
  if (context.onProgress) {
447
463
  context.onProgress({
448
464
  phase: 'analysis',
@@ -557,7 +573,8 @@ export class JSASTAnalyzer extends Plugin {
557
573
  currentPlugin: 'JSASTAnalyzer',
558
574
  message: `Processed ${result.module.name}`,
559
575
  totalFiles: modules.length,
560
- processedFiles: results.indexOf(result) + 1
576
+ processedFiles: results.indexOf(result) + 1,
577
+ currentService: result.module.file || result.module.name
561
578
  });
562
579
  }
563
580
  }
@@ -1719,6 +1736,11 @@ export class JSASTAnalyzer extends Plugin {
1719
1736
  if (processedConstructorCalls.has(nodeKey)) {
1720
1737
  return;
1721
1738
  }
1739
+
1740
+ // Skip in-function calls — handled by NewExpressionHandler in analyzeFunctionBody
1741
+ const functionParent = newPath.getFunctionParent();
1742
+ if (functionParent) return;
1743
+
1722
1744
  processedConstructorCalls.add(nodeKey);
1723
1745
 
1724
1746
  // Determine className from callee
@@ -1742,7 +1764,8 @@ export class JSASTAnalyzer extends Plugin {
1742
1764
  isBuiltin,
1743
1765
  file: module.file,
1744
1766
  line,
1745
- column
1767
+ column,
1768
+ parentScopeId: module.id
1746
1769
  });
1747
1770
 
1748
1771
  // REG-334: If this is Promise constructor with executor callback,
@@ -311,6 +311,15 @@ export class GraphBuilder {
311
311
  line: constructorCall.line,
312
312
  column: constructorCall.column
313
313
  } as GraphNode);
314
+
315
+ // SCOPE -> CONTAINS -> CONSTRUCTOR_CALL
316
+ if (constructorCall.parentScopeId) {
317
+ this._bufferEdge({
318
+ type: 'CONTAINS',
319
+ src: constructorCall.parentScopeId,
320
+ dst: constructorCall.id
321
+ });
322
+ }
314
323
  }
315
324
 
316
325
  // Phase 2: Delegate to domain builders
@@ -52,7 +52,7 @@ export class ModuleRuntimeBuilder implements DomainBuilder {
52
52
 
53
53
  private bufferImportNodes(module: ModuleNode, imports: ImportInfo[]): void {
54
54
  for (const imp of imports) {
55
- const { source, specifiers, line, column, isDynamic, isResolvable, dynamicPath } = imp;
55
+ const { source, specifiers, line, column, importKind, isDynamic, isResolvable, dynamicPath } = imp;
56
56
 
57
57
  // REG-273: Handle side-effect-only imports (no specifiers)
58
58
  if (specifiers.length === 0) {
@@ -66,7 +66,8 @@ export class ModuleRuntimeBuilder implements DomainBuilder {
66
66
  {
67
67
  imported: '*', // no specific export
68
68
  local: source, // source becomes local
69
- sideEffect: true // mark as side-effect import
69
+ sideEffect: true, // mark as side-effect import
70
+ importBinding: importKind || 'value'
70
71
  }
71
72
  );
72
73
 
@@ -110,6 +111,7 @@ export class ModuleRuntimeBuilder implements DomainBuilder {
110
111
  imported: spec.imported,
111
112
  local: spec.local,
112
113
  sideEffect: false, // regular imports are not side-effects
114
+ importBinding: spec.importKind === 'type' ? 'type' : (importKind || 'value'),
113
115
  // importType is auto-detected from imported field
114
116
  // Dynamic import fields
115
117
  isDynamic,
@@ -47,7 +47,8 @@ export class NewExpressionHandler extends FunctionBodyHandler {
47
47
  isBuiltin,
48
48
  file: ctx.module.file,
49
49
  line,
50
- column
50
+ column,
51
+ parentScopeId: ctx.getCurrentScopeId()
51
52
  });
52
53
 
53
54
  // REG-334: If this is Promise constructor with executor callback,
@@ -338,6 +338,7 @@ export interface ConstructorCallInfo {
338
338
  file: string;
339
339
  line: number;
340
340
  column: number;
341
+ parentScopeId?: string;
341
342
  }
342
343
 
343
344
  // === CLASS DECLARATION INFO ===
@@ -501,6 +502,7 @@ export interface ImportInfo {
501
502
  line: number;
502
503
  column?: number; // Column position for ImportNode
503
504
  specifiers: ImportSpecifier[];
505
+ importKind?: 'value' | 'type' | 'typeof'; // TypeScript: import type { ... }
504
506
  isDynamic?: boolean; // true for dynamic import() expressions
505
507
  isResolvable?: boolean; // true if path is a string literal (statically analyzable)
506
508
  dynamicPath?: string; // original expression for template/variable paths
@@ -509,6 +511,7 @@ export interface ImportInfo {
509
511
  export interface ImportSpecifier {
510
512
  imported: string; // имя в экспортируемом модуле (default, *, или имя)
511
513
  local: string; // имя в текущем модуле
514
+ importKind?: 'value' | 'type' | 'typeof'; // specifier-level: import { type X } from '...'
512
515
  }
513
516
 
514
517
  // === EXPORT INFO ===
@@ -41,6 +41,7 @@ export type ExtractVariableNamesCallback = (pattern: Node) => VariableInfo[];
41
41
  interface ImportSpecifierInfo {
42
42
  imported: string;
43
43
  local: string;
44
+ importKind?: 'value' | 'type' | 'typeof'; // specifier-level: import { type X } from '...'
44
45
  }
45
46
 
46
47
  /**
@@ -51,6 +52,7 @@ interface ImportInfo {
51
52
  specifiers: ImportSpecifierInfo[];
52
53
  line: number;
53
54
  column?: number;
55
+ importKind?: 'value' | 'type' | 'typeof'; // TypeScript: import type { ... }
54
56
  isDynamic?: boolean; // true for dynamic import() expressions
55
57
  isResolvable?: boolean; // true if path is a string literal (statically analyzable)
56
58
  dynamicPath?: string; // original expression for template/variable paths
@@ -106,13 +108,16 @@ export class ImportExportVisitor extends ASTVisitor {
106
108
  node.specifiers.forEach((spec) => {
107
109
  if (spec.type === 'ImportSpecifier') {
108
110
  // import { foo, bar } from './module'
111
+ // import { type Foo, bar } from './module' (specifier-level type)
109
112
  const importSpec = spec as ImportSpecifier;
110
113
  const importedName = importSpec.imported.type === 'Identifier'
111
114
  ? importSpec.imported.name
112
115
  : importSpec.imported.value;
116
+ const specKind = (importSpec as ImportSpecifier & { importKind?: string }).importKind;
113
117
  specifiers.push({
114
118
  imported: importedName,
115
- local: importSpec.local.name
119
+ local: importSpec.local.name,
120
+ importKind: specKind as ImportSpecifierInfo['importKind']
116
121
  });
117
122
  } else if (spec.type === 'ImportDefaultSpecifier') {
118
123
  // import foo from './module'
@@ -135,7 +140,8 @@ export class ImportExportVisitor extends ASTVisitor {
135
140
  source,
136
141
  specifiers,
137
142
  line: getLine(node),
138
- column: getColumn(node)
143
+ column: getColumn(node),
144
+ importKind: (node as ImportDeclaration & { importKind?: string }).importKind as ImportInfo['importKind']
139
145
  });
140
146
  },
141
147
 
@@ -102,20 +102,30 @@ export class ImportExportLinker extends Plugin {
102
102
  const currentDir = dirname(imp.file!);
103
103
  const basePath = join(currentDir, imp.source!);
104
104
 
105
- // Try different extensions
105
+ // Build candidate paths: append extensions + TypeScript .js→.ts redirect
106
+ const candidates: string[] = [];
106
107
  const extensions = ['', '.js', '.ts', '.jsx', '.tsx', '/index.js', '/index.ts'];
108
+ for (const ext of extensions) {
109
+ candidates.push(basePath + ext);
110
+ }
111
+ // TypeScript convention: imports use .js but actual files are .ts
112
+ if (basePath.endsWith('.js')) {
113
+ candidates.push(basePath.slice(0, -3) + '.ts');
114
+ candidates.push(basePath.slice(0, -3) + '.tsx');
115
+ } else if (basePath.endsWith('.jsx')) {
116
+ candidates.push(basePath.slice(0, -4) + '.tsx');
117
+ }
118
+
107
119
  let targetFile: string | null = null;
108
120
  let targetExports: Map<string, ExportNode> | null = null;
109
121
 
110
- for (const ext of extensions) {
111
- const testPath = basePath + ext;
122
+ for (const testPath of candidates) {
112
123
  if (exportIndex.has(testPath)) {
113
124
  targetFile = testPath;
114
125
  targetExports = exportIndex.get(testPath)!;
115
126
  break;
116
127
  }
117
- // Also check modulesByFile in case exports haven't been indexed yet
118
- if (!targetFile && modulesByFile.has(testPath)) {
128
+ if (modulesByFile.has(testPath)) {
119
129
  targetFile = testPath;
120
130
  targetExports = exportIndex.get(testPath) || new Map();
121
131
  break;
@@ -18,15 +18,14 @@
18
18
  * await backend.flush();
19
19
  */
20
20
 
21
- import { RFDBClient } from '@grafema/rfdb-client';
22
- import { existsSync, unlinkSync } from 'fs';
23
- import { spawn, type ChildProcess } from 'child_process';
21
+ import { RFDBClient, type BatchHandle } from '@grafema/rfdb-client';
22
+ import type { ChildProcess } from 'child_process';
24
23
  import { join, dirname } from 'path';
25
- import { setTimeout as sleep } from 'timers/promises';
26
24
 
27
25
  import type { WireNode, WireEdge, FieldDeclaration, CommitDelta, AttrQuery as RFDBAttrQuery } from '@grafema/types';
28
26
  import type { NodeType, EdgeType } from '@grafema/types';
29
- import { findRfdbBinary } from '../../utils/findRfdbBinary.js';
27
+ import { startRfdbServer } from '../../utils/startRfdbServer.js';
28
+ import { GRAFEMA_VERSION, getSchemaVersion } from '../../version.js';
30
29
  import type { BaseNodeRecord, EdgeRecord, AnyBrandedNode } from '@grafema/types';
31
30
  import { brandNodeInternal } from '../../core/brandNodeInternal.js';
32
31
  import type { AttrQuery, GraphStats, GraphExport } from '../../core/GraphBackend.js';
@@ -201,66 +200,19 @@ export class RFDBServerBackend {
201
200
  }
202
201
 
203
202
  /**
204
- * Find RFDB server binary using shared utility.
205
- * Delegates to findRfdbBinary() for consistent search across all entry points.
206
- */
207
- private _findServerBinary(): string | null {
208
- const binaryPath = findRfdbBinary();
209
- if (binaryPath) {
210
- this.log(`[RFDBServerBackend] Found binary: ${binaryPath}`);
211
- }
212
- return binaryPath;
213
- }
214
-
215
- /**
216
- * Start RFDB server process
203
+ * Start RFDB server process using shared utility.
217
204
  */
218
205
  private async _startServer(): Promise<void> {
219
206
  if (!this.dbPath) {
220
207
  throw new Error('dbPath required to start RFDB server');
221
208
  }
222
209
 
223
- // Find server binary - check multiple locations
224
- const binaryPath = this._findServerBinary();
225
- if (!binaryPath) {
226
- throw new Error(
227
- 'RFDB server binary not found.\n' +
228
- 'Install @grafema/rfdb: npm install @grafema/rfdb\n' +
229
- 'Or build from source: cargo build --release --bin rfdb-server'
230
- );
231
- }
232
-
233
- // Remove stale socket
234
- if (existsSync(this.socketPath)) {
235
- unlinkSync(this.socketPath);
236
- }
237
-
238
- this.log(`[RFDBServerBackend] Starting: ${binaryPath} ${this.dbPath} --socket ${this.socketPath}`);
239
-
240
- this.serverProcess = spawn(binaryPath, [this.dbPath, '--socket', this.socketPath], {
241
- stdio: ['ignore', 'ignore', 'inherit'], // stdin/stdout ignored, stderr inherited
242
- detached: true, // Allow server to outlive this process
243
- });
244
-
245
- // Don't let server process prevent parent from exiting
246
- this.serverProcess.unref();
247
-
248
- this.serverProcess.on('error', (err: Error) => {
249
- this.logError(`[RFDBServerBackend] Server process error:`, err);
210
+ await startRfdbServer({
211
+ dbPath: this.dbPath,
212
+ socketPath: this.socketPath,
213
+ waitTimeoutMs: 5000,
214
+ logger: this.silent ? undefined : { debug: (m: string) => this.log(m) },
250
215
  });
251
-
252
- // Wait for socket to appear
253
- let attempts = 0;
254
- while (!existsSync(this.socketPath) && attempts < 50) {
255
- await sleep(100);
256
- attempts++;
257
- }
258
-
259
- if (!existsSync(this.socketPath)) {
260
- throw new Error(`RFDB server failed to start (socket not created after ${attempts * 100}ms)`);
261
- }
262
-
263
- this.log(`[RFDBServerBackend] Server started on ${this.socketPath}`);
264
216
  }
265
217
 
266
218
  /**
@@ -274,9 +226,28 @@ export class RFDBServerBackend {
274
226
  try {
275
227
  const hello = await this.client.hello(3);
276
228
  this.protocolVersion = hello.protocolVersion;
229
+ this._checkServerVersion(hello.serverVersion);
277
230
  } catch {
278
231
  // Server predates hello command or doesn't support v3 — safe v2 fallback
279
232
  this.protocolVersion = 2;
233
+ this.log('[RFDBServerBackend] WARNING: Server does not support version negotiation. Consider updating rfdb-server.');
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Validate server version against expected client version.
239
+ * Warns on mismatch but never fails — version differences are informational.
240
+ */
241
+ private _checkServerVersion(serverVersion: string): void {
242
+ if (!serverVersion) return;
243
+ const expected = getSchemaVersion(GRAFEMA_VERSION);
244
+ const actual = getSchemaVersion(serverVersion);
245
+ if (actual !== expected) {
246
+ this.log(
247
+ `[RFDBServerBackend] WARNING: rfdb-server version mismatch — ` +
248
+ `server v${serverVersion}, expected v${GRAFEMA_VERSION}. ` +
249
+ `Update with: grafema server restart`
250
+ );
280
251
  }
281
252
  }
282
253
 
@@ -769,10 +740,13 @@ export class RFDBServerBackend {
769
740
  /**
770
741
  * Commit the current batch to the server atomically.
771
742
  * Returns a CommitDelta describing what changed.
743
+ *
744
+ * @param tags - Optional tags for the commit
745
+ * @param deferIndex - When true, server writes data but skips index rebuild.
772
746
  */
773
- async commitBatch(tags?: string[]): Promise<CommitDelta> {
747
+ async commitBatch(tags?: string[], deferIndex?: boolean, protectedTypes?: string[]): Promise<CommitDelta> {
774
748
  if (!this.client) throw new Error('Not connected to RFDB server');
775
- return this.client.commitBatch(tags);
749
+ return this.client.commitBatch(tags, deferIndex, protectedTypes);
776
750
  }
777
751
 
778
752
  /**
@@ -822,6 +796,24 @@ export class RFDBServerBackend {
822
796
  this.client.abortBatch();
823
797
  }
824
798
 
799
+ /**
800
+ * Rebuild all secondary indexes after deferred-index commits (REG-487).
801
+ * Call this once after a series of commitBatch(tags, true) calls.
802
+ */
803
+ async rebuildIndexes(): Promise<void> {
804
+ if (!this.client) throw new Error('Not connected to RFDB server');
805
+ await this.client.rebuildIndexes();
806
+ }
807
+
808
+ /**
809
+ * Create an isolated batch handle for concurrent-safe batching (REG-487).
810
+ * Each handle has its own buffers — safe for parallel workers.
811
+ */
812
+ createBatch(): BatchHandle {
813
+ if (!this.client) throw new Error('Not connected to RFDB server');
814
+ return this.client.createBatch();
815
+ }
816
+
825
817
  // ===========================================================================
826
818
  // Export/Import
827
819
  // ===========================================================================