Class: ActiveSanction::Doctor::Finding

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/active_sanction/doctor/finding.rb

Overview

One thing a diagnosis noticed about one source.

finding.source     # => :ofac_sdn
finding.severity   # => :warn
finding.check      # => :remarks_coverage
finding.observed   # => 0.714
finding.baseline   # => 0.973
finding.to_s       # => "remarks coverage 71.4% (was 97.3%): \"Passport No.\" x 1880 unrecognized"

Three severities, and what separates them

:error  this list is not what it was, and screening against it is
      unsafe -- it did not parse, it parsed to nothing, a column
      holds something else now, a field that every record carried
      is gone from all of them
:warn   a measurement moved further than a list of this kind moves in
      a day, and a human should look at it before the next sync
:info   something changed, or something is unrecognized, and it is
      within what these files do on their own

The line between error and warn is not how big the number is. It is whether the reading can be explained by the list changing rather than by the file changing. A quarter of the records disappearing is a warn, because a delisting wave looks exactly like that and deciding which one it was is a judgment nothing here is entitled to make. A column that used to be numeric and is now full of company names is an error, because nothing a publisher does to its list can do that to its file.

Observed and baseline are numbers, not prose

message is written for a person reading a terminal at three in the morning. observed and baseline are for everything else: a threshold in a monitoring rule, a graph of a fill rate over ninety days, the instrumentation hooks (#59) a host application alerts through. Both are nil for a finding that is not a measurement -- a parse failure has no number.

Instances are frozen on construction and compare by value.

Constant Summary collapse

SEVERITIES =

Ordered, least serious first: comparing two severities is comparing their positions here.

T.let(%i[info warn error].freeze, T::Array[Symbol])

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source:, severity:, check:, message:, observed: nil, baseline: nil) ⇒ void

Parameters:

  • source (T.untyped)
  • severity (T.untyped)
  • check (T.untyped)
  • message (T.untyped)
  • observed (T.untyped) (defaults to: nil)
  • baseline (T.untyped) (defaults to: nil)


102
103
104
105
106
107
108
109
110
# File 'lib/active_sanction/doctor/finding.rb', line 102

def initialize(source:, severity:, check:, message:, observed: nil, baseline: nil)
  @source = T.let(symbol!(:source, source), Symbol)
  @severity = T.let(severity!(severity), Symbol)
  @check = T.let(symbol!(:check, check), Symbol)
  @message = T.let(message!(message), String)
  @observed = T.let(observed, T.untyped)
  @baseline = T.let(baseline, T.untyped)
  freeze
end

Instance Attribute Details

#baseline ⇒ T.untyped (readonly)

Returns:

  • (T.untyped)


81
82
83
# File 'lib/active_sanction/doctor/finding.rb', line 81

def baseline
  @baseline
end

#check ⇒ Symbol (readonly)

Which check produced this, as a stable machine name -- :record_count, :fill_identifiers, :column_ent_num. What a monitoring rule is written against, and what stays the same when the message is reworded.

Returns:

  • (Symbol)


69
70
71
# File 'lib/active_sanction/doctor/finding.rb', line 69

def check
  @check
end

#message ⇒ String (readonly)

Returns:

  • (String)


72
73
74
# File 'lib/active_sanction/doctor/finding.rb', line 72

def message
  @message
end

#observed ⇒ T.untyped (readonly)

What was measured this run, and what it was measured against. nil for a finding that is not a measurement, and baseline is nil as well for the first run of a source, where there was nothing to compare with.

Returns:

  • (T.untyped)


78
79
80
# File 'lib/active_sanction/doctor/finding.rb', line 78

def observed
  @observed
end

#severity ⇒ Symbol (readonly)

One of SEVERITIES.

Returns:

  • (Symbol)


63
64
65
# File 'lib/active_sanction/doctor/finding.rb', line 63

def severity
  @severity
end

#source ⇒ Symbol (readonly)

Returns:

  • (Symbol)


59
60
61
# File 'lib/active_sanction/doctor/finding.rb', line 59

def source
  @source
end

Class Method Details

.at_least?(first, second) ⇒ Boolean

Whether first is at least as serious as second.

Parameters:

  • first (Symbol)
  • second (Symbol)

Returns:

  • (Boolean)


94
95
96
# File 'lib/active_sanction/doctor/finding.rb', line 94

def self.at_least?(first, second)
  SEVERITIES.index(first).to_i >= SEVERITIES.index(second).to_i
end

.from_h(hash) ⇒ T.attached_class

Parameters:

  • hash (T.untyped)

Returns:

  • (T.attached_class)

Raises:



84
85
86
87
88
89
90
# File 'lib/active_sanction/doctor/finding.rb', line 84

def self.from_h(hash)
  attributes = hash.to_h.transform_keys(&:to_sym)
  unknown = attributes.keys - MEMBERS
  raise InvalidArgument, "unknown Doctor::Finding attribute(s): #{unknown.join(", ")}" if unknown.any?

  T.unsafe(self).new(**attributes)
end

Instance Method Details

#==(other) ⇒ Boolean Also known as: eql?

Parameters:

  • other (T.untyped)

Returns:

  • (Boolean)


144
145
146
147
148
# File 'lib/active_sanction/doctor/finding.rb', line 144

def ==(other)
  return false unless other.instance_of?(self.class)

  to_h == other.to_h
end

#at_least?(level) ⇒ Boolean

Whether this finding is at least as serious as level.

Parameters:

  • level (T.untyped)

Returns:

  • (Boolean)


123
# File 'lib/active_sanction/doctor/finding.rb', line 123

def at_least?(level) = Finding.at_least?(severity, symbol!(:severity, level))

#compared? ⇒ Boolean

Whether there was anything to compare against. False says the reading was held to a committed floor rather than to the last sync.

Returns:

  • (Boolean)


128
# File 'lib/active_sanction/doctor/finding.rb', line 128

def compared? = !baseline.nil?

#error? ⇒ Boolean

Returns:

  • (Boolean)


113
# File 'lib/active_sanction/doctor/finding.rb', line 113

def error? = severity == :error

#hash ⇒ Integer

Returns:

  • (Integer)


152
# File 'lib/active_sanction/doctor/finding.rb', line 152

def hash = [self.class, to_h].hash

#info? ⇒ Boolean

Returns:

  • (Boolean)


119
# File 'lib/active_sanction/doctor/finding.rb', line 119

def info? = severity == :info

#inspect ⇒ String

Returns:

  • (String)


155
# File 'lib/active_sanction/doctor/finding.rb', line 155

def inspect = "#<#{self.class} #{source} #{severity} #{check}: #{message}>"

#to_h ⇒ Hash{Symbol => T.untyped}

Returns:

  • (Hash{Symbol => T.untyped})


131
132
133
134
# File 'lib/active_sanction/doctor/finding.rb', line 131

def to_h
  { source: source, severity: severity, check: check, message: message,
    observed: observed, baseline: baseline }
end

#to_line ⇒ String

The line the report prints under a source.

Returns:

  • (String)


141
# File 'lib/active_sanction/doctor/finding.rb', line 141

def to_line = "  #{severity.to_s.ljust(5)}  #{message}"

#to_s ⇒ String

Returns:

  • (String)


137
# File 'lib/active_sanction/doctor/finding.rb', line 137

def to_s = message

#warn? ⇒ Boolean

Returns:

  • (Boolean)


116
# File 'lib/active_sanction/doctor/finding.rb', line 116

def warn? = severity == :warn