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.
@@ -8,7 +8,7 @@ import {
8
8
  processBatch,
9
9
  resolveUser,
10
10
  walkFiles
11
- } from "../chunk-6XUJIHN3.js";
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 * BAR_WIDTH);
98
- const empty = BAR_WIDTH - filled;
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}${chalk.bold(name)}${padding} ${bar} ${pct.padStart(4)} (${readCount}/${folder.fileCount} files)`
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}${chalk.bold(name)}${padding} ${bar} ${pct.padStart(4)}`
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
- chalk.bold(`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)})`)
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 generateTreemapHTML(result) {
202
- const dataJson = JSON.stringify(result.tree);
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: 300px;
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
- function getNodeScore(node) {
306
- return node.score;
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(getNodeScore(d.data.data));
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 = getNodeScore(data);
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(gitClient, trackedFiles);
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: classifyRisk(names.length)
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({ type: "folder", path: "", lines: 0, fileCount: 0, avgContributors: 0, busFactor: 0, riskLevel: "safe", children }, (f) => {
601
- fileScores.push(f);
602
- });
603
- const totalContributors = fileScores.reduce((sum, f) => sum + f.contributorCount, 0);
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: classifyRisk(busFactor),
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 * 0.5);
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 chalk2 from "chalk";
727
+ import chalk3 from "chalk";
673
728
  function riskBadge(level) {
674
729
  switch (level) {
675
730
  case "risk":
676
- return chalk2.bgRed.white(" RISK ");
731
+ return chalk3.bgRed.white(" RISK ");
677
732
  case "moderate":
678
- return chalk2.bgYellow.black(" MOD ");
733
+ return chalk3.bgYellow.black(" MOD ");
679
734
  case "safe":
680
- return chalk2.bgGreen.black(" SAFE ");
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 chalk2.red;
743
+ return chalk3.red;
689
744
  case "moderate":
690
- return chalk2.yellow;
745
+ return chalk3.yellow;
691
746
  default:
692
- return chalk2.green;
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}${chalk2.bold(name.padEnd(24))} ${String(child.avgContributors).padStart(4)} avg ${String(child.busFactor).padStart(2)} ${riskBadge(child.riskLevel)}`
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
- chalk2.bold(
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 ? chalk2.red : result.overallBusFactor <= 2 ? chalk2.yellow : chalk2.green;
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(chalk2.red.bold(`Risk Files (0-1 contributors):`));
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 ? chalk2.red("0 people") : chalk2.yellow(`1 person (${names})`);
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
- chalk2.gray(` ... and ${result.riskFiles.length - 20} more`)
793
+ chalk3.gray(` ... and ${result.riskFiles.length - 20} more`)
739
794
  );
740
795
  }
741
796
  console.log("");
742
797
  } else {
743
- console.log(chalk2.green("No high-risk files found."));
798
+ console.log(chalk3.green("No high-risk files found."));
744
799
  console.log("");
745
800
  }
746
- console.log(chalk2.bold("Folder Coverage:"));
801
+ console.log(chalk3.bold("Folder Coverage:"));
747
802
  console.log(
748
- chalk2.gray(
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
- * { margin: 0; padding: 0; box-sizing: border-box; }
773
- body {
774
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
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
- #tooltip {
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
- function findNode(node, path) {
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 chalk3 from "chalk";
1203
- var BAR_WIDTH2 = 20;
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) => formatPercent2(s.score).padStart(5)).join(" ");
1246
- lines.push(`${prefix}${chalk3.bold(displayName)} ${scores}`);
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
- chalk3.bold(
1259
- `GitFamiliar \u2014 ${repoName} (${getModeLabel2(mode)}, ${userSummaries.length} users)`
1198
+ chalk4.bold(
1199
+ `GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)}, ${userSummaries.length} users)`
1260
1200
  )
1261
1201
  );
1262
1202
  console.log("");
1263
- console.log(chalk3.bold("Overall:"));
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 = makeBar2(summary.overallScore);
1267
- const pct = formatPercent2(summary.overallScore);
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(chalk3.bold("Folders:") + " ".repeat(nameWidth - 4) + headerNames);
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
- * { margin: 0; padding: 0; box-sizing: border-box; }
1302
- body {
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
- #tooltip {
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
- function findNode(node, path) {
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 || DEFAULT_WINDOW;
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 chalk4 from "chalk";
1563
+ import chalk5 from "chalk";
1735
1564
  function riskBadge2(level) {
1736
1565
  switch (level) {
1737
1566
  case "critical":
1738
- return chalk4.bgRed.white.bold(" CRIT ");
1567
+ return chalk5.bgRed.white.bold(" CRIT ");
1739
1568
  case "high":
1740
- return chalk4.bgRedBright.white(" HIGH ");
1569
+ return chalk5.bgRedBright.white(" HIGH ");
1741
1570
  case "medium":
1742
- return chalk4.bgYellow.black(" MED ");
1571
+ return chalk5.bgYellow.black(" MED ");
1743
1572
  case "low":
1744
- return chalk4.bgGreen.black(" LOW ");
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 chalk4.red;
1579
+ return chalk5.red;
1751
1580
  case "high":
1752
- return chalk4.redBright;
1581
+ return chalk5.redBright;
1753
1582
  case "medium":
1754
- return chalk4.yellow;
1583
+ return chalk5.yellow;
1755
1584
  case "low":
1756
- return chalk4.green;
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
- chalk4.bold(`GitFamiliar \u2014 ${modeLabel}${userLabel} \u2014 ${repoName}`)
1594
+ chalk5.bold(`GitFamiliar \u2014 ${modeLabel}${userLabel} \u2014 ${repoName}`)
1766
1595
  );
1767
- console.log(chalk4.gray(` Time window: last ${timeWindow} days`));
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(chalk4.gray(" No files changed in the time window."));
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
- chalk4.gray(
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(chalk4.gray(" " + "\u2500".repeat(90)));
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
- chalk4.gray(` ... and ${activeFiles.length - displayCount} more files`)
1627
+ chalk5.gray(` ... and ${activeFiles.length - displayCount} more files`)
1799
1628
  );
1800
1629
  }
1801
1630
  console.log("");
1802
- console.log(chalk4.bold("Summary:"));
1631
+ console.log(chalk5.bold("Summary:"));
1803
1632
  if (summary.critical > 0) {
1804
1633
  console.log(
1805
- ` ${chalk4.red.bold(`\u{1F534} Critical Risk: ${summary.critical} files`)}`
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
- ` ${chalk4.redBright(`\u{1F7E0} High Risk: ${summary.high} files`)}`
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
- ` ${chalk4.yellow(`\u{1F7E1} Medium Risk: ${summary.medium} files`)}`
1644
+ ` ${chalk5.yellow(`\u{1F7E1} Medium Risk: ${summary.medium} files`)}`
1816
1645
  );
1817
1646
  }
1818
1647
  console.log(
1819
- ` ${chalk4.green(`\u{1F7E2} Low Risk: ${summary.low} files`)}`
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
- chalk4.gray(
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
- * { margin: 0; padding: 0; box-sizing: border-box; }
1861
- body {
1862
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1863
- background: #1a1a2e;
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
- function generateUnifiedHTML(data) {
2163
- const scoringCommittedJson = JSON.stringify(data.scoring.committed.tree);
2164
- const scoringCodeCoverageJson = JSON.stringify(
2165
- data.scoring.codeCoverage.tree
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
- </style>
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 isMultiUserCheck = options.team || Array.isArray(options.user) && options.user.length > 1;
3373
- if (options.html && !options.hotspot && !options.contributorsPerFile && !isMultiUserCheck) {
3374
- const data = await computeUnified(options);
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
- return;
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(result2, repoPath);
3779
+ await generateAndOpenHotspotHTML(result, repoPath);
3382
3780
  } else {
3383
- renderHotspotTerminal(result2);
3781
+ renderHotspotTerminal(result);
3384
3782
  }
3385
- return;
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(result2, repoPath);
3786
+ await generateAndOpenCoverageHTML(result, repoPath);
3391
3787
  } else {
3392
- renderCoverageTerminal(result2);
3788
+ renderCoverageTerminal(result);
3393
3789
  }
3394
- return;
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(result2, repoPath);
3793
+ await generateAndOpenMultiUserHTML(result, repoPath);
3401
3794
  } else {
3402
- renderMultiUserTerminal(result2);
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
- renderTerminal(result);
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}`);