Merge origin/main into delphi-support

Integrate main branch changes (WASM grammar architecture, centralized
resolution caches, SQLite adapter) with delphi-support branch. Pascal
grammar is now built as WASM and shipped in src/extraction/wasm/ for
consistency with the WASM-based grammar loading approach.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Olaf Monien
2026-02-17 20:23:54 +01:00
co-authored by Claude Opus 4.6
33 changed files with 1266 additions and 958 deletions
+5 -10
View File
@@ -105,7 +105,7 @@ function resolveRelativeImport(
// Try the path as-is first
const basePath = path.resolve(fromDir, importPath);
const relativePath = path.relative(projectRoot, basePath);
const relativePath = path.relative(projectRoot, basePath).replace(/\\/g, '/');
// Try each extension
for (const ext of extensions) {
@@ -442,15 +442,10 @@ export function resolveViaImport(
ref: UnresolvedRef,
context: ResolutionContext
): ResolvedRef | null {
// Use cached import mappings or extract and cache them
let imports = importMappingCache.get(ref.filePath);
if (!imports) {
const content = context.readFile(ref.filePath);
if (!content) {
return null;
}
imports = extractImportMappings(ref.filePath, content, ref.language);
importMappingCache.set(ref.filePath, imports);
// Use cached import mappings (avoids re-reading and re-parsing per ref)
const imports = context.getImportMappings(ref.filePath, ref.language);
if (imports.length === 0 && !context.readFile(ref.filePath)) {
return null;
}
// Check if the reference name matches any import
+55 -4
View File
@@ -15,9 +15,10 @@ import {
ResolutionResult,
ResolutionContext,
FrameworkResolver,
ImportMapping,
} from './types';
import { matchReference, clearFuzzyIndex } from './name-matcher';
import { resolveViaImport, clearImportMappingCache } from './import-resolver';
import { matchReference } from './name-matcher';
import { resolveViaImport, extractImportMappings } from './import-resolver';
import { detectFrameworks } from './frameworks';
import { logDebug } from '../errors';
@@ -40,6 +41,9 @@ export class ReferenceResolver {
private qualifiedNameCache: Map<string, Node[]> = new Map();
private kindCache: Map<string, Node[]> = new Map();
private nodeByIdCache: Map<string, Node> = new Map();
private lowerNameCache: Map<string, Node[]> = new Map();
private importMappingCache: Map<string, ImportMapping[]> = new Map();
private knownFiles: Set<string> | null = null;
private cachesWarmed = false;
constructor(projectRoot: string, queries: QueryBuilder) {
@@ -91,8 +95,20 @@ export class ReferenceResolver {
// Index by ID
this.nodeByIdCache.set(node.id, node);
// Index by lowercase name (for fuzzy matching)
const lowerName = node.name.toLowerCase();
const byLower = this.lowerNameCache.get(lowerName);
if (byLower) {
byLower.push(node);
} else {
this.lowerNameCache.set(lowerName, [node]);
}
}
// Pre-build known files set from index
this.knownFiles = new Set(this.queries.getAllFiles().map((f) => f.path));
this.cachesWarmed = true;
}
@@ -106,8 +122,9 @@ export class ReferenceResolver {
this.qualifiedNameCache.clear();
this.kindCache.clear();
this.nodeByIdCache.clear();
clearImportMappingCache();
clearFuzzyIndex();
this.lowerNameCache.clear();
this.importMappingCache.clear();
this.knownFiles = null;
this.cachesWarmed = false;
}
@@ -150,6 +167,14 @@ export class ReferenceResolver {
},
fileExists: (filePath: string) => {
// Check pre-built known files set first (O(1))
if (this.knownFiles) {
const normalized = filePath.replace(/\\/g, '/');
if (this.knownFiles.has(filePath) || this.knownFiles.has(normalized)) {
return true;
}
}
// Fall back to filesystem for files not yet indexed
const fullPath = path.join(this.projectRoot, filePath);
try {
return fs.existsSync(fullPath);
@@ -183,6 +208,32 @@ export class ReferenceResolver {
getAllFiles: () => {
return this.queries.getAllFiles().map((f) => f.path);
},
getNodesByLowerName: (lowerName: string) => {
if (this.cachesWarmed) {
return this.lowerNameCache.get(lowerName) ?? [];
}
// Fallback: scan all nodes (expensive, but only used if cache not warm)
return this.queries.getAllNodes().filter(
(n) => n.name.toLowerCase() === lowerName
);
},
getImportMappings: (filePath: string, language) => {
const cacheKey = filePath;
const cached = this.importMappingCache.get(cacheKey);
if (cached) return cached;
const content = this.context.readFile(filePath);
if (!content) {
this.importMappingCache.set(cacheKey, []);
return [];
}
const mappings = extractImportMappings(filePath, content, language);
this.importMappingCache.set(cacheKey, mappings);
return mappings;
},
};
}
+8 -32
View File
@@ -190,16 +190,6 @@ function findBestMatch(
return bestNode;
}
// Lazily-built case-insensitive index for fuzzy matching
let fuzzyIndex: Map<string, Node[]> | null = null;
/**
* Clear the fuzzy match index (call between indexing runs)
*/
export function clearFuzzyIndex(): void {
fuzzyIndex = null;
}
/**
* Fuzzy match - last resort with lower confidence
*/
@@ -207,38 +197,24 @@ export function matchFuzzy(
ref: UnresolvedRef,
context: ResolutionContext
): ResolvedRef | null {
// Build case-insensitive index on first use
if (!fuzzyIndex) {
fuzzyIndex = new Map();
const kinds: Array<Node['kind']> = ['function', 'method', 'class'];
for (const kind of kinds) {
for (const node of context.getNodesByKind(kind)) {
const lower = node.name.toLowerCase();
const existing = fuzzyIndex.get(lower);
if (existing) {
existing.push(node);
} else {
fuzzyIndex.set(lower, [node]);
}
}
}
}
const lowerName = ref.referenceName.toLowerCase();
// Exact case-insensitive match via index (O(1) lookup)
const caseInsensitive = fuzzyIndex.get(lowerName);
// Use pre-built lowercase index for O(1) lookup instead of scanning all nodes
const candidates = context.getNodesByLowerName(lowerName);
if (caseInsensitive && caseInsensitive.length === 1) {
// Filter to callable kinds only (function, method, class)
const callableKinds = new Set(['function', 'method', 'class']);
const callableCandidates = candidates.filter((n) => callableKinds.has(n.kind));
if (callableCandidates.length === 1) {
return {
original: ref,
targetNodeId: caseInsensitive[0]!.id,
targetNodeId: callableCandidates[0]!.id,
confidence: 0.5,
resolvedBy: 'fuzzy',
};
}
// Skip prefix matching — too expensive and low value (confidence 0.3)
return null;
}
+4
View File
@@ -79,6 +79,10 @@ export interface ResolutionContext {
getProjectRoot(): string;
/** Get all files */
getAllFiles(): string[];
/** Get nodes by lowercase name (O(1) lookup for fuzzy matching) */
getNodesByLowerName(lowerName: string): Node[];
/** Get cached import mappings for a file */
getImportMappings(filePath: string, language: Language): ImportMapping[];
}
/**