Module: Moult::Flags::Staleness

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

Overview

The confidence-graded per-finding model for feature-flag STALENESS — this slice's first real use of Moult's protected per-finding confidence API. Where Classification grades recorded USAGE (a reference is a fact, so its confidence is null), staleness is a genuine judgement: given a flag's observed references and the state the provider snapshot reports for its key, how strong a candidate is it for removal?

Like the static<->runtime coverage merge (see Coverage::Resolver), the snapshot is EVIDENCE, not proof. A flag is never asserted certainly stale or dead. The provider state (archived / disabled / fully rolled out) and the join result (absent — referenced in code, unknown to the provider) raise confidence; dynamic, non-literal keys in the codebase LOWER it, because the snapshot may be partial or the key resolved dynamically.

Staleness.classify is a pure function of the joined facts — no IO, no Prism, no clock — so it is pinned against hand-built inputs exactly like ABC, the coverage Resolver, the duplication Confidence model, Boundaries::Severity, and Classification. The statuses and confidence knees are deliberate v1 heuristics; drift is a bug.

Time-based "stale-since" decay (a flag untouched for N days) is deferred: it needs a now clock and a threshold knee, so it would make this model impure. The snapshot's exported_at and a flag's updated_at are captured as evidence to seed it later, exactly as coverage captured collected_at while deferring stale-detection.

Defined Under Namespace

Classes: Assessment, Reason

Constant Summary collapse

ARCHIVED =

The provider explicitly retired the flag. The strongest removal candidate.

"archived"
ABSENT =

Referenced in code, but the snapshot has no such key (deleted/renamed in the provider, or managed elsewhere). Strong, but humbled by dynamic references.

"absent"
DISABLED =

Disabled in the provider (served to no one): the enabled branch is unreachable.

"disabled"
ROLLED_OUT =

Fully rolled out (enabled, no targeting — one variant served to all): the other branch is never taken.

"rolled_out"
ACTIVE =

Enabled with targeting (serving multiple variations): actively evaluated, NOT a removal candidate.

"active"
STATUSES =
[ARCHIVED, ABSENT, DISABLED, ROLLED_OUT, ACTIVE].freeze
ARCHIVED_CONFIDENCE =

Pinned confidence knees per status (removal-candidate strength).

0.9
ABSENT_CONFIDENCE =
0.7
ROLLED_OUT_CONFIDENCE =
0.6
DISABLED_CONFIDENCE =
0.5
ACTIVE_CONFIDENCE =
0.0
DYNAMIC_REFERENCE_PENALTY =

Humility modifier: subtracted from an absent candidate when the codebase has dynamic (non-literal-key) flag references — the key the static scan could not resolve may BE this one, so the snapshot's silence is less trustworthy.

0.2

Class Method Summary collapse

Class Method Details

.absent(has_dynamic_references) ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
# File 'lib/moult/flags/staleness.rb', line 90

def absent(has_dynamic_references)
  reasons = [Reason.new(rule: :absent_from_provider,
    detail: "referenced in code but unknown to the provider snapshot (deleted or renamed in the provider); a candidate for removal")]
  confidence = ABSENT_CONFIDENCE
  if has_dynamic_references
    confidence -= DYNAMIC_REFERENCE_PENALTY
    reasons << Reason.new(rule: :dynamic_references,
      detail: "the codebase has dynamic (non-literal) flag keys, so the snapshot may be incomplete; confidence lowered")
  end
  assess(ABSENT, confidence, reasons)
end

.active(_state) ⇒ Object



123
124
125
126
127
# File 'lib/moult/flags/staleness.rb', line 123

def active(_state)
  reasons = [Reason.new(rule: :active,
    detail: "enabled with targeting (serving multiple variations); actively evaluated — not a removal candidate")]
  assess(ACTIVE, ACTIVE_CONFIDENCE, reasons)
end

.archived(state) ⇒ Object



102
103
104
105
106
107
# File 'lib/moult/flags/staleness.rb', line 102

def archived(state)
  reasons = [Reason.new(rule: :provider_archived,
    detail: "the provider marks this flag archived/retired; a strong candidate for removal")]
  reasons << updated_at_reason(state)
  assess(ARCHIVED, ARCHIVED_CONFIDENCE, reasons.compact)
end

.assess(status, confidence, reasons) ⇒ Object



136
137
138
# File 'lib/moult/flags/staleness.rb', line 136

def assess(status, confidence, reasons)
  Assessment.new(status: status, confidence: clamp(confidence), reasons: reasons)
end

.clamp(value) ⇒ Object



140
141
142
# File 'lib/moult/flags/staleness.rb', line 140

def clamp(value)
  value.clamp(0.0, 1.0).round(2)
end

.classify(state:, has_dynamic_references: false) ⇒ Assessment

Parameters:

  • state (Snapshot::FlagState, nil)

    the provider's state for this key, or nil when the snapshot does not know the key (an absent candidate)

  • has_dynamic_references (Boolean) (defaults to: false)

    whether the codebase has any dynamic (non-literal-key) flag references (a snapshot-completeness caveat)

Returns:



82
83
84
85
86
87
88
# File 'lib/moult/flags/staleness.rb', line 82

def classify(state:, has_dynamic_references: false)
  return absent(has_dynamic_references) if state.nil?
  return archived(state) if state.archived
  return disabled(state) if state.enabled == false
  return rolled_out(state) if state.enabled && !state.has_targeting
  active(state)
end

.disabled(state) ⇒ Object



109
110
111
112
113
114
# File 'lib/moult/flags/staleness.rb', line 109

def disabled(state)
  reasons = [Reason.new(rule: :provider_disabled,
    detail: "disabled in the provider (served to no one); the enabled branch is unreachable — a candidate for removal")]
  reasons << updated_at_reason(state)
  assess(DISABLED, DISABLED_CONFIDENCE, reasons.compact)
end

.rolled_out(state) ⇒ Object



116
117
118
119
120
121
# File 'lib/moult/flags/staleness.rb', line 116

def rolled_out(state)
  reasons = [Reason.new(rule: :fully_rolled_out,
    detail: "enabled with no targeting (one variant served to all); the other branch is never taken — a candidate for removal")]
  reasons << updated_at_reason(state)
  assess(ROLLED_OUT, ROLLED_OUT_CONFIDENCE, reasons.compact)
end

.updated_at_reason(state) ⇒ Object

An evidence note for a captured last-modified timestamp (deferred time-decay seed). nil when the snapshot recorded none, so it is compacted out.



131
132
133
134
# File 'lib/moult/flags/staleness.rb', line 131

def updated_at_reason(state)
  return nil unless state.updated_at
  Reason.new(rule: :last_modified, detail: "provider last modified this flag at #{state.updated_at}")
end