Module: Moult::Scoring

Defined in:
lib/moult/scoring.rb

Overview

Aggregates the per-method ABC and per-file churn into a ranked Report.

File complexity is the sum of its methods' ABC; the file score is complexity x churn. This raw product is dominated by outliers - acceptable for v0.1, but Scoring.combine is isolated so a normalisation strategy (log, rank, z-score) can drop in later without touching the rest of the pipeline.

Files with no methods (or only zero-scoring ones) are omitted: they cannot be a complexity hotspot. Ranking is score-descending, with complexity then path as deterministic tie-breakers (so 0-churn files - e.g. outside a repo - still order by complexity rather than arbitrarily).

Constant Summary collapse

DEFAULT_WORST_METHODS =
3

Class Method Summary collapse

Class Method Details

.build_method(method_def, rel) ⇒ Object



69
70
71
72
73
74
75
76
# File 'lib/moult/scoring.rb', line 69

def build_method(method_def, rel)
  Report::Method.new(
    symbol_id: SymbolId.for(path: rel, start_line: method_def.span.start_line, fqname: method_def.name),
    name: method_def.name,
    span: method_def.span,
    abc: ABC.score(method_def.node)
  )
end

.build_report(root:, files:, churn:, worst_methods: DEFAULT_WORST_METHODS, git_ref: nil, generated_at: nil, churn_window: nil, churn_since: nil) ⇒ Report

Parameters:

  • root (String)

    absolute analysis root

  • files (Array<String>)

    absolute paths of Ruby files to analyse

  • churn (Hash{String=>Integer})

    path (relative to root) => commit count

  • worst_methods (Integer) (defaults to: DEFAULT_WORST_METHODS)

    how many worst methods to keep per file

Returns:



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/moult/scoring.rb', line 27

def build_report(root:, files:, churn:, worst_methods: DEFAULT_WORST_METHODS,
  git_ref: nil, generated_at: nil, churn_window: nil, churn_since: nil)
  hotspots = files.filter_map do |abs|
    hotspot_for(abs, root: root, churn: churn, worst_methods: worst_methods)
  end
  hotspots.sort_by! { |h| [-h.score, -h.complexity, h.path] }

  Report.new(
    root: root,
    hotspots: hotspots,
    git_ref: git_ref,
    generated_at: generated_at,
    churn_window: churn_window,
    churn_since: churn_since
  )
end

.combine(complexity, churn) ⇒ Numeric

The v0.1 scoring rule. Swap-point for future normalisation.

Returns:

  • (Numeric)


65
66
67
# File 'lib/moult/scoring.rb', line 65

def combine(complexity, churn)
  complexity * churn
end

.hotspot_for(abs, root:, churn:, worst_methods:) ⇒ Report::Hotspot?

Returns nil when the file has no scoring methods.

Returns:



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/moult/scoring.rb', line 45

def hotspot_for(abs, root:, churn:, worst_methods:)
  rel = relative_path(abs, root)
  methods = Parser.parse_file(abs).map { |m| build_method(m, rel) }
  complexity = methods.sum(0.0, &:abc)
  return nil if complexity.zero?

  churn_count = churn[rel]
  kept = methods.sort_by { |m| -m.abc }.first(worst_methods)

  Report::Hotspot.new(
    path: rel,
    score: combine(complexity, churn_count).round(2),
    complexity: complexity.round(2),
    churn: churn_count,
    methods: kept
  )
end

.relative_path(abs, root) ⇒ Object



78
79
80
# File 'lib/moult/scoring.rb', line 78

def relative_path(abs, root)
  SymbolId.relative_path(abs, root)
end