git-archaeologist 1.0.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.
Files changed (44) hide show
  1. package/README.md +84 -0
  2. package/demo.gif +0 -0
  3. package/dist/analyzers/busFactorAnalyzer.d.ts +6 -0
  4. package/dist/analyzers/busFactorAnalyzer.d.ts.map +1 -0
  5. package/dist/analyzers/busFactorAnalyzer.js +82 -0
  6. package/dist/analyzers/busFactorAnalyzer.js.map +1 -0
  7. package/dist/analyzers/curseScorer.d.ts +3 -0
  8. package/dist/analyzers/curseScorer.d.ts.map +1 -0
  9. package/dist/analyzers/curseScorer.js +53 -0
  10. package/dist/analyzers/curseScorer.js.map +1 -0
  11. package/dist/analyzers/ownershipAnalyzer.d.ts +7 -0
  12. package/dist/analyzers/ownershipAnalyzer.d.ts.map +1 -0
  13. package/dist/analyzers/ownershipAnalyzer.js +38 -0
  14. package/dist/analyzers/ownershipAnalyzer.js.map +1 -0
  15. package/dist/core/gitParser.d.ts +7 -0
  16. package/dist/core/gitParser.d.ts.map +1 -0
  17. package/dist/core/gitParser.js +145 -0
  18. package/dist/core/gitParser.js.map +1 -0
  19. package/dist/core/orchestrator.d.ts +3 -0
  20. package/dist/core/orchestrator.d.ts.map +1 -0
  21. package/dist/core/orchestrator.js +66 -0
  22. package/dist/core/orchestrator.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +127 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/output/formatter.d.ts +9 -0
  28. package/dist/output/formatter.d.ts.map +1 -0
  29. package/dist/output/formatter.js +76 -0
  30. package/dist/output/formatter.js.map +1 -0
  31. package/dist/output/htmlReport.d.ts +3 -0
  32. package/dist/output/htmlReport.d.ts.map +1 -0
  33. package/dist/output/htmlReport.js +194 -0
  34. package/dist/output/htmlReport.js.map +1 -0
  35. package/dist/output/terminalRenderer.d.ts +3 -0
  36. package/dist/output/terminalRenderer.d.ts.map +1 -0
  37. package/dist/output/terminalRenderer.js +158 -0
  38. package/dist/output/terminalRenderer.js.map +1 -0
  39. package/dist/types.d.ts +67 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +3 -0
  42. package/dist/types.js.map +1 -0
  43. package/git-arch-report-express.html +513 -0
  44. package/package.json +46 -0
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # git-archaeologist
2
+
3
+ Run one command. Find the time bombs in any codebase.
4
+
5
+ ![demo](./demo.gif)
6
+
7
+ ---
8
+
9
+ I wrote this after two days tracking a bug that started because I touched a file 53 different people had already destroyed. This tool finds those files before you touch them.
10
+
11
+ ---
12
+
13
+ ## What it does
14
+
15
+ **Cursed file score** — not just most changed. A file touched 100 times in 6 months by 12 developers who never talked scores way higher than one touched 100 times over 5 years by the same person.
16
+
17
+ **Bus factor per folder** — not per repo. Knowing the lib/ folder will be orphaned the day Douglas leaves is actionable. Knowing the whole repo has bus factor 2 is useless.
18
+
19
+ **Implicit coupling** — files that always change together even though nothing in the code connects them. Hidden dependencies. Future outages.
20
+
21
+ **Ownership** — not who created the file. Who owns the lines still alive in HEAD right now.
22
+
23
+ ---
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install -g git-archaeologist
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Usage
34
+
35
+ ```bash
36
+ # full report
37
+ git-arch analyze /path/to/repo
38
+
39
+ # just cursed files
40
+ git-arch cursed --top 10
41
+
42
+ # shareable HTML report
43
+ git-arch analyze /path/to/repo --html
44
+
45
+ # raw JSON
46
+ git-arch analyze /path/to/repo --json
47
+ ```
48
+
49
+ ---
50
+
51
+ ## What it found on Express.js
52
+
53
+ Express has 1716 commits and 230 contributors.
54
+
55
+ `lib/response.js` — 128 changes, 53 authors, curse score 2261. The core of Express. A disaster waiting to happen.
56
+
57
+ Every single module — lib/, test/, examples/, benchmarks/ — has bus factor 1. One person. Douglas Christopher Wilson. If he stops tomorrow nobody else fully understands any of it.
58
+
59
+ `benchmarks/Makefile` and `benchmarks/run` have 100% coupling. They are one file pretending to be two.
60
+
61
+ ---
62
+
63
+ ## The formula
64
+
65
+ ```
66
+ curse_score = changes x log2(authors+1) x exp(-0.5 x age_years) x log2(churn_rate+2)
67
+ ```
68
+
69
+ The exponential decay on age means old chaos that stabilized does not show up. Only current danger.
70
+
71
+ ---
72
+
73
+ ## Run locally
74
+
75
+ ```bash
76
+ git clone https://github.com/SushantVerma7969/git-archaeologist.git
77
+ cd git-archaeologist
78
+ npm install && npm run build
79
+ node dist/index.js analyze /any/repo
80
+ ```
81
+
82
+ ---
83
+
84
+ MIT. Use it however you want.
package/demo.gif ADDED
Binary file
@@ -0,0 +1,6 @@
1
+ import { FileStats, BusFactor, CouplingPair } from '../types';
2
+ export declare function analyzeBusFactor(fileStatsMap: Map<string, FileStats>, authorNameMap: Map<string, string>): BusFactor[];
3
+ export declare function analyzeCoupling(commits: Array<{
4
+ filesChanged: string[];
5
+ }>, minCoChanges?: number): CouplingPair[];
6
+ //# sourceMappingURL=busFactorAnalyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"busFactorAnalyzer.d.ts","sourceRoot":"","sources":["../../src/analyzers/busFactorAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE9D,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,EACpC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,SAAS,EAAE,CAwDb;AAED,wBAAgB,eAAe,CAC7B,OAAO,EAAE,KAAK,CAAC;IAAE,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,EAC1C,YAAY,GAAE,MAAU,GACvB,YAAY,EAAE,CAwChB"}
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeBusFactor = analyzeBusFactor;
4
+ exports.analyzeCoupling = analyzeCoupling;
5
+ function analyzeBusFactor(fileStatsMap, authorNameMap) {
6
+ // Group files by top-level folder
7
+ const folderMap = new Map();
8
+ for (const [, stats] of fileStatsMap) {
9
+ const parts = stats.filepath.split('/');
10
+ const folder = parts.length > 1 ? parts[0] : '(root)';
11
+ if (!folderMap.has(folder)) {
12
+ folderMap.set(folder, new Map());
13
+ }
14
+ const authorTotals = folderMap.get(folder);
15
+ for (const [email, count] of stats.authorChanges) {
16
+ authorTotals.set(email, (authorTotals.get(email) ?? 0) + count);
17
+ }
18
+ }
19
+ const results = [];
20
+ for (const [folder, authorTotals] of folderMap) {
21
+ const totalChanges = Array.from(authorTotals.values()).reduce((a, b) => a + b, 0);
22
+ if (totalChanges === 0)
23
+ continue;
24
+ const sorted = Array.from(authorTotals.entries())
25
+ .sort((a, b) => b[1] - a[1]);
26
+ // Bus factor = how many top authors account for >50% of all changes
27
+ let cumulative = 0;
28
+ let busFactor = 0;
29
+ const atRiskAuthors = [];
30
+ for (const [email, count] of sorted) {
31
+ cumulative += count;
32
+ busFactor += 1;
33
+ atRiskAuthors.push(authorNameMap.get(email) ?? email);
34
+ if (cumulative / totalChanges >= 0.5)
35
+ break;
36
+ }
37
+ const filesAtRisk = Array.from(fileStatsMap.values()).filter((s) => s.filepath.startsWith(folder + '/')).length;
38
+ let warning = '';
39
+ if (busFactor === 1) {
40
+ warning = `⚠️ Single point of failure — only ${atRiskAuthors[0]} owns this module`;
41
+ }
42
+ else if (busFactor === 2) {
43
+ warning = `⚡ High risk — only 2 people understand this module`;
44
+ }
45
+ else {
46
+ warning = `✓ Healthy ownership spread`;
47
+ }
48
+ results.push({ scope: folder, busFactor, atRiskAuthors, filesAtRisk, warning });
49
+ }
50
+ return results.sort((a, b) => a.busFactor - b.busFactor);
51
+ }
52
+ function analyzeCoupling(commits, minCoChanges = 3) {
53
+ const coChangeMap = new Map();
54
+ const fileChangeCount = new Map();
55
+ for (const commit of commits) {
56
+ const files = commit.filesChanged.filter((f) => f.length > 0);
57
+ for (const file of files) {
58
+ fileChangeCount.set(file, (fileChangeCount.get(file) ?? 0) + 1);
59
+ }
60
+ // For every pair of files in this commit, increment their co-change count
61
+ for (let i = 0; i < files.length; i++) {
62
+ for (let j = i + 1; j < files.length; j++) {
63
+ const key = [files[i], files[j]].sort().join('|||');
64
+ coChangeMap.set(key, (coChangeMap.get(key) ?? 0) + 1);
65
+ }
66
+ }
67
+ }
68
+ const results = [];
69
+ for (const [key, coChanges] of coChangeMap) {
70
+ if (coChanges < minCoChanges)
71
+ continue;
72
+ const [fileA, fileB] = key.split('|||');
73
+ const maxChanges = Math.max(fileChangeCount.get(fileA) ?? 1, fileChangeCount.get(fileB) ?? 1);
74
+ // Coupling score = how often they change together relative to how often each changes
75
+ const couplingScore = Math.round((coChanges / maxChanges) * 1000) / 10;
76
+ results.push({ fileA, fileB, coChanges, couplingScore });
77
+ }
78
+ return results
79
+ .sort((a, b) => b.couplingScore - a.couplingScore)
80
+ .slice(0, 30);
81
+ }
82
+ //# sourceMappingURL=busFactorAnalyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"busFactorAnalyzer.js","sourceRoot":"","sources":["../../src/analyzers/busFactorAnalyzer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { FileStats, CursedFile } from '../types';
2
+ export declare function scoreCursedFiles(fileStatsMap: Map<string, FileStats>, topN?: number): CursedFile[];
3
+ //# sourceMappingURL=curseScorer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"curseScorer.d.ts","sourceRoot":"","sources":["../../src/analyzers/curseScorer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAmBjD,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,EACpC,IAAI,GAAE,MAAW,GAChB,UAAU,EAAE,CAmCd"}
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scoreCursedFiles = scoreCursedFiles;
4
+ const NOW = Date.now() / 1000;
5
+ const ONE_YEAR_SECS = 365 * 24 * 60 * 60;
6
+ function recencyWeight(lastChangedTimestamp) {
7
+ const ageInYears = (NOW - lastChangedTimestamp) / ONE_YEAR_SECS;
8
+ // Files touched recently score higher — exponential decay
9
+ return Math.exp(-0.5 * ageInYears);
10
+ }
11
+ function churnRate(timeline) {
12
+ if (timeline.length < 2)
13
+ return 0;
14
+ const sorted = [...timeline].sort((a, b) => a - b);
15
+ const spanYears = (sorted[sorted.length - 1] - sorted[0]) / ONE_YEAR_SECS;
16
+ if (spanYears === 0)
17
+ return timeline.length;
18
+ return timeline.length / spanYears;
19
+ }
20
+ function scoreCursedFiles(fileStatsMap, topN = 20) {
21
+ const results = [];
22
+ for (const [, stats] of fileStatsMap) {
23
+ const authorCount = stats.uniqueAuthors.size;
24
+ const recency = recencyWeight(stats.lastChanged);
25
+ const churn = churnRate(stats.changeTimeline);
26
+ // Curse score formula:
27
+ // Base = total changes × author count
28
+ // Multiplied by recency (recent files are more dangerous)
29
+ // Multiplied by churn rate (files changed frequently per year)
30
+ const curseScore = Math.round(stats.totalChanges * Math.log2(authorCount + 1) * recency * Math.log2(churn + 2) * 100) / 100;
31
+ const reasons = [];
32
+ if (stats.totalChanges > 50)
33
+ reasons.push(`Changed ${stats.totalChanges} times`);
34
+ if (authorCount > 5)
35
+ reasons.push(`Touched by ${authorCount} different authors`);
36
+ if (churn > 20)
37
+ reasons.push(`High churn rate (${Math.round(churn)}x/year)`);
38
+ if (recency > 0.8)
39
+ reasons.push('Modified very recently');
40
+ results.push({
41
+ filepath: stats.filepath,
42
+ curseScore,
43
+ totalChanges: stats.totalChanges,
44
+ uniqueAuthors: authorCount,
45
+ recencyWeight: Math.round(recency * 100) / 100,
46
+ reasons: reasons.length > 0 ? reasons : ['Mild instability'],
47
+ });
48
+ }
49
+ return results
50
+ .sort((a, b) => b.curseScore - a.curseScore)
51
+ .slice(0, topN);
52
+ }
53
+ //# sourceMappingURL=curseScorer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"curseScorer.js","sourceRoot":"","sources":["../../src/analyzers/curseScorer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { FileStats, FileOwnership } from '../types';
2
+ export declare function analyzeOwnership(fileStatsMap: Map<string, FileStats>, authorNameMap: Map<string, string>): FileOwnership[];
3
+ export declare function buildAuthorNameMap(commits: Array<{
4
+ authorEmail: string;
5
+ authorName: string;
6
+ }>): Map<string, string>;
7
+ //# sourceMappingURL=ownershipAnalyzer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownershipAnalyzer.d.ts","sourceRoot":"","sources":["../../src/analyzers/ownershipAnalyzer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEpD,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,EACpC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,aAAa,EAAE,CA2BjB;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,KAAK,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GAC1D,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAQrB"}
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeOwnership = analyzeOwnership;
4
+ exports.buildAuthorNameMap = buildAuthorNameMap;
5
+ function analyzeOwnership(fileStatsMap, authorNameMap) {
6
+ const results = [];
7
+ for (const [, stats] of fileStatsMap) {
8
+ if (stats.totalChanges === 0)
9
+ continue;
10
+ const contributors = Array.from(stats.authorChanges.entries())
11
+ .map(([email, changes]) => ({
12
+ name: authorNameMap.get(email) ?? email,
13
+ email,
14
+ changes,
15
+ percent: Math.round((changes / stats.totalChanges) * 1000) / 10,
16
+ }))
17
+ .sort((a, b) => b.changes - a.changes);
18
+ const top = contributors[0];
19
+ results.push({
20
+ filepath: stats.filepath,
21
+ owner: top.name,
22
+ ownerEmail: top.email,
23
+ ownershipPercent: top.percent,
24
+ contributors,
25
+ });
26
+ }
27
+ return results.sort((a, b) => b.ownershipPercent - a.ownershipPercent);
28
+ }
29
+ function buildAuthorNameMap(commits) {
30
+ const map = new Map();
31
+ for (const c of commits) {
32
+ if (!map.has(c.authorEmail)) {
33
+ map.set(c.authorEmail, c.authorName);
34
+ }
35
+ }
36
+ return map;
37
+ }
38
+ //# sourceMappingURL=ownershipAnalyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ownershipAnalyzer.js","sourceRoot":"","sources":["../../src/analyzers/ownershipAnalyzer.ts"],"names":[],"mappings":";;AAEA,4CA8BC;AAED,gDAUC;AA1CD,SAAgB,gBAAgB,CAC9B,YAAoC,EACpC,aAAkC;IAElC,MAAM,OAAO,GAAoB,EAAE,CAAC;IAEpC,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,YAAY,EAAE,CAAC;QACrC,IAAI,KAAK,CAAC,YAAY,KAAK,CAAC;YAAE,SAAS;QAEvC,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC;aAC3D,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC;YAC1B,IAAI,EAAE,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK;YACvC,KAAK;YACL,OAAO;YACP,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE;SAChE,CAAC,CAAC;aACF,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC;QAEzC,MAAM,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QAE5B,OAAO,CAAC,IAAI,CAAC;YACX,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,KAAK,EAAE,GAAG,CAAC,IAAI;YACf,UAAU,EAAE,GAAG,CAAC,KAAK;YACrB,gBAAgB,EAAE,GAAG,CAAC,OAAO;YAC7B,YAAY;SACb,CAAC,CAAC;IACL,CAAC;IAED,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,GAAG,CAAC,CAAC,gBAAgB,CAAC,CAAC;AACzE,CAAC;AAED,SAAgB,kBAAkB,CAChC,OAA2D;IAE3D,MAAM,GAAG,GAAG,IAAI,GAAG,EAAkB,CAAC;IACtC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC;YAC5B,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { CommitRecord, FileStats } from '../types';
2
+ export declare function validateRepo(repoPath: string): void;
3
+ export declare function getRepoName(repoPath: string): string;
4
+ export declare function getTotalCommitCount(repoPath: string): number;
5
+ export declare function parseCommits(repoPath: string): CommitRecord[];
6
+ export declare function buildFileStats(commits: CommitRecord[]): Map<string, FileStats>;
7
+ //# sourceMappingURL=gitParser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitParser.d.ts","sourceRoot":"","sources":["../../src/core/gitParser.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAEnD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CASnD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAcpD;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ5D;AAYD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,EAAE,CAyC7D;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CA+B9E"}
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.validateRepo = validateRepo;
37
+ exports.getRepoName = getRepoName;
38
+ exports.getTotalCommitCount = getTotalCommitCount;
39
+ exports.parseCommits = parseCommits;
40
+ exports.buildFileStats = buildFileStats;
41
+ const child_process_1 = require("child_process");
42
+ const path = __importStar(require("path"));
43
+ function validateRepo(repoPath) {
44
+ try {
45
+ (0, child_process_1.execSync)('git rev-parse --is-inside-work-tree', {
46
+ cwd: repoPath,
47
+ stdio: 'pipe',
48
+ });
49
+ }
50
+ catch {
51
+ throw new Error(`Not a valid git repository: ${repoPath}`);
52
+ }
53
+ }
54
+ function getRepoName(repoPath) {
55
+ try {
56
+ const remote = (0, child_process_1.execSync)('git remote get-url origin', {
57
+ cwd: repoPath,
58
+ stdio: 'pipe',
59
+ })
60
+ .toString()
61
+ .trim();
62
+ const match = remote.match(/\/([^/]+?)(\.git)?$/);
63
+ if (match)
64
+ return match[1];
65
+ }
66
+ catch {
67
+ // no remote, fall back to folder name
68
+ }
69
+ return path.basename(path.resolve(repoPath));
70
+ }
71
+ function getTotalCommitCount(repoPath) {
72
+ const out = (0, child_process_1.execSync)('git rev-list --count HEAD', {
73
+ cwd: repoPath,
74
+ stdio: 'pipe',
75
+ })
76
+ .toString()
77
+ .trim();
78
+ return parseInt(out, 10);
79
+ }
80
+ function sanitizeFilePath(raw) {
81
+ // git sometimes wraps paths containing special chars in double quotes
82
+ // e.g. "test/some file.js" — strip the surrounding quotes
83
+ let p = raw.trim();
84
+ if (p.startsWith('"') && p.endsWith('"')) {
85
+ p = p.slice(1, -1);
86
+ }
87
+ return p;
88
+ }
89
+ function parseCommits(repoPath) {
90
+ const DELIMITER = '||GITARCH||';
91
+ const BEGIN_MARKER = 'BEGINCOMMIT' + DELIMITER;
92
+ const raw = (0, child_process_1.execSync)(`git log --pretty=format:"${BEGIN_MARKER}%H${DELIMITER}%ae${DELIMITER}%an${DELIMITER}%at" --name-only`, { cwd: repoPath, stdio: 'pipe', maxBuffer: 512 * 1024 * 1024 }).toString();
93
+ const commits = [];
94
+ const blocks = raw
95
+ .split(BEGIN_MARKER)
96
+ .map((b) => b.trim())
97
+ .filter(Boolean);
98
+ for (const block of blocks) {
99
+ const newlineIdx = block.indexOf('\n');
100
+ const header = newlineIdx === -1 ? block.trim() : block.substring(0, newlineIdx).trim();
101
+ const filesRaw = newlineIdx === -1 ? '' : block.substring(newlineIdx + 1).trim();
102
+ const parts = header.split(DELIMITER);
103
+ if (parts.length !== 4)
104
+ continue;
105
+ const [hash, authorEmail, authorName, tsRaw] = parts;
106
+ const timestamp = parseInt(tsRaw, 10);
107
+ if (isNaN(timestamp))
108
+ continue;
109
+ const filesChanged = filesRaw
110
+ .split('\n')
111
+ .map((f) => sanitizeFilePath(f))
112
+ .filter((f) => f.length > 0);
113
+ commits.push({ hash, authorEmail, authorName, timestamp, filesChanged });
114
+ }
115
+ return commits;
116
+ }
117
+ function buildFileStats(commits) {
118
+ const statsMap = new Map();
119
+ for (const commit of commits) {
120
+ for (const filepath of commit.filesChanged) {
121
+ if (!statsMap.has(filepath)) {
122
+ statsMap.set(filepath, {
123
+ filepath,
124
+ totalChanges: 0,
125
+ uniqueAuthors: new Set(),
126
+ authorChanges: new Map(),
127
+ firstChanged: commit.timestamp,
128
+ lastChanged: commit.timestamp,
129
+ changeTimeline: [],
130
+ });
131
+ }
132
+ const stats = statsMap.get(filepath);
133
+ stats.totalChanges += 1;
134
+ stats.uniqueAuthors.add(commit.authorEmail);
135
+ stats.authorChanges.set(commit.authorEmail, (stats.authorChanges.get(commit.authorEmail) ?? 0) + 1);
136
+ if (commit.timestamp < stats.firstChanged)
137
+ stats.firstChanged = commit.timestamp;
138
+ if (commit.timestamp > stats.lastChanged)
139
+ stats.lastChanged = commit.timestamp;
140
+ stats.changeTimeline.push(commit.timestamp);
141
+ }
142
+ }
143
+ return statsMap;
144
+ }
145
+ //# sourceMappingURL=gitParser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitParser.js","sourceRoot":"","sources":["../../src/core/gitParser.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIA,oCASC;AAED,kCAcC;AAED,kDAQC;AAYD,oCAyCC;AAED,wCA+BC;AA7HD,iDAAyC;AACzC,2CAA6B;AAG7B,SAAgB,YAAY,CAAC,QAAgB;IAC3C,IAAI,CAAC;QACH,IAAA,wBAAQ,EAAC,qCAAqC,EAAE;YAC9C,GAAG,EAAE,QAAQ;YACb,KAAK,EAAE,MAAM;SACd,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAC;IAC7D,CAAC;AACH,CAAC;AAED,SAAgB,WAAW,CAAC,QAAgB;IAC1C,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAA,wBAAQ,EAAC,2BAA2B,EAAE;YACnD,GAAG,EAAE,QAAQ;YACb,KAAK,EAAE,MAAM;SACd,CAAC;aACC,QAAQ,EAAE;aACV,IAAI,EAAE,CAAC;QACV,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;QAClD,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,SAAgB,mBAAmB,CAAC,QAAgB;IAClD,MAAM,GAAG,GAAG,IAAA,wBAAQ,EAAC,2BAA2B,EAAE;QAChD,GAAG,EAAE,QAAQ;QACb,KAAK,EAAE,MAAM;KACd,CAAC;SACC,QAAQ,EAAE;SACV,IAAI,EAAE,CAAC;IACV,OAAO,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAW;IACnC,sEAAsE;IACtE,0DAA0D;IAC1D,IAAI,CAAC,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IACnB,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAgB,YAAY,CAAC,QAAgB;IAC3C,MAAM,SAAS,GAAG,aAAa,CAAC;IAChC,MAAM,YAAY,GAAG,aAAa,GAAG,SAAS,CAAC;IAE/C,MAAM,GAAG,GAAG,IAAA,wBAAQ,EAClB,4BAA4B,YAAY,KAAK,SAAS,MAAM,SAAS,MAAM,SAAS,kBAAkB,EACtG,EAAE,GAAG,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI,EAAE,CAC/D,CAAC,QAAQ,EAAE,CAAC;IAEb,MAAM,OAAO,GAAmB,EAAE,CAAC;IAEnC,MAAM,MAAM,GAAG,GAAG;SACf,KAAK,CAAC,YAAY,CAAC;SACnB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IAEnB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAEvC,MAAM,MAAM,GACV,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAC;QAE3E,MAAM,QAAQ,GACZ,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAElE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACtC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAEjC,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC;QACrD,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACtC,IAAI,KAAK,CAAC,SAAS,CAAC;YAAE,SAAS;QAE/B,MAAM,YAAY,GAAG,QAAQ;aAC1B,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;aACvC,MAAM,CAAC,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAEvC,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAgB,cAAc,CAAC,OAAuB;IACpD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAqB,CAAC;IAE9C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,KAAK,MAAM,QAAQ,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;YAC3C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC5B,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE;oBACrB,QAAQ;oBACR,YAAY,EAAE,CAAC;oBACf,aAAa,EAAE,IAAI,GAAG,EAAE;oBACxB,aAAa,EAAE,IAAI,GAAG,EAAE;oBACxB,YAAY,EAAE,MAAM,CAAC,SAAS;oBAC9B,WAAW,EAAE,MAAM,CAAC,SAAS;oBAC7B,cAAc,EAAE,EAAE;iBACnB,CAAC,CAAC;YACL,CAAC;YAED,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAE,CAAC;YACtC,KAAK,CAAC,YAAY,IAAI,CAAC,CAAC;YACxB,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YAC5C,KAAK,CAAC,aAAa,CAAC,GAAG,CACrB,MAAM,CAAC,WAAW,EAClB,CAAC,KAAK,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CACvD,CAAC;YACF,IAAI,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,YAAY;gBAAE,KAAK,CAAC,YAAY,GAAG,MAAM,CAAC,SAAS,CAAC;YACjF,IAAI,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,WAAW;gBAAE,KAAK,CAAC,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC;YAC/E,KAAK,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { AnalysisResult } from '../types';
2
+ export declare function analyze(repoPath: string): Promise<AnalysisResult>;
3
+ //# sourceMappingURL=orchestrator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/core/orchestrator.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAY1C,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAiEvE"}
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.analyze = analyze;
7
+ const ora_1 = __importDefault(require("ora"));
8
+ const gitParser_1 = require("./gitParser");
9
+ const curseScorer_1 = require("../analyzers/curseScorer");
10
+ const ownershipAnalyzer_1 = require("../analyzers/ownershipAnalyzer");
11
+ const busFactorAnalyzer_1 = require("../analyzers/busFactorAnalyzer");
12
+ async function analyze(repoPath) {
13
+ const spinner = (0, ora_1.default)({ text: 'Validating repository...', color: 'magenta' }).start();
14
+ try {
15
+ // Step 1 — validate
16
+ (0, gitParser_1.validateRepo)(repoPath);
17
+ const repoName = (0, gitParser_1.getRepoName)(repoPath);
18
+ const totalCommits = (0, gitParser_1.getTotalCommitCount)(repoPath);
19
+ spinner.text = `Parsing ${totalCommits.toLocaleString()} commits in ${repoName}...`;
20
+ // Step 2 — parse all commits
21
+ const commits = (0, gitParser_1.parseCommits)(repoPath);
22
+ spinner.text = 'Building file statistics...';
23
+ // Step 3 — build per-file stats
24
+ const fileStats = (0, gitParser_1.buildFileStats)(commits);
25
+ // Step 4 — build author name lookup
26
+ const authorNameMap = (0, ownershipAnalyzer_1.buildAuthorNameMap)(commits);
27
+ spinner.text = 'Scoring cursed files...';
28
+ // Step 5 — run all analyzers
29
+ const cursedFiles = (0, curseScorer_1.scoreCursedFiles)(fileStats);
30
+ spinner.text = 'Analyzing ownership...';
31
+ const ownership = (0, ownershipAnalyzer_1.analyzeOwnership)(fileStats, authorNameMap);
32
+ spinner.text = 'Calculating bus factor...';
33
+ const busFactor = (0, busFactorAnalyzer_1.analyzeBusFactor)(fileStats, authorNameMap);
34
+ spinner.text = 'Detecting implicit coupling...';
35
+ const coupling = (0, busFactorAnalyzer_1.analyzeCoupling)(commits);
36
+ // Step 6 — collect date range
37
+ const allTimestamps = commits.map((c) => c.timestamp);
38
+ const minTs = Math.min(...allTimestamps);
39
+ const maxTs = Math.max(...allTimestamps);
40
+ // Step 7 — count unique authors
41
+ const allAuthors = new Set(commits.map((c) => c.authorEmail));
42
+ spinner.succeed(`Analysis complete — ${fileStats.size.toLocaleString()} files scanned`);
43
+ return {
44
+ repoPath,
45
+ repoName,
46
+ analyzedAt: new Date(),
47
+ totalCommits,
48
+ totalFiles: fileStats.size,
49
+ totalAuthors: allAuthors.size,
50
+ dateRange: {
51
+ from: new Date(minTs * 1000),
52
+ to: new Date(maxTs * 1000),
53
+ },
54
+ cursedFiles,
55
+ ownership,
56
+ busFactor,
57
+ coupling,
58
+ fileStats,
59
+ };
60
+ }
61
+ catch (err) {
62
+ spinner.fail('Analysis failed');
63
+ throw err;
64
+ }
65
+ }
66
+ //# sourceMappingURL=orchestrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orchestrator.js","sourceRoot":"","sources":["../../src/core/orchestrator.ts"],"names":[],"mappings":";;;;;AAaA,0BAiEC;AA9ED,8CAAsB;AAEtB,2CAMqB;AACrB,0DAA4D;AAC5D,sEAAsF;AACtF,sEAAmF;AAE5E,KAAK,UAAU,OAAO,CAAC,QAAgB;IAC5C,MAAM,OAAO,GAAG,IAAA,aAAG,EAAC,EAAE,IAAI,EAAE,0BAA0B,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;IAEpF,IAAI,CAAC;QACH,oBAAoB;QACpB,IAAA,wBAAY,EAAC,QAAQ,CAAC,CAAC;QACvB,MAAM,QAAQ,GAAG,IAAA,uBAAW,EAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,YAAY,GAAG,IAAA,+BAAmB,EAAC,QAAQ,CAAC,CAAC;QACnD,OAAO,CAAC,IAAI,GAAG,WAAW,YAAY,CAAC,cAAc,EAAE,eAAe,QAAQ,KAAK,CAAC;QAEpF,6BAA6B;QAC7B,MAAM,OAAO,GAAG,IAAA,wBAAY,EAAC,QAAQ,CAAC,CAAC;QACvC,OAAO,CAAC,IAAI,GAAG,6BAA6B,CAAC;QAE7C,gCAAgC;QAChC,MAAM,SAAS,GAAG,IAAA,0BAAc,EAAC,OAAO,CAAC,CAAC;QAE1C,oCAAoC;QACpC,MAAM,aAAa,GAAG,IAAA,sCAAkB,EAAC,OAAO,CAAC,CAAC;QAElD,OAAO,CAAC,IAAI,GAAG,yBAAyB,CAAC;QAEzC,6BAA6B;QAC7B,MAAM,WAAW,GAAG,IAAA,8BAAgB,EAAC,SAAS,CAAC,CAAC;QAEhD,OAAO,CAAC,IAAI,GAAG,wBAAwB,CAAC;QACxC,MAAM,SAAS,GAAG,IAAA,oCAAgB,EAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QAE7D,OAAO,CAAC,IAAI,GAAG,2BAA2B,CAAC;QAC3C,MAAM,SAAS,GAAG,IAAA,oCAAgB,EAAC,SAAS,EAAE,aAAa,CAAC,CAAC;QAE7D,OAAO,CAAC,IAAI,GAAG,gCAAgC,CAAC;QAChD,MAAM,QAAQ,GAAG,IAAA,mCAAe,EAAC,OAAO,CAAC,CAAC;QAE1C,8BAA8B;QAC9B,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC;QACzC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC;QAEzC,gCAAgC;QAChC,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC;QAE9D,OAAO,CAAC,OAAO,CAAC,uBAAuB,SAAS,CAAC,IAAI,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAAC;QAExF,OAAO;YACL,QAAQ;YACR,QAAQ;YACR,UAAU,EAAE,IAAI,IAAI,EAAE;YACtB,YAAY;YACZ,UAAU,EAAE,SAAS,CAAC,IAAI;YAC1B,YAAY,EAAE,UAAU,CAAC,IAAI;YAC7B,SAAS,EAAE;gBACT,IAAI,EAAE,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;gBAC5B,EAAE,EAAE,IAAI,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;aAC3B;YACD,WAAW;YACX,SAAS;YACT,SAAS;YACT,QAAQ;YACR,SAAS;SACV,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAChC,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}