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.
- package/README.md +109 -78
- package/dist/bin/gitfamiliar.js +337 -82
- package/dist/bin/gitfamiliar.js.map +1 -1
- package/dist/{chunk-R5MGQGFI.js → chunk-NGCQUB2H.js} +1 -1
- package/dist/chunk-NGCQUB2H.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-R5MGQGFI.js.map +0 -1
package/dist/bin/gitfamiliar.js
CHANGED
|
@@ -8,9 +8,10 @@ import {
|
|
|
8
8
|
processBatch,
|
|
9
9
|
resolveUser,
|
|
10
10
|
walkFiles
|
|
11
|
-
} from "../chunk-
|
|
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(
|
|
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(
|
|
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({
|
|
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(
|
|
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(
|
|
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/
|
|
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/
|
|
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:
|
|
2152
|
-
color:
|
|
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: #
|
|
2161
|
-
border-bottom: 1px solid
|
|
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:
|
|
2167
|
-
#header .info { font-size: 13px; color:
|
|
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:
|
|
2173
|
-
border-bottom: 1px solid
|
|
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:
|
|
2243
|
+
color: var(--text-dim);
|
|
2180
2244
|
border-bottom: 2px solid transparent;
|
|
2181
2245
|
font-size: 14px;
|
|
2182
|
-
transition:
|
|
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:
|
|
2192
|
-
border-bottom: 1px solid
|
|
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:
|
|
2201
|
-
border: 1px solid
|
|
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
|
|
2275
|
+
transition: all var(--transition-smooth);
|
|
2276
|
+
box-shadow: var(--shadow-sm);
|
|
2206
2277
|
}
|
|
2207
|
-
.subtab:hover { color:
|
|
2208
|
-
.subtab.active { color:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
2342
|
+
background: var(--bg-panel);
|
|
2228
2343
|
font-size: 13px;
|
|
2229
|
-
border-bottom: 1px solid
|
|
2344
|
+
border-bottom: 1px solid var(--border);
|
|
2230
2345
|
display: none;
|
|
2231
2346
|
}
|
|
2232
2347
|
#breadcrumb.visible { display: block; }
|
|
2233
|
-
#breadcrumb span {
|
|
2234
|
-
|
|
2235
|
-
|
|
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:
|
|
2241
|
-
border-bottom: 1px solid
|
|
2363
|
+
background: var(--bg-panel);
|
|
2364
|
+
border-bottom: 1px solid var(--border);
|
|
2242
2365
|
font-size: 12px;
|
|
2243
|
-
color:
|
|
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:
|
|
2260
|
-
|
|
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:
|
|
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:
|
|
2267
|
-
border-bottom: 1px solid
|
|
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 .
|
|
2271
|
-
|
|
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:
|
|
2275
|
-
border-radius:
|
|
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:
|
|
2281
|
-
.risk-high { background:
|
|
2282
|
-
.risk-medium { background:
|
|
2283
|
-
.risk-low { background:
|
|
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:
|
|
2290
|
-
border-bottom: 1px solid
|
|
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:
|
|
2297
|
-
border: 1px solid
|
|
2298
|
-
background:
|
|
2299
|
-
color:
|
|
2300
|
-
border-radius:
|
|
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:
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
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:
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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,
|
|
2338
|
-
border-radius:
|
|
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:
|
|
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:
|
|
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
|
|
2435
|
-
<div class="row"><div class="swatch" style="background
|
|
2436
|
-
<div class="row"><div class="swatch" style="background
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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"
|