gitfamiliar 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ import {
8
8
  processBatch,
9
9
  resolveUser,
10
10
  walkFiles
11
- } from "../chunk-NGCQUB2H.js";
11
+ } from "../chunk-6XUJIHN3.js";
12
12
 
13
13
  // src/cli/index.ts
14
14
  import { createRequire } from "module";
@@ -25,7 +25,7 @@ var DEFAULT_EXPIRATION = {
25
25
 
26
26
  // src/cli/options.ts
27
27
  function parseOptions(raw, repoPath) {
28
- const mode = validateMode(raw.mode || "binary");
28
+ const mode = validateMode(raw.mode || "committed");
29
29
  let weights = DEFAULT_WEIGHTS;
30
30
  if (raw.weights) {
31
31
  weights = parseWeights(raw.weights);
@@ -45,7 +45,8 @@ function parseOptions(raw, repoPath) {
45
45
  hotspot = "personal";
46
46
  }
47
47
  }
48
- const windowDays = raw.window ? parseInt(raw.window, 10) : void 0;
48
+ const sinceRaw = raw.since || raw.window;
49
+ const sinceDays = sinceRaw ? parseInt(sinceRaw, 10) : void 0;
49
50
  return {
50
51
  mode,
51
52
  user,
@@ -54,13 +55,20 @@ function parseOptions(raw, repoPath) {
54
55
  weights,
55
56
  repoPath,
56
57
  team: raw.team || false,
57
- teamCoverage: raw.teamCoverage || false,
58
+ contributorsPerFile: raw.contributorsPerFile || raw.contributors || raw.teamCoverage || false,
58
59
  hotspot,
59
- window: windowDays
60
+ since: sinceDays
60
61
  };
61
62
  }
63
+ var MODE_ALIASES = {
64
+ binary: "committed",
65
+ authorship: "code-coverage"
66
+ };
62
67
  function validateMode(mode) {
63
- const valid = ["binary", "authorship", "weighted"];
68
+ if (mode in MODE_ALIASES) {
69
+ return MODE_ALIASES[mode];
70
+ }
71
+ const valid = ["committed", "code-coverage", "weighted"];
64
72
  if (!valid.includes(mode)) {
65
73
  throw new Error(
66
74
  `Invalid mode: "${mode}". Valid modes: ${valid.join(", ")}`
@@ -99,10 +107,10 @@ function formatPercent(score) {
99
107
  }
100
108
  function getModeLabel(mode) {
101
109
  switch (mode) {
102
- case "binary":
103
- return "Binary mode";
104
- case "authorship":
105
- return "Authorship mode";
110
+ case "committed":
111
+ return "Committed mode";
112
+ case "code-coverage":
113
+ return "Code Coverage mode";
106
114
  case "weighted":
107
115
  return "Weighted mode";
108
116
  default:
@@ -129,7 +137,7 @@ function renderFolder(node, indent, mode, maxDepth) {
129
137
  NAME_COLUMN_WIDTH - prefixWidth - name.length
130
138
  );
131
139
  const padding = " ".repeat(padWidth);
132
- if (mode === "binary") {
140
+ if (mode === "committed") {
133
141
  const readCount = folder.readCount || 0;
134
142
  lines.push(
135
143
  `${prefix}${chalk.bold(name)}${padding} ${bar} ${pct.padStart(4)} (${readCount}/${folder.fileCount} files)`
@@ -153,7 +161,7 @@ function renderTerminal(result) {
153
161
  chalk.bold(`GitFamiliar \u2014 ${repoName} (${getModeLabel(mode)})`)
154
162
  );
155
163
  console.log("");
156
- if (mode === "binary") {
164
+ if (mode === "committed") {
157
165
  const readCount = tree.readCount || 0;
158
166
  const pct = formatPercent(tree.score);
159
167
  console.log(`Overall: ${readCount}/${tree.fileCount} files (${pct})`);
@@ -167,7 +175,7 @@ function renderTerminal(result) {
167
175
  console.log(line);
168
176
  }
169
177
  console.log("");
170
- if (mode === "binary") {
178
+ if (mode === "committed") {
171
179
  const { writtenCount } = result;
172
180
  console.log(`Written: ${writtenCount} files`);
173
181
  console.log("");
@@ -1087,7 +1095,7 @@ async function computeMultiUser(options) {
1087
1095
  ...options,
1088
1096
  user: userName,
1089
1097
  team: false,
1090
- teamCoverage: false
1098
+ contributorsPerFile: false
1091
1099
  };
1092
1100
  const result = await computeFamiliarity(userOptions);
1093
1101
  results.push({ userName, result });
@@ -1209,10 +1217,10 @@ function formatPercent2(score) {
1209
1217
  }
1210
1218
  function getModeLabel2(mode) {
1211
1219
  switch (mode) {
1212
- case "binary":
1213
- return "Binary mode";
1214
- case "authorship":
1215
- return "Authorship mode";
1220
+ case "committed":
1221
+ return "Committed mode";
1222
+ case "code-coverage":
1223
+ return "Code Coverage mode";
1216
1224
  case "weighted":
1217
1225
  return "Weighted mode";
1218
1226
  default:
@@ -1257,7 +1265,7 @@ function renderMultiUserTerminal(result) {
1257
1265
  const name = truncateName(summary.user.name, 14).padEnd(14);
1258
1266
  const bar = makeBar2(summary.overallScore);
1259
1267
  const pct = formatPercent2(summary.overallScore);
1260
- if (mode === "binary") {
1268
+ if (mode === "committed") {
1261
1269
  console.log(
1262
1270
  ` ${name} ${bar} ${pct.padStart(4)} (${summary.writtenCount}/${totalFiles} files)`
1263
1271
  );
@@ -1627,7 +1635,7 @@ async function computeHotspots(options) {
1627
1635
  const repoRoot = await gitClient.getRepoRoot();
1628
1636
  const filter = createFilter(repoRoot);
1629
1637
  const tree = await buildFileTree(gitClient, filter);
1630
- const timeWindow = options.window || DEFAULT_WINDOW;
1638
+ const timeWindow = options.since || DEFAULT_WINDOW;
1631
1639
  const isTeamMode = options.hotspot === "team";
1632
1640
  const trackedFiles = /* @__PURE__ */ new Set();
1633
1641
  walkFiles(tree, (f) => trackedFiles.add(f.path));
@@ -1649,7 +1657,7 @@ async function computeHotspots(options) {
1649
1657
  const result = await computeFamiliarity({
1650
1658
  ...options,
1651
1659
  team: false,
1652
- teamCoverage: false
1660
+ contributorsPerFile: false
1653
1661
  });
1654
1662
  userName = result.userName;
1655
1663
  familiarityMap = /* @__PURE__ */ new Map();
@@ -2104,10 +2112,10 @@ async function generateAndOpenHotspotHTML(result, repoPath) {
2104
2112
  // src/core/unified.ts
2105
2113
  async function computeUnified(options) {
2106
2114
  console.log("Computing unified dashboard data...");
2107
- console.log(" [1/4] Scoring (binary, authorship, weighted)...");
2108
- const [binary, authorship, weighted] = await Promise.all([
2109
- computeFamiliarity({ ...options, mode: "binary" }),
2110
- computeFamiliarity({ ...options, mode: "authorship" }),
2115
+ console.log(" [1/4] Scoring (committed, code-coverage, weighted)...");
2116
+ const [committed, codeCoverage, weighted] = await Promise.all([
2117
+ computeFamiliarity({ ...options, mode: "committed" }),
2118
+ computeFamiliarity({ ...options, mode: "code-coverage" }),
2111
2119
  computeFamiliarity({ ...options, mode: "weighted" })
2112
2120
  ]);
2113
2121
  console.log(" [2/4] Team coverage...");
@@ -2138,9 +2146,9 @@ async function computeUnified(options) {
2138
2146
  });
2139
2147
  console.log("Done.");
2140
2148
  return {
2141
- repoName: binary.repoName,
2142
- userName: binary.userName,
2143
- scoring: { binary, authorship, weighted },
2149
+ repoName: committed.repoName,
2150
+ userName: committed.userName,
2151
+ scoring: { committed, codeCoverage, weighted },
2144
2152
  coverage,
2145
2153
  hotspot,
2146
2154
  hotspotTeamFamiliarity,
@@ -2152,8 +2160,10 @@ async function computeUnified(options) {
2152
2160
  import { writeFileSync as writeFileSync5 } from "fs";
2153
2161
  import { join as join5 } from "path";
2154
2162
  function generateUnifiedHTML(data) {
2155
- const scoringBinaryJson = JSON.stringify(data.scoring.binary.tree);
2156
- const scoringAuthorshipJson = JSON.stringify(data.scoring.authorship.tree);
2163
+ const scoringCommittedJson = JSON.stringify(data.scoring.committed.tree);
2164
+ const scoringCodeCoverageJson = JSON.stringify(
2165
+ data.scoring.codeCoverage.tree
2166
+ );
2157
2167
  const scoringWeightedJson = JSON.stringify(data.scoring.weighted.tree);
2158
2168
  const coverageTreeJson = JSON.stringify(data.coverage.tree);
2159
2169
  const coverageRiskJson = JSON.stringify(data.coverage.riskFiles);
@@ -2530,12 +2540,12 @@ function generateUnifiedHTML(data) {
2530
2540
  <body>
2531
2541
  <div id="header">
2532
2542
  <h1>GitFamiliar \u2014 ${data.repoName}</h1>
2533
- <div class="info">${data.userName} | ${data.scoring.binary.totalFiles} files</div>
2543
+ <div class="info">${data.userName} | ${data.scoring.committed.totalFiles} files</div>
2534
2544
  </div>
2535
2545
 
2536
2546
  <div id="tabs">
2537
2547
  <div class="tab active" onclick="switchTab('scoring')">Scoring</div>
2538
- <div class="tab" onclick="switchTab('coverage')">Coverage</div>
2548
+ <div class="tab" onclick="switchTab('coverage')">Contributors</div>
2539
2549
  <div class="tab" onclick="switchTab('multiuser')">Multi-User</div>
2540
2550
  <div class="tab" onclick="switchTab('hotspots')">Hotspots</div>
2541
2551
  </div>
@@ -2544,7 +2554,7 @@ function generateUnifiedHTML(data) {
2544
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.
2545
2555
  </div>
2546
2556
  <div id="tab-desc-coverage" class="tab-desc">
2547
- Team knowledge distribution: how many people have contributed to each file. Low contributor count = high bus factor risk.
2557
+ Contributors per file: how many people have committed to each file. Low contributor count = high bus factor risk.
2548
2558
  </div>
2549
2559
  <div id="tab-desc-multiuser" class="tab-desc">
2550
2560
  Compare familiarity scores across team members. Select a user to see the codebase colored by their knowledge.
@@ -2554,9 +2564,9 @@ function generateUnifiedHTML(data) {
2554
2564
  </div>
2555
2565
 
2556
2566
  <div id="scoring-controls" class="visible">
2557
- <button class="subtab active" onclick="switchScoringMode('binary')">Binary</button>
2558
- <button class="subtab" onclick="switchScoringMode('authorship')">Authorship</button>
2559
- <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>
2560
2570
  <div id="weight-controls">
2561
2571
  <span>Blame:</span>
2562
2572
  <span class="weight-label" id="blame-label">50%</span>
@@ -2576,13 +2586,13 @@ function generateUnifiedHTML(data) {
2576
2586
 
2577
2587
  <div id="hotspot-controls">
2578
2588
  <label>Mode:</label>
2579
- <button class="subtab active" onclick="switchHotspotMode('personal')">Personal</button>
2580
- <button class="subtab" onclick="switchHotspotMode('team')">Team</button>
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>
2581
2591
  <span class="sep-v"></span>
2582
2592
  <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>
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>
2586
2596
  </div>
2587
2597
 
2588
2598
  <div id="breadcrumb"><span onclick="zoomTo('')">root</span></div>
@@ -2632,8 +2642,8 @@ function generateUnifiedHTML(data) {
2632
2642
  <script>
2633
2643
  // \u2500\u2500 Data \u2500\u2500
2634
2644
  const scoringData = {
2635
- binary: ${scoringBinaryJson},
2636
- authorship: ${scoringAuthorshipJson},
2645
+ committed: ${scoringCommittedJson},
2646
+ 'code-coverage': ${scoringCodeCoverageJson},
2637
2647
  weighted: ${scoringWeightedJson},
2638
2648
  };
2639
2649
  const coverageData = ${coverageTreeJson};
@@ -2646,14 +2656,14 @@ const multiUserSummaries = ${multiUserSummariesJson};
2646
2656
 
2647
2657
  // \u2500\u2500 State \u2500\u2500
2648
2658
  let activeTab = 'scoring';
2649
- let scoringMode = 'binary';
2659
+ let scoringMode = 'committed';
2650
2660
  let blameWeight = 0.5;
2651
2661
  let scoringPath = '';
2652
2662
  let coveragePath = '';
2653
2663
  let multiuserPath = '';
2654
2664
  let currentUser = 0;
2655
2665
  let hotspotMode = 'personal';
2656
- let hotspotScoring = 'binary';
2666
+ let hotspotScoring = 'committed';
2657
2667
  const rendered = { scoring: false, coverage: false, hotspots: false, multiuser: false };
2658
2668
 
2659
2669
  // \u2500\u2500 Hotspot recalculation utilities \u2500\u2500
@@ -2668,8 +2678,8 @@ function extractFlatScores(node) {
2668
2678
  }
2669
2679
 
2670
2680
  const personalScores = {
2671
- binary: extractFlatScores(scoringData.binary),
2672
- authorship: extractFlatScores(scoringData.authorship),
2681
+ committed: extractFlatScores(scoringData.committed),
2682
+ 'code-coverage': extractFlatScores(scoringData['code-coverage']),
2673
2683
  weighted: extractFlatScores(scoringData.weighted),
2674
2684
  };
2675
2685
 
@@ -2691,7 +2701,7 @@ function recalculateHotspotData() {
2691
2701
  function switchHotspotMode(mode) {
2692
2702
  hotspotMode = mode;
2693
2703
  document.querySelectorAll('#hotspot-controls .subtab:not(.hs-scoring)').forEach(el => {
2694
- el.classList.toggle('active', el.textContent.toLowerCase() === mode);
2704
+ el.classList.toggle('active', el.dataset.mode === mode);
2695
2705
  });
2696
2706
  // Disable scoring buttons in team mode
2697
2707
  const isTeam = mode === 'team';
@@ -2706,7 +2716,7 @@ function switchHotspotScoring(mode) {
2706
2716
  if (hotspotMode === 'team') return;
2707
2717
  hotspotScoring = mode;
2708
2718
  document.querySelectorAll('#hotspot-controls .hs-scoring').forEach(el => {
2709
- el.classList.toggle('active', el.textContent.toLowerCase() === mode);
2719
+ el.classList.toggle('active', el.dataset.mode === mode);
2710
2720
  });
2711
2721
  renderHotspot();
2712
2722
  renderHotspotSidebar();
@@ -2806,8 +2816,8 @@ function truncateLabel(name, w, h) {
2806
2816
 
2807
2817
  // \u2500\u2500 Tab switching \u2500\u2500
2808
2818
  const modeDescriptions = {
2809
- binary: 'Binary: Have you ever committed to this file? Yes (green) or No (red).',
2810
- authorship: 'Authorship: How much of the current code did you write? Based on git blame line ownership.',
2819
+ committed: 'Committed: Have you ever committed to this file? Yes (green) or No (red).',
2820
+ 'code-coverage': 'Code Coverage: How much of the current code did you write? Based on git blame line ownership.',
2811
2821
  weighted: 'Weighted: Combines blame ownership and commit history with adjustable weights. Use the sliders to tune.',
2812
2822
  };
2813
2823
 
@@ -2883,7 +2893,7 @@ function switchScoringMode(mode) {
2883
2893
  scoringPath = '';
2884
2894
  updateBreadcrumb('');
2885
2895
  document.querySelectorAll('#scoring-controls .subtab').forEach(el => {
2886
- el.classList.toggle('active', el.textContent.toLowerCase() === mode);
2896
+ el.classList.toggle('active', el.dataset.mode === mode);
2887
2897
  });
2888
2898
  document.getElementById('weight-controls').classList.toggle('visible', mode === 'weighted');
2889
2899
  document.getElementById('mode-desc-text').textContent = modeDescriptions[mode];
@@ -3337,8 +3347,8 @@ function createProgram() {
3337
3347
  const program2 = new Command();
3338
3348
  program2.name("gitfamiliar").description("Visualize your code familiarity from Git history").version(pkg.version).option(
3339
3349
  "-m, --mode <mode>",
3340
- "Scoring mode: binary, authorship, weighted",
3341
- "binary"
3350
+ "Scoring mode: committed, code-coverage, weighted",
3351
+ "committed"
3342
3352
  ).option(
3343
3353
  "-u, --user <user>",
3344
3354
  "Git user name or email (repeatable for comparison)",
@@ -3352,18 +3362,15 @@ function createProgram() {
3352
3362
  "-w, --weights <weights>",
3353
3363
  'Weights for weighted mode: blame,commit (e.g., "0.5,0.5")'
3354
3364
  ).option("--team", "Compare all contributors", false).option(
3355
- "--team-coverage",
3356
- "Show team coverage map (bus factor analysis)",
3365
+ "--contributors-per-file",
3366
+ "Analyze number of contributors per file (bus factor)",
3357
3367
  false
3358
- ).option("--hotspot [mode]", "Hotspot analysis: personal (default) or team").option(
3359
- "--window <days>",
3360
- "Time window for hotspot analysis in days (default: 90)"
3361
- ).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) => {
3362
3369
  try {
3363
3370
  const repoPath = process.cwd();
3364
3371
  const options = parseOptions(rawOptions, repoPath);
3365
3372
  const isMultiUserCheck = options.team || Array.isArray(options.user) && options.user.length > 1;
3366
- if (options.html && !options.hotspot && !options.teamCoverage && !isMultiUserCheck) {
3373
+ if (options.html && !options.hotspot && !options.contributorsPerFile && !isMultiUserCheck) {
3367
3374
  const data = await computeUnified(options);
3368
3375
  await generateAndOpenUnifiedHTML(data, repoPath);
3369
3376
  return;
@@ -3377,7 +3384,7 @@ function createProgram() {
3377
3384
  }
3378
3385
  return;
3379
3386
  }
3380
- if (options.teamCoverage) {
3387
+ if (options.contributorsPerFile) {
3381
3388
  const result2 = await computeTeamCoverage(options);
3382
3389
  if (options.html) {
3383
3390
  await generateAndOpenCoverageHTML(result2, repoPath);