Walks the live class graph and classifies auto-invoked methods along the
gated/ungated dispatch axis. Marshal calls respond_to? before invoking
marshal_load and _load, while hash, eql?, <=>, []= and to_s are dispatched
blind, so a class can be dead as a Marshal entry point and live as a #hash
entry point.
Rediscovers Gem::Requirement#marshal_load, Gem::Version#marshal_load and
Gem::Specification._load with source locations, from reflection alone, with no
class names hardcoded anywhere in the scanner.
A new control asserts the M1 parser and the M3 scanner agree. The parser reads
bytes off a payload, the scanner walks the class graph, neither consults the
other, and every sink the parser finds in a real Gem::Requirement payload is
one the scanner independently located. Two routes to the same fact.
Raw output was unusable at 163 ungated candidates dominated by to_s and hash,
which nearly every class defines. The reachability predicate from the research
narrows that to methods that are zero-arity AND touch instance state, cutting
163 to 12 and keeping 7.4 percent. Gated sinks are always reachable.
Two corrections found while building the predicate:
RubyVM::AbstractSyntaxTree.of does not work on Ruby 4.0. It raises
'cannot get AST for ISEQ compiled by prism' because prism is now the default
parser. Any tool reaching for that API on modern Ruby is broken. Replaced with
Prism, which ships as a default gem.
The literal reads-its-own-ivars predicate is too narrow. Gem::Requirement#hash
calls the requirements attr_reader rather than @requirements, so an instance
variable check reports false on a method that plainly operates on instance
state. Widened to instance variable reads OR implicit-self calls.
55 tests, 112 assertions, 0 failures across both suites.