ruby-lean 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4553b3d3a834471cf5bfbda717aa0931d752c5d9d198b002c435c7078341d644
4
+ data.tar.gz: bcd4971c8b74cdfb009f36c373813bfeb4b2a0a1658cb3f681ef713005b882b3
5
+ SHA512:
6
+ metadata.gz: eca97ad8086fe7fb52844200a0f32a44e0d5fd9629d8722c5a58df8edf43a10ca2b8febca0b00c58f9ce931fb084f746e9ebb5d9655d9fca91250cb8e01feaf1
7
+ data.tar.gz: 25331d162f42aa60e36dd068fea8a4c70af392bfd19f041d82173af1e14a2f62d0f2079e14d1fbfa5b2bdd9d13e100ce9923ee101e10e023ab99c84b51a29781
data/CHANGELOG.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-11-13
11
+
12
+ ### Added
13
+
14
+ - __`lint` command__: Checks whether all files declared in `.gemspec` and included in `.gem` packages comply with leanness and packaging rules.
15
+ - __Dual-layer inspection__: Analyzes effective gemspec declarations (`Gem::Specification#files`) and verifies built `.gem` archive contents in isolated temporary storage.
16
+ - __Lint rules__: Comprehensive detection of missing files, invalid paths, test suites, CI metadata, coverage reports, editor settings, development build tools, VCS files, and dynamic `git ls-files` discovery.
17
+ - __`--strict` flag__: Enables failing exit code on advisory warnings for CI pipelines.
18
+ - __`--no-archive` flag__: Allows specification-only inspection without building `.gem` archives.
19
+ - __`--format json` option__: Structured, deterministic JSON output for scripting and CI integration.
20
+ - __`--gemspec` option__: Explicitly specify `.gemspec` file path.
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 - ∞ Raphael Stolt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # ruby-lean
2
+
3
+ Validate and inspect RubyGem package contents to keep your published gems lean.
4
+
5
+ ## Why?
6
+
7
+ When you publish a Ruby gem to RubyGems.org, RubyGems packages all files resolved by `Gem::Specification#files` in your `.gemspec`. In many gems, files are selected using loose directory globs (`Dir["**/*"]`) or broad `git ls-files` commands. That means your published gem can unintentionally include development artefacts like IDE settings, test suites, build output, documentation drafts, and CI configuration. These files bloat the gem download size, slow down `gem install` and `bundle install`, and expose internal project structure to downstream users.
8
+
9
+ `ruby-lean` audits your `.gemspec` file list and verifies built `.gem` archives so only the source code and assets your users need end up in the published gem.
10
+
11
+ ## Installation
12
+
13
+ ### From RubyGems
14
+
15
+ ```bash
16
+ gem install ruby-lean
17
+ ```
18
+
19
+ ### From source
20
+
21
+ ```bash
22
+ git clone git@github.com:lean-packages/ruby-lean.git && cd ruby-lean
23
+ rake build
24
+ gem install ./ruby-lean-*.gem
25
+ ```
26
+
27
+ This places the `ruby-lean` binary in your Ruby environment's executable directory, which should be on your PATH.
28
+
29
+ ## Usage
30
+
31
+ ```
32
+ ruby-lean <COMMAND>
33
+ ```
34
+
35
+ ### Commands
36
+
37
+ | Command | Description |
38
+ |---|---|
39
+ | `lint` | Check if any unnecessary or invalid files are included in the gem. |
40
+
41
+ ### `lint` — Check package contents
42
+
43
+ Verifies that all files declared in `.gemspec` and packaged in `.gem` meet leanness and validity policies.
44
+
45
+ ```bash
46
+ # Check the current directory
47
+ ruby-lean lint
48
+
49
+ # Check a specific project directory
50
+ ruby-lean lint /path/to/my-gem
51
+
52
+ # Check a specific gemspec file
53
+ ruby-lean lint --gemspec /path/to/my-gem.gemspec
54
+
55
+ # Fail on advisory warnings (recommended for CI)
56
+ ruby-lean lint --strict
57
+
58
+ # Inspect gemspec file list only without building an archive
59
+ ruby-lean lint --no-archive
60
+
61
+ # JSON output for scripting
62
+ ruby-lean lint --format json
63
+ ```
64
+
65
+ __`--strict` flag:__ Treats advisory warnings (such as development test files, CI metadata, and build tools) as failures, exiting with code `1`.
66
+
67
+ __`--no-archive` flag:__ Skips building a temporary `.gem` archive and inspects only the effective gemspec file list on disk.
68
+
69
+ ## Options and flags
70
+
71
+ All commands support these options and flags:
72
+
73
+ | Option / Flag | Description |
74
+ |---|---|
75
+ | `--gemspec <PATH>` | Path to explicit `.gemspec` file |
76
+ | `-f, --format <FORMAT>` | Output format: `text` or `json` (default: `text`) |
77
+ | `-s, --strict` | Treat advisory warnings as errors |
78
+ | `--no-archive` | Skip building and verifying temporary `.gem` archive |
79
+ | `-q, --quiet` | Suppress non-essential informational output |
80
+ | `-v, --version` | Print version information |
81
+ | `-h, --help` | Show help message |
82
+
83
+ ## Lint rules
84
+
85
+ `ruby-lean` evaluates package contents against high-signal rules:
86
+
87
+ ### Errors (Always fail policy)
88
+
89
+ | Rule ID | Description |
90
+ |---|---|
91
+ | `RUBYLEAN_INVALID_SPEC` | Gemspec syntax error, unloadable specification, or validation failure. |
92
+ | `RUBYLEAN_MISSING_FILE` | Declared file does not exist on disk. |
93
+ | `RUBYLEAN_INVALID_PATH` | Absolute path, directory entry, or path escaping the gem root (`../`). |
94
+ | `RUBYLEAN_BUILD_ERROR` | RubyGems archive packaging failure. |
95
+ | `RUBYLEAN_ARCHIVE_MISMATCH` | Discrepancy between gemspec declared files and actual `.gem` archive contents. |
96
+
97
+ ### Advisory warnings (Fail with `--strict`)
98
+
99
+ | Rule ID | Description |
100
+ |---|---|
101
+ | `RUBYLEAN_DEV_FILE` | Test or specification files (`spec/`, `test/`, `features/`) included in gem. |
102
+ | `RUBYLEAN_CI_METADATA` | CI configuration files (`.github/`, `.gitlab/`, `.circleci/`, etc.) included. |
103
+ | `RUBYLEAN_COVERAGE` | Test coverage artifacts or configs (`coverage/`, `.simplecov`) included. |
104
+ | `RUBYLEAN_EDITOR_CONFIG` | Editor or IDE configuration files (`.vscode/`, `.idea/`, etc.) included. |
105
+ | `RUBYLEAN_BUILD_TOOL` | Development build tools (`Gemfile`, `Gemfile.lock`, `Rakefile`, `.rubocop.yml`) included. |
106
+ | `RUBYLEAN_VCS_METADATA` | VCS metadata (`.gitignore`, `.gitattributes`) included. |
107
+ | `RUBYLEAN_GIT_DEPENDENT` | Gemspec source relies on `git ls-files` for dynamic file discovery (advisory). |
108
+ | `RUBYLEAN_SYMLINK` | Packaged file is a symbolic link. |
109
+
110
+ *Note:* Standard documentation and runtime assets (`README*`, `LICENSE*`, `CHANGELOG*`, `CONTRIBUTING*`, `docs/`, `lib/`, `exe/`) are explicitly preserved and never flagged.
111
+
112
+ ## Exit codes
113
+
114
+ | Exit Code | Status | Description |
115
+ |---|---|---|
116
+ | `0` | `SUCCESS` | Gem passed policy (0 errors; warnings allowed unless `--strict`). |
117
+ | `1` | `LINT_FAILED` | Lint policy failed (errors found, or warnings found with `--strict`). |
118
+ | `2` | `EXECUTION_ERROR` | Usage error, invalid options, missing gemspec, or gemspec evaluation failure. |
119
+
120
+ ## CI integration
121
+
122
+ Add `ruby-lean lint` to your CI pipeline to enforce that gem packages stay clean:
123
+
124
+ ```yaml
125
+ # Example GitHub Actions step
126
+ - name: Check RubyGem package contents
127
+ run: ruby-lean lint --strict
128
+ ```
129
+
130
+ ## JSON output
131
+
132
+ All commands support `--format json` for scripting and automation. Example `lint` output:
133
+
134
+ ```json
135
+ {
136
+ "schemaVersion": "1",
137
+ "tool": {
138
+ "name": "ruby-lean",
139
+ "version": "0.1.0"
140
+ },
141
+ "package": {
142
+ "name": "example-gem",
143
+ "version": "1.0.0",
144
+ "gemspec": "example-gem.gemspec"
145
+ },
146
+ "inspection": {
147
+ "mode": "archive",
148
+ "archiveVerified": true,
149
+ "fileCount": 4,
150
+ "totalBytes": 1420
151
+ },
152
+ "summary": {
153
+ "errors": 0,
154
+ "warnings": 1,
155
+ "info": 0
156
+ },
157
+ "findings": [
158
+ {
159
+ "rule": "RUBYLEAN_DEV_FILE",
160
+ "severity": "warning",
161
+ "message": "Development-oriented test or specification files are included in the package.",
162
+ "paths": [
163
+ "spec/example_spec.rb"
164
+ ],
165
+ "suggestedAction": "Filter out test directories (spec/, test/, features/) from spec.files in your gemspec."
166
+ }
167
+ ]
168
+ }
169
+ ```
170
+
171
+ ## Security considerations
172
+
173
+ A `.gemspec` file contains executable Ruby code. Evaluating a gemspec via RubyGems executes Ruby instructions. `ruby-lean` is intended for **trusted local gem repositories** and maintainers inspecting their own code. Evaluating untrusted gemspecs is not sandboxed.
174
+
175
+ ## AI disclaimer
176
+
177
+ This package is developed with the assistance of Artificial Intelligence (AI). AI tools are used throughout the development
178
+ process, including code generation, documentation, testing, and maintenance.
179
+
180
+ While reasonable care is taken to review and validate AI-generated contributions, AI-generated code is not inherently correct,
181
+ secure, or free of defects. All contributions remain subject to human review and project quality standards.
182
+
183
+ As with any software, use this package at your own risk. No guarantee is made regarding its fitness for a particular purpose,
184
+ and the maintainers assume no liability for damages resulting from its use.
185
+
186
+ ## Development
187
+
188
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development instructions.
189
+
190
+ ## License
191
+
192
+ This project is licensed under the MIT license. Please see [LICENSE.md](LICENSE.md) for more details.
193
+
194
+ ## Related projects
195
+
196
+ - [cargo-lean](https://github.com/lean-packages/cargo-lean) — The `Rust` counterpart.
197
+ - [php-lean](https://github.com/lean-packages/php-lean) — The `PHP` counterpart.
198
+ - [py-lean](https://github.com/lean-packages/py-lean) — The `Python` equivalent.
199
+ - [js-lean](https://github.com/lean-packages/js-lean) — The `JavaScript` equivalent.
data/exe/ruby-lean ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/ruby_lean"
5
+
6
+ exit(RubyLean::CLI.start(ARGV))
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "rubygems/package"
5
+ require "tmpdir"
6
+ require "open3"
7
+ require "zlib"
8
+
9
+ module RubyLean
10
+ class ArchiveInspector
11
+ ArchiveData = Struct.new(:archive_files, :file_count, :total_bytes, :largest_files, :build_warnings, keyword_init: true)
12
+
13
+ def self.inspect_package(package)
14
+ new.inspect_package(package)
15
+ end
16
+
17
+ def inspect_package(package)
18
+ Dir.mktmpdir("ruby-lean-") do |tmpdir|
19
+ output_gem = File.join(tmpdir, "#{package.name}-#{package.version}.gem")
20
+ gemspec_filename = File.basename(package.gemspec_path)
21
+
22
+ cmd = ["gem", "build", gemspec_filename, "--output", output_gem]
23
+ stdout, stderr, status = Open3.capture3(*cmd, chdir: package.gem_root)
24
+
25
+ unless status.success? && File.exist?(output_gem)
26
+ error_msg = stderr.to_s.strip
27
+ error_msg = stdout.to_s.strip if error_msg.empty?
28
+ error_msg = "Unknown gem build error" if error_msg.empty?
29
+ raise BuildError, error_msg
30
+ end
31
+
32
+ parse_archive(output_gem, stderr)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def parse_archive(gem_path, stderr)
39
+ archive_files = []
40
+ file_entries = []
41
+ total_bytes = 0
42
+
43
+ File.open(gem_path, "rb") do |io|
44
+ Gem::Package::TarReader.new(io) do |reader|
45
+ reader.each do |entry|
46
+ next unless entry.full_name == "data.tar.gz"
47
+
48
+ gz = Zlib::GzipReader.new(entry)
49
+ Gem::Package::TarReader.new(gz) do |data_reader|
50
+ data_reader.each do |data_entry|
51
+ next unless data_entry.file?
52
+
53
+ fname = data_entry.full_name
54
+ fsize = data_entry.header.size
55
+
56
+ archive_files << fname
57
+ file_entries << { "path" => fname, "bytes" => fsize }
58
+ total_bytes += fsize
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ largest_files = file_entries.sort_by { |e| -e["bytes"] }.first(5)
66
+
67
+ ArchiveData.new(
68
+ archive_files: archive_files.sort,
69
+ file_count: archive_files.size,
70
+ total_bytes: total_bytes,
71
+ largest_files: largest_files,
72
+ build_warnings: stderr.to_s.strip
73
+ )
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module RubyLean
6
+ class CLI
7
+ def self.start(args = ARGV)
8
+ new.start(args)
9
+ end
10
+
11
+ def start(args = ARGV)
12
+ options = {
13
+ format: "text",
14
+ strict: false,
15
+ archive: true,
16
+ quiet: false
17
+ }
18
+
19
+ args = args.dup
20
+
21
+ # Check top-level flags first
22
+ if args.include?("-v") || args.include?("--version")
23
+ puts "ruby-lean #{RubyLean::VERSION}"
24
+ return 0
25
+ end
26
+
27
+ # Extract command if provided
28
+ command = "lint"
29
+ if args.first == "lint"
30
+ args.shift
31
+ elsif args.first && !args.first.start_with?("-") && File.exist?(args.first)
32
+ # First arg is a path
33
+ options[:path] = args.shift
34
+ end
35
+
36
+ parser = OptionParser.new do |opts|
37
+ opts.banner = "Usage: ruby-lean lint [PATH] [options]"
38
+ opts.separator ""
39
+ opts.separator "Options:"
40
+
41
+ opts.on("--gemspec PATH", "Path to explicit .gemspec file") do |path|
42
+ options[:gemspec] = path
43
+ end
44
+
45
+ opts.on("-f", "--format FORMAT", %w[text json], "Output format (text, json; default: text)") do |fmt|
46
+ options[:format] = fmt
47
+ end
48
+
49
+ opts.on("-s", "--strict", "Fail on warnings as well as errors") do
50
+ options[:strict] = true
51
+ end
52
+
53
+ opts.on("--no-archive", "Inspect gemspec file list only without building archive") do
54
+ options[:archive] = false
55
+ end
56
+
57
+ opts.on("-q", "--quiet", "Suppress non-essential informational output") do
58
+ options[:quiet] = true
59
+ end
60
+
61
+ opts.on_tail("-v", "--version", "Print version and exit") do
62
+ puts "ruby-lean #{RubyLean::VERSION}"
63
+ return 0
64
+ end
65
+
66
+ opts.on_tail("-h", "--help", "Show this help message and exit") do
67
+ puts opts
68
+ return 0
69
+ end
70
+ end
71
+
72
+ begin
73
+ remaining = parser.parse!(args)
74
+ options[:path] ||= remaining.first || "."
75
+ rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e
76
+ warn "Error: #{e.message}"
77
+ warn "Run 'ruby-lean --help' for usage instructions."
78
+ return 2
79
+ end
80
+
81
+ begin
82
+ Command::Lint.run(options)
83
+ rescue ExecutionError => e
84
+ warn "Error: #{e.message}"
85
+ 2
86
+ rescue StandardError => e
87
+ warn "Unexpected error: #{e.message}"
88
+ warn e.backtrace.join("\n") if $DEBUG
89
+ 2
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLean
4
+ module Command
5
+ class Lint
6
+ def self.run(options = {})
7
+ new(options).run
8
+ end
9
+
10
+ def initialize(options = {})
11
+ @options = options
12
+ @target_path = options[:path] || "."
13
+ @explicit_gemspec = options[:gemspec]
14
+ @format = (options[:format] || "text").to_s.downcase
15
+ @strict = !!options[:strict]
16
+ @archive = options.fetch(:archive, true)
17
+ @quiet = !!options[:quiet]
18
+ end
19
+
20
+ def run
21
+ package = GemspecLoader.discover_and_load(@target_path, explicit_gemspec: @explicit_gemspec)
22
+
23
+ result = if @archive
24
+ inspect_with_archive(package)
25
+ else
26
+ inspect_specification_only(package)
27
+ end
28
+
29
+ output = render_result(result)
30
+
31
+ if @format == "json"
32
+ puts output
33
+ else
34
+ puts output unless output.empty?
35
+ end
36
+
37
+ result.exit_code(strict: @strict)
38
+ end
39
+
40
+ private
41
+
42
+ def inspect_with_archive(package)
43
+ archive_data = nil
44
+ build_error_finding = nil
45
+
46
+ begin
47
+ archive_data = ArchiveInspector.inspect_package(package)
48
+ rescue BuildError => e
49
+ build_error_finding = Model::Finding.new(
50
+ rule: "RUBYLEAN_BUILD_ERROR",
51
+ severity: "error",
52
+ message: "Failed to build .gem archive: #{e.message}",
53
+ paths: [package.relative_gemspec_path],
54
+ suggested_action: "Fix gemspec errors or build issues reported by RubyGems."
55
+ )
56
+ end
57
+
58
+ if archive_data
59
+ findings = Rules::Engine.audit(package, archive_files: archive_data.archive_files)
60
+ Model::Result.new(
61
+ package: package,
62
+ inspection_mode: "archive",
63
+ archive_verified: true,
64
+ file_count: archive_data.file_count,
65
+ total_bytes: archive_data.total_bytes,
66
+ largest_files: archive_data.largest_files,
67
+ archive_files: archive_data.archive_files,
68
+ findings: findings
69
+ )
70
+ else
71
+ # Fallback to spec rules plus build error finding
72
+ findings = Rules::Engine.audit(package, archive_files: nil)
73
+ findings << build_error_finding if build_error_finding
74
+
75
+ Model::Result.new(
76
+ package: package,
77
+ inspection_mode: "archive",
78
+ archive_verified: false,
79
+ file_count: package.effective_files.size,
80
+ total_bytes: calculate_disk_bytes(package),
81
+ findings: findings
82
+ )
83
+ end
84
+ end
85
+
86
+ def inspect_specification_only(package)
87
+ findings = Rules::Engine.audit(package, archive_files: nil)
88
+
89
+ Model::Result.new(
90
+ package: package,
91
+ inspection_mode: "specification",
92
+ archive_verified: false,
93
+ file_count: package.effective_files.size,
94
+ total_bytes: calculate_disk_bytes(package),
95
+ findings: findings
96
+ )
97
+ end
98
+
99
+ def calculate_disk_bytes(package)
100
+ total = 0
101
+ package.effective_files.each do |rel_path|
102
+ full_path = File.join(package.gem_root, rel_path)
103
+ total += File.size(full_path) if File.file?(full_path)
104
+ end
105
+ total
106
+ rescue StandardError
107
+ nil
108
+ end
109
+
110
+ def render_result(result)
111
+ case @format
112
+ when "json"
113
+ Reporters::JsonReporter.render(result)
114
+ else
115
+ Reporters::TextReporter.render(result, strict: @strict, quiet: @quiet)
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "pathname"
5
+
6
+ module RubyLean
7
+ class GemspecLoader
8
+ def self.discover_and_load(target_path = ".", explicit_gemspec: nil)
9
+ new.discover_and_load(target_path, explicit_gemspec: explicit_gemspec)
10
+ end
11
+
12
+ def discover_and_load(target_path = ".", explicit_gemspec: nil)
13
+ gemspec_path = resolve_gemspec_path(target_path, explicit_gemspec)
14
+ load_gemspec(gemspec_path)
15
+ end
16
+
17
+ def resolve_gemspec_path(target_path = ".", explicit_gemspec = nil)
18
+ if explicit_gemspec
19
+ path = File.expand_path(explicit_gemspec)
20
+ unless File.exist?(path) && File.file?(path)
21
+ raise ExecutionError, "Specified gemspec does not exist: #{explicit_gemspec}"
22
+ end
23
+
24
+ return path
25
+ end
26
+
27
+ target = File.expand_path(target_path || ".")
28
+
29
+ if File.file?(target) && target.end_with?(".gemspec")
30
+ return target
31
+ end
32
+
33
+ unless File.directory?(target)
34
+ raise ExecutionError, "Target path is not a directory or gemspec: #{target_path}"
35
+ end
36
+
37
+ matches = Dir.glob(File.join(target, "*.gemspec"))
38
+ if matches.empty?
39
+ raise ExecutionError, "No .gemspec file found in #{target_path}. Specify one with --gemspec PATH"
40
+ elsif matches.size > 1
41
+ names = matches.map { |m| File.basename(m) }.join(", ")
42
+ raise ExecutionError, "Multiple .gemspec files found (#{names}). Specify one with --gemspec PATH"
43
+ end
44
+
45
+ matches.first
46
+ end
47
+
48
+ def load_gemspec(gemspec_path)
49
+ gemspec_path = File.expand_path(gemspec_path)
50
+ gem_root = File.dirname(gemspec_path)
51
+
52
+ source_text = File.read(gemspec_path) rescue ""
53
+ git_dependent = detect_git_dependency(source_text)
54
+
55
+ spec = nil
56
+ # Evaluate gemspec in its directory context
57
+ Dir.chdir(gem_root) do
58
+ begin
59
+ code = File.read(gemspec_path, mode: "r:UTF-8:-")
60
+ spec = eval(code, binding, gemspec_path)
61
+ rescue StandardError, ScriptError => e
62
+ raise GemspecLoadError, "Failed to evaluate gemspec #{File.basename(gemspec_path)}: #{e.message}"
63
+ end
64
+ end
65
+
66
+ if spec.nil? || !spec.is_a?(Gem::Specification)
67
+ raise GemspecLoadError, "Failed to evaluate gemspec: #{File.basename(gemspec_path)} is invalid or empty"
68
+ end
69
+
70
+ spec.loaded_from = gemspec_path
71
+
72
+ effective_files = Array(spec.files).map(&:to_s).uniq.sort
73
+
74
+ Model::Package.new(
75
+ name: spec.name || File.basename(gemspec_path, ".gemspec"),
76
+ version: spec.version ? spec.version.to_s : "0.0.0",
77
+ gemspec_path: gemspec_path,
78
+ gem_root: gem_root,
79
+ effective_files: effective_files,
80
+ spec: spec,
81
+ git_dependent: git_dependent
82
+ )
83
+ end
84
+
85
+ private
86
+
87
+ def detect_git_dependency(source)
88
+ # Check if gemspec uses `git ls-files` or similar git command
89
+ source.match?(/`\s*git\s+ls-files/i) ||
90
+ source.match?(/%x\[\s*git\s+ls-files/i) ||
91
+ source.match?(/IO\.popen\(\s*\[?['"]git['"],\s*['"]ls-files['"]/i) ||
92
+ source.match?(/Open3\.(?:capture[23]|popen3)\(\s*['"]git\s+ls-files/i)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLean
4
+ module Model
5
+ class Finding
6
+ SEVERITIES = %w[error warning info].freeze
7
+
8
+ attr_reader :rule, :severity, :message, :paths, :suggested_action
9
+
10
+ def initialize(rule:, severity:, message:, paths: [], suggested_action: nil)
11
+ @rule = rule.to_s
12
+ @severity = severity.to_s.downcase
13
+ @message = message.to_s
14
+ @paths = Array(paths).map(&:to_s).sort
15
+ @suggested_action = suggested_action&.to_s
16
+ end
17
+
18
+ def error?
19
+ @severity == "error"
20
+ end
21
+
22
+ def warning?
23
+ @severity == "warning"
24
+ end
25
+
26
+ def info?
27
+ @severity == "info"
28
+ end
29
+
30
+ def to_h
31
+ data = {
32
+ "rule" => @rule,
33
+ "severity" => @severity,
34
+ "message" => @message,
35
+ "paths" => @paths
36
+ }
37
+ data["suggestedAction"] = @suggested_action if @suggested_action
38
+ data
39
+ end
40
+
41
+ def ==(other)
42
+ other.is_a?(Finding) &&
43
+ rule == other.rule &&
44
+ severity == other.severity &&
45
+ message == other.message &&
46
+ paths == other.paths &&
47
+ suggested_action == other.suggested_action
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLean
4
+ module Model
5
+ class Package
6
+ attr_reader :name, :version, :gemspec_path, :gem_root, :effective_files, :spec, :git_dependent
7
+
8
+ def initialize(name:, version:, gemspec_path:, gem_root:, effective_files: [], spec: nil, git_dependent: false)
9
+ @name = name.to_s
10
+ @version = version.to_s
11
+ @gemspec_path = gemspec_path.to_s
12
+ @gem_root = File.expand_path(gem_root.to_s)
13
+ @effective_files = Array(effective_files).map(&:to_s).uniq.sort
14
+ @spec = spec
15
+ @git_dependent = !!git_dependent
16
+ end
17
+
18
+ def relative_gemspec_path
19
+ Pathname.new(@gemspec_path).relative_path_from(Pathname.new(@gem_root)).to_s
20
+ rescue ArgumentError
21
+ @gemspec_path
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module RubyLean
6
+ module Model
7
+ class Result
8
+ attr_reader :package, :inspection_mode, :archive_verified, :file_count, :total_bytes, :largest_files, :archive_files, :findings
9
+
10
+ def initialize(package:, inspection_mode: "archive", archive_verified: false, file_count: 0, total_bytes: nil, largest_files: [], archive_files: nil, findings: [])
11
+ @package = package
12
+ @inspection_mode = inspection_mode.to_s
13
+ @archive_verified = !!archive_verified
14
+ @file_count = Integer(file_count)
15
+ @total_bytes = total_bytes ? Integer(total_bytes) : nil
16
+ @largest_files = Array(largest_files)
17
+ @archive_files = archive_files ? Array(archive_files).map(&:to_s).sort : nil
18
+ @findings = Array(findings).sort_by { |f| [severity_rank(f.severity), f.rule, f.paths.first || ""] }
19
+ end
20
+
21
+ def errors
22
+ @findings.select(&:error?)
23
+ end
24
+
25
+ def warnings
26
+ @findings.select(&:warning?)
27
+ end
28
+
29
+ def info_items
30
+ @findings.select(&:info?)
31
+ end
32
+
33
+ def errors_count
34
+ errors.size
35
+ end
36
+
37
+ def warnings_count
38
+ warnings.size
39
+ end
40
+
41
+ def info_count
42
+ info_items.size
43
+ end
44
+
45
+ def passed?(strict: false)
46
+ return false if errors_count > 0
47
+ return false if strict && warnings_count > 0
48
+
49
+ true
50
+ end
51
+
52
+ def exit_code(strict: false)
53
+ return 1 unless passed?(strict: strict)
54
+
55
+ 0
56
+ end
57
+
58
+ def to_h
59
+ data = {
60
+ "schemaVersion" => "1",
61
+ "tool" => {
62
+ "name" => "ruby-lean",
63
+ "version" => RubyLean::VERSION
64
+ },
65
+ "package" => {
66
+ "name" => @package.name,
67
+ "version" => @package.version,
68
+ "gemspec" => @package.relative_gemspec_path
69
+ },
70
+ "inspection" => {
71
+ "mode" => @inspection_mode,
72
+ "archiveVerified" => @archive_verified,
73
+ "fileCount" => @file_count
74
+ },
75
+ "summary" => {
76
+ "errors" => errors_count,
77
+ "warnings" => warnings_count,
78
+ "info" => info_count
79
+ },
80
+ "findings" => @findings.map(&:to_h)
81
+ }
82
+
83
+ data["inspection"]["totalBytes"] = @total_bytes if @total_bytes
84
+ data
85
+ end
86
+
87
+ private
88
+
89
+ def severity_rank(sev)
90
+ case sev.to_s.downcase
91
+ when "error" then 0
92
+ when "warning" then 1
93
+ when "info" then 2
94
+ else 3
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module RubyLean
6
+ module Reporters
7
+ class JsonReporter
8
+ def self.render(result)
9
+ new(result).render
10
+ end
11
+
12
+ def initialize(result)
13
+ @result = result
14
+ end
15
+
16
+ def render
17
+ JSON.pretty_generate(@result.to_h)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLean
4
+ module Reporters
5
+ class TextReporter
6
+ def self.render(result, strict: false, quiet: false)
7
+ new(result, strict: strict, quiet: quiet).render
8
+ end
9
+
10
+ def initialize(result, strict: false, quiet: false)
11
+ @result = result
12
+ @strict = strict
13
+ @quiet = quiet
14
+ end
15
+
16
+ def render
17
+ lines = []
18
+
19
+ unless @quiet
20
+ lines << "ruby-lean lint: #{@result.package.name} #{@result.package.version}"
21
+ lines << " Gemspec: #{@result.package.relative_gemspec_path}"
22
+ mode_label = @result.archive_verified ? "Archive verified (.gem)" : "Specification-only (--no-archive)"
23
+ lines << " Mode: #{mode_label}"
24
+ lines << " Files: #{@result.file_count}"
25
+ if @result.total_bytes
26
+ lines << " Total size: #{format_bytes(@result.total_bytes)}"
27
+ end
28
+ lines << ""
29
+ end
30
+
31
+ if @result.findings.empty?
32
+ lines << "No issues found. Gem packaging passes policy."
33
+ lines << "Policy passed."
34
+ else
35
+ # Group findings by severity
36
+ errors = @result.errors
37
+ warnings = @result.warnings
38
+ info = @result.info_items
39
+
40
+ if errors.any?
41
+ lines << "ERRORS (#{errors.size}):"
42
+ errors.each { |f| lines.concat(format_finding(f)) }
43
+ lines << ""
44
+ end
45
+
46
+ if warnings.any?
47
+ lines << "WARNINGS (#{warnings.size}):"
48
+ warnings.each { |f| lines.concat(format_finding(f)) }
49
+ lines << ""
50
+ end
51
+
52
+ if info.any?
53
+ lines << "INFO (#{info.size}):"
54
+ info.each { |f| lines.concat(format_finding(f)) }
55
+ lines << ""
56
+ end
57
+
58
+ summary_text = "Summary: #{@result.errors_count} error(s), #{@result.warnings_count} warning(s)"
59
+ summary_text += " (strict mode enabled)" if @strict
60
+ lines << summary_text
61
+
62
+ if @result.passed?(strict: @strict)
63
+ lines << "Policy passed."
64
+ else
65
+ lines << "Policy failed."
66
+ end
67
+ end
68
+
69
+ lines.join("\n")
70
+ end
71
+
72
+ private
73
+
74
+ def format_finding(finding)
75
+ out = []
76
+ out << " [#{finding.rule}] #{finding.message}"
77
+ finding.paths.each do |p|
78
+ out << " - #{p}"
79
+ end
80
+ if finding.suggested_action
81
+ out << " Suggested action: #{finding.suggested_action}"
82
+ end
83
+ out
84
+ end
85
+
86
+ def format_bytes(bytes)
87
+ if bytes < 1024
88
+ "#{bytes} B"
89
+ elsif bytes < 1024 * 1024
90
+ "#{(bytes / 1024.0).round(1)} KB (#{bytes} bytes)"
91
+ else
92
+ "#{(bytes / (1024.0 * 1024.0)).round(2)} MB (#{bytes} bytes)"
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,301 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "set"
5
+
6
+ module RubyLean
7
+ module Rules
8
+ class Engine
9
+ def self.audit(package, archive_files: nil)
10
+ new(package, archive_files: archive_files).audit
11
+ end
12
+
13
+ def initialize(package, archive_files: nil)
14
+ @package = package
15
+ @archive_files = archive_files
16
+ @gem_root = package.gem_root
17
+ @effective_files = package.effective_files
18
+ end
19
+
20
+ def audit
21
+ findings = []
22
+
23
+ # 1. Path validity and existence checks (Errors)
24
+ findings.concat(check_paths_and_missing_files)
25
+
26
+ # 2. Advisory warning checks
27
+ findings.concat(check_dev_files)
28
+ findings.concat(check_ci_metadata)
29
+ findings.concat(check_coverage_artifacts)
30
+ findings.concat(check_editor_configs)
31
+ findings.concat(check_build_tools)
32
+ findings.concat(check_vcs_metadata)
33
+ findings.concat(check_symlinks)
34
+ findings.concat(check_git_dependency)
35
+
36
+ # 3. Archive mismatch checks (if archive was inspected)
37
+ findings.concat(check_archive_mismatches) if @archive_files
38
+
39
+ findings
40
+ end
41
+
42
+ private
43
+
44
+ def check_paths_and_missing_files
45
+ findings = []
46
+ invalid_paths = []
47
+ missing_files = []
48
+
49
+ gem_root_pathname = Pathname.new(@gem_root).cleanpath
50
+
51
+ @effective_files.each do |rel_path|
52
+ # Check for absolute path or root escaping traversal
53
+ if rel_path.start_with?("/") || rel_path.start_with?("\\")
54
+ invalid_paths << rel_path
55
+ next
56
+ end
57
+
58
+ expanded_path = File.expand_path(rel_path, @gem_root)
59
+ expanded_pathname = Pathname.new(expanded_path).cleanpath
60
+
61
+ # Check if path escapes gem root
62
+ begin
63
+ relative_to_root = expanded_pathname.relative_path_from(gem_root_pathname).to_s
64
+ if relative_to_root.start_with?("..")
65
+ invalid_paths << rel_path
66
+ next
67
+ end
68
+ rescue ArgumentError
69
+ invalid_paths << rel_path
70
+ next
71
+ end
72
+
73
+ # Check existence
74
+ if !File.exist?(expanded_path)
75
+ missing_files << rel_path
76
+ elsif File.directory?(expanded_path)
77
+ # Declaring a directory as a file entry
78
+ invalid_paths << rel_path
79
+ end
80
+ end
81
+
82
+ if invalid_paths.any?
83
+ findings << Model::Finding.new(
84
+ rule: "RUBYLEAN_INVALID_PATH",
85
+ severity: "error",
86
+ message: "Gemspec contains absolute paths, directory entries, or paths traversing outside the gem root.",
87
+ paths: invalid_paths,
88
+ suggested_action: "Remove absolute paths and directory entries; ensure all paths are relative to the gem root."
89
+ )
90
+ end
91
+
92
+ if missing_files.any?
93
+ findings << Model::Finding.new(
94
+ rule: "RUBYLEAN_MISSING_FILE",
95
+ severity: "error",
96
+ message: "Files referenced in gemspec effective file list do not exist on disk.",
97
+ paths: missing_files,
98
+ suggested_action: "Ensure declared files exist or remove non-existent references from spec.files."
99
+ )
100
+ end
101
+
102
+ findings
103
+ end
104
+
105
+ def check_dev_files
106
+ matches = @effective_files.select do |path|
107
+ dev_file?(path)
108
+ end
109
+
110
+ return [] if matches.empty?
111
+
112
+ [
113
+ Model::Finding.new(
114
+ rule: "RUBYLEAN_DEV_FILE",
115
+ severity: "warning",
116
+ message: "Development-oriented test or specification files are included in the package.",
117
+ paths: matches,
118
+ suggested_action: "Filter out test directories (spec/, test/, features/) from spec.files in your gemspec."
119
+ )
120
+ ]
121
+ end
122
+
123
+ def dev_file?(path)
124
+ p = path.downcase
125
+ # Test directories
126
+ return true if p.start_with?("spec/", "test/", "tests/", "features/", "benchmarks/")
127
+ return true if p.match?(%r{^[^/]+/(?:spec|test|tests|features)/})
128
+
129
+ # Standalone test files outside lib/
130
+ return true if !p.start_with?("lib/", "exe/", "bin/", "ext/") && (
131
+ p.end_with?("_test.rb", "_spec.rb") ||
132
+ File.basename(p).start_with?("test_")
133
+ )
134
+
135
+ false
136
+ end
137
+
138
+ def check_ci_metadata
139
+ matches = @effective_files.select do |path|
140
+ p = path.downcase
141
+ p.start_with?(".github/", ".gitlab/", ".circleci/", ".buildkite/") ||
142
+ %w[.travis.yml azure-pipelines.yml appveyor.yml .appveyor.yml .codecov.yml jenkinsfile].include?(File.basename(p))
143
+ end
144
+
145
+ return [] if matches.empty?
146
+
147
+ [
148
+ Model::Finding.new(
149
+ rule: "RUBYLEAN_CI_METADATA",
150
+ severity: "warning",
151
+ message: "Continuous Integration (CI) configuration or workflow files are included in the package.",
152
+ paths: matches,
153
+ suggested_action: "Exclude .github/, .gitlab/, and other CI configurations from spec.files."
154
+ )
155
+ ]
156
+ end
157
+
158
+ def check_coverage_artifacts
159
+ matches = @effective_files.select do |path|
160
+ p = path.downcase
161
+ p.start_with?("coverage/") || %w[.simplecov .simplecov_spawn].include?(File.basename(p))
162
+ end
163
+
164
+ return [] if matches.empty?
165
+
166
+ [
167
+ Model::Finding.new(
168
+ rule: "RUBYLEAN_COVERAGE",
169
+ severity: "warning",
170
+ message: "Test coverage configuration or generated report artifacts are included in the package.",
171
+ paths: matches,
172
+ suggested_action: "Remove coverage reports and .simplecov from spec.files."
173
+ )
174
+ ]
175
+ end
176
+
177
+ def check_editor_configs
178
+ matches = @effective_files.select do |path|
179
+ p = path.downcase
180
+ p.start_with?(".idea/", ".vscode/") ||
181
+ %w[.editorconfig .project .classpath .settings].include?(File.basename(p)) ||
182
+ p.end_with?(".swp", ".swo", "~")
183
+ end
184
+
185
+ return [] if matches.empty?
186
+
187
+ [
188
+ Model::Finding.new(
189
+ rule: "RUBYLEAN_EDITOR_CONFIG",
190
+ severity: "warning",
191
+ message: "Editor or IDE configuration files are included in the package.",
192
+ paths: matches,
193
+ suggested_action: "Exclude editor and IDE configuration directories and files from spec.files."
194
+ )
195
+ ]
196
+ end
197
+
198
+ def check_build_tools
199
+ build_tools_filenames = %w[
200
+ gemfile gemfile.lock rakefile .rubocop.yml .rubocop_todo.yml
201
+ .standard.yml .yardopts brewfile procfile .tool-versions .ruby-version
202
+ ]
203
+
204
+ matches = @effective_files.select do |path|
205
+ base = File.basename(path).downcase
206
+ build_tools_filenames.include?(base) || path.start_with?("bin/setup", "bin/console")
207
+ end
208
+
209
+ return [] if matches.empty?
210
+
211
+ [
212
+ Model::Finding.new(
213
+ rule: "RUBYLEAN_BUILD_TOOL",
214
+ severity: "warning",
215
+ message: "Development build tools, linters, or lockfiles are included in the package.",
216
+ paths: matches,
217
+ suggested_action: "Exclude Gemfile, Rakefile, linters, and development scripts from spec.files."
218
+ )
219
+ ]
220
+ end
221
+
222
+ def check_vcs_metadata
223
+ matches = @effective_files.select do |path|
224
+ p = path.downcase
225
+ p.start_with?(".git/", ".hg/", ".svn/") ||
226
+ %w[.gitignore .gitattributes .gitmodules].include?(File.basename(p))
227
+ end
228
+
229
+ return [] if matches.empty?
230
+
231
+ [
232
+ Model::Finding.new(
233
+ rule: "RUBYLEAN_VCS_METADATA",
234
+ severity: "warning",
235
+ message: "Version control system (VCS) metadata or configuration files are included in the package.",
236
+ paths: matches,
237
+ suggested_action: "Exclude .gitignore, .gitattributes, and other repository metadata from spec.files."
238
+ )
239
+ ]
240
+ end
241
+
242
+ def check_symlinks
243
+ matches = @effective_files.select do |path|
244
+ full_path = File.join(@gem_root, path)
245
+ File.symlink?(full_path)
246
+ end
247
+
248
+ return [] if matches.empty?
249
+
250
+ [
251
+ Model::Finding.new(
252
+ rule: "RUBYLEAN_SYMLINK",
253
+ severity: "warning",
254
+ message: "Symbolic link included in package files.",
255
+ paths: matches,
256
+ suggested_action: "Replace symlinks with regular files or ensure target files are valid package contents."
257
+ )
258
+ ]
259
+ end
260
+
261
+ def check_git_dependency
262
+ return [] unless @package.git_dependent
263
+
264
+ [
265
+ Model::Finding.new(
266
+ rule: "RUBYLEAN_GIT_DEPENDENT",
267
+ severity: "warning",
268
+ message: "Gemspec uses dynamic git ls-files discovery, which may include unneeded files or fail in non-git environments.",
269
+ paths: [@package.relative_gemspec_path],
270
+ suggested_action: "Use explicit file lists or filtered Dir.glob expressions instead of invoking git ls-files."
271
+ )
272
+ ]
273
+ end
274
+
275
+ def check_archive_mismatches
276
+ gemspec_set = @effective_files.to_set
277
+ archive_set = @archive_files.to_set
278
+
279
+ missing_in_archive = (gemspec_set - archive_set).to_a.sort
280
+ extra_in_archive = (archive_set - gemspec_set).to_a.sort
281
+
282
+ findings = []
283
+ if missing_in_archive.any? || extra_in_archive.any?
284
+ discrepancy_details = []
285
+ discrepancy_details.concat(missing_in_archive.map { |f| "#{f} (missing in .gem archive)" })
286
+ discrepancy_details.concat(extra_in_archive.map { |f| "#{f} (present in archive but not in gemspec)" })
287
+
288
+ findings << Model::Finding.new(
289
+ rule: "RUBYLEAN_ARCHIVE_MISMATCH",
290
+ severity: "error",
291
+ message: "Discrepancy detected between gemspec declared files and actual built .gem archive contents.",
292
+ paths: missing_in_archive + extra_in_archive,
293
+ suggested_action: "Review gemspec file patterns and build process to ensure archive contents match expectations."
294
+ )
295
+ end
296
+
297
+ findings
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLean
4
+ VERSION = "0.1.0"
5
+ end
data/lib/ruby_lean.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ruby_lean/version"
4
+ require_relative "ruby_lean/model/finding"
5
+ require_relative "ruby_lean/model/package"
6
+ require_relative "ruby_lean/model/result"
7
+ require_relative "ruby_lean/gemspec_loader"
8
+ require_relative "ruby_lean/archive_inspector"
9
+ require_relative "ruby_lean/rules/engine"
10
+ require_relative "ruby_lean/reporters/text_reporter"
11
+ require_relative "ruby_lean/reporters/json_reporter"
12
+ require_relative "ruby_lean/command/lint"
13
+ require_relative "ruby_lean/cli"
14
+
15
+ module RubyLean
16
+ class Error < StandardError; end
17
+ class ExecutionError < Error; end
18
+ class GemspecLoadError < ExecutionError; end
19
+ class BuildError < ExecutionError; end
20
+ end
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-lean
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - lean-packages contributors
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-11-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A Ruby-native CLI to audit RubyGem package distributions, detect unnecessary
14
+ files, and inspect archive contents.
15
+ email:
16
+ - maintainers@lean-packages.org
17
+ executables:
18
+ - ruby-lean
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - LICENSE.md
24
+ - README.md
25
+ - exe/ruby-lean
26
+ - lib/ruby_lean.rb
27
+ - lib/ruby_lean/archive_inspector.rb
28
+ - lib/ruby_lean/cli.rb
29
+ - lib/ruby_lean/command/lint.rb
30
+ - lib/ruby_lean/gemspec_loader.rb
31
+ - lib/ruby_lean/model/finding.rb
32
+ - lib/ruby_lean/model/package.rb
33
+ - lib/ruby_lean/model/result.rb
34
+ - lib/ruby_lean/reporters/json_reporter.rb
35
+ - lib/ruby_lean/reporters/text_reporter.rb
36
+ - lib/ruby_lean/rules/engine.rb
37
+ - lib/ruby_lean/version.rb
38
+ homepage: https://github.com/lean-packages/ruby-lean
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ homepage_uri: https://github.com/lean-packages/ruby-lean
43
+ source_code_uri: https://github.com/lean-packages/ruby-lean
44
+ changelog_uri: https://github.com/lean-packages/ruby-lean/blob/main/CHANGELOG.md
45
+ post_install_message:
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 2.6.0
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ requirements: []
60
+ rubygems_version: 3.5.22
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: Audits RubyGem distribution contents using native RubyGems packaging rules
64
+ test_files: []