Module: StillActive::ConstraintHelper
- Extended by:
- ConstraintHelper
- Included in:
- ConstraintHelper
- Defined in:
- lib/helpers/constraint_helper.rb
Overview
Reads a declared dependency constraint (the requirement string a package publishes for one of its runtime deps, e.g. "~> 4.2", "< 5.0", "= 1.2.3") and answers how much it caps you: is there an upper bound, and how many majors behind the dependency's current latest does it hold you?
This is the constraint half of the poison-pill signal (constraint-tightness x maintenance-state): a dormant package that caps a still-evolving dep below its latest major holds your tree hostage with no hope of the cap lifting. A fixed cap gets more poisonous purely with time, as the capped dep ships new majors while the cap stays frozen.
Grammar is coarse but cross-ecosystem: Ruby pessimistic (~>), pip compatible (~=), semver caret/tilde (^ ~), exact (= == ===), and plain </<=. Precision is at the MAJOR level, which is all the signal needs ("N majors behind").
Constant Summary collapse
- POISON_KINDS =
The constraint kinds that make a dep a poison-pill candidate: an upper bound that blocks upgrades, or an exact pin that freezes the whole tree. A
:permissiveconstraint is neither. Shared with the workflow so the poison definition lives in one place. [:ceiling, :exact_pin].freeze
- EXACT_PIN =
An exact pin: = / == / === before a digit (NOT >= or <=).
/\A={1,3}\s*v?\d/- BARE_VERSION =
A bare version with no operator (npm/cargo exact form), e.g. "1.2.3", "v1".
/\Av?\d+(?:\.\d+)*\z/- CLAUSE =
Operator + version at the head of a clause.
/\A(===?|=|<=|<|~>|~=|\^|~)?\s*v?(\d+(?:\.\d+)*)/- AND_SEPARATOR =
Whitespace that separates npm AND clauses (">=1.2.0 <2.0.0"), i.e. a space before an operator. It won't split a Ruby clause like "~> 4.2" (the space there precedes a digit, not an operator). The quantifier is POSSESSIVE (
\s++, not\s+): a lookahead after a greedy\s+backtracks once per trailing space, which is quadratic on a long run of spaces (ReDoS on the lockfile-derived requirement string). Possessive matching never backtracks. /\s++(?=[<>=~^])/- MAX_REQUIREMENT_LENGTH =
A real declared requirement ("< 5.0, >= 4.0.1", "^1 || ^2 || ^3") is short. Anything past this is not a parseable constraint, so cap the input up front: it bounds every regex below to linear work and refuses to mint a pill from garbage (an over-long string reads as permissive, never a false ceiling).
256- SEVERITY =
Severity tiers for a compatibility ceiling, worst-last (mirrors StatusHelper::SEVERITY -- an ordinal set compared by index, never a numeric composite). :note is FYI, :warning is review-and-plan, :critical is act-now.
[:note, :warning, :critical].freeze
Instance Method Summary collapse
-
#analyze(requirement:, dep_latest:) ⇒ Object
=> { kind: :permissive|:ceiling|:exact_pin, majors_behind: Integer }.
-
#constraint_severity(finding) ⇒ Object
Tier one ceiling finding.
-
#poison_ceiling?(requirement:, dep_latest:) ⇒ Boolean
The poison condition: a below-latest ceiling or exact pin.
-
#poison_findings(deps) ⇒ Object
Build the poison-pill receipts for a package's declared runtime deps.
-
#reachable_within_cap?(finding, version) ⇒ Boolean
True when
versionsits at or below the highest major the cap allows -- i.e. -
#severity_at_or_above?(severity, threshold) ⇒ Boolean
For the --fail-if-poison gate: is
severityat or abovethreshold?. -
#top_findings(findings, limit: 3) ⇒ Object
Select the worst
limitfindings for a display receipt, plus the full count so a renderer can say "+N more" / "N total". -
#worst_severity(findings) ⇒ Object
The worst tier across a gem's CEILING findings (exact-pins are a milder hazard, not poison, and don't carry a tier), or nil when there are none.
Instance Method Details
#analyze(requirement:, dep_latest:) ⇒ Object
=> { kind: :permissive|:ceiling|:exact_pin, majors_behind: Integer }
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/helpers/constraint_helper.rb', line 47 def analyze(requirement:, dep_latest:) return { kind: :permissive, majors_behind: 0 } if requirement.to_s.length > MAX_REQUIREMENT_LENGTH # `||` is an OR-range (npm): satisfied by ANY branch, so an unbounded branch # lifts the cap entirely and the effective ceiling is the LOOSEST branch. # Reading only the first branch would invent a false pill on the common # `^2.0.0 || ^3.0.0` "supports several majors" form. branches = requirement.to_s.split("||").map { |branch| clauses_of(branch) } branches = [[]] if branches.empty? ceilings = branches.map { |clauses| branch_ceiling(clauses) } latest_major = major(dep_latest) return { kind: :permissive, majors_behind: 0 } if ceilings.any?(&:nil?) behind = latest_major ? [latest_major - ceilings.max, 0].max : 0 kind = branches.all? { |clauses| all_exact?(clauses) } ? :exact_pin : :ceiling { kind: kind, majors_behind: behind } end |
#constraint_severity(finding) ⇒ Object
Tier one ceiling finding. Magnitude-driven: the real-project dogfood showed Ruby findings are bimodal -- 1 major behind is trivial (a done gem pinning an old minor), 3+ is a genuine upgrade wall (e.g. capping actionmailer below Rails 6). Popularity was rejected as an input (it ranks the framework blocker below a utility) and a vulnerable-gem escalator was rejected (that is the vulnerability finding's job, not the cap's).
77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/helpers/constraint_helper.rb', line 77 def constraint_severity(finding) # A runtime ceiling that strands you on an end-of-life Ruby is act-now: no # patched runtime is reachable. This short-circuit lets the language-ceiling # signal (which carries no majors_behind) reuse the same tierer as poison. return :critical if finding[:eol_forced] case finding[:majors_behind] when 2 then :warning when 3.. then :critical else :note end end |
#poison_ceiling?(requirement:, dep_latest:) ⇒ Boolean
The poison condition: a below-latest ceiling or exact pin. A permissive constraint, or a cap at/above the dep's latest major, is not a pill.
129 130 131 132 |
# File 'lib/helpers/constraint_helper.rb', line 129 def poison_ceiling?(requirement:, dep_latest:) result = analyze(requirement: requirement, dep_latest: dep_latest) POISON_KINDS.include?(result[:kind]) && result[:majors_behind].positive? end |
#poison_findings(deps) ⇒ Object
Build the poison-pill receipts for a package's declared runtime deps. Each
dep is { package_name:, requirements: }; the block resolves a dep name to
its current latest version string (or nil when unresolvable, which drops the
dep rather than guessing). Returns only the below-latest ceilings and exact
pins, each as a self-contained receipt. Shared by the native Bundler path and
the cross-ecosystem lens, which differ only in how they resolve dep_latest.
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/helpers/constraint_helper.rb', line 140 def poison_findings(deps) deps.filter_map do |dep| dep_latest = yield(dep[:package_name]) next if dep_latest.nil? result = analyze(requirement: dep[:requirements], dep_latest: dep_latest) next unless POISON_KINDS.include?(result[:kind]) && result[:majors_behind].positive? { dependency: dep[:package_name], requirement: dep[:requirements], dep_latest: dep_latest, majors_behind: result[:majors_behind], kind: result[:kind], } end end |
#reachable_within_cap?(finding, version) ⇒ Boolean
True when version sits at or below the highest major the cap allows -- i.e.
you could upgrade the capped dep to it WITHOUT breaking the cap. The ceiling
major is recovered from the poison finding itself (dep_latest minus
majors_behind), at the same major precision the poison signal already uses, so
a fix that lands OUTSIDE the cap ("below the fix") is detected without
re-parsing the raw requirement and its pre/dev-release quirks (< 5.0.0dev).
Precision is MAJOR-level by design, and that errs deliberately toward reachable:
a within-major cap (~> 4.2.1, < 5.5) reads a same-major fix (4.9.0, 5.29.6)
as reachable even though the cap forbids it. So a genuinely-stuck within-major
cap is UNDER-reported (downgraded to the still-visible "vulnerable" tier), never
falsely promoted -- when this DOES return false for every fix, the below-the-fix
claim is sound (every fix is in a strictly higher major than the cap allows).
Unparseable/absent inputs read false (never claims reachable when we can't tell).
118 119 120 121 122 123 124 125 |
# File 'lib/helpers/constraint_helper.rb', line 118 def reachable_within_cap?(finding, version) fix_major = major(version) latest_major = major(finding[:dep_latest]) behind = finding[:majors_behind] return false if fix_major.nil? || latest_major.nil? || behind.nil? fix_major <= latest_major - behind end |
#severity_at_or_above?(severity, threshold) ⇒ Boolean
For the --fail-if-poison gate: is severity at or above threshold?
98 99 100 101 102 |
# File 'lib/helpers/constraint_helper.rb', line 98 def severity_at_or_above?(severity, threshold) return false if severity.nil? SEVERITY.index(severity) >= SEVERITY.index(threshold) end |
#top_findings(findings, limit: 3) ⇒ Object
Select the worst limit findings for a display receipt, plus the full count
so a renderer can say "+N more" / "N total". Worst-first by majors_behind,
ties broken by dependency name so the output is stable and diffable. Shared
by the terminal and markdown renderers so the selection can't drift.
162 163 164 165 |
# File 'lib/helpers/constraint_helper.rb', line 162 def top_findings(findings, limit: 3) ranked = findings.sort_by { |f| [-f[:majors_behind], f[:dependency].to_s] } { shown: ranked.first(limit), total: findings.length } end |
#worst_severity(findings) ⇒ Object
The worst tier across a gem's CEILING findings (exact-pins are a milder hazard, not poison, and don't carry a tier), or nil when there are none.
92 93 94 95 |
# File 'lib/helpers/constraint_helper.rb', line 92 def worst_severity(findings) tiers = findings.filter_map { constraint_severity(_1) if _1[:kind] == :ceiling } tiers.max_by { SEVERITY.index(_1) } end |