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.
- package/README.md +84 -0
- package/demo.gif +0 -0
- package/dist/analyzers/busFactorAnalyzer.d.ts +6 -0
- package/dist/analyzers/busFactorAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/busFactorAnalyzer.js +82 -0
- package/dist/analyzers/busFactorAnalyzer.js.map +1 -0
- package/dist/analyzers/curseScorer.d.ts +3 -0
- package/dist/analyzers/curseScorer.d.ts.map +1 -0
- package/dist/analyzers/curseScorer.js +53 -0
- package/dist/analyzers/curseScorer.js.map +1 -0
- package/dist/analyzers/ownershipAnalyzer.d.ts +7 -0
- package/dist/analyzers/ownershipAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/ownershipAnalyzer.js +38 -0
- package/dist/analyzers/ownershipAnalyzer.js.map +1 -0
- package/dist/core/gitParser.d.ts +7 -0
- package/dist/core/gitParser.d.ts.map +1 -0
- package/dist/core/gitParser.js +145 -0
- package/dist/core/gitParser.js.map +1 -0
- package/dist/core/orchestrator.d.ts +3 -0
- package/dist/core/orchestrator.d.ts.map +1 -0
- package/dist/core/orchestrator.js +66 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +127 -0
- package/dist/index.js.map +1 -0
- package/dist/output/formatter.d.ts +9 -0
- package/dist/output/formatter.d.ts.map +1 -0
- package/dist/output/formatter.js +76 -0
- package/dist/output/formatter.js.map +1 -0
- package/dist/output/htmlReport.d.ts +3 -0
- package/dist/output/htmlReport.d.ts.map +1 -0
- package/dist/output/htmlReport.js +194 -0
- package/dist/output/htmlReport.js.map +1 -0
- package/dist/output/terminalRenderer.d.ts +3 -0
- package/dist/output/terminalRenderer.d.ts.map +1 -0
- package/dist/output/terminalRenderer.js +158 -0
- package/dist/output/terminalRenderer.js.map +1 -0
- package/dist/types.d.ts +67 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/git-arch-report-express.html +513 -0
- 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
|
+

|
|
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 @@
|
|
|
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":";;AAmBA,4CAsCC;AAvDD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAC9B,MAAM,aAAa,GAAG,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAEzC,SAAS,aAAa,CAAC,oBAA4B;IACjD,MAAM,UAAU,GAAG,CAAC,GAAG,GAAG,oBAAoB,CAAC,GAAG,aAAa,CAAC;IAChE,0DAA0D;IAC1D,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,UAAU,CAAC,CAAC;AACrC,CAAC;AAED,SAAS,SAAS,CAAC,QAAkB;IACnC,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IAClC,MAAM,MAAM,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACnD,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC;IAC1E,IAAI,SAAS,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC,MAAM,CAAC;IAC5C,OAAO,QAAQ,CAAC,MAAM,GAAG,SAAS,CAAC;AACrC,CAAC;AAED,SAAgB,gBAAgB,CAC9B,YAAoC,EACpC,OAAe,EAAE;IAEjB,MAAM,OAAO,GAAiB,EAAE,CAAC;IAEjC,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,YAAY,EAAE,CAAC;QACrC,MAAM,WAAW,GAAG,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC;QAC7C,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACjD,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QAE9C,uBAAuB;QACvB,sCAAsC;QACtC,0DAA0D;QAC1D,+DAA+D;QAC/D,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAC3B,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,GAAG,CACvF,GAAG,GAAG,CAAC;QAER,MAAM,OAAO,GAAa,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,YAAY,GAAG,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,WAAW,KAAK,CAAC,YAAY,QAAQ,CAAC,CAAC;QACjF,IAAI,WAAW,GAAG,CAAC;YAAE,OAAO,CAAC,IAAI,CAAC,cAAc,WAAW,oBAAoB,CAAC,CAAC;QACjF,IAAI,KAAK,GAAG,EAAE;YAAE,OAAO,CAAC,IAAI,CAAC,oBAAoB,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC7E,IAAI,OAAO,GAAG,GAAG;YAAE,OAAO,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;QAE1D,OAAO,CAAC,IAAI,CAAC;YACX,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,UAAU;YACV,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,aAAa,EAAE,WAAW;YAC1B,aAAa,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,GAAG;YAC9C,OAAO,EAAE,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC;SAC7D,CAAC,CAAC;IACL,CAAC;IAED,OAAO,OAAO;SACX,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC;SAC3C,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AACpB,CAAC"}
|
|
@@ -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":"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"}
|
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|