@grafema/core 0.2.7 → 0.2.9

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 (72) hide show
  1. package/dist/Orchestrator.d.ts +3 -1
  2. package/dist/Orchestrator.d.ts.map +1 -1
  3. package/dist/Orchestrator.js +25 -8
  4. package/dist/Orchestrator.js.map +1 -1
  5. package/dist/PhaseRunner.d.ts +4 -5
  6. package/dist/PhaseRunner.d.ts.map +1 -1
  7. package/dist/PhaseRunner.js +31 -20
  8. package/dist/PhaseRunner.js.map +1 -1
  9. package/dist/plugins/analysis/DatabaseAnalyzer.d.ts.map +1 -1
  10. package/dist/plugins/analysis/DatabaseAnalyzer.js +3 -4
  11. package/dist/plugins/analysis/DatabaseAnalyzer.js.map +1 -1
  12. package/dist/plugins/analysis/ExpressAnalyzer.d.ts.map +1 -1
  13. package/dist/plugins/analysis/ExpressAnalyzer.js +1 -0
  14. package/dist/plugins/analysis/ExpressAnalyzer.js.map +1 -1
  15. package/dist/plugins/analysis/ExpressResponseAnalyzer.d.ts.map +1 -1
  16. package/dist/plugins/analysis/ExpressResponseAnalyzer.js +11 -34
  17. package/dist/plugins/analysis/ExpressResponseAnalyzer.js.map +1 -1
  18. package/dist/plugins/analysis/ExpressRouteAnalyzer.d.ts.map +1 -1
  19. package/dist/plugins/analysis/ExpressRouteAnalyzer.js +1 -0
  20. package/dist/plugins/analysis/ExpressRouteAnalyzer.js.map +1 -1
  21. package/dist/plugins/analysis/NestJSRouteAnalyzer.d.ts.map +1 -1
  22. package/dist/plugins/analysis/NestJSRouteAnalyzer.js +1 -0
  23. package/dist/plugins/analysis/NestJSRouteAnalyzer.js.map +1 -1
  24. package/dist/plugins/analysis/ReactAnalyzer.d.ts.map +1 -1
  25. package/dist/plugins/analysis/ReactAnalyzer.js +1 -0
  26. package/dist/plugins/analysis/ReactAnalyzer.js.map +1 -1
  27. package/dist/plugins/analysis/SQLiteAnalyzer.d.ts.map +1 -1
  28. package/dist/plugins/analysis/SQLiteAnalyzer.js +7 -9
  29. package/dist/plugins/analysis/SQLiteAnalyzer.js.map +1 -1
  30. package/dist/plugins/analysis/ServiceLayerAnalyzer.d.ts.map +1 -1
  31. package/dist/plugins/analysis/ServiceLayerAnalyzer.js +7 -9
  32. package/dist/plugins/analysis/ServiceLayerAnalyzer.js.map +1 -1
  33. package/dist/plugins/analysis/SocketIOAnalyzer.d.ts.map +1 -1
  34. package/dist/plugins/analysis/SocketIOAnalyzer.js +1 -0
  35. package/dist/plugins/analysis/SocketIOAnalyzer.js.map +1 -1
  36. package/dist/plugins/analysis/ast/GraphBuilder.d.ts +18 -8
  37. package/dist/plugins/analysis/ast/GraphBuilder.d.ts.map +1 -1
  38. package/dist/plugins/analysis/ast/GraphBuilder.js +92 -34
  39. package/dist/plugins/analysis/ast/GraphBuilder.js.map +1 -1
  40. package/dist/plugins/enrichment/MethodCallResolver.d.ts.map +1 -1
  41. package/dist/plugins/enrichment/MethodCallResolver.js +8 -3
  42. package/dist/plugins/enrichment/MethodCallResolver.js.map +1 -1
  43. package/dist/plugins/enrichment/method-call/MethodCallIndexers.d.ts +22 -0
  44. package/dist/plugins/enrichment/method-call/MethodCallIndexers.d.ts.map +1 -1
  45. package/dist/plugins/enrichment/method-call/MethodCallIndexers.js +138 -0
  46. package/dist/plugins/enrichment/method-call/MethodCallIndexers.js.map +1 -1
  47. package/dist/plugins/enrichment/method-call/MethodCallResolution.d.ts +11 -1
  48. package/dist/plugins/enrichment/method-call/MethodCallResolution.d.ts.map +1 -1
  49. package/dist/plugins/enrichment/method-call/MethodCallResolution.js +39 -1
  50. package/dist/plugins/enrichment/method-call/MethodCallResolution.js.map +1 -1
  51. package/dist/storage/backends/RFDBServerBackend.d.ts +9 -0
  52. package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -1
  53. package/dist/storage/backends/RFDBServerBackend.js +40 -0
  54. package/dist/storage/backends/RFDBServerBackend.js.map +1 -1
  55. package/package.json +3 -3
  56. package/src/Orchestrator.ts +25 -8
  57. package/src/PhaseRunner.ts +34 -24
  58. package/src/plugins/analysis/DatabaseAnalyzer.ts +3 -5
  59. package/src/plugins/analysis/ExpressAnalyzer.ts +1 -0
  60. package/src/plugins/analysis/ExpressResponseAnalyzer.ts +11 -36
  61. package/src/plugins/analysis/ExpressRouteAnalyzer.ts +1 -0
  62. package/src/plugins/analysis/NestJSRouteAnalyzer.ts +1 -0
  63. package/src/plugins/analysis/ReactAnalyzer.ts +1 -0
  64. package/src/plugins/analysis/SQLiteAnalyzer.ts +10 -11
  65. package/src/plugins/analysis/ServiceLayerAnalyzer.ts +7 -9
  66. package/src/plugins/analysis/SocketIOAnalyzer.ts +1 -0
  67. package/src/plugins/analysis/ast/GraphBuilder.ts +96 -35
  68. package/src/plugins/analysis/ast/builders/types.ts +1 -1
  69. package/src/plugins/enrichment/MethodCallResolver.ts +17 -3
  70. package/src/plugins/enrichment/method-call/MethodCallIndexers.ts +159 -0
  71. package/src/plugins/enrichment/method-call/MethodCallResolution.ts +53 -1
  72. package/src/storage/backends/RFDBServerBackend.ts +39 -0
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * GraphBuilder - orchestrator that delegates to domain-specific builders
3
- * OPTIMIZED: Uses batched writes to reduce FFI overhead
3
+ * Writes nodes/edges directly to graph during RFDBClient batch window.
4
+ * Only FUNCTION nodes are deferred (pending metadata mutation by ModuleRuntimeBuilder).
4
5
  */
5
6
 
6
7
  import type { GraphBackend, NodeRecord } from '@grafema/types';
@@ -36,8 +37,19 @@ export class GraphBuilder {
36
37
  // Track singleton nodes to avoid duplicates (net:stdio, net:request, etc.)
37
38
  private _createdSingletons: Set<string> = new Set();
38
39
 
39
- // Batching buffers for optimized writes
40
- private _nodeBuffer: GraphNode[] = [];
40
+ // Graph reference for direct writes (set during build(), cleared after)
41
+ private _graph: GraphBackend | null = null;
42
+
43
+ // Pending function nodes (deferred until domain builders can mutate metadata)
44
+ private _pendingFunctions: Map<string, GraphNode> = new Map();
45
+
46
+ // Sync batch mode: push directly to RFDBClient batch arrays (no intermediate buffer)
47
+ private _useSyncBatch: boolean = false;
48
+ private _directNodeCount: number = 0;
49
+ private _directEdgeCount: number = 0;
50
+
51
+ // Fallback buffers (when graph.batchNode is not available)
52
+ private _nodeBuffer: unknown[] = [];
41
53
  private _edgeBuffer: GraphEdge[] = [];
42
54
 
43
55
  // Domain builders
@@ -72,7 +84,7 @@ export class GraphBuilder {
72
84
  bufferEdge: (edge) => this._bufferEdge(edge),
73
85
  isCreated: (key) => this._createdSingletons.has(key),
74
86
  markCreated: (key) => { this._createdSingletons.add(key); },
75
- findBufferedNode: (id) => this._nodeBuffer.find(n => n.id === id),
87
+ findBufferedNode: (id) => this._pendingFunctions.get(id),
76
88
  findFunctionByName: (functions, name, file, callScopeId) =>
77
89
  this.findFunctionByName(functions, name, file, callScopeId),
78
90
  resolveVariableInScope: (name, scopePath, file, variables) =>
@@ -84,46 +96,79 @@ export class GraphBuilder {
84
96
  }
85
97
 
86
98
  /**
87
- * Buffer a node for batched writing
99
+ * Buffer a node for batched writing.
100
+ * INVARIANT: Only FUNCTION nodes are deferred (stored in _pendingFunctions)
101
+ * because ModuleRuntimeBuilder mutates their metadata (rejectionPatterns)
102
+ * after buffering. All other nodes go to sync batch or fallback buffer.
88
103
  */
89
104
  private _bufferNode(node: GraphNode): void {
90
- this._nodeBuffer.push(node);
105
+ if (!this._graph) throw new Error('_bufferNode called outside build() — _graph is null');
106
+ const branded = brandNodeInternal(node as unknown as NodeRecord);
107
+ if ((node as Record<string, unknown>).type === 'FUNCTION') {
108
+ this._pendingFunctions.set(node.id, branded as unknown as GraphNode);
109
+ } else if (this._useSyncBatch) {
110
+ this._graph.batchNode!(branded as unknown as Parameters<NonNullable<GraphBackend['batchNode']>>[0]);
111
+ this._directNodeCount++;
112
+ } else {
113
+ this._nodeBuffer.push(branded);
114
+ }
91
115
  }
92
116
 
93
117
  /**
94
- * Buffer an edge for batched writing
118
+ * Buffer an edge for batched writing.
95
119
  */
96
120
  private _bufferEdge(edge: GraphEdge): void {
97
- this._edgeBuffer.push(edge);
121
+ if (!this._graph) throw new Error('_bufferEdge called outside build() — _graph is null');
122
+ if (this._useSyncBatch) {
123
+ this._graph.batchEdge!(edge as unknown as Parameters<NonNullable<GraphBackend['batchEdge']>>[0]);
124
+ this._directEdgeCount++;
125
+ } else {
126
+ this._edgeBuffer.push(edge);
127
+ }
98
128
  }
99
129
 
100
130
  /**
101
- * Flush all buffered nodes to the graph
131
+ * Flush pending function nodes to the graph.
132
+ * In sync batch mode, pushes each to batchNode. In fallback mode, uses addNodes.
133
+ * Returns count of function nodes flushed.
102
134
  */
103
- private async _flushNodes(graph: GraphBackend): Promise<number> {
104
- if (this._nodeBuffer.length > 0) {
105
- // Brand nodes before flushing - they're validated by builders
106
- const brandedNodes = this._nodeBuffer.map(node => brandNodeInternal(node as unknown as NodeRecord));
107
- await graph.addNodes(brandedNodes);
108
- const count = this._nodeBuffer.length;
109
- this._nodeBuffer = [];
110
- return count;
135
+ private async _flushPendingFunctions(graph: GraphBackend): Promise<number> {
136
+ const pendingFunctions = Array.from(this._pendingFunctions.values());
137
+ if (pendingFunctions.length === 0) return 0;
138
+
139
+ // Nodes already branded in _bufferNode() — no need to brand again
140
+ if (this._useSyncBatch) {
141
+ for (const node of pendingFunctions) {
142
+ graph.batchNode!(node as unknown as Parameters<NonNullable<GraphBackend['batchNode']>>[0]);
143
+ }
144
+ } else {
145
+ await graph.addNodes(pendingFunctions as unknown as Parameters<GraphBackend['addNodes']>[0]);
111
146
  }
112
- return 0;
147
+
148
+ const count = pendingFunctions.length;
149
+ this._pendingFunctions.clear();
150
+ return count;
113
151
  }
114
152
 
115
153
  /**
116
- * Flush all buffered edges to the graph
117
- * Note: skip_validation=true because nodes were just flushed
154
+ * Flush fallback buffers (only used when sync batch is not available).
118
155
  */
119
- private async _flushEdges(graph: GraphBackend): Promise<number> {
156
+ private async _flushFallbackBuffers(graph: GraphBackend): Promise<{ nodes: number; edges: number }> {
157
+ let nodesCreated = 0;
158
+ if (this._nodeBuffer.length > 0) {
159
+ await graph.addNodes(this._nodeBuffer as unknown as Parameters<GraphBackend['addNodes']>[0]);
160
+ nodesCreated = this._nodeBuffer.length;
161
+ }
162
+
163
+ let edgesCreated = 0;
120
164
  if (this._edgeBuffer.length > 0) {
121
- await (graph as GraphBackend & { addEdges(e: GraphEdge[], skip?: boolean): Promise<void> }).addEdges(this._edgeBuffer, true /* skip_validation */);
122
- const count = this._edgeBuffer.length;
123
- this._edgeBuffer = [];
124
- return count;
165
+ await (graph as GraphBackend & { addEdges(e: GraphEdge[], skip?: boolean): Promise<void> }).addEdges(this._edgeBuffer, true);
166
+ edgesCreated = this._edgeBuffer.length;
125
167
  }
126
- return 0;
168
+
169
+ this._nodeBuffer = [];
170
+ this._edgeBuffer = [];
171
+ return { nodes: nodesCreated, edges: edgesCreated };
127
172
  }
128
173
 
129
174
  /**
@@ -150,7 +195,12 @@ export class GraphBuilder {
150
195
  hasTopLevelAwait = false
151
196
  } = data;
152
197
 
153
- // Reset buffers for this build
198
+ // Reset state for this build
199
+ this._graph = graph;
200
+ this._pendingFunctions.clear();
201
+ this._useSyncBatch = typeof graph.batchNode === 'function' && typeof graph.batchEdge === 'function';
202
+ this._directNodeCount = 0;
203
+ this._directEdgeCount = 0;
154
204
  this._nodeBuffer = [];
155
205
  this._edgeBuffer = [];
156
206
 
@@ -275,9 +325,20 @@ export class GraphBuilder {
275
325
  this._typeSystemBuilder.buffer(module, data);
276
326
  this._moduleRuntimeBuilder.buffer(module, data);
277
327
 
278
- // FLUSH: Write all nodes first, then edges in single batch calls
279
- const nodesCreated = await this._flushNodes(graph);
280
- const edgesCreated = await this._flushEdges(graph);
328
+ // FLUSH: Write pending function nodes (after domain builders mutated metadata)
329
+ const functionsCount = await this._flushPendingFunctions(graph);
330
+
331
+ // Flush fallback buffers if not using sync batch
332
+ let fallbackNodes = 0;
333
+ let fallbackEdges = 0;
334
+ if (!this._useSyncBatch) {
335
+ const fallback = await this._flushFallbackBuffers(graph);
336
+ fallbackNodes = fallback.nodes;
337
+ fallbackEdges = fallback.edges;
338
+ }
339
+
340
+ const nodesCreated = this._useSyncBatch ? this._directNodeCount + functionsCount : fallbackNodes + functionsCount;
341
+ const edgesCreated = this._useSyncBatch ? this._directEdgeCount : fallbackEdges;
281
342
 
282
343
  // Handle async operations for ASSIGNED_FROM with CLASS lookups
283
344
  const classAssignmentEdges = await this.createClassAssignmentEdges(variableAssignments, graph);
@@ -289,6 +350,8 @@ export class GraphBuilder {
289
350
  // REG-297: Update MODULE node with hasTopLevelAwait metadata
290
351
  await this.updateModuleTopLevelAwaitMetadata(module, graph, hasTopLevelAwait);
291
352
 
353
+ this._graph = null; // release reference
354
+
292
355
  return { nodes: nodesCreated, edges: edgesCreated + classAssignmentEdges };
293
356
  }
294
357
 
@@ -528,11 +591,9 @@ export class GraphBuilder {
528
591
  const file = parts.length >= 3 ? parts[2] : null;
529
592
 
530
593
  let classNode: { id: string; name: string; file?: string } | null = null;
531
- for await (const node of graph.queryNodes({ type: 'CLASS' })) {
532
- if (node.name === className && (!file || node.file === file)) {
533
- classNode = node as { id: string; name: string; file?: string };
534
- break;
535
- }
594
+ for await (const node of graph.queryNodes(file ? { type: 'CLASS', name: className, file } : { type: 'CLASS', name: className })) {
595
+ classNode = node as { id: string; name: string; file?: string };
596
+ break;
536
597
  }
537
598
 
538
599
  if (classNode) {
@@ -28,7 +28,7 @@ export interface BuilderContext {
28
28
  isCreated(singletonKey: string): boolean;
29
29
  markCreated(singletonKey: string): void;
30
30
 
31
- // Buffered node lookup (for metadata updates, e.g., rejection patterns)
31
+ // Pending function node lookup (for metadata updates by ModuleRuntimeBuilder)
32
32
  findBufferedNode(id: string): GraphNode | undefined;
33
33
 
34
34
  // Scope-aware variable/parameter resolution (REG-309)
@@ -27,7 +27,12 @@ import { StrictModeError } from '../../errors/GrafemaError.js';
27
27
  import { BUILTIN_PROTOTYPE_METHODS } from './method-call/MethodCallData.js';
28
28
  import type { MethodCallNode, LibraryCallStats } from './method-call/MethodCallData.js';
29
29
  import { isExternalMethod, isBuiltInObject, trackLibraryCall } from './method-call/MethodCallDetectors.js';
30
- import { buildClassMethodIndex, buildVariableTypeIndex } from './method-call/MethodCallIndexers.js';
30
+ import {
31
+ buildClassMethodIndex,
32
+ buildVariableTypeIndex,
33
+ buildInterfaceMethodIndex,
34
+ buildInterfaceImplementationIndex
35
+ } from './method-call/MethodCallIndexers.js';
31
36
  import { resolveMethodCall } from './method-call/MethodCallResolution.js';
32
37
  import { analyzeResolutionFailure, generateContextualSuggestion } from './method-call/MethodCallErrorAnalysis.js';
33
38
 
@@ -45,7 +50,7 @@ export class MethodCallResolver extends Plugin {
45
50
  edges: ['CALLS']
46
51
  },
47
52
  dependencies: ['ImportExportLinker'],
48
- consumes: ['CONTAINS', 'INSTANCE_OF', 'DERIVES_FROM'],
53
+ consumes: ['CONTAINS', 'INSTANCE_OF', 'DERIVES_FROM', 'IMPLEMENTS', 'EXTENDS'],
49
54
  produces: ['CALLS']
50
55
  };
51
56
  }
@@ -104,6 +109,13 @@ export class MethodCallResolver extends Plugin {
104
109
 
105
110
  const variableTypes = await buildVariableTypeIndex(graph, logger);
106
111
 
112
+ // Build interface indexes for CHA fallback (REG-485)
113
+ const methodToInterfaces = await buildInterfaceMethodIndex(graph, logger);
114
+ logger.info('Indexed interface methods', { methods: methodToInterfaces.size });
115
+
116
+ const interfaceImpls = await buildInterfaceImplementationIndex(graph, logger);
117
+ logger.info('Indexed interface implementations', { interfaces: interfaceImpls.size });
118
+
107
119
  // Cache for containing class lookups (local to this execution)
108
120
  const containingClassCache = new Map<string, BaseNodeRecord | null>();
109
121
 
@@ -162,7 +174,9 @@ export class MethodCallResolver extends Plugin {
162
174
  classMethodIndex,
163
175
  variableTypes,
164
176
  graph,
165
- containingClassCache
177
+ containingClassCache,
178
+ methodToInterfaces,
179
+ interfaceImpls
166
180
  );
167
181
 
168
182
  if (targetMethod) {
@@ -58,6 +58,165 @@ export async function buildClassMethodIndex(
58
58
  return index;
59
59
  }
60
60
 
61
+ /**
62
+ * Builds an index of interface methods for CHA-based resolution (REG-485).
63
+ * Maps method name to the set of interface names that declare (or inherit) that method.
64
+ *
65
+ * Algorithm:
66
+ * 1. Query all INTERFACE nodes and extract method names from properties
67
+ * 2. Track EXTENDS edges between interfaces
68
+ * 3. Flatten inherited methods (walk EXTENDS chain with cycle protection, max depth 10)
69
+ * 4. Build final map: method name -> set of interface names
70
+ */
71
+ export async function buildInterfaceMethodIndex(
72
+ graph: PluginContext['graph'],
73
+ logger: Logger
74
+ ): Promise<Map<string, Set<string>>> {
75
+ const startTime = Date.now();
76
+ const index = new Map<string, Set<string>>();
77
+
78
+ // Collect direct methods per interface and EXTENDS relationships
79
+ const interfaceMethods = new Map<string, Set<string>>();
80
+ const interfaceExtends = new Map<string, string[]>();
81
+
82
+ for await (const node of graph.queryNodes({ nodeType: 'INTERFACE' })) {
83
+ const interfaceName = node.name as string;
84
+ if (!interfaceName) continue;
85
+
86
+ // Parse properties field (may be JSON string or already parsed array)
87
+ let properties: Array<{ name: string; type?: string }> = [];
88
+ if (node.properties) {
89
+ try {
90
+ properties = typeof node.properties === 'string'
91
+ ? JSON.parse(node.properties)
92
+ : node.properties as Array<{ name: string; type?: string }>;
93
+ } catch {
94
+ // Skip unparseable properties
95
+ }
96
+ }
97
+
98
+ const methods = new Set<string>();
99
+ if (Array.isArray(properties)) {
100
+ for (const prop of properties) {
101
+ if (prop && typeof prop.name === 'string' && prop.name) {
102
+ methods.add(prop.name);
103
+ }
104
+ }
105
+ }
106
+ interfaceMethods.set(interfaceName, methods);
107
+
108
+ // Track EXTENDS edges for inheritance flattening
109
+ const extendsEdges = await graph.getOutgoingEdges(node.id, ['EXTENDS']);
110
+ if (extendsEdges.length > 0) {
111
+ const parentNames: string[] = [];
112
+ for (const edge of extendsEdges) {
113
+ const parentNode = await graph.getNode(edge.dst);
114
+ if (parentNode && parentNode.name) {
115
+ parentNames.push(parentNode.name as string);
116
+ }
117
+ }
118
+ if (parentNames.length > 0) {
119
+ interfaceExtends.set(interfaceName, parentNames);
120
+ }
121
+ }
122
+ }
123
+
124
+ // Flatten inherited methods with cycle protection
125
+ const resolvedMethods = new Map<string, Set<string>>();
126
+
127
+ function collectMethods(name: string, visited: Set<string>, depth: number): Set<string> {
128
+ if (depth > 10 || visited.has(name)) return new Set();
129
+ if (resolvedMethods.has(name)) return resolvedMethods.get(name)!;
130
+
131
+ visited.add(name);
132
+ const ownMethods = interfaceMethods.get(name) || new Set<string>();
133
+ const allMethods = new Set(ownMethods);
134
+
135
+ const parents = interfaceExtends.get(name);
136
+ if (parents) {
137
+ for (const parent of parents) {
138
+ const parentMethods = collectMethods(parent, visited, depth + 1);
139
+ for (const m of parentMethods) {
140
+ allMethods.add(m);
141
+ }
142
+ }
143
+ }
144
+
145
+ resolvedMethods.set(name, allMethods);
146
+ return allMethods;
147
+ }
148
+
149
+ for (const name of interfaceMethods.keys()) {
150
+ collectMethods(name, new Set(), 0);
151
+ }
152
+
153
+ // Build final index: method name -> set of interface names
154
+ for (const [interfaceName, methods] of resolvedMethods) {
155
+ for (const methodName of methods) {
156
+ let interfaces = index.get(methodName);
157
+ if (!interfaces) {
158
+ interfaces = new Set();
159
+ index.set(methodName, interfaces);
160
+ }
161
+ interfaces.add(interfaceName);
162
+ }
163
+ }
164
+
165
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
166
+ logger.debug('Built interface method index', {
167
+ interfaces: interfaceMethods.size,
168
+ methods: index.size,
169
+ time: `${elapsed}s`
170
+ });
171
+
172
+ return index;
173
+ }
174
+
175
+ /**
176
+ * Builds an index of interface implementations for CHA-based resolution (REG-485).
177
+ * Maps interface name to the set of class names that implement it.
178
+ *
179
+ * Algorithm:
180
+ * 1. Query all CLASS nodes
181
+ * 2. For each class, get outgoing IMPLEMENTS edges
182
+ * 3. For each IMPLEMENTS edge, get the target INTERFACE node name
183
+ * 4. Accumulate: interface name -> Set of class names
184
+ */
185
+ export async function buildInterfaceImplementationIndex(
186
+ graph: PluginContext['graph'],
187
+ logger: Logger
188
+ ): Promise<Map<string, Set<string>>> {
189
+ const startTime = Date.now();
190
+ const index = new Map<string, Set<string>>();
191
+
192
+ for await (const classNode of graph.queryNodes({ nodeType: 'CLASS' })) {
193
+ const className = classNode.name as string;
194
+ if (!className) continue;
195
+
196
+ const implementsEdges = await graph.getOutgoingEdges(classNode.id, ['IMPLEMENTS']);
197
+ for (const edge of implementsEdges) {
198
+ const interfaceNode = await graph.getNode(edge.dst);
199
+ if (!interfaceNode || !interfaceNode.name) continue;
200
+
201
+ const interfaceName = interfaceNode.name as string;
202
+ let classes = index.get(interfaceName);
203
+ if (!classes) {
204
+ classes = new Set();
205
+ index.set(interfaceName, classes);
206
+ }
207
+ classes.add(className);
208
+ }
209
+ }
210
+
211
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
212
+ logger.debug('Built interface implementation index', {
213
+ interfaces: index.size,
214
+ time: `${elapsed}s`
215
+ });
216
+
217
+ return index;
218
+ }
219
+
61
220
  /**
62
221
  * Builds an index of variables and their types (from INSTANCE_OF edges).
63
222
  */
@@ -18,13 +18,16 @@ import type { MethodCallNode, ClassEntry } from './MethodCallData.js';
18
18
  * 2. Local class in same file
19
19
  * 3. "this" reference to containing class
20
20
  * 4. Variable type index (INSTANCE_OF)
21
+ * 5. Interface-aware CHA fallback (REG-485)
21
22
  */
22
23
  export async function resolveMethodCall(
23
24
  methodCall: MethodCallNode,
24
25
  classMethodIndex: Map<string, ClassEntry>,
25
26
  variableTypes: Map<string, string>,
26
27
  graph: PluginContext['graph'],
27
- containingClassCache: Map<string, BaseNodeRecord | null>
28
+ containingClassCache: Map<string, BaseNodeRecord | null>,
29
+ methodToInterfaces?: Map<string, Set<string>>,
30
+ interfaceImpls?: Map<string, Set<string>>
28
31
  ): Promise<BaseNodeRecord | null> {
29
32
  const { object, method, file } = methodCall;
30
33
 
@@ -88,6 +91,55 @@ export async function resolveMethodCall(
88
91
  }
89
92
  }
90
93
 
94
+ // 5. Interface-aware CHA fallback (REG-485)
95
+ if (methodToInterfaces && interfaceImpls) {
96
+ const chaResult = await resolveViaInterfaceCHA(
97
+ method, classMethodIndex, methodToInterfaces, interfaceImpls, graph
98
+ );
99
+ if (chaResult) return chaResult;
100
+ }
101
+
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Step 5: Interface-aware CHA fallback (REG-485).
107
+ *
108
+ * When steps 1-4 fail, look up method name in interface method index,
109
+ * find implementing classes, and resolve to their method definition.
110
+ * Also checks parent classes via DERIVES_FROM for inherited methods.
111
+ * This enables resolution for calls on variables typed as interfaces.
112
+ */
113
+ export async function resolveViaInterfaceCHA(
114
+ methodName: string,
115
+ classMethodIndex: Map<string, ClassEntry>,
116
+ methodToInterfaces: Map<string, Set<string>>,
117
+ interfaceImpls: Map<string, Set<string>>,
118
+ graph: PluginContext['graph']
119
+ ): Promise<BaseNodeRecord | null> {
120
+ const candidateInterfaces = methodToInterfaces.get(methodName);
121
+ if (!candidateInterfaces || candidateInterfaces.size === 0) return null;
122
+
123
+ for (const interfaceName of candidateInterfaces) {
124
+ const implementingClasses = interfaceImpls.get(interfaceName);
125
+ if (!implementingClasses) continue;
126
+
127
+ for (const className of implementingClasses) {
128
+ const classEntry = classMethodIndex.get(className);
129
+ if (!classEntry) continue;
130
+
131
+ if (classEntry.methods.has(methodName)) {
132
+ return classEntry.methods.get(methodName)!;
133
+ }
134
+
135
+ // Check parent classes via DERIVES_FROM chain
136
+ const inherited = await findMethodInParentClasses(
137
+ classEntry.classNode, methodName, classMethodIndex, graph
138
+ );
139
+ if (inherited) return inherited;
140
+ }
141
+ }
142
+
91
143
  return null;
92
144
  }
93
145
 
@@ -775,6 +775,45 @@ export class RFDBServerBackend {
775
775
  return this.client.commitBatch(tags);
776
776
  }
777
777
 
778
+ /**
779
+ * Synchronously batch a node. Must be inside beginBatch/commitBatch.
780
+ * Bypasses async wrapper for direct batch insertion.
781
+ */
782
+ batchNode(node: InputNode): void {
783
+ if (!this.client) throw new Error('Not connected');
784
+ const { id, type, nodeType, node_type, name, file, exported, ...rest } = node;
785
+ const useV3 = this.protocolVersion >= 3;
786
+ const wire: Record<string, unknown> = {
787
+ id: String(id),
788
+ nodeType: (nodeType || node_type || type || 'UNKNOWN'),
789
+ name: name || '',
790
+ file: file || '',
791
+ exported: exported || false,
792
+ metadata: useV3 ? JSON.stringify(rest) : JSON.stringify({ originalId: String(id), ...rest }),
793
+ };
794
+ if (useV3) {
795
+ wire.semanticId = String(id);
796
+ }
797
+ this.client.batchNode(wire as Parameters<typeof this.client.batchNode>[0]);
798
+ }
799
+
800
+ /**
801
+ * Synchronously batch an edge. Must be inside beginBatch/commitBatch.
802
+ */
803
+ batchEdge(edge: InputEdge): void {
804
+ if (!this.client) throw new Error('Not connected');
805
+ const { src, dst, type, edgeType, edge_type, etype, metadata, ...rest } = edge;
806
+ const edgeTypeStr = edgeType || edge_type || (etype as string) || type;
807
+ if (typeof edgeTypeStr === 'string') this.edgeTypes.add(edgeTypeStr);
808
+ const flatMetadata = { ...rest, ...(typeof metadata === 'object' && metadata !== null ? metadata as Record<string, unknown> : {}) };
809
+ this.client.batchEdge({
810
+ src: String(src),
811
+ dst: String(dst),
812
+ edgeType: (edgeTypeStr || 'UNKNOWN'),
813
+ metadata: JSON.stringify(flatMetadata),
814
+ } as Record<string, unknown>);
815
+ }
816
+
778
817
  /**
779
818
  * Abort the current batch, discarding all buffered data.
780
819
  */