Module: Moult::Gate::Evaluation

Defined in:
lib/moult/gate/evaluation.rb

Overview

The pure verdict engine: given already-scoped observations and a Policy, it decides each rule's outcome and the single top-level verdict. No IO, no git, no analysis objects — just the policy applied to hand-buildable facts — so it is pinned in test/test_gate_policy.rb. Drift is a bug.

The verdict is an auditable APPLICATION of a recorded policy over confidence-graded candidates; it never claims code is certainly wrong or dead. A rule whose backing analysis didn't run is marked evaluated: false and never fails the gate (a broken tool is a tool-error concern, surfaced by the CLI exit code, not a policy violation).

Defined Under Namespace

Classes: BoundaryObs, ComplexityObs, Contribution, DeadCodeObs, DuplicationObs, Observations, Reason, RuleOutcome, Verdict

Constant Summary collapse

SEVERITY_SCALE =
Boundaries::Severity::SCALE

Class Method Summary collapse

Class Method Details

.boundary_rule(obs, policy, diagnostic) ⇒ Object



101
102
103
104
105
106
107
108
109
110
# File 'lib/moult/gate/evaluation.rb', line 101

def boundary_rule(obs, policy, diagnostic)
  t = policy.boundary_max_severity
  ti = SEVERITY_SCALE.index(t) || 0
  outcome("no_new_high_severity_boundary", "<= #{t}", obs, diagnostic, "architecture_boundary") do |list|
    violating = list.select { |o| (SEVERITY_SCALE.index(o.severity) || 0) > ti }
    detail = phrase(violating, "no new boundary violation exceeds #{t} severity in changed files",
      "new boundary violation(s) above #{t} severity in changed files")
    [violating, list.map(&:severity).max_by { |s| SEVERITY_SCALE.index(s) || -1 }, detail, ->(o) { o.severity }]
  end
end

.complexity_rule(obs, policy, diagnostic) ⇒ Object



112
113
114
115
116
117
118
119
120
# File 'lib/moult/gate/evaluation.rb', line 112

def complexity_rule(obs, policy, diagnostic)
  t = policy.complexity_ceiling
  outcome("new_code_complexity_ceiling", "<= #{t}", obs, diagnostic, "complexity") do |list|
    violating = list.select { |o| o.abc > t }
    detail = phrase(violating, "no changed method exceeds ABC complexity #{t}",
      "changed method(s) exceed ABC complexity #{t}")
    [violating, list.map(&:abc).max, detail, ->(o) { o.abc }]
  end
end

.dead_code_rule(obs, policy, diagnostic) ⇒ Object

---- rules ----------------------------------------------------------------

Each rule names the threshold and the genuinely varying bits — which observations violate it, the worst observed value, the per-finding value, and a noun phrase for the detail — and hands them to outcome, which owns the shared RuleOutcome construction.



91
92
93
94
95
96
97
98
99
# File 'lib/moult/gate/evaluation.rb', line 91

def dead_code_rule(obs, policy, diagnostic)
  t = policy.dead_code_max_confidence
  outcome("no_new_dead_code", "< #{t}", obs, diagnostic, "dead_code") do |list|
    violating = list.select { |o| o.confidence >= t }
    detail = phrase(violating, "no new dead-code candidate reaches confidence #{t} on changed lines",
      "new dead-code candidate(s) at or above confidence #{t} on changed lines")
    [violating, list.map(&:confidence).max, detail, ->(o) { o.confidence }]
  end
end

.duplication_rule(obs, policy, diagnostic) ⇒ Object



122
123
124
125
126
127
128
129
130
# File 'lib/moult/gate/evaluation.rb', line 122

def duplication_rule(obs, policy, diagnostic)
  t = policy.duplication_max_mass
  outcome("new_code_duplication_ceiling", "<= #{t}", obs, diagnostic, "structural_duplication") do |list|
    violating = list.select { |o| o.mass > t }
    detail = phrase(violating, "no clone group touching the diff exceeds mass #{t}",
      "clone group(s) touching the diff exceed mass #{t}")
    [violating, list.map(&:mass).max, detail, ->(o) { o.mass }]
  end
end

.evaluate(observations:, policy:) ⇒ Verdict

Parameters:

Returns:



70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/moult/gate/evaluation.rb', line 70

def evaluate(observations:, policy:)
  diags = observations.diagnostics || {}
  rules = [
    dead_code_rule(observations.dead_code, policy, diags[:dead_code]),
    boundary_rule(observations.boundaries, policy, diags[:boundaries]),
    complexity_rule(observations.complexity, policy, diags[:complexity]),
    duplication_rule(observations.duplication, policy, diags[:duplication])
  ]

  failed = rules.select { |r| r.evaluated && r.passed == false }
  verdict = failed.empty? ? "pass" : "fail"
  Verdict.new(verdict: verdict, reasons: verdict_reasons(verdict, failed), rules: rules)
end

.outcome(rule, threshold, obs, diagnostic, category) ⇒ Object

Build a rule's outcome. The block, given the (non-nil) observation list, returns [violating, observed, detail, value_extractor]; a nil list means the backing analysis was skipped, so the rule is not evaluated and cannot fail.



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/moult/gate/evaluation.rb', line 137

def outcome(rule, threshold, obs, diagnostic, category)
  return skipped(rule, threshold, diagnostic) if obs.nil?

  violating, observed, detail, value = yield(obs)
  RuleOutcome.new(
    rule: rule, evaluated: true, observed: observed, threshold: threshold,
    passed: violating.empty?,
    reasons: [Reason.new(rule: rule.to_sym, detail: detail)],
    findings: violating.map { |o| Contribution.new(category: category, path: o.path, symbol_id: o.symbol_id, line: o.line, value: value.call(o)) }
  )
end

.phrase(violating, none, some) ⇒ Object

"no X ..." when nothing violates, " X ..." otherwise.



159
160
161
# File 'lib/moult/gate/evaluation.rb', line 159

def phrase(violating, none, some)
  violating.empty? ? none : "#{violating.size} #{some}"
end

.skipped(rule, threshold, diagnostic) ⇒ Object



149
150
151
152
153
154
155
156
# File 'lib/moult/gate/evaluation.rb', line 149

def skipped(rule, threshold, diagnostic)
  RuleOutcome.new(
    rule: rule, evaluated: false,
    observed: nil, threshold: threshold, passed: nil,
    reasons: [Reason.new(rule: :skipped, detail: diagnostic || "backing analysis did not run; rule not evaluated")],
    findings: []
  )
end

.verdict_reasons(verdict, failed) ⇒ Object



163
164
165
166
167
168
169
# File 'lib/moult/gate/evaluation.rb', line 163

def verdict_reasons(verdict, failed)
  if verdict == "pass"
    [Reason.new(rule: :clean_as_you_code, detail: "all evaluated policy rules passed on the scoped changes")]
  else
    failed.map { |r| Reason.new(rule: r.rule.to_sym, detail: r.reasons.first.detail) }
  end
end