feat(analyzer): influence from eigenvector centrality over the real review graph
Change
Replace the projectID/100 "cross-group" proxy (30% of the influence score) with a named network centrality computed over the real review/collaboration graph manifold already builds in connections.go.
GitLab project IDs are global creation-order integers with no namespace relationship, so bucketing by projectID/100 measured noise, not organizational reach. The methodology (round 1 section 4.1; round 2 section 4) calls for a centrality over the real edges. The projectID/100 bucketing is removed entirely.
Centrality method + weighting (per the methodology doc, section 2.4)
influence = 0.5*eigenvector_centrality + 0.3*review_reach + 0.2*project_breadth- Eigenvector centrality (50%, dominant): Bonacich 1987. Power iteration on the symmetric weighted adjacency built from the same edges
ComputeConnectionsbuilds: Merge Request (MR) reviewer/author pairs, MR merge-user/author pairs, and project co-membership (with the same large-project member cap). L2-normalized each step, capped at 100 iterations, tolerance 1e-9. Pure standard library. - Review reach (30%): distinct MR authors reviewed; a transparent degree baseline (Freeman 1978).
- Project breadth (20%): distinct projects active in.
Each factor is percentile-normalized to [0,1], then weighted-summed. Edge-definition caveat noted in code (Nia 2010): centrality is sensitive to how the graph is built; the edge set is reported (undirected weighted review + merge + co-membership).
New constants (Centrality, CentralityMaxIter, CentralityTolerance) live in the Methodology config. Dashboard influence-methodology prose updated to match.
Before / after (captured 300-user baseline)
Top-10 influencers, before (projectID/100 blend) vs after (centrality blend):
| Rank | BEFORE user (score) | AFTER user (score) |
|---|---|---|
| 1 | 300 (0.981) | 203 (0.881) |
| 2 | 298 (0.979) | 98 (0.855) |
| 3 | 292 (0.974) | 111 (0.818) |
| 4 | 275 (0.963) | 121 (0.806) |
| 5 | 279 (0.952) | 85 (0.798) |
| 6 | 299 (0.950) | 105 (0.796) |
| 7 | 290 (0.948) | 210 (0.792) |
| 8 | 285 (0.946) | 79 (0.788) |
| 9 | 291 (0.942) | 205 (0.785) |
| 10 | 288 (0.941) | 204 (0.784) |
Top-10 overlap: 0 of 10. The old top-10 was dominated by high-numbered (late-created) user IDs touching high-ID projects, the exact projectID/100 artifact. The new top-10 surfaces users who are genuinely central in the review graph.
Distribution (n=301, all nonzero in both):
| min | median | max | nonzero | |
|---|---|---|---|---|
| BEFORE | 0.026 | 0.295 | 0.981 | 301/301 |
| AFTER | 0.001 | 0.382 | 0.881 | 301/301 |
Downstream effects
Influence-only, as expected. Domain scores (0 changed), archetypes (0 changed), gaps (identical), health score (43 -> 43), archetype distribution, domain statistics, and adoption sentiment are all unchanged.
The one legitimate downstream consumer is team risk_score: ComputeTeamRisk blends 0.3*(1 - avg_influence), so all 20 teams shifted risk_score slightly (no other team field changed). This is reported honestly, not a regression.
Validation
go vet ./...: cleanCGO_ENABLED=1 go test -race ./...: pass (all packages)- golangci-lint: 0 issues
- coverage: 67.5% total (>= 65% gate); analyzer package 94.0%
- Deterministic: a double-analyze of the same input is byte-identical across profiles, archetypes, teams, gaps, summary.
Test values: existing influence tests assert ordering/bounds invariants (not pinned values) and still pass under the new formula. Added TestInfluenceScores_EigenvectorBlend pinning the new computed outputs with a comment. No test unrelated to influence changed.