Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 56 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,68 @@

![codemap screenshot](assets/codemap.png)

## Your agent's first minute

```bash
brew tap JordanCoin/tap && brew install codemap # one static binary
cd your-repo && codemap setup # hooks + MCP for Claude Code and Codex
codemap mcp # stdio MCP server for any other client
```

After `codemap setup`, the agent gets three answers it cannot get from the source text, at session start, before an edit, and on request.

**Where things are.** `codemap .`

```
╭────────────────────────────── codemap ──────────────────────────────╮
│ Files: 289 | Size: 2.3MB │
│ Top Extensions: .go (227), .yml (35), .md (23), .sh (2), .ps1 (1) │
╰─────────────────────────────────────────────────────────────────────╯
codemap
├── analysis/ (2 files, 3.6KB, all .go)
├── cmd/ (47 files, 463.2KB, all .go)
├── config/ (2 files, 23.2KB, all .go)
...
```

**Who depends on this.** `codemap --importers config/config.go`

```
⚠️ HUB FILE: config/config.go
Imported by 40 files - changes have wide impact!

Dependents:
• blast_radius.go
• cmd/config.go
... and 38 more
Coverage: complete
```

**Where is the code that does X.** `codemap find "hub importers"`

```
main.go
matched: resolveImportersInvocation, buildImportersReport, runImportersMode
importers: 0
Coverage: complete
```

Ranked by path and symbol match, each hit tagged with its importer count. Lexical only, and it says so.

**And the one line every answer carries.** Every dependency answer reports a coverage status: `complete`, `partial`, or `unavailable`, with the source that could not be trusted. A partial graph never reads as a complete one, so "nothing imports this" and "I couldn't tell" are different answers.

## What it's for

An agent reading your repo can see what a file *says*. It can't cheaply see what depends on that file — that answer lives in `go.mod`, Cargo workspace membership, `package.json` `exports` maps, and `tsconfig` path aliases, not in the source text.
An agent reading your repo can see what a file *says*. It can't cheaply see what depends on that file. That answer lives in `go.mod`, Cargo workspace membership, `package.json` `exports` maps, and `tsconfig` path aliases, not in the source text.

codemap computes three things:

| | |
|---|---|
| **Orientation** | A structure map with the most-imported files called out. Cheap cold start, useful when an agent has no memory of the last hour. |
| **Dependency graph** | Imports resolved through each ecosystem's real rules not string matching. |
| **Dependency graph** | Imports resolved through each ecosystem's real rules, not string matching. |
| **Blast radius** | Who breaks if you change this file. |

And one thing that matters more than any of them: **it tells you when it doesn't know.** Every dependency answer carries a coverage status, so a partial graph never reads as a complete one.

```bash
codemap . # structure + hubs
codemap --importers path/to/file # who depends on this
codemap --diff # what changed vs main
```

## Install

```bash
Expand Down Expand Up @@ -479,6 +521,10 @@ Next:
- [ ] Community skill registry (`codemap skill add <name>`)
- [ ] Enhanced analysis (entry points, key types)

## Peers

[sem](https://github.com/Ataraxy-Labs/sem) does symbol-level impact analysis. [serena](https://github.com/oraios/serena) and [codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp) are broader MCP servers with semantic retrieval, and [zvec-grep](https://github.com/zvec-ai/zvec-grep) is hybrid search. codemap's difference: imports resolved through each ecosystem's real rules, a coverage status on every answer, and hub warnings before an edit.

## Contributing

Fork → branch → commit → PR. See [CONTRIBUTING.md](CONTRIBUTING.md) before adding a new language.
Expand Down
25 changes: 20 additions & 5 deletions cmd/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ func hookSessionStart(root string) error {
break
}
importers := len(info.Importers[hub])
fmt.Printf(" ⚠️ HUB FILE: %s (imported by %d files)\n", hub, importers)
fmt.Printf(" ⚠️ HUB FILE: %s (imported by %s)\n", hub, filesText(importers))
}
} else if shouldSkipHubAnalysis(fileCount, fileCountKnown) {
fmt.Printf("ℹ️ Hub analysis skipped for large repo (%d files)\n", fileCount)
Expand Down Expand Up @@ -915,9 +915,9 @@ func hookPromptSubmit(root string) error {
for _, file := range filesMentioned {
if importers := info.Importers[file]; len(importers) > 0 {
if scanner.CountHubImporters(importers) >= scanner.HubThreshold {
output = append(output, fmt.Sprintf(" ⚠️ %s is a HUB (imported by %d files)", file, len(importers)))
output = append(output, fmt.Sprintf(" ⚠️ %s is a HUB (imported by %s)", file, filesText(len(importers))))
} else {
output = append(output, fmt.Sprintf(" 📍 %s (imported by %d files)", file, len(importers)))
output = append(output, fmt.Sprintf(" 📍 %s (imported by %s)", file, filesText(len(importers))))
}
}
}
Expand Down Expand Up @@ -1084,9 +1084,9 @@ func planCodemapNextSteps(intent TaskIntent, info *hubInfo) []codemapNextStep {
reason := "check callers before editing"
switch {
case hubImporterCount >= scanner.HubThreshold:
reason = fmt.Sprintf("check blast radius before editing this hub (%d importers)", importerCount)
reason = fmt.Sprintf("check blast radius before editing this hub (%s)", importersNoun(importerCount))
case importerCount > 0:
reason = fmt.Sprintf("check callers before editing (%d importers)", importerCount)
reason = fmt.Sprintf("check callers before editing (%s)", importersNoun(importerCount))
}
add("codemap --importers "+shellQuoteIfNeeded(primaryFile), reason)
if hubImporterCount >= scanner.HubThreshold {
Expand Down Expand Up @@ -2322,3 +2322,18 @@ func indexOf(slice []string, val string) int {
}
return len(slice)
}

// filesText and importersNoun keep counts grammatical: "1 file", "2 files".
func filesText(n int) string {
if n == 1 {
return "1 file"
}
return fmt.Sprintf("%d files", n)
}

func importersNoun(n int) string {
if n == 1 {
return "1 importer"
}
return fmt.Sprintf("%d importers", n)
}
5 changes: 3 additions & 2 deletions collide.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,9 @@ func printCollideUsage(fs *flag.FlagSet) {
fmt.Fprintln(os.Stderr, "Usage: codemap collide [options]")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Rank open pull requests by the merge-order hazard they share: which pairs")
fmt.Fprintln(os.Stderr, "change the same files, weighted by how many files import them. CI cannot see")
fmt.Fprintln(os.Stderr, "this, because every PR is built against main and never against its siblings.")
fmt.Fprintln(os.Stderr, "change the same files, weighted by how many files import them. It names the")
fmt.Fprintln(os.Stderr, "pairs worth building together before they reach a merge queue, with the shared")
fmt.Fprintln(os.Stderr, "files and importer counts, so the queue's first failure is not the first warning.")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Weights are labelled with the granularity they were measured at. A language")
fmt.Fprintln(os.Stderr, "whose imports name files (TypeScript, JavaScript, Python) reports importers of")
Expand Down
Loading