Skip to content

Commit d79ccde

Browse files
committed
feat: add git collector using agent-analyzer binary
New git collector in lib/collectors/ that runs agent-analyzer git-map init and extracts health, hotspots, contributors, AI ratio, bus factor, conventions, and release info. Registered in collect() dispatch.
1 parent 5be6dce commit d79ccde

File tree

2 files changed

+153
-1
lines changed

2 files changed

+153
-1
lines changed

lib/collectors/git.js

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/**
2+
* Git History Collector
3+
*
4+
* Collects git history analysis data using the agent-analyzer binary.
5+
* Runs a full git-map init and extracts key metrics for downstream consumers.
6+
*
7+
* @module lib/collectors/git
8+
*/
9+
10+
'use strict';
11+
12+
const binary = require('../binary');
13+
14+
const DEFAULT_OPTIONS = {
15+
top: 20,
16+
adjustForAi: false,
17+
cwd: process.cwd()
18+
};
19+
20+
/**
21+
* Collect git history data for the given repository.
22+
*
23+
* Runs agent-analyzer git-map init to produce a full map, then extracts
24+
* key metrics (hotspots, bus factor, AI ratio, etc.) from the result.
25+
*
26+
* @param {Object} [options={}] - Collection options
27+
* @param {string} [options.cwd] - Repository path (default: process.cwd())
28+
* @param {number} [options.top=20] - Number of hotspots to return
29+
* @param {boolean} [options.adjustForAi=false] - Adjust bus factor for AI commits
30+
* @returns {Object} Git history metrics
31+
*/
32+
function collectGitData(options = {}) {
33+
const opts = { ...DEFAULT_OPTIONS, ...options };
34+
const cwd = opts.cwd || process.cwd();
35+
36+
try {
37+
binary.ensureBinarySync();
38+
} catch (err) {
39+
return {
40+
available: false,
41+
error: `Binary not available: ${err.message}`
42+
};
43+
}
44+
45+
let map;
46+
try {
47+
const json = binary.runAnalyzer(['git-map', 'init', cwd]);
48+
map = JSON.parse(json);
49+
} catch (err) {
50+
return {
51+
available: false,
52+
error: `Git analysis failed: ${err.message}`
53+
};
54+
}
55+
56+
// Extract metrics from the map
57+
const fileActivity = map.fileActivity || {};
58+
const contributors = map.contributors || {};
59+
const aiAttribution = map.aiAttribution || {};
60+
const commitShape = map.commitShape || {};
61+
const conventions = map.conventions || {};
62+
const releases = map.releases || {};
63+
64+
// Hotspots: sort files by change count
65+
const hotspots = Object.entries(fileActivity)
66+
.map(([path, activity]) => ({
67+
path,
68+
changes: activity.totalChanges || 0,
69+
authors: activity.authors ? Object.keys(activity.authors).length : 0,
70+
lastChanged: activity.lastChanged || null
71+
}))
72+
.sort((a, b) => b.changes - a.changes)
73+
.slice(0, opts.top);
74+
75+
// Contributors summary
76+
const humans = contributors.humans || {};
77+
const humanList = Object.entries(humans)
78+
.map(([name, data]) => ({
79+
name,
80+
commits: data.commitCount || 0,
81+
firstSeen: data.firstSeen || null,
82+
lastSeen: data.lastSeen || null
83+
}))
84+
.sort((a, b) => b.commits - a.commits);
85+
86+
// Bus factor: people covering 80% of commits
87+
const totalCommits = humanList.reduce((sum, c) => sum + c.commits, 0);
88+
let cumulative = 0;
89+
let busFactor = 0;
90+
for (const contributor of humanList) {
91+
cumulative += contributor.commits;
92+
busFactor++;
93+
if (cumulative >= totalCommits * 0.8) break;
94+
}
95+
96+
// AI ratio
97+
const aiTotal = (aiAttribution.attributed || 0) + (aiAttribution.heuristic || 0);
98+
const allCommits = map.git?.totalCommitsAnalyzed || totalCommits;
99+
const aiRatio = allCommits > 0 ? aiTotal / allCommits : 0;
100+
101+
return {
102+
available: true,
103+
health: {
104+
active: humanList.length > 0,
105+
busFactor,
106+
aiRatio: Math.round(aiRatio * 100) / 100,
107+
totalCommits: allCommits,
108+
totalContributors: humanList.length
109+
},
110+
hotspots,
111+
contributors: humanList.slice(0, 10),
112+
aiAttribution: {
113+
ratio: Math.round(aiRatio * 100) / 100,
114+
attributed: aiAttribution.attributed || 0,
115+
heuristic: aiAttribution.heuristic || 0,
116+
none: aiAttribution.none || 0,
117+
tools: aiAttribution.tools || {}
118+
},
119+
busFactor,
120+
conventions: {
121+
style: conventions.style || null,
122+
prefixes: conventions.prefixes || {},
123+
scopes: conventions.scopes || {}
124+
},
125+
releaseInfo: {
126+
tagCount: releases.tags ? releases.tags.length : 0,
127+
lastRelease: releases.tags && releases.tags.length > 0
128+
? releases.tags[releases.tags.length - 1]
129+
: null,
130+
cadence: releases.cadence || null
131+
},
132+
commitShape: {
133+
typicalSize: commitShape.typicalSize || null,
134+
filesPerCommit: commitShape.filesPerCommit || null,
135+
mergeCount: commitShape.mergeCount || 0
136+
}
137+
};
138+
}
139+
140+
module.exports = {
141+
collectGitData,
142+
DEFAULT_OPTIONS
143+
};

lib/collectors/index.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const github = require('./github');
1313
const documentation = require('./documentation');
1414
const codebase = require('./codebase');
1515
const docsPatterns = require('./docs-patterns');
16+
const git = require('./git');
1617

1718
const DEFAULT_OPTIONS = {
1819
collectors: ['github', 'docs', 'code'],
@@ -50,7 +51,8 @@ function collect(options = {}) {
5051
github: null,
5152
docs: null,
5253
code: null,
53-
docsPatterns: null
54+
docsPatterns: null,
55+
git: null
5456
};
5557

5658
// Collect from each enabled collector
@@ -70,6 +72,10 @@ function collect(options = {}) {
7072
data.docsPatterns = docsPatterns.collect(opts);
7173
}
7274

75+
if (collectors.includes('git')) {
76+
data.git = git.collectGitData(opts);
77+
}
78+
7379
return data;
7480
}
7581

@@ -103,6 +109,7 @@ module.exports = {
103109
documentation,
104110
codebase,
105111
docsPatterns,
112+
git,
106113

107114
// Re-export commonly used functions for convenience
108115
scanGitHubState: github.scanGitHubState,
@@ -121,6 +128,8 @@ module.exports = {
121128
isInternalExport: docsPatterns.isInternalExport,
122129
isEntryPoint: docsPatterns.isEntryPoint,
123130

131+
collectGitData: git.collectGitData,
132+
124133
// Constants
125134
DEFAULT_OPTIONS
126135
};

0 commit comments

Comments
 (0)