Skip to content

Expand maintenance signals to transitive deps (keep alternatives direct-only) #60

Description

@SeanLF

Summary

Extend still_active's maintenance signals (stale releases, archived repo, last-commit age, advisories) to the full transitive lockfile graph, while keeping the "suggest an alternative" output scoped to direct dependencies only. When a transitive dep trips a signal, surface the path to the direct parent that pulls it in.

This is the follow-up to the direct/declared-dep work (#41 / #59). #59 fixed a correctness gap (a gemspec project's declared runtime deps were dropped); this is a deliberate scope expansion, tracked separately.

Motivation

Today still_active flags the deps you declare. But an unmaintained or archived gem you ship transitively is real risk you can't see, and the CVE tools we already compose with enumerate the whole resolved graph anyway, so the transitive set is already in hand.

Prior art (verified)

  • libyear-bundler — full transitive by default. It shells out to bundle outdated --parseable with no scope flag; Bundler's own man page documents --only-explicit as "Only list gems specified in your Gemfile, not their dependencies" — i.e. transitive is the default and you opt out. (Verified against libyear-bundler lib/libyear_bundler/bundle_outdated.rb + bundle help outdated on Bundler 4.0.12.) This is the closest analog we already compose with.
  • CVE scanners are all full-tree: bundler-audit (@lockfile.specs, not .dependencies), npm audit, cargo audit, pip-audit.
  • Snyk — the actionability rule: you can only fix/swap a direct dependency; for transitive deps Snyk gives recommendations plus a dependency path, not a direct swap.
  • Socket.dev — maintenance is scored tree-wide, but each dep is still labelled direct vs transitive in the UI.

Proposed design

  • Maintenance signals (stale / archived / last-commit / advisory) → computed over the full transitive set from the lockfile.
  • Alternativesdirect deps only. "Replace gem X with better-maintained Y" is incoherent for a gem the user didn't choose.
  • Path-to-parent for transitive flags: turn an un-actionable transitive signal into an actionable direct one, e.g. "your direct gem A pulls in archived B — consider replacing A." (Snyk's dependency-path / Socket's direct-vs-transitive labelling.)

Cost / considerations

  • Full-transitive means version-source + repo-signal lookups for many more gems (still_active's own providers, not just the CVE feed). libyear pays exactly this cost and accepts it, but it's an N× API-volume jump — design the cache around it, and lean on the CVE tools' already-enumerated graph rather than re-walking.
  • Keep the --only-explicit-style escape hatch in mind: some users will want direct-only output. A scope flag (default transitive for signals, opt-out to direct) mirrors Bundler's own ergonomics.

Scope boundary

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions