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.
- package/README.md +109 -78
- package/dist/bin/gitfamiliar.js +394 -132
- package/dist/bin/gitfamiliar.js.map +1 -1
- package/dist/{chunk-R5MGQGFI.js → chunk-6XUJIHN3.js} +3 -3
- package/dist/chunk-6XUJIHN3.js.map +1 -0
- package/dist/index.d.ts +3 -3
- 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-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 || "
|
|
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
|
|
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
|
-
|
|
58
|
+
contributorsPerFile: raw.contributorsPerFile || raw.contributors || raw.teamCoverage || false,
|
|
57
59
|
hotspot,
|
|
58
|
-
|
|
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
|
-
|
|
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 "
|
|
102
|
-
return "
|
|
103
|
-
case "
|
|
104
|
-
return "
|
|
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 === "
|
|
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 === "
|
|
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 === "
|
|
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
|
-
|
|
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 "
|
|
1212
|
-
return "
|
|
1213
|
-
case "
|
|
1214
|
-
return "
|
|
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 === "
|
|
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.
|
|
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(
|
|
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(
|
|
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({
|
|
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(
|
|
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(
|
|
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 (
|
|
2089
|
-
const [
|
|
2090
|
-
computeFamiliarity({ ...options, mode: "
|
|
2091
|
-
computeFamiliarity({ ...options, mode: "
|
|
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/
|
|
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/
|
|
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:
|
|
2109
|
-
userName:
|
|
2110
|
-
scoring: {
|
|
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
|
|
2122
|
-
const
|
|
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:
|
|
2152
|
-
color:
|
|
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: #
|
|
2161
|
-
border-bottom: 1px solid
|
|
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:
|
|
2167
|
-
#header .info { font-size: 13px; color:
|
|
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:
|
|
2173
|
-
border-bottom: 1px solid
|
|
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:
|
|
2253
|
+
color: var(--text-dim);
|
|
2180
2254
|
border-bottom: 2px solid transparent;
|
|
2181
2255
|
font-size: 14px;
|
|
2182
|
-
transition:
|
|
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:
|
|
2192
|
-
border-bottom: 1px solid
|
|
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:
|
|
2201
|
-
border: 1px solid
|
|
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
|
|
2285
|
+
transition: all var(--transition-smooth);
|
|
2286
|
+
box-shadow: var(--shadow-sm);
|
|
2206
2287
|
}
|
|
2207
|
-
.subtab:hover { color:
|
|
2208
|
-
.subtab.active { color:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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:
|
|
2352
|
+
background: var(--bg-panel);
|
|
2228
2353
|
font-size: 13px;
|
|
2229
|
-
border-bottom: 1px solid
|
|
2354
|
+
border-bottom: 1px solid var(--border);
|
|
2230
2355
|
display: none;
|
|
2231
2356
|
}
|
|
2232
2357
|
#breadcrumb.visible { display: block; }
|
|
2233
|
-
#breadcrumb span {
|
|
2234
|
-
|
|
2235
|
-
|
|
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:
|
|
2241
|
-
border-bottom: 1px solid
|
|
2373
|
+
background: var(--bg-panel);
|
|
2374
|
+
border-bottom: 1px solid var(--border);
|
|
2242
2375
|
font-size: 12px;
|
|
2243
|
-
color:
|
|
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:
|
|
2260
|
-
|
|
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:
|
|
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:
|
|
2267
|
-
border-bottom: 1px solid
|
|
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 .
|
|
2271
|
-
|
|
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:
|
|
2275
|
-
border-radius:
|
|
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:
|
|
2281
|
-
.risk-high { background:
|
|
2282
|
-
.risk-medium { background:
|
|
2283
|
-
.risk-low { background:
|
|
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:
|
|
2290
|
-
border-bottom: 1px solid
|
|
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:
|
|
2297
|
-
border: 1px solid
|
|
2298
|
-
background:
|
|
2299
|
-
color:
|
|
2300
|
-
border-radius:
|
|
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:
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
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:
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
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,
|
|
2338
|
-
border-radius:
|
|
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:
|
|
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:
|
|
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.
|
|
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')">
|
|
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
|
-
|
|
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('
|
|
2382
|
-
<button class="subtab" onclick="switchScoringMode('
|
|
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
|
|
2435
|
-
<div class="row"><div class="swatch" style="background
|
|
2436
|
-
<div class="row"><div class="swatch" style="background
|
|
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
|
-
|
|
2449
|
-
|
|
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 = '
|
|
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
|
-
|
|
2564
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
3348
|
+
program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version(pkg.version).option(
|
|
3084
3349
|
"-m, --mode <mode>",
|
|
3085
|
-
"Scoring mode:
|
|
3086
|
-
"
|
|
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
|
-
"--
|
|
3101
|
-
"
|
|
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.
|
|
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.
|
|
3387
|
+
if (options.contributorsPerFile) {
|
|
3126
3388
|
const result2 = await computeTeamCoverage(options);
|
|
3127
3389
|
if (options.html) {
|
|
3128
3390
|
await generateAndOpenCoverageHTML(result2, repoPath);
|