Class: ActiveSanction::Scorer::Reason

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

Overview

One line of the answer to "why did this score 87?".

ActiveSanction::Scorer::Reason.new(
factor: :dob, detail: "year 1948 matches", contribution: 6.0
)

The explanation adds up

A Result's explanation is a list of these, and their contributions sum to exactly the score. That is the whole design: the name blend produces the first one and every secondary identifier appends a signed delta, so a reviewer reading the list downwards arrives at the number on the report rather than at something near it.

It is a property the suite holds rather than a coincidence of the arithmetic. Contributions are rounded once, here, and the score is the sum of the rounded values -- not the rounded sum, which is how a total ends up one tenth away from the figures printed beside it. Where clamping moves the total off the sum, that correction is itself a reason (:clamp), because a score that silently stopped at 100 is a score whose explanation no longer explains it.

Why this is a required output and not a debugging aid

A compliance officer has to defend a screening decision to an examiner, and "the library said 87" is not a defence. Both directions matter: an alert that cannot be accounted for cannot be cleared, and a clearance that cannot be accounted for is the one an examiner asks about. So every adjustment the scorer makes writes one of these, including the ones that lower a score.

Instances are frozen on construction and compare by value.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(factor:, detail:, contribution:) ⇒ void

Parameters:

  • factor (T.untyped)
  • detail (T.untyped)
  • contribution (T.untyped)


86
87
88
89
90
91
# File 'lib/active_sanction/scorer/reason.rb', line 86

def initialize(factor:, detail:, contribution:)
  @factor = T.let(factor!(factor), Symbol)
  @detail = T.let(detail!(detail), String)
  @contribution = T.let(Float(contribution).round(PRECISION).to_f, Float)
  freeze
end

Instance Attribute Details

#contribution ⇒ Float (readonly)

Signed, in the same 0..100 units as the score. Positive raises the score and negative lowers it.

Returns:

  • (Float)


74
75
76
# File 'lib/active_sanction/scorer/reason.rb', line 74

def contribution
  @contribution
end

#detail ⇒ String (readonly)

Written for a person, and it names what was compared rather than which rule fired: "year 1948 matches", not "dob_overlap". A reviewer reading it should not need this library's vocabulary.

Returns:

  • (String)


69
70
71
# File 'lib/active_sanction/scorer/reason.rb', line 69

def detail
  @detail
end

#factor ⇒ Symbol (readonly)

Returns:

  • (Symbol)


63
64
65
# File 'lib/active_sanction/scorer/reason.rb', line 63

def factor
  @factor
end

Class Method Details

.from_h(hash) ⇒ T.attached_class

Parameters:

  • hash (T.untyped)

Returns:

  • (T.attached_class)

Raises:



77
78
79
80
81
82
83
# File 'lib/active_sanction/scorer/reason.rb', line 77

def self.from_h(hash)
  attributes = hash.to_h.transform_keys(&:to_sym)
  unknown = attributes.keys - MEMBERS
  raise InvalidArgument, "unknown Reason 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)


104
105
106
107
108
# File 'lib/active_sanction/scorer/reason.rb', line 104

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

  to_h == other.to_h
end

#hash ⇒ Integer

Returns:

  • (Integer)


112
# File 'lib/active_sanction/scorer/reason.rb', line 112

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

#inspect ⇒ String

Returns:

  • (String)


115
# File 'lib/active_sanction/scorer/reason.rb', line 115

def inspect = "#<#{self.class} #{self}>"

#penalty? ⇒ Boolean

Returns:

  • (Boolean)


94
# File 'lib/active_sanction/scorer/reason.rb', line 94

def penalty? = contribution.negative?

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

Returns:

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


97
# File 'lib/active_sanction/scorer/reason.rb', line 97

def to_h = { factor: factor, detail: detail, contribution: contribution }

#to_s ⇒ String

The line a report prints: "+6.0 dob: year 1948 matches".

Returns:

  • (String)


101
# File 'lib/active_sanction/scorer/reason.rb', line 101

def to_s = format("%+.#{PRECISION}f %s: %s", contribution, factor, detail)