Code review map · free & open source

Read AI-written code the way it runs.

Unfold shows a code change as the functions it touches, laid out by who calls whom. Review what Claude Code, Cursor or Copilot wrote block by block, not file by file.

$ npx unfold-pr main --open
View on GitHub
Apache-2.0Built with tree-sitterDeterministic — never guesses
COMING SOONUnfold Cloud — hosted, private maps for every pull request
01 / 06 — THE PROBLEM

A diff sorted by file name hides the flow.

Agents write a lot of code fast. The diff hands it back in alphabetical order, so you read helpers before you know who needs them, and the place the change starts is buried halfway down. Where does it begin, what does it call, what does each piece do?

FILE ORDER · how the diff shows it
  1. src/bursts.tsfindBursts
  2. src/log-summary.ts…countByLevel, errorsBySource, formatDuration, summariseLog
  3. src/parse.tsLEVEL_ORDER, LINE_PATTERN, parseLog…
  4. src/severity.tsfilterBySeverity

The entry point, summariseLog, is the fourth function in the second file.

CALL ORDER · how Unfold shows it
T0summariseLog
T1parseLogcountByLevelerrorsBySourcetopRecurringMessagesfindBurstsformatDuration
T2parseLogLinefilterBySeverity
T3parseTimestampLINE_PATTERNLEVEL_ORDER

Start where the change starts. Follow the calls down to helpers and constants.

02 / 06 — THE MAP

Every changed file, split into its functions and stacked by who calls whom.

Entry points first, then what they call, then helpers and constants. Each block holds the whole function with the changed lines marked. Arrows are real calls, across files too.

changed line
call in the same file
call to another file
.unfold/map.html — main…feature/log-bursts12 blocks · 4 files · example: a log analyser
Tier 0 · entry points Tier 1 · called by entry Tier 2 · helpers Tier 3 · leaves & constants
summariseLogfan-out 6src/log-summary.ts:12–23
export function summariseLog(raw: string, opts: Options) {
const entries = parseLog(raw);
const counts = countByLevel(entries);
const errors = errorsBySource(entries, opts.minLevel);
const top = topRecurringMessages(entries, 5);
const bursts = findBursts(entries, opts.windowMs);
return {
counts, errors, top,
bursts: bursts.map(b => ({ ...b, span: formatDuration(b.ms) })),
};
}
parseLogparse.ts:8
return raw
.split("\n")
.map(parseLogLine)
.filter(Boolean);
countByLevel:25
const c = {};
for (const e of list)
c[e.level] =
(c[e.level] ?? 0) + 1;
return c;
errorsBySource:32
const out = new Map();
for (const e of list) {
if (!filterBySeverity(
e, min)) continue;
add(out, e.source);
topRecurringMessages
const freq = new Map();
for (const e of list)
tally(freq, e.msg);
return [...freq]
.slice(0, n);
findBursts120 lines
const out = [];
let start = 0;
for (let i = 1; …) {
const d = t[i] - t[i-1];
if (d > win) { … }
formatDuration:58
if (ms < 1000)
return ms + "ms";
if (ms < 60_000)
return `${ms / 1e3}s`;
return `${ms / 6e4}m`;
parseLogLinesrc/parse.ts:16
const m = LINE_PATTERN.exec(line);
if (!m) return null;
const level = m[2].toLowerCase();
if (!(level in LEVEL_ORDER)) return null;
return { at: parseTimestamp(m[1]), … };
filterBySeveritysrc/severity.ts:5
export function filterBySeverity(
e: Entry, min: Level,
) {
return e.rank >= min.rank;
}
parseTimestampparse.ts:31
const d = new Date(s);
return isNaN(+d) ? null : d;
LINE_PATTERNconst
const LINE_PATTERN =
/^(\S+) \[(\w+)\] (.*)$/;
LEVEL_ORDERconst
const LEVEL_ORDER = {
debug: 0, info: 1, warn: 2,
error: 3, fatal: 4 };
READING THIS MAP Start at summariseLog. Two of its six calls leave the file: findBursts lives in src/bursts.ts, filterBySeverity in src/severity.ts. The new 120-line findBursts is flagged, so it gets your attention before the one-line edits.

Illustration of a map for a small example repo. On a phone, scroll the map sideways.

03 / 06 — FOR YOU AND YOUR AGENT

One command. A map for you, a brief for your agent.

Run it locally against any branch. Unfold writes .unfold/ at the repo root and keeps it out of git for you.

map.htmlThe interactive map, for you.
map.mdFor your AI agent: what to check first, calls between files, reading order with file:line ranges.
map.jsonThe full data, for tools.
terminal
$ npx unfold-pr main --open
wrote .unfold/map.html
wrote .unfold/map.md
wrote .unfold/map.json
.unfold/map.mdexcerpt
## Check first
findBursts        src/bursts.ts:4–123       120 lines

## Calls between files
summariseLog   → findBursts        src/bursts.ts
errorsBySource → filterBySeverity  src/severity.ts

## Reading order
1. summariseLog    src/log-summary.ts:12–23
2. parseLog        src/parse.ts:8–14
3. parseLogLine    src/parse.ts:16–29
4. errorsBySource  src/log-summary.ts:32–44
…
04 / 06 — ON EVERY PULL REQUEST

Add one step. Every PR gets its map.

With the GitHub Action, each pull request gets one comment: a summary, the review flags and a link to the interactive map.

.github/workflows/review.yml
steps:
  # …your existing steps
  - uses: Alhiane/unfold-pr@v0
unfoldbot · pull request comment
Unfold map for this pull request
12 changed blocks across 4 files. Start at summariseLog (src/log-summary.ts:12–23).
REVIEW FLAGS
120 linesfindBursts
new · no in-file callersfindBursts
fan-out 6summariseLog
Open the interactive map
05 / 06 — WHAT IT UNDERSTANDS

Built from the real code. Not an AI reviewer.

Unfold parses your code with tree-sitter. Same change in, same map out. It links a call only when the target is unambiguous — a missing arrow is better than a wrong one.

CALLS BETWEEN FILES · TS / JS / VUE AND PHP
Vue component authService.ts Laravel controller AuthenticationService TokenManager
Languages
TESTED
TypeScriptJavaScriptVuePHPPythonBash
PARSED
GoRubyRustJavaKotlinC#SwiftC/C++
Review flags
private · never calledA private function nothing in the change calls.
new · no in-file callersNew code nothing in its own file calls yet.
fan-out 8A function that calls many others.
120 linesA block long enough to read slowly.
What it won't do
  • Guess at a call target it can't resolve.
  • Summarise or judge your code with a model.
  • Rewrite your code — every block shows it as written.
06 / 06 — UNFOLD AND UNFOLD CLOUD

Free and open source today. Unfold Cloud for private repos, coming soon.

Unfoldfree, open source · Apache-2.0
  • Local CLI: map.html, map.md and map.json in .unfold/
  • GitHub Action with one comment per pull request
  • All languages, flags and cross-file arrows
$ npx unfold-pr main --open
Unfold CloudCOMING SOON
  • Hosted, private interactive maps stored for every pull request
  • Open from the PR, or inside it with a browser extension
  • For private repositories, behind GitHub sign-in
One email when Unfold Cloud opens.
OPEN SOURCE

Read the code that reads your code.

Unfold is Apache-2.0. The parser, the layout and the Action are all in one repository. Issues and pull requests welcome.

LICENSEApache-2.0
PARSERtree-sitter
RUNSlocally · in CI
OUTPUTdeterministic