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:
- 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.
- 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
- Assemble the final scan result deterministically from inventory and import-extraction artifacts.
- Require
importMap in every newly generated scan result.
- 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
[].
- Make
compute-batches.mjs fail with a clear diagnostic when importMap is absent, incomplete, or malformed instead of defaulting to {}.
- 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
Scope note
These defects may be easier to track as two implementation tasks while retaining a shared end-to-end acceptance test:
- Deterministic scan-result assembly and strict
importMap validation.
- 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
What happened?
Environment
/understandanalysis and subsequent incremental runsSummary
The
/understandpipeline can produce an edgeless or materially incomplete dependency graph for C# projects because of two independent defects:scan-result.jsoncan omitimportMapwhen the deterministic inventory output is written directly as the final scan artifact without merging the import-extraction result.using Namespace;directives are resolved as though the namespace identified a single.csfile, even though an ordinary C#usingdirective 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
importMapfrom being silently accepted.The resulting graph may have complete file-level inventory while still missing meaningful import topology, semantic batching inputs,
batchImportData, andimportsrelationships.Environment
2.7.6/understand --fulland subsequent incremental runsMinimal reproduction
Given a C# project such as:
Expected behavior
importMapwith exactly one key for every scanned file.using Example.App.Services;directive is represented as a namespace-level dependency.RequestService, resolves unambiguously within the importing project's reference closure.Observed behavior
scan-result.jsoncan contain noimportMapproperty.Example.App.Servicesis treated as a filename-like suffix such asExample/App/Services.cs.compute-batches.mjstreats an absent map as an empty object and allows generation to continue without import topology.Confirmed defect 1: final scan assembly can omit
importMapscan-project.mjsdeliberately 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 separateextract-import-map.mjsoutput before writing the finalscan-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 noimportMap.The failure is silently masked by this fallback in batching:
This converts a broken scan contract into an edgeless dependency graph instead of failing before analysis.
Proposed correction
importMapin every newly generated scan result.importMapis an object;[].compute-batches.mjsfail with a clear diagnostic whenimportMapis absent, incomplete, or malformed instead of defaulting to{}.Confirmed defect 2: C# namespaces are resolved as type filenames
The C# extractor correctly captures the dotted source of an ordinary
usingdirective. The resolver then applies Java/Kotlin-style dotted type-to-file resolution: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:
global using;The current structural import representation also loses the distinction among:
using Namespace;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:
This differs from package systems where importing a package is itself the compilation dependency. A C# ordinary
usingdirective 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" ] } }importMapshould 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:
Namespace identities should be project-qualified because the same namespace name may exist in more than one assembly.
Concrete file-edge policy
Emit
file -> fileimport edges only when concrete evidence exists, for example:using static Namespace.Type;resolves to a declared type;If multiple unrelated candidates remain, emit only the namespace-module dependency. Never select the first matching
.csfile arbitrarily.The namespace metadata should not feed file-level Louvain batching directly. Existing concrete
importMapedges 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:
usingdirectives;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.mjsskills/understand/extract-import-map.mjsskills/understand/compute-batches.mjsskills/understand/merge-batch-graphs.pyagents/project-scanner.mdpackages/core/src/types.tspackages/core/src/plugins/extractors/csharp-extractor.tsThe C# extractor needs to preserve enough information to distinguish:
Backward compatibility
The change can remain additive and backward-compatible:
importMap: Record<string, string[]>unchanged.modulenode type for project and namespace modules.importMap.Required tests
C# extraction and resolution
importMap.Scan assembly and batching
importMap.importMapkeys exactly matches the number of scanned files.Graph assembly
Incremental integration
Acceptance criteria
scan-result.jsoncontains a completeimportMap.using Namespace;no longer attempts to resolveNamespace.cs.Scope note
These defects may be easier to track as two implementation tasks while retaining a shared end-to-end acceptance test:
importMapvalidation.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