diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 8ac9645be..755ec1e29 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -700,20 +700,17 @@ export const processCalls = async ( const logSkipped = isVerboseIngestionEnabled(); const skippedByLang = logSkipped ? new Map() : null; - // ── Two-pass split for accumulator ordering correctness ── - // When bindingAccumulator is present, the Phase 9 fallback in - // verifyConstructorBindings reads from the accumulator. If we flush and - // verify in the same per-file iteration, consumer files processed before - // their provider files won't see the provider's bindings — causing silent - // misses. Fix: pre-pass flushes ALL files' TypeEnv bindings into the - // accumulator before the main loop runs verifyConstructorBindings. - // This mirrors the worker path where all appendFile calls complete before - // processCallsFromExtracted runs. For the sequential path (<15 files), - // buffering per-file state is negligible. - // - // When bindingAccumulator is absent (legacy/Phase 14 path), the existing - // single-pass behavior is preserved — no pre-pass, no buffering. - interface PrePassState { + // ── Prepare-then-resolve: single preparation loop, deferred resolution ── + // All files are prepared (parse → query → heritage → TypeEnv) in one loop, + // then resolved (verifyConstructorBindings → call edges) in a second loop. + // This ensures: + // 1. When bindingAccumulator is present, ALL files flush their TypeEnv + // bindings before ANY verifyConstructorBindings reads — fixing the + // consumer-before-provider ordering bug on the sequential path. + // 2. globalParentMap is fully populated before resolution, improving + // cross-file isSubclassOf accuracy regardless of file order. + // For the sequential path (<15 files), buffering per-file state is negligible. + interface PreparedFile { file: { path: string; content: string }; language: SupportedLanguages; provider: ReturnType; @@ -722,240 +719,125 @@ export const processCalls = async ( parentMap: ReadonlyMap; typeEnv: ReturnType; } - const prePassStates: PrePassState[] | undefined = bindingAccumulator ? [] : undefined; + const prepared: PreparedFile[] = []; - if (bindingAccumulator) { - // ── Pre-pass: build TypeEnv, flush to accumulator, buffer state ── - for (let i = 0; i < files.length; i++) { - const file = files[i]; - if (i % 20 === 0) await yieldToEventLoop(); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (i % 20 === 0) await yieldToEventLoop(); - const language = getLanguageFromFilename(file.path); - if (!language) continue; - if (!isLanguageAvailable(language)) { - if (skippedByLang) { - skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1); - } - continue; + const language = getLanguageFromFilename(file.path); + if (!language) continue; + if (!isLanguageAvailable(language)) { + if (skippedByLang) { + skippedByLang.set(language, (skippedByLang.get(language) ?? 0) + 1); } - - const provider = getProvider(language); - const queryStr = provider.treeSitterQueries; - if (!queryStr) continue; - - await loadLanguage(language, file.path); - - let tree = astCache.get(file.path); - if (!tree) { - try { - tree = parser.parse(file.content, undefined, { - bufferSize: getTreeSitterBufferSize(file.content.length), - }); - } catch (parseError) { - continue; - } - astCache.set(file.path, tree); - } - - let matches; - try { - const lang = parser.getLanguage(); - const query = new Parser.Query(lang, queryStr); - matches = query.matches(tree.rootNode); - } catch (queryError) { - console.warn(`Query error for ${file.path}:`, queryError); - continue; - } - - // Extract heritage for parentMap (same as main loop) - const fileParentMap = new Map(); - for (const match of matches) { - const captureMap: Record = {}; - match.captures.forEach((c) => (captureMap[c.name] = c.node)); - if (captureMap['heritage.class'] && captureMap['heritage.extends']) { - const className: string = captureMap['heritage.class'].text; - const parentName: string = captureMap['heritage.extends'].text; - const extendsNode = captureMap['heritage.extends']; - const fieldDecl = extendsNode.parent; - if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name')) - continue; - let parents = fileParentMap.get(className); - if (!parents) { - parents = []; - fileParentMap.set(className, parents); - } - if (!parents.includes(parentName)) parents.push(parentName); - } - } - const parentMap: ReadonlyMap = fileParentMap; - for (const [cls, parents] of fileParentMap) { - let global = globalParentMap.get(cls); - let seen = globalParentSeen.get(cls); - if (!global) { - global = []; - globalParentMap.set(cls, global); - } - if (!seen) { - seen = new Set(); - globalParentSeen.set(cls, seen); - } - for (const p of parents) { - if (!seen.has(p)) { - seen.add(p); - global.push(p); - } - } - } - - const importedBindings = importedBindingsMap?.get(file.path); - const importedReturnTypes = importedReturnTypesMap?.get(file.path); - const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path); - const typeEnv = buildTypeEnv(tree, language, { - symbolTable: ctx.symbols, - parentMap, - importedBindings, - importedReturnTypes, - importedRawReturnTypes, - enclosingFunctionFinder: provider?.enclosingFunctionFinder, - extractFunctionName: provider?.methodExtractor?.extractFunctionName, - }); - if (typeEnv && exportedTypeMap) { - const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph); - if (fileExports) exportedTypeMap.set(file.path, fileExports); - } - typeEnv.flush(file.path, bindingAccumulator); - - prePassStates!.push({ file, language, provider, tree, matches, parentMap, typeEnv }); + continue; } + + const provider = getProvider(language); + const queryStr = provider.treeSitterQueries; + if (!queryStr) continue; + + await loadLanguage(language, file.path); + + let tree = astCache.get(file.path); + if (!tree) { + try { + tree = parser.parse(file.content, undefined, { + bufferSize: getTreeSitterBufferSize(file.content.length), + }); + } catch (parseError) { + continue; + } + astCache.set(file.path, tree); + } + + let matches; + try { + const lang = parser.getLanguage(); + const query = new Parser.Query(lang, queryStr); + matches = query.matches(tree.rootNode); + } catch (queryError) { + console.warn(`Query error for ${file.path}:`, queryError); + continue; + } + + // Extract heritage from query matches to build parentMap for buildTypeEnv. + // Heritage-processor runs in PARALLEL, so graph edges don't exist when buildTypeEnv runs. + const fileParentMap = new Map(); + for (const match of matches) { + const captureMap: Record = {}; + match.captures.forEach((c) => (captureMap[c.name] = c.node)); + if (captureMap['heritage.class'] && captureMap['heritage.extends']) { + const className: string = captureMap['heritage.class'].text; + const parentName: string = captureMap['heritage.extends'].text; + const extendsNode = captureMap['heritage.extends']; + const fieldDecl = extendsNode.parent; + if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name')) + continue; + let parents = fileParentMap.get(className); + if (!parents) { + parents = []; + fileParentMap.set(className, parents); + } + if (!parents.includes(parentName)) parents.push(parentName); + } + } + const parentMap: ReadonlyMap = fileParentMap; + // Merge per-file heritage into globalParentMap for cross-file isSubclassOf lookups. + for (const [cls, parents] of fileParentMap) { + let global = globalParentMap.get(cls); + let seen = globalParentSeen.get(cls); + if (!global) { + global = []; + globalParentMap.set(cls, global); + } + if (!seen) { + seen = new Set(); + globalParentSeen.set(cls, seen); + } + for (const p of parents) { + if (!seen.has(p)) { + seen.add(p); + global.push(p); + } + } + } + + const importedBindings = importedBindingsMap?.get(file.path); + const importedReturnTypes = importedReturnTypesMap?.get(file.path); + const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path); + const typeEnv = buildTypeEnv(tree, language, { + symbolTable: ctx.symbols, + parentMap, + importedBindings, + importedReturnTypes, + importedRawReturnTypes, + enclosingFunctionFinder: provider?.enclosingFunctionFinder, + extractFunctionName: provider?.methodExtractor?.extractFunctionName, + }); + if (typeEnv && exportedTypeMap) { + const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph); + if (fileExports) exportedTypeMap.set(file.path, fileExports); + } + if (bindingAccumulator) { + typeEnv.flush(file.path, bindingAccumulator); + } + + prepared.push({ file, language, provider, tree, matches, parentMap, typeEnv }); } - // ── Main loop: resolve calls (and optionally build TypeEnv if no pre-pass) ── - const loopSource = prePassStates ?? files; - for (let i = 0; i < loopSource.length; i++) { - const isPrePassed = prePassStates !== undefined; - const entry = loopSource[i]; - const file = isPrePassed - ? (entry as PrePassState).file - : (entry as { path: string; content: string }); + // ── Resolution loop: verify constructor bindings and resolve calls ── + // The accumulator (if present) is now fully populated from the preparation + // loop above, so verifyConstructorBindings sees all provider bindings + // regardless of file processing order. + for (let i = 0; i < prepared.length; i++) { + const { file, language, provider, tree, matches, parentMap, typeEnv } = prepared[i]; enclosingFnExtractCache.clear(); onProgress?.(i + 1, files.length); if (i % 20 === 0) await yieldToEventLoop(); - let language: SupportedLanguages; - let provider: ReturnType; - let tree: ReturnType; - let matches: ReturnType; - let typeEnv: ReturnType; - let parentMap: ReadonlyMap; - - if (isPrePassed) { - // Reuse state from pre-pass — TypeEnv and flush already done - const state = entry as PrePassState; - language = state.language; - provider = state.provider; - tree = state.tree; - matches = state.matches; - typeEnv = state.typeEnv; - parentMap = state.parentMap; - } else { - // Legacy single-pass path (no bindingAccumulator) - const lang = getLanguageFromFilename(file.path); - if (!lang) continue; - if (!isLanguageAvailable(lang)) { - if (skippedByLang) { - skippedByLang.set(lang, (skippedByLang.get(lang) ?? 0) + 1); - } - continue; - } - language = lang; - - provider = getProvider(language); - const queryStr = provider.treeSitterQueries; - if (!queryStr) continue; - - await loadLanguage(language, file.path); - - tree = astCache.get(file.path)!; - if (!tree) { - try { - tree = parser.parse(file.content, undefined, { - bufferSize: getTreeSitterBufferSize(file.content.length), - }); - } catch (parseError) { - continue; - } - astCache.set(file.path, tree); - } - - try { - const parseLang = parser.getLanguage(); - const query = new Parser.Query(parseLang, queryStr); - matches = query.matches(tree.rootNode); - } catch (queryError) { - console.warn(`Query error for ${file.path}:`, queryError); - continue; - } - - // Heritage extraction (same as pre-pass, only runs in legacy path) - const fileParentMap = new Map(); - for (const match of matches) { - const captureMap: Record = {}; - match.captures.forEach((c) => (captureMap[c.name] = c.node)); - if (captureMap['heritage.class'] && captureMap['heritage.extends']) { - const className: string = captureMap['heritage.class'].text; - const parentName: string = captureMap['heritage.extends'].text; - const extendsNode = captureMap['heritage.extends']; - const fieldDecl = extendsNode.parent; - if (fieldDecl?.type === 'field_declaration' && fieldDecl.childForFieldName('name')) - continue; - let parents = fileParentMap.get(className); - if (!parents) { - parents = []; - fileParentMap.set(className, parents); - } - if (!parents.includes(parentName)) parents.push(parentName); - } - } - for (const [cls, parents] of fileParentMap) { - let global = globalParentMap.get(cls); - let seen = globalParentSeen.get(cls); - if (!global) { - global = []; - globalParentMap.set(cls, global); - } - if (!seen) { - seen = new Set(); - globalParentSeen.set(cls, seen); - } - for (const p of parents) { - if (!seen.has(p)) { - seen.add(p); - global.push(p); - } - } - } - - parentMap = fileParentMap; - - const importedBindings = importedBindingsMap?.get(file.path); - const importedReturnTypes = importedReturnTypesMap?.get(file.path); - const importedRawReturnTypes = importedRawReturnTypesMap?.get(file.path); - typeEnv = buildTypeEnv(tree, language, { - symbolTable: ctx.symbols, - parentMap, - importedBindings, - importedReturnTypes, - importedRawReturnTypes, - enclosingFunctionFinder: provider?.enclosingFunctionFinder, - extractFunctionName: provider?.methodExtractor?.extractFunctionName, - }); - if (typeEnv && exportedTypeMap) { - const fileExports = collectExportedBindings(typeEnv, file.path, ctx.symbols, graph); - if (fileExports) exportedTypeMap.set(file.path, fileExports); - } - } - const callRouter = provider.callRouter; const verifiedReceivers =