gitfamiliar 0.10.0 → 0.11.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 +48 -35
- package/dist/bin/gitfamiliar.js +1052 -659
- package/dist/bin/gitfamiliar.js.map +1 -1
- package/dist/{chunk-6XUJIHN3.js → chunk-3SYLST7V.js} +9 -9
- package/dist/chunk-3SYLST7V.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-6XUJIHN3.js.map +0 -1
package/dist/bin/gitfamiliar.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
processBatch,
|
|
9
9
|
resolveUser,
|
|
10
10
|
walkFiles
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-3SYLST7V.js";
|
|
12
12
|
|
|
13
13
|
// src/cli/index.ts
|
|
14
14
|
import { createRequire } from "module";
|
|
@@ -22,6 +22,19 @@ var DEFAULT_WEIGHTS = {
|
|
|
22
22
|
var DEFAULT_EXPIRATION = {
|
|
23
23
|
policy: "never"
|
|
24
24
|
};
|
|
25
|
+
var HOTSPOT_RISK_THRESHOLDS = {
|
|
26
|
+
critical: 0.6,
|
|
27
|
+
high: 0.4,
|
|
28
|
+
medium: 0.2
|
|
29
|
+
};
|
|
30
|
+
var COVERAGE_RISK_THRESHOLDS = {
|
|
31
|
+
risk: 1,
|
|
32
|
+
// <= 1 contributor
|
|
33
|
+
moderate: 3
|
|
34
|
+
// <= 3 contributors
|
|
35
|
+
};
|
|
36
|
+
var DEFAULT_HOTSPOT_WINDOW = 90;
|
|
37
|
+
var BUS_FACTOR_TARGET = 0.5;
|
|
25
38
|
|
|
26
39
|
// src/cli/options.ts
|
|
27
40
|
function parseOptions(raw, repoPath) {
|
|
@@ -57,7 +70,8 @@ function parseOptions(raw, repoPath) {
|
|
|
57
70
|
team: raw.team || false,
|
|
58
71
|
contributorsPerFile: raw.contributorsPerFile || raw.contributors || raw.teamCoverage || false,
|
|
59
72
|
hotspot,
|
|
60
|
-
since: sinceDays
|
|
73
|
+
since: sinceDays,
|
|
74
|
+
demo: raw.demo || false
|
|
61
75
|
};
|
|
62
76
|
}
|
|
63
77
|
var MODE_ALIASES = {
|
|
@@ -89,13 +103,15 @@ function parseWeights(s) {
|
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
// src/cli/output/terminal.ts
|
|
106
|
+
import chalk2 from "chalk";
|
|
107
|
+
|
|
108
|
+
// src/cli/output/terminal-utils.ts
|
|
92
109
|
import chalk from "chalk";
|
|
93
|
-
var BAR_WIDTH = 10;
|
|
94
110
|
var FILLED_CHAR = "\u2588";
|
|
95
111
|
var EMPTY_CHAR = "\u2591";
|
|
96
|
-
function makeBar(score) {
|
|
97
|
-
const filled = Math.round(score *
|
|
98
|
-
const empty =
|
|
112
|
+
function makeBar(score, width = 10) {
|
|
113
|
+
const filled = Math.round(score * width);
|
|
114
|
+
const empty = width - filled;
|
|
99
115
|
const bar = FILLED_CHAR.repeat(filled) + EMPTY_CHAR.repeat(empty);
|
|
100
116
|
if (score >= 0.8) return chalk.green(bar);
|
|
101
117
|
if (score >= 0.5) return chalk.yellow(bar);
|
|
@@ -117,6 +133,8 @@ function getModeLabel(mode) {
|
|
|
117
133
|
return mode;
|
|
118
134
|
}
|
|
119
135
|
}
|
|
136
|
+
|
|
137
|
+
// src/cli/output/terminal.ts
|
|
120
138
|
var NAME_COLUMN_WIDTH = 24;
|
|
121
139
|
function renderFolder(node, indent, mode, maxDepth) {
|
|
122
140
|
const lines = [];
|
|
@@ -140,11 +158,11 @@ function renderFolder(node, indent, mode, maxDepth) {
|
|
|
140
158
|
if (mode === "committed") {
|
|
141
159
|
const readCount = folder.readCount || 0;
|
|
142
160
|
lines.push(
|
|
143
|
-
`${prefix}${
|
|
161
|
+
`${prefix}${chalk2.bold(name)}${padding} ${bar} ${pct.padStart(4)} (${readCount}/${folder.fileCount} files)`
|
|
144
162
|
);
|
|
145
163
|
} else {
|
|
146
164
|
lines.push(
|
|
147
|
-
`${prefix}${
|
|
165
|
+
`${prefix}${chalk2.bold(name)}${padding} ${bar} ${pct.padStart(4)}`
|
|
148
166
|
);
|
|
149
167
|
}
|
|
150
168
|
if (indent < maxDepth) {
|
|
@@ -158,7 +176,7 @@ function renderTerminal(result) {
|
|
|
158
176
|
const { tree, repoName, mode } = result;
|
|
159
177
|
console.log("");
|
|
160
178
|
console.log(
|
|
161
|
-
|
|
179
|
+
chalk2.bold(`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)})`)
|
|
162
180
|
);
|
|
163
181
|
console.log("");
|
|
164
182
|
if (mode === "committed") {
|
|
@@ -197,18 +215,9 @@ async function openBrowser(filePath) {
|
|
|
197
215
|
}
|
|
198
216
|
}
|
|
199
217
|
|
|
200
|
-
// src/cli/output/html.ts
|
|
201
|
-
function
|
|
202
|
-
|
|
203
|
-
const mode = result.mode;
|
|
204
|
-
const repoName = result.repoName;
|
|
205
|
-
return `<!DOCTYPE html>
|
|
206
|
-
<html lang="en">
|
|
207
|
-
<head>
|
|
208
|
-
<meta charset="UTF-8">
|
|
209
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
210
|
-
<title>GitFamiliar \u2014 ${repoName}</title>
|
|
211
|
-
<style>
|
|
218
|
+
// src/cli/output/html-shared.ts
|
|
219
|
+
function getBaseStyles() {
|
|
220
|
+
return `
|
|
212
221
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
213
222
|
body {
|
|
214
223
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
@@ -226,17 +235,6 @@ function generateTreemapHTML(result) {
|
|
|
226
235
|
}
|
|
227
236
|
#header h1 { font-size: 18px; color: #e94560; }
|
|
228
237
|
#header .info { font-size: 14px; color: #a0a0a0; }
|
|
229
|
-
#breadcrumb {
|
|
230
|
-
padding: 8px 24px;
|
|
231
|
-
background: #16213e;
|
|
232
|
-
font-size: 13px;
|
|
233
|
-
border-bottom: 1px solid #0f3460;
|
|
234
|
-
}
|
|
235
|
-
#breadcrumb span { cursor: pointer; color: #5eadf7; }
|
|
236
|
-
#breadcrumb span:hover { text-decoration: underline; }
|
|
237
|
-
#breadcrumb .sep { color: #666; margin: 0 4px; }
|
|
238
|
-
|
|
239
|
-
#treemap { width: 100%; }
|
|
240
238
|
#tooltip {
|
|
241
239
|
position: absolute;
|
|
242
240
|
pointer-events: none;
|
|
@@ -248,8 +246,23 @@ function generateTreemapHTML(result) {
|
|
|
248
246
|
line-height: 1.6;
|
|
249
247
|
display: none;
|
|
250
248
|
z-index: 100;
|
|
251
|
-
max-width:
|
|
249
|
+
max-width: 350px;
|
|
250
|
+
}`;
|
|
251
|
+
}
|
|
252
|
+
function getBreadcrumbStyles() {
|
|
253
|
+
return `
|
|
254
|
+
#breadcrumb {
|
|
255
|
+
padding: 8px 24px;
|
|
256
|
+
background: #16213e;
|
|
257
|
+
font-size: 13px;
|
|
258
|
+
border-bottom: 1px solid #0f3460;
|
|
252
259
|
}
|
|
260
|
+
#breadcrumb span { cursor: pointer; color: #5eadf7; }
|
|
261
|
+
#breadcrumb span:hover { text-decoration: underline; }
|
|
262
|
+
#breadcrumb .sep { color: #666; margin: 0 4px; }`;
|
|
263
|
+
}
|
|
264
|
+
function getGradientLegendStyles() {
|
|
265
|
+
return `
|
|
253
266
|
#legend {
|
|
254
267
|
position: absolute;
|
|
255
268
|
bottom: 16px;
|
|
@@ -267,45 +280,22 @@ function generateTreemapHTML(result) {
|
|
|
267
280
|
border-radius: 3px;
|
|
268
281
|
margin: 4px 0;
|
|
269
282
|
}
|
|
270
|
-
#legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }
|
|
271
|
-
</style>
|
|
272
|
-
</head>
|
|
273
|
-
<body>
|
|
274
|
-
<div id="header">
|
|
275
|
-
<h1>GitFamiliar \u2014 ${repoName}</h1>
|
|
276
|
-
<div class="info">${mode.charAt(0).toUpperCase() + mode.slice(1)} mode | ${result.totalFiles} files</div>
|
|
277
|
-
</div>
|
|
278
|
-
<div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
|
|
279
|
-
|
|
280
|
-
<div id="treemap"></div>
|
|
281
|
-
<div id="tooltip"></div>
|
|
282
|
-
<div id="legend">
|
|
283
|
-
<div>Familiarity</div>
|
|
284
|
-
<div class="gradient-bar"></div>
|
|
285
|
-
<div class="labels"><span>0%</span><span>50%</span><span>100%</span></div>
|
|
286
|
-
</div>
|
|
287
|
-
|
|
288
|
-
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
289
|
-
<script>
|
|
290
|
-
const rawData = ${dataJson};
|
|
291
|
-
const mode = "${mode}";
|
|
292
|
-
let currentPath = '';
|
|
293
|
-
|
|
294
|
-
function scoreColor(score) {
|
|
295
|
-
if (score <= 0) return '#e94560';
|
|
296
|
-
if (score >= 1) return '#27ae60';
|
|
297
|
-
if (score < 0.5) {
|
|
298
|
-
const t = score / 0.5;
|
|
299
|
-
return d3.interpolateRgb('#e94560', '#f5a623')(t);
|
|
300
|
-
}
|
|
301
|
-
const t = (score - 0.5) / 0.5;
|
|
302
|
-
return d3.interpolateRgb('#f5a623', '#27ae60')(t);
|
|
283
|
+
#legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }`;
|
|
303
284
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
285
|
+
function getSidebarStyles() {
|
|
286
|
+
return `
|
|
287
|
+
#main { display: flex; height: calc(100vh - 90px); }
|
|
288
|
+
#sidebar {
|
|
289
|
+
width: 300px;
|
|
290
|
+
background: #16213e;
|
|
291
|
+
border-left: 1px solid #0f3460;
|
|
292
|
+
overflow-y: auto;
|
|
293
|
+
padding: 16px;
|
|
294
|
+
}
|
|
295
|
+
#sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }`;
|
|
307
296
|
}
|
|
308
|
-
|
|
297
|
+
function getTreemapUtilsScript() {
|
|
298
|
+
return `
|
|
309
299
|
function findNode(node, path) {
|
|
310
300
|
if (node.path === path) return node;
|
|
311
301
|
if (node.children) {
|
|
@@ -317,14 +307,6 @@ function findNode(node, path) {
|
|
|
317
307
|
return null;
|
|
318
308
|
}
|
|
319
309
|
|
|
320
|
-
function totalLines(node) {
|
|
321
|
-
if (node.type === 'file') return Math.max(1, node.lines);
|
|
322
|
-
if (!node.children) return 1;
|
|
323
|
-
let sum = 0;
|
|
324
|
-
for (const c of node.children) sum += totalLines(c);
|
|
325
|
-
return Math.max(1, sum);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
310
|
function buildHierarchy(node) {
|
|
329
311
|
if (node.type === 'file') {
|
|
330
312
|
return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };
|
|
@@ -336,6 +318,84 @@ function buildHierarchy(node) {
|
|
|
336
318
|
};
|
|
337
319
|
}
|
|
338
320
|
|
|
321
|
+
function truncateLabel(name, w, h) {
|
|
322
|
+
if (w < 36 || h < 18) return '';
|
|
323
|
+
const maxChars = Math.floor((w - 8) / 6.5);
|
|
324
|
+
if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\u2026';
|
|
325
|
+
return name;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function zoomTo(path) {
|
|
329
|
+
currentPath = path;
|
|
330
|
+
const el = document.getElementById('breadcrumb');
|
|
331
|
+
const parts = path ? path.split('/') : [];
|
|
332
|
+
let html = '<span onclick="zoomTo(\\'\\')">root</span>';
|
|
333
|
+
let accumulated = '';
|
|
334
|
+
for (const part of parts) {
|
|
335
|
+
accumulated = accumulated ? accumulated + '/' + part : part;
|
|
336
|
+
const p = accumulated;
|
|
337
|
+
html += '<span class="sep">/</span><span onclick="zoomTo(\\'' + p + '\\')">' + part + '</span>';
|
|
338
|
+
}
|
|
339
|
+
el.innerHTML = html;
|
|
340
|
+
render();
|
|
341
|
+
}`;
|
|
342
|
+
}
|
|
343
|
+
function getScoreColorScript() {
|
|
344
|
+
return `
|
|
345
|
+
function scoreColor(score) {
|
|
346
|
+
if (score <= 0) return '#e94560';
|
|
347
|
+
if (score >= 1) return '#27ae60';
|
|
348
|
+
if (score < 0.5) {
|
|
349
|
+
const t = score / 0.5;
|
|
350
|
+
return d3.interpolateRgb('#e94560', '#f5a623')(t);
|
|
351
|
+
}
|
|
352
|
+
const t = (score - 0.5) / 0.5;
|
|
353
|
+
return d3.interpolateRgb('#f5a623', '#27ae60')(t);
|
|
354
|
+
}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/cli/output/html.ts
|
|
358
|
+
function generateTreemapHTML(result) {
|
|
359
|
+
const dataJson = JSON.stringify(result.tree);
|
|
360
|
+
const mode = result.mode;
|
|
361
|
+
const repoName = result.repoName;
|
|
362
|
+
return `<!DOCTYPE html>
|
|
363
|
+
<html lang="en">
|
|
364
|
+
<head>
|
|
365
|
+
<meta charset="UTF-8">
|
|
366
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
367
|
+
<title>GitFamiliar \u2014 ${repoName}</title>
|
|
368
|
+
<style>
|
|
369
|
+
${getBaseStyles()}
|
|
370
|
+
${getBreadcrumbStyles()}
|
|
371
|
+
#treemap { width: 100%; }
|
|
372
|
+
${getGradientLegendStyles()}
|
|
373
|
+
</style>
|
|
374
|
+
</head>
|
|
375
|
+
<body>
|
|
376
|
+
<div id="header">
|
|
377
|
+
<h1>GitFamiliar \u2014 ${repoName}</h1>
|
|
378
|
+
<div class="info">${mode.charAt(0).toUpperCase() + mode.slice(1)} mode | ${result.totalFiles} files</div>
|
|
379
|
+
</div>
|
|
380
|
+
<div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
|
|
381
|
+
|
|
382
|
+
<div id="treemap"></div>
|
|
383
|
+
<div id="tooltip"></div>
|
|
384
|
+
<div id="legend">
|
|
385
|
+
<div>Familiarity</div>
|
|
386
|
+
<div class="gradient-bar"></div>
|
|
387
|
+
<div class="labels"><span>0%</span><span>50%</span><span>100%</span></div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
391
|
+
<script>
|
|
392
|
+
const rawData = ${dataJson};
|
|
393
|
+
const mode = "${mode}";
|
|
394
|
+
let currentPath = '';
|
|
395
|
+
|
|
396
|
+
${getScoreColorScript()}
|
|
397
|
+
${getTreemapUtilsScript()}
|
|
398
|
+
|
|
339
399
|
function render() {
|
|
340
400
|
const container = document.getElementById('treemap');
|
|
341
401
|
container.innerHTML = '';
|
|
@@ -351,7 +411,6 @@ function render() {
|
|
|
351
411
|
const children = targetNode.children || [];
|
|
352
412
|
if (children.length === 0) return;
|
|
353
413
|
|
|
354
|
-
// Build full nested hierarchy from the current target
|
|
355
414
|
const hierarchyData = {
|
|
356
415
|
name: targetNode.path || 'root',
|
|
357
416
|
children: children.map(c => buildHierarchy(c)),
|
|
@@ -381,13 +440,12 @@ function render() {
|
|
|
381
440
|
.join('g')
|
|
382
441
|
.attr('transform', d => \`translate(\${d.x0},\${d.y0})\`);
|
|
383
442
|
|
|
384
|
-
// Rect
|
|
385
443
|
groups.append('rect')
|
|
386
444
|
.attr('width', d => Math.max(0, d.x1 - d.x0))
|
|
387
445
|
.attr('height', d => Math.max(0, d.y1 - d.y0))
|
|
388
446
|
.attr('fill', d => {
|
|
389
447
|
if (!d.data.data) return '#333';
|
|
390
|
-
return scoreColor(
|
|
448
|
+
return scoreColor(d.data.data.score);
|
|
391
449
|
})
|
|
392
450
|
.attr('opacity', d => d.children ? 0.35 : 0.88)
|
|
393
451
|
.attr('stroke', '#1a1a2e')
|
|
@@ -414,7 +472,6 @@ function render() {
|
|
|
414
472
|
tooltip.style.display = 'none';
|
|
415
473
|
});
|
|
416
474
|
|
|
417
|
-
// Labels
|
|
418
475
|
groups.append('text')
|
|
419
476
|
.attr('x', 4)
|
|
420
477
|
.attr('y', 14)
|
|
@@ -422,21 +479,13 @@ function render() {
|
|
|
422
479
|
.attr('font-size', d => d.children ? '11px' : '10px')
|
|
423
480
|
.attr('font-weight', d => d.children ? 'bold' : 'normal')
|
|
424
481
|
.style('pointer-events', 'none')
|
|
425
|
-
.text(d =>
|
|
426
|
-
const w = d.x1 - d.x0;
|
|
427
|
-
const h = d.y1 - d.y0;
|
|
428
|
-
const name = d.data.name || '';
|
|
429
|
-
if (w < 36 || h < 18) return '';
|
|
430
|
-
const maxChars = Math.floor((w - 8) / 6.5);
|
|
431
|
-
if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\u2026';
|
|
432
|
-
return name;
|
|
433
|
-
});
|
|
482
|
+
.text(d => truncateLabel(d.data.name || '', d.x1 - d.x0, d.y1 - d.y0));
|
|
434
483
|
}
|
|
435
484
|
|
|
436
485
|
function showTooltip(data, event) {
|
|
437
486
|
const tooltip = document.getElementById('tooltip');
|
|
438
487
|
const name = data.path || '';
|
|
439
|
-
const score =
|
|
488
|
+
const score = data.score;
|
|
440
489
|
let html = '<strong>' + name + '</strong>';
|
|
441
490
|
html += '<br>Score: ' + Math.round(score * 100) + '%';
|
|
442
491
|
html += '<br>Lines: ' + data.lines.toLocaleString();
|
|
@@ -450,7 +499,6 @@ function showTooltip(data, event) {
|
|
|
450
499
|
if (data.commitScore !== undefined) {
|
|
451
500
|
html += '<br>Commit: ' + Math.round(data.commitScore * 100) + '%';
|
|
452
501
|
}
|
|
453
|
-
|
|
454
502
|
if (data.isExpired) {
|
|
455
503
|
html += '<br><span style="color:#e94560">Expired</span>';
|
|
456
504
|
}
|
|
@@ -460,25 +508,6 @@ function showTooltip(data, event) {
|
|
|
460
508
|
tooltip.style.top = (event.pageY - 14) + 'px';
|
|
461
509
|
}
|
|
462
510
|
|
|
463
|
-
function zoomTo(path) {
|
|
464
|
-
currentPath = path;
|
|
465
|
-
updateBreadcrumb();
|
|
466
|
-
render();
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function updateBreadcrumb() {
|
|
470
|
-
const el = document.getElementById('breadcrumb');
|
|
471
|
-
const parts = currentPath ? currentPath.split('/') : [];
|
|
472
|
-
let html = '<span onclick="zoomTo(\\'\\')">root</span>';
|
|
473
|
-
let accumulated = '';
|
|
474
|
-
for (const part of parts) {
|
|
475
|
-
accumulated = accumulated ? accumulated + '/' + part : part;
|
|
476
|
-
const p = accumulated;
|
|
477
|
-
html += \`<span class="sep">/</span><span onclick="zoomTo('\${p}')">\${part}</span>\`;
|
|
478
|
-
}
|
|
479
|
-
el.innerHTML = html;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
511
|
window.addEventListener('resize', render);
|
|
483
512
|
render();
|
|
484
513
|
</script>
|
|
@@ -493,6 +522,19 @@ async function generateAndOpenHTML(result, repoPath) {
|
|
|
493
522
|
await openBrowser(outputPath);
|
|
494
523
|
}
|
|
495
524
|
|
|
525
|
+
// src/core/risk.ts
|
|
526
|
+
function classifyHotspotRisk(risk) {
|
|
527
|
+
if (risk >= HOTSPOT_RISK_THRESHOLDS.critical) return "critical";
|
|
528
|
+
if (risk >= HOTSPOT_RISK_THRESHOLDS.high) return "high";
|
|
529
|
+
if (risk >= HOTSPOT_RISK_THRESHOLDS.medium) return "medium";
|
|
530
|
+
return "low";
|
|
531
|
+
}
|
|
532
|
+
function classifyCoverageRisk(contributorCount) {
|
|
533
|
+
if (contributorCount <= COVERAGE_RISK_THRESHOLDS.risk) return "risk";
|
|
534
|
+
if (contributorCount <= COVERAGE_RISK_THRESHOLDS.moderate) return "moderate";
|
|
535
|
+
return "safe";
|
|
536
|
+
}
|
|
537
|
+
|
|
496
538
|
// src/git/contributors.ts
|
|
497
539
|
var COMMIT_SEP = "GITFAMILIAR_SEP";
|
|
498
540
|
async function getAllContributors(gitClient, minCommits = 1) {
|
|
@@ -554,7 +596,10 @@ async function computeTeamCoverage(options) {
|
|
|
554
596
|
const tree = await buildFileTree(gitClient, filter);
|
|
555
597
|
const trackedFiles = /* @__PURE__ */ new Set();
|
|
556
598
|
walkFiles(tree, (f) => trackedFiles.add(f.path));
|
|
557
|
-
const fileContributors = await bulkGetFileContributors(
|
|
599
|
+
const fileContributors = await bulkGetFileContributors(
|
|
600
|
+
gitClient,
|
|
601
|
+
trackedFiles
|
|
602
|
+
);
|
|
558
603
|
const allContributors = await getAllContributors(gitClient);
|
|
559
604
|
const coverageTree = buildCoverageTree(tree, fileContributors);
|
|
560
605
|
const riskFiles = [];
|
|
@@ -573,11 +618,6 @@ async function computeTeamCoverage(options) {
|
|
|
573
618
|
overallBusFactor: calculateBusFactor(fileContributors)
|
|
574
619
|
};
|
|
575
620
|
}
|
|
576
|
-
function classifyRisk(contributorCount) {
|
|
577
|
-
if (contributorCount <= 1) return "risk";
|
|
578
|
-
if (contributorCount <= 3) return "moderate";
|
|
579
|
-
return "safe";
|
|
580
|
-
}
|
|
581
621
|
function buildCoverageTree(node, fileContributors) {
|
|
582
622
|
const children = [];
|
|
583
623
|
for (const child of node.children) {
|
|
@@ -590,17 +630,32 @@ function buildCoverageTree(node, fileContributors) {
|
|
|
590
630
|
lines: child.lines,
|
|
591
631
|
contributorCount: names.length,
|
|
592
632
|
contributors: names,
|
|
593
|
-
riskLevel:
|
|
633
|
+
riskLevel: classifyCoverageRisk(names.length)
|
|
594
634
|
});
|
|
595
635
|
} else {
|
|
596
636
|
children.push(buildCoverageTree(child, fileContributors));
|
|
597
637
|
}
|
|
598
638
|
}
|
|
599
639
|
const fileScores = [];
|
|
600
|
-
walkCoverageFiles(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
640
|
+
walkCoverageFiles(
|
|
641
|
+
{
|
|
642
|
+
type: "folder",
|
|
643
|
+
path: "",
|
|
644
|
+
lines: 0,
|
|
645
|
+
fileCount: 0,
|
|
646
|
+
avgContributors: 0,
|
|
647
|
+
busFactor: 0,
|
|
648
|
+
riskLevel: "safe",
|
|
649
|
+
children
|
|
650
|
+
},
|
|
651
|
+
(f) => {
|
|
652
|
+
fileScores.push(f);
|
|
653
|
+
}
|
|
654
|
+
);
|
|
655
|
+
const totalContributors = fileScores.reduce(
|
|
656
|
+
(sum, f) => sum + f.contributorCount,
|
|
657
|
+
0
|
|
658
|
+
);
|
|
604
659
|
const avgContributors = fileScores.length > 0 ? totalContributors / fileScores.length : 0;
|
|
605
660
|
const folderFileContributors = /* @__PURE__ */ new Map();
|
|
606
661
|
for (const f of fileScores) {
|
|
@@ -614,7 +669,7 @@ function buildCoverageTree(node, fileContributors) {
|
|
|
614
669
|
fileCount: node.fileCount,
|
|
615
670
|
avgContributors: Math.round(avgContributors * 10) / 10,
|
|
616
671
|
busFactor,
|
|
617
|
-
riskLevel:
|
|
672
|
+
riskLevel: classifyCoverageRisk(busFactor),
|
|
618
673
|
children
|
|
619
674
|
};
|
|
620
675
|
}
|
|
@@ -630,7 +685,7 @@ function walkCoverageFiles(node, visitor) {
|
|
|
630
685
|
function calculateBusFactor(fileContributors) {
|
|
631
686
|
const totalFiles = fileContributors.size;
|
|
632
687
|
if (totalFiles === 0) return 0;
|
|
633
|
-
const target = Math.ceil(totalFiles *
|
|
688
|
+
const target = Math.ceil(totalFiles * BUS_FACTOR_TARGET);
|
|
634
689
|
const contributorFiles = /* @__PURE__ */ new Map();
|
|
635
690
|
for (const [file, contributors] of fileContributors) {
|
|
636
691
|
for (const contributor of contributors) {
|
|
@@ -669,15 +724,15 @@ function calculateBusFactor(fileContributors) {
|
|
|
669
724
|
}
|
|
670
725
|
|
|
671
726
|
// src/cli/output/coverage-terminal.ts
|
|
672
|
-
import
|
|
727
|
+
import chalk3 from "chalk";
|
|
673
728
|
function riskBadge(level) {
|
|
674
729
|
switch (level) {
|
|
675
730
|
case "risk":
|
|
676
|
-
return
|
|
731
|
+
return chalk3.bgRed.white(" RISK ");
|
|
677
732
|
case "moderate":
|
|
678
|
-
return
|
|
733
|
+
return chalk3.bgYellow.black(" MOD ");
|
|
679
734
|
case "safe":
|
|
680
|
-
return
|
|
735
|
+
return chalk3.bgGreen.black(" SAFE ");
|
|
681
736
|
default:
|
|
682
737
|
return level;
|
|
683
738
|
}
|
|
@@ -685,11 +740,11 @@ function riskBadge(level) {
|
|
|
685
740
|
function riskColor(level) {
|
|
686
741
|
switch (level) {
|
|
687
742
|
case "risk":
|
|
688
|
-
return
|
|
743
|
+
return chalk3.red;
|
|
689
744
|
case "moderate":
|
|
690
|
-
return
|
|
745
|
+
return chalk3.yellow;
|
|
691
746
|
default:
|
|
692
|
-
return
|
|
747
|
+
return chalk3.green;
|
|
693
748
|
}
|
|
694
749
|
}
|
|
695
750
|
function renderFolder2(node, indent, maxDepth) {
|
|
@@ -704,7 +759,7 @@ function renderFolder2(node, indent, maxDepth) {
|
|
|
704
759
|
const name = (child.path.split("/").pop() || child.path) + "/";
|
|
705
760
|
const color = riskColor(child.riskLevel);
|
|
706
761
|
lines.push(
|
|
707
|
-
`${prefix}${
|
|
762
|
+
`${prefix}${chalk3.bold(name.padEnd(24))} ${String(child.avgContributors).padStart(4)} avg ${String(child.busFactor).padStart(2)} ${riskBadge(child.riskLevel)}`
|
|
708
763
|
);
|
|
709
764
|
if (indent < maxDepth) {
|
|
710
765
|
lines.push(...renderFolder2(child, indent + 1, maxDepth));
|
|
@@ -716,36 +771,36 @@ function renderFolder2(node, indent, maxDepth) {
|
|
|
716
771
|
function renderCoverageTerminal(result) {
|
|
717
772
|
console.log("");
|
|
718
773
|
console.log(
|
|
719
|
-
|
|
774
|
+
chalk3.bold(
|
|
720
775
|
`GitFamiliar \u2014 Team Coverage (${result.totalFiles} files, ${result.totalContributors} contributors)`
|
|
721
776
|
)
|
|
722
777
|
);
|
|
723
778
|
console.log("");
|
|
724
|
-
const bfColor = result.overallBusFactor <= 1 ?
|
|
779
|
+
const bfColor = result.overallBusFactor <= 1 ? chalk3.red : result.overallBusFactor <= 2 ? chalk3.yellow : chalk3.green;
|
|
725
780
|
console.log(`Overall Bus Factor: ${bfColor.bold(String(result.overallBusFactor))}`);
|
|
726
781
|
console.log("");
|
|
727
782
|
if (result.riskFiles.length > 0) {
|
|
728
|
-
console.log(
|
|
783
|
+
console.log(chalk3.red.bold(`Risk Files (0-1 contributors):`));
|
|
729
784
|
const displayFiles = result.riskFiles.slice(0, 20);
|
|
730
785
|
for (const file of displayFiles) {
|
|
731
786
|
const count = file.contributorCount;
|
|
732
787
|
const names = file.contributors.join(", ");
|
|
733
|
-
const label = count === 0 ?
|
|
788
|
+
const label = count === 0 ? chalk3.red("0 people") : chalk3.yellow(`1 person (${names})`);
|
|
734
789
|
console.log(` ${file.path.padEnd(40)} ${label}`);
|
|
735
790
|
}
|
|
736
791
|
if (result.riskFiles.length > 20) {
|
|
737
792
|
console.log(
|
|
738
|
-
|
|
793
|
+
chalk3.gray(` ... and ${result.riskFiles.length - 20} more`)
|
|
739
794
|
);
|
|
740
795
|
}
|
|
741
796
|
console.log("");
|
|
742
797
|
} else {
|
|
743
|
-
console.log(
|
|
798
|
+
console.log(chalk3.green("No high-risk files found."));
|
|
744
799
|
console.log("");
|
|
745
800
|
}
|
|
746
|
-
console.log(
|
|
801
|
+
console.log(chalk3.bold("Folder Coverage:"));
|
|
747
802
|
console.log(
|
|
748
|
-
|
|
803
|
+
chalk3.gray(
|
|
749
804
|
` ${"Folder".padEnd(24)} ${"Avg Contrib".padStart(11)} ${"Bus Factor".padStart(10)} Risk`
|
|
750
805
|
)
|
|
751
806
|
);
|
|
@@ -769,42 +824,9 @@ function generateCoverageHTML(result) {
|
|
|
769
824
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
770
825
|
<title>GitFamiliar \u2014 Team Coverage \u2014 ${result.repoName}</title>
|
|
771
826
|
<style>
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
background: #1a1a2e;
|
|
776
|
-
color: #e0e0e0;
|
|
777
|
-
overflow: hidden;
|
|
778
|
-
}
|
|
779
|
-
#header {
|
|
780
|
-
padding: 16px 24px;
|
|
781
|
-
background: #16213e;
|
|
782
|
-
border-bottom: 1px solid #0f3460;
|
|
783
|
-
display: flex;
|
|
784
|
-
align-items: center;
|
|
785
|
-
justify-content: space-between;
|
|
786
|
-
}
|
|
787
|
-
#header h1 { font-size: 18px; color: #e94560; }
|
|
788
|
-
#header .info { font-size: 14px; color: #a0a0a0; }
|
|
789
|
-
#breadcrumb {
|
|
790
|
-
padding: 8px 24px;
|
|
791
|
-
background: #16213e;
|
|
792
|
-
font-size: 13px;
|
|
793
|
-
border-bottom: 1px solid #0f3460;
|
|
794
|
-
}
|
|
795
|
-
#breadcrumb span { cursor: pointer; color: #5eadf7; }
|
|
796
|
-
#breadcrumb span:hover { text-decoration: underline; }
|
|
797
|
-
#breadcrumb .sep { color: #666; margin: 0 4px; }
|
|
798
|
-
#main { display: flex; height: calc(100vh - 90px); }
|
|
799
|
-
#treemap { flex: 1; }
|
|
800
|
-
#sidebar {
|
|
801
|
-
width: 300px;
|
|
802
|
-
background: #16213e;
|
|
803
|
-
border-left: 1px solid #0f3460;
|
|
804
|
-
overflow-y: auto;
|
|
805
|
-
padding: 16px;
|
|
806
|
-
}
|
|
807
|
-
#sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }
|
|
827
|
+
${getBaseStyles()}
|
|
828
|
+
${getBreadcrumbStyles()}
|
|
829
|
+
${getSidebarStyles()}
|
|
808
830
|
#sidebar .risk-file {
|
|
809
831
|
padding: 6px 0;
|
|
810
832
|
border-bottom: 1px solid #0f3460;
|
|
@@ -812,19 +834,7 @@ function generateCoverageHTML(result) {
|
|
|
812
834
|
}
|
|
813
835
|
#sidebar .risk-file .path { color: #e0e0e0; word-break: break-all; }
|
|
814
836
|
#sidebar .risk-file .meta { color: #888; margin-top: 2px; }
|
|
815
|
-
#
|
|
816
|
-
position: absolute;
|
|
817
|
-
pointer-events: none;
|
|
818
|
-
background: rgba(22, 33, 62, 0.95);
|
|
819
|
-
border: 1px solid #0f3460;
|
|
820
|
-
border-radius: 6px;
|
|
821
|
-
padding: 10px 14px;
|
|
822
|
-
font-size: 13px;
|
|
823
|
-
line-height: 1.6;
|
|
824
|
-
display: none;
|
|
825
|
-
z-index: 100;
|
|
826
|
-
max-width: 320px;
|
|
827
|
-
}
|
|
837
|
+
#treemap { flex: 1; }
|
|
828
838
|
#legend {
|
|
829
839
|
position: absolute;
|
|
830
840
|
bottom: 16px;
|
|
@@ -881,27 +891,7 @@ function folderColor(riskLevel) {
|
|
|
881
891
|
}
|
|
882
892
|
}
|
|
883
893
|
|
|
884
|
-
|
|
885
|
-
if (node.path === path) return node;
|
|
886
|
-
if (node.children) {
|
|
887
|
-
for (const child of node.children) {
|
|
888
|
-
const found = findNode(child, path);
|
|
889
|
-
if (found) return found;
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
return null;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function buildHierarchy(node) {
|
|
896
|
-
if (node.type === 'file') {
|
|
897
|
-
return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };
|
|
898
|
-
}
|
|
899
|
-
return {
|
|
900
|
-
name: node.path.split('/').pop() || node.path,
|
|
901
|
-
data: node,
|
|
902
|
-
children: (node.children || []).map(c => buildHierarchy(c)),
|
|
903
|
-
};
|
|
904
|
-
}
|
|
894
|
+
${getTreemapUtilsScript()}
|
|
905
895
|
|
|
906
896
|
function render() {
|
|
907
897
|
const container = document.getElementById('treemap');
|
|
@@ -983,15 +973,7 @@ function render() {
|
|
|
983
973
|
.attr('font-size', d => d.children ? '11px' : '10px')
|
|
984
974
|
.attr('font-weight', d => d.children ? 'bold' : 'normal')
|
|
985
975
|
.style('pointer-events', 'none')
|
|
986
|
-
.text(d =>
|
|
987
|
-
const w = d.x1 - d.x0;
|
|
988
|
-
const h = d.y1 - d.y0;
|
|
989
|
-
const name = d.data.name || '';
|
|
990
|
-
if (w < 36 || h < 18) return '';
|
|
991
|
-
const maxChars = Math.floor((w - 8) / 6.5);
|
|
992
|
-
if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\u2026';
|
|
993
|
-
return name;
|
|
994
|
-
});
|
|
976
|
+
.text(d => truncateLabel(d.data.name || '', d.x1 - d.x0, d.y1 - d.y0));
|
|
995
977
|
}
|
|
996
978
|
|
|
997
979
|
function showTooltip(data, event) {
|
|
@@ -1016,22 +998,6 @@ function showTooltip(data, event) {
|
|
|
1016
998
|
tooltip.style.top = (event.pageY - 14) + 'px';
|
|
1017
999
|
}
|
|
1018
1000
|
|
|
1019
|
-
function zoomTo(path) {
|
|
1020
|
-
currentPath = path;
|
|
1021
|
-
const el = document.getElementById('breadcrumb');
|
|
1022
|
-
const parts = path ? path.split('/') : [];
|
|
1023
|
-
let html = '<span onclick="zoomTo(\\'\\')">root</span>';
|
|
1024
|
-
let accumulated = '';
|
|
1025
|
-
for (const part of parts) {
|
|
1026
|
-
accumulated = accumulated ? accumulated + '/' + part : part;
|
|
1027
|
-
const p = accumulated;
|
|
1028
|
-
html += '<span class="sep">/</span><span onclick="zoomTo(\\'' + p + '\\')">' + part + '</span>';
|
|
1029
|
-
}
|
|
1030
|
-
el.innerHTML = html;
|
|
1031
|
-
render();
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
// Render risk sidebar
|
|
1035
1001
|
function renderRiskSidebar() {
|
|
1036
1002
|
const container = document.getElementById('risk-list');
|
|
1037
1003
|
if (riskFiles.length === 0) {
|
|
@@ -1199,34 +1165,8 @@ function findFolderInTree(node, targetPath) {
|
|
|
1199
1165
|
}
|
|
1200
1166
|
|
|
1201
1167
|
// src/cli/output/multi-user-terminal.ts
|
|
1202
|
-
import
|
|
1203
|
-
var
|
|
1204
|
-
var FILLED_CHAR2 = "\u2588";
|
|
1205
|
-
var EMPTY_CHAR2 = "\u2591";
|
|
1206
|
-
function makeBar2(score, width = BAR_WIDTH2) {
|
|
1207
|
-
const filled = Math.round(score * width);
|
|
1208
|
-
const empty = width - filled;
|
|
1209
|
-
const bar = FILLED_CHAR2.repeat(filled) + EMPTY_CHAR2.repeat(empty);
|
|
1210
|
-
if (score >= 0.8) return chalk3.green(bar);
|
|
1211
|
-
if (score >= 0.5) return chalk3.yellow(bar);
|
|
1212
|
-
if (score > 0) return chalk3.red(bar);
|
|
1213
|
-
return chalk3.gray(bar);
|
|
1214
|
-
}
|
|
1215
|
-
function formatPercent2(score) {
|
|
1216
|
-
return `${Math.round(score * 100)}%`;
|
|
1217
|
-
}
|
|
1218
|
-
function getModeLabel2(mode) {
|
|
1219
|
-
switch (mode) {
|
|
1220
|
-
case "committed":
|
|
1221
|
-
return "Committed mode";
|
|
1222
|
-
case "code-coverage":
|
|
1223
|
-
return "Code Coverage mode";
|
|
1224
|
-
case "weighted":
|
|
1225
|
-
return "Weighted mode";
|
|
1226
|
-
default:
|
|
1227
|
-
return mode;
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1168
|
+
import chalk4 from "chalk";
|
|
1169
|
+
var BAR_WIDTH = 20;
|
|
1230
1170
|
function truncateName(name, maxLen) {
|
|
1231
1171
|
if (name.length <= maxLen) return name;
|
|
1232
1172
|
return name.slice(0, maxLen - 1) + "\u2026";
|
|
@@ -1242,8 +1182,8 @@ function renderFolder3(node, indent, maxDepth, nameWidth) {
|
|
|
1242
1182
|
const prefix = " ".repeat(indent);
|
|
1243
1183
|
const name = (child.path.split("/").pop() || child.path) + "/";
|
|
1244
1184
|
const displayName = truncateName(name, nameWidth).padEnd(nameWidth);
|
|
1245
|
-
const scores = child.userScores.map((s) =>
|
|
1246
|
-
lines.push(`${prefix}${
|
|
1185
|
+
const scores = child.userScores.map((s) => formatPercent(s.score).padStart(5)).join(" ");
|
|
1186
|
+
lines.push(`${prefix}${chalk4.bold(displayName)} ${scores}`);
|
|
1247
1187
|
if (indent < maxDepth) {
|
|
1248
1188
|
lines.push(...renderFolder3(child, indent + 1, maxDepth, nameWidth));
|
|
1249
1189
|
}
|
|
@@ -1255,16 +1195,16 @@ function renderMultiUserTerminal(result) {
|
|
|
1255
1195
|
const { tree, repoName, mode, userSummaries, totalFiles } = result;
|
|
1256
1196
|
console.log("");
|
|
1257
1197
|
console.log(
|
|
1258
|
-
|
|
1259
|
-
`GitFamiliar \u2014 ${repoName} (${
|
|
1198
|
+
chalk4.bold(
|
|
1199
|
+
`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)}, ${userSummaries.length} users)`
|
|
1260
1200
|
)
|
|
1261
1201
|
);
|
|
1262
1202
|
console.log("");
|
|
1263
|
-
console.log(
|
|
1203
|
+
console.log(chalk4.bold("Overall:"));
|
|
1264
1204
|
for (const summary of userSummaries) {
|
|
1265
1205
|
const name = truncateName(summary.user.name, 14).padEnd(14);
|
|
1266
|
-
const bar =
|
|
1267
|
-
const pct =
|
|
1206
|
+
const bar = makeBar(summary.overallScore, BAR_WIDTH);
|
|
1207
|
+
const pct = formatPercent(summary.overallScore);
|
|
1268
1208
|
if (mode === "committed") {
|
|
1269
1209
|
console.log(
|
|
1270
1210
|
` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount}/${totalFiles} files)`
|
|
@@ -1276,7 +1216,7 @@ function renderMultiUserTerminal(result) {
|
|
|
1276
1216
|
console.log("");
|
|
1277
1217
|
const nameWidth = 20;
|
|
1278
1218
|
const headerNames = userSummaries.map((s) => truncateName(s.user.name, 7).padStart(7)).join(" ");
|
|
1279
|
-
console.log(
|
|
1219
|
+
console.log(chalk4.bold("Folders:") + " ".repeat(nameWidth - 4) + headerNames);
|
|
1280
1220
|
const folderLines = renderFolder3(tree, 1, 2, nameWidth);
|
|
1281
1221
|
for (const line of folderLines) {
|
|
1282
1222
|
console.log(line);
|
|
@@ -1298,22 +1238,8 @@ function generateMultiUserHTML(result) {
|
|
|
1298
1238
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1299
1239
|
<title>GitFamiliar \u2014 ${result.repoName} \u2014 Multi-User</title>
|
|
1300
1240
|
<style>
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1304
|
-
background: #1a1a2e;
|
|
1305
|
-
color: #e0e0e0;
|
|
1306
|
-
overflow: hidden;
|
|
1307
|
-
}
|
|
1308
|
-
#header {
|
|
1309
|
-
padding: 16px 24px;
|
|
1310
|
-
background: #16213e;
|
|
1311
|
-
border-bottom: 1px solid #0f3460;
|
|
1312
|
-
display: flex;
|
|
1313
|
-
align-items: center;
|
|
1314
|
-
justify-content: space-between;
|
|
1315
|
-
}
|
|
1316
|
-
#header h1 { font-size: 18px; color: #e94560; }
|
|
1241
|
+
${getBaseStyles()}
|
|
1242
|
+
${getBreadcrumbStyles()}
|
|
1317
1243
|
#header .controls { display: flex; align-items: center; gap: 12px; }
|
|
1318
1244
|
#header select {
|
|
1319
1245
|
padding: 4px 12px;
|
|
@@ -1323,46 +1249,8 @@ function generateMultiUserHTML(result) {
|
|
|
1323
1249
|
border-radius: 4px;
|
|
1324
1250
|
font-size: 13px;
|
|
1325
1251
|
}
|
|
1326
|
-
#header .info { font-size: 14px; color: #a0a0a0; }
|
|
1327
|
-
#breadcrumb {
|
|
1328
|
-
padding: 8px 24px;
|
|
1329
|
-
background: #16213e;
|
|
1330
|
-
font-size: 13px;
|
|
1331
|
-
border-bottom: 1px solid #0f3460;
|
|
1332
|
-
}
|
|
1333
|
-
#breadcrumb span { cursor: pointer; color: #5eadf7; }
|
|
1334
|
-
#breadcrumb span:hover { text-decoration: underline; }
|
|
1335
|
-
#breadcrumb .sep { color: #666; margin: 0 4px; }
|
|
1336
1252
|
#treemap { width: 100%; }
|
|
1337
|
-
|
|
1338
|
-
position: absolute;
|
|
1339
|
-
pointer-events: none;
|
|
1340
|
-
background: rgba(22, 33, 62, 0.95);
|
|
1341
|
-
border: 1px solid #0f3460;
|
|
1342
|
-
border-radius: 6px;
|
|
1343
|
-
padding: 10px 14px;
|
|
1344
|
-
font-size: 13px;
|
|
1345
|
-
line-height: 1.6;
|
|
1346
|
-
display: none;
|
|
1347
|
-
z-index: 100;
|
|
1348
|
-
max-width: 350px;
|
|
1349
|
-
}
|
|
1350
|
-
#legend {
|
|
1351
|
-
position: absolute;
|
|
1352
|
-
bottom: 16px;
|
|
1353
|
-
right: 16px;
|
|
1354
|
-
background: rgba(22, 33, 62, 0.9);
|
|
1355
|
-
border: 1px solid #0f3460;
|
|
1356
|
-
border-radius: 6px;
|
|
1357
|
-
padding: 10px;
|
|
1358
|
-
font-size: 12px;
|
|
1359
|
-
}
|
|
1360
|
-
#legend .gradient-bar {
|
|
1361
|
-
width: 120px; height: 12px;
|
|
1362
|
-
background: linear-gradient(to right, #e94560, #f5a623, #27ae60);
|
|
1363
|
-
border-radius: 3px; margin: 4px 0;
|
|
1364
|
-
}
|
|
1365
|
-
#legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }
|
|
1253
|
+
${getGradientLegendStyles()}
|
|
1366
1254
|
</style>
|
|
1367
1255
|
</head>
|
|
1368
1256
|
<body>
|
|
@@ -1396,54 +1284,25 @@ const select = document.getElementById('userSelect');
|
|
|
1396
1284
|
userNames.forEach((name, i) => {
|
|
1397
1285
|
const opt = document.createElement('option');
|
|
1398
1286
|
opt.value = i;
|
|
1399
|
-
const summary = summaries[i];
|
|
1400
|
-
opt.textContent = name + ' (' + Math.round(summary.overallScore * 100) + '%)';
|
|
1401
|
-
select.appendChild(opt);
|
|
1402
|
-
});
|
|
1403
|
-
|
|
1404
|
-
function changeUser() {
|
|
1405
|
-
currentUser = parseInt(select.value);
|
|
1406
|
-
render();
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
function scoreColor(score) {
|
|
1410
|
-
if (score <= 0) return '#e94560';
|
|
1411
|
-
if (score >= 1) return '#27ae60';
|
|
1412
|
-
if (score < 0.5) {
|
|
1413
|
-
const t = score / 0.5;
|
|
1414
|
-
return d3.interpolateRgb('#e94560', '#f5a623')(t);
|
|
1415
|
-
}
|
|
1416
|
-
const t = (score - 0.5) / 0.5;
|
|
1417
|
-
return d3.interpolateRgb('#f5a623', '#27ae60')(t);
|
|
1287
|
+
const summary = summaries[i];
|
|
1288
|
+
opt.textContent = name + ' (' + Math.round(summary.overallScore * 100) + '%)';
|
|
1289
|
+
select.appendChild(opt);
|
|
1290
|
+
});
|
|
1291
|
+
|
|
1292
|
+
function changeUser() {
|
|
1293
|
+
currentUser = parseInt(select.value);
|
|
1294
|
+
render();
|
|
1418
1295
|
}
|
|
1419
1296
|
|
|
1297
|
+
${getScoreColorScript()}
|
|
1298
|
+
|
|
1420
1299
|
function getUserScore(node) {
|
|
1421
1300
|
if (!node.userScores || node.userScores.length === 0) return node.score;
|
|
1422
1301
|
const s = node.userScores[currentUser];
|
|
1423
1302
|
return s ? s.score : 0;
|
|
1424
1303
|
}
|
|
1425
1304
|
|
|
1426
|
-
|
|
1427
|
-
if (node.path === path) return node;
|
|
1428
|
-
if (node.children) {
|
|
1429
|
-
for (const child of node.children) {
|
|
1430
|
-
const found = findNode(child, path);
|
|
1431
|
-
if (found) return found;
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
return null;
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
function buildHierarchy(node) {
|
|
1438
|
-
if (node.type === 'file') {
|
|
1439
|
-
return { name: node.path.split('/').pop(), data: node, value: Math.max(1, node.lines) };
|
|
1440
|
-
}
|
|
1441
|
-
return {
|
|
1442
|
-
name: node.path.split('/').pop() || node.path,
|
|
1443
|
-
data: node,
|
|
1444
|
-
children: (node.children || []).map(c => buildHierarchy(c)),
|
|
1445
|
-
};
|
|
1446
|
-
}
|
|
1305
|
+
${getTreemapUtilsScript()}
|
|
1447
1306
|
|
|
1448
1307
|
function render() {
|
|
1449
1308
|
const container = document.getElementById('treemap');
|
|
@@ -1524,15 +1383,7 @@ function render() {
|
|
|
1524
1383
|
.attr('font-size', d => d.children ? '11px' : '10px')
|
|
1525
1384
|
.attr('font-weight', d => d.children ? 'bold' : 'normal')
|
|
1526
1385
|
.style('pointer-events', 'none')
|
|
1527
|
-
.text(d =>
|
|
1528
|
-
const w = d.x1 - d.x0;
|
|
1529
|
-
const h = d.y1 - d.y0;
|
|
1530
|
-
const name = d.data.name || '';
|
|
1531
|
-
if (w < 36 || h < 18) return '';
|
|
1532
|
-
const maxChars = Math.floor((w - 8) / 6.5);
|
|
1533
|
-
if (name.length > maxChars) return name.slice(0, maxChars - 1) + '\\u2026';
|
|
1534
|
-
return name;
|
|
1535
|
-
});
|
|
1386
|
+
.text(d => truncateLabel(d.data.name || '', d.x1 - d.x0, d.y1 - d.y0));
|
|
1536
1387
|
}
|
|
1537
1388
|
|
|
1538
1389
|
function showTooltip(data, event) {
|
|
@@ -1562,21 +1413,6 @@ function showTooltip(data, event) {
|
|
|
1562
1413
|
tooltip.style.top = (event.pageY - 14) + 'px';
|
|
1563
1414
|
}
|
|
1564
1415
|
|
|
1565
|
-
function zoomTo(path) {
|
|
1566
|
-
currentPath = path;
|
|
1567
|
-
const el = document.getElementById('breadcrumb');
|
|
1568
|
-
const parts = path ? path.split('/') : [];
|
|
1569
|
-
let html = '<span onclick="zoomTo(\\'\\')">root</span>';
|
|
1570
|
-
let accumulated = '';
|
|
1571
|
-
for (const part of parts) {
|
|
1572
|
-
accumulated = accumulated ? accumulated + '/' + part : part;
|
|
1573
|
-
const p = accumulated;
|
|
1574
|
-
html += '<span class="sep">/</span><span onclick="zoomTo(\\'' + p + '\\')">' + part + '</span>';
|
|
1575
|
-
}
|
|
1576
|
-
el.innerHTML = html;
|
|
1577
|
-
render();
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
1416
|
window.addEventListener('resize', render);
|
|
1581
1417
|
render();
|
|
1582
1418
|
</script>
|
|
@@ -1625,7 +1461,6 @@ async function bulkGetChangeFrequency(gitClient, days, trackedFiles) {
|
|
|
1625
1461
|
}
|
|
1626
1462
|
|
|
1627
1463
|
// src/core/hotspot.ts
|
|
1628
|
-
var DEFAULT_WINDOW = 90;
|
|
1629
1464
|
async function computeHotspots(options) {
|
|
1630
1465
|
const gitClient = new GitClient(options.repoPath);
|
|
1631
1466
|
if (!await gitClient.isRepo()) {
|
|
@@ -1635,7 +1470,7 @@ async function computeHotspots(options) {
|
|
|
1635
1470
|
const repoRoot = await gitClient.getRepoRoot();
|
|
1636
1471
|
const filter = createFilter(repoRoot);
|
|
1637
1472
|
const tree = await buildFileTree(gitClient, filter);
|
|
1638
|
-
const timeWindow = options.since ||
|
|
1473
|
+
const timeWindow = options.since || DEFAULT_HOTSPOT_WINDOW;
|
|
1639
1474
|
const isTeamMode = options.hotspot === "team";
|
|
1640
1475
|
const trackedFiles = /* @__PURE__ */ new Set();
|
|
1641
1476
|
walkFiles(tree, (f) => trackedFiles.add(f.path));
|
|
@@ -1705,12 +1540,6 @@ async function computeHotspots(options) {
|
|
|
1705
1540
|
summary
|
|
1706
1541
|
};
|
|
1707
1542
|
}
|
|
1708
|
-
function classifyHotspotRisk(risk) {
|
|
1709
|
-
if (risk >= 0.6) return "critical";
|
|
1710
|
-
if (risk >= 0.4) return "high";
|
|
1711
|
-
if (risk >= 0.2) return "medium";
|
|
1712
|
-
return "low";
|
|
1713
|
-
}
|
|
1714
1543
|
async function computeTeamAvgFamiliarity(gitClient, trackedFiles, options) {
|
|
1715
1544
|
const contributors = await getAllContributors(gitClient, 1);
|
|
1716
1545
|
const totalContributors = Math.max(1, contributors.length);
|
|
@@ -1731,29 +1560,29 @@ async function computeTeamAvgFamiliarity(gitClient, trackedFiles, options) {
|
|
|
1731
1560
|
}
|
|
1732
1561
|
|
|
1733
1562
|
// src/cli/output/hotspot-terminal.ts
|
|
1734
|
-
import
|
|
1563
|
+
import chalk5 from "chalk";
|
|
1735
1564
|
function riskBadge2(level) {
|
|
1736
1565
|
switch (level) {
|
|
1737
1566
|
case "critical":
|
|
1738
|
-
return
|
|
1567
|
+
return chalk5.bgRed.white.bold(" CRIT ");
|
|
1739
1568
|
case "high":
|
|
1740
|
-
return
|
|
1569
|
+
return chalk5.bgRedBright.white(" HIGH ");
|
|
1741
1570
|
case "medium":
|
|
1742
|
-
return
|
|
1571
|
+
return chalk5.bgYellow.black(" MED ");
|
|
1743
1572
|
case "low":
|
|
1744
|
-
return
|
|
1573
|
+
return chalk5.bgGreen.black(" LOW ");
|
|
1745
1574
|
}
|
|
1746
1575
|
}
|
|
1747
1576
|
function riskColor2(level) {
|
|
1748
1577
|
switch (level) {
|
|
1749
1578
|
case "critical":
|
|
1750
|
-
return
|
|
1579
|
+
return chalk5.red;
|
|
1751
1580
|
case "high":
|
|
1752
|
-
return
|
|
1581
|
+
return chalk5.redBright;
|
|
1753
1582
|
case "medium":
|
|
1754
|
-
return
|
|
1583
|
+
return chalk5.yellow;
|
|
1755
1584
|
case "low":
|
|
1756
|
-
return
|
|
1585
|
+
return chalk5.green;
|
|
1757
1586
|
}
|
|
1758
1587
|
}
|
|
1759
1588
|
function renderHotspotTerminal(result) {
|
|
@@ -1762,24 +1591,24 @@ function renderHotspotTerminal(result) {
|
|
|
1762
1591
|
const modeLabel = hotspotMode === "team" ? "Team Hotspots" : "Personal Hotspots";
|
|
1763
1592
|
const userLabel = userName ? ` (${userName})` : "";
|
|
1764
1593
|
console.log(
|
|
1765
|
-
|
|
1594
|
+
chalk5.bold(`GitFamiliar \u2014 ${modeLabel}${userLabel} \u2014 ${repoName}`)
|
|
1766
1595
|
);
|
|
1767
|
-
console.log(
|
|
1596
|
+
console.log(chalk5.gray(` Time window: last ${timeWindow} days`));
|
|
1768
1597
|
console.log("");
|
|
1769
1598
|
const activeFiles = files.filter((f) => f.changeFrequency > 0);
|
|
1770
1599
|
if (activeFiles.length === 0) {
|
|
1771
|
-
console.log(
|
|
1600
|
+
console.log(chalk5.gray(" No files changed in the time window."));
|
|
1772
1601
|
console.log("");
|
|
1773
1602
|
return;
|
|
1774
1603
|
}
|
|
1775
1604
|
const displayCount = Math.min(30, activeFiles.length);
|
|
1776
1605
|
const topFiles = activeFiles.slice(0, displayCount);
|
|
1777
1606
|
console.log(
|
|
1778
|
-
|
|
1607
|
+
chalk5.gray(
|
|
1779
1608
|
` ${"Rank".padEnd(5)} ${"File".padEnd(42)} ${"Familiarity".padStart(11)} ${"Changes".padStart(8)} ${"Risk".padStart(6)} Level`
|
|
1780
1609
|
)
|
|
1781
1610
|
);
|
|
1782
|
-
console.log(
|
|
1611
|
+
console.log(chalk5.gray(" " + "\u2500".repeat(90)));
|
|
1783
1612
|
for (let i = 0; i < topFiles.length; i++) {
|
|
1784
1613
|
const f = topFiles[i];
|
|
1785
1614
|
const rank = String(i + 1).padEnd(5);
|
|
@@ -1795,33 +1624,33 @@ function renderHotspotTerminal(result) {
|
|
|
1795
1624
|
}
|
|
1796
1625
|
if (activeFiles.length > displayCount) {
|
|
1797
1626
|
console.log(
|
|
1798
|
-
|
|
1627
|
+
chalk5.gray(` ... and ${activeFiles.length - displayCount} more files`)
|
|
1799
1628
|
);
|
|
1800
1629
|
}
|
|
1801
1630
|
console.log("");
|
|
1802
|
-
console.log(
|
|
1631
|
+
console.log(chalk5.bold("Summary:"));
|
|
1803
1632
|
if (summary.critical > 0) {
|
|
1804
1633
|
console.log(
|
|
1805
|
-
` ${
|
|
1634
|
+
` ${chalk5.red.bold(`\u{1F534} Critical Risk: ${summary.critical} files`)}`
|
|
1806
1635
|
);
|
|
1807
1636
|
}
|
|
1808
1637
|
if (summary.high > 0) {
|
|
1809
1638
|
console.log(
|
|
1810
|
-
` ${
|
|
1639
|
+
` ${chalk5.redBright(`\u{1F7E0} High Risk: ${summary.high} files`)}`
|
|
1811
1640
|
);
|
|
1812
1641
|
}
|
|
1813
1642
|
if (summary.medium > 0) {
|
|
1814
1643
|
console.log(
|
|
1815
|
-
` ${
|
|
1644
|
+
` ${chalk5.yellow(`\u{1F7E1} Medium Risk: ${summary.medium} files`)}`
|
|
1816
1645
|
);
|
|
1817
1646
|
}
|
|
1818
1647
|
console.log(
|
|
1819
|
-
` ${
|
|
1648
|
+
` ${chalk5.green(`\u{1F7E2} Low Risk: ${summary.low} files`)}`
|
|
1820
1649
|
);
|
|
1821
1650
|
console.log("");
|
|
1822
1651
|
if (summary.critical > 0 || summary.high > 0) {
|
|
1823
1652
|
console.log(
|
|
1824
|
-
|
|
1653
|
+
chalk5.gray(
|
|
1825
1654
|
" Recommendation: Focus code review and knowledge transfer on critical/high risk files."
|
|
1826
1655
|
)
|
|
1827
1656
|
);
|
|
@@ -1857,33 +1686,11 @@ function generateHotspotHTML(result) {
|
|
|
1857
1686
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1858
1687
|
<title>GitFamiliar \u2014 ${modeLabel} \u2014 ${result.repoName}</title>
|
|
1859
1688
|
<style>
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
color: #e0e0e0;
|
|
1865
|
-
overflow: hidden;
|
|
1866
|
-
}
|
|
1867
|
-
#header {
|
|
1868
|
-
padding: 16px 24px;
|
|
1869
|
-
background: #16213e;
|
|
1870
|
-
border-bottom: 1px solid #0f3460;
|
|
1871
|
-
display: flex;
|
|
1872
|
-
align-items: center;
|
|
1873
|
-
justify-content: space-between;
|
|
1874
|
-
}
|
|
1875
|
-
#header h1 { font-size: 18px; color: #e94560; }
|
|
1876
|
-
#header .info { font-size: 14px; color: #a0a0a0; }
|
|
1877
|
-
#main { display: flex; height: calc(100vh - 60px); }
|
|
1689
|
+
${getBaseStyles()}
|
|
1690
|
+
${getSidebarStyles()}
|
|
1691
|
+
#main { height: calc(100vh - 60px); }
|
|
1692
|
+
#sidebar { width: 320px; }
|
|
1878
1693
|
#chart { flex: 1; position: relative; }
|
|
1879
|
-
#sidebar {
|
|
1880
|
-
width: 320px;
|
|
1881
|
-
background: #16213e;
|
|
1882
|
-
border-left: 1px solid #0f3460;
|
|
1883
|
-
overflow-y: auto;
|
|
1884
|
-
padding: 16px;
|
|
1885
|
-
}
|
|
1886
|
-
#sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }
|
|
1887
1694
|
.hotspot-item {
|
|
1888
1695
|
padding: 8px 0;
|
|
1889
1696
|
border-bottom: 1px solid #0f3460;
|
|
@@ -1903,19 +1710,6 @@ function generateHotspotHTML(result) {
|
|
|
1903
1710
|
.risk-high { background: #f07040; color: white; }
|
|
1904
1711
|
.risk-medium { background: #f5a623; color: black; }
|
|
1905
1712
|
.risk-low { background: #27ae60; color: white; }
|
|
1906
|
-
#tooltip {
|
|
1907
|
-
position: absolute;
|
|
1908
|
-
pointer-events: none;
|
|
1909
|
-
background: rgba(22, 33, 62, 0.95);
|
|
1910
|
-
border: 1px solid #0f3460;
|
|
1911
|
-
border-radius: 6px;
|
|
1912
|
-
padding: 10px 14px;
|
|
1913
|
-
font-size: 13px;
|
|
1914
|
-
line-height: 1.6;
|
|
1915
|
-
display: none;
|
|
1916
|
-
z-index: 100;
|
|
1917
|
-
max-width: 350px;
|
|
1918
|
-
}
|
|
1919
1713
|
#zone-labels { position: absolute; pointer-events: none; }
|
|
1920
1714
|
.zone-label {
|
|
1921
1715
|
position: absolute;
|
|
@@ -2159,37 +1953,10 @@ async function computeUnified(options) {
|
|
|
2159
1953
|
// src/cli/output/unified-html.ts
|
|
2160
1954
|
import { writeFileSync as writeFileSync5 } from "fs";
|
|
2161
1955
|
import { join as join5 } from "path";
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
);
|
|
2167
|
-
const scoringWeightedJson = JSON.stringify(data.scoring.weighted.tree);
|
|
2168
|
-
const coverageTreeJson = JSON.stringify(data.coverage.tree);
|
|
2169
|
-
const coverageRiskJson = JSON.stringify(data.coverage.riskFiles);
|
|
2170
|
-
const hotspotJson = JSON.stringify(
|
|
2171
|
-
data.hotspot.files.filter((f) => f.changeFrequency > 0).map((f) => ({
|
|
2172
|
-
path: f.path,
|
|
2173
|
-
lines: f.lines,
|
|
2174
|
-
familiarity: f.familiarity,
|
|
2175
|
-
changeFrequency: f.changeFrequency,
|
|
2176
|
-
risk: f.risk,
|
|
2177
|
-
riskLevel: f.riskLevel
|
|
2178
|
-
}))
|
|
2179
|
-
);
|
|
2180
|
-
const hotspotTeamFamJson = JSON.stringify(data.hotspotTeamFamiliarity);
|
|
2181
|
-
const multiUserTreeJson = JSON.stringify(data.multiUser.tree);
|
|
2182
|
-
const multiUserSummariesJson = JSON.stringify(data.multiUser.userSummaries);
|
|
2183
|
-
const multiUserNamesJson = JSON.stringify(
|
|
2184
|
-
data.multiUser.users.map((u) => u.name)
|
|
2185
|
-
);
|
|
2186
|
-
return `<!DOCTYPE html>
|
|
2187
|
-
<html lang="en">
|
|
2188
|
-
<head>
|
|
2189
|
-
<meta charset="UTF-8">
|
|
2190
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
2191
|
-
<title>GitFamiliar \u2014 ${data.repoName}</title>
|
|
2192
|
-
<style>
|
|
1956
|
+
|
|
1957
|
+
// src/cli/output/unified-html-styles.ts
|
|
1958
|
+
function getUnifiedStyles() {
|
|
1959
|
+
return `
|
|
2193
1960
|
:root {
|
|
2194
1961
|
--bg-base: #1a1a2e;
|
|
2195
1962
|
--bg-panel: #16213e;
|
|
@@ -2514,146 +2281,32 @@ function generateUnifiedHTML(data) {
|
|
|
2514
2281
|
display: none;
|
|
2515
2282
|
z-index: 50;
|
|
2516
2283
|
box-shadow: var(--shadow-md);
|
|
2517
|
-
}
|
|
2518
|
-
.legend.active { display: block; }
|
|
2519
|
-
.legend .gradient-bar {
|
|
2520
|
-
width: 120px;
|
|
2521
|
-
height: 12px;
|
|
2522
|
-
background: linear-gradient(to right, var(--color-critical), var(--color-medium), var(--color-safe));
|
|
2523
|
-
border-radius: 6px;
|
|
2524
|
-
margin: 4px 0;
|
|
2525
|
-
box-shadow: inset 0 1px 2px rgba(0,0,0,0.2);
|
|
2526
|
-
}
|
|
2527
|
-
.legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: var(--text-dim); }
|
|
2528
|
-
.legend .row { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
|
|
2529
|
-
.legend .swatch { width: 14px; height: 14px; border-radius: 4px; box-shadow: var(--shadow-sm); }
|
|
2530
|
-
|
|
2531
|
-
/* Zone labels for hotspot */
|
|
2532
|
-
#zone-labels { position: absolute; pointer-events: none; }
|
|
2533
|
-
.zone-label {
|
|
2534
|
-
position: absolute;
|
|
2535
|
-
font-size: 16px;
|
|
2536
|
-
font-weight: bold;
|
|
2537
|
-
}
|
|
2538
|
-
|
|
2539
|
-
</head>
|
|
2540
|
-
<body>
|
|
2541
|
-
<div id="header">
|
|
2542
|
-
<h1>GitFamiliar \u2014 ${data.repoName}</h1>
|
|
2543
|
-
<div class="info">${data.userName} | ${data.scoring.committed.totalFiles} files</div>
|
|
2544
|
-
</div>
|
|
2545
|
-
|
|
2546
|
-
<div id="tabs">
|
|
2547
|
-
<div class="tab active" onclick="switchTab('scoring')">Scoring</div>
|
|
2548
|
-
<div class="tab" onclick="switchTab('coverage')">Contributors</div>
|
|
2549
|
-
<div class="tab" onclick="switchTab('multiuser')">Multi-User</div>
|
|
2550
|
-
<div class="tab" onclick="switchTab('hotspots')">Hotspots</div>
|
|
2551
|
-
</div>
|
|
2552
|
-
|
|
2553
|
-
<div id="tab-desc-scoring" class="tab-desc visible">
|
|
2554
|
-
Your personal familiarity with each file, based on Git history. Larger blocks = more lines of code. Color shows how well you know each file.
|
|
2555
|
-
</div>
|
|
2556
|
-
<div id="tab-desc-coverage" class="tab-desc">
|
|
2557
|
-
Contributors per file: how many people have committed to each file. Low contributor count = high bus factor risk.
|
|
2558
|
-
</div>
|
|
2559
|
-
<div id="tab-desc-multiuser" class="tab-desc">
|
|
2560
|
-
Compare familiarity scores across team members. Select a user to see the codebase colored by their knowledge.
|
|
2561
|
-
</div>
|
|
2562
|
-
<div id="tab-desc-hotspots" class="tab-desc">
|
|
2563
|
-
Files that change frequently but are poorly understood. Top-left = danger zone (high change, low familiarity).
|
|
2564
|
-
</div>
|
|
2565
|
-
|
|
2566
|
-
<div id="scoring-controls" class="visible">
|
|
2567
|
-
<button class="subtab active" data-mode="committed" onclick="switchScoringMode('committed')">Committed</button>
|
|
2568
|
-
<button class="subtab" data-mode="code-coverage" onclick="switchScoringMode('code-coverage')">Code Coverage</button>
|
|
2569
|
-
<button class="subtab" data-mode="weighted" onclick="switchScoringMode('weighted')">Weighted</button>
|
|
2570
|
-
<div id="weight-controls">
|
|
2571
|
-
<span>Blame:</span>
|
|
2572
|
-
<span class="weight-label" id="blame-label">50%</span>
|
|
2573
|
-
<input type="range" id="blame-slider" min="0" max="100" value="50" oninput="onWeightChange()">
|
|
2574
|
-
<span>Commit:</span>
|
|
2575
|
-
<span class="weight-label" id="commit-label">50%</span>
|
|
2576
|
-
</div>
|
|
2577
|
-
</div>
|
|
2578
|
-
<div id="scoring-mode-desc" class="tab-desc visible" style="padding-top:0">
|
|
2579
|
-
<span id="mode-desc-text">Binary: Have you ever committed to this file? Yes (green) or No (red).</span>
|
|
2580
|
-
</div>
|
|
2581
|
-
|
|
2582
|
-
<div id="multiuser-controls">
|
|
2583
|
-
<label>View as:</label>
|
|
2584
|
-
<select id="userSelect" onchange="onUserChange()"></select>
|
|
2585
|
-
</div>
|
|
2586
|
-
|
|
2587
|
-
<div id="hotspot-controls">
|
|
2588
|
-
<label>Mode:</label>
|
|
2589
|
-
<button class="subtab active" data-mode="personal" onclick="switchHotspotMode('personal')">Personal</button>
|
|
2590
|
-
<button class="subtab" data-mode="team" onclick="switchHotspotMode('team')">Team</button>
|
|
2591
|
-
<span class="sep-v"></span>
|
|
2592
|
-
<label>Scoring:</label>
|
|
2593
|
-
<button class="subtab hs-scoring active" data-mode="committed" onclick="switchHotspotScoring('committed')">Committed</button>
|
|
2594
|
-
<button class="subtab hs-scoring" data-mode="code-coverage" onclick="switchHotspotScoring('code-coverage')">Code Coverage</button>
|
|
2595
|
-
<button class="subtab hs-scoring" data-mode="weighted" onclick="switchHotspotScoring('weighted')">Weighted</button>
|
|
2596
|
-
</div>
|
|
2597
|
-
|
|
2598
|
-
<div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
|
|
2599
|
-
|
|
2600
|
-
<div id="content-area">
|
|
2601
|
-
<div id="tab-scoring" class="tab-content active"></div>
|
|
2602
|
-
<div id="tab-coverage" class="tab-content with-sidebar">
|
|
2603
|
-
<div class="viz-area" id="coverage-viz"></div>
|
|
2604
|
-
<div class="sidebar" id="coverage-sidebar">
|
|
2605
|
-
<h3>Risk Files (0-1 contributors)</h3>
|
|
2606
|
-
<div id="risk-list"></div>
|
|
2607
|
-
</div>
|
|
2608
|
-
</div>
|
|
2609
|
-
<div id="tab-multiuser" class="tab-content"></div>
|
|
2610
|
-
<div id="tab-hotspots" class="tab-content with-sidebar">
|
|
2611
|
-
<div class="viz-area" id="hotspot-viz">
|
|
2612
|
-
<div id="zone-labels"></div>
|
|
2613
|
-
</div>
|
|
2614
|
-
<div class="sidebar" id="hotspot-sidebar">
|
|
2615
|
-
<h3>Top Hotspots</h3>
|
|
2616
|
-
<div id="hotspot-list"></div>
|
|
2617
|
-
</div>
|
|
2618
|
-
</div>
|
|
2619
|
-
</div>
|
|
2620
|
-
|
|
2621
|
-
<div id="tooltip"></div>
|
|
2622
|
-
|
|
2623
|
-
<!-- Legends -->
|
|
2624
|
-
<div class="legend active" id="legend-scoring">
|
|
2625
|
-
<div>Familiarity</div>
|
|
2626
|
-
<div class="gradient-bar"></div>
|
|
2627
|
-
<div class="labels"><span>0%</span><span>50%</span><span>100%</span></div>
|
|
2628
|
-
</div>
|
|
2629
|
-
<div class="legend" id="legend-coverage">
|
|
2630
|
-
<div>Contributors</div>
|
|
2631
|
-
<div class="row"><div class="swatch" style="background:var(--color-critical)"></div> 0\u20131 (Risk)</div>
|
|
2632
|
-
<div class="row"><div class="swatch" style="background:var(--color-medium)"></div> 2\u20133 (Moderate)</div>
|
|
2633
|
-
<div class="row"><div class="swatch" style="background:var(--color-safe)"></div> 4+ (Safe)</div>
|
|
2634
|
-
</div>
|
|
2635
|
-
<div class="legend" id="legend-multiuser">
|
|
2636
|
-
<div>Familiarity</div>
|
|
2637
|
-
<div class="gradient-bar"></div>
|
|
2638
|
-
<div class="labels"><span>0%</span><span>50%</span><span>100%</span></div>
|
|
2639
|
-
</div>
|
|
2640
|
-
|
|
2641
|
-
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
2642
|
-
<script>
|
|
2643
|
-
// \u2500\u2500 Data \u2500\u2500
|
|
2644
|
-
const scoringData = {
|
|
2645
|
-
committed: ${scoringCommittedJson},
|
|
2646
|
-
'code-coverage': ${scoringCodeCoverageJson},
|
|
2647
|
-
weighted: ${scoringWeightedJson},
|
|
2648
|
-
};
|
|
2649
|
-
const coverageData = ${coverageTreeJson};
|
|
2650
|
-
const coverageRiskFiles = ${coverageRiskJson};
|
|
2651
|
-
const hotspotData = ${hotspotJson};
|
|
2652
|
-
const hotspotTeamFamiliarity = ${hotspotTeamFamJson};
|
|
2653
|
-
const multiUserData = ${multiUserTreeJson};
|
|
2654
|
-
const multiUserNames = ${multiUserNamesJson};
|
|
2655
|
-
const multiUserSummaries = ${multiUserSummariesJson};
|
|
2284
|
+
}
|
|
2285
|
+
.legend.active { display: block; }
|
|
2286
|
+
.legend .gradient-bar {
|
|
2287
|
+
width: 120px;
|
|
2288
|
+
height: 12px;
|
|
2289
|
+
background: linear-gradient(to right, var(--color-critical), var(--color-medium), var(--color-safe));
|
|
2290
|
+
border-radius: 6px;
|
|
2291
|
+
margin: 4px 0;
|
|
2292
|
+
box-shadow: inset 0 1px 2px rgba(0,0,0,0.2);
|
|
2293
|
+
}
|
|
2294
|
+
.legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: var(--text-dim); }
|
|
2295
|
+
.legend .row { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
|
|
2296
|
+
.legend .swatch { width: 14px; height: 14px; border-radius: 4px; box-shadow: var(--shadow-sm); }
|
|
2297
|
+
|
|
2298
|
+
/* Zone labels for hotspot */
|
|
2299
|
+
#zone-labels { position: absolute; pointer-events: none; }
|
|
2300
|
+
.zone-label {
|
|
2301
|
+
position: absolute;
|
|
2302
|
+
font-size: 16px;
|
|
2303
|
+
font-weight: bold;
|
|
2304
|
+
}`;
|
|
2305
|
+
}
|
|
2656
2306
|
|
|
2307
|
+
// src/cli/output/unified-html-scripts.ts
|
|
2308
|
+
function getUnifiedScripts() {
|
|
2309
|
+
return `
|
|
2657
2310
|
// \u2500\u2500 State \u2500\u2500
|
|
2658
2311
|
let activeTab = 'scoring';
|
|
2659
2312
|
let scoringMode = 'committed';
|
|
@@ -3324,7 +2977,159 @@ window.addEventListener('resize', () => {
|
|
|
3324
2977
|
else if (activeTab === 'coverage') renderCoverage();
|
|
3325
2978
|
else if (activeTab === 'hotspots') renderHotspot();
|
|
3326
2979
|
else if (activeTab === 'multiuser') renderMultiUser();
|
|
3327
|
-
})
|
|
2980
|
+
});`;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// src/cli/output/unified-html.ts
|
|
2984
|
+
function generateUnifiedHTML(data) {
|
|
2985
|
+
const scoringCommittedJson = JSON.stringify(data.scoring.committed.tree);
|
|
2986
|
+
const scoringCodeCoverageJson = JSON.stringify(
|
|
2987
|
+
data.scoring.codeCoverage.tree
|
|
2988
|
+
);
|
|
2989
|
+
const scoringWeightedJson = JSON.stringify(data.scoring.weighted.tree);
|
|
2990
|
+
const coverageTreeJson = JSON.stringify(data.coverage.tree);
|
|
2991
|
+
const coverageRiskJson = JSON.stringify(data.coverage.riskFiles);
|
|
2992
|
+
const hotspotJson = JSON.stringify(
|
|
2993
|
+
data.hotspot.files.filter((f) => f.changeFrequency > 0).map((f) => ({
|
|
2994
|
+
path: f.path,
|
|
2995
|
+
lines: f.lines,
|
|
2996
|
+
familiarity: f.familiarity,
|
|
2997
|
+
changeFrequency: f.changeFrequency,
|
|
2998
|
+
risk: f.risk,
|
|
2999
|
+
riskLevel: f.riskLevel
|
|
3000
|
+
}))
|
|
3001
|
+
);
|
|
3002
|
+
const hotspotTeamFamJson = JSON.stringify(data.hotspotTeamFamiliarity);
|
|
3003
|
+
const multiUserTreeJson = JSON.stringify(data.multiUser.tree);
|
|
3004
|
+
const multiUserSummariesJson = JSON.stringify(data.multiUser.userSummaries);
|
|
3005
|
+
const multiUserNamesJson = JSON.stringify(
|
|
3006
|
+
data.multiUser.users.map((u) => u.name)
|
|
3007
|
+
);
|
|
3008
|
+
return `<!DOCTYPE html>
|
|
3009
|
+
<html lang="en">
|
|
3010
|
+
<head>
|
|
3011
|
+
<meta charset="UTF-8">
|
|
3012
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
3013
|
+
<title>GitFamiliar \u2014 ${data.repoName}</title>
|
|
3014
|
+
<style>${getUnifiedStyles()}</style>
|
|
3015
|
+
</head>
|
|
3016
|
+
<body>
|
|
3017
|
+
<div id="header">
|
|
3018
|
+
<h1>GitFamiliar \u2014 ${data.repoName}</h1>
|
|
3019
|
+
<div class="info">${data.userName} | ${data.scoring.committed.totalFiles} files</div>
|
|
3020
|
+
</div>
|
|
3021
|
+
|
|
3022
|
+
<div id="tabs">
|
|
3023
|
+
<div class="tab active" onclick="switchTab('scoring')">Scoring</div>
|
|
3024
|
+
<div class="tab" onclick="switchTab('coverage')">Contributors</div>
|
|
3025
|
+
<div class="tab" onclick="switchTab('multiuser')">Team</div>
|
|
3026
|
+
<div class="tab" onclick="switchTab('hotspots')">Hotspots</div>
|
|
3027
|
+
</div>
|
|
3028
|
+
|
|
3029
|
+
<div id="tab-desc-scoring" class="tab-desc visible">
|
|
3030
|
+
Your personal familiarity with each file, based on Git history. Larger blocks = more lines of code. Color shows how well you know each file.
|
|
3031
|
+
</div>
|
|
3032
|
+
<div id="tab-desc-coverage" class="tab-desc">
|
|
3033
|
+
Contributors per file: how many people have committed to each file. Low contributor count = high bus factor risk.
|
|
3034
|
+
</div>
|
|
3035
|
+
<div id="tab-desc-multiuser" class="tab-desc">
|
|
3036
|
+
Compare familiarity scores across the team. Select a user to see the codebase colored by their knowledge.
|
|
3037
|
+
</div>
|
|
3038
|
+
<div id="tab-desc-hotspots" class="tab-desc">
|
|
3039
|
+
Files that change frequently but are poorly understood. Top-left = danger zone (high change, low familiarity).
|
|
3040
|
+
</div>
|
|
3041
|
+
|
|
3042
|
+
<div id="scoring-controls" class="visible">
|
|
3043
|
+
<button class="subtab active" data-mode="committed" onclick="switchScoringMode('committed')">Committed</button>
|
|
3044
|
+
<button class="subtab" data-mode="code-coverage" onclick="switchScoringMode('code-coverage')">Code Coverage</button>
|
|
3045
|
+
<button class="subtab" data-mode="weighted" onclick="switchScoringMode('weighted')">Weighted</button>
|
|
3046
|
+
<div id="weight-controls">
|
|
3047
|
+
<span>Blame:</span>
|
|
3048
|
+
<span class="weight-label" id="blame-label">50%</span>
|
|
3049
|
+
<input type="range" id="blame-slider" min="0" max="100" value="50" oninput="onWeightChange()">
|
|
3050
|
+
<span>Commit:</span>
|
|
3051
|
+
<span class="weight-label" id="commit-label">50%</span>
|
|
3052
|
+
</div>
|
|
3053
|
+
</div>
|
|
3054
|
+
<div id="scoring-mode-desc" class="tab-desc visible" style="padding-top:0">
|
|
3055
|
+
<span id="mode-desc-text">Binary: Have you ever committed to this file? Yes (green) or No (red).</span>
|
|
3056
|
+
</div>
|
|
3057
|
+
|
|
3058
|
+
<div id="multiuser-controls">
|
|
3059
|
+
<label>View as:</label>
|
|
3060
|
+
<select id="userSelect" onchange="onUserChange()"></select>
|
|
3061
|
+
</div>
|
|
3062
|
+
|
|
3063
|
+
<div id="hotspot-controls">
|
|
3064
|
+
<label>Mode:</label>
|
|
3065
|
+
<button class="subtab active" data-mode="personal" onclick="switchHotspotMode('personal')">Personal</button>
|
|
3066
|
+
<button class="subtab" data-mode="team" onclick="switchHotspotMode('team')">Team</button>
|
|
3067
|
+
<span class="sep-v"></span>
|
|
3068
|
+
<label>Scoring:</label>
|
|
3069
|
+
<button class="subtab hs-scoring active" data-mode="committed" onclick="switchHotspotScoring('committed')">Committed</button>
|
|
3070
|
+
<button class="subtab hs-scoring" data-mode="code-coverage" onclick="switchHotspotScoring('code-coverage')">Code Coverage</button>
|
|
3071
|
+
<button class="subtab hs-scoring" data-mode="weighted" onclick="switchHotspotScoring('weighted')">Weighted</button>
|
|
3072
|
+
</div>
|
|
3073
|
+
|
|
3074
|
+
<div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
|
|
3075
|
+
|
|
3076
|
+
<div id="content-area">
|
|
3077
|
+
<div id="tab-scoring" class="tab-content active"></div>
|
|
3078
|
+
<div id="tab-coverage" class="tab-content with-sidebar">
|
|
3079
|
+
<div class="viz-area" id="coverage-viz"></div>
|
|
3080
|
+
<div class="sidebar" id="coverage-sidebar">
|
|
3081
|
+
<h3>Risk Files (0-1 contributors)</h3>
|
|
3082
|
+
<div id="risk-list"></div>
|
|
3083
|
+
</div>
|
|
3084
|
+
</div>
|
|
3085
|
+
<div id="tab-multiuser" class="tab-content"></div>
|
|
3086
|
+
<div id="tab-hotspots" class="tab-content with-sidebar">
|
|
3087
|
+
<div class="viz-area" id="hotspot-viz">
|
|
3088
|
+
<div id="zone-labels"></div>
|
|
3089
|
+
</div>
|
|
3090
|
+
<div class="sidebar" id="hotspot-sidebar">
|
|
3091
|
+
<h3>Top Hotspots</h3>
|
|
3092
|
+
<div id="hotspot-list"></div>
|
|
3093
|
+
</div>
|
|
3094
|
+
</div>
|
|
3095
|
+
</div>
|
|
3096
|
+
|
|
3097
|
+
<div id="tooltip"></div>
|
|
3098
|
+
|
|
3099
|
+
<!-- Legends -->
|
|
3100
|
+
<div class="legend active" id="legend-scoring">
|
|
3101
|
+
<div>Familiarity</div>
|
|
3102
|
+
<div class="gradient-bar"></div>
|
|
3103
|
+
<div class="labels"><span>0%</span><span>50%</span><span>100%</span></div>
|
|
3104
|
+
</div>
|
|
3105
|
+
<div class="legend" id="legend-coverage">
|
|
3106
|
+
<div>Contributors</div>
|
|
3107
|
+
<div class="row"><div class="swatch" style="background:var(--color-critical)"></div> 0\u20131 (Risk)</div>
|
|
3108
|
+
<div class="row"><div class="swatch" style="background:var(--color-medium)"></div> 2\u20133 (Moderate)</div>
|
|
3109
|
+
<div class="row"><div class="swatch" style="background:var(--color-safe)"></div> 4+ (Safe)</div>
|
|
3110
|
+
</div>
|
|
3111
|
+
<div class="legend" id="legend-multiuser">
|
|
3112
|
+
<div>Familiarity</div>
|
|
3113
|
+
<div class="gradient-bar"></div>
|
|
3114
|
+
<div class="labels"><span>0%</span><span>50%</span><span>100%</span></div>
|
|
3115
|
+
</div>
|
|
3116
|
+
|
|
3117
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
3118
|
+
<script>
|
|
3119
|
+
// \u2500\u2500 Data \u2500\u2500
|
|
3120
|
+
const scoringData = {
|
|
3121
|
+
committed: ${scoringCommittedJson},
|
|
3122
|
+
'code-coverage': ${scoringCodeCoverageJson},
|
|
3123
|
+
weighted: ${scoringWeightedJson},
|
|
3124
|
+
};
|
|
3125
|
+
const coverageData = ${coverageTreeJson};
|
|
3126
|
+
const coverageRiskFiles = ${coverageRiskJson};
|
|
3127
|
+
const hotspotData = ${hotspotJson};
|
|
3128
|
+
const hotspotTeamFamiliarity = ${hotspotTeamFamJson};
|
|
3129
|
+
const multiUserData = ${multiUserTreeJson};
|
|
3130
|
+
const multiUserNames = ${multiUserNamesJson};
|
|
3131
|
+
const multiUserSummaries = ${multiUserSummariesJson};
|
|
3132
|
+
${getUnifiedScripts()}
|
|
3328
3133
|
</script>
|
|
3329
3134
|
</body>
|
|
3330
3135
|
</html>`;
|
|
@@ -3337,6 +3142,600 @@ async function generateAndOpenUnifiedHTML(data, repoPath) {
|
|
|
3337
3142
|
await openBrowser(outputPath);
|
|
3338
3143
|
}
|
|
3339
3144
|
|
|
3145
|
+
// src/core/demo.ts
|
|
3146
|
+
var REPO_NAME = "acme-web-app";
|
|
3147
|
+
var ALICE = { name: "Alice Chen", email: "alice@acme.dev" };
|
|
3148
|
+
var BOB = { name: "Bob Kim", email: "bob@acme.dev" };
|
|
3149
|
+
var CHARLIE = {
|
|
3150
|
+
name: "Charlie Rivera",
|
|
3151
|
+
email: "charlie@acme.dev"
|
|
3152
|
+
};
|
|
3153
|
+
var DIANA = { name: "Diana Patel", email: "diana@acme.dev" };
|
|
3154
|
+
var USERS = [ALICE, BOB, CHARLIE, DIANA];
|
|
3155
|
+
var DEMO_FILES = [
|
|
3156
|
+
// Root config files
|
|
3157
|
+
{
|
|
3158
|
+
path: "package.json",
|
|
3159
|
+
lines: 25,
|
|
3160
|
+
familiarity: { alice: 0.6, bob: 0.2, charlie: 0.3, diana: 0.8 },
|
|
3161
|
+
written: { alice: true, bob: false, charlie: false, diana: true },
|
|
3162
|
+
contributors: ["Alice Chen", "Diana Patel"],
|
|
3163
|
+
changeFrequency: 1,
|
|
3164
|
+
lastChanged: "2026-02-01"
|
|
3165
|
+
},
|
|
3166
|
+
{
|
|
3167
|
+
path: "tsconfig.json",
|
|
3168
|
+
lines: 15,
|
|
3169
|
+
familiarity: { alice: 0.55, bob: 0.1, charlie: 0.25, diana: 0.75 },
|
|
3170
|
+
written: { alice: true, bob: false, charlie: false, diana: true },
|
|
3171
|
+
contributors: ["Alice Chen", "Diana Patel"],
|
|
3172
|
+
changeFrequency: 0,
|
|
3173
|
+
lastChanged: null
|
|
3174
|
+
},
|
|
3175
|
+
{
|
|
3176
|
+
path: "README.md",
|
|
3177
|
+
lines: 80,
|
|
3178
|
+
familiarity: { alice: 0.4, bob: 0.3, charlie: 0.25, diana: 0.2 },
|
|
3179
|
+
written: { alice: true, bob: true, charlie: false, diana: false },
|
|
3180
|
+
contributors: ["Alice Chen", "Bob Kim", "Charlie Rivera"],
|
|
3181
|
+
changeFrequency: 1,
|
|
3182
|
+
lastChanged: "2026-02-10"
|
|
3183
|
+
},
|
|
3184
|
+
{
|
|
3185
|
+
path: "Dockerfile",
|
|
3186
|
+
lines: 35,
|
|
3187
|
+
familiarity: { alice: 0.3, bob: 0, charlie: 0, diana: 0.9 },
|
|
3188
|
+
written: { alice: false, bob: false, charlie: false, diana: true },
|
|
3189
|
+
contributors: ["Diana Patel"],
|
|
3190
|
+
changeFrequency: 2,
|
|
3191
|
+
lastChanged: "2026-02-20"
|
|
3192
|
+
},
|
|
3193
|
+
// CI/CD
|
|
3194
|
+
{
|
|
3195
|
+
path: ".github/workflows/ci.yml",
|
|
3196
|
+
lines: 65,
|
|
3197
|
+
familiarity: { alice: 0.15, bob: 0, charlie: 0, diana: 0.95 },
|
|
3198
|
+
written: { alice: false, bob: false, charlie: false, diana: true },
|
|
3199
|
+
contributors: ["Diana Patel"],
|
|
3200
|
+
changeFrequency: 3,
|
|
3201
|
+
lastChanged: "2026-02-18"
|
|
3202
|
+
},
|
|
3203
|
+
{
|
|
3204
|
+
path: ".github/workflows/deploy.yml",
|
|
3205
|
+
lines: 45,
|
|
3206
|
+
familiarity: { alice: 0.1, bob: 0, charlie: 0, diana: 0.9 },
|
|
3207
|
+
written: { alice: false, bob: false, charlie: false, diana: true },
|
|
3208
|
+
contributors: ["Diana Patel"],
|
|
3209
|
+
changeFrequency: 2,
|
|
3210
|
+
lastChanged: "2026-02-15"
|
|
3211
|
+
},
|
|
3212
|
+
// src/ core
|
|
3213
|
+
{
|
|
3214
|
+
path: "src/index.ts",
|
|
3215
|
+
lines: 30,
|
|
3216
|
+
familiarity: { alice: 0.85, bob: 0.25, charlie: 0.1, diana: 0.4 },
|
|
3217
|
+
written: { alice: true, bob: true, charlie: false, diana: false },
|
|
3218
|
+
contributors: ["Alice Chen", "Bob Kim"],
|
|
3219
|
+
changeFrequency: 1,
|
|
3220
|
+
lastChanged: "2026-01-20"
|
|
3221
|
+
},
|
|
3222
|
+
{
|
|
3223
|
+
path: "src/config.ts",
|
|
3224
|
+
lines: 55,
|
|
3225
|
+
familiarity: { alice: 0.75, bob: 0.15, charlie: 0.2, diana: 0.6 },
|
|
3226
|
+
written: { alice: true, bob: true, charlie: true, diana: true },
|
|
3227
|
+
contributors: ["Alice Chen", "Bob Kim", "Charlie Rivera", "Diana Patel"],
|
|
3228
|
+
changeFrequency: 3,
|
|
3229
|
+
lastChanged: "2026-02-22"
|
|
3230
|
+
},
|
|
3231
|
+
{
|
|
3232
|
+
path: "src/app.ts",
|
|
3233
|
+
lines: 120,
|
|
3234
|
+
familiarity: { alice: 0.9, bob: 0.1, charlie: 0.1, diana: 0.45 },
|
|
3235
|
+
written: { alice: true, bob: false, charlie: false, diana: true },
|
|
3236
|
+
contributors: ["Alice Chen", "Diana Patel"],
|
|
3237
|
+
changeFrequency: 2,
|
|
3238
|
+
lastChanged: "2026-02-05"
|
|
3239
|
+
},
|
|
3240
|
+
// src/routes
|
|
3241
|
+
{
|
|
3242
|
+
path: "src/routes/auth.ts",
|
|
3243
|
+
lines: 95,
|
|
3244
|
+
familiarity: { alice: 0.9, bob: 0, charlie: 0, diana: 0.1 },
|
|
3245
|
+
written: { alice: true, bob: false, charlie: false, diana: false },
|
|
3246
|
+
contributors: ["Alice Chen"],
|
|
3247
|
+
changeFrequency: 3,
|
|
3248
|
+
lastChanged: "2026-02-12"
|
|
3249
|
+
},
|
|
3250
|
+
{
|
|
3251
|
+
path: "src/routes/users.ts",
|
|
3252
|
+
lines: 130,
|
|
3253
|
+
familiarity: { alice: 0.85, bob: 0.2, charlie: 0, diana: 0.1 },
|
|
3254
|
+
written: { alice: true, bob: true, charlie: false, diana: false },
|
|
3255
|
+
contributors: ["Alice Chen", "Bob Kim"],
|
|
3256
|
+
changeFrequency: 3,
|
|
3257
|
+
lastChanged: "2026-02-14"
|
|
3258
|
+
},
|
|
3259
|
+
{
|
|
3260
|
+
path: "src/routes/products.ts",
|
|
3261
|
+
lines: 180,
|
|
3262
|
+
familiarity: { alice: 0.8, bob: 0.15, charlie: 0, diana: 0.05 },
|
|
3263
|
+
written: { alice: true, bob: true, charlie: false, diana: false },
|
|
3264
|
+
contributors: ["Alice Chen", "Bob Kim"],
|
|
3265
|
+
changeFrequency: 4,
|
|
3266
|
+
lastChanged: "2026-02-19"
|
|
3267
|
+
},
|
|
3268
|
+
{
|
|
3269
|
+
path: "src/routes/orders.ts",
|
|
3270
|
+
lines: 210,
|
|
3271
|
+
familiarity: { alice: 0.25, bob: 0, charlie: 0, diana: 0.05 },
|
|
3272
|
+
written: { alice: true, bob: false, charlie: false, diana: false },
|
|
3273
|
+
contributors: ["Alice Chen"],
|
|
3274
|
+
changeFrequency: 12,
|
|
3275
|
+
lastChanged: "2026-02-25"
|
|
3276
|
+
},
|
|
3277
|
+
// src/middleware
|
|
3278
|
+
{
|
|
3279
|
+
path: "src/middleware/auth.ts",
|
|
3280
|
+
lines: 75,
|
|
3281
|
+
familiarity: { alice: 0.85, bob: 0, charlie: 0, diana: 0.15 },
|
|
3282
|
+
written: { alice: true, bob: false, charlie: false, diana: false },
|
|
3283
|
+
contributors: ["Alice Chen"],
|
|
3284
|
+
changeFrequency: 1,
|
|
3285
|
+
lastChanged: "2026-01-15"
|
|
3286
|
+
},
|
|
3287
|
+
{
|
|
3288
|
+
path: "src/middleware/logging.ts",
|
|
3289
|
+
lines: 50,
|
|
3290
|
+
familiarity: { alice: 0.8, bob: 0, charlie: 0, diana: 0.5 },
|
|
3291
|
+
written: { alice: true, bob: false, charlie: false, diana: true },
|
|
3292
|
+
contributors: ["Alice Chen", "Diana Patel"],
|
|
3293
|
+
changeFrequency: 1,
|
|
3294
|
+
lastChanged: "2026-01-25"
|
|
3295
|
+
},
|
|
3296
|
+
{
|
|
3297
|
+
path: "src/middleware/error-handler.ts",
|
|
3298
|
+
lines: 60,
|
|
3299
|
+
familiarity: { alice: 0.4, bob: 0, charlie: 0, diana: 0.2 },
|
|
3300
|
+
written: { alice: true, bob: false, charlie: false, diana: false },
|
|
3301
|
+
contributors: ["Alice Chen"],
|
|
3302
|
+
changeFrequency: 6,
|
|
3303
|
+
lastChanged: "2026-02-23"
|
|
3304
|
+
},
|
|
3305
|
+
// src/models
|
|
3306
|
+
{
|
|
3307
|
+
path: "src/models/user.ts",
|
|
3308
|
+
lines: 85,
|
|
3309
|
+
familiarity: { alice: 0.88, bob: 0.2, charlie: 0, diana: 0.1 },
|
|
3310
|
+
written: { alice: true, bob: true, charlie: false, diana: false },
|
|
3311
|
+
contributors: ["Alice Chen", "Bob Kim"],
|
|
3312
|
+
changeFrequency: 2,
|
|
3313
|
+
lastChanged: "2026-02-08"
|
|
3314
|
+
},
|
|
3315
|
+
{
|
|
3316
|
+
path: "src/models/product.ts",
|
|
3317
|
+
lines: 90,
|
|
3318
|
+
familiarity: { alice: 0.85, bob: 0, charlie: 0, diana: 0.05 },
|
|
3319
|
+
written: { alice: true, bob: false, charlie: false, diana: false },
|
|
3320
|
+
contributors: ["Alice Chen"],
|
|
3321
|
+
changeFrequency: 1,
|
|
3322
|
+
lastChanged: "2026-01-10"
|
|
3323
|
+
},
|
|
3324
|
+
{
|
|
3325
|
+
path: "src/models/order.ts",
|
|
3326
|
+
lines: 110,
|
|
3327
|
+
familiarity: { alice: 0.7, bob: 0, charlie: 0, diana: 0.05 },
|
|
3328
|
+
written: { alice: true, bob: false, charlie: false, diana: false },
|
|
3329
|
+
contributors: ["Alice Chen"],
|
|
3330
|
+
changeFrequency: 5,
|
|
3331
|
+
lastChanged: "2026-02-21"
|
|
3332
|
+
},
|
|
3333
|
+
// src/services
|
|
3334
|
+
{
|
|
3335
|
+
path: "src/services/email.ts",
|
|
3336
|
+
lines: 70,
|
|
3337
|
+
familiarity: { alice: 0.8, bob: 0, charlie: 0, diana: 0.15 },
|
|
3338
|
+
written: { alice: true, bob: false, charlie: false, diana: false },
|
|
3339
|
+
contributors: ["Alice Chen"],
|
|
3340
|
+
changeFrequency: 1,
|
|
3341
|
+
lastChanged: "2026-01-05"
|
|
3342
|
+
},
|
|
3343
|
+
{
|
|
3344
|
+
path: "src/services/payment.ts",
|
|
3345
|
+
lines: 150,
|
|
3346
|
+
familiarity: { alice: 0.5, bob: 0, charlie: 0, diana: 0.35 },
|
|
3347
|
+
written: { alice: true, bob: false, charlie: false, diana: true },
|
|
3348
|
+
contributors: ["Alice Chen", "Diana Patel"],
|
|
3349
|
+
changeFrequency: 9,
|
|
3350
|
+
lastChanged: "2026-02-24"
|
|
3351
|
+
},
|
|
3352
|
+
{
|
|
3353
|
+
path: "src/services/inventory.ts",
|
|
3354
|
+
lines: 95,
|
|
3355
|
+
familiarity: { alice: 0.55, bob: 0, charlie: 0, diana: 0.1 },
|
|
3356
|
+
written: { alice: true, bob: false, charlie: false, diana: false },
|
|
3357
|
+
contributors: ["Alice Chen"],
|
|
3358
|
+
changeFrequency: 5,
|
|
3359
|
+
lastChanged: "2026-02-17"
|
|
3360
|
+
},
|
|
3361
|
+
// src/utils
|
|
3362
|
+
{
|
|
3363
|
+
path: "src/utils/validators.ts",
|
|
3364
|
+
lines: 65,
|
|
3365
|
+
familiarity: { alice: 0.85, bob: 0.15, charlie: 0.1, diana: 0.2 },
|
|
3366
|
+
written: { alice: true, bob: true, charlie: false, diana: false },
|
|
3367
|
+
contributors: ["Alice Chen", "Bob Kim"],
|
|
3368
|
+
changeFrequency: 1,
|
|
3369
|
+
lastChanged: "2026-01-30"
|
|
3370
|
+
},
|
|
3371
|
+
{
|
|
3372
|
+
path: "src/utils/helpers.ts",
|
|
3373
|
+
lines: 45,
|
|
3374
|
+
familiarity: { alice: 0.8, bob: 0, charlie: 0.05, diana: 0.1 },
|
|
3375
|
+
written: { alice: true, bob: false, charlie: false, diana: false },
|
|
3376
|
+
contributors: ["Alice Chen"],
|
|
3377
|
+
changeFrequency: 0,
|
|
3378
|
+
lastChanged: null
|
|
3379
|
+
},
|
|
3380
|
+
// frontend
|
|
3381
|
+
{
|
|
3382
|
+
path: "frontend/App.tsx",
|
|
3383
|
+
lines: 140,
|
|
3384
|
+
familiarity: { alice: 0.15, bob: 0, charlie: 0.85, diana: 0.05 },
|
|
3385
|
+
written: { alice: true, bob: false, charlie: true, diana: false },
|
|
3386
|
+
contributors: ["Charlie Rivera", "Alice Chen"],
|
|
3387
|
+
changeFrequency: 7,
|
|
3388
|
+
lastChanged: "2026-02-24"
|
|
3389
|
+
},
|
|
3390
|
+
{
|
|
3391
|
+
path: "frontend/index.tsx",
|
|
3392
|
+
lines: 20,
|
|
3393
|
+
familiarity: { alice: 0.1, bob: 0, charlie: 0.8, diana: 0 },
|
|
3394
|
+
written: { alice: false, bob: false, charlie: true, diana: false },
|
|
3395
|
+
contributors: ["Charlie Rivera"],
|
|
3396
|
+
changeFrequency: 1,
|
|
3397
|
+
lastChanged: "2026-01-12"
|
|
3398
|
+
},
|
|
3399
|
+
{
|
|
3400
|
+
path: "frontend/components/Header.tsx",
|
|
3401
|
+
lines: 85,
|
|
3402
|
+
familiarity: { alice: 0.1, bob: 0, charlie: 0.9, diana: 0 },
|
|
3403
|
+
written: { alice: false, bob: false, charlie: true, diana: false },
|
|
3404
|
+
contributors: ["Charlie Rivera"],
|
|
3405
|
+
changeFrequency: 2,
|
|
3406
|
+
lastChanged: "2026-02-03"
|
|
3407
|
+
},
|
|
3408
|
+
{
|
|
3409
|
+
path: "frontend/components/ProductList.tsx",
|
|
3410
|
+
lines: 160,
|
|
3411
|
+
familiarity: { alice: 0.1, bob: 0, charlie: 0.85, diana: 0 },
|
|
3412
|
+
written: { alice: false, bob: false, charlie: true, diana: false },
|
|
3413
|
+
contributors: ["Charlie Rivera"],
|
|
3414
|
+
changeFrequency: 4,
|
|
3415
|
+
lastChanged: "2026-02-16"
|
|
3416
|
+
},
|
|
3417
|
+
{
|
|
3418
|
+
path: "frontend/components/Cart.tsx",
|
|
3419
|
+
lines: 190,
|
|
3420
|
+
familiarity: { alice: 0.08, bob: 0, charlie: 0.92, diana: 0 },
|
|
3421
|
+
written: { alice: false, bob: false, charlie: true, diana: false },
|
|
3422
|
+
contributors: ["Charlie Rivera"],
|
|
3423
|
+
changeFrequency: 8,
|
|
3424
|
+
lastChanged: "2026-02-22"
|
|
3425
|
+
},
|
|
3426
|
+
{
|
|
3427
|
+
path: "frontend/components/Checkout.tsx",
|
|
3428
|
+
lines: 220,
|
|
3429
|
+
familiarity: { alice: 0.05, bob: 0, charlie: 0.95, diana: 0 },
|
|
3430
|
+
written: { alice: false, bob: false, charlie: true, diana: false },
|
|
3431
|
+
contributors: ["Charlie Rivera"],
|
|
3432
|
+
changeFrequency: 10,
|
|
3433
|
+
lastChanged: "2026-02-25"
|
|
3434
|
+
},
|
|
3435
|
+
{
|
|
3436
|
+
path: "frontend/styles/global.css",
|
|
3437
|
+
lines: 120,
|
|
3438
|
+
familiarity: { alice: 0.05, bob: 0, charlie: 0.9, diana: 0 },
|
|
3439
|
+
written: { alice: false, bob: false, charlie: true, diana: false },
|
|
3440
|
+
contributors: ["Charlie Rivera"],
|
|
3441
|
+
changeFrequency: 2,
|
|
3442
|
+
lastChanged: "2026-02-11"
|
|
3443
|
+
}
|
|
3444
|
+
];
|
|
3445
|
+
function buildDemoTree(files, createFolder) {
|
|
3446
|
+
const allDirs = /* @__PURE__ */ new Set();
|
|
3447
|
+
for (const file of files) {
|
|
3448
|
+
const parts = file.path.split("/");
|
|
3449
|
+
for (let i = 1; i < parts.length; i++) {
|
|
3450
|
+
allDirs.add(parts.slice(0, i).join("/"));
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
const dirChildren = /* @__PURE__ */ new Map();
|
|
3454
|
+
for (const file of files) {
|
|
3455
|
+
const lastSlash = file.path.lastIndexOf("/");
|
|
3456
|
+
const parentDir = lastSlash >= 0 ? file.path.substring(0, lastSlash) : "";
|
|
3457
|
+
if (!dirChildren.has(parentDir)) dirChildren.set(parentDir, []);
|
|
3458
|
+
dirChildren.get(parentDir).push(file);
|
|
3459
|
+
}
|
|
3460
|
+
const sortedDirs = [...allDirs].sort(
|
|
3461
|
+
(a, b) => b.split("/").length - a.split("/").length
|
|
3462
|
+
);
|
|
3463
|
+
for (const dir of sortedDirs) {
|
|
3464
|
+
const children = dirChildren.get(dir) || [];
|
|
3465
|
+
const folder = createFolder(dir, children);
|
|
3466
|
+
const lastSlash = dir.lastIndexOf("/");
|
|
3467
|
+
const parentDir = lastSlash >= 0 ? dir.substring(0, lastSlash) : "";
|
|
3468
|
+
if (!dirChildren.has(parentDir)) dirChildren.set(parentDir, []);
|
|
3469
|
+
dirChildren.get(parentDir).push(folder);
|
|
3470
|
+
}
|
|
3471
|
+
const rootChildren = dirChildren.get("") || [];
|
|
3472
|
+
return createFolder("", rootChildren);
|
|
3473
|
+
}
|
|
3474
|
+
function sortChildren(children) {
|
|
3475
|
+
return [...children].sort((a, b) => {
|
|
3476
|
+
if (a.type !== b.type) return a.type === "folder" ? -1 : 1;
|
|
3477
|
+
return a.path.localeCompare(b.path);
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
function buildScoringTree(files, mode) {
|
|
3481
|
+
return buildDemoTree(files, (path, children) => {
|
|
3482
|
+
let totalLines = 0;
|
|
3483
|
+
let weightedScore = 0;
|
|
3484
|
+
let fileCount = 0;
|
|
3485
|
+
let readCount = 0;
|
|
3486
|
+
for (const child of children) {
|
|
3487
|
+
totalLines += child.lines;
|
|
3488
|
+
weightedScore += child.lines * child.score;
|
|
3489
|
+
if (child.type === "file") {
|
|
3490
|
+
fileCount++;
|
|
3491
|
+
if (child.score > 0) readCount++;
|
|
3492
|
+
} else {
|
|
3493
|
+
fileCount += child.fileCount;
|
|
3494
|
+
readCount += child.readCount || 0;
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
const folder = {
|
|
3498
|
+
type: "folder",
|
|
3499
|
+
path,
|
|
3500
|
+
lines: totalLines,
|
|
3501
|
+
score: totalLines > 0 ? weightedScore / totalLines : 0,
|
|
3502
|
+
fileCount,
|
|
3503
|
+
children: sortChildren(children)
|
|
3504
|
+
};
|
|
3505
|
+
if (mode === "committed") folder.readCount = readCount;
|
|
3506
|
+
return folder;
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
function getDemoFamiliarityResult(mode) {
|
|
3510
|
+
const m = mode || "committed";
|
|
3511
|
+
const files = DEMO_FILES.map((file) => {
|
|
3512
|
+
const base = {
|
|
3513
|
+
type: "file",
|
|
3514
|
+
path: file.path,
|
|
3515
|
+
lines: file.lines,
|
|
3516
|
+
score: 0
|
|
3517
|
+
};
|
|
3518
|
+
switch (m) {
|
|
3519
|
+
case "committed":
|
|
3520
|
+
base.score = file.written.alice ? 1 : 0;
|
|
3521
|
+
base.isWritten = file.written.alice;
|
|
3522
|
+
break;
|
|
3523
|
+
case "code-coverage":
|
|
3524
|
+
base.score = file.familiarity.alice;
|
|
3525
|
+
break;
|
|
3526
|
+
case "weighted":
|
|
3527
|
+
base.blameScore = file.familiarity.alice;
|
|
3528
|
+
base.commitScore = Math.min(
|
|
3529
|
+
1,
|
|
3530
|
+
file.written.alice ? file.familiarity.alice * 0.9 + 0.1 : 0
|
|
3531
|
+
);
|
|
3532
|
+
base.score = 0.5 * base.blameScore + 0.5 * base.commitScore;
|
|
3533
|
+
break;
|
|
3534
|
+
}
|
|
3535
|
+
return base;
|
|
3536
|
+
});
|
|
3537
|
+
const tree = buildScoringTree(files, m);
|
|
3538
|
+
const writtenCount = DEMO_FILES.filter((file) => file.written.alice).length;
|
|
3539
|
+
return {
|
|
3540
|
+
tree,
|
|
3541
|
+
repoName: REPO_NAME,
|
|
3542
|
+
userName: ALICE.name,
|
|
3543
|
+
mode: m,
|
|
3544
|
+
writtenCount,
|
|
3545
|
+
totalFiles: DEMO_FILES.length
|
|
3546
|
+
};
|
|
3547
|
+
}
|
|
3548
|
+
function getDemoHotspotResult() {
|
|
3549
|
+
const maxFreq = Math.max(...DEMO_FILES.map((file) => file.changeFrequency));
|
|
3550
|
+
const files = DEMO_FILES.map((file) => {
|
|
3551
|
+
const normalizedFreq = maxFreq > 0 ? file.changeFrequency / maxFreq : 0;
|
|
3552
|
+
const familiarity = file.familiarity.alice;
|
|
3553
|
+
const risk = normalizedFreq * (1 - familiarity);
|
|
3554
|
+
return {
|
|
3555
|
+
path: file.path,
|
|
3556
|
+
lines: file.lines,
|
|
3557
|
+
familiarity,
|
|
3558
|
+
changeFrequency: file.changeFrequency,
|
|
3559
|
+
lastChanged: file.lastChanged ? new Date(file.lastChanged) : null,
|
|
3560
|
+
risk,
|
|
3561
|
+
riskLevel: classifyHotspotRisk(risk)
|
|
3562
|
+
};
|
|
3563
|
+
}).sort((a, b) => b.risk - a.risk);
|
|
3564
|
+
const summary = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
3565
|
+
for (const hotspot of files) summary[hotspot.riskLevel]++;
|
|
3566
|
+
return {
|
|
3567
|
+
files,
|
|
3568
|
+
repoName: REPO_NAME,
|
|
3569
|
+
userName: ALICE.name,
|
|
3570
|
+
hotspotMode: "personal",
|
|
3571
|
+
timeWindow: DEFAULT_HOTSPOT_WINDOW,
|
|
3572
|
+
summary
|
|
3573
|
+
};
|
|
3574
|
+
}
|
|
3575
|
+
function buildCoverageTree2(files) {
|
|
3576
|
+
return buildDemoTree(
|
|
3577
|
+
files,
|
|
3578
|
+
(path, children) => {
|
|
3579
|
+
let totalLines = 0;
|
|
3580
|
+
let fileCount = 0;
|
|
3581
|
+
let totalContributors = 0;
|
|
3582
|
+
for (const child of children) {
|
|
3583
|
+
totalLines += child.lines;
|
|
3584
|
+
if (child.type === "file") {
|
|
3585
|
+
fileCount++;
|
|
3586
|
+
totalContributors += child.contributorCount;
|
|
3587
|
+
} else {
|
|
3588
|
+
const folderChild = child;
|
|
3589
|
+
fileCount += folderChild.fileCount;
|
|
3590
|
+
totalContributors += folderChild.avgContributors * folderChild.fileCount;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
const avgContributors = fileCount > 0 ? Math.round(totalContributors / fileCount * 10) / 10 : 0;
|
|
3594
|
+
const busFactor = avgContributors >= 4 ? 3 : avgContributors >= 2 ? 2 : 1;
|
|
3595
|
+
return {
|
|
3596
|
+
type: "folder",
|
|
3597
|
+
path,
|
|
3598
|
+
lines: totalLines,
|
|
3599
|
+
fileCount,
|
|
3600
|
+
avgContributors,
|
|
3601
|
+
busFactor,
|
|
3602
|
+
riskLevel: busFactor <= 1 ? "risk" : busFactor <= 2 ? "moderate" : "safe",
|
|
3603
|
+
children: sortChildren(children)
|
|
3604
|
+
};
|
|
3605
|
+
}
|
|
3606
|
+
);
|
|
3607
|
+
}
|
|
3608
|
+
function getDemoCoverageResult() {
|
|
3609
|
+
const coverageFiles = DEMO_FILES.map((file) => ({
|
|
3610
|
+
type: "file",
|
|
3611
|
+
path: file.path,
|
|
3612
|
+
lines: file.lines,
|
|
3613
|
+
contributorCount: file.contributors.length,
|
|
3614
|
+
contributors: file.contributors,
|
|
3615
|
+
riskLevel: classifyCoverageRisk(file.contributors.length)
|
|
3616
|
+
}));
|
|
3617
|
+
const tree = buildCoverageTree2(coverageFiles);
|
|
3618
|
+
const riskFiles = coverageFiles.filter((file) => file.contributorCount <= 1).sort((a, b) => a.contributorCount - b.contributorCount);
|
|
3619
|
+
return {
|
|
3620
|
+
tree,
|
|
3621
|
+
repoName: REPO_NAME,
|
|
3622
|
+
totalContributors: USERS.length,
|
|
3623
|
+
totalFiles: DEMO_FILES.length,
|
|
3624
|
+
riskFiles,
|
|
3625
|
+
overallBusFactor: 1
|
|
3626
|
+
};
|
|
3627
|
+
}
|
|
3628
|
+
function buildMultiUserTree(files) {
|
|
3629
|
+
return buildDemoTree(
|
|
3630
|
+
files,
|
|
3631
|
+
(path, children) => {
|
|
3632
|
+
let totalLines = 0;
|
|
3633
|
+
let fileCount = 0;
|
|
3634
|
+
const userTotals = USERS.map(() => 0);
|
|
3635
|
+
for (const child of children) {
|
|
3636
|
+
totalLines += child.lines;
|
|
3637
|
+
if (child.type === "file") {
|
|
3638
|
+
fileCount++;
|
|
3639
|
+
const fileNode = child;
|
|
3640
|
+
fileNode.userScores.forEach((userScore, i) => {
|
|
3641
|
+
userTotals[i] += userScore.score * fileNode.lines;
|
|
3642
|
+
});
|
|
3643
|
+
} else {
|
|
3644
|
+
const folderNode = child;
|
|
3645
|
+
fileCount += folderNode.fileCount;
|
|
3646
|
+
folderNode.userScores.forEach((userScore, i) => {
|
|
3647
|
+
userTotals[i] += userScore.score * folderNode.lines;
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
const userScores = USERS.map((user, i) => ({
|
|
3652
|
+
user,
|
|
3653
|
+
score: totalLines > 0 ? userTotals[i] / totalLines : 0
|
|
3654
|
+
}));
|
|
3655
|
+
const avgScore = userScores.reduce((sum, entry) => sum + entry.score, 0) / userScores.length;
|
|
3656
|
+
return {
|
|
3657
|
+
type: "folder",
|
|
3658
|
+
path,
|
|
3659
|
+
lines: totalLines,
|
|
3660
|
+
score: avgScore,
|
|
3661
|
+
fileCount,
|
|
3662
|
+
userScores,
|
|
3663
|
+
children: sortChildren(children)
|
|
3664
|
+
};
|
|
3665
|
+
}
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
function getDemoMultiUserResult() {
|
|
3669
|
+
const files = DEMO_FILES.map((file) => {
|
|
3670
|
+
const scores = [
|
|
3671
|
+
file.familiarity.alice,
|
|
3672
|
+
file.familiarity.bob,
|
|
3673
|
+
file.familiarity.charlie,
|
|
3674
|
+
file.familiarity.diana
|
|
3675
|
+
];
|
|
3676
|
+
const userScores = USERS.map((user, i) => ({
|
|
3677
|
+
user,
|
|
3678
|
+
score: scores[i],
|
|
3679
|
+
isWritten: [
|
|
3680
|
+
file.written.alice,
|
|
3681
|
+
file.written.bob,
|
|
3682
|
+
file.written.charlie,
|
|
3683
|
+
file.written.diana
|
|
3684
|
+
][i]
|
|
3685
|
+
}));
|
|
3686
|
+
return {
|
|
3687
|
+
type: "file",
|
|
3688
|
+
path: file.path,
|
|
3689
|
+
lines: file.lines,
|
|
3690
|
+
score: scores.reduce((a, b) => a + b, 0) / scores.length,
|
|
3691
|
+
userScores
|
|
3692
|
+
};
|
|
3693
|
+
});
|
|
3694
|
+
const tree = buildMultiUserTree(files);
|
|
3695
|
+
const userSummaries = USERS.map((user, i) => {
|
|
3696
|
+
const key = ["alice", "bob", "charlie", "diana"][i];
|
|
3697
|
+
const writtenCount = DEMO_FILES.filter((file) => file.written[key]).length;
|
|
3698
|
+
const totalLines = DEMO_FILES.reduce((sum, file) => sum + file.lines, 0);
|
|
3699
|
+
const weightedScore = DEMO_FILES.reduce(
|
|
3700
|
+
(sum, file) => sum + file.familiarity[key] * file.lines,
|
|
3701
|
+
0
|
|
3702
|
+
);
|
|
3703
|
+
return {
|
|
3704
|
+
user,
|
|
3705
|
+
writtenCount,
|
|
3706
|
+
overallScore: totalLines > 0 ? weightedScore / totalLines : 0
|
|
3707
|
+
};
|
|
3708
|
+
});
|
|
3709
|
+
return {
|
|
3710
|
+
tree,
|
|
3711
|
+
repoName: REPO_NAME,
|
|
3712
|
+
users: USERS,
|
|
3713
|
+
mode: "committed",
|
|
3714
|
+
totalFiles: DEMO_FILES.length,
|
|
3715
|
+
userSummaries
|
|
3716
|
+
};
|
|
3717
|
+
}
|
|
3718
|
+
function getDemoUnifiedData() {
|
|
3719
|
+
const hotspotTeamFamiliarity = {};
|
|
3720
|
+
for (const file of DEMO_FILES) {
|
|
3721
|
+
const avg = (file.familiarity.alice + file.familiarity.bob + file.familiarity.charlie + file.familiarity.diana) / 4;
|
|
3722
|
+
hotspotTeamFamiliarity[file.path] = avg;
|
|
3723
|
+
}
|
|
3724
|
+
return {
|
|
3725
|
+
repoName: REPO_NAME,
|
|
3726
|
+
userName: ALICE.name,
|
|
3727
|
+
scoring: {
|
|
3728
|
+
committed: getDemoFamiliarityResult("committed"),
|
|
3729
|
+
codeCoverage: getDemoFamiliarityResult("code-coverage"),
|
|
3730
|
+
weighted: getDemoFamiliarityResult("weighted")
|
|
3731
|
+
},
|
|
3732
|
+
coverage: getDemoCoverageResult(),
|
|
3733
|
+
hotspot: getDemoHotspotResult(),
|
|
3734
|
+
hotspotTeamFamiliarity,
|
|
3735
|
+
multiUser: getDemoMultiUserResult()
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
|
|
3340
3739
|
// src/cli/index.ts
|
|
3341
3740
|
var require2 = createRequire(import.meta.url);
|
|
3342
3741
|
var pkg = require2("../../package.json");
|
|
@@ -3365,49 +3764,43 @@ function createProgram() {
|
|
|
3365
3764
|
"--contributors-per-file",
|
|
3366
3765
|
"Analyze number of contributors per file (bus factor)",
|
|
3367
3766
|
false
|
|
3368
|
-
).option("--contributors", "Alias for --contributors-per-file").option("--team-coverage", "Deprecated alias for --contributors-per-file").option("--hotspot [mode]", "Hotspot analysis: personal (default) or team").option("--since <days>", "Hotspot analysis period in days (default: 90)").option("--window <days>", "Deprecated alias for --since").action(async (rawOptions) => {
|
|
3767
|
+
).option("--contributors", "Alias for --contributors-per-file").option("--team-coverage", "Deprecated alias for --contributors-per-file").option("--hotspot [mode]", "Hotspot analysis: personal (default) or team").option("--since <days>", "Hotspot analysis period in days (default: 90)").option("--window <days>", "Deprecated alias for --since").option("--demo", "Show demo with sample data (no git repo needed)", false).action(async (rawOptions) => {
|
|
3369
3768
|
try {
|
|
3370
3769
|
const repoPath = process.cwd();
|
|
3371
3770
|
const options = parseOptions(rawOptions, repoPath);
|
|
3372
|
-
const
|
|
3373
|
-
|
|
3374
|
-
|
|
3771
|
+
const isDemo = options.demo;
|
|
3772
|
+
const isMultiUser = options.team || Array.isArray(options.user) && options.user.length > 1;
|
|
3773
|
+
if (options.html && !options.hotspot && !options.contributorsPerFile && !isMultiUser) {
|
|
3774
|
+
const data = isDemo ? getDemoUnifiedData() : await computeUnified(options);
|
|
3375
3775
|
await generateAndOpenUnifiedHTML(data, repoPath);
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
if (options.hotspot) {
|
|
3379
|
-
const result2 = await computeHotspots(options);
|
|
3776
|
+
} else if (options.hotspot) {
|
|
3777
|
+
const result = isDemo ? getDemoHotspotResult() : await computeHotspots(options);
|
|
3380
3778
|
if (options.html) {
|
|
3381
|
-
await generateAndOpenHotspotHTML(
|
|
3779
|
+
await generateAndOpenHotspotHTML(result, repoPath);
|
|
3382
3780
|
} else {
|
|
3383
|
-
renderHotspotTerminal(
|
|
3781
|
+
renderHotspotTerminal(result);
|
|
3384
3782
|
}
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
if (options.contributorsPerFile) {
|
|
3388
|
-
const result2 = await computeTeamCoverage(options);
|
|
3783
|
+
} else if (options.contributorsPerFile) {
|
|
3784
|
+
const result = isDemo ? getDemoCoverageResult() : await computeTeamCoverage(options);
|
|
3389
3785
|
if (options.html) {
|
|
3390
|
-
await generateAndOpenCoverageHTML(
|
|
3786
|
+
await generateAndOpenCoverageHTML(result, repoPath);
|
|
3391
3787
|
} else {
|
|
3392
|
-
renderCoverageTerminal(
|
|
3788
|
+
renderCoverageTerminal(result);
|
|
3393
3789
|
}
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
const isMultiUser = options.team || Array.isArray(options.user) && options.user.length > 1;
|
|
3397
|
-
if (isMultiUser) {
|
|
3398
|
-
const result2 = await computeMultiUser(options);
|
|
3790
|
+
} else if (isMultiUser) {
|
|
3791
|
+
const result = isDemo ? getDemoMultiUserResult() : await computeMultiUser(options);
|
|
3399
3792
|
if (options.html) {
|
|
3400
|
-
await generateAndOpenMultiUserHTML(
|
|
3793
|
+
await generateAndOpenMultiUserHTML(result, repoPath);
|
|
3401
3794
|
} else {
|
|
3402
|
-
renderMultiUserTerminal(
|
|
3795
|
+
renderMultiUserTerminal(result);
|
|
3403
3796
|
}
|
|
3404
|
-
return;
|
|
3405
|
-
}
|
|
3406
|
-
const result = await computeFamiliarity(options);
|
|
3407
|
-
if (options.html) {
|
|
3408
|
-
await generateAndOpenHTML(result, repoPath);
|
|
3409
3797
|
} else {
|
|
3410
|
-
|
|
3798
|
+
const result = isDemo ? getDemoFamiliarityResult(options.mode) : await computeFamiliarity(options);
|
|
3799
|
+
if (options.html) {
|
|
3800
|
+
await generateAndOpenHTML(result, repoPath);
|
|
3801
|
+
} else {
|
|
3802
|
+
renderTerminal(result);
|
|
3803
|
+
}
|
|
3411
3804
|
}
|
|
3412
3805
|
} catch (error) {
|
|
3413
3806
|
console.error(`Error: ${error.message}`);
|