This guide covers why Strata exists, what questions it answers, how to read the report, and how it works inside. For installation and options, see the README.
git blame tells you who last touched each line, but only for one moment: now. Strata turns that snapshot into a film.
The key technical trick is that line contents are not needed. The hunk headers of git log -p -U0 (@@ -a,b +c,d @@) say exactly which lines died and how many were born. The replay is therefore fast and light on memory. At HEAD it matches git blame --first-parent line for line. This was verified on seven real repositories, including git itself.
| Who | Question | Where to look |
|---|---|---|
| Tech lead | Where should refactoring effort go? | Hotspots: change frequency × complexity |
| Tech lead | Where does the architecture lie? | Fault lines: modules that change together although they should not |
| Manager | What happens if a key person leaves? | Bus factor per module, knowledge silos, share of code by inactive authors |
| Newcomer | Which parts are the ancient core and which are recent additions? | Geological map in age mode |
| OSS maintainer | Is the project alive, and who carries it? | Seismograph, stratigraphy by author |
| Everyone | How stable is our code? | Half-life of source vs tests vs docs |
| Retrospectives | What happened here in 2014? | Fossils, extinctions, big bangs |
Some findings from real projects. Each name links to the full report, so you can check the finding yourself:
Open a sample report next to this section to follow along. The report is laid out like a geological survey sheet: a title cartouche with metadata, then the column and field notes, then nine plates.
The report works on phones and in dark mode. It needs no server: every chart is drawn as SVG from JSON embedded in the page.
flowchart LR
A["git log --numstat -z<br/>(all commits)"] --> H[history.ts<br/>graph, authors, churn]
B["git log --first-parent -p -U0<br/>(oldest → newest)"] --> PP[patch-parser.ts<br/>hunks per file] --> R[replay.ts<br/>line-origin arrays]
H --> R
C["git cat-file --batch<br/>(HEAD blobs)"] --> S[headscan.ts<br/>LOC, complexity, fossils]
R --> S
S --> M["mergeblame.ts<br/>git diff-tree --stdin<br/>(side commits)"]
H --> X[analyze.ts<br/>hotspots · coupling · knowledge<br/>Kaplan–Meier · strata · events]
R --> X
M --> X
X --> N[narrative.ts + i18n.ts<br/>findings EN/RU]
X --> J[(survey JSON)]
J --> P[report/render.ts<br/>template + client/*.ts]
N --> P
P --> O[[single HTML file]]
The pipeline has six stages:
history.ts). One streaming git log --numstat -z gives parents, timestamps, mailmapped authors and per-file churn for every commit. Identities that share a name or an email are merged with union-find, and bots are flagged. The first-parent chain is extracted, and every merge gets the list of side-branch commits it brought in.patch-parser.ts + replay.ts, the heart of the system). PatchParser turns the patch stream into per-file hunk lists without keeping line text (except inside merges, where the text is needed to detect moves). For each commit on the first-parent chain, the file’s hunks are applied in reverse order with splice. Removed lines are deaths: their age goes into weekly bins for the survival curve. New lines get the current commit’s origin. Running tallies of living lines per layer, per author and per module are kept, and 320 snapshots of them are taken over the history.headscan.ts). A single git cat-file --batch reads every text file at HEAD. It measures lines and indentation complexity, cross-checks each file’s replayed length against the real one, and digs up fossils.replay.ts + mergeblame.ts). See below.analyze.ts, coupling.ts, paths.ts). Hotspots, temporal coupling, adaptive modules, bus factor, Kaplan–Meier (survival.ts), extinctions, activity. Everything ends up in one Survey object, typed in types.ts, which is also what --json writes.narrative.ts, i18n.ts, report/). Findings are stored as structured facts and rendered in English or Russian, both in the terminal and in the page. The browser code (report/client/*.ts) is vanilla TypeScript that draws SVG; there is no framework and no CDN. render.ts strips the types and concatenates the client files into one inline script.The data flow is easiest to follow through the types in src/types.ts:
Commit and Person come out of pass 1 (readHistory, resolveIdentities).OriginIds. An origin says who wrote a line and when: an author, a time, the first-parent commit that brought it in and, for merged lines, the side-branch commit credited. Lines that share an origin share one id, so a file is just an array of small integers. Replay keeps origins in parallel arrays (oAt, oAuthor, oCommit, oSide) rather than objects, which keeps memory small even on git/git (1.7M living lines).FileDiff and Hunk (patch-parser.ts) are the only view of a commit the replay needs.Survey: Strata, Survival, FileReport, Hotspot, CouplingReport, ModuleReport, PersonReport, Fossil, CommitEvent, Pulse and the Facts of the narrative.A merge diff against its first parent looks as if whoever pressed Merge wrote all the merged code. Strata corrects this in three steps:
mergeblame.ts).
git diff-tree --stdin call.Each step adds accuracy, and strata verify measures the result. On requests, crediting merged lines to whoever merged them matches plain git blame authorship for 62% of lines. With all three steps it matches 98.4%.
log(1+changes)/log(1+max) × log(1+complexity)/log(1+max). Frequency uses the last 12 months, or the whole history if the project is quiet. Only source and test files are ranked.shared commits / mean(revisions of a, revisions of b), with at least 5 shared commits. Mass commits that touch more than 30 files are ignored as noise. The borehole panel uses confidence instead: the share of this file’s commits that also touched the partner.a.js was renamed to b.js in 2015 and a new a.js appeared in 2018, the new file’s history does not stick to b.js.All of the following cases are handled and covered by tests:
~/.gitconfig. Settings such as diff.noprefix or diff.interHunkContext would break the replay, so every option that shapes diff output is pinned.-diff in .gitattributes (Strata diffs them itself);<. Template placeholders are substituted in a single pass, so a commit message that reads `` cannot break the page.git log up to 6× slower.Edge cases found during two independent reviews are pinned down by regression tests in test/edge.test.ts.
strata verify <repo> runs three checks:
git blame --first-parent.git blame, which follows merges into side branches.| Repository | Commits | Files with wrong length | Commit per line vs blame --first-parent |
Author per line vs blame |
…if merges were credited to the merger |
|---|---|---|---|---|---|
| expressjs/express | 6.2k | 0 / 214 | 100.000% | 98.5% | 81.4% |
| psf/requests | 6.5k | 0 / 122 | 100.000% | 98.4% | 62.0% |
| jqlang/jq | 2.0k | 0 / 396 | 100.000% | 99.8% | 98.7% |
| tj/commander.js | 1.5k | 0 / 215 | 99.982% ¹ | 99.6% | 95.1% |
| vitejs/vite | 9.7k | 0 / 2721 | 100.000% | 99.0% | 97.5% |
| pallets/flask | 5.6k | 0 / 230 | 100.000% | 85.0% | 70.6% |
| git/git | 75k | 0 / 4782 | 100.000% | 95.5% | 10.1% |
¹ Two lines in byte-identical fixture files where blame picks a different copy of the same content.
Flask scores lower because it uses long-lived maintenance branches with mass reformatting. Plain blame follows those changes through renames inside side branches, which a first-parent replay cannot see.
git blame without -M/-C. Moves inside merges are the exception.strata diff A..B: how the geology changed over a release.