Module: StillActive::StatusHelper

Extended by:
StatusHelper
Included in:
StatusHelper
Defined in:
lib/still_active/helpers/status_helper.rb

Overview

Collapses a gem's several maintenance signals into one categorical verdict, so a machine/LLM consumer (or another tool's report) can display and threshold a single value instead of re-deriving it from activity_level + archived + vulnerability_count. This is deliberately NOT a numeric composite: an earlier 0-100 score was removed because a weighted average let missing data read as "perfect health". Here, :unknown stays :unknown -- absence of data is never rendered as :ok.

Constant Summary collapse

SEVERITY =

Worst-first lifecycle verdict. The key distinction (from the "done gems" critique and validated against the maintenance-tooling landscape): a clean, long-dormant gem is :legacy ("done", low risk), NOT a problem -- whereas a dormant or archived gem carrying an unpatched advisory is :dead (no one is going to fix it, migrate). :unknown is least severe -- an absence, not a finding -- so missing data never reads as :ok. :deprecated sits above :archived and below :vulnerable. An archived repo is circumstantial (development may simply have moved), whereas a deprecation is the maintainer explicitly telling you to stop using the package, frequently naming the replacement. A live vulnerability is still the more urgent thing.

[:unknown, :ok, :legacy, :stale, :archived, :deprecated, :vulnerable, :dead].freeze

Instance Method Summary collapse

Instance Method Details

#gem_status(gem_data) ⇒ Object

Returns :dead, :vulnerable, :deprecated, :archived, :legacy, :stale, :ok, or :unknown.



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/still_active/helpers/status_helper.rb', line 30

def gem_status(gem_data)
  vulnerable = gem_data[:vulnerability_count].to_i.positive?

  # A pinned version the registry can't resolve (yanked, typo, or nonexistent)
  # has no version-specific data to judge, so package-level health must not read
  # it as :ok. Absence of data is :unknown, never a false all-clear. Guarded on
  # `!vulnerable` so a detected advisory always wins, in case a future source
  # ever attaches one to an otherwise-unresolved version.
  return :unknown if gem_data[:version_unresolved] && !vulnerable

  level = ActivityHelper.activity_level(gem_data)
  # The maintainer's own declaration that this package should no longer be
  # used. Unlike every other input here it is a stated fact rather than a date
  # heuristic, so it is read before the activity level rather than blended
  # with it.
  deprecated = gem_data[:deprecated] == true

  if vulnerable
    # A vulnerability in a dormant, archived or deprecated gem won't be patched
    # -> :dead; in an actively-released gem a fix is plausible -> :vulnerable.
    # Deprecation counts even on a gem still shipping releases: the maintainer
    # has said to stop using it, so waiting for the patch is not a plan.
    return (deprecated || [:critical, :archived].include?(level)) ? :dead : :vulnerable
  end

  # Deliberately ahead of the :critical -> :legacy branch below. :legacy means
  # "long-dormant but done, low risk", and a deprecated package is the exact
  # case that must not be filed there: left-pad is dormant AND clean AND its
  # maintainer says to use String.prototype.padStart() instead.
  return :deprecated if deprecated

  case level
  when :archived
    # archived != EOL: a repo archived while the gem still publishes recent
    # releases (development moved to a monorepo) isn't dead -- let the
    # releases speak, but keep :stale so the archived repo stays a yellow flag.
    (ActivityHelper.release_recency_level(gem_data) == :ok) ? :stale : :archived
  when :critical then :legacy # long-dormant but clean: feature-complete, not a fire
  else level # :stale / :ok / :unknown
  end
end

#project_status(result, ruby_info: nil) ⇒ Object

The single worst gem status across the audit. An EOL Ruby floors the project at :vulnerable (the runtime itself is a live, actionable risk). :unknown only wins when nothing better is known.



75
76
77
78
79
80
81
82
83
84
# File 'lib/still_active/helpers/status_helper.rb', line 75

def project_status(result, ruby_info: nil)
  statuses = result.each_value.map { |data| gem_status(data) }
  statuses << :vulnerable if ruby_info&.dig(:eol) == true
  return :unknown if statuses.empty?

  # Every value gem_status returns is in SEVERITY today; if a future status
  # isn't, rank it most-severe so it surfaces in the rollup rather than being
  # silently masked by a milder finding.
  statuses.max_by { |status| SEVERITY.index(status) || SEVERITY.length }
end