@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.
- package/dist/Orchestrator.d.ts +13 -0
- package/dist/Orchestrator.d.ts.map +1 -1
- package/dist/Orchestrator.js +84 -2
- package/dist/Orchestrator.js.map +1 -1
- package/dist/ParallelAnalysisRunner.d.ts.map +1 -1
- package/dist/ParallelAnalysisRunner.js +28 -41
- package/dist/ParallelAnalysisRunner.js.map +1 -1
- package/dist/PhaseRunner.d.ts +2 -0
- package/dist/PhaseRunner.d.ts.map +1 -1
- package/dist/PhaseRunner.js +5 -1
- package/dist/PhaseRunner.js.map +1 -1
- package/dist/core/IncrementalReanalyzer.d.ts +3 -3
- package/dist/core/IncrementalReanalyzer.d.ts.map +1 -1
- package/dist/core/IncrementalReanalyzer.js +12 -12
- package/dist/core/IncrementalReanalyzer.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/analysis/JSASTAnalyzer.d.ts.map +1 -1
- package/dist/plugins/analysis/JSASTAnalyzer.js +20 -4
- package/dist/plugins/analysis/JSASTAnalyzer.js.map +1 -1
- package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/GraphBuilder.js +8 -0
- package/dist/plugins/analysis/ast/GraphBuilder.js.map +1 -1
- package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.js +4 -2
- package/dist/plugins/analysis/ast/builders/ModuleRuntimeBuilder.js.map +1 -1
- package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.js +2 -1
- package/dist/plugins/analysis/ast/handlers/NewExpressionHandler.js.map +1 -1
- package/dist/plugins/analysis/ast/types.d.ts +3 -0
- package/dist/plugins/analysis/ast/types.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.d.ts.map +1 -1
- package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js +6 -2
- package/dist/plugins/analysis/ast/visitors/ImportExportVisitor.js.map +1 -1
- package/dist/plugins/enrichment/ImportExportLinker.d.ts.map +1 -1
- package/dist/plugins/enrichment/ImportExportLinker.js +15 -5
- package/dist/plugins/enrichment/ImportExportLinker.js.map +1 -1
- package/dist/storage/backends/RFDBServerBackend.d.ts +21 -7
- package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
- package/dist/storage/backends/RFDBServerBackend.js +48 -48
- package/dist/storage/backends/RFDBServerBackend.js.map +1 -1
- package/dist/utils/startRfdbServer.d.ts +44 -0
- package/dist/utils/startRfdbServer.d.ts.map +1 -0
- package/dist/utils/startRfdbServer.js +79 -0
- package/dist/utils/startRfdbServer.js.map +1 -0
- package/package.json +3 -3
- package/src/Orchestrator.ts +91 -2
- package/src/ParallelAnalysisRunner.ts +30 -48
- package/src/PhaseRunner.ts +7 -1
- package/src/core/IncrementalReanalyzer.ts +15 -15
- package/src/index.ts +4 -0
- package/src/plugins/analysis/JSASTAnalyzer.ts +27 -4
- package/src/plugins/analysis/ast/GraphBuilder.ts +9 -0
- package/src/plugins/analysis/ast/builders/ModuleRuntimeBuilder.ts +4 -2
- package/src/plugins/analysis/ast/handlers/NewExpressionHandler.ts +2 -1
- package/src/plugins/analysis/ast/types.ts +3 -0
- package/src/plugins/analysis/ast/visitors/ImportExportVisitor.ts +8 -2
- package/src/plugins/enrichment/ImportExportLinker.ts +15 -5
- package/src/storage/backends/RFDBServerBackend.ts +52 -60
- 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
|
|
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,
|
|
127
|
-
unlinkSync(socketPath);
|
|
132
|
+
this.logger.debug('Stale socket found, will be removed by startRfdbServer');
|
|
128
133
|
}
|
|
129
134
|
}
|
|
130
135
|
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
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 =
|
|
148
|
-
|
|
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
|
|
package/src/PhaseRunner.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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',
|
|
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',
|
|
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',
|
|
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(
|
|
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
|
|
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,
|
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
// ===========================================================================
|