impact_score 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f51a2a44a58f5e947305b65a5a894f999086f42110493498fbc60faa35abe3b6
4
+ data.tar.gz: f3493e5b71269d76b1c0f8f8627bebfebfe6558fca7a144b2047b5beb822855a
5
+ SHA512:
6
+ metadata.gz: 871dfb27c6d8f32e326a7f276a5c5b2ea2874bf47f0e4dd8be979b72efdefbee7cf0a9c43271201f64eb6d6b79f5995d6d25a7650057c3336e8086d8dd6a6c38
7
+ data.tar.gz: 1c71cdf5e8e811bf40265a245ae11c173fa3f2877a1fbf0d650b97f1e71d56263bb35c3e7ff717a604af0d055974d255c6e53cd78fa8a0e986c5cd6587a8018d
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Emma Hyde
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # impact-score-cli
2
+
3
+ Compute engineering impact scores from a CSV and compare users, with tunable weights.
4
+
5
+ - Default weights (multiples of 5): PRs 75%, Quality Reviews 5%, Cycle Time 15%, Reviews 5%
6
+ - Commands: `calc` (single user) and `compare` (two users)
7
+
8
+ ## Install
9
+
10
+ RubyGems (once published):
11
+
12
+ ```bash
13
+ gem install impact_score
14
+ ```
15
+
16
+ Homebrew (HEAD for latest):
17
+
18
+ ```bash
19
+ brew tap emmahyde/tap
20
+ brew install --HEAD emmahyde/tap/impact-score
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ Compare two users:
26
+
27
+ ```bash
28
+ impact-score compare dx_report.csv user_one user_two
29
+ ```
30
+
31
+ Single user:
32
+
33
+ ```bash
34
+ impact-score calc dx_report.csv user_one
35
+ ```
36
+
37
+ Custom weights:
38
+
39
+ ```bash
40
+ impact-score compare dx_report.csv user_one user_two --weights 50,40,5,5
41
+ ```
42
+
43
+ CSV must include columns: `github_username`, `prs_per_week`, `avg_cycle_time_days`, `quality_reviews_per_week`, `reviews_per_week`.
44
+
45
+ ## Releasing
46
+
47
+ - Set repo secret `RUBYGEMS_API_KEY`
48
+ - Tag a release: `git tag v0.1.0 && git push --tags`
49
+ - GitHub Actions will build and push to RubyGems
data/bin/impact-score ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+ require "impact_score/cli"
6
+ ImpactScore::CLI.start
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "impact_score"
5
+ spec.version = File.read(File.join(__dir__, "lib", "impact_score", "version.rb")).match(/VERSION\s*=\s*"([^"]+)"/)[1]
6
+ spec.summary = "Compute engineering impact scores from CSV with tunable weights"
7
+ spec.description = "CLI and library for computing engineering impact scores and comparisons from CSV exports."
8
+ spec.homepage = "https://github.com/emmahyde/impact-score-cli"
9
+ spec.license = "MIT"
10
+
11
+ spec.author = "Emma Hyde"
12
+ spec.email = "emmajhyde@gmail.com"
13
+
14
+ spec.files = Dir["lib/**/*", "bin/*", "README.md", "LICENSE", "impact_score.gemspec"]
15
+ spec.bindir = "bin"
16
+ spec.executables = ["impact-score"]
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.required_ruby_version = ">= 3.0"
20
+
21
+ spec.add_development_dependency "rake"
22
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+
5
+ module ImpactScore
6
+ Weights = Struct.new(:prs, :quality, :cycle, :reviews)
7
+
8
+ class Calculator
9
+ DEFAULT_WEIGHTS = Weights.new(0.75, 0.05, 0.15, 0.05)
10
+
11
+ def initialize(csv_path, weights: DEFAULT_WEIGHTS)
12
+ @data = CSV.read(csv_path, headers: true)
13
+ @weights = weights
14
+ compute_medians!
15
+ end
16
+
17
+ def compare(user1, user2)
18
+ u1 = find_user(user1)
19
+ u2 = find_user(user2)
20
+ raise "User not found: #{user1}" unless u1
21
+ raise "User not found: #{user2}" unless u2
22
+
23
+ c1 = contributions(u1)
24
+ c2 = contributions(u2)
25
+ [c1.merge(total: sum(c1)), c2.merge(total: sum(c2))]
26
+ end
27
+
28
+ def calc(user)
29
+ u = find_user(user)
30
+ raise "User not found: #{user}" unless u
31
+ c = contributions(u)
32
+ c.merge(total: sum(c))
33
+ end
34
+
35
+ private
36
+
37
+ def find_user(username)
38
+ @data.find { |r| r["github_username"] == username }
39
+ end
40
+
41
+ def compute_medians!
42
+ active = @data.select { |r| r["prs_per_week"].to_f > 0 }
43
+ prs_values = active.map { |r| r["prs_per_week"].to_f }.sort
44
+ cycle_values = active.map { |r| r["avg_cycle_time_days"].to_f }.sort
45
+ @median_prs = median(prs_values)
46
+ @median_cycle = median(cycle_values)
47
+ end
48
+
49
+ def median(values)
50
+ return 0.0 if values.empty?
51
+ mid = values.length / 2
52
+ values.length.odd? ? values[mid].to_f : (values[mid - 1].to_f + values[mid].to_f) / 2.0
53
+ end
54
+
55
+ def contributions(row)
56
+ prs = row["prs_per_week"].to_f
57
+ qrv = row["quality_reviews_per_week"].to_f
58
+ cyc = row["avg_cycle_time_days"].to_f
59
+ rvw = row["reviews_per_week"].to_f
60
+
61
+ prs_pts = (prs - @median_prs) * @weights.prs
62
+ qual_pts = qrv * @weights.quality
63
+ cycle_pts = (@median_cycle - cyc) * @weights.cycle
64
+ reviews_pts = rvw * @weights.reviews
65
+
66
+ {
67
+ prs: round2(prs_pts),
68
+ quality: round2(qual_pts),
69
+ cycle: round2(cycle_pts),
70
+ reviews: round2(reviews_pts)
71
+ }
72
+ end
73
+
74
+ def sum(h)
75
+ round2(h[:prs] + h[:quality] + h[:cycle] + h[:reviews])
76
+ end
77
+
78
+ def round2(x) = (x.to_f * 100).round / 100.0
79
+ end
80
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "impact_score"
5
+
6
+ module ImpactScore
7
+ class CLI
8
+ def self.start
9
+ new.start
10
+ end
11
+
12
+ def start
13
+ if ARGV.empty? || %w[-h --help help].include?(ARGV[0])
14
+ return puts(help_text)
15
+ end
16
+
17
+ cmd = ARGV.shift
18
+ case cmd
19
+ when "compare"
20
+ csv, user1, user2, opts = parse_args(3)
21
+ calc = ImpactScore::Calculator.new(csv, weights: opts[:weights])
22
+ c1, c2 = calc.compare(user1, user2)
23
+ print_compare(user1, user2, c1, c2, opts[:weights])
24
+ when "calc"
25
+ csv, user, opts = parse_args(2)
26
+ calc = ImpactScore::Calculator.new(csv, weights: opts[:weights])
27
+ res = calc.calc(user)
28
+ print_single(user, res, opts[:weights])
29
+ else
30
+ abort "Unknown command: #{cmd}\n\n#{help_text}"
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def parse_args(min)
37
+ opts = { weights: ImpactScore::Calculator::DEFAULT_WEIGHTS }
38
+ parser = OptionParser.new do |o|
39
+ o.on("--weights A,B,C,D", Array, "Weights as percentages (prs,quality,cycle,reviews)") do |arr|
40
+ raise "Need 4 weights" unless arr.size == 4
41
+ vals = arr.map { |x| x.to_f / 100.0 }
42
+ opts[:weights] = ImpactScore::Weights.new(*vals)
43
+ end
44
+ end
45
+
46
+ args = []
47
+ while args.size < min && ARGV.any?
48
+ args << ARGV.shift
49
+ end
50
+ parser.parse!(ARGV)
51
+ [*args, opts]
52
+ end
53
+
54
+ def print_compare(u1, u2, c1, c2, w)
55
+ puts "Weights: PRs #{(w.prs*100).to_i}%, Quality #{(w.quality*100).to_i}%, Cycle #{(w.cycle*100).to_i}%, Reviews #{(w.reviews*100).to_i}%"
56
+ puts "%-25s %15s %15s %12s" % ["Component", u1, u2, "Δ"]
57
+ puts "-" * 70
58
+ %i[prs quality cycle reviews total].each do |k|
59
+ v1 = c1[k]
60
+ v2 = c2[k]
61
+ puts "%-25s %15.2f %15.2f %12.2f" % [k.to_s.capitalize, v1, v2, v1 - v2]
62
+ end
63
+ end
64
+
65
+ def print_single(u, c, w)
66
+ puts "Weights: PRs #{(w.prs*100).to_i}%, Quality #{(w.quality*100).to_i}%, Cycle #{(w.cycle*100).to_i}%, Reviews #{(w.reviews*100).to_i}%"
67
+ puts "%-25s %15s" % ["Component", u]
68
+ puts "-" * 45
69
+ %i[prs quality cycle reviews total].each do |k|
70
+ v = c[k]
71
+ puts "%-25s %15.2f" % [k.to_s.capitalize, v]
72
+ end
73
+ end
74
+
75
+ def help_text
76
+ <<~TXT
77
+ impact-score – compute and compare impact scores from CSV
78
+
79
+ Commands:
80
+ impact-score compare CSV USER1 USER2 [--weights 75,5,15,5]
81
+ impact-score calc CSV USER [--weights 75,5,15,5]
82
+
83
+ CSV must contain columns: github_username, prs_per_week, avg_cycle_time_days, quality_reviews_per_week, reviews_per_week.
84
+ TXT
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ImpactScore
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "impact_score/version"
4
+ require "impact_score/calculator"
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: impact_score
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Emma Hyde
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-10-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: CLI and library for computing engineering impact scores and comparisons
28
+ from CSV exports.
29
+ email: emmajhyde@gmail.com
30
+ executables:
31
+ - impact-score
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - LICENSE
36
+ - README.md
37
+ - bin/impact-score
38
+ - impact_score.gemspec
39
+ - lib/impact_score.rb
40
+ - lib/impact_score/calculator.rb
41
+ - lib/impact_score/cli.rb
42
+ - lib/impact_score/version.rb
43
+ homepage: https://github.com/emmahyde/impact-score-cli
44
+ licenses:
45
+ - MIT
46
+ metadata: {}
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '3.0'
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.5.22
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Compute engineering impact scores from CSV with tunable weights
66
+ test_files: []