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
absentcandidate 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
- .absent(has_dynamic_references) ⇒ Object
- .active(_state) ⇒ Object
- .archived(state) ⇒ Object
- .assess(status, confidence, reasons) ⇒ Object
- .clamp(value) ⇒ Object
- .classify(state:, has_dynamic_references: false) ⇒ Assessment
- .disabled(state) ⇒ Object
- .rolled_out(state) ⇒ Object
-
.updated_at_reason(state) ⇒ Object
An evidence note for a captured last-modified timestamp (deferred time-decay seed).
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
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 |