joemattie
stuff
← index
project pageupdated 2026-08-06

palimpsest

History-aware impact analysis for git repos: the dependencies your refactors hid

pal is a Rust CLI that reads a repo’s entire git history into SQLite and answers “if I touch this file, what else moves”. It’s shipped: 0.2.0 is on npm as @joemattie/palimpsest (npx-runnable, with prebuilt binaries for Linux x64/arm64, macOS x64/arm64, and Windows x64), and cargo install from source works if you have a Rust toolchain. A palimpsest is a manuscript scraped clean and rewritten, where the earlier text stays faintly legible underneath. That’s your codebase.

I built it because of a large repo at my day job. What burns agent tokens constantly there is exploring interdependency between files that aren’t obviously linked anymore but were in the past. An LSP answers “what imports this” at HEAD and has nothing to say about the years before it, which are exactly where the surprises live.

Three signals

Three ways to know what else a file touches, in increasing order of how hard they are to get anywhere else:

  1. Live structural edges. a.ts imports b.ts at HEAD. Exact, cheap, and any LSP will tell you the same thing. It’s here to anchor the rest.
  2. Evolutionary coupling. Two files with no static link that change together in most commits touching either. Static analysis can’t see this, by construction.
  3. Ghost edges. encoder.ts imported frame.ts until a commit extracted an interface between them. The import is gone. They’ve co-changed nine times since. The contract survived; only the analyzer’s ability to see it died.

A static edge disappearing rarely means the coupling ended; it usually means the coupling went dark. Interface extraction, dependency inversion, event buses, plugin registries, config-string dispatch: every one of these preserves the contract while deleting the evidence. Whether the co-change kept going after the severance is how you tell “refactored but still coupled” from “genuinely decoupled”.

Commands

pal index walks the history once and writes .pal/index.db; everything else queries it. pal blast <file> gives the ranked blast radius, pal ghosts lists severed edges that still co-change, pal cochange shows evolutionary coupling with no structural edge, and pal why <a> <b> digs up the severing commit plus its full evidence chain. There’s also pal timeline (a file’s birth, renames, and edge events), pal drift for stale docs, pal hotspots, and pal search (full-text over commit messages). Every command takes --json with a stable schema, and historical paths resolve through the rename chain.

pal serve draws all of it in a browser: a self-contained local page (no external requests) with the import graph and co-change coupling as a force layout, per-file “moves with” detail, and a hidden-coupling panel.

The pal serve force layout of the three.js repository: thousands of colored nodes clustered by directory (examples in blue, src in green, docs in orange), connected by faint live-import edges and red co-change edges

The repo also ships a Claude Code skill that teaches an agent to query the index before editing files. That’s the point of the whole thing: one pal blast --json call instead of an afternoon of grepping around reconstructing folklore.

One honest caveat

Ghost detection needs a language where imports actually resolve. On Rails, classic Ruby autoloading means almost nothing requires anything, so palimpsest finds rich co-change structure but only a handful of ghosts. JS, TS, Python, Rust, and Go give it much more to work with.

writing2026-08-06 · 4 min read
palimpsest: finding the dependencies your refactors hid

A Rust CLI that reads your entire git history and finds the files that still move together after the import between them died.

read the writeup →