diff --git a/gitnexus/src/core/ingestion/csharp-namespace-gate.ts b/gitnexus/src/core/ingestion/csharp-namespace-gate.ts index 12b697e82..df806bf2b 100644 --- a/gitnexus/src/core/ingestion/csharp-namespace-gate.ts +++ b/gitnexus/src/core/ingestion/csharp-namespace-gate.ts @@ -11,20 +11,80 @@ import type { CSharpNamespaceEvidence } from './language-config.js'; +/** + * Top-level namespace segments that clearly belong to the BCL / runtime / a + * ubiquitous third-party package — i.e. roots a normal repo does NOT declare. + * These stay gated even when the namespace scan is truncated, so a single + * unreadable file / capped subtree can't silently re-enable BCL→local suffix + * matches repo-wide (#1881). A repo that legitimately declares one of these + * roots is still allowed via the alignment escape hatch below. + */ +const CSHARP_EXTERNAL_ROOTS: ReadonlySet = new Set([ + // .NET BCL / runtime + 'System', + 'Microsoft', + 'Windows', + 'Mono', + // ubiquitous third-party NuGet roots + 'Newtonsoft', + 'Serilog', + 'AutoMapper', + 'MediatR', + 'Polly', + 'FluentValidation', + 'Grpc', + 'Google', + 'Azure', + 'Amazon', + 'AWSSDK', + // common test frameworks + 'Xunit', + 'NUnit', + 'Moq', + 'FluentAssertions', + 'NSubstitute', + 'Shouldly', +]); + +/** Whether `targetRaw`'s top-level segment is a clearly-external root. */ +function isExternalRoot(targetRaw: string): boolean { + const dot = targetRaw.indexOf('.'); + const top = dot === -1 ? targetRaw : targetRaw.slice(0, dot); + return CSHARP_EXTERNAL_ROOTS.has(top); +} + /** * Whether the unanchored suffix fallback may run for `targetRaw`. * * Fails OPEN when the namespace scan was truncated (large repos must not * silently lose legitimate edges, #1881 #11) and when no evidence was - * threaded at all (preserves legacy permissive behavior). Otherwise defers - * to {@link importAlignsWithDeclaredNamespaces}. + * threaded at all (preserves legacy permissive behavior). The truncation + * fail-open is carved out for clearly-external roots (BCL / well-known + * packages) that the repo does not declare, so one incomplete scan can't + * re-open the #1881 hole repo-wide. Otherwise defers to + * {@link importAlignsWithDeclaredNamespaces}. */ export function csharpSuffixFallbackAllowed( targetRaw: string, evidence: CSharpNamespaceEvidence | undefined, ): boolean { if (evidence === undefined) return true; - if (evidence.truncated) return true; + if (evidence.truncated) { + // Keep clearly-external roots blocked through truncation UNLESS the repo + // actually declares an aligning namespace (the alignment check is the + // escape hatch — a repo that declares `namespace System;` still resolves). + if ( + isExternalRoot(targetRaw) && + !importAlignsWithDeclaredNamespaces( + targetRaw, + evidence.declaredNamespaces, + evidence.rootNamespaces, + ) + ) { + return false; + } + return true; + } return importAlignsWithDeclaredNamespaces( targetRaw, evidence.declaredNamespaces, diff --git a/gitnexus/test/unit/scope-resolution/csharp/csharp-imports.test.ts b/gitnexus/test/unit/scope-resolution/csharp/csharp-imports.test.ts index 361bb663b..c0c49bfa9 100644 --- a/gitnexus/test/unit/scope-resolution/csharp/csharp-imports.test.ts +++ b/gitnexus/test/unit/scope-resolution/csharp/csharp-imports.test.ts @@ -467,16 +467,40 @@ describe('csharpSuffixFallbackAllowed — fail-open safety valves (#1881)', () = expect(csharpSuffixFallbackAllowed('System.Threading.Tasks', undefined)).toBe(true); }); - it('fails OPEN (allows) when the namespace scan was truncated (#6)', () => { - // Same non-aligned import again: an incomplete (capped/unreadable) scan - // must not silently drop a legitimate edge, so truncation re-opens the - // fallback for every import. + it('keeps a clearly-external BCL root BLOCKED even when the scan was truncated (#1881, Codex F1)', () => { + // A single truncation must NOT silently re-enable BCL→local suffix matches + // repo-wide: System.* stays gated through truncation when the repo does not + // declare it. (This reverses the prior blanket-fail-open for external roots.) expect( csharpSuffixFallbackAllowed('System.Threading.Tasks', { declaredNamespaces: declared, rootNamespaces: roots, truncated: true, }), + ).toBe(false); + }); + + it('fails OPEN for a genuinely local-looking import when the scan was truncated (#6)', () => { + // Non-external roots still fail open under truncation so an incomplete + // (capped/unreadable) scan does not silently drop a legitimate in-repo edge. + expect( + csharpSuffixFallbackAllowed('MyApp.Internal.Widget', { + declaredNamespaces: declared, + rootNamespaces: roots, + truncated: true, + }), + ).toBe(true); + }); + + it('lets an external root fail OPEN through truncation when the repo declares it (escape hatch)', () => { + // If the repo actually declares the (normally-external) root, the alignment + // escape hatch allows the import even under truncation. + expect( + csharpSuffixFallbackAllowed('System.Threading.Tasks', { + declaredNamespaces: new Set(['System.Threading']), + rootNamespaces: new Set(['System']), + truncated: true, + }), ).toBe(true); }); });