palimpsest: finding the dependencies your refactors hid
I work on a fairly large repo at my day job, and a thing that burns agent tokens constantly is exploring interdependency: “if I touch this file, what else moves?” An LSP answers that for live imports. It has nothing to say about files that are not obviously linked anymore but were in the past, and those are exactly the ones that bite you.
So I built palimpsest: a Rust CLI (pal) that reads a repo’s entire git history into SQLite and answers the question from evidence instead of vibes. A palimpsest is a manuscript scraped clean and rewritten, with the earlier text still faintly legible underneath, which is roughly what a long-lived codebase looks like from the inside.
Three signals
There are three ways to know “what else does this file touch,” in increasing order of how hard they are to get anywhere else:
- Live structural edges:
a.tsimportsb.tsat HEAD. Exact, cheap, and any LSP will tell you the same thing. It’s here to anchor the rest, not because it’s novel. - Evolutionary coupling: Two files with no static link that change together in most commits touching either. Static analysis can’t see this, by construction.
- Ghost edges:
encoder.tsimportedframe.tsuntil a commit extracted an interface between them. The import is gone. They’ve co-changed nine times since. The contract survived the refactor; what died was the analyzer’s ability to see it.
The point of the third one: a static edge disappearing rarely means the coupling ended. Interface extraction, dependency inversion, event buses, plugin registries, config-string dispatch: every one of these preserves the contract while deleting the evidence. The severance commit is a detector for exactly the class of dependency HEAD-time tooling can’t find, and whether the co-change kept going afterward is how you tell “refactored but still coupled” from “genuinely decoupled.”
Pointing it at three.js
For a public demo I indexed three.js: 27,840 commits and 11,900 files (about half of them dead), which took around nine minutes on my machine. pal serve then draws all of it as a force layout: faint edges are live imports, red edges are co-change coupling.

Click a file and the panel shows what history says it drags along, separate from what it imports:

pal blast gives the same answer in the terminal, and on WebGLRenderer.js the top hit is a ghost:
blast radius of src/renderers/WebGLRenderer.js
0.80 src/scenes/Scene.js
GHOST call: severed 840254269d5b 2012-02-25 "Renamed Scene'
.addObject() and .removeObject() to .__addObject() and
.__removeObject()"; co-changed 16x since
A rename commit from 2012 broke the visible call. The renderer and the scene graph have co-changed 16 times since, and no import graph is going to tell you that.
The hidden-coupling tab is where this gets interesting. Top of the list for three.js: the WebGPU backend and its WebGL fallback, 78 co-changes with zero imports between them, because they’re the same behavior implemented twice. Right below: a GLSL shader chunk and PhysicalLightingModel.js at lift 149.9, the same lighting math maintained in two shading languages. Everyone who works on that codebase presumably knows these pairs. Now the tool knows too, which means an agent knows on its first query instead of after an afternoon of archaeology.

The agent angle
This exists to save tokens. Instead of an agent grepping around a big repo reconstructing folklore, pal blast <file> --json is one call with a stable schema, and the repo ships a Claude Code skill so agents reach for it on their own. Historical paths resolve through the rename chain, so “what happened to the file that used to be here” works too. There’s also pal why (the severing commit plus the full evidence chain), pal timeline, pal drift for stale docs, and pal hotspots.
The ghost detector needs a language where imports actually resolve. On Rails, classic Ruby autoloading means almost nothing requires anything, so palimpsest found rich co-change structure but only a handful of ghosts. On JS, TS, Python, Rust, or Go it has a lot more to work with.
Install is npx @joemattie/palimpsest (prebuilt binaries for five platforms) or cargo install from the source. The visualizer in these screenshots is pal serve, which is self-contained and makes no external requests. It went from initial commit to 0.2.0 on npm in three days.