Module: Moult::Flags

Defined in:
lib/moult/flags.rb,
lib/moult/flags/snapshot.rb,
lib/moult/flags/staleness.rb,
lib/moult/flags/classification.rb

Overview

Orchestrates the feature-flag analysis: it asks the FlagScanner for every OpenFeature flag-evaluation call site, groups them by flag key, attributes each site to its enclosing method (best-effort, for the cross-analysis join), and grades each group through the pure Classification model. The result is a FlagsReport cataloguing flag USAGE.

When a provider snapshot is supplied (the static<->provider merge, the flags analogue of the static<->runtime coverage merge), it ALSO joins each flag key to the provider's recorded state and grades a confidence-graded Staleness candidate — the first real use of the per-finding confidence slot in this slice. The snapshot is evidence, never proof; nothing here asserts a flag is certainly stale or dead.

This is the only layer that joins the facts to symbols and to the provider; Classification and Staleness stay pure functions of the observed signals so they can be pinned in isolation, FlagScanner stays the sole keeper of the OpenFeature call shape, and Snapshot the sole keeper of the export format.

Defined Under Namespace

Modules: Classification, Snapshot, Staleness Classes: MethodIndex

Class Method Summary collapse

Class Method Details

.build_report(root:, files:, git_ref: nil, generated_at: nil, snapshot: nil) ⇒ FlagsReport

Parameters:

  • root (String)

    absolute analysis root

  • files (Array<String>)

    absolute Ruby file paths to scan

  • snapshot (Snapshot::FlagSet, nil) (defaults to: nil)

    a merged provider snapshot; when given, each finding gains a confidence-graded staleness candidate joined on flag_key

Returns:



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/moult/flags.rb', line 29

def build_report(root:, files:, git_ref: nil, generated_at: nil, snapshot: nil)
  sites = files.flat_map { |abs| scan(abs, root) }
  methods = MethodIndex.new(root: root, files: files)

  literal, dynamic = sites.partition { |s| !s.flag_key.nil? }
  has_dynamic = dynamic.size.positive?
  findings = literal.group_by(&:flag_key).map { |key, group| finding_for(key, group, methods, snapshot, has_dynamic) }
  # With a snapshot, strongest staleness candidate first (then refs, then key);
  # without, most-referenced first. Either way alphabetical by key breaks ties so
  # output is stable.
  findings.sort_by! do |f|
    f.staleness ? [-f.staleness.confidence, -f.reference_count, f.flag_key] : [-f.reference_count, f.flag_key]
  end

  FlagsReport.new(
    root: root,
    findings: findings,
    dynamic_references: dynamic.size,
    git_ref: git_ref,
    generated_at: generated_at,
    provider_source: snapshot&.source
  )
end

.finding_for(key, sites, methods, snapshot = nil, has_dynamic = false) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/moult/flags.rb', line 59

def finding_for(key, sites, methods, snapshot = nil, has_dynamic = false)
  assessment = Classification.classify(
    value_types: sites.map(&:value_type),
    default_values: sites.map(&:default_value)
  )
  occurrences = sites
    .sort_by { |s| [s.path, s.line] }
    .map { |s| FlagsReport::Occurrence.new(symbol_id: methods.symbol_id_at(s.path, s.line), path: s.path, line: s.line, method_name: s.method_name) }
  staleness = staleness_for(key, snapshot, has_dynamic)
  FlagsReport::Finding.new(
    flag_key: key,
    value_type: assessment.value_type,
    reference_count: assessment.reference_count,
    default_values: assessment.default_values,
    reasons: assessment.reasons,
    occurrences: occurrences,
    staleness: staleness
  )
end

.scan(abs, root) ⇒ Object



53
54
55
56
57
# File 'lib/moult/flags.rb', line 53

def scan(abs, root)
  FlagScanner.scan_file(abs, SymbolId.relative_path(abs, root))
rescue
  []
end

.staleness_for(key, snapshot, has_dynamic) ⇒ Object

The staleness candidate for a key, joined on the literal flag_key (the flags join key, mirroring how coverage joins on symbol_id). nil when no snapshot was supplied, leaving the finding byte-for-byte v1-identical.



82
83
84
85
# File 'lib/moult/flags.rb', line 82

def staleness_for(key, snapshot, has_dynamic)
  return nil unless snapshot
  Staleness.classify(state: snapshot.state_for(key), has_dynamic_references: has_dynamic)
end