Skip to content

bug: Full scans can omit importMap, and C# namespace using directives are resolved as file paths #635

Description

@Phenom3Xii

What happened?

Environment

  • AI coding environment: Codex App
  • Invocation: Understand Anything skill invoked from Codex App
  • Affected workflows: full /understand analysis and subsequent incremental runs
  • Operating system: Windows
  • Project shape: multi-project .NET/C# solution with namespace-based imports, project references, tests, migrations, documentation, and non-code files

Summary

The /understand pipeline can produce an edgeless or materially incomplete dependency graph for C# projects because of two independent defects:

  1. The final scan-result.json can omit importMap when the deterministic inventory output is written directly as the final scan artifact without merging the import-extraction result.
  2. Ordinary C# using Namespace; directives are resolved as though the namespace identified a single .cs file, even though an ordinary C# using directive normally imports a namespace rather than a source file.

These defects are independent. Fixing scan assembly alone still leaves C# namespace resolution incorrect. Fixing C# resolution alone does not prevent a missing importMap from being silently accepted.

The resulting graph may have complete file-level inventory while still missing meaningful import topology, semantic batching inputs, batchImportData, and imports relationships.

Environment

  • Understand Anything version: 2.7.6
  • Affected workflows: /understand --full and subsequent incremental runs
  • Platform observed: Windows
  • Project shape: multi-project .NET/C# solution with namespace-based imports, project references, tests, migrations, documentation, and non-code files

Minimal reproduction

Given a C# project such as:

// src/App/Services/RequestService.cs
namespace Example.App.Services;

public class RequestService
{
}
// src/App/Consumers/RequestConsumer.cs
using Example.App.Services;

namespace Example.App.Consumers;

public class RequestConsumer
{
    private readonly RequestService _service;
}

Expected behavior

  • The final scan result contains a complete importMap with exactly one key for every scanned file.
  • An ordinary using Example.App.Services; directive is represented as a namespace-level dependency.
  • A concrete file dependency may also be emitted when a referenced type, such as RequestService, resolves unambiguously within the importing project's reference closure.

Observed behavior

  • scan-result.json can contain no importMap property.
  • When C# imports are resolved, Example.App.Services is treated as a filename-like suffix such as Example/App/Services.cs.
  • That path generally does not exist, so the directive resolves to no internal file target.
  • compute-batches.mjs treats an absent map as an empty object and allows generation to continue without import topology.
  • Batch import data and file-level import relationships are consequently empty or incomplete despite successful source parsing.

Confirmed defect 1: final scan assembly can omit importMap

scan-project.mjs deliberately produces the deterministic inventory subset, including files, categories, line counts, ignore accounting, complexity, and statistics. Its documented workflow requires the project-scanner stage to merge this inventory with the separate extract-import-map.mjs output before writing the final scan-result.json.

The merge is currently orchestration-driven rather than enforced by deterministic code. If the inventory output is written directly to scan-result.json, the artifact appears structurally valid but contains no importMap.

The failure is silently masked by this fallback in batching:

const importMap = scan.importMap || {};

This converts a broken scan contract into an edgeless dependency graph instead of failing before analysis.

Proposed correction

  1. Assemble the final scan result deterministically from inventory and import-extraction artifacts.
  2. Require importMap in every newly generated scan result.
  3. Validate that:
    • importMap is an object;
    • every scanned path appears exactly once as a key;
    • every value is an array;
    • every target is an existing internal scanned path;
    • non-code and unresolved files use [].
  4. Make compute-batches.mjs fail with a clear diagnostic when importMap is absent, incomplete, or malformed instead of defaulting to {}.
  5. Recompute deterministic inventory and import metadata before incremental batching. Only the LLM analysis of unchanged files should remain incremental.

Confirmed defect 2: C# namespaces are resolved as type filenames

The C# extractor correctly captures the dotted source of an ordinary using directive. The resolver then applies Java/Kotlin-style dotted type-to-file resolution:

resolveDottedFqn(rawImport, ".cs", ctx.csIndex)

That model can be appropriate when an import identifies a concrete type that conventionally maps to a source filename. It is not correct for an ordinary C# namespace import.

C# namespaces:

  • do not identify one source file;
  • may be declared across many files;
  • need not match directory structure;
  • may span multiple source roots;
  • may use the same name in multiple projects or assemblies;
  • may be introduced through global using;
  • do not prove that every type in the namespace is used by the importer.

The current structural import representation also loses the distinction among:

  • ordinary using Namespace;
  • alias using Alias = Namespace.Type;
  • using static Namespace.Type;
  • global using Namespace;

These forms require different resolution behavior.

Why namespace imports should not expand to every declaring file

Mapping one using Example.App.Services; directive to every file declaring that namespace would represent name availability as concrete dependency.

That approach would:

  • create misleading all-to-all file relationships;
  • imply use of types that are never referenced;
  • produce excessive edge counts and high-degree hubs;
  • distort Louvain batching and cross-batch neighbor maps;
  • trigger avoidable truncation or deduplication pressure;
  • duplicate namespace membership relationships;
  • obscure evidence-backed type-level dependencies.

This differs from package systems where importing a package is itself the compilation dependency. A C# ordinary using directive primarily brings names into scope.

Proposed additive representation

Preserve the existing file-level contract:

{
  "importMap": {
    "src/App/Consumers/RequestConsumer.cs": [
      "src/App/Services/RequestService.cs"
    ]
  }
}

importMap should remain strictly file-to-file and contain only concrete, evidence-backed internal dependencies.

One possible additive representation for C# namespace metadata is:

{
  "namespaceImportMap": {
    "src/App/Consumers/RequestConsumer.cs": [
      "csharp-namespace:src/App/App.csproj:Example.App.Services"
    ]
  },
  "csharpNamespaces": {
    "csharp-namespace:src/App/App.csproj:Example.App.Services": {
      "name": "Example.App.Services",
      "projectPath": "src/App/App.csproj",
      "declaringFiles": [
        "src/App/Services/RequestService.cs",
        "src/App/Services/NotificationService.cs"
      ]
    }
  }
}

The exact field and identifier names are illustrative. The important contract is to keep project-qualified namespace relationships separate from concrete file dependencies.

Graph assembly could materialize bounded module nodes:

module:csharp-project:<project>
  contains -> module:csharp-namespace:<project>:<namespace>
    contains -> declaring file nodes

importing file
  imports -> namespace module
  imports -> concrete file nodes only when resolution is evidence-backed

Namespace identities should be project-qualified because the same namespace name may exist in more than one assembly.

Concrete file-edge policy

Emit file -> file import edges only when concrete evidence exists, for example:

  • using static Namespace.Type; resolves to a declared type;
  • an alias target resolves to a declared type rather than a namespace;
  • a referenced type, base class, implemented interface, attribute, object construction, qualified symbol, or member access resolves uniquely within the importer's project-reference closure;
  • a partial type resolves to all files that jointly declare that type.

If multiple unrelated candidates remain, emit only the namespace-module dependency. Never select the first matching .cs file arbitrarily.

The namespace metadata should not feed file-level Louvain batching directly. Existing concrete importMap edges may continue to drive batching.

Incremental-generation requirement

Incremental generation currently reuses preserved scan metadata. Changes to any of the following can therefore be evaluated against stale dependency information:

  • using directives;
  • namespace declarations;
  • declared types;
  • project references;
  • added, deleted, renamed, or moved files.

The deterministic inventory and import metadata should be recomputed for the full eligible inventory before selecting changed analysis batches. This is substantially cheaper than repeating LLM analysis and avoids complex reverse-invalidation rules.

Likely implementation surface

The smallest affected surface appears to include:

  • skills/understand/scan-project.mjs
  • skills/understand/extract-import-map.mjs
  • skills/understand/compute-batches.mjs
  • skills/understand/merge-batch-graphs.py
  • agents/project-scanner.md
  • packages/core/src/types.ts
  • packages/core/src/plugins/extractors/csharp-extractor.ts
  • C# extractor, scan/import-map, batch, merge, and incremental integration tests

The C# extractor needs to preserve enough information to distinguish:

  • declared namespaces;
  • fully qualified declared types;
  • ordinary, alias, static, and global using directives;
  • alias targets where applicable.

Backward compatibility

The change can remain additive and backward-compatible:

  • Keep importMap: Record<string, string[]> unchanged.
  • Add namespace metadata as optional fields.
  • Preserve existing behavior for non-C# language resolvers.
  • Keep existing graphs readable.
  • Use the existing module node type for project and namespace modules.
  • Allow older valid scan results to omit the new C# namespace fields.
  • Require every newly generated scan result to contain a complete base importMap.
  • Require a fresh deterministic scan for previously generated artifacts that lack or violate the base contract.

Required tests

C# extraction and resolution

  • Ordinary, global, alias, and static using directives.
  • File-scoped, block-scoped, nested, and multiple namespace declarations.
  • A namespace containing many files produces one bounded namespace dependency rather than one file edge per declaration.
  • An exact type reference produces concrete file targets only when unambiguous.
  • Ambiguous duplicate type names never select an arbitrary file.
  • Partial types retain all declaration files for the resolved type.
  • The same namespace in separate projects remains project-qualified.
  • External framework namespaces remain external or unresolved.
  • Duplicate and global usings produce deduplicated graph edges.
  • Tree-sitter initialization failure still produces a complete empty base importMap.

Scan assembly and batching

  • Final scan assembly always includes importMap.
  • The number of importMap keys exactly matches the number of scanned files.
  • Non-code files have empty arrays.
  • Missing, malformed, or incomplete maps fail before batch analysis with a clear diagnostic.
  • Namespace metadata does not create file-level Louvain fan-out.

Graph assembly

  • Project and namespace module nodes are created once and deduplicated.
  • Namespace membership and import edges contain no dangling references.
  • Concrete file-import recovery continues to deduplicate file-to-file edges.
  • No self-import edges are emitted.
  • Existing non-C# import-map behavior remains unchanged.

Incremental integration

  • Changing a namespace declaration refreshes affected metadata.
  • Adding, deleting, renaming, or moving a C# type refreshes concrete and namespace relationships.
  • Changing a project reference refreshes project-qualified namespace resolution.
  • Incremental generation recomputes deterministic inventory and import metadata before batching changed files.
  • Added files that were absent from a previous inventory cannot be omitted by reuse of stale scan metadata.

Acceptance criteria

  • Every newly generated scan-result.json contains a complete importMap.
  • Missing, malformed, or incomplete import maps fail before batch analysis.
  • Ordinary C# using Namespace; no longer attempts to resolve Namespace.cs.
  • C# namespace imports produce bounded project-qualified namespace relationships.
  • Concrete C# file import edges are emitted only with unambiguous evidence.
  • Ambiguous types never cause arbitrary file selection.
  • Namespace imports do not create all-to-all file dependencies.
  • File-level community detection continues to use concrete dependencies only.
  • Existing language resolvers and graph consumers remain compatible.
  • Full and incremental workflows have end-to-end coverage for the enforced scan-result contract and C# dependency model.

Scope note

These defects may be easier to track as two implementation tasks while retaining a shared end-to-end acceptance test:

  1. Deterministic scan-result assembly and strict importMap validation.
  2. Project-qualified C# namespace modeling and evidence-backed concrete type resolution.

Minimal reproduction

No response

Plugin version

2.7.6

Platform / client

Other (please describe in "What happened?")

OS + Node version

Windows 10 + Node 26.3.0

Primary language of the analyzed project

No response

Approximate file count of the analyzed project

No response

Relevant logs

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions