Module: Moult::Flags::Classification

Defined in:
lib/moult/flags/classification.rb

Overview

The per-finding model for feature flags — this slice's realisation of Moult's protected per-finding API. Like a packwerk boundary violation (see Boundaries::Severity), a flag reference is a recorded FACT, not a probabilistic candidate: the scanner saw the call site. So we never manufacture a fake confidence (the finding's confidence is null); the per-finding signal is a categorical CLASSIFICATION instead — the flag's value_type, how many times it is referenced, and the literal default value(s) observed.

The genuinely confidence-graded judgement — staleness (is this flag dead / obsolete?) — needs a live OpenFeature provider to know which keys still exist, and is deferred (like the Coverband/Flipper live stores). So the humility invariant holds in this register too: a static scan can never prove a flag is unused (it may be referenced dynamically, via provider config, or from outside the codebase), and nothing here says it is.

Classification.classify is a pure function of the observed signals — no IO, no Prism nodes — so it is pinned against hand-built inputs exactly like ABC, the coverage Resolver, the duplication Confidence model, and Boundaries::Severity. Drift is a bug.

Defined Under Namespace

Classes: Assessment, Reason

Constant Summary collapse

CATEGORY =
"feature_flag"
VALUE_TYPES =

The value-type classification. boolean/string/number/object are read from the fetch__* method; unknown is reserved for a flag referenced with more than one type (an ambiguity we record rather than resolve).

%w[boolean string number object unknown].freeze
MIXED =
"unknown"

Class Method Summary collapse

Class Method Details

.classify(value_types:, default_values:) ⇒ Assessment

Parameters:

  • value_types (Array<String>)

    one observed value_type per call site

  • default_values (Array<String, nil>)

    one observed literal default per call site (nil where the default was not a literal)

Returns:



53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/moult/flags/classification.rb', line 53

def classify(value_types:, default_values:)
  observed = value_types.uniq.sort
  value_type = (observed.size == 1) ? observed.first : MIXED
  reference_count = value_types.size
  defaults = default_values.compact.uniq.sort

  reasons = [type_reason(value_type, observed, reference_count)]
  reasons << Reason.new(rule: :reference_count, detail: "referenced at #{pluralize(reference_count, "call site")}")
  reasons << Reason.new(rule: :default_values, detail: "observed default value(s): #{defaults.join(", ")}") unless defaults.empty?

  Assessment.new(value_type: value_type, reference_count: reference_count, default_values: defaults, reasons: reasons)
end

.pluralize(count, noun) ⇒ Object



74
75
76
# File 'lib/moult/flags/classification.rb', line 74

def pluralize(count, noun)
  "#{count} #{noun}#{"s" unless count == 1}"
end

.type_reason(value_type, observed, reference_count) ⇒ Object



66
67
68
69
70
71
72
# File 'lib/moult/flags/classification.rb', line 66

def type_reason(value_type, observed, reference_count)
  if value_type == MIXED
    Reason.new(rule: :mixed_value_types, detail: "referenced with differing value types (#{observed.join(", ")}); the flag type is ambiguous")
  else
    Reason.new(rule: :"#{value_type}_flag", detail: "evaluated as a #{value_type} flag across #{pluralize(reference_count, "reference")}")
  end
end