The Stdlib Census
The Stdlib Census: "it's in the standard library" is a statement with a shelf life. Default gems become bundled gems on a published schedule, and every `require` your gem makes is either covered by ruby itself, covered by your gemspec, or a warning that hasn't fired yet. This census reads lib/'s requires, cross-checks the gemspec, and flags everything the next Ruby will bill you for.
Data & Pipelines
Round 13
Hiroshi Shibata
exit 0
bundle exec ruby examples/stdlib_census.rb
a real captured run
THE STDLIB CENSUS (27 distinct requires across lib/)
zeitwerk DECLARED gemspec: zeitwerk
tty-table DECLARED gemspec: tty-table
tty-spinner DECLARED gemspec: tty-spinner
tty-progressbar DECLARED gemspec: tty-progressbar
tty-cursor DECLARED gemspec: tty-cursor
tty-box DECLARED gemspec: tty-box
thor DECLARED gemspec: thor
pastel DECLARED gemspec: pastel
ostruct DECLARED gemspec: ostruct
openai DECLARED gemspec: ruby-openai
async DECLARED gemspec: async
logger DECLARED gemspec: logger
async/barrier DECLARED gemspec: async
async/semaphore DECLARED gemspec: async
cgi DECLARED gemspec: cgi
dry/schema DECLARED gemspec: dry-schema
yaml CORE default gem, no promotion scheduled
uri CORE default gem, no promotion scheduled
date CORE default gem, no promotion scheduled
digest CORE default gem, no promotion scheduled
time CORE default gem, no promotion scheduled
singleton CORE default gem, no promotion scheduled
set CORE default gem, no promotion scheduled
securerandom CORE default gem, no promotion scheduled
fileutils CORE default gem, no promotion scheduled
json CORE default gem, no promotion scheduled
net/http CORE default gem, no promotion scheduled
the receipts: ostruct was declared during the 3.4 warning wave,
and this census's own first run caught TWO more - logger
(promoted to bundled in 3.5) and cgi (trimmed to a bundled gem
in 3.5, used here for CGI.escape) - both now declared in the
gemspec with comments saying why. the round-11 'time' bug was
the same lesson at file scope: require what you use; a
transitive require is a loan, and rubies refinance.
release engineering isn't glamorous: it's reading the NEWS file
of every ruby release AS IF your gem's install matrix depends
on it, because it does. this census is 60 lines; run it in CI
and the 3.5 upgrade becomes a non-event instead of an issue
tracker full of LoadErrors.
source
# frozen_string_literal: true # The Stdlib Census: "it's in the standard library" is a statement # with a shelf life. Default gems become bundled gems on a published # schedule, and every `require` your gem makes is either covered by # ruby itself, covered by your gemspec, or a warning that hasn't # fired yet. This census reads lib/'s requires, cross-checks the # gemspec, and flags everything the next Ruby will bill you for. # # bundle exec ruby examples/stdlib_census.rb # # Runs offline; exits 1 if any require is covered by nobody. require class="s">"bundler/setup" # The census reads the agentic SOURCE - resolve the installed gem. # Dependencies are asked of RubyGems as DATA, not regexed from the # gemspec text: bundler NORMALIZES gemspecs in git checkouts (the # authored `add_dependency "x"` becomes a serialized # `add_runtime_dependency(%q<x>...)`), so text-scanning "the # gemspec" gives different answers for path, git, and packaged # installs of the very same commit. The spec object tells one truth. AGENTIC_SPEC = Gem:class="y">:Specification.find_by_name(class="s">"agentic") LIB = File.join(AGENTIC_SPEC.gem_dir, class="s">"lib") # require name -> gem name, where they differ GEM_FOR = { class="s">"dry/schema" => class="s">"dry-schema", class="s">"openai" => class="s">"ruby-openai", class="s">"async/semaphore" => class="s">"async", class="s">"async" => class="s">"async" }.freeze # Default gems already promoted to bundled (3.4 wave) or announced # for promotion (3.5 wave) - require them without declaring them and # a future ruby upgrade breaks your users' bundle GEMIFIED = %w[ ostruct pstore benchmark logger rdoc fiddle irb reline win32ole csv drb mutex_m base64 bigdecimal getoptlong observer rinda resolv-replace syslog abbrev nkf ].freeze # Genuinely-safe stdlib for the foreseeable schedule CORE_SAFE = %w[ json fileutils time date yaml securerandom set singleton net/http uri open3 stringio tmpdir digest erb forwardable ].freeze requires = Dir[File.join(LIB, class="s">"**/*.rb")].flat_map { |file| File.readlines(file, encoding: class="s">"UTF-8").filter_map { |line| name = line[/\Arequire class="s">"([^"]+)"/, 1] [name, File.basename(file)] if name } }.group_by(&class="y">:first).transform_values { |rows| rows.map(&class="y">:last).uniq } declared = AGENTIC_SPEC.runtime_dependencies.map(&class="y">:name) verdicts = requires.keys.sort.map do |name| gem_name = GEM_FOR[name] || name.split(class="s">"/").first verdict = if declared.include?(gem_name) || declared.any? { |d| gem_name.start_with?(d) } [class="y">:declared, class="s">"gemspec: #{gem_name}"] elsif GEMIFIED.include?(name) declared.include?(name) ? [class="y">:declared, class="s">"gemspec: #{name}"] : [class="y">:gemified, class="s">"PROMOTED to bundled gem - declare it or a ruby upgrade breaks the bundle"] elsif CORE_SAFE.include?(name) [class="y">:core, class="s">"default gem, no promotion scheduled"] else [class="y">:uncovered, class="s">"COVERED BY NOBODY - works today by accident"] end [name, verdict] end puts class="s">"THE STDLIB CENSUS (#{requires.size} distinct requires across lib/)" puts order = {uncovered: 0, gemified: 1, declared: 2, core: 3} verdicts.sort_by { |_, (kind, _)| order[kind] }.each do |name, (kind, note)| marker = {uncovered: class="s">"!!", gemified: class="s">" !", declared: class="s">" ", core: class="s">" "}[kind] puts format(class="s">" %s %-18s %-10s %s", marker, name, kind.to_s.upcase, note) end uncovered = verdicts.count { |_, (kind, _)| kind == class="y">:uncovered } gemified = verdicts.select { |_, (kind, _)| kind == class="y">:gemified }.map(&class="y">:first) puts puts class="s">" the receipts: ostruct was declared during the 3.4 warning wave," puts class="s">" and this census's own first run caught TWO more - logger" puts class="s">" (promoted to bundled in 3.5) and cgi (trimmed to a bundled gem" puts class="s">" in 3.5, used here for CGI.escape) - both now declared in the" puts class="s">" gemspec with comments saying why. the round-11 'time' bug was" puts class="s">" the same lesson at file scope: require what you use; a" puts class="s">" transitive require is a loan, and rubies refinance." if gemified.any? puts class="s">" still to declare before the next ruby: #{gemified.join(", class="s">")}." end puts class="s">" release engineering isn't glamorous: it's reading the NEWS file" puts class="s">" of every ruby release AS IF your gem's install matrix depends" puts class="s">" on it, because it does. this census is 60 lines; run it in CI" puts class="s">" and the 3.5 upgrade becomes a non-event instead of an issue" puts class="s">" tracker full of LoadErrors." exit(uncovered.zero? ? 0 : 1)