A field guide to any codebase, written by the code itself.
What depends on what, what changes together,
and who knows the part you are about to touch.
The first week on an unfamiliar codebase is spent asking questions nobody has written down. Where does this start. What breaks if I touch this. Why do these two files always seem to change together. Who do I ask about this part.
The usual answer is documentation, and documentation has one fatal property: it starts going stale the moment it is written. The architecture diagram in the wiki describes a system that existed eighteen months ago, and everybody knows it, so nobody trusts it, so nobody updates it.
The answers are already in the repository. The import graph knows what depends on what. The commit history knows what changes together, how often each file is touched, and who has touched it. Neither can go stale, because neither is written down — both are derived, every time you ask.
So this reads them instead of asking you to maintain a document.
Two lists, both derived: the files nothing imports, which is where execution begins, and the files everything leans on. This is hono, a repository neither of us wrote:
hono — 385 files, 960 imports between them, 1790 commits by 326 people over 4.7 years
entry points — nothing in the repository imports these
11 benchmarks/routers/src/bench.mts
7 benchmarks/routers-deno/src/bench.mts
most depended on — the modules everything leans on
72 src/hono.ts
71 src/context.ts
62 src/types.ts
Four hundred files, and three of them carry the whole design. That is the first thing worth knowing and the last thing anyone writes down.
Not a text search for the file name — the actual import chain, followed all the way, nearest first:
Changing src/core/types.ts reaches 47 files (26 of them import it directly).
Nearest first, with the shortest import chain:
1 src/core/bridge/adapt.ts
1 src/core/cache/codec.ts
1 src/core/git/parseLog.ts
2 src/node/indexer.ts
That is this repository describing itself. In the editor the same figure sits above the file you are looking at, as one line: 26 files import this — a change reaches 47.
This is the half that cannot be done with static analysis. Files that are committed together over and over, whether or not they refer to each other:
changes with — how often the other file changes in the same commit, then the reverse
71% / 58% 15x src/adapter/deno/serve-static.ts
62% / 43% 13x src/adapter/cloudflare-workers/serve-static.ts
Three runtime adapters in hono implementing the same feature separately. None of them imports another, so no compiler, linter or dependency graph will ever connect them — but change one and there is a two in three chance the others need the same change. Nobody writes that down either. You learn it by breaking it.
Ownership is worked out from lines written, and reported only when it is actually a problem — one person wrote nearly all of a file and has stopped contributing:
knowledge at risk — one person wrote it and has stopped contributing
91% by Kei Kamikawa, last seen 2.4 years ago src/utils/jwt/jws.ts
100% by watany, last seen 2.2 years ago src/adapter/bun/ssg.ts
On a small team one person owning a file is the normal state, so flagging that would fire on everything and be read as noise. The signal is the person having gone.
The same analysis runs as an MCP server, so an agent can ask directly instead of grepping its way to a worse answer. Six tools: overview, describe_file, impact_of, changes_with, hotspots, reindex.
An agent that asks impact_of gets the transitive answer in one call and about two hundred tokens. The same agent grepping for a file name reads forty files, still misses the indirect dependents, and spends fifty times the context getting there.
Field Guide needs Node 20 or newer and git on the PATH. It reads a repository, so
it does nothing useful outside one.
From the Marketplace — the search box in the Extensions view, or:
code --install-extension mkbaser.fieldguideOr take the signed VSIX from Releases:
code --install-extension fieldguide.vsixOr build it yourself:
git clone https://github.com/mustafakbaser/field-guide.git
cd field-guide && npm install && npm run package
code --install-extension fieldguide.vsixThe first minute. Open a repository and click the Field Guide icon in the activity bar. The first index takes a few seconds on a small repository and rather longer on a large one — reading the git log is two thirds of it — and it is written to a cache outside the working tree, so every later start is quick. Then:
- The Map view answers "where do I start reading": the files nothing imports, the files everything leans on, the files changing most, and the files nobody owns any more.
- The Active File view follows whatever you have open — what it imports, what imports it, how far a change to it reaches, and what tends to change alongside it.
- A line above each file says how many others import it.
fieldguide.codeLens.enabledturns it off.
If the view says there is no repository, the folder is not one, or it is not trusted yet — see the note at the end of this section. Anything that goes wrong is written to View → Output → Field Guide, at warning level, so it is visible without changing a setting.
The MCP server ships as one self-contained file, so there is nothing to build:
/plugin marketplace add mustafakbaser/field-guide
/plugin install fieldguide@mustafakbaser-tools
Then ask about a repository in plain language. The agent has six tools — overview,
describe_file, impact_of, changes_with, hotspots, reindex — and reaches for them
instead of grepping.
It speaks stdio and takes the repository as its argument:
{
"mcpServers": {
"fieldguide": {
"command": "node",
"args": ["/absolute/path/to/field-guide/dist/fieldguide-mcp.js", "/path/to/your/repo"]
}
}
}With no argument it maps whichever repository the working directory is in. --cache-dir
puts the index somewhere other than the platform cache directory, which matters in CI or a
sandbox.
The extension does nothing until you trust the folder, and that is not a formality: a
repository's own .git/config can name commands git executes — core.fsmonitor is run by
git ls-files — so reading an untrusted repository means running its author's code.
Every feature can be switched off on its own.
| Setting | Default | |
|---|---|---|
fieldguide.activeFile.enabled |
true |
The Active File view: what the open file imports, what imports it, how far a change reaches, what changes alongside it |
fieldguide.codeLens.enabled |
true |
The one line above each file saying how many others import it |
fieldguide.limit |
10 |
How many entries each section of the map lists |
fieldguide.coupling.minConfidence |
0.4 |
How strong a historical relationship has to be before it is shown. 0.4 means the other file changed in at least 40% of the commits that touched this one. Lower it to see more, at the cost of coincidences |
In a multi-root workspace each repository is indexed separately, in its own process, with its own cache.
The import graph is resolved through the TypeScript compiler's own module resolution rather than a reimplementation of it, so paths aliases, package exports maps and monorepo workspace packages reached through node_modules symlinks all resolve the way your build resolves them. Getting this wrong shows up as a map with missing edges and nothing to indicate it, which is the worst shape a bug can take here.
Change frequency is a commit count with each commit weighted by how recent it is, halving every 90 days. A raw count ranks by age on any long-lived repository: a file rewritten heavily five years ago and untouched since would outrank the one the team is fighting with this month.
Coupling is a conditional probability — of the commits that touched A, what share also touched B — reported in both directions, because the relationship is usually lopsided. Pairs seen fewer than three times, or under 30% confidence, are dropped. Commits touching more than 25 files are excluded outright: a formatter run or a dependency bump couples everything it touches to everything else it touches, and all of it is spurious.
Ownership is weighted by lines added rather than by commits, because someone who wrote a file in two large commits knows it better than someone who adjusted an import in ten small ones. Bus factor is the fewest people who between them account for half the file.
Freshness is established rather than assumed. Before answering, it checks HEAD, the set of tracked files, and the modification time of each of them, and re-reads anything that moved. Write a file and ask about it in the next breath and you get an answer about the file you just wrote. On a repository large enough that the check itself is slow, and only then, it is spaced out in proportion to what it costs.
No telemetry. No network requests. Not as a policy — there is no code anywhere in this project that opens a socket.
What it returns is a different question, and worth being precise about. In the editor the answers stay on your machine. Through the MCP server they become part of your agent's conversation, and that conversation goes wherever your agent already sends it — so treat them exactly as you treat the code the agent is already reading. Author names appear in ownership output; email addresses are used as an identity key and are never rendered.
Text that comes out of a repository — file paths, module specifiers, author names — is escaped before it is shown. A module specifier can carry a newline while looking entirely ordinary in the source, and printed verbatim it produces a line indistinguishable from one this program wrote. For a tool whose whole purpose is being pointed at code you have not read, that is worth closing.
The index is cached outside the working tree, so nothing appears in git status and there is nothing to add to .gitignore.
- The import graph is TypeScript and JavaScript only. Every file git tracks is in the history, the ownership map and the coupling analysis whatever its language — so a Python service beside a TypeScript front end still has churn, owners and co-change — but only these are parsed for imports.
- Ownership is approximated from the commit log, not from
git blame. Blame is more accurate for the lines that survived and costs one process per file, which is the difference between indexing in seconds and in hours. - A file deleted and later recreated under the same name shares one history. Git records no rename, and telling the two apart needs content similarity.
- A shallow clone is reported, not repaired. Every history figure covers only the commits present, so the map says so and declines to cache anything it read.
- There is no picture. A force-directed graph of four hundred files is a hairball. A useful one is a real design problem, not a weekend.
One thing it reports that you might not expect: imports whose capitalisation differs from the file on disk. They work on macOS and Windows and fail on Linux, so they are a portability bug that a CI run would otherwise find later and more expensively.
npm install
npm run check # lint, typecheck, unit and integration tests
npm run watch # then F5 to launch an Extension Development Host
npm run test:editor # runs the extension inside a downloaded VS Code
npm run package # builds fieldguide.vsixsrc/core is plain TypeScript that imports nothing from Node, the MCP SDK or any editor — the parsers, the metrics, the graph and the cache invalidation rules all live there and are tested without a repository. src/node runs git and reads the filesystem. src/bridge runs the analysis as a child process, src/mcp serves agents, src/vscode is the editor adapter. A lint rule keeps the boundary honest, and the build fails if the editor bundle grows past 200KB — the extension host is shared with every extension the user has installed, and the analysis carries a compiler.
If you want to work on it, see CONTRIBUTING.md.
MIT © Mustafa Kürşad BAŞER
