Class: Rigor::Analysis::Baseline

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/analysis/baseline.rb,
sig/rigor/analysis/baseline.rbs

Overview

ADR-22 Slice 1 — per-project baseline filter. See lib/rigor/analysis/baseline.rb for the full contract.

Defined Under Namespace

Classes: Bucket, DriftRow, LoadError

Constant Summary collapse

CURRENT_VERSION =
1

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(buckets, project_root: Dir.pwd) ⇒ Baseline

Returns a new instance of Baseline.

Parameters:

  • buckets (Object)


181
182
183
184
185
186
187
188
189
190
191
# File 'lib/rigor/analysis/baseline.rb', line 181

def initialize(buckets, project_root: Dir.pwd)
  @project_root = Pathname.new(project_root)
  @buckets = buckets.freeze
  # For each (file, qualified_rule) pair, two arrays:
  # - rule-ID rows (message_regex == nil)
  # - message-pattern rows (message_regex != nil)
  # The matcher walks message-pattern rows first (tighter match takes precedence); diagnostics that
  # don't match any message row fall through to the rule-ID row if one exists.
  @by_pair = buckets.group_by { |b| [b.file, b.rule] }.freeze
  freeze
end

Instance Attribute Details

#bucketsObject (readonly)

Returns the value of attribute buckets.

Returns:

  • (Object)


179
180
181
# File 'lib/rigor/analysis/baseline.rb', line 179

def buckets
  @buckets
end

Class Method Details

.from_diagnostics(diagnostics, match_mode: :rule, project_root: Dir.pwd) ⇒ Baseline

Build a baseline from a current run's diagnostic stream.

Parameters:

  • diagnostics (Object)
  • match_mode: (:rule, :message) (defaults to: :rule)

Returns:



85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/rigor/analysis/baseline.rb', line 85

def from_diagnostics(diagnostics, match_mode: :rule, project_root: Dir.pwd)
  raise ArgumentError, "match_mode must be :rule or :message" unless %i[rule message].include?(match_mode)

  grouped = group_for_baseline(diagnostics, match_mode, project_root)
  buckets = grouped.map do |key, entries|
    Bucket.new(
      file: key[0],
      rule: key[1],
      message_regex: key[2],
      count: entries.size
    )
  end
  new(buckets, project_root: project_root)
end

.load(path, project_root: Dir.pwd) ⇒ Baseline?

Load a baseline file from disk. Returns nil when path is nil (no baseline configured). Raises LoadError on malformed or unsupported content.

Parameters:

  • path (::String, nil)

Returns:



71
72
73
74
75
76
77
# File 'lib/rigor/analysis/baseline.rb', line 71

def load(path, project_root: Dir.pwd)
  return nil if path.nil?
  return new([], project_root: project_root) unless File.exist?(path)

  raw = YAML.safe_load_file(path, permitted_classes: [Symbol])
  parse_loaded(raw, path: path, project_root: project_root)
end

Instance Method Details

#audit(diagnostics) ⇒ Object

Report bucket-level drift for each recorded baseline bucket.

Parameters:

  • diagnostics (Object)

Returns:

  • (Object)


242
243
244
245
246
247
248
249
250
251
252
253
254
255
# File 'lib/rigor/analysis/baseline.rb', line 242

def audit(diagnostics)
  counts = Hash.new(0)
  diagnostics.each do |diag|
    next if diag.qualified_rule.nil? || diag.path.nil?

    bucket = claim_bucket_for(diag)
    counts[bucket_key(bucket)] += 1 if bucket
  end

  buckets.map do |bucket|
    actual = counts[bucket_key(bucket)]
    DriftRow.new(bucket: bucket, actual_count: actual, status: status_for(actual, bucket.count))
  end
end

#empty?Boolean

Returns:

  • (Boolean)


283
284
285
# File 'lib/rigor/analysis/baseline.rb', line 283

def empty?
  buckets.empty?
end

#filter(diagnostics) ⇒ [untyped, ::Integer]

Apply the baseline filter. Returns [surfaced_diagnostics, silenced_count].

Parameters:

  • diagnostics (Object)

Returns:

  • ([untyped, ::Integer])


198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/rigor/analysis/baseline.rb', line 198

def filter(diagnostics)
  return [diagnostics, 0] if buckets.empty?

  grouped = group_diagnostics_for_filtering(diagnostics)
  surfaced = []
  silenced_count = 0

  grouped.each_value do |entries|
    bucket = entries[:bucket]
    diags = entries[:diagnostics]
    # No matching bucket → all surface as new findings. `actual <= count` → all silenced (within
    # threshold, WD4). `actual >  count` → all surface (over threshold, WD4).
    if bucket && diags.size <= bucket.count
      silenced_count += diags.size
    else
      surfaced.concat(diags)
    end
  end

  # Diagnostics that lacked a rule or a path bypass the baseline entirely (the baseline can't address
  # them).
  unkeyable = diagnostics.reject { |d| d.qualified_rule && d.path }
  [surfaced + unkeyable, silenced_count]
end

#size::Integer

Number of recorded buckets.

Returns:

  • (::Integer)


279
280
281
# File 'lib/rigor/analysis/baseline.rb', line 279

def size
  buckets.size
end

#to_yaml::String

Serialise to a YAML string.

Returns:

  • (::String)


266
267
268
269
270
271
272
273
274
275
276
# File 'lib/rigor/analysis/baseline.rb', line 266

def to_yaml
  rows = buckets.map do |bucket|
    row = { "file" => bucket.file, "rule" => bucket.rule }
    row["message"] = bucket.message_regex.source if bucket.message_regex
    row["count"] = bucket.count
    row
  end

  document = { "version" => CURRENT_VERSION, "ignored" => rows }
  YAML.dump(document)
end

#without(buckets_to_drop) ⇒ Baseline

Return a new Baseline with the given buckets removed.

Parameters:

  • buckets_to_drop (Object)

Returns:



259
260
261
262
# File 'lib/rigor/analysis/baseline.rb', line 259

def without(buckets_to_drop)
  dropset = buckets_to_drop.to_set
  self.class.new(buckets.reject { |b| dropset.include?(b) }, project_root: @project_root)
end