gitfamiliar 0.8.0 → 0.10.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,9 +8,10 @@ import {
8
8
  processBatch,
9
9
  resolveUser,
10
10
  walkFiles
11
- } from "../chunk-R5MGQGFI.js";
11
+ } from "../chunk-6XUJIHN3.js";
12
12
 
13
13
  // src/cli/index.ts
14
+ import { createRequire } from "module";
14
15
  import { Command } from "commander";
15
16
 
16
17
  // src/core/types.ts
@@ -24,7 +25,7 @@ var DEFAULT_EXPIRATION = {
24
25
 
25
26
  // src/cli/options.ts
26
27
  function parseOptions(raw, repoPath) {
27
- const mode = validateMode(raw.mode || "binary");
28
+ const mode = validateMode(raw.mode || "committed");
28
29
  let weights = DEFAULT_WEIGHTS;
29
30
  if (raw.weights) {
30
31
  weights = parseWeights(raw.weights);
@@ -44,7 +45,8 @@ function parseOptions(raw, repoPath) {
44
45
  hotspot = "personal";
45
46
  }
46
47
  }
47
- const windowDays = raw.window ? parseInt(raw.window, 10) : void 0;
48
+ const sinceRaw = raw.since || raw.window;
49
+ const sinceDays = sinceRaw ? parseInt(sinceRaw, 10) : void 0;
48
50
  return {
49
51
  mode,
50
52
  user,
@@ -53,13 +55,20 @@ function parseOptions(raw, repoPath) {
53
55
  weights,
54
56
  repoPath,
55
57
  team: raw.team || false,
56
- teamCoverage: raw.teamCoverage || false,
58
+ contributorsPerFile: raw.contributorsPerFile || raw.contributors || raw.teamCoverage || false,
57
59
  hotspot,
58
- window: windowDays
60
+ since: sinceDays
59
61
  };
60
62
  }
63
+ var MODE_ALIASES = {
64
+ binary: "committed",
65
+ authorship: "code-coverage"
66
+ };
61
67
  function validateMode(mode) {
62
- const valid = ["binary", "authorship", "weighted"];
68
+ if (mode in MODE_ALIASES) {
69
+ return MODE_ALIASES[mode];
70
+ }
71
+ const valid = ["committed", "code-coverage", "weighted"];
63
72
  if (!valid.includes(mode)) {
64
73
  throw new Error(
65
74
  `Invalid mode: "${mode}". Valid modes: ${valid.join(", ")}`
@@ -98,10 +107,10 @@ function formatPercent(score) {
98
107
  }
99
108
  function getModeLabel(mode) {
100
109
  switch (mode) {
101
- case "binary":
102
- return "Binary mode";
103
- case "authorship":
104
- return "Authorship mode";
110
+ case "committed":
111
+ return "Committed mode";
112
+ case "code-coverage":
113
+ return "Code Coverage mode";
105
114
  case "weighted":
106
115
  return "Weighted mode";
107
116
  default:
@@ -128,7 +137,7 @@ function renderFolder(node, indent, mode, maxDepth) {
128
137
  NAME_COLUMN_WIDTH - prefixWidth - name.length
129
138
  );
130
139
  const padding = " ".repeat(padWidth);
131
- if (mode === "binary") {
140
+ if (mode === "committed") {
132
141
  const readCount = folder.readCount || 0;
133
142
  lines.push(
134
143
  `${prefix}${chalk.bold(name)}${padding} ${bar} ${pct.padStart(4)} (${readCount}/${folder.fileCount} files)`
@@ -152,7 +161,7 @@ function renderTerminal(result) {
152
161
  chalk.bold(`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)})`)
153
162
  );
154
163
  console.log("");
155
- if (mode === "binary") {
164
+ if (mode === "committed") {
156
165
  const readCount = tree.readCount || 0;
157
166
  const pct = formatPercent(tree.score);
158
167
  console.log(`Overall: ${readCount}/${tree.fileCount} files (${pct})`);
@@ -166,7 +175,7 @@ function renderTerminal(result) {
166
175
  console.log(line);
167
176
  }
168
177
  console.log("");
169
- if (mode === "binary") {
178
+ if (mode === "committed") {
170
179
  const { writtenCount } = result;
171
180
  console.log(`Written: ${writtenCount} files`);
172
181
  console.log("");
@@ -1086,7 +1095,7 @@ async function computeMultiUser(options) {
1086
1095
  ...options,
1087
1096
  user: userName,
1088
1097
  team: false,
1089
- teamCoverage: false
1098
+ contributorsPerFile: false
1090
1099
  };
1091
1100
  const result = await computeFamiliarity(userOptions);
1092
1101
  results.push({ userName, result });
@@ -1208,10 +1217,10 @@ function formatPercent2(score) {
1208
1217
  }
1209
1218
  function getModeLabel2(mode) {
1210
1219
  switch (mode) {
1211
- case "binary":
1212
- return "Binary mode";
1213
- case "authorship":
1214
- return "Authorship mode";
1220
+ case "committed":
1221
+ return "Committed mode";
1222
+ case "code-coverage":
1223
+ return "Code Coverage mode";
1215
1224
  case "weighted":
1216
1225
  return "Weighted mode";
1217
1226
  default:
@@ -1256,7 +1265,7 @@ function renderMultiUserTerminal(result) {
1256
1265
  const name = truncateName(summary.user.name, 14).padEnd(14);
1257
1266
  const bar = makeBar2(summary.overallScore);
1258
1267
  const pct = formatPercent2(summary.overallScore);
1259
- if (mode === "binary") {
1268
+ if (mode === "committed") {
1260
1269
  console.log(
1261
1270
  ` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount}/${totalFiles} files)`
1262
1271
  );
@@ -1626,18 +1635,30 @@ async function computeHotspots(options) {
1626
1635
  const repoRoot = await gitClient.getRepoRoot();
1627
1636
  const filter = createFilter(repoRoot);
1628
1637
  const tree = await buildFileTree(gitClient, filter);
1629
- const timeWindow = options.window || DEFAULT_WINDOW;
1638
+ const timeWindow = options.since || DEFAULT_WINDOW;
1630
1639
  const isTeamMode = options.hotspot === "team";
1631
1640
  const trackedFiles = /* @__PURE__ */ new Set();
1632
1641
  walkFiles(tree, (f) => trackedFiles.add(f.path));
1633
- const changeFreqMap = await bulkGetChangeFrequency(gitClient, timeWindow, trackedFiles);
1642
+ const changeFreqMap = await bulkGetChangeFrequency(
1643
+ gitClient,
1644
+ timeWindow,
1645
+ trackedFiles
1646
+ );
1634
1647
  let familiarityMap;
1635
1648
  let userName;
1636
1649
  if (isTeamMode) {
1637
- familiarityMap = await computeTeamAvgFamiliarity(gitClient, trackedFiles, options);
1650
+ familiarityMap = await computeTeamAvgFamiliarity(
1651
+ gitClient,
1652
+ trackedFiles,
1653
+ options
1654
+ );
1638
1655
  } else {
1639
1656
  const userFlag = Array.isArray(options.user) ? options.user[0] : options.user;
1640
- const result = await computeFamiliarity({ ...options, team: false, teamCoverage: false });
1657
+ const result = await computeFamiliarity({
1658
+ ...options,
1659
+ team: false,
1660
+ contributorsPerFile: false
1661
+ });
1641
1662
  userName = result.userName;
1642
1663
  familiarityMap = /* @__PURE__ */ new Map();
1643
1664
  walkFiles(result.tree, (f) => {
@@ -1693,12 +1714,18 @@ function classifyHotspotRisk(risk) {
1693
1714
  async function computeTeamAvgFamiliarity(gitClient, trackedFiles, options) {
1694
1715
  const contributors = await getAllContributors(gitClient, 1);
1695
1716
  const totalContributors = Math.max(1, contributors.length);
1696
- const fileContributors = await bulkGetFileContributors(gitClient, trackedFiles);
1717
+ const fileContributors = await bulkGetFileContributors(
1718
+ gitClient,
1719
+ trackedFiles
1720
+ );
1697
1721
  const result = /* @__PURE__ */ new Map();
1698
1722
  for (const filePath of trackedFiles) {
1699
1723
  const contribs = fileContributors.get(filePath);
1700
1724
  const count = contribs ? contribs.size : 0;
1701
- result.set(filePath, Math.min(1, count / Math.max(1, totalContributors * 0.3)));
1725
+ result.set(
1726
+ filePath,
1727
+ Math.min(1, count / Math.max(1, totalContributors * 0.3))
1728
+ );
1702
1729
  }
1703
1730
  return result;
1704
1731
  }
@@ -2085,31 +2112,46 @@ async function generateAndOpenHotspotHTML(result, repoPath) {
2085
2112
  // src/core/unified.ts
2086
2113
  async function computeUnified(options) {
2087
2114
  console.log("Computing unified dashboard data...");
2088
- console.log(" [1/4] Scoring (binary, authorship, weighted)...");
2089
- const [binary, authorship, weighted] = await Promise.all([
2090
- computeFamiliarity({ ...options, mode: "binary" }),
2091
- computeFamiliarity({ ...options, mode: "authorship" }),
2115
+ console.log(" [1/4] Scoring (committed, code-coverage, weighted)...");
2116
+ const [committed, codeCoverage, weighted] = await Promise.all([
2117
+ computeFamiliarity({ ...options, mode: "committed" }),
2118
+ computeFamiliarity({ ...options, mode: "code-coverage" }),
2092
2119
  computeFamiliarity({ ...options, mode: "weighted" })
2093
2120
  ]);
2094
2121
  console.log(" [2/4] Team coverage...");
2095
2122
  const coverage = await computeTeamCoverage(options);
2096
- console.log(" [3/4] Hotspot analysis...");
2123
+ console.log(" [3/5] Hotspot analysis...");
2097
2124
  const hotspot = await computeHotspots({
2098
2125
  ...options,
2099
2126
  hotspot: "personal"
2100
2127
  });
2101
- console.log(" [4/4] Multi-user comparison...");
2128
+ console.log(" [4/5] Hotspot team familiarity...");
2129
+ const gitClient = new GitClient(options.repoPath);
2130
+ const repoRoot = await gitClient.getRepoRoot();
2131
+ const filter = createFilter(repoRoot);
2132
+ const tree = await buildFileTree(gitClient, filter);
2133
+ const trackedFiles = /* @__PURE__ */ new Set();
2134
+ walkFiles(tree, (f) => trackedFiles.add(f.path));
2135
+ const teamFamMap = await computeTeamAvgFamiliarity(
2136
+ gitClient,
2137
+ trackedFiles,
2138
+ options
2139
+ );
2140
+ const hotspotTeamFamiliarity = {};
2141
+ for (const [k, v] of teamFamMap) hotspotTeamFamiliarity[k] = v;
2142
+ console.log(" [5/5] Multi-user comparison...");
2102
2143
  const multiUser = await computeMultiUser({
2103
2144
  ...options,
2104
2145
  team: true
2105
2146
  });
2106
2147
  console.log("Done.");
2107
2148
  return {
2108
- repoName: binary.repoName,
2109
- userName: binary.userName,
2110
- scoring: { binary, authorship, weighted },
2149
+ repoName: committed.repoName,
2150
+ userName: committed.userName,
2151
+ scoring: { committed, codeCoverage, weighted },
2111
2152
  coverage,
2112
2153
  hotspot,
2154
+ hotspotTeamFamiliarity,
2113
2155
  multiUser
2114
2156
  };
2115
2157
  }
@@ -2118,8 +2160,10 @@ async function computeUnified(options) {
2118
2160
  import { writeFileSync as writeFileSync5 } from "fs";
2119
2161
  import { join as join5 } from "path";
2120
2162
  function generateUnifiedHTML(data) {
2121
- const scoringBinaryJson = JSON.stringify(data.scoring.binary.tree);
2122
- const scoringAuthorshipJson = JSON.stringify(data.scoring.authorship.tree);
2163
+ const scoringCommittedJson = JSON.stringify(data.scoring.committed.tree);
2164
+ const scoringCodeCoverageJson = JSON.stringify(
2165
+ data.scoring.codeCoverage.tree
2166
+ );
2123
2167
  const scoringWeightedJson = JSON.stringify(data.scoring.weighted.tree);
2124
2168
  const coverageTreeJson = JSON.stringify(data.coverage.tree);
2125
2169
  const coverageRiskJson = JSON.stringify(data.coverage.riskFiles);
@@ -2133,6 +2177,7 @@ function generateUnifiedHTML(data) {
2133
2177
  riskLevel: f.riskLevel
2134
2178
  }))
2135
2179
  );
2180
+ const hotspotTeamFamJson = JSON.stringify(data.hotspotTeamFamiliarity);
2136
2181
  const multiUserTreeJson = JSON.stringify(data.multiUser.tree);
2137
2182
  const multiUserSummariesJson = JSON.stringify(data.multiUser.userSummaries);
2138
2183
  const multiUserNamesJson = JSON.stringify(
@@ -2145,11 +2190,34 @@ function generateUnifiedHTML(data) {
2145
2190
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2146
2191
  <title>GitFamiliar \u2014 ${data.repoName}</title>
2147
2192
  <style>
2193
+ :root {
2194
+ --bg-base: #1a1a2e;
2195
+ --bg-panel: #16213e;
2196
+ --accent: #e94560;
2197
+ --accent-hover: #ff5577;
2198
+ --border: #0f3460;
2199
+ --text-primary: #e0e0e0;
2200
+ --text-secondary: #a0a0a0;
2201
+ --text-dim: #888;
2202
+ --link: #5eadf7;
2203
+ --color-critical: #e94560;
2204
+ --color-high: #f07040;
2205
+ --color-medium: #f5a623;
2206
+ --color-safe: #27ae60;
2207
+ --shadow-sm: 0 2px 4px rgba(0,0,0,0.3);
2208
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.4);
2209
+ --shadow-lg: 0 8px 24px rgba(0,0,0,0.5);
2210
+ --shadow-glow-accent: 0 0 20px rgba(233,69,96,0.3);
2211
+ --glass-bg: rgba(22,33,62,0.85);
2212
+ --glass-border: rgba(94,173,247,0.15);
2213
+ --transition-fast: 0.15s ease;
2214
+ --transition-smooth: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2215
+ }
2148
2216
  * { margin: 0; padding: 0; box-sizing: border-box; }
2149
2217
  body {
2150
2218
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2151
- background: #1a1a2e;
2152
- color: #e0e0e0;
2219
+ background: var(--bg-base);
2220
+ color: var(--text-primary);
2153
2221
  overflow: hidden;
2154
2222
  display: flex;
2155
2223
  flex-direction: column;
@@ -2157,39 +2225,51 @@ function generateUnifiedHTML(data) {
2157
2225
  }
2158
2226
  #header {
2159
2227
  padding: 12px 24px;
2160
- background: #16213e;
2161
- border-bottom: 1px solid #0f3460;
2228
+ background: linear-gradient(135deg, var(--bg-panel) 0%, #1a2844 100%);
2229
+ border-bottom: 1px solid var(--border);
2230
+ box-shadow: var(--shadow-md);
2162
2231
  display: flex;
2163
2232
  align-items: center;
2164
2233
  justify-content: space-between;
2234
+ position: relative;
2235
+ z-index: 10;
2165
2236
  }
2166
- #header h1 { font-size: 18px; color: #e94560; }
2167
- #header .info { font-size: 13px; color: #a0a0a0; }
2237
+ #header h1 { font-size: 18px; color: var(--accent); text-shadow: 0 0 20px rgba(233,69,96,0.4); }
2238
+ #header .info { font-size: 13px; color: var(--text-secondary); }
2168
2239
 
2169
2240
  /* Tabs */
2170
2241
  #tabs {
2171
2242
  display: flex;
2172
- background: #16213e;
2173
- border-bottom: 1px solid #0f3460;
2243
+ background: var(--bg-panel);
2244
+ border-bottom: 1px solid var(--border);
2174
2245
  padding: 0 24px;
2246
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
2247
+ position: relative;
2248
+ z-index: 9;
2175
2249
  }
2176
2250
  #tabs .tab {
2177
2251
  padding: 10px 20px;
2178
2252
  cursor: pointer;
2179
- color: #888;
2253
+ color: var(--text-dim);
2180
2254
  border-bottom: 2px solid transparent;
2181
2255
  font-size: 14px;
2182
- transition: color 0.15s;
2256
+ transition: all var(--transition-smooth);
2257
+ position: relative;
2258
+ }
2259
+ #tabs .tab:hover { color: var(--text-primary); transform: translateY(-1px); }
2260
+ #tabs .tab.active {
2261
+ color: var(--accent);
2262
+ border-bottom-color: var(--accent);
2263
+ text-shadow: 0 0 10px rgba(233,69,96,0.5);
2264
+ background: linear-gradient(to bottom, transparent, rgba(233,69,96,0.05));
2183
2265
  }
2184
- #tabs .tab:hover { color: #ccc; }
2185
- #tabs .tab.active { color: #e94560; border-bottom-color: #e94560; }
2186
2266
 
2187
2267
  /* Sub-tabs (scoring modes) */
2188
2268
  #scoring-controls {
2189
2269
  display: none;
2190
2270
  padding: 8px 24px;
2191
- background: #16213e;
2192
- border-bottom: 1px solid #0f3460;
2271
+ background: var(--bg-panel);
2272
+ border-bottom: 1px solid var(--border);
2193
2273
  align-items: center;
2194
2274
  gap: 16px;
2195
2275
  }
@@ -2197,50 +2277,103 @@ function generateUnifiedHTML(data) {
2197
2277
  .subtab {
2198
2278
  padding: 5px 14px;
2199
2279
  cursor: pointer;
2200
- color: #888;
2201
- border: 1px solid #0f3460;
2280
+ color: var(--text-dim);
2281
+ border: 1px solid var(--border);
2202
2282
  border-radius: 4px;
2203
2283
  font-size: 12px;
2204
2284
  background: transparent;
2205
- transition: all 0.15s;
2285
+ transition: all var(--transition-smooth);
2286
+ box-shadow: var(--shadow-sm);
2206
2287
  }
2207
- .subtab:hover { color: #ccc; border-color: #555; }
2208
- .subtab.active { color: #e94560; border-color: #e94560; background: rgba(233,69,96,0.1); }
2288
+ .subtab:hover { color: var(--text-primary); border-color: #555; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.3); }
2289
+ .subtab.active { color: var(--accent); border-color: var(--accent); background: rgba(233,69,96,0.1); box-shadow: 0 0 12px rgba(233,69,96,0.3); }
2209
2290
  #weight-controls {
2210
2291
  display: none;
2211
2292
  align-items: center;
2212
2293
  gap: 8px;
2213
2294
  margin-left: 24px;
2214
2295
  font-size: 12px;
2215
- color: #a0a0a0;
2296
+ color: var(--text-secondary);
2216
2297
  }
2217
2298
  #weight-controls.visible { display: flex; }
2218
2299
  #weight-controls input[type="range"] {
2219
2300
  width: 120px;
2220
- accent-color: #e94560;
2301
+ -webkit-appearance: none;
2302
+ appearance: none;
2303
+ background: transparent;
2304
+ cursor: pointer;
2305
+ }
2306
+ #weight-controls input[type="range"]::-webkit-slider-runnable-track {
2307
+ height: 6px;
2308
+ background: linear-gradient(to right, var(--accent), var(--link));
2309
+ border-radius: 3px;
2310
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.3);
2311
+ }
2312
+ #weight-controls input[type="range"]::-moz-range-track {
2313
+ height: 6px;
2314
+ background: linear-gradient(to right, var(--accent), var(--link));
2315
+ border-radius: 3px;
2316
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.3);
2317
+ }
2318
+ #weight-controls input[type="range"]::-webkit-slider-thumb {
2319
+ -webkit-appearance: none;
2320
+ width: 16px;
2321
+ height: 16px;
2322
+ background: var(--accent);
2323
+ border-radius: 50%;
2324
+ cursor: pointer;
2325
+ margin-top: -5px;
2326
+ box-shadow: 0 2px 6px rgba(0,0,0,0.4), 0 0 8px rgba(233,69,96,0.4);
2327
+ transition: all var(--transition-fast);
2328
+ }
2329
+ #weight-controls input[type="range"]::-moz-range-thumb {
2330
+ width: 16px;
2331
+ height: 16px;
2332
+ background: var(--accent);
2333
+ border: none;
2334
+ border-radius: 50%;
2335
+ cursor: pointer;
2336
+ box-shadow: 0 2px 6px rgba(0,0,0,0.4), 0 0 8px rgba(233,69,96,0.4);
2337
+ transition: all var(--transition-fast);
2338
+ }
2339
+ #weight-controls input[type="range"]::-webkit-slider-thumb:hover {
2340
+ transform: scale(1.2);
2341
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5), 0 0 16px rgba(233,69,96,0.6);
2221
2342
  }
2222
- #weight-controls .weight-label { min-width: 36px; text-align: right; color: #e0e0e0; }
2343
+ #weight-controls input[type="range"]::-moz-range-thumb:hover {
2344
+ transform: scale(1.2);
2345
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5), 0 0 16px rgba(233,69,96,0.6);
2346
+ }
2347
+ #weight-controls .weight-label { min-width: 36px; text-align: right; color: var(--text-primary); }
2223
2348
 
2224
2349
  /* Breadcrumb */
2225
2350
  #breadcrumb {
2226
2351
  padding: 8px 24px;
2227
- background: #16213e;
2352
+ background: var(--bg-panel);
2228
2353
  font-size: 13px;
2229
- border-bottom: 1px solid #0f3460;
2354
+ border-bottom: 1px solid var(--border);
2230
2355
  display: none;
2231
2356
  }
2232
2357
  #breadcrumb.visible { display: block; }
2233
- #breadcrumb span { cursor: pointer; color: #5eadf7; }
2234
- #breadcrumb span:hover { text-decoration: underline; }
2235
- #breadcrumb .sep { color: #666; margin: 0 4px; }
2358
+ #breadcrumb span {
2359
+ cursor: pointer;
2360
+ color: var(--link);
2361
+ padding: 3px 10px;
2362
+ border-radius: 12px;
2363
+ transition: all var(--transition-fast);
2364
+ display: inline-block;
2365
+ }
2366
+ #breadcrumb span:hover { background: rgba(94,173,247,0.12); text-shadow: 0 0 8px rgba(94,173,247,0.4); }
2367
+ #breadcrumb .sep { color: var(--text-dim); margin: 0 2px; padding: 0; }
2368
+ #breadcrumb .sep:hover { background: transparent; text-shadow: none; }
2236
2369
 
2237
2370
  /* Tab descriptions */
2238
2371
  .tab-desc {
2239
2372
  padding: 8px 24px;
2240
- background: #16213e;
2241
- border-bottom: 1px solid #0f3460;
2373
+ background: var(--bg-panel);
2374
+ border-bottom: 1px solid var(--border);
2242
2375
  font-size: 12px;
2243
- color: #888;
2376
+ color: var(--text-dim);
2244
2377
  display: none;
2245
2378
  }
2246
2379
  .tab-desc.visible { display: block; }
@@ -2250,71 +2383,120 @@ function generateUnifiedHTML(data) {
2250
2383
  .tab-content { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
2251
2384
  .tab-content.active { display: block; }
2252
2385
  .tab-content.with-sidebar.active { display: flex; }
2386
+ .tab-content svg { animation: fadeInScale 0.4s ease-out; }
2387
+ @keyframes fadeInScale {
2388
+ from { opacity: 0; transform: scale(0.98); }
2389
+ to { opacity: 1; transform: scale(1); }
2390
+ }
2253
2391
 
2254
2392
  /* Layout with sidebar */
2255
2393
  .with-sidebar .viz-area { flex: 1; position: relative; height: 100%; }
2256
2394
  .with-sidebar .sidebar {
2257
2395
  width: 300px;
2258
2396
  height: 100%;
2259
- background: #16213e;
2260
- border-left: 1px solid #0f3460;
2397
+ background: rgba(22,33,62,0.95);
2398
+ backdrop-filter: blur(8px);
2399
+ -webkit-backdrop-filter: blur(8px);
2400
+ border-left: 1px solid var(--border);
2401
+ box-shadow: -4px 0 16px rgba(0,0,0,0.3);
2261
2402
  overflow-y: auto;
2262
2403
  padding: 16px;
2263
2404
  }
2264
- .sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }
2405
+ .sidebar h3 { font-size: 14px; margin-bottom: 12px; color: var(--accent); text-shadow: 0 0 10px rgba(233,69,96,0.3); }
2265
2406
  .sidebar .risk-file, .sidebar .hotspot-item {
2266
- padding: 6px 0;
2267
- border-bottom: 1px solid #0f3460;
2407
+ padding: 8px 10px;
2408
+ border-bottom: 1px solid var(--border);
2268
2409
  font-size: 12px;
2410
+ border-radius: 4px;
2411
+ margin-bottom: 2px;
2412
+ transition: all var(--transition-fast);
2269
2413
  }
2270
- .sidebar .path { color: #e0e0e0; word-break: break-all; }
2271
- .sidebar .meta { color: #888; margin-top: 2px; }
2414
+ .sidebar .risk-file:hover, .sidebar .hotspot-item:hover {
2415
+ background: rgba(94,173,247,0.06);
2416
+ border-left: 3px solid var(--accent);
2417
+ padding-left: 13px;
2418
+ transform: translateX(2px);
2419
+ }
2420
+ .sidebar .path { color: var(--text-primary); word-break: break-all; }
2421
+ .sidebar .meta { color: var(--text-dim); margin-top: 2px; }
2272
2422
  .risk-badge {
2273
2423
  display: inline-block;
2274
- padding: 1px 6px;
2275
- border-radius: 3px;
2424
+ padding: 2px 8px;
2425
+ border-radius: 10px;
2276
2426
  font-size: 10px;
2277
2427
  font-weight: bold;
2278
2428
  margin-left: 4px;
2429
+ box-shadow: var(--shadow-sm);
2279
2430
  }
2280
- .risk-critical { background: #e94560; color: white; }
2281
- .risk-high { background: #f07040; color: white; }
2282
- .risk-medium { background: #f5a623; color: black; }
2283
- .risk-low { background: #27ae60; color: white; }
2431
+ .risk-critical { background: var(--color-critical); color: white; box-shadow: 0 0 8px rgba(233,69,96,0.4); }
2432
+ .risk-high { background: var(--color-high); color: white; box-shadow: 0 0 6px rgba(240,112,64,0.4); }
2433
+ .risk-medium { background: var(--color-medium); color: black; }
2434
+ .risk-low { background: var(--color-safe); color: white; }
2284
2435
 
2285
2436
  /* Multi-user controls */
2286
2437
  #multiuser-controls {
2287
2438
  display: none;
2288
2439
  padding: 8px 24px;
2289
- background: #16213e;
2290
- border-bottom: 1px solid #0f3460;
2440
+ background: var(--bg-panel);
2441
+ border-bottom: 1px solid var(--border);
2291
2442
  align-items: center;
2292
2443
  gap: 12px;
2293
2444
  }
2294
2445
  #multiuser-controls.visible { display: flex; }
2295
2446
  #multiuser-controls select {
2296
- padding: 4px 12px;
2297
- border: 1px solid #0f3460;
2298
- background: #1a1a2e;
2299
- color: #e0e0e0;
2300
- border-radius: 4px;
2447
+ padding: 6px 14px;
2448
+ border: 1px solid var(--border);
2449
+ background: var(--bg-base);
2450
+ color: var(--text-primary);
2451
+ border-radius: 6px;
2301
2452
  font-size: 13px;
2453
+ box-shadow: var(--shadow-sm);
2454
+ transition: all var(--transition-fast);
2455
+ cursor: pointer;
2456
+ }
2457
+ #multiuser-controls select:hover { border-color: var(--link); box-shadow: 0 0 8px rgba(94,173,247,0.2); }
2458
+ #multiuser-controls select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 12px rgba(233,69,96,0.3); }
2459
+ #multiuser-controls label { font-size: 13px; color: var(--text-dim); }
2460
+
2461
+ /* Hotspot controls */
2462
+ #hotspot-controls {
2463
+ display: none;
2464
+ padding: 8px 24px;
2465
+ background: var(--bg-panel);
2466
+ border-bottom: 1px solid var(--border);
2467
+ align-items: center;
2468
+ gap: 12px;
2469
+ }
2470
+ #hotspot-controls.visible { display: flex; }
2471
+ #hotspot-controls label { font-size: 13px; color: var(--text-dim); }
2472
+ #hotspot-controls .sep-v {
2473
+ width: 1px;
2474
+ height: 20px;
2475
+ background: var(--border);
2476
+ margin: 0 8px;
2477
+ }
2478
+ .subtab.disabled {
2479
+ opacity: 0.35;
2480
+ pointer-events: none;
2481
+ cursor: default;
2302
2482
  }
2303
- #multiuser-controls label { font-size: 13px; color: #888; }
2304
2483
 
2305
2484
  /* Tooltip */
2306
2485
  #tooltip {
2307
2486
  position: absolute;
2308
2487
  pointer-events: none;
2309
- background: rgba(22, 33, 62, 0.95);
2310
- border: 1px solid #0f3460;
2311
- border-radius: 6px;
2312
- padding: 10px 14px;
2488
+ background: var(--glass-bg);
2489
+ backdrop-filter: blur(12px);
2490
+ -webkit-backdrop-filter: blur(12px);
2491
+ border: 1px solid var(--glass-border);
2492
+ border-radius: 8px;
2493
+ padding: 12px 16px;
2313
2494
  font-size: 13px;
2314
2495
  line-height: 1.6;
2315
2496
  display: none;
2316
2497
  z-index: 100;
2317
2498
  max-width: 350px;
2499
+ box-shadow: var(--shadow-lg);
2318
2500
  }
2319
2501
 
2320
2502
  /* Legends */
@@ -2322,25 +2504,29 @@ function generateUnifiedHTML(data) {
2322
2504
  position: absolute;
2323
2505
  bottom: 16px;
2324
2506
  right: 16px;
2325
- background: rgba(22, 33, 62, 0.9);
2326
- border: 1px solid #0f3460;
2327
- border-radius: 6px;
2328
- padding: 10px;
2507
+ background: var(--glass-bg);
2508
+ backdrop-filter: blur(10px);
2509
+ -webkit-backdrop-filter: blur(10px);
2510
+ border: 1px solid var(--glass-border);
2511
+ border-radius: 10px;
2512
+ padding: 12px 14px;
2329
2513
  font-size: 12px;
2330
2514
  display: none;
2331
2515
  z-index: 50;
2516
+ box-shadow: var(--shadow-md);
2332
2517
  }
2333
2518
  .legend.active { display: block; }
2334
2519
  .legend .gradient-bar {
2335
2520
  width: 120px;
2336
2521
  height: 12px;
2337
- background: linear-gradient(to right, #e94560, #f5a623, #27ae60);
2338
- border-radius: 3px;
2522
+ background: linear-gradient(to right, var(--color-critical), var(--color-medium), var(--color-safe));
2523
+ border-radius: 6px;
2339
2524
  margin: 4px 0;
2525
+ box-shadow: inset 0 1px 2px rgba(0,0,0,0.2);
2340
2526
  }
2341
- .legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }
2527
+ .legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: var(--text-dim); }
2342
2528
  .legend .row { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
2343
- .legend .swatch { width: 14px; height: 14px; border-radius: 3px; }
2529
+ .legend .swatch { width: 14px; height: 14px; border-radius: 4px; box-shadow: var(--shadow-sm); }
2344
2530
 
2345
2531
  /* Zone labels for hotspot */
2346
2532
  #zone-labels { position: absolute; pointer-events: none; }
@@ -2354,12 +2540,12 @@ function generateUnifiedHTML(data) {
2354
2540
  <body>
2355
2541
  <div id="header">
2356
2542
  <h1>GitFamiliar \u2014 ${data.repoName}</h1>
2357
- <div class="info">${data.userName} | ${data.scoring.binary.totalFiles} files</div>
2543
+ <div class="info">${data.userName} | ${data.scoring.committed.totalFiles} files</div>
2358
2544
  </div>
2359
2545
 
2360
2546
  <div id="tabs">
2361
2547
  <div class="tab active" onclick="switchTab('scoring')">Scoring</div>
2362
- <div class="tab" onclick="switchTab('coverage')">Coverage</div>
2548
+ <div class="tab" onclick="switchTab('coverage')">Contributors</div>
2363
2549
  <div class="tab" onclick="switchTab('multiuser')">Multi-User</div>
2364
2550
  <div class="tab" onclick="switchTab('hotspots')">Hotspots</div>
2365
2551
  </div>
@@ -2368,7 +2554,7 @@ function generateUnifiedHTML(data) {
2368
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.
2369
2555
  </div>
2370
2556
  <div id="tab-desc-coverage" class="tab-desc">
2371
- Team knowledge distribution: how many people have contributed to each file. Low contributor count = high bus factor risk.
2557
+ Contributors per file: how many people have committed to each file. Low contributor count = high bus factor risk.
2372
2558
  </div>
2373
2559
  <div id="tab-desc-multiuser" class="tab-desc">
2374
2560
  Compare familiarity scores across team members. Select a user to see the codebase colored by their knowledge.
@@ -2378,9 +2564,9 @@ function generateUnifiedHTML(data) {
2378
2564
  </div>
2379
2565
 
2380
2566
  <div id="scoring-controls" class="visible">
2381
- <button class="subtab active" onclick="switchScoringMode('binary')">Binary</button>
2382
- <button class="subtab" onclick="switchScoringMode('authorship')">Authorship</button>
2383
- <button class="subtab" onclick="switchScoringMode('weighted')">Weighted</button>
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>
2384
2570
  <div id="weight-controls">
2385
2571
  <span>Blame:</span>
2386
2572
  <span class="weight-label" id="blame-label">50%</span>
@@ -2398,6 +2584,17 @@ function generateUnifiedHTML(data) {
2398
2584
  <select id="userSelect" onchange="onUserChange()"></select>
2399
2585
  </div>
2400
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
+
2401
2598
  <div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
2402
2599
 
2403
2600
  <div id="content-area">
@@ -2431,9 +2628,9 @@ function generateUnifiedHTML(data) {
2431
2628
  </div>
2432
2629
  <div class="legend" id="legend-coverage">
2433
2630
  <div>Contributors</div>
2434
- <div class="row"><div class="swatch" style="background:#e94560"></div> 0\u20131 (Risk)</div>
2435
- <div class="row"><div class="swatch" style="background:#f5a623"></div> 2\u20133 (Moderate)</div>
2436
- <div class="row"><div class="swatch" style="background:#27ae60"></div> 4+ (Safe)</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>
2437
2634
  </div>
2438
2635
  <div class="legend" id="legend-multiuser">
2439
2636
  <div>Familiarity</div>
@@ -2445,27 +2642,86 @@ function generateUnifiedHTML(data) {
2445
2642
  <script>
2446
2643
  // \u2500\u2500 Data \u2500\u2500
2447
2644
  const scoringData = {
2448
- binary: ${scoringBinaryJson},
2449
- authorship: ${scoringAuthorshipJson},
2645
+ committed: ${scoringCommittedJson},
2646
+ 'code-coverage': ${scoringCodeCoverageJson},
2450
2647
  weighted: ${scoringWeightedJson},
2451
2648
  };
2452
2649
  const coverageData = ${coverageTreeJson};
2453
2650
  const coverageRiskFiles = ${coverageRiskJson};
2454
2651
  const hotspotData = ${hotspotJson};
2652
+ const hotspotTeamFamiliarity = ${hotspotTeamFamJson};
2455
2653
  const multiUserData = ${multiUserTreeJson};
2456
2654
  const multiUserNames = ${multiUserNamesJson};
2457
2655
  const multiUserSummaries = ${multiUserSummariesJson};
2458
2656
 
2459
2657
  // \u2500\u2500 State \u2500\u2500
2460
2658
  let activeTab = 'scoring';
2461
- let scoringMode = 'binary';
2659
+ let scoringMode = 'committed';
2462
2660
  let blameWeight = 0.5;
2463
2661
  let scoringPath = '';
2464
2662
  let coveragePath = '';
2465
2663
  let multiuserPath = '';
2466
2664
  let currentUser = 0;
2665
+ let hotspotMode = 'personal';
2666
+ let hotspotScoring = 'committed';
2467
2667
  const rendered = { scoring: false, coverage: false, hotspots: false, multiuser: false };
2468
2668
 
2669
+ // \u2500\u2500 Hotspot recalculation utilities \u2500\u2500
2670
+ function extractFlatScores(node) {
2671
+ const map = {};
2672
+ function walk(n) {
2673
+ if (n.type === 'file') { map[n.path] = n.score; }
2674
+ else if (n.children) { n.children.forEach(walk); }
2675
+ }
2676
+ walk(node);
2677
+ return map;
2678
+ }
2679
+
2680
+ const personalScores = {
2681
+ committed: extractFlatScores(scoringData.committed),
2682
+ 'code-coverage': extractFlatScores(scoringData['code-coverage']),
2683
+ weighted: extractFlatScores(scoringData.weighted),
2684
+ };
2685
+
2686
+ function recalculateHotspotData() {
2687
+ const famScores = hotspotMode === 'personal'
2688
+ ? personalScores[hotspotScoring]
2689
+ : hotspotTeamFamiliarity;
2690
+ const maxFreq = d3.max(hotspotData, d => d.changeFrequency) || 1;
2691
+ return hotspotData.map(d => {
2692
+ const familiarity = famScores[d.path] || 0;
2693
+ const normalizedFreq = d.changeFrequency / maxFreq;
2694
+ const risk = normalizedFreq * (1 - familiarity);
2695
+ return { ...d, familiarity, risk,
2696
+ riskLevel: risk >= 0.6 ? 'critical' : risk >= 0.4 ? 'high' : risk >= 0.2 ? 'medium' : 'low',
2697
+ };
2698
+ }).sort((a, b) => b.risk - a.risk);
2699
+ }
2700
+
2701
+ function switchHotspotMode(mode) {
2702
+ hotspotMode = mode;
2703
+ document.querySelectorAll('#hotspot-controls .subtab:not(.hs-scoring)').forEach(el => {
2704
+ el.classList.toggle('active', el.dataset.mode === mode);
2705
+ });
2706
+ // Disable scoring buttons in team mode
2707
+ const isTeam = mode === 'team';
2708
+ document.querySelectorAll('#hotspot-controls .hs-scoring').forEach(el => {
2709
+ el.classList.toggle('disabled', isTeam);
2710
+ });
2711
+ renderHotspot();
2712
+ renderHotspotSidebar();
2713
+ }
2714
+
2715
+ function switchHotspotScoring(mode) {
2716
+ if (hotspotMode === 'team') return;
2717
+ hotspotScoring = mode;
2718
+ document.querySelectorAll('#hotspot-controls .hs-scoring').forEach(el => {
2719
+ el.classList.toggle('active', el.dataset.mode === mode);
2720
+ });
2721
+ renderHotspot();
2722
+ renderHotspotSidebar();
2723
+ }
2724
+
2469
2725
  // \u2500\u2500 Common utilities \u2500\u2500
2470
2726
  function scoreColor(score) {
2471
2727
  if (score <= 0) return '#e94560';
@@ -2560,8 +2816,8 @@ function truncateLabel(name, w, h) {
2560
2816
 
2561
2817
  // \u2500\u2500 Tab switching \u2500\u2500
2562
2818
  const modeDescriptions = {
2563
- binary: 'Binary: Have you ever committed to this file? Yes (green) or No (red).',
2564
- authorship: 'Authorship: How much of the current code did you write? Based on git blame line ownership.',
2819
+ committed: 'Committed: Have you ever committed to this file? Yes (green) or No (red).',
2820
+ 'code-coverage': 'Code Coverage: How much of the current code did you write? Based on git blame line ownership.',
2565
2821
  weighted: 'Weighted: Combines blame ownership and commit history with adjustable weights. Use the sliders to tune.',
2566
2822
  };
2567
2823
 
@@ -2583,6 +2839,7 @@ function switchTab(tab) {
2583
2839
  document.getElementById('scoring-controls').classList.toggle('visible', tab === 'scoring');
2584
2840
  document.getElementById('scoring-mode-desc').classList.toggle('visible', tab === 'scoring');
2585
2841
  document.getElementById('multiuser-controls').classList.toggle('visible', tab === 'multiuser');
2842
+ document.getElementById('hotspot-controls').classList.toggle('visible', tab === 'hotspots');
2586
2843
 
2587
2844
  // Show/hide breadcrumb
2588
2845
  const showBreadcrumb = tab === 'scoring' || tab === 'coverage' || tab === 'multiuser';
@@ -2636,7 +2893,7 @@ function switchScoringMode(mode) {
2636
2893
  scoringPath = '';
2637
2894
  updateBreadcrumb('');
2638
2895
  document.querySelectorAll('#scoring-controls .subtab').forEach(el => {
2639
- el.classList.toggle('active', el.textContent.toLowerCase() === mode);
2896
+ el.classList.toggle('active', el.dataset.mode === mode);
2640
2897
  });
2641
2898
  document.getElementById('weight-controls').classList.toggle('visible', mode === 'weighted');
2642
2899
  document.getElementById('mode-desc-text').textContent = modeDescriptions[mode];
@@ -2857,15 +3114,17 @@ function renderHotspot() {
2857
3114
  height = contentH;
2858
3115
  }
2859
3116
 
3117
+ const currentData = recalculateHotspotData();
3118
+
2860
3119
  const margin = { top: 30, right: 30, bottom: 60, left: 70 };
2861
3120
  const innerW = width - margin.left - margin.right;
2862
3121
  const innerH = height - margin.top - margin.bottom;
2863
3122
 
2864
- const maxFreq = d3.max(hotspotData, d => d.changeFrequency) || 1;
3123
+ const maxFreq = d3.max(currentData, d => d.changeFrequency) || 1;
2865
3124
 
2866
3125
  const x = d3.scaleLinear().domain([0, 1]).range([0, innerW]);
2867
3126
  const y = d3.scaleLinear().domain([0, maxFreq * 1.1]).range([innerH, 0]);
2868
- const r = d3.scaleSqrt().domain([0, d3.max(hotspotData, d => d.lines) || 1]).range([3, 20]);
3127
+ const r = d3.scaleSqrt().domain([0, d3.max(currentData, d => d.lines) || 1]).range([3, 20]);
2869
3128
 
2870
3129
  const svg = d3.select('#hotspot-viz').append('svg').attr('width', width).attr('height', height);
2871
3130
  const g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
@@ -2912,7 +3171,7 @@ function renderHotspot() {
2912
3171
  labels.appendChild(safeLabel);
2913
3172
 
2914
3173
  // Data points
2915
- g.selectAll('circle').data(hotspotData).join('circle')
3174
+ g.selectAll('circle').data(currentData).join('circle')
2916
3175
  .attr('cx', d => x(d.familiarity))
2917
3176
  .attr('cy', d => y(d.changeFrequency))
2918
3177
  .attr('r', d => r(d.lines))
@@ -2920,8 +3179,10 @@ function renderHotspot() {
2920
3179
  .attr('opacity', 0.7)
2921
3180
  .attr('stroke', 'none')
2922
3181
  .style('cursor', 'pointer')
3182
+ .style('filter', d => d.riskLevel === 'critical' ? 'drop-shadow(0 0 6px rgba(233,69,96,0.8))' : 'none')
3183
+ .style('transition', 'all 0.2s ease')
2923
3184
  .on('mouseover', function(event, d) {
2924
- d3.select(this).attr('opacity', 1).attr('stroke', '#fff').attr('stroke-width', 2);
3185
+ d3.select(this).attr('opacity', 1).attr('stroke', '#fff').attr('stroke-width', 2).style('filter', 'drop-shadow(0 0 12px rgba(255,255,255,0.5))');
2925
3186
  showTooltipAt(
2926
3187
  '<strong>' + d.path + '</strong>' +
2927
3188
  '<br>Familiarity: ' + Math.round(d.familiarity * 100) + '%' +
@@ -2932,15 +3193,17 @@ function renderHotspot() {
2932
3193
  );
2933
3194
  })
2934
3195
  .on('mousemove', moveTooltip)
2935
- .on('mouseout', function() {
2936
- d3.select(this).attr('opacity', 0.7).attr('stroke', 'none');
3196
+ .on('mouseout', function(event, d) {
3197
+ const origFilter = d.riskLevel === 'critical' ? 'drop-shadow(0 0 6px rgba(233,69,96,0.8))' : 'none';
3198
+ d3.select(this).attr('opacity', 0.7).attr('stroke', 'none').style('filter', origFilter);
2937
3199
  hideTooltip();
2938
3200
  });
2939
3201
  }
2940
3202
 
2941
3203
  function renderHotspotSidebar() {
2942
3204
  const container = document.getElementById('hotspot-list');
2943
- const top = hotspotData.slice(0, 30);
3205
+ const currentData = recalculateHotspotData();
3206
+ const top = currentData.slice(0, 30);
2944
3207
  if (top.length === 0) {
2945
3208
  container.innerHTML = '<div style="color:#888">No active files in time window.</div>';
2946
3209
  return;
@@ -3075,15 +3338,17 @@ async function generateAndOpenUnifiedHTML(data, repoPath) {
3075
3338
  }
3076
3339
 
3077
3340
  // src/cli/index.ts
3341
+ var require2 = createRequire(import.meta.url);
3342
+ var pkg = require2("../../package.json");
3078
3343
  function collect(value, previous) {
3079
3344
  return previous.concat([value]);
3080
3345
  }
3081
3346
  function createProgram() {
3082
3347
  const program2 = new Command();
3083
- program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version("0.1.1").option(
3348
+ program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version(pkg.version).option(
3084
3349
  "-m, --mode <mode>",
3085
- "Scoring mode: binary, authorship, weighted",
3086
- "binary"
3350
+ "Scoring mode: committed, code-coverage, weighted",
3351
+ "committed"
3087
3352
  ).option(
3088
3353
  "-u, --user <user>",
3089
3354
  "Git user name or email (repeatable for comparison)",
@@ -3097,18 +3362,15 @@ function createProgram() {
3097
3362
  "-w, --weights <weights>",
3098
3363
  'Weights for weighted mode: blame,commit (e.g., "0.5,0.5")'
3099
3364
  ).option("--team", "Compare all contributors", false).option(
3100
- "--team-coverage",
3101
- "Show team coverage map (bus factor analysis)",
3365
+ "--contributors-per-file",
3366
+ "Analyze number of contributors per file (bus factor)",
3102
3367
  false
3103
- ).option("--hotspot [mode]", "Hotspot analysis: personal (default) or team").option(
3104
- "--window <days>",
3105
- "Time window for hotspot analysis in days (default: 90)"
3106
- ).action(async (rawOptions) => {
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) => {
3107
3369
  try {
3108
3370
  const repoPath = process.cwd();
3109
3371
  const options = parseOptions(rawOptions, repoPath);
3110
3372
  const isMultiUserCheck = options.team || Array.isArray(options.user) && options.user.length > 1;
3111
- if (options.html && !options.hotspot && !options.teamCoverage && !isMultiUserCheck) {
3373
+ if (options.html && !options.hotspot && !options.contributorsPerFile && !isMultiUserCheck) {
3112
3374
  const data = await computeUnified(options);
3113
3375
  await generateAndOpenUnifiedHTML(data, repoPath);
3114
3376
  return;
@@ -3122,7 +3384,7 @@ function createProgram() {
3122
3384
  }
3123
3385
  return;
3124
3386
  }
3125
- if (options.teamCoverage) {
3387
+ if (options.contributorsPerFile) {
3126
3388
  const result2 = await computeTeamCoverage(options);
3127
3389
  if (options.html) {
3128
3390
  await generateAndOpenCoverageHTML(result2, repoPath);