ruby-maat 1.0.0 → 1.3.4

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.
@@ -105,7 +105,7 @@ module RubyMaat
105
105
  end
106
106
 
107
107
  # Filter by minimum revisions and format results
108
- filtered_results = results.values.map do |result|
108
+ filtered_results = results.values.filter_map do |result|
109
109
  next if result[:revisions].size < min_revs
110
110
 
111
111
  {
@@ -114,7 +114,7 @@ module RubyMaat
114
114
  deleted: result[:deleted],
115
115
  commits: result[:revisions].size
116
116
  }
117
- end.compact
117
+ end
118
118
 
119
119
  # Sort by total churn descending
120
120
  filtered_results.sort_by! { |r| -(r[:added] + r[:deleted]) }
@@ -76,13 +76,13 @@ module RubyMaat
76
76
  results = []
77
77
 
78
78
  entity_authors.each do |entity, authors|
79
- total_revisions = authors.values.map(&:size).sum
79
+ total_revisions = authors.values.sum(&:size)
80
80
  next if total_revisions < min_revs
81
81
 
82
82
  # Find author with most revisions
83
83
  main_author, revisions = authors.max_by { |_author, revs| revs.size }
84
84
 
85
- total_revisions = authors.values.map(&:size).sum
85
+ total_revisions = authors.values.sum(&:size)
86
86
 
87
87
  results << {
88
88
  entity: entity,
@@ -122,14 +122,14 @@ module RubyMaat
122
122
  results = []
123
123
 
124
124
  entity_contributions.each do |entity, authors|
125
- total_revisions = authors.values.map(&:size).sum
125
+ total_revisions = authors.values.sum(&:size)
126
126
  next if total_revisions < min_revs
127
127
 
128
128
  # Calculate fractal value: 1 - sum(p_i^2) where p_i is proportion of each author
129
- sum_of_squares = authors.values.map do |revisions|
129
+ sum_of_squares = authors.values.sum do |revisions|
130
130
  proportion = revisions.size.to_f / total_revisions
131
131
  proportion**2
132
- end.sum
132
+ end
133
133
 
134
134
  fractal_value = 1.0 - sum_of_squares
135
135
 
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module RubyMaat
6
+ module AnalysisPresets
7
+ # Analysis configurations with appropriate time ranges and descriptions
8
+ ANALYSIS_CONFIGS = {
9
+ "authors" => {
10
+ name: "Developer Activity Analysis",
11
+ description: "Shows number of developers working on each module",
12
+ time_sensitive: true,
13
+ presets: {
14
+ "team-activity" => {
15
+ description: "Team activity patterns (6 months)",
16
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
17
+ },
18
+ "recent-team" => {
19
+ description: "Recent team changes (3 months)",
20
+ since: -> { (Date.today - 90).strftime("%Y-%m-%d") }
21
+ },
22
+ "team-history" => {
23
+ description: "Complete team evolution (2 years)",
24
+ since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
25
+ }
26
+ }
27
+ },
28
+
29
+ "coupling" => {
30
+ name: "Logical Coupling Analysis",
31
+ description: "Finds modules that change together (hidden dependencies)",
32
+ time_sensitive: true,
33
+ presets: {
34
+ "recent-coupling" => {
35
+ description: "Current coupling patterns (3 months)",
36
+ since: -> { (Date.today - 90).strftime("%Y-%m-%d") }
37
+ },
38
+ "coupling-trends" => {
39
+ description: "Coupling evolution (1 year)",
40
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
41
+ },
42
+ "architecture-review" => {
43
+ description: "Deep architecture analysis (6 months)",
44
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
45
+ }
46
+ }
47
+ },
48
+
49
+ "age" => {
50
+ name: "Code Age Analysis",
51
+ description: "Shows how recently each module was modified",
52
+ time_sensitive: false,
53
+ presets: {
54
+ "full-history" => {
55
+ description: "Complete age analysis (all history)"
56
+ }
57
+ }
58
+ },
59
+
60
+ "abs-churn" => {
61
+ name: "Absolute Code Churn",
62
+ description: "Total lines added/deleted over time",
63
+ time_sensitive: true,
64
+ presets: {
65
+ "recent-churn" => {
66
+ description: "Recent development activity (6 months)",
67
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
68
+ },
69
+ "yearly-churn" => {
70
+ description: "Annual churn patterns (1 year)",
71
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
72
+ },
73
+ "project-churn" => {
74
+ description: "Project lifecycle churn (2 years)",
75
+ since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
76
+ }
77
+ }
78
+ },
79
+
80
+ "author-churn" => {
81
+ name: "Author Churn Analysis",
82
+ description: "Code churn by individual developers",
83
+ time_sensitive: true,
84
+ presets: {
85
+ "contributor-activity" => {
86
+ description: "Current contributor patterns (6 months)",
87
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
88
+ },
89
+ "team-contributions" => {
90
+ description: "Team contribution history (1 year)",
91
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
92
+ }
93
+ }
94
+ },
95
+
96
+ "entity-churn" => {
97
+ name: "Module Churn Analysis",
98
+ description: "Churn by individual modules/files",
99
+ time_sensitive: true,
100
+ presets: {
101
+ "hotspot-analysis" => {
102
+ description: "Current hotspots (6 months)",
103
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
104
+ },
105
+ "stability-review" => {
106
+ description: "Module stability patterns (1 year)",
107
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
108
+ }
109
+ }
110
+ },
111
+
112
+ "entity-ownership" => {
113
+ name: "Code Ownership Analysis",
114
+ description: "Shows ownership distribution of code",
115
+ time_sensitive: true,
116
+ presets: {
117
+ "current-ownership" => {
118
+ description: "Current ownership patterns (1 year)",
119
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
120
+ },
121
+ "ownership-evolution" => {
122
+ description: "Ownership changes over time (2 years)",
123
+ since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
124
+ }
125
+ }
126
+ },
127
+
128
+ "main-dev" => {
129
+ name: "Main Developer Analysis",
130
+ description: "Identifies primary developer for each module",
131
+ time_sensitive: true,
132
+ presets: {
133
+ "current-maintainers" => {
134
+ description: "Current module maintainers (1 year)",
135
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
136
+ },
137
+ "maintainer-history" => {
138
+ description: "Maintainer evolution (2 years)",
139
+ since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
140
+ }
141
+ }
142
+ },
143
+
144
+ "entity-effort" => {
145
+ name: "Development Effort Analysis",
146
+ description: "Effort distribution across modules",
147
+ time_sensitive: true,
148
+ presets: {
149
+ "effort-focus" => {
150
+ description: "Recent effort distribution (6 months)",
151
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
152
+ },
153
+ "effort-trends" => {
154
+ description: "Effort patterns over time (1 year)",
155
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
156
+ }
157
+ }
158
+ },
159
+
160
+ "communication" => {
161
+ name: "Team Communication Analysis",
162
+ description: "Developer collaboration patterns",
163
+ time_sensitive: true,
164
+ presets: {
165
+ "team-collaboration" => {
166
+ description: "Current collaboration patterns (6 months)",
167
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
168
+ },
169
+ "communication-trends" => {
170
+ description: "Communication evolution (1 year)",
171
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
172
+ }
173
+ }
174
+ },
175
+
176
+ "summary" => {
177
+ name: "Project Summary",
178
+ description: "High-level project statistics",
179
+ time_sensitive: true,
180
+ presets: {
181
+ "project-overview" => {
182
+ description: "Current project state (1 year)",
183
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
184
+ },
185
+ "full-summary" => {
186
+ description: "Complete project history"
187
+ }
188
+ }
189
+ },
190
+
191
+ "revisions" => {
192
+ name: "Revision Count Analysis",
193
+ description: "Number of changes per module",
194
+ time_sensitive: true,
195
+ presets: {
196
+ "activity-hotspots" => {
197
+ description: "Current activity patterns (6 months)",
198
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
199
+ },
200
+ "change-history" => {
201
+ description: "Complete change patterns (2 years)",
202
+ since: -> { (Date.today - 730).strftime("%Y-%m-%d") }
203
+ }
204
+ }
205
+ },
206
+
207
+ # Analyses that don't benefit from time filtering
208
+ "identity" => {
209
+ name: "Identity Analysis",
210
+ description: "Raw parsed data (debugging/export)",
211
+ time_sensitive: false,
212
+ presets: {
213
+ "full-data" => {
214
+ description: "Complete dataset export"
215
+ }
216
+ }
217
+ },
218
+
219
+ "soc" => {
220
+ name: "Sum of Coupling",
221
+ description: "Total coupling strength per module",
222
+ time_sensitive: true,
223
+ presets: {
224
+ "coupling-strength" => {
225
+ description: "Current coupling analysis (6 months)",
226
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
227
+ }
228
+ }
229
+ },
230
+
231
+ "refactoring-main-dev" => {
232
+ name: "Refactoring Main Developer",
233
+ description: "Main developer in refactoring contexts",
234
+ time_sensitive: true,
235
+ presets: {
236
+ "refactoring-leads" => {
237
+ description: "Recent refactoring activity (1 year)",
238
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
239
+ }
240
+ }
241
+ },
242
+
243
+ "main-dev-by-revs" => {
244
+ name: "Main Developer (by Revisions)",
245
+ description: "Main developer by revision count",
246
+ time_sensitive: true,
247
+ presets: {
248
+ "revision-leaders" => {
249
+ description: "Current revision leaders (1 year)",
250
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
251
+ }
252
+ }
253
+ },
254
+
255
+ "fragmentation" => {
256
+ name: "Development Fragmentation",
257
+ description: "How fragmented development effort is",
258
+ time_sensitive: true,
259
+ presets: {
260
+ "team-fragmentation" => {
261
+ description: "Current team fragmentation (6 months)",
262
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
263
+ }
264
+ }
265
+ },
266
+
267
+ "messages" => {
268
+ name: "Commit Message Analysis",
269
+ description: "Analyzes commit message patterns",
270
+ time_sensitive: true,
271
+ presets: {
272
+ "message-patterns" => {
273
+ description: "Recent commit patterns (6 months)",
274
+ since: -> { (Date.today - 180).strftime("%Y-%m-%d") }
275
+ },
276
+ "communication-style" => {
277
+ description: "Team communication style (1 year)",
278
+ since: -> { (Date.today - 365).strftime("%Y-%m-%d") }
279
+ }
280
+ }
281
+ }
282
+ }.freeze
283
+
284
+ def self.analysis_config(analysis_name)
285
+ ANALYSIS_CONFIGS[analysis_name]
286
+ end
287
+
288
+ def self.available_analyses
289
+ ANALYSIS_CONFIGS.keys.sort
290
+ end
291
+
292
+ def self.analysis_description(analysis_name)
293
+ config = ANALYSIS_CONFIGS[analysis_name]
294
+ return "Unknown analysis" unless config
295
+ "#{config[:name]} - #{config[:description]}"
296
+ end
297
+
298
+ def self.presets_for_analysis(analysis_name)
299
+ config = ANALYSIS_CONFIGS[analysis_name]
300
+ return {} unless config
301
+
302
+ # Evaluate lambda functions in preset options
303
+ config[:presets].transform_values do |preset|
304
+ preset.transform_values { |v| v.is_a?(Proc) ? v.call : v }
305
+ end
306
+ end
307
+
308
+ def self.time_sensitive?(analysis_name)
309
+ config = ANALYSIS_CONFIGS[analysis_name]
310
+ config ? config[:time_sensitive] : false
311
+ end
312
+ end
313
+ end
data/lib/ruby_maat/app.rb CHANGED
@@ -44,6 +44,7 @@ module RubyMaat
44
44
  change_records = parser.parse
45
45
 
46
46
  # Apply data transformations
47
+ change_records = apply_merge_grouping(change_records)
47
48
  change_records = apply_grouping(change_records)
48
49
  change_records = apply_temporal_grouping(change_records)
49
50
  change_records = apply_team_mapping(change_records)
@@ -94,6 +95,30 @@ module RubyMaat
94
95
  end
95
96
  end
96
97
 
98
+ def apply_merge_grouping(change_records)
99
+ return change_records unless @options[:group_by_merge]
100
+ return change_records if change_records.nil? || change_records.empty?
101
+
102
+ # Check for non-nil parent_revisions (not non-empty) because root commits
103
+ # legitimately have parent_revisions == [] when parsed with the parents format.
104
+ records_with_parents = change_records.count { |record| !record.parent_revisions.nil? }
105
+ records_without_parents = change_records.count { |record| record.parent_revisions.nil? }
106
+
107
+ if records_with_parents.zero?
108
+ raise ArgumentError,
109
+ "--group-by-merge requires parent revision metadata in the log. " \
110
+ "Please regenerate the log using the 'pr-coupling' preset or another format that includes parent hashes."
111
+ elsif records_without_parents.positive?
112
+ raise ArgumentError,
113
+ "--group-by-merge requires parent revision metadata for all commits, " \
114
+ "but the provided log mixes records with and without parent hashes. " \
115
+ "Please regenerate the log using the 'pr-coupling' preset or another format that includes parent hashes."
116
+ end
117
+
118
+ grouper = RubyMaat::Groupers::MergeCommitGrouper.new
119
+ grouper.group(change_records)
120
+ end
121
+
97
122
  def apply_grouping(change_records)
98
123
  return change_records unless @options[:group]
99
124
 
@@ -4,9 +4,10 @@ module RubyMaat
4
4
  # Represents a single change/modification record from VCS
5
5
  # This is the fundamental data structure that flows through the entire pipeline
6
6
  class ChangeRecord
7
- attr_reader :entity, :author, :date, :revision, :message, :loc_added, :loc_deleted
7
+ attr_reader :entity, :author, :date, :revision, :message, :loc_added, :loc_deleted, :parent_revisions
8
8
 
9
- def initialize(entity:, author:, date:, revision:, message: nil, loc_added: nil, loc_deleted: nil)
9
+ def initialize(entity:, author:, date:, revision:, message: nil, loc_added: nil, loc_deleted: nil,
10
+ parent_revisions: nil)
10
11
  @entity = entity
11
12
  @author = author
12
13
  @date = date.is_a?(Date) ? date : Date.parse(date)
@@ -14,6 +15,14 @@ module RubyMaat
14
15
  @message = message
15
16
  @loc_added = loc_added.to_i if loc_added && (!loc_added.is_a?(Float) || !loc_added.nan?)
16
17
  @loc_deleted = loc_deleted.to_i if loc_deleted && (!loc_deleted.is_a?(Float) || !loc_deleted.nan?)
18
+ @parent_revisions = parent_revisions
19
+ end
20
+
21
+ # True when the commit has multiple parents (i.e., a merge commit).
22
+ # Returns false when parent metadata is unavailable (nil), since the parser
23
+ # could not determine the parent count.
24
+ def merge_commit?
25
+ parent_revisions.is_a?(Array) && parent_revisions.size >= 2
17
26
  end
18
27
 
19
28
  def to_h
@@ -24,7 +33,8 @@ module RubyMaat
24
33
  revision: revision,
25
34
  message: message,
26
35
  loc_added: loc_added,
27
- loc_deleted: loc_deleted
36
+ loc_deleted: loc_deleted,
37
+ parent_revisions: parent_revisions
28
38
  }
29
39
  end
30
40