stellwerk 0.0.1 → 0.0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a59c8761699131fdfb30f5a83782ba2b71a1bcc12bd59fed46f071a9306aba7
4
- data.tar.gz: 1e7c0b76429f09eb9139493bf0e2b0a2b2f22599ed9e7d2b9ad6c5176ec571df
3
+ metadata.gz: f5bc717030b6f075aebb0615a37ee45cf32aafe5cc7d5e9e7d5bbf50350f6cfa
4
+ data.tar.gz: 6d03317e06b5d44b174b734424a72543943f5f62a39ff4d3cba9a6b1a64a9b0d
5
5
  SHA512:
6
- metadata.gz: acfa6f9020aaf20e6ecc94b91cef9ac5331ab70b32f64113a0ebe6baa8ef5cb1f13dc2638cbd98a820bdf946d3598546abbafa550f710080f1f69086f213a8b7
7
- data.tar.gz: d32d3de5760a3c9f01992cb0c24472b249220d13c859c07fd189e3b85876a8678b3ac0b7b0e52be4028d7da1699525aa758e01bfec2a908bedea8a2d3af36094
6
+ metadata.gz: a87d9aa67acfa4a3c5df1119d6c8aaa1e1f4ea98c8a923445cd970c1b783e3a25db0bae5f5b12950d61fc975ed75c6a1423ecf62ff5b31c3a81d16e9ce76428e
7
+ data.tar.gz: 11a17aa329b2194b587e66d5e316e978c026dd2114795ce7dda1c95434e9bb037b981972625db420679de2df5eff66f6ccc26186891fb0b48d9facecdf7857b9
data/AGENTS.md ADDED
@@ -0,0 +1,9 @@
1
+ # Agent Instructions
2
+
3
+ ## Keep it simple
4
+
5
+ We're in pre-alpha. Don't overengineer, don't overdocument, don't put too much emphasis on UX. We want to prove the basics first.
6
+
7
+ ## Usage Expectations
8
+
9
+ This gem can only be used with Rails applications for now. In theory we can support any Ruby codebase using zeitwerk, but that's not a priority at this point.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.0.2] - 2026-02-15
4
+
5
+ - MVP: Basic architecture enforcement with layered architecture support
6
+ - **breaking:** invoke stellwerk via rake task (`rails stellwerk:check`), the executable is removed
7
+ - pull actual autoloaders from application, or use faked autoloaders to omit app startup via `rails stellwerk:check_simple`
8
+
3
9
  ## [0.0.1] - 2025-12-10
4
10
 
5
- - Initial release
11
+ - Initial release without any functionality. Essentially, name squatting.
data/IMPLEMENTATION.md CHANGED
@@ -63,8 +63,13 @@ Considerations:
63
63
  jobs should probably live in the business logic layer
64
64
  - what about lib?
65
65
 
66
- ## TODO
66
+ ## Todos
67
+
68
+ - [ ] Implement VSCode integration (lots of things TBD)
69
+
70
+ ## Ideas
67
71
 
68
72
  - Shortcuts
69
73
  - for CLI command, use a CLI library instead of hand-rolling it
70
74
  - probably, `optparse` built into the stdlib
75
+ - Support non-rails applications using Zeitwerk, probably by adding an executable
data/README.md CHANGED
@@ -1,62 +1,47 @@
1
1
  # Stellwerk
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ Stellwerk is a Ruby gem that helps you enforce architectural rules in your Ruby on Rails application. It analyzes your codebase and identifies violations of architectural constraints you specify.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/stellwerk`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ ## Name
6
6
 
7
- ## Installation
7
+ The name "Stellwerk" is derived from the German word for "signal station" in a railway context. It keeps your Rails on track!
8
8
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
9
+ ## Installation
10
10
 
11
11
  Install the gem and add to the application's Gemfile by executing:
12
12
 
13
13
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
14
+ bundle add stellwerk
15
15
  ```
16
16
 
17
17
  If bundler is not being used to manage dependencies, install the gem by executing:
18
18
 
19
19
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
20
+ gem install stellwerk
21
21
  ```
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
26
-
27
- ## Development
28
-
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
-
31
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
-
33
- ## Publishing to RubyGems
25
+ Run the check task in your Rails app:
34
26
 
35
- 1. Update the version in `lib/stellwerk/version.rb`. Push / merge to main.
36
- 2. Build the gem:
37
-
38
- ```bash
39
- gem build stellwerk.gemspec
40
- ```
27
+ ```bash
28
+ bin/rails stellwerk:check
29
+ ```
41
30
 
42
- This should produce `stellwerk-<version>.gem`.
43
- 3. Sign in to RubyGems (only needed once):
31
+ Run a simpler check without booting `:environment` (uses statically defined Zeitwerk loaders for `app/*` and `lib`) and will probably miss stuff in more complex Rails apps:
44
32
 
45
- ```bash
46
- gem signin
47
- ```
33
+ ```bash
34
+ bin/rails stellwerk:check_simple
35
+ ```
48
36
 
49
- 4. Push the built gem:
37
+ ## Development
50
38
 
51
- ```bash
52
- gem push stellwerk-<version>.gem
53
- ```
39
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
54
40
 
55
- 5. Tag the release:
41
+ ## Publishing to RubyGems
56
42
 
57
- ```bash
58
- git tag v<version> && git push origin v<version>
59
- ```
43
+ 1. Update the version in [`lib/stellwerk/version.rb`](lib/stellwerk/version.rb). Push / merge to main.
44
+ 2. `rake release`
60
45
 
61
46
  ## Contributing
62
47
 
@@ -0,0 +1,58 @@
1
+ require "pathname"
2
+ require "reference_extractor"
3
+ require "zeitwerk"
4
+ require "parallel"
5
+
6
+ require "stellwerk/config"
7
+ require "stellwerk/printer"
8
+
9
+ module Stellwerk
10
+ module Commands
11
+ class Check
12
+ def initialize(root_path, autoloaders: nil)
13
+ @root_path = Pathname.new(root_path)
14
+ @autoloaders = autoloaders || fake_autoloaders
15
+ end
16
+
17
+ def run
18
+ puts "running check..."
19
+ config = Stellwerk::Config.new(@root_path.join("stellwerk.yml"))
20
+
21
+ # build graph using reference_extractor
22
+ extractor = ReferenceExtractor::Extractor.new(
23
+ autoloaders: @autoloaders,
24
+ root_path: @root_path
25
+ )
26
+
27
+ all_files = @root_path.find
28
+ .select { |path| path.to_s.end_with?(".rb") }
29
+ .reject { |path| path.relative_path_from(@root_path).to_s.start_with?("db/") }
30
+ puts "collected #{all_files.length} files"
31
+
32
+ before = Time.now
33
+ edgelist = Parallel.flat_map(all_files, in_processes: Parallel.processor_count - 1) do |file|
34
+ extractor.references_from_file(file)
35
+ end
36
+ puts "extracted #{edgelist.length} references in #{(Time.now - before).round(2)} seconds"
37
+
38
+ # check rules against graph, collect violations
39
+ puts "checking rules..."
40
+ violations = config.rules.flat_map do |rule|
41
+ rule.find_violations(edgelist)
42
+ end
43
+
44
+ Stellwerk::Printer.new(violations).print
45
+ end
46
+
47
+ private
48
+
49
+ def fake_autoloaders
50
+ loader = Zeitwerk::Loader.new
51
+ @root_path.join("app").each_child { |child| loader.push_dir(child) }
52
+ loader.push_dir(@root_path.join("lib"))
53
+ loader.setup
54
+ [loader]
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,28 @@
1
+ require "active_support/core_ext/string"
2
+
3
+ module Stellwerk
4
+ class Config
5
+ class UnknownRule < StandardError; end
6
+
7
+ attr_reader :rules
8
+
9
+ def initialize(filepath)
10
+ config = YAML.load_file(filepath)
11
+
12
+ @rules = initialize_rules(config["rules"])
13
+ end
14
+
15
+ def initialize_rules(rules_config)
16
+ rules_config.map do |rule_name, rule_config|
17
+ begin
18
+ require "stellwerk/rules/#{rule_name}"
19
+ rule_class = ("Stellwerk::Rules::" + rule_name.camelize).constantize
20
+ rescue LoadError, NameError
21
+ raise(UnknownRule, "Unknown rule: #{rule_name}")
22
+ end
23
+
24
+ rule_class.new(rule_config)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ module Stellwerk
2
+ class Printer
3
+ def initialize(violations)
4
+ @violations = violations
5
+ end
6
+
7
+ def print
8
+ puts "\nFound #{@violations.length} violations."
9
+
10
+ @violations.group_by { |violation| violation.rule }.each do |rule, violations|
11
+ puts "#{rule.name.titleize} rule violations:"
12
+ violations.each do |violation|
13
+ puts " " + format_reference(violation.reference)
14
+ end
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def format_reference(reference)
21
+ source = "#{reference.relative_path}:#{reference.source_location.line}"
22
+ target = "#{reference.constant.name}, defined in #{reference.constant.location}"
23
+ "#{source} refers to #{target}"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stellwerk
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load File.expand_path("../tasks/stellwerk.rake", __dir__)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,58 @@
1
+ require "pathname"
2
+
3
+ require "stellwerk/violation"
4
+
5
+ module Stellwerk
6
+ module Rules
7
+ class Layers
8
+ class InvalidLayersSpec < RuntimeError; end
9
+
10
+ def initialize(rule_spec)
11
+ @layers = rule_spec.map { |layer| Array(layer).map { |path_string| Pathname.new(path_string) } }
12
+ validate_layers!
13
+ end
14
+
15
+ def find_violations(reference_graph)
16
+ relevant_graph = filter_graph(reference_graph)
17
+
18
+ relevant_graph.map do |reference|
19
+ # TO DO: violations should include some rule specific violation context
20
+ # e.g. from layer and to layer
21
+ if layer_index(reference.relative_path) > layer_index(reference.constant.location)
22
+ Violation.new(:layers, reference)
23
+ end
24
+ end.compact
25
+ end
26
+
27
+ def filter_graph(reference_graph)
28
+ reference_graph.select do |reference|
29
+ all_components.any? { |component| path_in_component?(reference.relative_path, component) } &&
30
+ all_components.any? { |component| path_in_component?(reference.constant.location, component) }
31
+ end
32
+ end
33
+
34
+ def all_components
35
+ @layers.flatten
36
+ end
37
+
38
+ def path_in_component?(path, component)
39
+ # TO DO: Find out whether we can use Pathname methods to determine whether one path contains another
40
+ component_dirname = component.to_s
41
+ component_dirname += "/" unless component.to_s.end_with?("/")
42
+ path.to_s.start_with?(component_dirname)
43
+ end
44
+
45
+ def layer_index(file_path)
46
+ @layers.index do |components|
47
+ components.any? { |component| path_in_component?(file_path, component) }
48
+ end
49
+ end
50
+
51
+ def validate_layers!
52
+ raise InvalidLayersSpec, "Overlapping layers" if all_components.count != all_components.uniq.count
53
+ raise InvalidLayersSpec, "No layers exist" if @layers.empty?
54
+ raise InvalidLayersSpec, "Empty layers exist" if @layers.any?(&:empty?)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stellwerk
4
- VERSION = "0.0.1"
4
+ VERSION = "0.0.2"
5
5
  end
@@ -0,0 +1,3 @@
1
+ module Stellwerk
2
+ Violation = Struct.new(:rule, :reference)
3
+ end
data/lib/stellwerk.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "stellwerk/version"
4
+ require_relative "stellwerk/commands/check"
5
+
6
+ require "rails/railtie"
7
+ require_relative "stellwerk/railtie"
4
8
 
5
9
  module Stellwerk
6
10
  class Error < StandardError; end
7
- # Your code goes here...
8
11
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :stellwerk do
4
+ task check: :environment do
5
+ autoloaders = [Rails.autoloaders.main, Rails.autoloaders.once].compact
6
+
7
+ Stellwerk::Commands::Check.new(Rails.root, autoloaders: autoloaders).run
8
+ end
9
+
10
+ task :check_simple do
11
+ Stellwerk::Commands::Check.new(Dir.pwd).run
12
+ end
13
+ end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stellwerk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Philip Theus
8
- bindir: exe
8
+ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
@@ -37,24 +37,57 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: parallel
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: railties
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
40
68
  email:
41
69
  - philip@simplexity.quest
42
- executables:
43
- - stellwerk
70
+ executables: []
44
71
  extensions: []
45
72
  extra_rdoc_files: []
46
73
  files:
47
74
  - ".tool-versions"
75
+ - AGENTS.md
48
76
  - CHANGELOG.md
49
77
  - CODE_OF_CONDUCT.md
50
78
  - IMPLEMENTATION.md
51
79
  - LICENSE.txt
52
80
  - README.md
53
81
  - Rakefile
54
- - exe/stellwerk
55
82
  - lib/stellwerk.rb
56
- - lib/stellwerk/command.rb
83
+ - lib/stellwerk/commands/check.rb
84
+ - lib/stellwerk/config.rb
85
+ - lib/stellwerk/printer.rb
86
+ - lib/stellwerk/railtie.rb
87
+ - lib/stellwerk/rules/layers.rb
57
88
  - lib/stellwerk/version.rb
89
+ - lib/stellwerk/violation.rb
90
+ - lib/tasks/stellwerk.rake
58
91
  homepage: https://github.com/exterm/stellwerk
59
92
  licenses:
60
93
  - MIT
data/exe/stellwerk DELETED
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "stellwerk/command"
@@ -1,3 +0,0 @@
1
- require "stellwerk"
2
-
3
-