80 lines
2.5 KiB
Ruby
80 lines
2.5 KiB
Ruby
# ©AngelaMos | 2026
|
|
# audit_gem.rb
|
|
# frozen_string_literal: true
|
|
|
|
require "rubygems/package"
|
|
require "digest"
|
|
require "fileutils"
|
|
|
|
GEM_PATH = ARGV.fetch(0)
|
|
TREE = ARGV.fetch(1)
|
|
EXPECTED_FLOOR = ARGV.fetch(2)
|
|
EXTRACT_ROOT = ARGV.fetch(3)
|
|
|
|
DOC_FILES = %w[README.md LICENSE].freeze
|
|
LIB_PREFIX = "lib/"
|
|
|
|
FORBIDDEN = {
|
|
"target_absent" => %r{\Atarget/},
|
|
"tests_absent" => %r{\Atest/},
|
|
"scripts_absent" => %r{\Ascripts/},
|
|
"dev_docs_absent" => %r{\Adocs/|\AAGENTS\.md\z},
|
|
"build_tooling_absent" => /\A(justfile|Gemfile|Gemfile\.lock|Rakefile|\.rubocop\.yml|\.gitignore)\z/,
|
|
"container_files_absent" => /Dockerfile|compose|config\.ru/i,
|
|
"lab_artifacts_absent" => %r{\.gem\z|\.marshal\z|\.bin\z|\Apayloads/|\Aloot/|canary}i
|
|
}.freeze
|
|
|
|
def tree_lib_files
|
|
Dir.chdir(TREE) { Dir.glob("#{LIB_PREFIX}**/*.rb").sort }
|
|
end
|
|
|
|
def emit(key, value)
|
|
puts "#{key}=#{value}"
|
|
end
|
|
|
|
def info(label, value)
|
|
puts "INFO #{label}: #{value}"
|
|
end
|
|
|
|
package = Gem::Package.new(GEM_PATH)
|
|
spec = package.spec
|
|
contents = package.contents.sort
|
|
expected = (DOC_FILES + tree_lib_files).sort
|
|
shipped_lib = contents.select { |path| path.start_with?(LIB_PREFIX) }
|
|
|
|
FileUtils.rm_rf(EXTRACT_ROOT)
|
|
FileUtils.mkdir_p(EXTRACT_ROOT)
|
|
package.extract_files(EXTRACT_ROOT)
|
|
|
|
missing = expected - contents
|
|
unexpected = contents - expected
|
|
|
|
drifted = shipped_lib.reject do |path|
|
|
source = File.join(TREE, path)
|
|
next false unless File.file?(source)
|
|
|
|
Digest::SHA256.file(File.join(EXTRACT_ROOT, path)).hexdigest ==
|
|
Digest::SHA256.file(source).hexdigest
|
|
end
|
|
|
|
orphaned = shipped_lib.reject { |path| File.file?(File.join(TREE, path)) }
|
|
|
|
info "gem", File.basename(GEM_PATH)
|
|
info "declares", "#{spec.name} #{spec.version} ruby #{spec.required_ruby_version}"
|
|
info "shipped", "#{contents.length} files, #{shipped_lib.length} under lib/"
|
|
info "worktree", "#{tree_lib_files.length} files under lib/"
|
|
info "missing", missing.empty? ? "none" : missing.join(", ")
|
|
info "unexpected", unexpected.empty? ? "none" : unexpected.join(", ")
|
|
info "drifted", drifted.empty? ? "none" : drifted.join(", ")
|
|
info "orphaned", orphaned.empty? ? "none" : orphaned.join(", ")
|
|
|
|
emit "every_declared_file_shipped", missing.empty?
|
|
emit "nothing_undeclared_shipped", unexpected.empty?
|
|
emit "lib_is_non_empty", shipped_lib.length.positive?
|
|
emit "lib_matches_worktree", drifted.empty? && orphaned.empty?
|
|
emit "floor_is_declared", spec.required_ruby_version.to_s == EXPECTED_FLOOR
|
|
|
|
FORBIDDEN.each do |key, pattern|
|
|
emit(key, contents.none? { |path| pattern.match?(path) })
|
|
end
|