repo-dive 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # repo-dive
2
2
 
3
- Dive into a git repository's history: per-commit snapshots, an indexed metrics catalog and an interactive dashboard.
3
+ _Dive into a git repo's history: per-commit snapshots, an indexed metrics catalog and an interactive dashboard_ <!-- markdownlint-disable-line MD036 -- a tagline, not a heading -->
4
4
 
5
- > **Still 0.x.**
6
- > The pipeline works end to end and has been run against repositories with tens of thousands of commits, but interfaces, the catalog format and the collector roster still move between minor versions — pin the version if you script against it.
7
- > Renamed from `repo-insighter` in 0.4.0.
5
+ [![npm version](https://img.shields.io/npm/v/repo-dive?logo=npm&color=3c7ef6&labelColor=333)](https://www.npmjs.com/package/repo-dive)
6
+ [![npm downloads](https://img.shields.io/npm/dm/repo-dive?logo=npm&color=3c7ef6&labelColor=333)](https://www.npmjs.com/package/repo-dive)
7
+ [![License](https://img.shields.io/badge/license-BSD--3--Clause-3c7ef6?logo=opensourceinitiative&logoColor=white&labelColor=333)](LICENSE.md)
8
+ [![Effect](https://img.shields.io/badge/Effect-v4-3c7ef6?logo=effect&logoColor=white&labelColor=333)](https://effect.website)
9
+ [![MCP](https://img.shields.io/badge/MCP-ready-3c7ef6?logo=modelcontextprotocol&logoColor=white&labelColor=333)](#ai-agents-mcp)
8
10
 
9
11
  ## What it does
10
12
 
11
- Point it at any git repository and get an explorable catalog of insights derived from its history:
13
+ Point `repo-dive` at any git repository and get an explorable catalog of insights derived from its history:
12
14
 
13
15
  ```sh
14
16
  cd /path/to/your/repo
@@ -27,10 +29,10 @@ See [docs/specs](docs/specs/README.md) for the architecture and [docs/research/p
27
29
 
28
30
  ## Usage
29
31
 
30
- Run from inside the repository you want to analyze (or pass `--repo /path/to/repo`):
32
+ Run from inside the repository you want to analyze (or pass `--repo /path/to/repo`).
33
+ Node 22.13 or newer is required.
31
34
 
32
35
  ```sh
33
- cd /path/to/your/repo
34
36
  npx repo-dive # the whole pipeline: scan + index + dashboard
35
37
  npx repo-dive scan # collect snapshots into .repo-dive/
36
38
  npx repo-dive index # roll up into the metrics cube + dashboard data
@@ -38,10 +40,10 @@ npx repo-dive dashboard # serve the interactive dashboard
38
40
  npx repo-dive status # show catalog coverage
39
41
  npx repo-dive collectors # list available collectors
40
42
  npx repo-dive report # export one shareable self-contained HTML file
43
+ npx repo-dive mcp # serve the cube to AI agents (Model Context Protocol)
44
+ npx repo-dive gc # clean up the catalog interactively
45
+ npx repo-dive ignore # keep other tools out of the catalog
41
46
  npx repo-dive query "SELECT metric, sum(value) FROM facts GROUP BY metric"
42
- npx repo-dive mcp # serve the cube to AI agents (Model Context Protocol)
43
- npx repo-dive gc # clean up the catalog interactively
44
- npx repo-dive ignore # keep other tools out of the catalog
45
47
  ```
46
48
 
47
49
  `scan` walks the repository's history and runs collectors against every commit (or a sample, per collector), writing raw snapshots into a `.repo-dive/` catalog inside the analyzed repo.
@@ -53,7 +55,7 @@ Collectors so far:
53
55
  - **churn** — lines added/deleted per commit, by file extension
54
56
  - **file-types** — file count and bytes per extension at each commit's tree
55
57
  - **directives** — eslint-disable comments by rule (block disables tracked as gray areas) and `@ts-ignore`/`@ts-expect-error`/`@ts-nocheck`
56
- - **dependencies** — total resolved packages from package-manager lockfiles, per package manager (pnpm, npm and yarn classic and berry; version-aware and monorepo-aware, extensible to more managers), plus direct/dev/optional dependencies and the number of manifests read straight from `package.json` files
58
+ - **dependencies** — resolved package totals from lockfiles, per package manager (pnpm, npm and yarn classic and berry), plus direct/dev/optional dependencies and manifest counts read straight from `package.json` files; version-aware, monorepo-aware and extensible to more managers
57
59
  - **todo-comments** — TODO/FIXME/HACK/XXX counts
58
60
  - **languages** — lines and file count per language across a commit's source files (lockfiles, minified bundles and generated data excluded)
59
61
  - **survival** — `git blame` line survival by extension, author and age cohort (sampled monthly)
@@ -61,7 +63,8 @@ Collectors so far:
61
63
  The catalog hides itself from git, but other tools that walk the repository (prettier, markdownlint, cspell, docker builds) each read one ignore file at its root.
62
64
  `scan` warns when the catalog is missing from those; `repo-dive ignore` adds it to every one of them.
63
65
 
64
- `index` normalizes raw snapshots into `.repo-dive/index/metrics.sqlite` (a facts-by-categories cube, rebuildable at any time) plus `dashboard.json`, and `dashboard` serves a local React app with interactive charts: languages over time, a GitHub-style commit calendar, monthly commits with AI-assisted share, churn, lint-suppression trends, dependency counts over time, code survival by cohort and author, and more.
66
+ `index` normalizes raw snapshots into `.repo-dive/index/metrics.sqlite` a facts-by-categories cube, rebuildable at any time plus `dashboard.json`.
67
+ `dashboard` then serves a local React app with interactive charts: languages over time, a GitHub-style commit calendar, monthly commits with AI-assisted share, churn, lint-suppression trends, dependency counts over time, code survival by cohort and author, and more.
65
68
 
66
69
  ## Configuration
67
70
 
@@ -98,7 +101,8 @@ export default defineConfig({
98
101
  ```
99
102
 
100
103
  `charts.weekStartsOn` sets the first day of the week in calendar-shaped charts such as the commit calendar (`"monday"` by default, `"sunday"` also supported).
101
- `contributors.aliases` merges the multiple identities one person commits under (work + personal email, GitHub noreply, name variants) so attribution, the contributors table and code-survival-by-contributor count them once; a group can also carry a `displayName`, a profile `url` and a `kind` (`human`/`bot`/`ai`, otherwise auto-derived — the dashboard badges bots and AI agents and lists them apart from humans).
104
+ `contributors.aliases` merges the multiple identities one person commits under (work + personal email, GitHub noreply, name variants) so attribution, the contributors table and code-survival-by-contributor count them once.
105
+ A group can also carry a `displayName`, a profile `url` and a `kind` — `human`/`bot`/`ai`, otherwise auto-derived, with the dashboard badging bots and AI agents and listing them apart from humans.
102
106
  `catalog.dir` moves the catalog; point it outside the repository (e.g. `"../repo-dive-catalogs/my-repo"`) to leave the analyzed working tree untouched altogether, ignore files included.
103
107
  Apart from `catalog`, which every command needs, the config is read by `index`.
104
108
  See [docs/specs/07-config.md](docs/specs/07-config.md) for details.
@@ -148,9 +152,19 @@ pnpm lint
148
152
  pnpm fix
149
153
  ```
150
154
 
155
+ To see how heavy the published package would be:
156
+
157
+ ```sh
158
+ pnpm build && pnpm report-package-size
159
+ ```
160
+
161
+ It prints the tarball and unpacked sizes with a per-file breakdown, comparing them against the previous measurement.
162
+ CI runs the same report on every push and adds it to the job summary, comparing against the latest `main`.
163
+
151
164
  ## Acknowledgements
152
165
 
153
166
  Thanks to [@WillJack20](https://github.com/WillJack20) for suggesting the name **repo-dive**.
167
+ The project was published as [repo-insighter](https://www.npmjs.com/package/repo-insighter) before 0.4.0.
154
168
 
155
169
  ## License
156
170
 
package/dist/cli.js CHANGED
@@ -52465,8 +52465,8 @@ var runWith = (command, config) => {
52465
52465
  };
52466
52466
  var package_default = {
52467
52467
  name: "repo-dive",
52468
- version: "0.7.0",
52469
- description: "Dive into a git repository's history: per-commit snapshots, an indexed metrics catalog and an interactive dashboard",
52468
+ version: "0.8.0",
52469
+ description: "Dive into a git repo's history: per-commit snapshots, an indexed metrics catalog and an interactive dashboard",
52470
52470
  keywords: [
52471
52471
  "git",
52472
52472
  "repository",
@@ -52530,6 +52530,7 @@ var package_default = {
52530
52530
  "lint:tsc": "tsc --noEmit && tsc -p dashboard --noEmit",
52531
52531
  "prepack": "pnpm build",
52532
52532
  "prepare": "node .husky/install.mjs",
52533
+ "report-package-size": "node scripts/report-package-size.ts",
52533
52534
  "test": "vitest run"
52534
52535
  },
52535
52536
  "lint-staged": { "**": ["prettier --ignore-unknown --write"] },
@@ -55159,12 +55160,6 @@ var NoCollectedCommitsError = class extends TaggedError("NoCollectedCommitsError
55159
55160
  }
55160
55161
  };
55161
55162
  /**
55162
- * Heuristic for AI coding assistants appearing as commit co-authors.
55163
- * Automation bots (renovate, dependabot, github-actions, …) are deliberately
55164
- * not "AI": they don't reflect assisted authorship.
55165
- */
55166
- var isAiCoAuthor = (coAuthor) => !/renovate|dependabot|github-actions/i.test(coAuthor) && /claude|copilot|cursor|chatgpt|openai|gemini|aider|devin|coderabbit|codegen|sweep|windsurf/i.test(coAuthor);
55167
- /**
55168
55163
  * Series labels non-human contributors fold into in survival data. The
55169
55164
  * dashboard matches these literal strings to color the bands with the
55170
55165
  * reserved kind colors, so change them in both places or not at all.
@@ -55173,10 +55168,26 @@ var kindGroupLabels = {
55173
55168
  bot: "Bots",
55174
55169
  ai: "AI agents"
55175
55170
  };
55176
- /** "Claude Fable 5 <noreply@anthropic.com>" → "Claude Fable 5" */
55177
- var coAuthorIdentity = (coAuthor) => {
55178
- const angleIndex = coAuthor.indexOf("<");
55179
- return (angleIndex === -1 ? coAuthor : coAuthor.slice(0, angleIndex)).trim() || coAuthor.trim();
55171
+ /**
55172
+ * Splits a `Co-authored-by:` trailer into its parts:
55173
+ * `"Claude Fable 5 <noreply@anthropic.com>"` → name + email.
55174
+ *
55175
+ * Keeping the email (rather than the display name alone) is what lets a
55176
+ * co-author resolve through the very same `resolveContributor` an author goes
55177
+ * through — so alias groups, `displayName`/`url` overrides and kind derivation
55178
+ * apply identically whether a person wrote a commit or helped with one.
55179
+ */
55180
+ var parseIdentity = (identity) => {
55181
+ const open = identity.indexOf("<");
55182
+ const close = identity.lastIndexOf(">");
55183
+ if (open === -1 || close < open) return {
55184
+ name: identity.trim(),
55185
+ email: ""
55186
+ };
55187
+ return {
55188
+ name: identity.slice(0, open).trim(),
55189
+ email: identity.slice(open + 1, close).trim()
55190
+ };
55180
55191
  };
55181
55192
  var sumMetric = (commit, metric, filter) => {
55182
55193
  let total = 0;
@@ -55202,13 +55213,26 @@ var sumByKey = (record, keyOf) => {
55202
55213
  return merged;
55203
55214
  };
55204
55215
  var buildDashboardData = (repoRoot, commits, config) => {
55205
- const aiCoAuthorsOf = (commit) => [...commit.factsByCollector.values()].flat().filter((fact) => fact.metric === "commits.coAuthor" && isAiCoAuthor(fact.categories?.["coAuthor"] ?? "")).map((fact) => coAuthorIdentity(fact.categories?.["coAuthor"] ?? ""));
55216
+ /** The raw `"Name <email>"` trailers on a commit, in the order git listed them. */
55217
+ const coAuthorsOf = (commit) => [...commit.factsByCollector.values()].flat().filter((fact) => fact.metric === "commits.coAuthor").map((fact) => fact.categories?.["coAuthor"] ?? "");
55218
+ /**
55219
+ * A co-author resolved the same way an author is. An identity with no email
55220
+ * (a bare `Co-authored-by: Some Name`) keys off its name instead, so those
55221
+ * don't all collapse into one empty-email bucket.
55222
+ */
55223
+ const resolveCoAuthor = (identity) => {
55224
+ const { name, email } = parseIdentity(identity);
55225
+ return {
55226
+ name,
55227
+ resolved: config.resolveContributor(email || name, name)
55228
+ };
55229
+ };
55206
55230
  const commitRows = commits.map((commit) => ({
55207
55231
  sha: commit.sha.slice(0, 10),
55208
55232
  date: commit.date,
55209
55233
  author: commit.authorEmail,
55210
55234
  kind: config.resolveContributor(commit.authorEmail, commit.authorName).kind,
55211
- ai: aiCoAuthorsOf(commit).length > 0,
55235
+ ai: coAuthorsOf(commit).some((identity) => resolveCoAuthor(identity).resolved.kind === "ai"),
55212
55236
  added: sumMetric(commit, "churn.added"),
55213
55237
  deleted: sumMetric(commit, "churn.deleted")
55214
55238
  }));
@@ -55287,32 +55311,80 @@ var buildDashboardData = (repoRoot, commits, config) => {
55287
55311
  };
55288
55312
  });
55289
55313
  const contributorMap = /* @__PURE__ */ new Map();
55290
- for (const [index, commit] of commits.entries()) {
55291
- const row = commitRows[index];
55292
- if (!row) continue;
55293
- const resolved = config.resolveContributor(commit.authorEmail, commit.authorName);
55294
- const key = resolved.canonicalEmail.toLowerCase();
55314
+ /**
55315
+ * The name as spelled on this commit (author line or trailer): a configured
55316
+ * displayName wins, otherwise the observed spelling, tidied so a bot's
55317
+ * `[bot]` suffix doesn't double up with its kind badge.
55318
+ */
55319
+ const nameOf = (resolved, observedName) => resolved.displayName ?? (observedName ? normalizeContributorName(observedName) : resolved.label);
55320
+ /**
55321
+ * Fetches or creates a contributor's bucket.
55322
+ *
55323
+ * Humans key off their canonical email alone, so an alias group folds every
55324
+ * spelling of one person together. Bots and AI agents key off their name too:
55325
+ * they share vendor noreply addresses — "Claude Fable 5" and "Claude Opus
55326
+ * 4.8" are both `<noreply@anthropic.com>` — and for them the name carries the
55327
+ * identity worth telling apart. Giving such a group a `displayName` in the
55328
+ * config merges them back into one.
55329
+ */
55330
+ const bucketFor = (resolved, observedName) => {
55331
+ const name = nameOf(resolved, observedName);
55332
+ const email = resolved.canonicalEmail.toLowerCase();
55333
+ const key = resolved.kind === "human" ? email : `${name.toLowerCase()}${email}`;
55295
55334
  const bucket = contributorMap.get(key) ?? {
55296
55335
  email: resolved.canonicalEmail,
55297
- name: resolved.label,
55336
+ name,
55298
55337
  url: resolved.url,
55299
55338
  kind: resolved.kind,
55300
55339
  commits: 0,
55301
55340
  added: 0,
55302
- deleted: 0
55341
+ deleted: 0,
55342
+ assistedBy: {},
55343
+ assisted: {}
55303
55344
  };
55304
- bucket.name = resolved.displayName ?? (commit.authorName ? normalizeContributorName(commit.authorName) : bucket.name);
55305
- bucket.commits += 1;
55306
- bucket.added += row.added;
55307
- bucket.deleted += row.deleted;
55345
+ bucket.name = name;
55308
55346
  contributorMap.set(key, bucket);
55309
- }
55310
- const contributors = [...contributorMap.values()].toSorted((left, right) => right.commits - left.commits).slice(0, 25);
55311
- const aiIdentityMap = /* @__PURE__ */ new Map();
55312
- for (const commit of commits) for (const identity of new Set(aiCoAuthorsOf(commit))) aiIdentityMap.set(identity, (aiIdentityMap.get(identity) ?? 0) + 1);
55313
- const aiIdentities = [...aiIdentityMap.entries()].toSorted(([, left], [, right]) => right - left).map(([identity, commitCount]) => ({
55314
- identity,
55315
- commits: commitCount
55347
+ return bucket;
55348
+ };
55349
+ for (const [index, commit] of commits.entries()) {
55350
+ const row = commitRows[index];
55351
+ if (!row) continue;
55352
+ const author = config.resolveContributor(commit.authorEmail, commit.authorName);
55353
+ const authorBucket = bucketFor(author, commit.authorName);
55354
+ authorBucket.commits += 1;
55355
+ authorBucket.added += row.added;
55356
+ authorBucket.deleted += row.deleted;
55357
+ const helpers = /* @__PURE__ */ new Set();
55358
+ for (const identity of coAuthorsOf(commit)) {
55359
+ const { name, resolved } = resolveCoAuthor(identity);
55360
+ if (resolved.kind !== author.kind) helpers.add(bucketFor(resolved, name));
55361
+ }
55362
+ const helperKinds = /* @__PURE__ */ new Set();
55363
+ for (const helper of helpers) {
55364
+ helper.assisted[author.kind] = (helper.assisted[author.kind] ?? 0) + 1;
55365
+ helperKinds.add(helper.kind);
55366
+ }
55367
+ for (const kind of helperKinds) authorBucket.assistedBy[kind] = (authorBucket.assistedBy[kind] ?? 0) + 1;
55368
+ }
55369
+ const sumCounts = (counts) => Object.values(counts).reduce((total, count) => total + count, 0);
55370
+ /** Ranking weight: authoring and helping both count as taking part. */
55371
+ const involvementOf = (bucket) => bucket.commits + sumCounts(bucket.assisted);
55372
+ /** Emitted as absent rather than `{}` so the JSON stays lean. */
55373
+ const countsOrUndefined = (counts) => Object.keys(counts).length === 0 ? void 0 : counts;
55374
+ const perKindCap = config.maxInCharts * 2;
55375
+ const keptPerKind = {
55376
+ human: 0,
55377
+ bot: 0,
55378
+ ai: 0
55379
+ };
55380
+ const contributors = [...contributorMap.values()].toSorted((left, right) => involvementOf(right) - involvementOf(left)).filter((bucket) => {
55381
+ if (keptPerKind[bucket.kind] >= perKindCap) return false;
55382
+ keptPerKind[bucket.kind] += 1;
55383
+ return true;
55384
+ }).map((bucket) => ({
55385
+ ...bucket,
55386
+ assistedBy: countsOrUndefined(bucket.assistedBy),
55387
+ assisted: countsOrUndefined(bucket.assisted)
55316
55388
  }));
55317
55389
  return {
55318
55390
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -55334,8 +55406,7 @@ var buildDashboardData = (repoRoot, commits, config) => {
55334
55406
  dependencies,
55335
55407
  topRules,
55336
55408
  survival,
55337
- contributors,
55338
- aiIdentities
55409
+ contributors
55339
55410
  };
55340
55411
  };
55341
55412
  var writeSqlite = (dbPath, commits) => {