How to understand a new codebase: a method for day one and week one
Understand a new codebase in a week: run it, find the entry points, trace one request, map imports and hotspots, then read the tests and git history.
By the pyrovue team · · 10 min read
To understand a new codebase, run it first, then find where execution starts and follow one real request from the entry point to the database and back. Next, map which files import which, and start reading at the files most of the code depends on, together with their tests. Use git history to see what changes often and who changed it, and write down what you learn as you go.
That order works because each step gives the next one something concrete. A running app lets you set breakpoints instead of guessing. One traced request shows you the layers in the order the code uses them. The import graph tells you which few files explain the rest, and the history tells you which of those are under active change.
This guide turns that into a plan for the first day and the first week, with commands for JavaScript and TypeScript, Python and Go, and notes on where AI assistants help and where they get the structure wrong.
A plan for day one and week one
| When | Step | What you end up with |
|---|---|---|
| Day 1 | Run the app and its tests | A working setup and a baseline of passing and failing tests |
| Day 1 | Find the entry points | The files where execution starts |
| Day 1 | Follow one request end to end | One path through every layer, file by file |
| Days 2 and 3 | Map the dependency structure | Layers, clusters, hotspots and import cycles |
| Days 3 and 4 | Read the tests for the hotspots | How the core objects are meant to behave |
| Days 4 and 5 | Read the git history | What changes often, and who to ask about it |
| All week | Write the map down | A notes file you can turn into a first pull request |
Run it first, then find the entry points
Get it running
Follow the README, and when a step fails, write down the fix. That list is your first contribution. If the README is thin, the CI workflow (.github/workflows/*.yml, .gitlab-ci.yml) is the most reliable install recipe in the repository, because it runs on every push. Then run the test suite once (pytest -q, npm test, go test ./...) so you know what was already failing before you touched anything.
If setup is the blocker, the guide to running a GitHub project covers the usual failures, and pyrovue can run a public GitHub repository in a box without installing anything on your laptop. For code from someone you don't know, read whether it's safe to run code from GitHub first.
Find where execution starts
Every program has a few files where control enters: a server's startup file, a CLI's main, a framework's route files, a job runner. These commands find most of them:
jq .scripts package.json # npm scripts: dev, start, build, test
grep -A4 '\[project.scripts\]' pyproject.toml # Python console commands
rg -n --type py '__name__ == .__main__.' # Python scripts run directly
rg -l --type go '^func main\(' # Go binaries, usually under cmd/
Then check the framework's conventions: app/ and pages/ in Next.js, urls.py in Django, flask routes for a Flask app, bin/rails routes in Rails, the interactive docs at /docs in FastAPI. The CMD line of a Dockerfile and a Procfile tell you what runs in production, which is not always what runs in development.
Follow one request end to end
Pick one action a user would recognise, such as signing in or creating an order, and trace it through every layer.
- Find its route: a routes command from the list above, or
rg -n "/orders"for the URL path. - Put a breakpoint in the handler (
breakpoint()in Python;debugger;withnode --inspectin Node) and trigger the action in the running app. - Step through, or print the whole call chain at once from the deepest point you reach:
import traceback; traceback.print_stack()in Python,console.trace()in JavaScript. A stack trace shows the middleware and framework layers that stepping skips over. - Write down each hop as
file:function, with what it reads and writes.
Your editor does most of the walking. In VS Code, Go to Definition is F12, Find All References is Shift+F12 and Go to Symbol in Workspace is Ctrl+T (Cmd+T on macOS); JetBrains IDEs have the same actions under Go to Declaration or Usages and Find Usages. Both work far better once the dependencies are installed, which is another reason to run the project first. Navigation stops at runtime wiring: dependency injection containers, decorators that register routes, plugin registries keyed by strings, event handlers. When go-to-definition gives up, search for the string the registry uses.
Map the dependency structure
Layers, clusters and hotspots
An import graph (one node per file, one arrow per import) answers three questions that reading files one by one doesn't:
- Layers. Which files are UI, routes, services and data access, and whether the arrows only point downwards.
- Clusters. Groups of files that import each other more than they import the rest. Community detection algorithms such as Louvain find them, and they usually match the modules a team would draw on a whiteboard.
- Hotspots. The files most others import. Fan-in counts a file's direct importers. PageRank goes further: a file's score comes from the scores of the files importing it, so a module imported by other central modules outranks one imported by a pile of leaf scripts.
High fan-in files hold the shared vocabulary: the models, types, configuration and request objects that every other file passes around. Read them first and the rest of the code becomes sentences made of words you know. Run pydeps on Flask's source (as of late October 2026) and count importers: flask.globals comes first, imported by 12 other Flask modules, because it defines request, session, g and current_app. A quick PageRank over the same graph ranks flask.wrappers (the Request and Response classes) second. Skip generic helpers such as logging utilities, which score high and explain little. Files with the highest fan-out are the opposite end: composition roots that wire everything together, useful for seeing the whole flow at once.
Tools by language
| Language | Tool | Command |
|---|---|---|
| JavaScript, TypeScript | madge | npx madge --image graph.svg --extensions ts,tsx src/ (needs Graphviz) |
| JavaScript, TypeScript | dependency-cruiser | npx dependency-cruiser --init, then npx dependency-cruiser src --include-only "^src" --output-type dot | dot -T svg > dependency-graph.svg |
| Python | pydeps | pip install pydeps, then pydeps src/yourpkg --noshow -o deps.svg (needs Graphviz) |
| Python | import-linter | Contracts that fail CI when a layer imports the wrong way |
| Go | go list | go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./... |
go mod graph is often suggested for Go, but it prints the module requirement graph, meaning which modules (yours and its third-party dependencies) require which, not which packages import which. For imports between your own packages, use go list. import-linter doesn't draw anything; it keeps the layering you discover from eroding.
For a ranked hotspot list without a picture, count importers from the JSON output:
# JavaScript or TypeScript: files ranked by number of importers
npx madge --json --extensions ts,tsx src/ \
| jq -r '[.[][]] | group_by(.) | map("\(length) \(.[0])") | .[]' | sort -rn | head -15
# Python: the same, from pydeps (replace flask with your package)
pydeps src/flask --show-deps --no-output --max-bacon 0 > deps.json
jq -r 'to_entries[] | select(.key | startswith("flask"))
| "\([.value.imported_by[]? | select(startswith("flask"))] | length) \(.key)"' deps.json \
| sort -rn | head -15
dependency-cruiser computes the same numbers with --metrics --output-type metrics: Ca is fan-in and Ce is fan-out.
Import cycles
Files that import each other in a loop are usually one module split across several files, or a layer boundary that has worn through. They are also where startup-order bugs live. npx madge --circular --extensions ts,tsx src/ lists them for JavaScript and TypeScript, pydeps yourpkg --show-cycles draws them for Python, and Go refuses to compile them (import cycle not allowed). The guide to Python circular imports explains why they fail and how to untangle them.
Read the tests next
Tests are the one form of documentation that runs. Read the tests for your hotspot files before the files themselves: they show how the core objects are built, which inputs matter and which behaviour someone cared enough to pin down. Fixtures and factories (conftest.py, tests/fixtures/, test helpers) show the shortest way to construct a valid instance of everything.
pytest --collect-only -q # every test id, which doubles as a feature list
pytest -x -vv -k "checkout" # run one area, stop at the first failure
go test -run TestCheckout -v ./... # the same in Go
End-to-end and integration tests are closest to a specification of user flows, so read them for the request you traced. Hotspots with no tests at all are where you should be most careful with a first change.
Use git history to find hotspots and owners
The history says what the code doesn't: which files change together, which are changing right now, and who knows them.
git log --stat --no-merges -20 # recent commits and the files each touched
git shortlog -sn --no-merges --since="1 year ago" HEAD # who commits most
git shortlog -sn --no-merges HEAD -- src/flask/app.py # who knows one file
git log --follow --oneline -- src/flask/app.py # one file's history across renames
git log -S "def full_dispatch_request" --oneline # commits that added or removed a string
Keep the HEAD on git shortlog: without a revision, it reads a log from standard input whenever standard input isn't a terminal, so in scripts and CI it prints nothing. --follow in git log works for a single file only.
To find churn hotspots, count how many commits touched each file:
git log --since="12 months ago" --format= --name-only -- src \
| grep -v '^$' | sort | uniq -c | sort -rn | head -20
Without the -- src path, lockfiles and CI configs fill the top of the list. In Flask's repository, src/flask/app.py was changed in 21 commits in the two years to late October 2026, more than any other source file. A file that is both high churn and high fan-in is where changes ripple furthest, so read its recent commits and the reviews on them.
Where AI helps and where it misleads
AI assistants are good at local questions: what does this 80-line function do, what is this framework idiom, what does this regex match, write me the jq one-liner above. They are also useful for turning your notes into a diagram and for drafting questions to ask the team.
They are least reliable at the question newcomers most want answered: how does this codebase fit together. A summary of structure can be confidently wrong in ways that are hard to spot:
- It describes what a typical project of that kind looks like, including folders and patterns this one doesn't have.
- It misses runtime wiring, the same places go-to-definition can't follow: containers, registries, decorators, environment-specific configuration.
- It only knows the files it was given, and large repositories don't fit.
Treat every structural claim as a lead to check. Ask for file paths and line numbers, open each one, and compare claims about layers and dependencies with the import graph, which comes from the code rather than from a model. pyrovue's guided tour follows the same rule: when Claude sets a repository up it writes the tour, and every stop is checked against real files and lines; without Claude, the tour is built from the import map.
The shortcut: a map before you open a file
pyrovue's Code Map does the mapping step for you. Paste a link to a public GitHub or gitlab.com repository, and pyrovue starts it in a box and reads the imports in up to 4,000 source files, resolving up to 20,000 imports in about two seconds, with no AI and no model calls. It covers 16 languages, including JavaScript and TypeScript (with Vue, Svelte and Astro files), Python, Go, Rust, Ruby, PHP, Java, Kotlin, C# and C/C++, and resolves tsconfig paths, @/ aliases, workspaces and Python src/ layouts.
The map shows the layers, the clusters (Louvain), the hotspots (PageRank), up to 25 import cycles and the entry points, and it stays after the box goes to sleep. Its limit is the one any static reader has: imports are read with patterns, not a full parser, so imports built at runtime are missed. For repositories larger than 4,000 source files, or for rules enforced in CI, dependency-cruiser and import-linter are the better tools. The free Hobby plan runs one box at a time for 20 box-hours a month; pricing has the rest.
Write the map down as you go
Keep one notes file open all week and fill it in as you work, not at the end:
# Notes: <project>
## How to run it (every fix the README was missing)
## Entry points (file paths)
## One request, hop by hop (file:function, reads, writes)
## Hotspots (top files by importers, one line each on what they hold)
## Layers and boundaries (what may import what; cycles found)
## Glossary (domain words and the types that represent them)
## Open questions (and who, per git shortlog, can answer them)
By Friday that file is most of an onboarding document. Send the README fixes as your first pull request, and offer the rest as an ARCHITECTURE.md, the short codemap of modules and how they relate that Aleksey Kladov recommends keeping next to the README. Then pick a small change in a file you have mapped, so your first review is about the change and not about where things live.