DepMesh — making file dependencies part of project architecture en ru
Coding agents often need to answer practical questions before making changes, such as:
- Which tests should be read before modifying this file?
- Which specifications govern this module?
- Which modules does this code import?
- Which artifacts are affected by this specification change?
- etc.
In other words, the agent must discover all dependencies between files and add them to the context to complete the task correctly.
To answer such questions, the agent must think, form a plan of action, execute it, and analyze the results. All of this consumes tokens, context, and time, without guaranteeing completeness and correctness of the result.
For example, the agent must decide how to search for each specific type of dependency. Sometimes, as in the case of import chains, the agent must read and parse the source code to understand which modules it imports, then generate paths to the corresponding files and read them — this is resource-intensive and inefficient.
The result of such "agentic search" is not guaranteed, the agent may forget to grep by function name and lose an important dependency, or miss a niche spec because it decided it is not needed in this particular case.
Moreover, for searching, the agent uses a bunch of tools, every one of which, by its very use, increases token consumption and eats up context. For the same task, the agent may choose different tools or call them with different parameters from time to time, which kills predictability and reproducibility of the result.
The standard ways to improve the situation are to integrate LSP or something similar as an agent tool, or to deploy one of the countless RAG systems. This helps, but does not solve all problems. For example, it does not guarantee completeness, minimality, and determinism of the result.
Meanwhile, since ancient times we have had a huge pool of utilities and libraries for file searching and source code analysis that can do the same work quickly and efficiently, without any LLMs. Each of them works much better than a probabilistic model in its specific area.
It would be great, I thought, to have a single abstraction over all these tools that provides the agent with a universal interface for extracting all dependencies between files (whatever you configure) and does not require it to think about what to search for or how to search for it.
So, it could be used, for example, like this:
> depmesh dependencies -r governed_by -r tested_by ./depmesh/cli/application.py
## governed_by
Specifications that apply to the artifact.
- @/specs/architecture/entities.md
- @/specs/architecture/errors.md
- @/specs/architecture/modules_layout.md
- @/specs/architecture/naming.md
- @/specs/architecture/static_analysis.md
- @/specs/architecture/tests.md
- @/specs/behavior/cli.md
- @/specs/behavior/file_paths.md
## tested_by
Tests that verify the artifact.
- @/depmesh/cli/tests/test_application.py
As a result, DepMesh was born.
How DepMesh works
DepMesh is a CLI tool with a configuration file (depmesh.toml) in the root of your project.
In the config, you specify two things:
- Kinds of relationships between files.
- Rules for searching related files.
The simplest config can look like this:
version = 1
[[relations]]
id = "tested_by"
description = "Tests that verify the artifact."
[[rules]]
relation = "tested_by"
input = { type = "glob", pattern = "./src/{**package_path}/{*module}.py" }
output = { type = "files", pattern = "./src/{package_path}/tests/test_{module}.py" }
In it, we define one type of dependency tested_by and a rule "Every source file .../module.py is connected by the tested_by relationship to the file .../tests/test_module.py".
Each rule is defined by three parameters:
relation— the type of relationship we are defining.input— a condition that determines whether we can apply the rule to the given file.output— a generator of file paths that are related to the input file through the specified relationship.
Besides the glob patterns from the example above, DepMesh supports calling third-party utilities, so you can analyze files however you want: use linters, LSP servers, parse source code with regular expressions, query a database, call an HTTP API, and so on. And all this without spending tokens or polluting the agent's context.
The syntax of the rules is quite powerful and flexible; for example, in one rule you can specify multiple file patterns and combine several generators.
You can find detailed documentation on how to work with the utility and its configuration in depmesh/README.md.
DepMesh does not infer reverse relationships
For a dependency like file --tested_by--> test_file, DepMesh cannot automatically infer the reverse dependency test_file --tests--> file.
This is a conscious decision, as the asymmetry in how we organize information means direct and reverse dependency searches can differ by orders of magnitude in time and resource cost. For example, the direct dependency imports can be processed very quickly (you just need to read one file and analyze its imports), while the reverse dependency imported_by may require traversing the entire project.
That's why the decision on which relationships to support is entirely up to the user.
Over time I'll improve this aspect, but for now we will follow the principle of "explicit is better than implicit" and define all dependencies explicitly in the config.
How I use DepMesh
You can find examples of real configs in the tool's own repository depmesh/depmesh.toml, as well as in the repository of my news reader feeds.fun/depmesh.toml.
Kinds of relationships
In my projects, I define the following relationships:
tested_by— tests that verify the source file.tests— source files that are verified by the tests.imports/uses— source files that are imported/used by this source file.imported_by/used_by— source files that import/use this source file.governed_by— specifications that govern the artifact.governs— artifacts that are governed by this specification.terms_defined_by— dictionaries that contain definitions of terms used in the file.defines_terms_for— files for which the dictionary contains definitions of terms.indexed_by— index files (index is a particular type of specification) that contain references to this file.indexes— files that are contained in this index.
You can define relationships between any files
DepMesh is "blind" to the type and content of files — their semantics are up to you.
As an example, I have specifications that govern other specifications: meta/general.md and here is part of the config for it:
# Every specification document is governed by the general specification rules.
# Note: general.md governs itself too.
[[rules]]
relation = "governed_by"
input = { type = "glob", pattern = "./specs/**/*.md" }
output = { type = "list", artifacts = ["./specs/meta/general.md"] }
# The general specification rules govern every specification document.
# Note: general.md governs itself too.
[[rules]]
relation = "governs"
input = { type = "one_of", artifacts = ["./specs/meta/general.md"] }
output = { type = "files", pattern = "./specs/**/*.md" }
Discovering relationships
I strive to define relationships at the file path level — standard names, standard paths, as in the case of tests: ./src/module.py always has a paired ./src/tests/test_module.py. I started doing this long before the rise of AI — it's convenient for humans; also, glob patterns work very quickly. For example, here is the agent spec for the layout of the Feeds Fun backend modules ./specs/backend_architecture/modules_layout.md.
However, with imports, of course, this trick does not work.
For Python, I use tach — a linter for dependencies between modules. Besides being able to return them in a script-friendly format, it is actually a linter. You can describe the import rules in the project, and it will check that they are followed, which is very convenient.
For Rust, I use a vibe-coded wrapper around cargo modules dependencies. Unfortunately, there are no established utilities for dependency analysis in Rust (which is strange). If you want to contribute to the community — this is a great opportunity — there is no conceptual complexity, you just need to spend some time.
Profit
- The agent does not need to think where it shouldn't have to. This means it spends fewer tokens, does not pollute the context, and works faster.
- The agent does not miss dependencies, which means it makes fewer mistakes and behaves more predictably.
- You can use DepMesh as a base component for more complex automation, as a universal interface to dependencies is needed not only by agents.
In the context of the last point, I am currently experimenting with a system that automatically monitors whether all project files are synchronized with each other, using DepMesh to identify dependencies and Donna to resolve desynchronization.
Read next
- Donna is ready to use
- Donna — predictability and controllability for your agents
- Notes on coding agents
- My GPTs and prompt engineering
- LLM agents are still unfit for real-world tasks
- Prompt engineering: building prompts from business cases
- Migrating from GPT-3.5-turbo to GPT-4o-mini
- Top LLM frameworks may not be as reliable as you think
- Dungeon generation — from simple to complex
- World Builders 2023: Preferences of strategy players