columnlens 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: f014ac1f374469c92d5161bea7ad72efb86a3a70292fc304a98bde5c494b9e5c
4
+ data.tar.gz: 57065f1a89c2300f1ff1f081a244e75d1df502e011622faf994916051516515a
5
+ SHA512:
6
+ metadata.gz: 66396ebec94c1ba101aedf36d2a23ca3bdc30d18f1dca4f21cd7c902c240af651387b8edc065a6029c2c449a0f98f6b34805f04837ba7412dd86e47b71808a33
7
+ data.tar.gz: b7fbf7af6247cb4fa59f94137613663af8f6e8f35cace68fb9ddc936b1a6b759a433fc05c7175183d5318f61d46a71510b5965f7e09fe36ae3a32df7431b8682
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.0
3
+
4
+ Metrics/MethodLength:
5
+ Max: 20
6
+
7
+ Style/Documentation:
8
+ Enabled: false
9
+
10
+ Style/StringLiterals:
11
+ EnforcedStyle: double_quotes
12
+
13
+ Style/StringLiteralsInInterpolation:
14
+ EnforcedStyle: double_quotes
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Fai Wong
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,136 @@
1
+ # ColumnLens
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/columnlens.svg)](https://badge.fury.io/rb/columnlens)
4
+
5
+ **columnlens** is a CLI tool to analyze your Ruby on Rails database schema. It helps detect unused, read-only, and orphaned columns, providing insight into schema hygiene and technical debt.
6
+
7
+ ---
8
+
9
+ ## ✨ Features
10
+
11
+ - Scan your full Rails database schema
12
+ - Detect actively used, write-only, read-only, and orphaned columns
13
+ - Respect system tables and configurable ignore rules
14
+
15
+ ---
16
+
17
+ ## 🧰 Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'columnlens'
23
+ ```
24
+
25
+ Then execute:
26
+
27
+ ```sh
28
+ $ bundle install
29
+ ```
30
+
31
+ Or install it globally:
32
+
33
+ ```sh
34
+ $ gem install columnlens
35
+ ```
36
+
37
+ ---
38
+
39
+ ## 🚀 Usage
40
+
41
+ From any Rails project:
42
+
43
+ ```sh
44
+ $ bundle exec columnlens scan
45
+ ```
46
+
47
+ Output example:
48
+
49
+ ```
50
+ 🔍 ColumnLens Scan Mode
51
+ Scanning full schema...
52
+
53
+ posts.title read_write
54
+ users.email read_write
55
+ users.last_seen_at orphaned
56
+ ...
57
+ ```
58
+
59
+ ---
60
+
61
+ ## ⚙️ Configuration
62
+
63
+ ColumnLens uses a .columnlens.yml in your project root:
64
+
65
+ ```yaml
66
+ system_tables:
67
+ - "^active_storage_"
68
+ - "^schema_migrations$"
69
+ - "^ar_internal_metadata$"
70
+
71
+ ignore:
72
+ scan:
73
+ orphaned:
74
+ - "posts"
75
+ - "users.created_at"
76
+ - "users.updated_at"
77
+ deep:
78
+ orphaned: []
79
+ ```
80
+
81
+ Defaults are provided in lib/config/default.yml and merged with the project config.
82
+
83
+ ---
84
+
85
+ ## 🔧 Development
86
+
87
+ To set up the project locally:
88
+
89
+ ```sh
90
+ $ git clone https://github.com/BestBitsLab/columnlens.git
91
+ $ cd columnlens
92
+ $ bin/setup
93
+ ```
94
+
95
+ You can experiment with the code via:
96
+
97
+ ```sh
98
+ $ bin/console
99
+ ```
100
+
101
+ To build and install the gem locally:
102
+
103
+ ```sh
104
+ $ bundle exec rake install
105
+ ```
106
+
107
+ To release a new version:
108
+
109
+ 1. Update the version in `lib/columnlens/version.rb`
110
+ 2. Run:
111
+
112
+ ```sh
113
+ $ bundle exec rake release
114
+ ```
115
+
116
+ This will tag, push, and publish to [RubyGems.org](https://rubygems.org).
117
+
118
+ ---
119
+
120
+ ## 🤝 Contributing
121
+
122
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/BestBitsLab/columnlens). This project is intended to be a safe, welcoming space for collaboration. Please read and follow the [code of conduct](https://github.com/BestBitsLab/columnlens/blob/main/CODE_OF_CONDUCT.md).
123
+
124
+ ---
125
+
126
+ ## 🪪 License
127
+
128
+ This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
129
+
130
+ ---
131
+
132
+ ## 📜 Code of Conduct
133
+
134
+ Everyone interacting in the ColumnLens project's codebase, issue trackers, and other community spaces is expected to follow the [Code of Conduct](https://github.com/BestBitsLab/columnlens/blob/main/CODE_OF_CONDUCT.md).
135
+
136
+ ---
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rubocop/rake_task"
5
+
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: :rubocop
data/exe/columnlens ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/columnlens"
5
+
6
+ Columnlens.run(ARGV)
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Columnlens
4
+ class Classifier
5
+ def self.classify(table, column, usage)
6
+ status =
7
+ case usage
8
+ when :read_write then :active
9
+ when :write_only then :write_only
10
+ when :read_only then :read_only
11
+ when :orphaned then :orphaned
12
+ when :system then :system
13
+ else :unknown
14
+ end
15
+
16
+ {
17
+ table: table,
18
+ column: column,
19
+ status: status
20
+ }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,10 @@
1
+ system_tables:
2
+ - "^active_storage_"
3
+ - "^schema_migrations$"
4
+ - "^ar_internal_metadata$"
5
+
6
+ ignore:
7
+ scan:
8
+ orphaned: []
9
+ deep:
10
+ orphaned: []
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "paths"
5
+
6
+ # Handles loading and merging default and project configurations for ColumnLens
7
+ module Columnlens
8
+ class Config
9
+ def self.load
10
+ defaults = load_yaml(Paths.default_config)
11
+ project = load_yaml(Paths.project_config)
12
+
13
+ deep_merge(defaults, project)
14
+ end
15
+
16
+ def self.load_yaml(path)
17
+ return {} unless path && File.exist?(path)
18
+
19
+ YAML.load_file(path) || {}
20
+ end
21
+
22
+ def self.deep_merge(base_config, project_config)
23
+ base_config.merge(project_config) do |_key, old, new|
24
+ old.is_a?(Hash) && new.is_a?(Hash) ? deep_merge(old, new) : new
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../project_root"
4
+
5
+ module Columnlens
6
+ module Core
7
+ class SchemaLoader
8
+ TABLE_REGEX = /create_table "([^"]+)"/
9
+ COLUMN_REGEX = /t\.\w+\s+"([^"]+)"/
10
+
11
+ def self.load
12
+ schema_path = File.join(ProjectRoot.detect, "db/schema.rb")
13
+
14
+ raise "Columnlens: db/schema.rb not found at #{schema_path}" unless File.exist?(schema_path)
15
+
16
+ schema_content = File.read(schema_path)
17
+ parse_tables(schema_content)
18
+ end
19
+
20
+ def self.parse_tables(schema_content)
21
+ tables = {}
22
+ current_table = nil
23
+
24
+ schema_content.each_line do |line|
25
+ if line.match?(TABLE_REGEX)
26
+ current_table = line[TABLE_REGEX, 1]
27
+ tables[current_table] ||= []
28
+
29
+ elsif current_table && line.match?(COLUMN_REGEX)
30
+ column = line[COLUMN_REGEX, 1]
31
+ tables[current_table] << column
32
+
33
+ elsif line.strip == "end"
34
+ current_table = nil
35
+ end
36
+ end
37
+
38
+ tables
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Columnlens
4
+ module Core
5
+ class UsageScanner
6
+ attr_reader :tables, :usage_results
7
+
8
+ def initialize(tables)
9
+ # tables: array of { name: "table_name", columns: ["col1", "col2"] }
10
+ @tables = tables
11
+ @usage_results = {}
12
+ end
13
+
14
+ def scan!
15
+ tables.each do |table|
16
+ table_name = table[:name]
17
+ columns = table[:columns]
18
+
19
+ columns.each do |col|
20
+ status = detect_column_usage(table_name, col)
21
+ mark_used(table_name, col, status)
22
+ end
23
+ end
24
+ usage_results
25
+ end
26
+
27
+ private
28
+
29
+ # Mark column usage
30
+ def mark_used(table_name, column, status)
31
+ usage_results[table_name] ||= {}
32
+ usage_results[table_name][column] = status
33
+ end
34
+
35
+ # Detect usage using multiple heuristics
36
+ def detect_column_usage(table_name, column)
37
+ return :read_write if association_column?(table_name, column)
38
+ return :read_write if scanned_in_code?(table_name, column)
39
+
40
+ :orphaned
41
+ end
42
+
43
+ # Detect foreign keys for associations
44
+ def association_column?(table_name, column)
45
+ # Common Rails convention: belongs_to :model → model_id
46
+ return true if column =~ /_id$/ && belongs_to_exists?(table_name, column)
47
+
48
+ false
49
+ end
50
+
51
+ def belongs_to_exists?(table_name, column)
52
+ model_class = table_name.singularize.camelize.safe_constantize
53
+ return false unless model_class.respond_to?(:reflect_on_all_associations)
54
+
55
+ model_class.reflect_on_all_associations(:belongs_to).any? do |assoc|
56
+ "#{assoc.name}_id" == column
57
+ end
58
+ rescue NameError
59
+ false
60
+ end
61
+
62
+ # Scan Ruby + view templates
63
+ def scanned_in_code?(_table_name, column)
64
+ patterns = [
65
+ "app/models/**/*.rb",
66
+ "app/controllers/**/*.rb",
67
+ "app/jobs/**/*.rb",
68
+ "app/views/**/*.erb",
69
+ "app/views/**/*.slim",
70
+ "app/views/**/*.haml",
71
+ "app/serializers/**/*.rb"
72
+ ]
73
+
74
+ patterns.any? do |pattern|
75
+ Dir.glob(pattern).any? do |file|
76
+ File.read(file).include?(column)
77
+ end
78
+ end
79
+ rescue Errno::ENOENT
80
+ false
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Columnlens
4
+ class IgnoreRules
5
+ def initialize(config)
6
+ @config = config
7
+ end
8
+
9
+ def system_table_patterns
10
+ Array(@config["system_tables"]).map { |p| Regexp.new(p) }
11
+ end
12
+
13
+ def ignored?(mode:, category:, table:, column:)
14
+ entries = Array(
15
+ @config.dig("ignore", mode.to_s, category.to_s)
16
+ )
17
+
18
+ entries.any? do |e|
19
+ e == table || e == "#{table}.#{column}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Columnlens
4
+ module Paths
5
+ def self.project_root
6
+ ProjectRoot.detect
7
+ end
8
+
9
+ def self.project_config
10
+ File.join(project_root, ".columnlens.yml")
11
+ end
12
+
13
+ def self.default_config
14
+ File.expand_path("../columnlens/config/default.yml", __dir__)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Columnlens
4
+ module ProjectRoot
5
+ def self.detect
6
+ return ENV["GITHUB_WORKSPACE"] if ENV["GITHUB_WORKSPACE"]
7
+ return Rails.root.to_s if defined?(Rails)
8
+
9
+ Dir.pwd
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Columnlens
4
+ class Reporter
5
+ ICONS = {
6
+ active: "🟢",
7
+ write_only: "🟡",
8
+ read_only: "🟠",
9
+ orphaned: "🔴"
10
+ }.freeze
11
+
12
+ def self.print(results)
13
+ grouped = results.group_by { |r| r[:status] }
14
+
15
+ grouped.each do |status, items|
16
+ puts "\n#{ICONS[status]} #{status.to_s.upcase} (#{items.count})"
17
+ items.each do |r|
18
+ puts " - #{r[:table]}.#{r[:column]}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Columnlens
4
+ class ResultFilter
5
+ def initialize(config, mode:)
6
+ @config = config
7
+ @mode = mode
8
+ end
9
+
10
+ def filter(results)
11
+ results.reject do |r|
12
+ system_table?(r[:table]) ||
13
+ ignored?(r)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def system_table?(table)
20
+ @config.system_table_patterns.any? { |p| table.match?(p) }
21
+ end
22
+
23
+ def ignored?(record)
24
+ @config.ignored?(
25
+ mode: @mode,
26
+ category: record[:status],
27
+ table: record[:table],
28
+ column: record[:column]
29
+ )
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "core/schema_loader"
4
+ require_relative "core/usage_scanner"
5
+ require_relative "classifier"
6
+ require_relative "config"
7
+ require_relative "ignore_rules"
8
+ require_relative "result_filter"
9
+ require_relative "reporter"
10
+
11
+ # Handles scanning the database schema and classifying column usage.
12
+ module Columnlens
13
+ class ScanMode
14
+ MODE = :scan
15
+
16
+ def self.run
17
+ puts "🔍 Columnlens Scan Mode"
18
+ puts "Scanning full schema...\n\n"
19
+
20
+ results = process_schema
21
+
22
+ Reporter.print(results)
23
+ end
24
+
25
+ def self.process_schema
26
+ schema = Core::SchemaLoader.load
27
+ scanner = Core::UsageScanner.new(map_tables(schema))
28
+ raw = scanner.scan!
29
+
30
+ results = classify_columns(raw)
31
+ filter_results(results)
32
+ end
33
+
34
+ def self.map_tables(schema)
35
+ schema.map { |table, columns| { name: table, columns: columns } }
36
+ end
37
+
38
+ def self.classify_columns(raw)
39
+ raw.flat_map do |table, columns|
40
+ columns.map do |column, usage|
41
+ Classifier.classify(table, column, usage)
42
+ end
43
+ end
44
+ end
45
+
46
+ def self.filter_results(results)
47
+ config = IgnoreRules.new(Config.load)
48
+ ResultFilter.new(config, mode: MODE).filter(results)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Columnlens
4
+ VERSION = "0.1.0"
5
+ end
data/lib/columnlens.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "columnlens/scan_mode"
4
+
5
+ module Columnlens
6
+ def self.run(args)
7
+ case args.first
8
+ when "scan"
9
+ Columnlens::ScanMode.run
10
+ else
11
+ puts <<~USAGE
12
+ Usage:
13
+ columnlens scan # Full schema inventory
14
+ USAGE
15
+ exit 1
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,4 @@
1
+ module Columnlens
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: columnlens
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Fai Wong
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-12-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ ColumnLens analyzes your Ruby on Rails codebase and database schema
15
+ to identify actively used, write-only, read-only, and orphaned columns.
16
+ Designed for CI, GitHub Actions, and continuous schema hygiene.
17
+ email:
18
+ - wongwf82@gmail.com
19
+ executables:
20
+ - columnlens
21
+ extensions: []
22
+ extra_rdoc_files: []
23
+ files:
24
+ - ".rubocop.yml"
25
+ - CODE_OF_CONDUCT.md
26
+ - LICENSE.txt
27
+ - README.md
28
+ - Rakefile
29
+ - exe/columnlens
30
+ - lib/columnlens.rb
31
+ - lib/columnlens/classifier.rb
32
+ - lib/columnlens/config.rb
33
+ - lib/columnlens/config/default.yml
34
+ - lib/columnlens/core/schema_loader.rb
35
+ - lib/columnlens/core/usage_scanner.rb
36
+ - lib/columnlens/ignore_rules.rb
37
+ - lib/columnlens/paths.rb
38
+ - lib/columnlens/project_root.rb
39
+ - lib/columnlens/reporter.rb
40
+ - lib/columnlens/result_filter.rb
41
+ - lib/columnlens/scan_mode.rb
42
+ - lib/columnlens/version.rb
43
+ - sig/columnlens.rbs
44
+ homepage: https://github.com/BestBitsLab/columnlens
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ homepage_uri: https://github.com/BestBitsLab/columnlens
49
+ source_code_uri: https://github.com/BestBitsLab/columnlens
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 3.0.0
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ requirements: []
65
+ rubygems_version: 3.2.3
66
+ signing_key:
67
+ specification_version: 4
68
+ summary: Detect unused, read-only, and orphaned database columns in Rails apps
69
+ test_files: []