gitfamiliar 0.8.0 → 0.9.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-NGCQUB2H.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
@@ -1630,14 +1631,26 @@ async function computeHotspots(options) {
1630
1631
  const isTeamMode = options.hotspot === "team";
1631
1632
  const trackedFiles = /* @__PURE__ */ new Set();
1632
1633
  walkFiles(tree, (f) => trackedFiles.add(f.path));
1633
- const changeFreqMap = await bulkGetChangeFrequency(gitClient, timeWindow, trackedFiles);
1634
+ const changeFreqMap = await bulkGetChangeFrequency(
1635
+ gitClient,
1636
+ timeWindow,
1637
+ trackedFiles
1638
+ );
1634
1639
  let familiarityMap;
1635
1640
  let userName;
1636
1641
  if (isTeamMode) {
1637
- familiarityMap = await computeTeamAvgFamiliarity(gitClient, trackedFiles, options);
1642
+ familiarityMap = await computeTeamAvgFamiliarity(
1643
+ gitClient,
1644
+ trackedFiles,
1645
+ options
1646
+ );
1638
1647
  } else {
1639
1648
  const userFlag = Array.isArray(options.user) ? options.user[0] : options.user;
1640
- const result = await computeFamiliarity({ ...options, team: false, teamCoverage: false });
1649
+ const result = await computeFamiliarity({
1650
+ ...options,
1651
+ team: false,
1652
+ teamCoverage: false
1653
+ });
1641
1654
  userName = result.userName;
1642
1655
  familiarityMap = /* @__PURE__ */ new Map();
1643
1656
  walkFiles(result.tree, (f) => {
@@ -1693,12 +1706,18 @@ function classifyHotspotRisk(risk) {
1693
1706
  async function computeTeamAvgFamiliarity(gitClient, trackedFiles, options) {
1694
1707
  const contributors = await getAllContributors(gitClient, 1);
1695
1708
  const totalContributors = Math.max(1, contributors.length);
1696
- const fileContributors = await bulkGetFileContributors(gitClient, trackedFiles);
1709
+ const fileContributors = await bulkGetFileContributors(
1710
+ gitClient,
1711
+ trackedFiles
1712
+ );
1697
1713
  const result = /* @__PURE__ */ new Map();
1698
1714
  for (const filePath of trackedFiles) {
1699
1715
  const contribs = fileContributors.get(filePath);
1700
1716
  const count = contribs ? contribs.size : 0;
1701
- result.set(filePath, Math.min(1, count / Math.max(1, totalContributors * 0.3)));
1717
+ result.set(
1718
+ filePath,
1719
+ Math.min(1, count / Math.max(1, totalContributors * 0.3))
1720
+ );
1702
1721
  }
1703
1722
  return result;
1704
1723
  }
@@ -2093,12 +2112,26 @@ async function computeUnified(options) {
2093
2112
  ]);
2094
2113
  console.log(" [2/4] Team coverage...");
2095
2114
  const coverage = await computeTeamCoverage(options);
2096
- console.log(" [3/4] Hotspot analysis...");
2115
+ console.log(" [3/5] Hotspot analysis...");
2097
2116
  const hotspot = await computeHotspots({
2098
2117
  ...options,
2099
2118
  hotspot: "personal"
2100
2119
  });
2101
- console.log(" [4/4] Multi-user comparison...");
2120
+ console.log(" [4/5] Hotspot team familiarity...");
2121
+ const gitClient = new GitClient(options.repoPath);
2122
+ const repoRoot = await gitClient.getRepoRoot();
2123
+ const filter = createFilter(repoRoot);
2124
+ const tree = await buildFileTree(gitClient, filter);
2125
+ const trackedFiles = /* @__PURE__ */ new Set();
2126
+ walkFiles(tree, (f) => trackedFiles.add(f.path));
2127
+ const teamFamMap = await computeTeamAvgFamiliarity(
2128
+ gitClient,
2129
+ trackedFiles,
2130
+ options
2131
+ );
2132
+ const hotspotTeamFamiliarity = {};
2133
+ for (const [k, v] of teamFamMap) hotspotTeamFamiliarity[k] = v;
2134
+ console.log(" [5/5] Multi-user comparison...");
2102
2135
  const multiUser = await computeMultiUser({
2103
2136
  ...options,
2104
2137
  team: true
@@ -2110,6 +2143,7 @@ async function computeUnified(options) {
2110
2143
  scoring: { binary, authorship, weighted },
2111
2144
  coverage,
2112
2145
  hotspot,
2146
+ hotspotTeamFamiliarity,
2113
2147
  multiUser
2114
2148
  };
2115
2149
  }
@@ -2133,6 +2167,7 @@ function generateUnifiedHTML(data) {
2133
2167
  riskLevel: f.riskLevel
2134
2168
  }))
2135
2169
  );
2170
+ const hotspotTeamFamJson = JSON.stringify(data.hotspotTeamFamiliarity);
2136
2171
  const multiUserTreeJson = JSON.stringify(data.multiUser.tree);
2137
2172
  const multiUserSummariesJson = JSON.stringify(data.multiUser.userSummaries);
2138
2173
  const multiUserNamesJson = JSON.stringify(
@@ -2145,11 +2180,34 @@ function generateUnifiedHTML(data) {
2145
2180
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
2146
2181
  <title>GitFamiliar \u2014 ${data.repoName}</title>
2147
2182
  <style>
2183
+ :root {
2184
+ --bg-base: #1a1a2e;
2185
+ --bg-panel: #16213e;
2186
+ --accent: #e94560;
2187
+ --accent-hover: #ff5577;
2188
+ --border: #0f3460;
2189
+ --text-primary: #e0e0e0;
2190
+ --text-secondary: #a0a0a0;
2191
+ --text-dim: #888;
2192
+ --link: #5eadf7;
2193
+ --color-critical: #e94560;
2194
+ --color-high: #f07040;
2195
+ --color-medium: #f5a623;
2196
+ --color-safe: #27ae60;
2197
+ --shadow-sm: 0 2px 4px rgba(0,0,0,0.3);
2198
+ --shadow-md: 0 4px 12px rgba(0,0,0,0.4);
2199
+ --shadow-lg: 0 8px 24px rgba(0,0,0,0.5);
2200
+ --shadow-glow-accent: 0 0 20px rgba(233,69,96,0.3);
2201
+ --glass-bg: rgba(22,33,62,0.85);
2202
+ --glass-border: rgba(94,173,247,0.15);
2203
+ --transition-fast: 0.15s ease;
2204
+ --transition-smooth: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2205
+ }
2148
2206
  * { margin: 0; padding: 0; box-sizing: border-box; }
2149
2207
  body {
2150
2208
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2151
- background: #1a1a2e;
2152
- color: #e0e0e0;
2209
+ background: var(--bg-base);
2210
+ color: var(--text-primary);
2153
2211
  overflow: hidden;
2154
2212
  display: flex;
2155
2213
  flex-direction: column;
@@ -2157,39 +2215,51 @@ function generateUnifiedHTML(data) {
2157
2215
  }
2158
2216
  #header {
2159
2217
  padding: 12px 24px;
2160
- background: #16213e;
2161
- border-bottom: 1px solid #0f3460;
2218
+ background: linear-gradient(135deg, var(--bg-panel) 0%, #1a2844 100%);
2219
+ border-bottom: 1px solid var(--border);
2220
+ box-shadow: var(--shadow-md);
2162
2221
  display: flex;
2163
2222
  align-items: center;
2164
2223
  justify-content: space-between;
2224
+ position: relative;
2225
+ z-index: 10;
2165
2226
  }
2166
- #header h1 { font-size: 18px; color: #e94560; }
2167
- #header .info { font-size: 13px; color: #a0a0a0; }
2227
+ #header h1 { font-size: 18px; color: var(--accent); text-shadow: 0 0 20px rgba(233,69,96,0.4); }
2228
+ #header .info { font-size: 13px; color: var(--text-secondary); }
2168
2229
 
2169
2230
  /* Tabs */
2170
2231
  #tabs {
2171
2232
  display: flex;
2172
- background: #16213e;
2173
- border-bottom: 1px solid #0f3460;
2233
+ background: var(--bg-panel);
2234
+ border-bottom: 1px solid var(--border);
2174
2235
  padding: 0 24px;
2236
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
2237
+ position: relative;
2238
+ z-index: 9;
2175
2239
  }
2176
2240
  #tabs .tab {
2177
2241
  padding: 10px 20px;
2178
2242
  cursor: pointer;
2179
- color: #888;
2243
+ color: var(--text-dim);
2180
2244
  border-bottom: 2px solid transparent;
2181
2245
  font-size: 14px;
2182
- transition: color 0.15s;
2246
+ transition: all var(--transition-smooth);
2247
+ position: relative;
2248
+ }
2249
+ #tabs .tab:hover { color: var(--text-primary); transform: translateY(-1px); }
2250
+ #tabs .tab.active {
2251
+ color: var(--accent);
2252
+ border-bottom-color: var(--accent);
2253
+ text-shadow: 0 0 10px rgba(233,69,96,0.5);
2254
+ background: linear-gradient(to bottom, transparent, rgba(233,69,96,0.05));
2183
2255
  }
2184
- #tabs .tab:hover { color: #ccc; }
2185
- #tabs .tab.active { color: #e94560; border-bottom-color: #e94560; }
2186
2256
 
2187
2257
  /* Sub-tabs (scoring modes) */
2188
2258
  #scoring-controls {
2189
2259
  display: none;
2190
2260
  padding: 8px 24px;
2191
- background: #16213e;
2192
- border-bottom: 1px solid #0f3460;
2261
+ background: var(--bg-panel);
2262
+ border-bottom: 1px solid var(--border);
2193
2263
  align-items: center;
2194
2264
  gap: 16px;
2195
2265
  }
@@ -2197,50 +2267,103 @@ function generateUnifiedHTML(data) {
2197
2267
  .subtab {
2198
2268
  padding: 5px 14px;
2199
2269
  cursor: pointer;
2200
- color: #888;
2201
- border: 1px solid #0f3460;
2270
+ color: var(--text-dim);
2271
+ border: 1px solid var(--border);
2202
2272
  border-radius: 4px;
2203
2273
  font-size: 12px;
2204
2274
  background: transparent;
2205
- transition: all 0.15s;
2275
+ transition: all var(--transition-smooth);
2276
+ box-shadow: var(--shadow-sm);
2206
2277
  }
2207
- .subtab:hover { color: #ccc; border-color: #555; }
2208
- .subtab.active { color: #e94560; border-color: #e94560; background: rgba(233,69,96,0.1); }
2278
+ .subtab:hover { color: var(--text-primary); border-color: #555; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.3); }
2279
+ .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
2280
  #weight-controls {
2210
2281
  display: none;
2211
2282
  align-items: center;
2212
2283
  gap: 8px;
2213
2284
  margin-left: 24px;
2214
2285
  font-size: 12px;
2215
- color: #a0a0a0;
2286
+ color: var(--text-secondary);
2216
2287
  }
2217
2288
  #weight-controls.visible { display: flex; }
2218
2289
  #weight-controls input[type="range"] {
2219
2290
  width: 120px;
2220
- accent-color: #e94560;
2291
+ -webkit-appearance: none;
2292
+ appearance: none;
2293
+ background: transparent;
2294
+ cursor: pointer;
2295
+ }
2296
+ #weight-controls input[type="range"]::-webkit-slider-runnable-track {
2297
+ height: 6px;
2298
+ background: linear-gradient(to right, var(--accent), var(--link));
2299
+ border-radius: 3px;
2300
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.3);
2221
2301
  }
2222
- #weight-controls .weight-label { min-width: 36px; text-align: right; color: #e0e0e0; }
2302
+ #weight-controls input[type="range"]::-moz-range-track {
2303
+ height: 6px;
2304
+ background: linear-gradient(to right, var(--accent), var(--link));
2305
+ border-radius: 3px;
2306
+ box-shadow: inset 0 1px 3px rgba(0,0,0,0.3);
2307
+ }
2308
+ #weight-controls input[type="range"]::-webkit-slider-thumb {
2309
+ -webkit-appearance: none;
2310
+ width: 16px;
2311
+ height: 16px;
2312
+ background: var(--accent);
2313
+ border-radius: 50%;
2314
+ cursor: pointer;
2315
+ margin-top: -5px;
2316
+ box-shadow: 0 2px 6px rgba(0,0,0,0.4), 0 0 8px rgba(233,69,96,0.4);
2317
+ transition: all var(--transition-fast);
2318
+ }
2319
+ #weight-controls input[type="range"]::-moz-range-thumb {
2320
+ width: 16px;
2321
+ height: 16px;
2322
+ background: var(--accent);
2323
+ border: none;
2324
+ border-radius: 50%;
2325
+ cursor: pointer;
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"]::-webkit-slider-thumb:hover {
2330
+ transform: scale(1.2);
2331
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5), 0 0 16px rgba(233,69,96,0.6);
2332
+ }
2333
+ #weight-controls input[type="range"]::-moz-range-thumb:hover {
2334
+ transform: scale(1.2);
2335
+ box-shadow: 0 4px 10px rgba(0,0,0,0.5), 0 0 16px rgba(233,69,96,0.6);
2336
+ }
2337
+ #weight-controls .weight-label { min-width: 36px; text-align: right; color: var(--text-primary); }
2223
2338
 
2224
2339
  /* Breadcrumb */
2225
2340
  #breadcrumb {
2226
2341
  padding: 8px 24px;
2227
- background: #16213e;
2342
+ background: var(--bg-panel);
2228
2343
  font-size: 13px;
2229
- border-bottom: 1px solid #0f3460;
2344
+ border-bottom: 1px solid var(--border);
2230
2345
  display: none;
2231
2346
  }
2232
2347
  #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; }
2348
+ #breadcrumb span {
2349
+ cursor: pointer;
2350
+ color: var(--link);
2351
+ padding: 3px 10px;
2352
+ border-radius: 12px;
2353
+ transition: all var(--transition-fast);
2354
+ display: inline-block;
2355
+ }
2356
+ #breadcrumb span:hover { background: rgba(94,173,247,0.12); text-shadow: 0 0 8px rgba(94,173,247,0.4); }
2357
+ #breadcrumb .sep { color: var(--text-dim); margin: 0 2px; padding: 0; }
2358
+ #breadcrumb .sep:hover { background: transparent; text-shadow: none; }
2236
2359
 
2237
2360
  /* Tab descriptions */
2238
2361
  .tab-desc {
2239
2362
  padding: 8px 24px;
2240
- background: #16213e;
2241
- border-bottom: 1px solid #0f3460;
2363
+ background: var(--bg-panel);
2364
+ border-bottom: 1px solid var(--border);
2242
2365
  font-size: 12px;
2243
- color: #888;
2366
+ color: var(--text-dim);
2244
2367
  display: none;
2245
2368
  }
2246
2369
  .tab-desc.visible { display: block; }
@@ -2250,71 +2373,120 @@ function generateUnifiedHTML(data) {
2250
2373
  .tab-content { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
2251
2374
  .tab-content.active { display: block; }
2252
2375
  .tab-content.with-sidebar.active { display: flex; }
2376
+ .tab-content svg { animation: fadeInScale 0.4s ease-out; }
2377
+ @keyframes fadeInScale {
2378
+ from { opacity: 0; transform: scale(0.98); }
2379
+ to { opacity: 1; transform: scale(1); }
2380
+ }
2253
2381
 
2254
2382
  /* Layout with sidebar */
2255
2383
  .with-sidebar .viz-area { flex: 1; position: relative; height: 100%; }
2256
2384
  .with-sidebar .sidebar {
2257
2385
  width: 300px;
2258
2386
  height: 100%;
2259
- background: #16213e;
2260
- border-left: 1px solid #0f3460;
2387
+ background: rgba(22,33,62,0.95);
2388
+ backdrop-filter: blur(8px);
2389
+ -webkit-backdrop-filter: blur(8px);
2390
+ border-left: 1px solid var(--border);
2391
+ box-shadow: -4px 0 16px rgba(0,0,0,0.3);
2261
2392
  overflow-y: auto;
2262
2393
  padding: 16px;
2263
2394
  }
2264
- .sidebar h3 { font-size: 14px; margin-bottom: 12px; color: #e94560; }
2395
+ .sidebar h3 { font-size: 14px; margin-bottom: 12px; color: var(--accent); text-shadow: 0 0 10px rgba(233,69,96,0.3); }
2265
2396
  .sidebar .risk-file, .sidebar .hotspot-item {
2266
- padding: 6px 0;
2267
- border-bottom: 1px solid #0f3460;
2397
+ padding: 8px 10px;
2398
+ border-bottom: 1px solid var(--border);
2268
2399
  font-size: 12px;
2400
+ border-radius: 4px;
2401
+ margin-bottom: 2px;
2402
+ transition: all var(--transition-fast);
2269
2403
  }
2270
- .sidebar .path { color: #e0e0e0; word-break: break-all; }
2271
- .sidebar .meta { color: #888; margin-top: 2px; }
2404
+ .sidebar .risk-file:hover, .sidebar .hotspot-item:hover {
2405
+ background: rgba(94,173,247,0.06);
2406
+ border-left: 3px solid var(--accent);
2407
+ padding-left: 13px;
2408
+ transform: translateX(2px);
2409
+ }
2410
+ .sidebar .path { color: var(--text-primary); word-break: break-all; }
2411
+ .sidebar .meta { color: var(--text-dim); margin-top: 2px; }
2272
2412
  .risk-badge {
2273
2413
  display: inline-block;
2274
- padding: 1px 6px;
2275
- border-radius: 3px;
2414
+ padding: 2px 8px;
2415
+ border-radius: 10px;
2276
2416
  font-size: 10px;
2277
2417
  font-weight: bold;
2278
2418
  margin-left: 4px;
2419
+ box-shadow: var(--shadow-sm);
2279
2420
  }
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; }
2421
+ .risk-critical { background: var(--color-critical); color: white; box-shadow: 0 0 8px rgba(233,69,96,0.4); }
2422
+ .risk-high { background: var(--color-high); color: white; box-shadow: 0 0 6px rgba(240,112,64,0.4); }
2423
+ .risk-medium { background: var(--color-medium); color: black; }
2424
+ .risk-low { background: var(--color-safe); color: white; }
2284
2425
 
2285
2426
  /* Multi-user controls */
2286
2427
  #multiuser-controls {
2287
2428
  display: none;
2288
2429
  padding: 8px 24px;
2289
- background: #16213e;
2290
- border-bottom: 1px solid #0f3460;
2430
+ background: var(--bg-panel);
2431
+ border-bottom: 1px solid var(--border);
2291
2432
  align-items: center;
2292
2433
  gap: 12px;
2293
2434
  }
2294
2435
  #multiuser-controls.visible { display: flex; }
2295
2436
  #multiuser-controls select {
2296
- padding: 4px 12px;
2297
- border: 1px solid #0f3460;
2298
- background: #1a1a2e;
2299
- color: #e0e0e0;
2300
- border-radius: 4px;
2437
+ padding: 6px 14px;
2438
+ border: 1px solid var(--border);
2439
+ background: var(--bg-base);
2440
+ color: var(--text-primary);
2441
+ border-radius: 6px;
2301
2442
  font-size: 13px;
2443
+ box-shadow: var(--shadow-sm);
2444
+ transition: all var(--transition-fast);
2445
+ cursor: pointer;
2446
+ }
2447
+ #multiuser-controls select:hover { border-color: var(--link); box-shadow: 0 0 8px rgba(94,173,247,0.2); }
2448
+ #multiuser-controls select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 12px rgba(233,69,96,0.3); }
2449
+ #multiuser-controls label { font-size: 13px; color: var(--text-dim); }
2450
+
2451
+ /* Hotspot controls */
2452
+ #hotspot-controls {
2453
+ display: none;
2454
+ padding: 8px 24px;
2455
+ background: var(--bg-panel);
2456
+ border-bottom: 1px solid var(--border);
2457
+ align-items: center;
2458
+ gap: 12px;
2459
+ }
2460
+ #hotspot-controls.visible { display: flex; }
2461
+ #hotspot-controls label { font-size: 13px; color: var(--text-dim); }
2462
+ #hotspot-controls .sep-v {
2463
+ width: 1px;
2464
+ height: 20px;
2465
+ background: var(--border);
2466
+ margin: 0 8px;
2467
+ }
2468
+ .subtab.disabled {
2469
+ opacity: 0.35;
2470
+ pointer-events: none;
2471
+ cursor: default;
2302
2472
  }
2303
- #multiuser-controls label { font-size: 13px; color: #888; }
2304
2473
 
2305
2474
  /* Tooltip */
2306
2475
  #tooltip {
2307
2476
  position: absolute;
2308
2477
  pointer-events: none;
2309
- background: rgba(22, 33, 62, 0.95);
2310
- border: 1px solid #0f3460;
2311
- border-radius: 6px;
2312
- padding: 10px 14px;
2478
+ background: var(--glass-bg);
2479
+ backdrop-filter: blur(12px);
2480
+ -webkit-backdrop-filter: blur(12px);
2481
+ border: 1px solid var(--glass-border);
2482
+ border-radius: 8px;
2483
+ padding: 12px 16px;
2313
2484
  font-size: 13px;
2314
2485
  line-height: 1.6;
2315
2486
  display: none;
2316
2487
  z-index: 100;
2317
2488
  max-width: 350px;
2489
+ box-shadow: var(--shadow-lg);
2318
2490
  }
2319
2491
 
2320
2492
  /* Legends */
@@ -2322,25 +2494,29 @@ function generateUnifiedHTML(data) {
2322
2494
  position: absolute;
2323
2495
  bottom: 16px;
2324
2496
  right: 16px;
2325
- background: rgba(22, 33, 62, 0.9);
2326
- border: 1px solid #0f3460;
2327
- border-radius: 6px;
2328
- padding: 10px;
2497
+ background: var(--glass-bg);
2498
+ backdrop-filter: blur(10px);
2499
+ -webkit-backdrop-filter: blur(10px);
2500
+ border: 1px solid var(--glass-border);
2501
+ border-radius: 10px;
2502
+ padding: 12px 14px;
2329
2503
  font-size: 12px;
2330
2504
  display: none;
2331
2505
  z-index: 50;
2506
+ box-shadow: var(--shadow-md);
2332
2507
  }
2333
2508
  .legend.active { display: block; }
2334
2509
  .legend .gradient-bar {
2335
2510
  width: 120px;
2336
2511
  height: 12px;
2337
- background: linear-gradient(to right, #e94560, #f5a623, #27ae60);
2338
- border-radius: 3px;
2512
+ background: linear-gradient(to right, var(--color-critical), var(--color-medium), var(--color-safe));
2513
+ border-radius: 6px;
2339
2514
  margin: 4px 0;
2515
+ box-shadow: inset 0 1px 2px rgba(0,0,0,0.2);
2340
2516
  }
2341
- .legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: #888; }
2517
+ .legend .labels { display: flex; justify-content: space-between; font-size: 10px; color: var(--text-dim); }
2342
2518
  .legend .row { display: flex; align-items: center; gap: 6px; margin: 3px 0; }
2343
- .legend .swatch { width: 14px; height: 14px; border-radius: 3px; }
2519
+ .legend .swatch { width: 14px; height: 14px; border-radius: 4px; box-shadow: var(--shadow-sm); }
2344
2520
 
2345
2521
  /* Zone labels for hotspot */
2346
2522
  #zone-labels { position: absolute; pointer-events: none; }
@@ -2398,6 +2574,17 @@ function generateUnifiedHTML(data) {
2398
2574
  <select id="userSelect" onchange="onUserChange()"></select>
2399
2575
  </div>
2400
2576
 
2577
+ <div id="hotspot-controls">
2578
+ <label>Mode:</label>
2579
+ <button class="subtab active" onclick="switchHotspotMode('personal')">Personal</button>
2580
+ <button class="subtab" onclick="switchHotspotMode('team')">Team</button>
2581
+ <span class="sep-v"></span>
2582
+ <label>Scoring:</label>
2583
+ <button class="subtab hs-scoring active" onclick="switchHotspotScoring('binary')">Binary</button>
2584
+ <button class="subtab hs-scoring" onclick="switchHotspotScoring('authorship')">Authorship</button>
2585
+ <button class="subtab hs-scoring" onclick="switchHotspotScoring('weighted')">Weighted</button>
2586
+ </div>
2587
+
2401
2588
  <div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
2402
2589
 
2403
2590
  <div id="content-area">
@@ -2431,9 +2618,9 @@ function generateUnifiedHTML(data) {
2431
2618
  </div>
2432
2619
  <div class="legend" id="legend-coverage">
2433
2620
  <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>
2621
+ <div class="row"><div class="swatch" style="background:var(--color-critical)"></div> 0\u20131 (Risk)</div>
2622
+ <div class="row"><div class="swatch" style="background:var(--color-medium)"></div> 2\u20133 (Moderate)</div>
2623
+ <div class="row"><div class="swatch" style="background:var(--color-safe)"></div> 4+ (Safe)</div>
2437
2624
  </div>
2438
2625
  <div class="legend" id="legend-multiuser">
2439
2626
  <div>Familiarity</div>
@@ -2452,6 +2639,7 @@ const scoringData = {
2452
2639
  const coverageData = ${coverageTreeJson};
2453
2640
  const coverageRiskFiles = ${coverageRiskJson};
2454
2641
  const hotspotData = ${hotspotJson};
2642
+ const hotspotTeamFamiliarity = ${hotspotTeamFamJson};
2455
2643
  const multiUserData = ${multiUserTreeJson};
2456
2644
  const multiUserNames = ${multiUserNamesJson};
2457
2645
  const multiUserSummaries = ${multiUserSummariesJson};
@@ -2464,8 +2652,66 @@ let scoringPath = '';
2464
2652
  let coveragePath = '';
2465
2653
  let multiuserPath = '';
2466
2654
  let currentUser = 0;
2655
+ let hotspotMode = 'personal';
2656
+ let hotspotScoring = 'binary';
2467
2657
  const rendered = { scoring: false, coverage: false, hotspots: false, multiuser: false };
2468
2658
 
2659
+ // \u2500\u2500 Hotspot recalculation utilities \u2500\u2500
2660
+ function extractFlatScores(node) {
2661
+ const map = {};
2662
+ function walk(n) {
2663
+ if (n.type === 'file') { map[n.path] = n.score; }
2664
+ else if (n.children) { n.children.forEach(walk); }
2665
+ }
2666
+ walk(node);
2667
+ return map;
2668
+ }
2669
+
2670
+ const personalScores = {
2671
+ binary: extractFlatScores(scoringData.binary),
2672
+ authorship: extractFlatScores(scoringData.authorship),
2673
+ weighted: extractFlatScores(scoringData.weighted),
2674
+ };
2675
+
2676
+ function recalculateHotspotData() {
2677
+ const famScores = hotspotMode === 'personal'
2678
+ ? personalScores[hotspotScoring]
2679
+ : hotspotTeamFamiliarity;
2680
+ const maxFreq = d3.max(hotspotData, d => d.changeFrequency) || 1;
2681
+ return hotspotData.map(d => {
2682
+ const familiarity = famScores[d.path] || 0;
2683
+ const normalizedFreq = d.changeFrequency / maxFreq;
2684
+ const risk = normalizedFreq * (1 - familiarity);
2685
+ return { ...d, familiarity, risk,
2686
+ riskLevel: risk >= 0.6 ? 'critical' : risk >= 0.4 ? 'high' : risk >= 0.2 ? 'medium' : 'low',
2687
+ };
2688
+ }).sort((a, b) => b.risk - a.risk);
2689
+ }
2690
+
2691
+ function switchHotspotMode(mode) {
2692
+ hotspotMode = mode;
2693
+ document.querySelectorAll('#hotspot-controls .subtab:not(.hs-scoring)').forEach(el => {
2694
+ el.classList.toggle('active', el.textContent.toLowerCase() === mode);
2695
+ });
2696
+ // Disable scoring buttons in team mode
2697
+ const isTeam = mode === 'team';
2698
+ document.querySelectorAll('#hotspot-controls .hs-scoring').forEach(el => {
2699
+ el.classList.toggle('disabled', isTeam);
2700
+ });
2701
+ renderHotspot();
2702
+ renderHotspotSidebar();
2703
+ }
2704
+
2705
+ function switchHotspotScoring(mode) {
2706
+ if (hotspotMode === 'team') return;
2707
+ hotspotScoring = mode;
2708
+ document.querySelectorAll('#hotspot-controls .hs-scoring').forEach(el => {
2709
+ el.classList.toggle('active', el.textContent.toLowerCase() === mode);
2710
+ });
2711
+ renderHotspot();
2712
+ renderHotspotSidebar();
2713
+ }
2714
+
2469
2715
  // \u2500\u2500 Common utilities \u2500\u2500
2470
2716
  function scoreColor(score) {
2471
2717
  if (score <= 0) return '#e94560';
@@ -2583,6 +2829,7 @@ function switchTab(tab) {
2583
2829
  document.getElementById('scoring-controls').classList.toggle('visible', tab === 'scoring');
2584
2830
  document.getElementById('scoring-mode-desc').classList.toggle('visible', tab === 'scoring');
2585
2831
  document.getElementById('multiuser-controls').classList.toggle('visible', tab === 'multiuser');
2832
+ document.getElementById('hotspot-controls').classList.toggle('visible', tab === 'hotspots');
2586
2833
 
2587
2834
  // Show/hide breadcrumb
2588
2835
  const showBreadcrumb = tab === 'scoring' || tab === 'coverage' || tab === 'multiuser';
@@ -2857,15 +3104,17 @@ function renderHotspot() {
2857
3104
  height = contentH;
2858
3105
  }
2859
3106
 
3107
+ const currentData = recalculateHotspotData();
3108
+
2860
3109
  const margin = { top: 30, right: 30, bottom: 60, left: 70 };
2861
3110
  const innerW = width - margin.left - margin.right;
2862
3111
  const innerH = height - margin.top - margin.bottom;
2863
3112
 
2864
- const maxFreq = d3.max(hotspotData, d => d.changeFrequency) || 1;
3113
+ const maxFreq = d3.max(currentData, d => d.changeFrequency) || 1;
2865
3114
 
2866
3115
  const x = d3.scaleLinear().domain([0, 1]).range([0, innerW]);
2867
3116
  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]);
3117
+ const r = d3.scaleSqrt().domain([0, d3.max(currentData, d => d.lines) || 1]).range([3, 20]);
2869
3118
 
2870
3119
  const svg = d3.select('#hotspot-viz').append('svg').attr('width', width).attr('height', height);
2871
3120
  const g = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
@@ -2912,7 +3161,7 @@ function renderHotspot() {
2912
3161
  labels.appendChild(safeLabel);
2913
3162
 
2914
3163
  // Data points
2915
- g.selectAll('circle').data(hotspotData).join('circle')
3164
+ g.selectAll('circle').data(currentData).join('circle')
2916
3165
  .attr('cx', d => x(d.familiarity))
2917
3166
  .attr('cy', d => y(d.changeFrequency))
2918
3167
  .attr('r', d => r(d.lines))
@@ -2920,8 +3169,10 @@ function renderHotspot() {
2920
3169
  .attr('opacity', 0.7)
2921
3170
  .attr('stroke', 'none')
2922
3171
  .style('cursor', 'pointer')
3172
+ .style('filter', d => d.riskLevel === 'critical' ? 'drop-shadow(0 0 6px rgba(233,69,96,0.8))' : 'none')
3173
+ .style('transition', 'all 0.2s ease')
2923
3174
  .on('mouseover', function(event, d) {
2924
- d3.select(this).attr('opacity', 1).attr('stroke', '#fff').attr('stroke-width', 2);
3175
+ 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
3176
  showTooltipAt(
2926
3177
  '<strong>' + d.path + '</strong>' +
2927
3178
  '<br>Familiarity: ' + Math.round(d.familiarity * 100) + '%' +
@@ -2932,15 +3183,17 @@ function renderHotspot() {
2932
3183
  );
2933
3184
  })
2934
3185
  .on('mousemove', moveTooltip)
2935
- .on('mouseout', function() {
2936
- d3.select(this).attr('opacity', 0.7).attr('stroke', 'none');
3186
+ .on('mouseout', function(event, d) {
3187
+ const origFilter = d.riskLevel === 'critical' ? 'drop-shadow(0 0 6px rgba(233,69,96,0.8))' : 'none';
3188
+ d3.select(this).attr('opacity', 0.7).attr('stroke', 'none').style('filter', origFilter);
2937
3189
  hideTooltip();
2938
3190
  });
2939
3191
  }
2940
3192
 
2941
3193
  function renderHotspotSidebar() {
2942
3194
  const container = document.getElementById('hotspot-list');
2943
- const top = hotspotData.slice(0, 30);
3195
+ const currentData = recalculateHotspotData();
3196
+ const top = currentData.slice(0, 30);
2944
3197
  if (top.length === 0) {
2945
3198
  container.innerHTML = '<div style="color:#888">No active files in time window.</div>';
2946
3199
  return;
@@ -3075,12 +3328,14 @@ async function generateAndOpenUnifiedHTML(data, repoPath) {
3075
3328
  }
3076
3329
 
3077
3330
  // src/cli/index.ts
3331
+ var require2 = createRequire(import.meta.url);
3332
+ var pkg = require2("../../package.json");
3078
3333
  function collect(value, previous) {
3079
3334
  return previous.concat([value]);
3080
3335
  }
3081
3336
  function createProgram() {
3082
3337
  const program2 = new Command();
3083
- program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version("0.1.1").option(
3338
+ program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version(pkg.version).option(
3084
3339
  "-m, --mode <mode>",
3085
3340
  "Scoring mode: binary, authorship, weighted",
3086
3341
  "binary"