Class: StudFinder::Newness

Inherits:
Object
  • Object
show all
Defined in:
lib/stud_finder/newness.rb

Overview

rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: Error, History

Constant Summary collapse

DEFAULT_DAYS =
30
DEFAULT_MIN_COMMITS =
3
SECONDS_PER_DAY =
86_400
SHALLOW_CLONE_WARNING =
{
  code: 'shallow_clone_newness_disabled',
  message: 'shallow git clone detected; newness rules disabled (use fetch-depth: 0 in CI for full newness behavior)'
}.freeze
UNSHALLOW_FAILED_WARNING =
{
  code: 'shallow_clone_unshallow_failed',
  message: 'auto-unshallow failed; evidence unavailable (use fetch-depth: 0 in CI or pass --no-auto-unshallow)'
}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(repo_path:, files:, days: DEFAULT_DAYS, min_commits: DEFAULT_MIN_COMMITS, enabled: true, now: Time.now) ⇒ Newness

Returns a new instance of Newness.



26
27
28
29
30
31
32
33
34
# File 'lib/stud_finder/newness.rb', line 26

def initialize(repo_path:, files:, days: DEFAULT_DAYS, min_commits: DEFAULT_MIN_COMMITS, enabled: true,
               now: Time.now)
  @repo_path = File.expand_path(repo_path)
  @files = files
  @days = days
  @min_commits = min_commits
  @enabled = enabled
  @now = now.to_i
end

Class Method Details

.apply(rows:, edges:, metadata:, branch_threshold: 'branch', coverage: nil) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/stud_finder/newness.rb', line 52

def self.apply(rows:, edges:, metadata:, branch_threshold: 'branch', coverage: nil)
  rows = rows.map do |row|
     = .fetch(row[:path], nil)
    nf = newness_fields()
    # Preserve floor escalation from the scorer (complexity_floor / fan_in_floor); newness rules
    # may still override it below for new files (trunk_adjacent or recency_floor always win).
    existing_escalation = row.fetch(:escalation, '')
    nf = nf.merge(escalation: existing_escalation) unless existing_escalation.to_s.empty?
    row.merge(nf).merge(evidence: evidence(, coverage&.key?(row[:path])))
  end
  # Rule 2: scorer "trunk" now means high composite risk, not fan-in-only structural coupling.
  trunk_paths = rows.select { |row| row[:classification] == 'trunk' }.to_set { |row| row[:path] }

  rows.map do |row|
    next row unless row[:new_file]

    dependencies = edges.fetch(row[:path], {}).fetch(:dependencies, [])
    if dependencies.any? { |path| trunk_paths.include?(path) }
      row.merge(classification: 'trunk', escalation: 'trunk_adjacent')
    elsif row[:classification] == 'leaf' || %w[complexity_floor fan_in_floor].include?(row[:escalation].to_s)
      row.merge(classification: branch_threshold, escalation: 'recency_floor')
    else
      row
    end
  end
end

.disabled_metadata(files) ⇒ Object



79
80
81
82
83
# File 'lib/stud_finder/newness.rb', line 79

def self.(files)
  files.to_h do |file|
    [file, { new_file: false, age_days: 0, total_commits: 0, escalation: '', metadata_available: false }]
  end
end

.evidence(metadata, explicit_coverage) ⇒ Object



106
107
108
109
110
111
112
113
114
115
116
# File 'lib/stud_finder/newness.rb', line 106

def self.evidence(, explicit_coverage)
  return nil unless &.fetch(:metadata_available, false)

  age_component = [.fetch(:age_days, 0) / 30.0, 1.0].min
  commits_component = [.fetch(:total_commits, 0) / 3.0, 1.0].min

  history_only = (age_component + commits_component) / 2.0
  with_coverage = ((age_component + commits_component + 1.0) / 3.0) if explicit_coverage

  [history_only, with_coverage].compact.max.round(4)
end

.newness_fields(metadata) ⇒ Object



95
96
97
98
99
100
101
102
103
104
# File 'lib/stud_finder/newness.rb', line 95

def self.newness_fields()
   ||= { new_file: false, age_days: 0, total_commits: 0, metadata_available: false }
  {
    new_file: .fetch(:new_file, false),
    age_days: .fetch(:age_days, 0),
    total_commits: .fetch(:total_commits, 0),
    newness_metadata_available: .fetch(:metadata_available, false),
    escalation: ''
  }
end

.shallow_repository?(repo_path) ⇒ Boolean

Returns:

  • (Boolean)


85
86
87
88
89
90
91
92
93
# File 'lib/stud_finder/newness.rb', line 85

def self.shallow_repository?(repo_path)
  repo_path = File.expand_path(repo_path)
  stdout, _stderr, status = Open3.capture3('git', 'rev-parse', '--is-shallow-repository', chdir: repo_path)
  return stdout.strip == 'true' if status.success?

  git_shallow_file?(repo_path)
rescue Errno::ENOENT
  git_shallow_file?(repo_path)
end

Instance Method Details

#callObject



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/stud_finder/newness.rb', line 36

def call
  return self.class.(@files) unless @enabled
  return self.class.(@files) if self.class.shallow_repository?(@repo_path)

  history = git_history
  @files.to_h do |file|
    first_epoch = history.first_commit_epoch[file]
    total_commits = history.total_commits.fetch(file, 0)
    age_days = age_days(first_epoch)
    is_new = new_file?(age_days, total_commits)

    [file, { new_file: is_new, age_days: age_days || 0, total_commits: total_commits, escalation: '',
             metadata_available: true }]
  end
end