refactor(pipeline): wire BindingAccumulator into chunked parse pipeline

Replace ad-hoc workerTypeEnvBindings array with BindingAccumulator class.
Add allScopeBindings to WorkerExtractedData for multi-scope binding
collection with backward-compat fallback to old typeEnvBindings format.
Return BindingAccumulator from runChunkedParseAndResolve and finalize
before Phase 14 cross-file binding propagation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
abhigyanpatwari 2026-04-09 02:21:07 +05:30
parent 2f72fcc8d1
commit a7fdb36113
2 changed files with 37 additions and 11 deletions

View file

@ -43,6 +43,7 @@ import type {
ExtractedToolDef,
FileConstructorBindings,
FileTypeEnvBindings,
FileAllScopeBindings,
ExtractedORMQuery,
} from './workers/parse-worker.js';
import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
@ -61,6 +62,7 @@ export interface WorkerExtractedData {
ormQueries: ExtractedORMQuery[];
constructorBindings: FileConstructorBindings[];
typeEnvBindings: FileTypeEnvBindings[];
allScopeBindings: FileAllScopeBindings[];
}
// ============================================================================
@ -95,6 +97,7 @@ const processParsingWithWorkers = async (
ormQueries: [],
constructorBindings: [],
typeEnvBindings: [],
allScopeBindings: [],
};
const total = files.length;
@ -119,6 +122,7 @@ const processParsingWithWorkers = async (
const allORMQueries: ExtractedORMQuery[] = [];
const allConstructorBindings: FileConstructorBindings[] = [];
const allTypeEnvBindings: FileTypeEnvBindings[] = [];
const allAllScopeBindings: FileAllScopeBindings[] = [];
for (const result of chunkResults) {
for (const node of result.nodes) {
graph.addNode({
@ -155,6 +159,7 @@ const processParsingWithWorkers = async (
if (result.ormQueries) for (const _item of result.ormQueries) allORMQueries.push(_item);
for (const _item of result.constructorBindings) allConstructorBindings.push(_item);
for (const _item of result.typeEnvBindings) allTypeEnvBindings.push(_item);
if (result.allScopeBindings) for (const _item of result.allScopeBindings) allAllScopeBindings.push(_item);
}
// Merge and log skipped languages from workers
@ -185,6 +190,7 @@ const processParsingWithWorkers = async (
ormQueries: allORMQueries,
constructorBindings: allConstructorBindings,
typeEnvBindings: allTypeEnvBindings,
allScopeBindings: allAllScopeBindings,
};
};

View file

@ -1,4 +1,5 @@
import { createKnowledgeGraph } from '../graph/graph.js';
import { BindingAccumulator } from './binding-accumulator.js';
import { processStructure } from './structure-processor.js';
import { processMarkdown } from './markdown-processor.js';
import { processCobol, isCobolFile, isJclFile } from './cobol-processor.js';
@ -650,6 +651,7 @@ async function runChunkedParseAndResolve(
allDecoratorRoutes: ExtractedDecoratorRoute[];
allToolDefs: ExtractedToolDef[];
allORMQueries: ExtractedORMQuery[];
bindingAccumulator: BindingAccumulator;
}> {
const symbolTable = ctx.symbols;
@ -782,7 +784,7 @@ async function runChunkedParseAndResolve(
// Phase 14: Collect exported type bindings for cross-file propagation
const exportedTypeMap: ExportedTypeMap = new Map();
// Accumulate file-scope TypeEnv bindings from workers (closes worker/sequential quality gap)
const workerTypeEnvBindings: { filePath: string; bindings: [string, string][] }[] = [];
const bindingAccumulator = new BindingAccumulator();
// Accumulate fetch() calls from workers for Next.js route matching
const allFetchCalls: ExtractedFetchCall[] = [];
// Accumulate framework-extracted routes (Laravel, etc.) for Route node creation
@ -916,9 +918,18 @@ async function runChunkedParseAndResolve(
});
}),
]);
// Collect TypeEnv file-scope bindings for exported type enrichment
if (chunkWorkerData.typeEnvBindings?.length) {
for (const _item of chunkWorkerData.typeEnvBindings) workerTypeEnvBindings.push(_item);
// Collect all-scope bindings into BindingAccumulator
if (chunkWorkerData.allScopeBindings?.length) {
for (const { filePath, bindings } of chunkWorkerData.allScopeBindings) {
const entries = bindings.map(([scope, varName, typeName]) => ({ scope, varName, typeName }));
bindingAccumulator.appendFile(filePath, entries);
}
} else if (chunkWorkerData.typeEnvBindings?.length) {
// Fallback: old-style file-scope-only bindings (backward compat)
for (const { filePath, bindings } of chunkWorkerData.typeEnvBindings) {
const entries = bindings.map(([varName, typeName]) => ({ scope: '', varName, typeName }));
bindingAccumulator.appendFile(filePath, entries);
}
}
// Collect fetch() calls for Next.js route matching
if (chunkWorkerData.fetchCalls?.length) {
@ -1068,14 +1079,11 @@ async function runChunkedParseAndResolve(
);
}
// ── Worker path quality enrichment: merge TypeEnv file-scope bindings into ExportedTypeMap ──
// Workers return file-scope bindings from their TypeEnv fixpoint (includes inferred types
// like `const config = getConfig()` → Config). Filter by graph isExported to match
// the sequential path's collectExportedBindings behavior.
if (workerTypeEnvBindings.length > 0) {
// ── Worker path quality enrichment: merge file-scope bindings into ExportedTypeMap ──
if (bindingAccumulator.fileCount > 0) {
let enriched = 0;
for (const { filePath, bindings } of workerTypeEnvBindings) {
for (const [name, type] of bindings) {
for (const filePath of bindingAccumulator.files()) {
for (const [name, type] of bindingAccumulator.fileScopeEntries(filePath)) {
// Verify the symbol is exported via graph node
const nodeId = `Function:${filePath}:${name}`;
const varNodeId = `Variable:${filePath}:${name}`;
@ -1128,6 +1136,7 @@ async function runChunkedParseAndResolve(
allDecoratorRoutes,
allToolDefs,
allORMQueries,
bindingAccumulator,
};
}
@ -1375,6 +1384,7 @@ export const runPipelineFromRepo = async (
allDecoratorRoutes,
allToolDefs,
allORMQueries,
bindingAccumulator,
} = await runChunkedParseAndResolve(
graph,
ctx,
@ -1697,6 +1707,16 @@ export const runPipelineFromRepo = async (
processORMQueries(graph, allORMQueries, isDev);
}
// Finalize — no more appends allowed
bindingAccumulator.finalize();
if (isDev && bindingAccumulator.totalBindings > 0) {
const memKB = Math.round(bindingAccumulator.estimateMemoryBytes() / 1024);
console.log(
`📦 BindingAccumulator: ${bindingAccumulator.totalBindings} bindings across ${bindingAccumulator.fileCount} files (~${memKB} KB)`,
);
}
// ── Phase 14: Cross-file binding propagation (topological level sort) ──
await runCrossFileBindingPropagation(
graph,