Menu

#193 [Feat] Related Files — find and suggest files connected to a selection

open
nobody
None
2026-08-08
2026-08-08
Anonymous
No

Originally created by: Avazbek22

The problem

Picking files by hand is DevProjex's strength — you see exactly what leaves your project. But it has a cost that grows with project size: the user has to know which files matter.

A typical situation. The user wants to ask an AI about AuthService.cs. They check it in the tree and export. The model answers poorly — it never saw IUserRepository, LoginRequest, AuthResult, the types the class is built from. The user didn't skip them on purpose; they just didn't hold the whole dependency picture in their head. Nobody does, past a few dozen files.

Today the user's options are bad: check whole folders (context bloats, tokens explode) or chase imports manually through the source (tedious, error-prone — the exact pain that pushes people back to CLI dump-everything tools).

Competing tools solve this by taking the human out: codesurf ranks files by query keywords and silently drops the rest; aider builds a repo map internally and never shows it. Both fail invisibly — when the ranking guesses wrong, the user gets a confidently bad answer and never learns why.

DevProjex should solve it the DevProjex way: the machine finds and explains, the human sees and decides.

The feature

Right-click a file in the tree → "Find related files".

DevProjex analyzes the project's code and shows a panel of connected files, each with a human-readable reason and a token cost. Nothing gets checked automatically — the user reviews, unchecks what they don't want, confirms. Confirmed files get checked in the tree like any manual selection.

Sketch of the interaction (final UI/UX is open — see the UI section):

right-click AuthService.cs  "Find related files"

┌─ Related to AuthService.cs ────────────────────────────┐
                                                        
  Dependencies (what this file uses)          +1,840 tk 
   IUserRepository.cs     constructor parameter type   
   IPasswordHasher.cs     constructor parameter type   
   ITokenService.cs       field type                   
   AuthResult.cs          return type                  
   LoginRequest.cs        method parameter type        
                                                        
  Dependents (what uses this file)            +4,120 tk 
   AuthController.cs      constructs AuthService       
   AuthServiceTests.cs    references AuthService       
                                                        
  Ambiguous                                             
   User  3 candidate declarations                     
                                                        
                              [Cancel]  [Add selected]  
└────────────────────────────────────────────────────────┘

Both directions matter and serve different jobs:

  • Dependencies — "give the AI everything this file needs" (the export case).
  • Dependents — "show me everything that touches this" (the review/refactoring case: I'm changing this interface, what breaks?).

Why this is worth building

This is the strongest objection to visual selection — "in a big repo you'll click forever" — answered without giving up visibility. It's also a combination no competitor has: graph suggestion + visible tree + manual confirmation + live token delta + the same Smart Ignore / Smart Secrets / Compression pipeline underneath. codesurf has the graph but hides the decision; aider has the map but locks it inside its own session; CLI packers have neither.

Hard-won context (read before designing)

This section exists so you don't re-walk paths we already walked. The conclusions below came from real research and real measurement — treat them as strong defaults, not dogma. If you find a genuinely better way, say so and explain why.

Do NOT adopt an external graph engine

We evaluated DeusData/codebase-memory-mcp (38k★, C, MIT, tree-sitter-based, real hybrid semantic resolvers including a serious C# binder model) and its fork network in depth. Conclusion: use nothing from it as a dependency. Reasons, in order of weight:

  1. It's a full application (MCP server, SQLite, CLI, watcher, 158 grammars), not a library. No stable embeddable ABI exists, in the main repo or any fork.
  2. It's a fast-moving neighbor-competitor. Its most serious fork sits 918 commits diverged with an unmergeable PR — that's the fate of anyone who couples to this codebase.
  3. It ships no Windows ARM64 binary; DevProjex releases on win-arm64.
  4. We just paid weeks to deliver ONE native dependency (tree-sitter grammars: single-file loading, signing, 6 RIDs, MSIX shape). A second native C engine, forked and self-maintained, is a project, not a feature — and this is a one-person project.
  5. Their own issue tracker documents the failure mode we must avoid: heuristic fallbacks (suffix match) linking production code to test mocks, and overloaded methods collapsing in the graph.
    CBM is useful exactly once: as a comparator in the pre-implementation spike (below).

Build on what already exists in this codebase

By the time this feature is built, DevProjex already has (from the Code Compression work):

  • tree-sitter parsing for 10 languages (C#, Java, Python, JavaScript, TypeScript, TSX, Go, Rust, C, C++), grammars delivered and loaded on all 6 release RIDs;
  • per-language query packs as data (bodies.scm, declarations.scm, manifests) — adding queries is adding files, not code;
  • a declarations index already built per file for the compression safety gate;
  • a proven cache pattern (path + content fingerprint + grammar identity + query hash + revision) used by both SecretScanCache and the compression cache;
  • the principle, enforced in code, that heuristics propose and humans decide, and that ambiguity is never silently converted into a confident answer.
    The related-files engine should be a thin layer on top of this, not a new subsystem.

Extraction model that survived review

Three evidence layers. Internally richer than what the user sees; externally it collapses to simple confidence.

Layer A — explicit relations. Imports/includes/module references, per language: Python import/from, TS/JS import (respecting tsconfig paths / package.json / index files), Go import paths, Java imports + source roots, C/C++ #include, Rust use/mod. One query file per language.

Honesty requirement learned in review: an import names a module, package, or header — not always a file. Python imports may hit __init__ re-exports; Go imports a directory; a C++ header is not its implementation file; TS resolution depends on config. So model these as evidence (Import, Include, ModuleReference, ProjectReference) plus a resolution status (Resolved — unique target in the manifest, Ambiguous — multiple candidates, External — outside the project, Unresolved). Only Resolved explicit relations are top-confidence.

Layer B — syntactic type relations. This is what makes C# work (C# has no file import — using names a namespace smeared across files), and it improves every other language too. Build: declarations index with namespaces/packages × type references extracted only from type syntactic positions × import/using context of the referencing file.

Type positions, not bare identifiers — this distinction is load-bearing. Field/property/parameter/return types, base types and interfaces, generic type arguments and constraints, attributes/annotations/decorators, new T(...), typeof(T), casts and type patterns, fully-qualified names. Matching every identifier against the declarations index was proposed and rejected: local variables and method names collide with type names and drown the result in noise. Restricting to type positions kills the noise before resolution. Nice side effect: AddScoped<IUserRepository, UserRepository>() yields two type-reference facts for free, covering most DI-registration cases without any framework awareness.

Unique target → strong confidence. Multiple targets → an ambiguous group, never a guess.

Layer C — heuristics (bare identifier matches, framework conventions, reflection hints). Not in v1. If ever added: off by default, never pre-checked.

Symbol identity — keep it minimal

ScopeId + LanguageId + SymbolKind + QualifiedName + GenericArity, with declaration sites as a list of files. Two things were explicitly removed after review: file path in the key (breaks C# partial classes — one symbol, several files) and parameter arity (belongs to a call graph; this is a file graph, overloads don't participate).

Monorepo scopes — separate but connectable

DevProjex's scope isolation (from Smart Ignore) stays. But a flat "edges never cross scopes" rule was rejected: apps/web → packages/shared-types is the reason monorepos exist. Policy: an explicit, resolved relation (project reference, workspace dependency, resolved import) may cross scopes and is flagged CrossScope — shown, perhaps grouped separately. A heuristic relation without workspace evidence may not cross. Name coincidence between neighboring projects must never create an edge.

The pipeline position and the AST lifecycle

Facts are extracted from original source, before compression cuts bodies (identifiers live in bodies; the graph must see them even though layer B mostly reads signatures — attributes and new T(...) sit inside bodies too). Order: parse → graph facts → compression edits → secrets → preview/export.

Do not assume parsed trees are lying around: compression parses only selected files and frees each Tree immediately (a hard rule from that feature — trees cost ~80 bytes per source char). The dependents direction needs facts for the whole eligible scope, so the first "Find related" triggers a background indexing pass — one parse per file, extract compact facts, free the tree immediately, keep only the facts. Subsequent calls are cache hits. One changed file re-indexes one file.

Two cache levels, not one

This was a genuine design catch: file facts and graph resolution invalidate differently.

  • File-facts cache: path + content fingerprint + language + grammar identity + query hashes. Holds imports, declarations, type references, module identity, diagnostics. Never holds trees or source.
  • Resolution generation: manifest generation + declarations-index revision + resolver-config fingerprint (tsconfig/jsconfig/package.json, .csproj + project references + global usings, go.mod/go.work, Cargo.toml, include paths...). Changing one tsconfig.json re-resolves hundreds of files that were not re-parsed. The design must express that: re-resolve without re-parse.

File-manifest ownership

DevProjex owns discovery. Smart Ignore, Git mode, filters, and scopes produce the manifest; the graph engine consumes it. There is exactly one scanner in this application. A second one inside the graph would eventually disagree with the tree — the class of bug we refuse to create.

UI/UX — direction, not prescription

Entry point: context menu on a file (later perhaps on a multi-selection). Beyond that, the layout above is a sketch. What must hold, whatever the final design:

  • Reasons are human sentences ("constructor parameter type", "resolved import"), not codes or scores. Multiple reasons for one file aggregate into one row ("field type, line 8 · parameter type, line 14"), never duplicate rows.
  • Dependencies and dependents are visually separate — they answer different questions and mixing them (as the early sketch did with Program.cs) confuses both.
  • Defaults follow confidence: resolved/strong pre-checked, ambiguous and possible unchecked. An ambiguous name renders as one expandable group of candidates ("User — 3 candidates"), not as N independent rows.
  • Token cost is visible before confirming — per group at least, ideally per file. This ties into the live counters users already trust.
  • Nothing applies without confirmation. Confirmed files become ordinary checked files in the tree — no special state to manage afterwards, fully undoable by unchecking.
  • Depth is 1 by default. If a "go deeper" affordance exists, each hop is an explicit user action, never automatic closure.
  • Empty and partial results are honest: "no related files found in the current scope" is a valid outcome; unresolved references can be shown in a diagnostics expander for the curious, never as suggestions.
    Naming note: "Find related files" (or similar) — a verb that promises finding, not adding. Early draft said "Add related files" and it read as if clicking would mutate the selection.

Known v1 limitations — record, don't fix

  • interface → implementation is not a dedicated edge kind (DI type arguments cover the common case via layer B).
  • Reflection, dynamic imports, codegen → Unresolved, diagnostics only.
  • No call graph. File-level edges only.
  • Depth 1; no transitive closure.

Out of scope (v1, most of it permanently)

Ranking/PageRank, token-budget fitting, automatic inclusion or exclusion, embeddings, graph visualization, SQLite or any persistent store beyond the existing cache pattern, a second native library, SCIP integration, framework-specific analyzers.

Pre-implementation spike (~1 day)

Run CBM as a separate pinned process (comparator, not ground truth — it errs too) on 3 repositories: a C# solution, a mixed monorepo, a TS workspace. 10 seed files, ~50 hand-verified relationships. The question is not a precision percentage. The question is: which classes of edges does the layered scheme miss versus CBM, and how much noise does layer B produce on real C#? If the answer is "misses little, noise near zero" — build. If layer B drowns in ambiguity on real code — rethink before writing the feature.

Hard C# cases to include as fixtures (sourced from CBM's own issue tracker and review): DI registrations, overloads, alias/global usings, duplicate class names across namespaces, interface/implementation pairs, extension methods, nested types, partial classes.

Acceptance

  • Right-click AuthService.cs → "Find related files" → the panel lists its field/parameter/return types under Dependencies, each with a reason; nothing is applied until confirmed.
  • Right-click IUserRepository.cs → Dependents lists the files that reference it.
  • Five same-named candidates appear as one ambiguous group, never as one invented edge.
  • A false suggestion costs one uncheck; confirming produces ordinary checked files in the tree.
  • Second invocation on an unchanged project is instant (cache); editing one file re-indexes only that file; editing tsconfig.json re-resolves without re-parsing.
  • Explicit apps/web → packages/shared-types crosses scopes and is flagged; a name coincidence between sibling projects never creates an edge.
  • Works across the 10 compression languages; C# flows through layer B.
  • Deterministic: same project, same config → same suggestions, byte for byte.

Fixture note for whoever implements

Test the mechanics, not a language × category matrix: resolution statuses, ambiguity grouping, cache invalidation on file change vs. config change, cross-scope policy, determinism, partial-class identity, the background indexing lifecycle (trees freed, facts kept). Human confirmation is the safety net for individual wrong suggestions; the fixtures guard the machinery that produces them. Roughly fifteen focused tests beat two hundred golden files.

Discussion


Log in to post a comment.