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
- .boundary_rule(obs, policy, diagnostic) ⇒ Object
- .complexity_rule(obs, policy, diagnostic) ⇒ Object
-
.dead_code_rule(obs, policy, diagnostic) ⇒ Object
---- rules ----------------------------------------------------------------.
- .duplication_rule(obs, policy, diagnostic) ⇒ Object
- .evaluate(observations:, policy:) ⇒ Verdict
-
.outcome(rule, threshold, obs, diagnostic, category) ⇒ Object
Build a rule's outcome.
-
.phrase(violating, none, some) ⇒ Object
"no X ..." when nothing violates, "
X ..." otherwise. - .skipped(rule, threshold, diagnostic) ⇒ Object
- .verdict_reasons(verdict, failed) ⇒ Object
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
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, "
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 |