abhigyanpatwari/GitNexus · warning

[csharp] namespace scan of ${repoRoot} truncated (dir cap ${

Error message

[csharp] namespace scan of ${repoRoot} truncated (dir cap ${CSHARP_SCAN_MAX_DIRS}, depth cap ${CSHARP_SCAN_MAX_DEPTH}, an unreadable directory, or an unreadable .cs file); the #1881 suffix-fallback gate fails open for unmatched usings

What it means

The C# config scan builds the set of declared namespaces by walking repoRoot (caps: CSHARP_SCAN_MAX_DIRS = 20,000 directories, CSHARP_SCAN_MAX_DEPTH = 24) and reading .cs files; the result is flagged truncated when any cap hits or a directory/.cs read is rejected. On truncation the #1881 suffix-fallback gate fails open repo-wide: unmatched usings fall back to suffix matching instead of being gated by declared namespaces.

Source

Thrown at gitnexus/src/core/ingestion/language-config.ts:458

      (name) => collectDeclaredNamespaces(path.join(dir, name), declaredNamespaces, rootNamespaces),
      { concurrency: CSHARP_SCAN_READ_CONCURRENCY },
    );
    // A `.cs` that was 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. The scan streams each
    // file, so file size no longer trips truncation. A rejected read arrives
    // here as `undefined`, which is `!== 'ok'` just like the old
    // `r.status !== 'fulfilled'` arm.
    for (const r of csResults) {
      if (r !== 'ok') truncated = true;
    }
  }

  if (truncated) {
    // Surface the fail-open so an incomplete scan (dir/depth cap, or an
    // unreadable directory or `.cs` file) silently disabling the #1881 gate
    // repo-wide is observable (#4) rather than a mystery edge regression.
    logger.warn(
      `[csharp] namespace scan of ${repoRoot} truncated (dir cap ${CSHARP_SCAN_MAX_DIRS}, depth cap ${CSHARP_SCAN_MAX_DEPTH}, an unreadable directory, or an unreadable .cs file); the #1881 suffix-fallback gate fails open for unmatched usings`,
    );
  }
  return { configs, declaredNamespaces, rootNamespaces, truncated };
}

// Generous soft budget for locating `<RootNamespace>`: a real .csproj declares
// it in the first PropertyGroup near the top, so this is only reached by a
// pathological project file with a huge leading ItemGroup and no early
// RootNamespace. On hit we OMIT the config rather than guess a root (Codex F4).
const CSPROJ_ROOT_SCAN_MAX_BYTES = 4 * 1024 * 1024;
// Overlap kept across stream chunks so a `<RootNamespace>` tag straddling a
// chunk boundary is still matched (the tag + a short namespace value fit well
// within this window).
const CSPROJ_TAG_OVERLAP = 512;

/**
 * Stream a `.csproj` just far enough to find `<RootNamespace>`, in constant

View on GitHub (pinned to 0d1aed942f)

Solutions

  1. Fix unreadable directories/.cs files (permissions) so every read succeeds
  2. Exclude bin/obj/vendor trees via ignore rules to get under the dir and depth caps
  3. Restructure project folders nested deeper than 24 levels
  4. If truncation persists, treat unmatched-using resolution as fail-open when reviewing results

Example fix

# before — committed bin/obj push scan over 20000 dirs
bin/
obj/
 (committed)

# after
echo 'bin/
obj/' >> .gitignore
git rm -r --cached bin obj
Defensive patterns

Strategy: validation

Validate before calling

import { readdirSync, accessSync, constants } from 'node:fs';

function auditCsharpScan(root: string): { dirs: number; maxDepth: number; unreadable: string[] } {
  let dirs = 0, maxDepth = 0;
  const unreadable: string[] = [];
  const walk = (dir: string, depth: number) => {
    dirs++; maxDepth = Math.max(maxDepth, depth);
    if (dirs > 20_000 || depth > 24) return;
    let entries;
    try { entries = readdirSync(dir, { withFileTypes: true }); }
    catch { unreadable.push(dir); return; }
    for (const e of entries) {
      if (e.isDirectory()) walk(`${dir}/${e.name}`, depth + 1);
      else if (e.name.endsWith('.cs')) {
        try { accessSync(`${dir}/${e.name}`, constants.R_OK); }
        catch { unreadable.push(`${dir}/${e.name}`); }
      }
    }
  };
  walk(root, 0);
  return { dirs, maxDepth, unreadable };
}
const a = auditCsharpScan(repoRoot);
if (a.dirs >= 20_000 || a.maxDepth >= 24 || a.unreadable.length > 0) {
  console.warn('C# namespace scan would truncate', a);
}

Type guard

function csharpScanWillComplete(a: { dirs: number; maxDepth: number; unreadable: string[] }): boolean {
  return a.dirs < 20_000 && a.maxDepth < 24 && a.unreadable.length === 0;
}

Prevention

When it happens

Trigger: Directory count over 20,000, project nesting deeper than 24 levels, or an unreadable directory/.cs file (permissions) during the namespace scan — any of these sets truncated=true.

Common situations: Solution folders with deep obj/bin nesting; permission-restricted checked-in folders; monorepos above the dir cap. Observable symptom: suspiciously permissive using-directive matches (fail-open suffix fallback).

Related errors


AI-assisted analysis of abhigyanpatwari/GitNexus@0d1aed942f (2026-08-20). Data as JSON: /api/errors/eae64694148e0de3. Report an issue: GitHub.