diff --git a/gitnexus/src/core/ingestion/language-config.ts b/gitnexus/src/core/ingestion/language-config.ts index 18cd72291..4657e275e 100644 --- a/gitnexus/src/core/ingestion/language-config.ts +++ b/gitnexus/src/core/ingestion/language-config.ts @@ -189,6 +189,10 @@ export async function loadComposerConfig(repoRoot: string): Promise\s*([^<]+)\s*<\/RootNamespace>/; @@ -218,8 +222,8 @@ function getCsharpStructureScanner(): Promise<(content: string) => CsharpFileStr * `namespace` declarations. That `.cs` read cost is the price of the #1881 * gate, not a saving: collapsing the csproj and namespace walks into one BFS * avoids a second directory traversal, but the per-file `.cs` reads are new - * work this scan introduces. Reads within a directory are issued in parallel - * (see below); directories are still visited breadth-first. + * work this scan introduces. Reads within a directory are issued in bounded + * windows (see below); directories are still visited breadth-first. */ export async function scanCSharpProject(repoRoot: string): Promise { const configs: CSharpProjectConfig[] = []; @@ -251,11 +255,13 @@ export async function scanCSharpProject(repoRoot: string): Promise[] = []; - const csReads: Promise[] = []; + // Collect read targets, then issue them in bounded windows (rather than all + // at once) so a directory with thousands of `.cs` files can't exhaust file + // descriptors / spike memory. csproj reads keep entry order (config + // precedence matters); `.cs` namespace results land in shared Sets where + // order is irrelevant. + const csprojNames: string[] = []; + const csNames: string[] = []; for (const entry of entries) { if (entry.isDirectory()) { if (CSHARP_SCAN_SKIP_DIRS.has(entry.name)) continue; @@ -267,26 +273,45 @@ export async function scanCSharpProject(repoRoot: string): Promise + readCsprojConfig(path.join(dir, name), name, repoRoot, dir, maxFileSizeBytes), + ), + ); + for (const r of settled) { + const config = r.status === 'fulfilled' ? r.value : null; + if (config) { + configs.push(config); + rootNamespaces.add(config.rootNamespace); + } } } - // A `.cs` that was skipped (oversized) or unreadable leaves its namespaces - // uncollected, so the scan is incomplete → mark truncated to fail the - // #1881 gate OPEN rather than wrongly suppress an import declared there. - for (const skipped of await Promise.all(csReads)) { - if (skipped) truncated = true; + for (let i = 0; i < csNames.length; i += CSHARP_SCAN_READ_CONCURRENCY) { + const batch = csNames.slice(i, i + CSHARP_SCAN_READ_CONCURRENCY); + const settled = await Promise.allSettled( + batch.map((name) => + collectDeclaredNamespaces( + path.join(dir, name), + declaredNamespaces, + rootNamespaces, + maxFileSizeBytes, + ), + ), + ); + // A `.cs` that was skipped (oversized), unreadable, or whose read/scan + // unexpectedly rejected leaves its namespaces uncollected → mark truncated + // to fail the #1881 gate OPEN rather than wrongly suppress an import. + for (const r of settled) { + if (r.status !== 'fulfilled' || r.value) truncated = true; + } } }