Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ Per-developer report combining multiple metrics.
| Active days | Unique dates with at least one commit |
| Pace | commits / active_days (smooths bursts — a dev with 100 commits on 2 days and silence for 28 shows pace=50, which reads as a steady rate but isn't) |
| Weekend % | commits on Saturday+Sunday / total commits × 100 |
| Scope | Top 5 directories by unique file count, as % of total files touched |
| Extensions | Top 5 file extensions the dev touched, sorted by **files desc** (tiebreak churn desc, then ext asc) so the displayed `Pct` is monotonic with the sort order and HTML bar widths read correctly. `Pct` is `Files/FilesTouched * 100`; the raw dev-attributable `Churn` (sum of `devLines[email]` across bucket files) is kept on the struct for JSON consumers who want a churn-ranked view. Answers the "language/skill fingerprint" question (`.go` + `.yaml` → backend+infra; `.tsx` + `.ts` + `.css` → frontend). **Caveats:** (1) bucket is derived from the file's canonical (post-rename) path — a dev who worked on `foo.js` pre-migration still shows up under `.ts` if it was later renamed; per-era per-dev attribution would need `byExt` to carry a dev dimension, which isn't tracked. (2) `Pct` values may sum to less than 100% when the dev appears as a contributor on files without adding lines (pure-rename contributions), since the extension aggregation only walks files with non-zero `devLines[email]`. |
| Scope | Top 5 directories by unique file count, as % of the dev's **authored** files — i.e. files where the dev added or removed at least one line. Pure renames (file appears in the dev's change set with zero line changes) are excluded from both numerator and denominator so the visible Pct values sum to 100% (modulo the top-5 truncation). Same denominator is used for Extensions and for the Herfindahl specialization index, keeping the three consistent. |
| Extensions | Top 5 file extensions the dev touched, sorted by **files desc** (tiebreak churn desc, then ext asc) so the displayed `Pct` is monotonic with the sort order and HTML bar widths read correctly. `Pct` is `Files / authored * 100` where `authored` is the count of files the dev added or removed at least one line on — same denominator as Scope, so Pcts sum to 100% modulo top-5 truncation. The raw dev-attributable `Churn` (sum of `devLines[email]` across bucket files) is kept on the struct for JSON consumers who want a churn-ranked view. Answers the "language/skill fingerprint" question (`.go` + `.yaml` → backend+infra; `.tsx` + `.ts` + `.css` → frontend). **Attribution caveat:** bucket is derived from the file's canonical (post-rename) path — a dev who worked on `foo.js` pre-migration still shows up under `.ts` if it was later renamed; per-era per-dev attribution would need `byExt` to carry a dev dimension, which isn't tracked. |
| Specialization | Herfindahl index over the **full** per-directory file-count distribution: Σ pᵢ² where pᵢ is the share of the dev's files in directory i. 1 = all files in one directory (narrow specialist); 1/N for a uniform spread across N directories; approaches 0 as the distribution widens. Computed before the top-5 Scope truncation so it reflects actual breadth. Labels (see `specBroadGeneralistMax`, `specBalancedMax`, `specFocusedMax` constants): `< 0.15` broad generalist, `< 0.35` balanced, `< 0.7` focused specialist, `≥ 0.7` narrow specialist. Herfindahl, not Gini, because Gini would collapse "1 file in 1 dir" and "1 file in each of 5 dirs" to the same value (both have zero inequality among buckets), which misses the specialization distinction. **Measures file distribution, not domain expertise** — see caveat below. **Display vs raw:** CLI and HTML show the value rounded to 3 decimals (`%.3f`) for readability; JSON output preserves the full float64. Band classification runs against the raw float, so a value like 0.149 lands in `broad generalist` even though %.2f would have rounded it to `0.15`. JSON consumers that reproduce the banding must use the raw value, not a rounded version. |
| Contribution type | Based on del/add ratio: growth (<0.4), balanced (0.4-0.8), refactor (>0.8) |
| Collaborators | Top 5 devs sharing code with this dev. Ranked by `shared_lines` (Σ min(linesA, linesB) across shared files), tiebreak `shared_files`, then email. Same `shared_lines` semantics as the Developer Network metric — discounts trivial one-line touches so "collaborator" reflects real overlap. |
Expand Down
7 changes: 4 additions & 3 deletions internal/report/profile_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,20 @@ footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid #d0d7de; col
{{range $i, $s := .Profile.Scope}}<div style="flex:{{printf "%.0f" $s.Pct}}; background:{{index (list "#0969da" "#2da44e" "#8250df" "#bf8700" "#cf222e") $i}}; display:flex; align-items:center; justify-content:center; color:#fff; font-size:10px; min-width:30px; overflow:hidden;" title="{{$s.Dir}} — {{$s.Files}} files ({{printf "%.0f" $s.Pct}}%)">{{if gt $s.Pct 8.0}}{{$s.Dir}} {{printf "%.0f" $s.Pct}}%{{end}}</div>{{end}}
</div>
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:4px; font-size:11px; color:#656d76;">
{{range $i, $s := .Profile.Scope}}<span><span style="display:inline-block; width:8px; height:8px; border-radius:2px; background:{{index (list "#0969da" "#2da44e" "#8250df" "#bf8700" "#cf222e") $i}};"></span> {{$s.Dir}} ({{printf "%.0f" $s.Pct}}%)</span>{{end}}
{{range $i, $s := .Profile.Scope}}<span><span style="display:inline-block; width:8px; height:8px; border-radius:2px; background:{{index (list "#0969da" "#2da44e" "#8250df" "#bf8700" "#cf222e") $i}};"></span> {{$s.Dir}} ({{printf "%.0f" $s.Pct}}%)</span>{{end}}{{if gt .Profile.ScopeHidden 0}}<span style="font-style:italic;">+{{.Profile.ScopeHidden}} more directories not shown</span>{{end}}
</div>
</div>

{{if .Profile.Extensions}}
<div style="margin-bottom:16px;">
<div style="font-size:13px; font-weight:600; margin-bottom:2px;">Extensions</div>
<div class="hint" style="margin-bottom:6px;">The dev's language/skill fingerprint by share of files touched. Extension attribution uses the file's current canonical path, so cross-extension renames (e.g. <code>.js → .ts</code>) credit pre-rename work to the new extension. · {{docRef "profile"}}</div>
{{/* Teal monochrome progression — intentionally a different color family from Scope's categorical palette above. Same-index colors in Scope (blue/green/purple/orange/red) would invite false cross-chart correlation ("the blue dir uses the blue ext"). The monochromatic treatment also visually signals that this is a single ordered distribution, not five independent categories. Stopped at #3fa3ae so even the lightest shade keeps adequate contrast with white text when a tail bucket is large enough to show a label. */}}
<div style="display:flex; height:28px; border-radius:4px; overflow:hidden; gap:1px;">
{{range $i, $e := .Profile.Extensions}}<div style="flex:{{printf "%.0f" $e.Pct}}; background:{{index (list "#0969da" "#2da44e" "#8250df" "#bf8700" "#cf222e") $i}}; display:flex; align-items:center; justify-content:center; color:#fff; font-size:10px; min-width:30px; overflow:hidden;" title="{{$e.Ext}} — {{$e.Files}} files ({{printf "%.0f" $e.Pct}}%)">{{if gt $e.Pct 8.0}}{{$e.Ext}} {{printf "%.0f" $e.Pct}}%{{end}}</div>{{end}}
{{range $i, $e := .Profile.Extensions}}<div style="flex:{{printf "%.0f" $e.Pct}}; background:{{index (list "#0e4c5b" "#176374" "#287e8c" "#3fa3ae" "#5dbdb7") $i}}; display:flex; align-items:center; justify-content:center; color:#fff; font-size:10px; min-width:30px; overflow:hidden;" title="{{$e.Ext}} — {{$e.Files}} files ({{printf "%.0f" $e.Pct}}%)">{{if gt $e.Pct 8.0}}{{$e.Ext}} {{printf "%.0f" $e.Pct}}%{{end}}</div>{{end}}
</div>
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:4px; font-size:11px; color:#656d76;">
{{range $i, $e := .Profile.Extensions}}<span><span style="display:inline-block; width:8px; height:8px; border-radius:2px; background:{{index (list "#0969da" "#2da44e" "#8250df" "#bf8700" "#cf222e") $i}};"></span> {{$e.Ext}} ({{printf "%.0f" $e.Pct}}%)</span>{{end}}
{{range $i, $e := .Profile.Extensions}}<span><span style="display:inline-block; width:8px; height:8px; border-radius:2px; background:{{index (list "#0e4c5b" "#176374" "#287e8c" "#3fa3ae" "#5dbdb7") $i}};"></span> {{$e.Ext}} ({{printf "%.0f" $e.Pct}}%)</span>{{end}}{{if gt .Profile.ExtensionsHidden 0}}<span style="font-style:italic;">+{{.Profile.ExtensionsHidden}} more extensions not shown</span>{{end}}
</div>
</div>
{{end}}
Expand Down
4 changes: 2 additions & 2 deletions internal/report/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,11 @@ footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid #d0d7de; col

<div style="display:grid; grid-template-columns:110px 1fr; gap:4px 12px; font-size:13px; margin-bottom:12px;">
<span style="color:#656d76;">Scope</span>
<span>{{range $i, $s := .Scope}}{{if $i}}, {{end}}<b>{{$s.Dir}}</b> ({{printf "%.0f" $s.Pct}}%){{end}}</span>
<span>{{range $i, $s := .Scope}}{{if $i}}, {{end}}<b>{{$s.Dir}}</b> ({{printf "%.0f" $s.Pct}}%){{end}}{{if gt .ScopeHidden 0}} <span style="color:#656d76; font-style:italic;">(+{{.ScopeHidden}} more)</span>{{end}}</span>

{{if .Extensions}}
<span style="color:#656d76;">Extensions</span>
<span>{{range $i, $e := .Extensions}}{{if $i}}, {{end}}<b>{{$e.Ext}}</b> ({{printf "%.0f" $e.Pct}}%){{end}}</span>
<span>{{range $i, $e := .Extensions}}{{if $i}}, {{end}}<b>{{$e.Ext}}</b> ({{printf "%.0f" $e.Pct}}%){{end}}{{if gt .ExtensionsHidden 0}} <span style="color:#656d76; font-style:italic;">(+{{.ExtensionsHidden}} more)</span>{{end}}</span>
{{end}}

<span style="color:#656d76;">Specialization</span>
Expand Down
Loading
Loading