carson 3.22.1 → 3.23.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.
@@ -67,8 +67,8 @@ module Carson
67
67
  results << entry
68
68
  end
69
69
 
70
- succeeded = results.count { it[ :status ] == "ok" }
71
- failed = results.count { it[ :status ] != "ok" }
70
+ succeeded = results.count { |entry| entry[ :status ] == "ok" }
71
+ failed = results.count { |entry| entry[ :status ] != "ok" }
72
72
  result = { command: "housekeep", status: failed.zero? ? "ok" : "partial", repos: results, succeeded: succeeded, failed: failed }
73
73
  housekeep_finish( result: result, exit_code: failed.zero? ? EXIT_OK : EXIT_ERROR, json_output: json_output, results: results, succeeded: succeeded, failed: failed )
74
74
  end
@@ -90,10 +90,10 @@ module Carson
90
90
  main_root = main_worktree_root
91
91
  items = []
92
92
 
93
- agent_prefixes = Worktree::AGENT_DIRS.filter_map do |dir|
93
+ agent_prefixes = Worktree::AGENT_DIRS.map do |dir|
94
94
  full = File.join( main_root, dir, "worktrees" )
95
95
  File.join( realpath_safe( full ), "" ) if Dir.exist?( full )
96
- end
96
+ end.compact
97
97
 
98
98
  worktree_list.each do |worktree|
99
99
  next if worktree.path == main_root
@@ -21,7 +21,7 @@ module Carson
21
21
  puts_line "Onboarding #{repo_name}..."
22
22
 
23
23
  if !global_config_exists? || !git_remote_exists?( remote_name: config.git_remote )
24
- if self.in.respond_to?( :tty? ) && self.in.tty?
24
+ if input_stream.respond_to?( :tty? ) && input_stream.tty?
25
25
  setup_status = setup!
26
26
  return setup_status unless setup_status == EXIT_OK
27
27
  else
@@ -48,7 +48,7 @@ module Carson
48
48
  hook_status = prepare!
49
49
  return hook_status unless hook_status == EXIT_OK
50
50
 
51
- drift_count = template_results.count { it.fetch( :status ) != "ok" }
51
+ drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
52
52
  stale_count = template_superseded_present.count
53
53
  template_status = template_apply!
54
54
  return template_status unless template_status == EXIT_OK
@@ -69,7 +69,7 @@ module Carson
69
69
  return hook_status unless hook_status == EXIT_OK
70
70
  puts_line "Hooks installed (#{config.managed_hooks.count} hooks)."
71
71
 
72
- template_drift_count = template_results.count { it.fetch( :status ) != "ok" }
72
+ template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
73
73
  stale_count = template_superseded_present.count
74
74
  template_status = with_captured_output { template_apply! }
75
75
  return template_status unless template_status == EXIT_OK
@@ -202,7 +202,7 @@ module Carson
202
202
  puts_line "#{repo_root} is not a git repository."
203
203
  return EXIT_ERROR
204
204
  end
205
- if self.in.respond_to?( :tty? ) && self.in.tty?
205
+ if input_stream.respond_to?( :tty? ) && input_stream.tty?
206
206
  puts_line ""
207
207
  puts_line "This will remove Carson hooks, managed .github/ files,"
208
208
  puts_line "and deregister this repository from portfolio governance."
@@ -251,7 +251,7 @@ module Carson
251
251
  return hook_status unless hook_status == EXIT_OK
252
252
  puts_line "Hooks installed (#{config.managed_hooks.count} hooks)."
253
253
 
254
- template_drift_count = template_results.count { it.fetch( :status ) != "ok" }
254
+ template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
255
255
  template_status = with_captured_output { template_apply! }
256
256
  return template_status unless template_status == EXIT_OK
257
257
  if template_drift_count.positive?
@@ -278,7 +278,7 @@ module Carson
278
278
 
279
279
  # Detects local branches with no upstream tracking ref — candidates for orphan pruning.
280
280
  def orphan_local_branches( active_branch:, cwd_branch: nil )
281
- git_capture!( "for-each-ref", "--format=%(refname:short)\t%(upstream:short)", "refs/heads" ).lines.filter_map do |line|
281
+ git_capture!( "for-each-ref", "--format=%(refname:short)\t%(upstream:short)", "refs/heads" ).lines.map do |line|
282
282
  branch, upstream = line.strip.split( "\t", 2 )
283
283
  branch = branch.to_s.strip
284
284
  upstream = upstream.to_s.strip
@@ -290,14 +290,14 @@ module Carson
290
290
  next if branch == TEMPLATE_SYNC_BRANCH
291
291
 
292
292
  branch
293
- end
293
+ end.compact
294
294
  end
295
295
 
296
296
  # Detects local branches whose upstream still exists but whose content is already on main.
297
297
  # Two-step evidence: (1) find the merge-base, (2) verify every file the branch changed
298
298
  # relative to the merge-base has identical content on main.
299
299
  def absorbed_local_branches( active_branch:, cwd_branch: nil )
300
- git_capture!( "for-each-ref", "--format=%(refname:short)\t%(upstream:short)\t%(upstream:track)", "refs/heads" ).lines.filter_map do |line|
300
+ git_capture!( "for-each-ref", "--format=%(refname:short)\t%(upstream:short)\t%(upstream:track)", "refs/heads" ).lines.map do |line|
301
301
  branch, upstream, track = line.strip.split( "\t", 3 )
302
302
  branch = branch.to_s.strip
303
303
  upstream = upstream.to_s.strip
@@ -313,7 +313,7 @@ module Carson
313
313
  next unless branch_absorbed_into_main?( branch: branch )
314
314
 
315
315
  { branch: branch, upstream: upstream }
316
- end
316
+ end.compact
317
317
  end
318
318
 
319
319
  # Returns true when the branch has no unique content relative to main.
@@ -28,8 +28,8 @@ module Carson
28
28
  puts_verbose "[Template Sync Check]"
29
29
  results = template_results
30
30
  stale = template_superseded_present
31
- drift_count = results.count { it.fetch( :status ) == "drift" }
32
- error_count = results.count { it.fetch( :status ) == "error" }
31
+ drift_count = results.count { |entry| entry.fetch( :status ) == "drift" }
32
+ error_count = results.count { |entry| entry.fetch( :status ) == "error" }
33
33
  stale_count = stale.count
34
34
  results.each do |entry|
35
35
  puts_verbose "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}"
@@ -42,7 +42,7 @@ module Carson
42
42
  summary_parts << "#{drift_count} of #{results.count} drifted" if drift_count.positive?
43
43
  summary_parts << "#{stale_count} stale" if stale_count.positive?
44
44
  puts_line "Templates: #{summary_parts.join( ", " )}"
45
- results.select { it.fetch( :status ) == "drift" }.each { |entry| puts_line " #{entry.fetch( :file )}" }
45
+ results.select { |entry| entry.fetch( :status ) == "drift" }.each { |entry| puts_line " #{entry.fetch( :file )}" }
46
46
  stale.each { |file| puts_line " #{file} — superseded" }
47
47
  else
48
48
  puts_line "Templates: #{results.count} files in sync"
@@ -137,7 +137,7 @@ module Carson
137
137
  removed += 1
138
138
  end
139
139
 
140
- error_count = results.count { it.fetch( :status ) == "error" }
140
+ error_count = results.count { |entry| entry.fetch( :status ) == "error" }
141
141
  puts_verbose "template_apply_summary: updated=#{applied} removed=#{removed} error=#{error_count}"
142
142
  unless verbose?
143
143
  if applied.positive? || removed.positive?
@@ -98,7 +98,9 @@ module Carson
98
98
  unresolved_threads = unresolved_thread_entries( details: details )
99
99
  actionable_top_level = actionable_top_level_items( details: details, pr_author: pr_author )
100
100
  acknowledgements = disposition_acknowledgements( details: details, pr_author: pr_author )
101
- unacknowledged_actionable = actionable_top_level.reject { acknowledged_by_disposition?( item: it, acknowledgements: acknowledgements ) }
101
+ unacknowledged_actionable = actionable_top_level.reject do |item|
102
+ acknowledged_by_disposition?( item: item, acknowledgements: acknowledgements )
103
+ end
102
104
  {
103
105
  latest_activity: latest_review_activity( details: details ),
104
106
  unresolved_threads: unresolved_threads,
@@ -112,8 +114,8 @@ module Carson
112
114
  def review_gate_signature( snapshot: )
113
115
  {
114
116
  latest_activity: snapshot.fetch( :latest_activity ).to_s,
115
- unresolved_urls: snapshot.fetch( :unresolved_threads ).map { it.fetch( :url ) }.sort,
116
- unacknowledged_urls: snapshot.fetch( :unacknowledged_actionable ).map { it.fetch( :url ) }.sort
117
+ unresolved_urls: snapshot.fetch( :unresolved_threads ).map { |entry| entry.fetch( :url ) }.sort,
118
+ unacknowledged_urls: snapshot.fetch( :unacknowledged_actionable ).map { |entry| entry.fetch( :url ) }.sort
117
119
  }
118
120
  end
119
121
 
@@ -138,7 +140,7 @@ module Carson
138
140
  # Normalise both sides by stripping the [bot] suffix for a consistent match.
139
141
  def bot_username?( author: )
140
142
  normalised = author.to_s.downcase.delete_suffix( "[bot]" )
141
- config.review_bot_usernames.any? { it.downcase.delete_suffix( "[bot]" ) == normalised }
143
+ config.review_bot_usernames.any? { |username| username.downcase.delete_suffix( "[bot]" ) == normalised }
142
144
  end
143
145
 
144
146
  def unresolved_thread_entries( details: )
@@ -149,7 +151,7 @@ module Carson
149
151
  comments = thread.fetch( :comments )
150
152
  first_comment = comments.first || {}
151
153
  next if bot_username?( author: first_comment.fetch( :author, "" ) )
152
- latest_time = comments.map { it.fetch( :created_at ) }.max.to_s
154
+ latest_time = comments.map { |comment| comment.fetch( :created_at ) }.max.to_s
153
155
  {
154
156
  url: blank_to( value: first_comment.fetch( :url, "" ), default: "#{details.fetch( :url )}#thread-#{index + 1}" ),
155
157
  author: first_comment.fetch( :author, "" ),
@@ -201,7 +203,7 @@ module Carson
201
203
  sources = []
202
204
  sources.concat( Array( details.fetch( :comments ) ) )
203
205
  sources.concat( Array( details.fetch( :reviews ) ) )
204
- sources.concat( Array( details.fetch( :review_threads ) ).flat_map { it.fetch( :comments ) } )
206
+ sources.concat( Array( details.fetch( :review_threads ) ).flat_map { |thread| thread.fetch( :comments ) } )
205
207
  sources.map do |entry|
206
208
  next unless entry.fetch( :author, "" ) == pr_author
207
209
  body = entry.fetch( :body, "" ).to_s
@@ -222,7 +224,7 @@ module Carson
222
224
  # True when any disposition acknowledgement references the specific finding URL.
223
225
  def acknowledged_by_disposition?( item:, acknowledgements: )
224
226
  acknowledgements.any? do |ack|
225
- Array( ack.fetch( :target_urls ) ).any? { it == item.fetch( :url ) }
227
+ Array( ack.fetch( :target_urls ) ).any? { |target_url| target_url == item.fetch( :url ) }
226
228
  end
227
229
  end
228
230
 
@@ -230,10 +232,10 @@ module Carson
230
232
  def latest_review_activity( details: )
231
233
  timestamps = []
232
234
  timestamps << details.fetch( :updated_at )
233
- timestamps.concat( Array( details.fetch( :comments ) ).map { it.fetch( :created_at ) } )
234
- timestamps.concat( Array( details.fetch( :reviews ) ).map { it.fetch( :created_at ) } )
235
- timestamps.concat( Array( details.fetch( :review_threads ) ).flat_map { it.fetch( :comments ) }.map { it.fetch( :created_at ) } )
236
- timestamps.map { parse_time_or_nil( text: it ) }.compact.max&.utc&.iso8601
235
+ timestamps.concat( Array( details.fetch( :comments ) ).map { |comment| comment.fetch( :created_at ) } )
236
+ timestamps.concat( Array( details.fetch( :reviews ) ).map { |review| review.fetch( :created_at ) } )
237
+ timestamps.concat( Array( details.fetch( :review_threads ) ).flat_map { |thread| thread.fetch( :comments ) }.map { |comment| comment.fetch( :created_at ) } )
238
+ timestamps.map { |timestamp| parse_time_or_nil( text: timestamp ) }.compact.max&.utc&.iso8601
237
239
  end
238
240
 
239
241
  def resolved_review_gate_pr_summary( owner:, repo:, pr_number:, pr_summary: )
@@ -339,7 +341,7 @@ module Carson
339
341
  if report.fetch( :block_reasons ).empty?
340
342
  lines << "- none"
341
343
  else
342
- report.fetch( :block_reasons ).each { lines << "- #{it}" }
344
+ report.fetch( :block_reasons ).each { |reason| lines << "- #{reason}" }
343
345
  end
344
346
  lines << ""
345
347
  lines << "## Unresolved Threads"
@@ -60,7 +60,7 @@ module Carson
60
60
  )
61
61
  end
62
62
 
63
- Array( details.fetch( :review_threads ) ).flat_map { it.fetch( :comments ) }.each do |comment|
63
+ Array( details.fetch( :review_threads ) ).flat_map { |thread| thread.fetch( :comments ) }.each do |comment|
64
64
  next if comment.fetch( :author ) == pr_author
65
65
  next if bot_username?( author: comment.fetch( :author ) )
66
66
  hits = matched_risk_keywords( text: comment.fetch( :body ) )
@@ -153,7 +153,7 @@ module Carson
153
153
  stdout_text, stderr_text, success, = gh_run( "issue", "list", "--repo", repo_slug, "--state", "all", "--limit", "100", "--json", "number,title,state,url,labels" )
154
154
  raise gh_error_text( stdout_text: stdout_text, stderr_text: stderr_text, fallback: "unable to list issues for review sweep" ) unless success
155
155
  issues = Array( JSON.parse( stdout_text ) )
156
- node = issues.find { it[ "title" ].to_s == config.review_tracking_issue_title }
156
+ node = issues.find { |issue| issue[ "title" ].to_s == config.review_tracking_issue_title }
157
157
  return nil if node.nil?
158
158
  {
159
159
  number: node[ "number" ],
@@ -25,7 +25,7 @@ module Carson
25
25
 
26
26
  # GitHub URL extraction for mapping disposition acknowledgements to finding URLs.
27
27
  def extract_github_urls( text: )
28
- text.to_s.scan( %r{https://github\.com/[^\s\)\]]+} ).map { it.sub( /[.,;:]+$/, "" ) }.uniq
28
+ text.to_s.scan( %r{https://github\.com/[^\s\)\]]+} ).map { |url| url.sub( /[.,;:]+$/, "" ) }.uniq
29
29
  end
30
30
 
31
31
  # Parse RFC3339 timestamps and return nil on blank/invalid values.
@@ -21,7 +21,7 @@ module Carson
21
21
  return write_setup_config( choices: cli_choices )
22
22
  end
23
23
 
24
- if self.in.respond_to?( :tty? ) && self.in.tty?
24
+ if input_stream.respond_to?( :tty? ) && input_stream.tty?
25
25
  interactive_setup!
26
26
  else
27
27
  silent_setup!
@@ -42,9 +42,6 @@ module Carson
42
42
  workflow_choice = prompt_workflow_style
43
43
  choices[ "workflow.style" ] = workflow_choice unless workflow_choice.nil?
44
44
 
45
- merge_choice = prompt_merge_method
46
- choices[ "govern.merge.method" ] = merge_choice unless merge_choice.nil?
47
-
48
45
  canonical_choice = prompt_canonical_lint_policy
49
46
  choices[ "lint.canonical" ] = canonical_choice unless canonical_choice.nil?
50
47
 
@@ -67,7 +64,7 @@ module Carson
67
64
  duplicates = duplicate_remote_groups( remotes: remotes )
68
65
  unless duplicates.empty?
69
66
  duplicates.each_value do |group|
70
- names = group.map { it.fetch( :name ) }.join( " and " )
67
+ names = group.map { |entry| entry.fetch( :name ) }.join( " and " )
71
68
  puts_verbose "duplicate_remotes: #{names} share the same URL"
72
69
  end
73
70
  end
@@ -91,10 +88,10 @@ module Carson
91
88
  end
92
89
 
93
90
  duplicates = duplicate_remote_groups( remotes: remotes )
94
- duplicate_names = duplicates.values.flatten.map { it.fetch( :name ) }.to_set
91
+ duplicate_names = duplicates.values.flatten.map { |entry| entry.fetch( :name ) }.to_set
95
92
  unless duplicates.empty?
96
93
  duplicates.each_value do |group|
97
- names = group.map { it.fetch( :name ) }.join( " and " )
94
+ names = group.map { |entry| entry.fetch( :name ) }.join( " and " )
98
95
  puts_line "Remotes #{names} share the same URL. Consider removing the duplicate."
99
96
  end
100
97
  end
@@ -139,21 +136,7 @@ module Carson
139
136
  { label: "branch — enforce PR-only merges (default)", value: "branch" },
140
137
  { label: "trunk — commit directly to main", value: "trunk" }
141
138
  ]
142
- default_index = options.index { it.fetch( :value ) == current } || 0
143
- prompt_choice( options: options, default: default_index )
144
- end
145
-
146
- def prompt_merge_method
147
- puts_line ""
148
- puts_line "Merge method"
149
- current = config.govern_merge_method
150
- puts_line " Currently: #{current}" unless current.nil? || current.empty?
151
- options = [
152
- { label: "squash — one commit per PR (recommended)", value: "squash" },
153
- { label: "rebase — linear history, individual commits", value: "rebase" },
154
- { label: "merge — merge commits", value: "merge" }
155
- ]
156
- default_index = options.index { it.fetch( :value ) == current } || 0
139
+ default_index = options.index { |option| option.fetch( :value ) == current } || 0
157
140
  prompt_choice( options: options, default: default_index )
158
141
  end
159
142
 
@@ -177,7 +160,7 @@ module Carson
177
160
  end
178
161
  output.print "#{BADGE} Choice [#{default + 1}]: "
179
162
  output.flush
180
- raw = self.in.gets
163
+ raw = input_stream.gets
181
164
  return options[ default ].fetch( :value ) if raw.nil?
182
165
 
183
166
  input = raw.to_s.strip
@@ -194,7 +177,7 @@ module Carson
194
177
  def prompt_custom_value( label: )
195
178
  output.print "#{BADGE} #{label}: "
196
179
  output.flush
197
- raw = self.in.gets
180
+ raw = input_stream.gets
198
181
  return nil if raw.nil?
199
182
 
200
183
  value = raw.to_s.strip
@@ -221,7 +204,7 @@ module Carson
221
204
  others << entry
222
205
  end
223
206
  end
224
- well_known.sort_by { WELL_KNOWN_REMOTES.index( it.fetch( :name ) ) || 999 } + others.sort_by { it.fetch( :name ) }
207
+ well_known.sort_by { |entry| WELL_KNOWN_REMOTES.index( entry.fetch( :name ) ) || 999 } + others.sort_by { |entry| entry.fetch( :name ) }
225
208
  end
226
209
 
227
210
  # Normalises a remote URL so SSH and HTTPS variants of the same host/path compare equal.
@@ -297,7 +280,7 @@ module Carson
297
280
 
298
281
  def detect_git_remote
299
282
  remotes = list_git_remotes
300
- remote_names = remotes.map { it.fetch( :name ) }
283
+ remote_names = remotes.map { |entry| entry.fetch( :name ) }
301
284
  return nil if remote_names.empty?
302
285
 
303
286
  return config.git_remote if remote_names.include?( config.git_remote )
@@ -396,7 +379,7 @@ module Carson
396
379
  hint = default ? "Y/n" : "y/N"
397
380
  output.print "#{BADGE} [#{hint}]: "
398
381
  output.flush
399
- raw = self.in.gets
382
+ raw = input_stream.gets
400
383
  return default if raw.nil?
401
384
 
402
385
  input = raw.to_s.strip.downcase