archunit 0.0.1 → 1.0.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 +4 -4
- data/API.md +290 -0
- data/CHANGELOG.md +45 -0
- data/README.md +389 -90
- data/assets/logo-rounded.png +0 -0
- data/lib/archunit/common/extraction/dependency_evidence.rb +44 -0
- data/lib/archunit/common/extraction/edge.rb +7 -2
- data/lib/archunit/common/extraction/import_kind.rb +2 -1
- data/lib/archunit/common/extraction/parse_diagnostic.rb +23 -0
- data/lib/archunit/common/fluentapi/check_options.rb +43 -2
- data/lib/archunit/common/fluentapi/checkable.rb +25 -1
- data/lib/archunit/common/logging/check_logger.rb +8 -0
- data/lib/archunit/common/logging/inspection.rb +59 -0
- data/lib/archunit/common/logging/logging_options.rb +5 -0
- data/lib/archunit/common/pattern.rb +18 -8
- data/lib/archunit/common/projection/cycles/johnson_cycles.rb +35 -19
- data/lib/archunit/common/projection/cycles/tarjan_scc.rb +30 -10
- data/lib/archunit/error/source_parse_error.rb +38 -0
- data/lib/archunit/extraction/arch_ignore.rb +91 -0
- data/lib/archunit/extraction/conservative_local_flow.rb +100 -0
- data/lib/archunit/extraction/constant_index.rb +110 -0
- data/lib/archunit/extraction/constant_visitor.rb +129 -0
- data/lib/archunit/extraction/discover_gemspec_load_paths.rb +96 -0
- data/lib/archunit/extraction/enumerate_source_files.rb +23 -8
- data/lib/archunit/extraction/extract_dependencies.rb +82 -14
- data/lib/archunit/extraction/extract_graph.rb +56 -12
- data/lib/archunit/extraction/extract_imports.rb +60 -124
- data/lib/archunit/extraction/extraction_profile.rb +105 -0
- data/lib/archunit/extraction/import_resolver.rb +56 -0
- data/lib/archunit/extraction/import_visitor.rb +104 -0
- data/lib/archunit/extraction/local_string_tracker.rb +80 -0
- data/lib/archunit/extraction/resolve_import.rb +5 -3
- data/lib/archunit/extraction/resolve_load_paths.rb +93 -0
- data/lib/archunit/extraction/resolved_import.rb +5 -2
- data/lib/archunit/extraction/validate_extraction_paths.rb +37 -0
- data/lib/archunit/files/extraction/extract_file_info.rb +14 -5
- data/lib/archunit/files/extraction/file_info.rb +1 -2
- data/lib/archunit/files/fluentapi/depend_on_external_module_condition.rb +2 -0
- data/lib/archunit/files/fluentapi/depend_on_external_module_condition_builder.rb +2 -0
- data/lib/archunit/files/fluentapi/depend_on_file_condition.rb +6 -0
- data/lib/archunit/files/fluentapi/depend_on_file_condition_builder.rb +6 -0
- data/lib/archunit/files/fluentapi/file_condition_builder.rb +12 -0
- data/lib/archunit/files/fluentapi/file_rule_support.rb +8 -1
- data/lib/archunit/files/fluentapi/files.rb +6 -0
- data/lib/archunit/files/fluentapi/match_pattern_file_condition_builder.rb +14 -0
- data/lib/archunit/files/fluentapi/positive_match_pattern_file_condition_builder.rb +2 -0
- data/lib/archunit/graph/fluentapi/graph.rb +5 -0
- data/lib/archunit/graph/fluentapi/project_graph_builder.rb +44 -0
- data/lib/archunit/graph/projection/aggregate_edges.rb +4 -2
- data/lib/archunit/graph/projection/graph_query_options.rb +1 -2
- data/lib/archunit/graph/projection/graph_report_edge.rb +12 -2
- data/lib/archunit/graph/rendering/json_renderer.rb +3 -1
- data/lib/archunit/layers/fluentapi/layer_definition_builder.rb +4 -0
- data/lib/archunit/layers/fluentapi/layer_dependency_rule_builder.rb +4 -0
- data/lib/archunit/layers/fluentapi/layered_architecture.rb +5 -0
- data/lib/archunit/layers/fluentapi/layers.rb +5 -0
- data/lib/archunit/metrics/extraction/extract_distance_info.rb +1 -2
- data/lib/archunit/metrics/extraction/extract_project_info.rb +6 -6
- data/lib/archunit/metrics/extraction/metric_info.rb +2 -4
- data/lib/archunit/metrics/extraction/source_metrics_visitor.rb +2 -4
- data/lib/archunit/metrics/fluentapi/custom_metric_builder.rb +2 -0
- data/lib/archunit/metrics/fluentapi/custom_metric_condition.rb +4 -1
- data/lib/archunit/metrics/fluentapi/logged_metric.rb +23 -0
- data/lib/archunit/metrics/fluentapi/metric_predicate_condition.rb +4 -1
- data/lib/archunit/metrics/fluentapi/metric_report_builder.rb +2 -0
- data/lib/archunit/metrics/fluentapi/metric_selection.rb +18 -2
- data/lib/archunit/metrics/fluentapi/metric_threshold_condition.rb +2 -1
- data/lib/archunit/metrics/fluentapi/metrics.rb +5 -0
- data/lib/archunit/metrics/fluentapi/metrics_builder.rb +15 -0
- data/lib/archunit/slices/fluentapi/diagram_slice_condition.rb +1 -0
- data/lib/archunit/slices/fluentapi/forbidden_slice_dependency_condition.rb +1 -0
- data/lib/archunit/slices/fluentapi/negative_slice_condition_builder.rb +2 -0
- data/lib/archunit/slices/fluentapi/positive_slice_condition_builder.rb +8 -0
- data/lib/archunit/slices/fluentapi/slice_scope_builder.rb +12 -0
- data/lib/archunit/slices/fluentapi/slices.rb +5 -0
- data/lib/archunit/testing/assert_passes.rb +4 -0
- data/lib/archunit/testing/result_factory.rb +16 -4
- data/lib/archunit/testing/rspec_adapter.rb +1 -2
- data/lib/archunit/testing.rb +7 -3
- data/lib/archunit/version.rb +1 -1
- data/lib/archunit.rb +9 -0
- metadata +20 -2
data/README.md
CHANGED
|
@@ -1,66 +1,85 @@
|
|
|
1
|
-
# ArchUnitRuby
|
|
1
|
+
# ArchUnitRuby - Architecture Testing
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
<p align="center" name="top">
|
|
4
|
+
<img src="assets/logo-rounded.png" width="180" alt="ArchUnitRuby logo">
|
|
5
|
+
</p>
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg"></a>
|
|
9
|
+
<a href="https://github.com/LukasNiessen/ArchUnitRuby/actions/workflows/ci.yml"><img alt="Build & tests" src="https://img.shields.io/github/actions/workflow/status/LukasNiessen/ArchUnitRuby/ci.yml?branch=main&label=build%20%26%20tests"></a>
|
|
10
|
+
<a href="https://github.com/LukasNiessen/ArchUnitRuby"><img alt="GitHub stars" src="https://img.shields.io/github/stars/LukasNiessen/ArchUnitRuby.svg"></a><br>
|
|
11
|
+
<a href="https://clickgems.clickhouse.com/dashboard/archunit"><img alt="Gem downloads" src="https://img.shields.io/gem/dt/archunit.svg"></a>
|
|
12
|
+
<a href="https://www.ruby-lang.org/"><img alt="Ruby 3.3+" src="https://img.shields.io/badge/Ruby-3.3%2B-CC342D?logo=ruby&logoColor=white"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
Enforce architecture rules in Ruby projects. Check dependency directions, detect circular
|
|
16
|
+
dependencies, enforce naming and location conventions, measure code quality, and generate
|
|
17
|
+
architecture reports as ordinary Ruby tests.
|
|
18
|
+
|
|
19
|
+
_Inspired by the amazing ArchUnit library, but not affiliated with ArchUnit._
|
|
20
|
+
|
|
21
|
+
[Setup](#-setup) · [Use Cases](#-use-cases) · [Features](#-features) · [API Guide](https://lukasniessen.github.io/ArchUnitRuby/file.API.html) · [Documentation](https://lukasniessen.github.io/ArchUnitRuby/) · [Contributing](CONTRIBUTING.md)
|
|
13
22
|
|
|
14
23
|
ArchUnitRuby turns a Ruby codebase into a dependency graph and lets you test that graph with rules
|
|
15
24
|
that read like English:
|
|
16
25
|
|
|
17
|
-
|
|
26
|
+
~~~ ruby
|
|
18
27
|
ArchUnit.project_files
|
|
19
28
|
.in_folder('app/api/**')
|
|
20
29
|
.should_not.depend_on_files
|
|
21
30
|
.in_folder('app/database/**')
|
|
22
|
-
|
|
31
|
+
~~~
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
is tested on Ruby 3.3, 3.4, and 4.0 on Linux and Ruby 4.0 on Windows.
|
|
26
|
-
[`archunit`](https://rubygems.org/gems/archunit)
|
|
33
|
+
ArchUnitRuby provides file, layer, slice, graph-reporting, and metric APIs with a documented 1.0
|
|
34
|
+
compatibility baseline. It is tested on Ruby 3.3, 3.4, and 4.0 on Linux and Ruby 4.0 on Windows.
|
|
35
|
+
The gem is named [`archunit`](https://rubygems.org/gems/archunit); check RubyGems for published-version
|
|
36
|
+
availability, since a development checkout can be ahead of the published gem.
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
[
|
|
38
|
+
ArchUnitRuby follows the shared ArchUnitEverything architecture and fluent-API standard used by
|
|
39
|
+
its siblings [ArchUnitTS](https://github.com/LukasNiessen/ArchUnitTS) and
|
|
40
|
+
[ArchUnitPython](https://github.com/LukasNiessen/ArchUnitPython), adapted where Ruby idioms differ.
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
The [documentation site](https://lukasniessen.github.io/ArchUnitRuby/) combines this guide, a
|
|
43
|
+
curated [API guide](https://lukasniessen.github.io/ArchUnitRuby/file.API.html), and a searchable,
|
|
44
|
+
source-generated reference. The same site is rebuilt and link-validated in CI before deployment
|
|
45
|
+
from `main`, so the published reference follows the repository.
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
searchable, source-generated API reference for every public module, class, and method. The same
|
|
35
|
-
site is rebuilt in CI and deployed from `main`, so the published reference follows the repository.
|
|
47
|
+
## ⚡ 5 min Quickstart
|
|
36
48
|
|
|
37
|
-
|
|
49
|
+
### Installation
|
|
38
50
|
|
|
39
|
-
ArchUnitRuby requires Ruby 3.3 or newer. Add it to your
|
|
51
|
+
ArchUnitRuby requires Ruby 3.3 or newer. These instructions target the 1.0 series. Add it to your
|
|
52
|
+
test dependencies once the target version is published:
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
~~~ ruby
|
|
42
55
|
# Gemfile
|
|
43
56
|
group :test do
|
|
44
|
-
gem 'archunit', '~> 0.0
|
|
57
|
+
gem 'archunit', '~> 1.0.0'
|
|
45
58
|
end
|
|
46
|
-
|
|
59
|
+
~~~
|
|
47
60
|
|
|
48
61
|
Then install it:
|
|
49
62
|
|
|
50
|
-
|
|
63
|
+
~~~ bash
|
|
51
64
|
bundle install
|
|
52
|
-
|
|
65
|
+
~~~
|
|
66
|
+
|
|
67
|
+
Or install it directly with `gem install archunit --version '~> 1.0.0'`.
|
|
53
68
|
|
|
54
|
-
|
|
69
|
+
[ClickGems download analytics](https://clickgems.clickhouse.com/dashboard/archunit) show trends by
|
|
70
|
+
date, gem version, Ruby version, system, and country. Download totals count package fetches rather
|
|
71
|
+
than unique users, so repeated installs and automated clients may contribute to the total. The
|
|
72
|
+
[RubyGems API](https://rubygems.org/api/v1/gems/archunit.json) provides the
|
|
73
|
+
current raw count and ClickGems provides the historical breakdown.
|
|
55
74
|
|
|
56
75
|
RSpec and Minitest integrations are optional; ArchUnitRuby does not install either test framework
|
|
57
76
|
for you.
|
|
58
77
|
|
|
59
|
-
|
|
78
|
+
### Add tests
|
|
60
79
|
|
|
61
80
|
Create `spec/architecture_spec.rb`:
|
|
62
81
|
|
|
63
|
-
|
|
82
|
+
~~~ ruby
|
|
64
83
|
require 'archunit'
|
|
65
84
|
|
|
66
85
|
RSpec.describe 'architecture' do
|
|
@@ -71,23 +90,83 @@ RSpec.describe 'architecture' do
|
|
|
71
90
|
expect(rule).to pass
|
|
72
91
|
end
|
|
73
92
|
end
|
|
74
|
-
|
|
93
|
+
~~~
|
|
75
94
|
|
|
76
95
|
Run it like any other specification:
|
|
77
96
|
|
|
78
|
-
|
|
97
|
+
~~~ bash
|
|
79
98
|
bundle exec rspec spec/architecture_spec.rb
|
|
80
|
-
|
|
99
|
+
~~~
|
|
100
|
+
|
|
101
|
+
### CI Integration
|
|
102
|
+
|
|
103
|
+
Architecture specifications run with the rest of the test suite, so no dedicated CI integration
|
|
104
|
+
is required:
|
|
105
|
+
|
|
106
|
+
~~~ yaml
|
|
107
|
+
# GitHub Actions
|
|
108
|
+
- name: Run architecture tests
|
|
109
|
+
run: bundle exec rspec spec/architecture_spec.rb
|
|
110
|
+
~~~
|
|
111
|
+
|
|
112
|
+
## 🚐 Setup
|
|
113
|
+
|
|
114
|
+
### Explain why a rule exists
|
|
115
|
+
|
|
116
|
+
Append `.because(...)` to a completed file, layer, slice, or metric rule:
|
|
117
|
+
|
|
118
|
+
~~~ ruby
|
|
119
|
+
rule = ArchUnit.project_files.in_folder('app/api/**')
|
|
120
|
+
.should_not.depend_on_files.in_folder('app/database/**')
|
|
121
|
+
.because('Keep persistence out of the API')
|
|
122
|
+
|
|
123
|
+
expect(rule).to pass
|
|
124
|
+
~~~
|
|
125
|
+
|
|
126
|
+
The explanation appears once in a failed RSpec, Minitest, or `ArchUnit.assert_passes` assertion.
|
|
127
|
+
It is available as `rule.rationale`. Calling `.because(...)` returns a distinct frozen rule;
|
|
128
|
+
the original rule and direct `check` results are unchanged. Reasons must be non-blank strings.
|
|
129
|
+
Add the explanation after all selectors and modifiers.
|
|
130
|
+
|
|
131
|
+
### Project Location
|
|
81
132
|
|
|
82
133
|
The project locator is optional. With no argument, ArchUnitRuby searches from the current directory
|
|
83
134
|
for a `Gemfile` or gemspec. Pass a directory or either marker file when analyzing another project:
|
|
84
135
|
|
|
85
|
-
|
|
136
|
+
~~~ ruby
|
|
86
137
|
ArchUnit.project_files('/workspace/my_app')
|
|
87
138
|
ArchUnit.project_files('/workspace/my_app/Gemfile')
|
|
88
|
-
|
|
139
|
+
~~~
|
|
89
140
|
|
|
90
|
-
|
|
141
|
+
Source discovery skips symlinked files and directories, including project-local aliases. This
|
|
142
|
+
avoids duplicate file identities and unintentionally parsing linked source outside the project.
|
|
143
|
+
Direct file-information extraction also checks the canonical source path against the project root.
|
|
144
|
+
|
|
145
|
+
### Project-wide exclusions
|
|
146
|
+
|
|
147
|
+
Add a `.archignore` at the detected project root to keep generated or intentionally out-of-scope
|
|
148
|
+
Ruby files out of dependency graphs and file-based metrics:
|
|
149
|
+
|
|
150
|
+
~~~ text
|
|
151
|
+
# Architecture exclusions (not a full gitignore parser)
|
|
152
|
+
/db/migrate/
|
|
153
|
+
generated/
|
|
154
|
+
**/*_generated.rb
|
|
155
|
+
~~~
|
|
156
|
+
|
|
157
|
+
Blank lines and full-line `#` comments are ignored. Bare names match at any directory depth;
|
|
158
|
+
patterns containing `/` are project-relative, and a leading `/` anchors a pattern to the root.
|
|
159
|
+
A trailing `/` matches directories and their descendants, not similarly named files. Windows
|
|
160
|
+
separators are normalized. Only the root file is read; nested `.archignore` files are not inherited.
|
|
161
|
+
|
|
162
|
+
These patterns combine with the built-in exclusions or explicit `exclude_patterns:`; `[]` disables
|
|
163
|
+
the defaults, not `.archignore`. Effective pattern changes automatically select a fresh cached graph.
|
|
164
|
+
Source-code edits still require `clear_cache: true` in a long-lived process. The file is limited to
|
|
165
|
+
1 MiB, must be a regular non-symlink file, and is decoded as UTF-8 with invalid bytes replaced.
|
|
166
|
+
Negation (`!pattern`), NUL characters, and a bare `/` are rejected with `ArchUnit::UserError`.
|
|
167
|
+
No configuration or application code is executed.
|
|
168
|
+
|
|
169
|
+
### Fluent Grammar
|
|
91
170
|
|
|
92
171
|
Every rule is built left to right from the same small grammar:
|
|
93
172
|
|
|
@@ -103,12 +182,12 @@ Every rule is built left to right from the same small grammar:
|
|
|
103
182
|
Building a rule is lazy and does not scan the filesystem. `check`, `measure`, snapshot/report
|
|
104
183
|
terminals, and export terminals perform the work. Builders are immutable, so a scope can be reused:
|
|
105
184
|
|
|
106
|
-
|
|
185
|
+
~~~ ruby
|
|
107
186
|
services = ArchUnit.project_files.in_folder('app/services/**')
|
|
108
187
|
|
|
109
188
|
cycle_rule = services.should.have_no_cycles
|
|
110
189
|
database_rule = services.should_not.depend_on_files.in_folder('app/database/**')
|
|
111
|
-
|
|
190
|
+
~~~
|
|
112
191
|
|
|
113
192
|
String patterns are anchored globs. `*` stays inside one path segment, `**` crosses directories,
|
|
114
193
|
and `?` matches one non-separator character. Most selectors also accept regular expressions;
|
|
@@ -118,16 +197,18 @@ Paths are project-relative and normalized to `/` separators.
|
|
|
118
197
|
A scope matching zero files returns `EmptyTestViolation`; it does not silently pass. Opt out only
|
|
119
198
|
when an empty result is genuinely valid:
|
|
120
199
|
|
|
121
|
-
|
|
200
|
+
~~~ ruby
|
|
122
201
|
rule.check(ArchUnit::CheckOptions.new(allow_empty_tests: true))
|
|
123
|
-
|
|
202
|
+
~~~
|
|
203
|
+
|
|
204
|
+
## 🐣 Features
|
|
124
205
|
|
|
125
|
-
|
|
206
|
+
### File Rules
|
|
126
207
|
|
|
127
208
|
File rules cover cycles, naming, location, internal dependencies, external modules, and custom
|
|
128
209
|
source predicates:
|
|
129
210
|
|
|
130
|
-
|
|
211
|
+
~~~ ruby
|
|
131
212
|
rules = [
|
|
132
213
|
ArchUnit.project_files.in_path('lib/**/*.rb').should.have_no_cycles,
|
|
133
214
|
ArchUnit.project_files.in_folder('app/services/**')
|
|
@@ -137,24 +218,24 @@ rules = [
|
|
|
137
218
|
]
|
|
138
219
|
|
|
139
220
|
rules.each { |rule| ArchUnit.assert_passes(rule) }
|
|
140
|
-
|
|
221
|
+
~~~
|
|
141
222
|
|
|
142
223
|
A custom predicate receives an immutable `FileInfo` with `path`, `name`, `extension`, `directory`,
|
|
143
224
|
complete `content`, and non-blank `lines_of_code`:
|
|
144
225
|
|
|
145
|
-
|
|
226
|
+
~~~ ruby
|
|
146
227
|
rule = ArchUnit.project_files.in_folder('app/services/**')
|
|
147
228
|
.should.adhere_to(
|
|
148
229
|
->(file) { file.lines_of_code < 300 },
|
|
149
230
|
'services must stay below 300 non-blank lines'
|
|
150
231
|
)
|
|
151
|
-
|
|
232
|
+
~~~
|
|
152
233
|
|
|
153
|
-
|
|
234
|
+
### Layer Dependencies
|
|
154
235
|
|
|
155
236
|
Named layers express an allowlist or blocklist over groups of files:
|
|
156
237
|
|
|
157
|
-
|
|
238
|
+
~~~ ruby
|
|
158
239
|
rule = ArchUnit.project_layers
|
|
159
240
|
.layer('api').defined_by('app/api/**/*.rb')
|
|
160
241
|
.layer('services').defined_by('app/services/**/*.rb')
|
|
@@ -164,44 +245,44 @@ rule = ArchUnit.project_layers
|
|
|
164
245
|
.where_layer('database').may_only_depend_on_layers
|
|
165
246
|
|
|
166
247
|
expect(rule).to pass
|
|
167
|
-
|
|
248
|
+
~~~
|
|
168
249
|
|
|
169
250
|
Dependencies within one layer are always allowed. Edges with an unassigned endpoint are ignored.
|
|
170
251
|
Calling `may_only_depend_on_layers` without targets seals a layer; `may_not_depend_on_layers`
|
|
171
252
|
requires at least one forbidden target.
|
|
172
253
|
|
|
173
|
-
|
|
254
|
+
### Architecture Slices and PlantUML
|
|
174
255
|
|
|
175
256
|
Slices group files by one captured path segment and preserve every concrete dependency as evidence:
|
|
176
257
|
|
|
177
|
-
|
|
258
|
+
~~~ ruby
|
|
178
259
|
slices = ArchUnit.project_slices.defined_by('lib/my_app/(**)/')
|
|
179
260
|
rule = slices.should_not.contain_dependency('api', 'database')
|
|
180
261
|
|
|
181
262
|
expect(rule).to pass
|
|
182
|
-
|
|
263
|
+
~~~
|
|
183
264
|
|
|
184
265
|
`(**)` is the slice capture. `defined_by_regex` uses the first regular-expression capture instead.
|
|
185
266
|
|
|
186
267
|
A checked-in PlantUML component diagram can also be the architecture contract:
|
|
187
268
|
|
|
188
|
-
|
|
269
|
+
~~~ ruby
|
|
189
270
|
rule = slices.should
|
|
190
271
|
.ignoring_external_slices
|
|
191
272
|
.adhere_to_diagram_in_file('docs/architecture.puml')
|
|
192
273
|
|
|
193
274
|
expect(rule).to pass
|
|
194
|
-
|
|
275
|
+
~~~
|
|
195
276
|
|
|
196
277
|
The supported subset recognizes components, directed dependencies, comments, and `@startuml` /
|
|
197
278
|
`@enduml`. Use `to_plantuml` or `export_as_plantuml(path)` to generate a diagram from the real graph.
|
|
198
279
|
|
|
199
|
-
|
|
280
|
+
### Dependency Graph Reports
|
|
200
281
|
|
|
201
282
|
Graph reporting builds one immutable snapshot and renders it consistently as DOT, Mermaid, D2, CSV,
|
|
202
283
|
JSON, or self-contained HTML:
|
|
203
284
|
|
|
204
|
-
|
|
285
|
+
~~~ ruby
|
|
205
286
|
report = ArchUnit.project_graph
|
|
206
287
|
.include_external_dependencies
|
|
207
288
|
.focus_on('app/services/**', 2)
|
|
@@ -210,17 +291,17 @@ report = ArchUnit.project_graph
|
|
|
210
291
|
|
|
211
292
|
puts report.summary.node_count
|
|
212
293
|
report.export_as_html('reports/services.html')
|
|
213
|
-
|
|
294
|
+
~~~
|
|
214
295
|
|
|
215
296
|
Queries include `focus_on`, `reachable_from`, and `dependents_of`. Collapse by folder depth or a
|
|
216
297
|
regular-expression replacement. Every format has an in-memory `to_<format>` and an
|
|
217
298
|
`export_as_<format>(path)` terminal.
|
|
218
299
|
|
|
219
|
-
|
|
300
|
+
### Code Metrics
|
|
220
301
|
|
|
221
302
|
Metric scopes select files and Ruby classes before measurement or assertion:
|
|
222
303
|
|
|
223
|
-
|
|
304
|
+
~~~ ruby
|
|
224
305
|
services = ArchUnit.metrics
|
|
225
306
|
.in_path('app/services/**/*.rb')
|
|
226
307
|
.for_classes_matching('*Service')
|
|
@@ -230,7 +311,7 @@ cohesion_rule = services.lcom.lcom4.should_be(1)
|
|
|
230
311
|
distance_rule = services.distance.instability.should_be_below(0.8)
|
|
231
312
|
|
|
232
313
|
[size_rule, cohesion_rule, distance_rule].each { |rule| ArchUnit.assert_passes(rule) }
|
|
233
|
-
|
|
314
|
+
~~~
|
|
234
315
|
|
|
235
316
|
Count metrics cover class methods and fields plus file lines, statements, imports, classes, and
|
|
236
317
|
top-level functions. Cohesion includes LCOM96a, LCOM96b, LCOM1-5, and LCOM*. Dependency-derived
|
|
@@ -240,61 +321,63 @@ distance. Zone guards detect the conventional zones of pain and uselessness.
|
|
|
240
321
|
Use `measure` for immutable numeric results, `custom_metric` for a calculation over `ClassInfo`, and
|
|
241
322
|
`export_as_html` for an offline metrics report:
|
|
242
323
|
|
|
243
|
-
|
|
324
|
+
~~~ ruby
|
|
244
325
|
services.count.export_as_html('reports/service-counts')
|
|
245
|
-
|
|
326
|
+
~~~
|
|
246
327
|
|
|
247
328
|
The threshold vocabulary is intentionally limited to `should_be_below`, `should_be_above`,
|
|
248
329
|
`should_be`, `should_be_below_or_equal`, `should_be_above_or_equal`, and `should_satisfy`.
|
|
249
330
|
|
|
250
|
-
|
|
331
|
+
### Pattern Exclusions
|
|
251
332
|
|
|
252
333
|
Every selector accepts `except:` in the same call. A plain pattern or array uses the parent
|
|
253
334
|
selector's context, including filenames for path and folder selectors:
|
|
254
335
|
|
|
255
|
-
|
|
336
|
+
~~~ ruby
|
|
256
337
|
scope = ArchUnit.project_files.in_path(
|
|
257
338
|
'app/**/*.rb',
|
|
258
339
|
except: ['app/generated/**', 'schema.rb']
|
|
259
340
|
)
|
|
260
|
-
|
|
341
|
+
~~~
|
|
261
342
|
|
|
262
343
|
Use explicit targets when needed. Supported keys are `in_path`, `in_folder`, `with_name`, and
|
|
263
344
|
`for_classes_matching`:
|
|
264
345
|
|
|
265
|
-
|
|
346
|
+
~~~ ruby
|
|
266
347
|
scope = ArchUnit.metrics.in_path(
|
|
267
348
|
'app/**/*.rb',
|
|
268
349
|
except: { in_folder: 'app/generated', with_name: '*_spec.rb' }
|
|
269
350
|
)
|
|
270
|
-
|
|
351
|
+
~~~
|
|
271
352
|
|
|
272
|
-
##
|
|
353
|
+
## 🐹 Use Cases
|
|
354
|
+
|
|
355
|
+
### Results and Test Frameworks
|
|
273
356
|
|
|
274
357
|
`check` returns an array of structured violations. Architecture disagreement is data, not an
|
|
275
358
|
exception:
|
|
276
359
|
|
|
277
|
-
|
|
360
|
+
~~~ ruby
|
|
278
361
|
violations = rule.check
|
|
279
362
|
violations.each { |violation| puts violation.class }
|
|
280
|
-
|
|
363
|
+
~~~
|
|
281
364
|
|
|
282
365
|
Translate that result into a test failure at the boundary that suits your suite:
|
|
283
366
|
|
|
284
|
-
|
|
367
|
+
~~~ ruby
|
|
285
368
|
expect(rule).to pass # RSpec
|
|
286
369
|
assert_passes(rule) # Minitest test case
|
|
287
370
|
ArchUnit.assert_passes(rule) # Framework-neutral
|
|
288
|
-
|
|
371
|
+
~~~
|
|
289
372
|
|
|
290
373
|
`ArchUnit.format_violations` and `ResultFactory` provide stable human-readable output. All
|
|
291
374
|
violations retain the concrete dependency, file, layer, slice, or metric evidence that caused them.
|
|
292
375
|
|
|
293
|
-
##
|
|
376
|
+
## 📝 Debug Logging & Configuration
|
|
294
377
|
|
|
295
378
|
Logging is off by default and belongs to one check; there is no process-global configuration:
|
|
296
379
|
|
|
297
|
-
|
|
380
|
+
~~~ ruby
|
|
298
381
|
logging = ArchUnit::LoggingOptions.new(
|
|
299
382
|
level: :debug,
|
|
300
383
|
output_directory: 'tmp/archunit-logs',
|
|
@@ -302,13 +385,54 @@ logging = ArchUnit::LoggingOptions.new(
|
|
|
302
385
|
)
|
|
303
386
|
|
|
304
387
|
violations = rule.check(ArchUnit::CheckOptions.new(logging: logging))
|
|
305
|
-
|
|
388
|
+
~~~
|
|
306
389
|
|
|
307
390
|
Levels are `debug`, `info`, `warn`, and `error`. The fixed events cover check start/end, progress,
|
|
308
391
|
violations, and metric evidence. `io:` defaults to `$stderr`, accepts any writable stream, and may be
|
|
309
392
|
`nil`. File output creates missing directories and writes timestamped `archunit-*.log` files.
|
|
310
393
|
|
|
311
|
-
|
|
394
|
+
At `:debug`, inspection includes the project root, each discovered file, every dependency with
|
|
395
|
+
its import kind and external flag, selected files, and all metric calculations (including passing
|
|
396
|
+
values). It observes the same graph on both cold and cached checks. Custom metrics run once.
|
|
397
|
+
Check options and returned violations retain their existing behavior.
|
|
398
|
+
|
|
399
|
+
| Level | Included output |
|
|
400
|
+
| --- | --- |
|
|
401
|
+
| `:debug` | Graph inspection, selections, metric values, and everything below |
|
|
402
|
+
| `:info` | Check lifecycle and progress, violations, technical errors |
|
|
403
|
+
| `:warn` | Violations and technical errors |
|
|
404
|
+
| `:error` | Technical errors |
|
|
405
|
+
|
|
406
|
+
Example debug lines (timestamp prefixes omitted):
|
|
407
|
+
|
|
408
|
+
~~~ text
|
|
409
|
+
[DEBUG] inspect: discovered file: "lib/service.rb"
|
|
410
|
+
[DEBUG] inspect: dependency: "lib/service.rb" -> "json" (external=true, kinds=[:require])
|
|
411
|
+
[DEBUG] inspect: selected file: "lib/service.rb"
|
|
412
|
+
[DEBUG] log metric: method_count=3 [lib/service.rb:Service]
|
|
413
|
+
~~~
|
|
414
|
+
|
|
415
|
+
For file-only CI artifacts, use `io: nil` with `output_directory:` and archive that directory.
|
|
416
|
+
Logging sink errors still propagate. Debug details are produced lazily; checks with logging
|
|
417
|
+
disabled create no output or log files.
|
|
418
|
+
|
|
419
|
+
### Readable, colored failure reports
|
|
420
|
+
|
|
421
|
+
The same formatted, numbered reports work with RSpec, Minitest, or standalone checks:
|
|
422
|
+
|
|
423
|
+
~~~ ruby
|
|
424
|
+
puts ArchUnit.format_violations(violations) # Detect terminal support
|
|
425
|
+
puts ArchUnit.format_violations(violations, color: true) # Force ANSI colors
|
|
426
|
+
puts ArchUnit.format_violations(violations, color: false) # Plain CI/file output
|
|
427
|
+
~~~
|
|
428
|
+
|
|
429
|
+
Failure headings are bold red, violation headings yellow, and successful results green. Reports
|
|
430
|
+
include dependency, file, or metric evidence. Automatic color detection respects `NO_COLOR`,
|
|
431
|
+
`TERM=dumb`, and non-terminal output. Log files remain plain text for searching and CI artifacts.
|
|
432
|
+
|
|
433
|
+
## 🕵️ Technical Deep Dive
|
|
434
|
+
|
|
435
|
+
### What ArchUnitRuby Extracts
|
|
312
436
|
|
|
313
437
|
ArchUnitRuby uses Prism and statically recognizes:
|
|
314
438
|
|
|
@@ -323,17 +447,121 @@ Project dependencies use normalized, project-relative paths. Standard-library an
|
|
|
323
447
|
retain the module name written in source. Inline or immediately preceding ignore directives can
|
|
324
448
|
suppress known compatibility imports:
|
|
325
449
|
|
|
326
|
-
|
|
450
|
+
~~~ ruby
|
|
327
451
|
require 'legacy/client' # archunit: ignore legacy/client
|
|
328
452
|
|
|
329
453
|
# archunit: ignore experimental/plugin
|
|
330
454
|
require 'experimental/plugin'
|
|
331
|
-
|
|
455
|
+
~~~
|
|
456
|
+
|
|
457
|
+
Preceding local assignments to literal strings are also recognized for all four import forms:
|
|
458
|
+
|
|
459
|
+
~~~ ruby
|
|
460
|
+
path = '../retrieval/store'
|
|
461
|
+
require_relative path
|
|
462
|
+
~~~
|
|
463
|
+
|
|
464
|
+
Inference follows assignment order within straight-line lexical scopes. Reassignment replaces the
|
|
465
|
+
value; aliases, mutation, unknown calls receiving the value, reflection, and unsupported control
|
|
466
|
+
flow invalidate it. Values are not carried across branches, loops, exception handlers, or captured
|
|
467
|
+
blocks. Once a closure or reflective binding can access an enclosing scope, local inference remains
|
|
468
|
+
disabled for that scope. Independent literal assignments inside a method or block can still resolve. Runtime values
|
|
469
|
+
such as `ENV.fetch('PLUGIN_PATH')`, interpolation, and method results remain unresolved. No source,
|
|
470
|
+
Gemfile, gemspec, initializer, or `const_missing` hook is executed.
|
|
471
|
+
|
|
472
|
+
### Constant References (Opt-in)
|
|
473
|
+
|
|
474
|
+
For autoloaded applications, enable dependencies from known project constants alongside imports:
|
|
475
|
+
|
|
476
|
+
~~~ ruby
|
|
477
|
+
options = ArchUnit::CheckOptions.new(dependency_analysis: :imports_and_constants)
|
|
478
|
+
|
|
479
|
+
# Detects Retrieval::Store.new even without require_relative.
|
|
480
|
+
rule = ArchUnit.project_files
|
|
481
|
+
.in_path('app/api/**')
|
|
482
|
+
.should_not.depend_on_files.in_path('app/retrieval/**')
|
|
483
|
+
violations = rule.check(options)
|
|
484
|
+
|
|
485
|
+
report = ArchUnit.project_graph.with_check_options(options)
|
|
486
|
+
puts report.to_json
|
|
487
|
+
puts ArchUnit.project_slices.defined_by('app/(**)/').to_plantuml(options)
|
|
488
|
+
values = ArchUnit.metrics.distance.instability.measure(options)
|
|
489
|
+
~~~
|
|
490
|
+
|
|
491
|
+
The default remains `dependency_analysis: :imports`. Expanded mode indexes declarations from the
|
|
492
|
+
same included source files, then resolves references to definition files. Files remain graph nodes.
|
|
493
|
+
It supports lexical nesting, compact namespaces, root-qualified names, and explicit superclass,
|
|
494
|
+
`include`, `prepend`, and `extend` references. Nested `module A; module B` includes `A` in lexical
|
|
495
|
+
lookup; top-level `module A::B` does not. A nearer namespace shadows a root namespace even when its
|
|
496
|
+
requested child is missing.
|
|
497
|
+
|
|
498
|
+
Ordinary `=` and conditional `||=` constant declarations are indexed, including qualified and
|
|
499
|
+
root-qualified forms. A conditional declaration records a possible definition file, not proof that
|
|
500
|
+
the assignment executes or its resulting value. Multiple candidate files stay ambiguous. Other
|
|
501
|
+
compound writes (`&&=`, operators) and multiple-assignment declarations are not inferred as new
|
|
502
|
+
definition owners; unsupported targets remain unresolved rather than inventing ownership.
|
|
503
|
+
|
|
504
|
+
References with multiple definition files (reopened classes/modules), missing definitions, and
|
|
505
|
+
dynamic definitions are omitted, never assigned to an arbitrary file. `Alias = Original` defines
|
|
506
|
+
`Alias` in its own file and records the reference to `Original`; lookup through aliases such as
|
|
507
|
+
`Alias::Child` is not followed. Inherited constant members and singleton-class lexical lookup are
|
|
508
|
+
not modeled. Unqualified root fallback is conservatively suppressed in scopes with an explicit
|
|
509
|
+
superclass, mixins, or dynamic constant operations. Use explicit `::Namespace::Constant` references
|
|
510
|
+
or import rules where those limits matter. Static analysis does not establish runtime load order
|
|
511
|
+
or whether a conditional use executes. Qualified declaration owners are stabilized before lookup;
|
|
512
|
+
if they do not stabilize within eight index passes, the project contributes imports only and its
|
|
513
|
+
constant uses are counted as unresolved.
|
|
514
|
+
|
|
515
|
+
In expanded mode, an import and a constant use between the same files produce one logical edge.
|
|
516
|
+
Its `import_kinds`
|
|
517
|
+
includes `:constant_reference`; `edge.evidence` and JSON graph exports retain the referenced name,
|
|
518
|
+
source span, and definition line for constant uses. Import evidence retains its call span. CSV,
|
|
519
|
+
HTML, and diagram reports use the same merged graph; CSV/HTML distinguish dependency kinds.
|
|
520
|
+
`# archunit: ignore` continues to suppress imports only, not constant references. Import-only mode
|
|
521
|
+
leaves `edge.evidence` empty for compatibility with existing edge-value comparisons;
|
|
522
|
+
`ArchUnit::Extraction.extract_imports` retains call locations in both modes.
|
|
523
|
+
|
|
524
|
+
For diagnostics, pass an `ArchUnit::Extraction::ExtractionProfile` to
|
|
525
|
+
`ArchUnit::Extraction.extract_graph(root, options: options, profile: profile)`. Its counters include
|
|
526
|
+
`unresolved_imports`, `unresolved_constant_references`, and `ambiguous_constant_references`; unknown
|
|
527
|
+
third-party constants do not become architecture violations. These counts describe a cold analysis;
|
|
528
|
+
a cache hit does not rerun diagnostics. Included malformed Ruby now raises
|
|
529
|
+
`ArchUnit::SourceParseError < ArchUnit::TechnicalError` in both modes and in AST metrics, rather
|
|
530
|
+
than silently returning a partial graph or zero counts. Its immutable `diagnostics` contain
|
|
531
|
+
`path`, one-based `line` and `column`, and the parser `message`; the extraction profile increments
|
|
532
|
+
`malformed_files`. Fix or explicitly exclude invalid files using the project source policy.
|
|
533
|
+
Fluent rule selectors do not limit graph discovery. Failed graph builds are not cached; after
|
|
534
|
+
editing previously cached valid source, the normal explicit `clear_cache: true` policy still applies.
|
|
535
|
+
This intentionally changes the 0.0.1 prototype's permissive behavior in the 1.0 API.
|
|
536
|
+
|
|
537
|
+
Expanded analysis uses a bounded Prism index and adds no runtime dependency. The design spike
|
|
538
|
+
considered [RubyDEX](https://github.com/Shopify/rubydex), whose semantic index is broader but adds a
|
|
539
|
+
Rust/native extension and platform packaging requirements. Reusing our existing Prism parser keeps
|
|
540
|
+
the Ruby 3.3/3.4/4.0 Windows/Linux support contract; each included source is parsed once per graph
|
|
541
|
+
build. A later semantic backend can broaden the documented lookup subset. Lookup fixtures follow
|
|
542
|
+
the [Ruby constant-scope documentation](https://docs.ruby-lang.org/en/master/syntax/modules_and_classes_rdoc.html#label-Constants).
|
|
543
|
+
|
|
544
|
+
Repositories containing multiple gems are discovered statically: every `*.gemspec` beneath the
|
|
545
|
+
project root contributes its adjacent existing `lib` directory to feature resolution. Gemspecs are
|
|
546
|
+
never evaluated. Add non-standard source roots explicitly through the per-check options bag; paths
|
|
547
|
+
are relative to the project root, must remain inside it, and use the order given after the normal
|
|
548
|
+
top-level `lib` and project-root search locations:
|
|
549
|
+
|
|
550
|
+
~~~ ruby
|
|
551
|
+
options = ArchUnit::CheckOptions.new(
|
|
552
|
+
load_paths: ['components/billing/source', 'plugins/search/lib']
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
violations = rule.check(options)
|
|
556
|
+
report = ArchUnit.project_graph.with_check_options(options)
|
|
557
|
+
~~~
|
|
332
558
|
|
|
333
|
-
|
|
334
|
-
|
|
559
|
+
Load-path choices and dependency-analysis mode affect graph caching. Equivalent normalized choices
|
|
560
|
+
reuse a cached graph, while a different mode or load-path set builds a separate graph. Use
|
|
561
|
+
`clear_cache: true` after changing files, constant definitions, or gemspec
|
|
562
|
+
layout within one process.
|
|
335
563
|
|
|
336
|
-
##
|
|
564
|
+
## 🐲 Example Repository
|
|
337
565
|
|
|
338
566
|
The [ArchUnitRuby RAG test repository](https://github.com/TristanKruse/ArchUnitRuby-TestRepo-RAG)
|
|
339
567
|
is a small layered retrieval-augmented-generation application with two deliberate architecture
|
|
@@ -344,32 +572,103 @@ ArchUnitRuby also dogfoods itself in `spec/architecture_spec.rb`: `common` is is
|
|
|
344
572
|
modules cannot depend on one another, implementation files cannot depend on the public surface, and
|
|
345
573
|
the complete library graph must remain cycle-free.
|
|
346
574
|
|
|
347
|
-
##
|
|
575
|
+
## 🦊 Contributing
|
|
348
576
|
|
|
349
|
-
|
|
577
|
+
Start with [CONTRIBUTING.md](CONTRIBUTING.md) for the complete development and pull-request guide.
|
|
578
|
+
|
|
579
|
+
~~~ bash
|
|
350
580
|
git clone https://github.com/LukasNiessen/ArchUnitRuby.git
|
|
351
581
|
cd ArchUnitRuby
|
|
352
582
|
bundle install
|
|
353
583
|
bundle exec rake
|
|
354
584
|
bundle exec rake docs
|
|
355
585
|
gem build archunit.gemspec --strict
|
|
356
|
-
|
|
586
|
+
~~~
|
|
357
587
|
|
|
358
|
-
`bundle exec rake` runs the randomized RSpec suite and RuboCop.
|
|
588
|
+
`bundle exec rake` validates release metadata, then runs the randomized RSpec suite and RuboCop.
|
|
589
|
+
CI additionally enforces 98% line
|
|
359
590
|
and 90% branch coverage, runs the dogfooding rules explicitly, builds the documentation, loads the
|
|
360
591
|
public API with Ruby warnings, tests optional RSpec/Minitest integrations, builds and installs the
|
|
361
592
|
gem artifact, runs the external RAG fixture, and checks Ruby 3.3, 3.4, and 4.0 across Ubuntu and
|
|
362
593
|
Windows.
|
|
363
594
|
|
|
595
|
+
Cold and warm extraction can be profiled independently with a generated multi-gem corpus. The
|
|
596
|
+
benchmark reports stage timings, resolution-cache effectiveness, Ruby heap growth, and peak RSS on
|
|
597
|
+
platforms that expose it:
|
|
598
|
+
|
|
599
|
+
~~~ bash
|
|
600
|
+
bundle exec ruby benchmark/extraction.rb
|
|
601
|
+
~~~
|
|
602
|
+
|
|
603
|
+
See the [benchmark guide](https://github.com/LukasNiessen/ArchUnitRuby/blob/main/benchmark/README.md)
|
|
604
|
+
for corpus controls, JSON output, and CI limits.
|
|
605
|
+
|
|
606
|
+
The separate [real-world canary](https://github.com/LukasNiessen/ArchUnitRuby/blob/main/benchmark/canary/README.md)
|
|
607
|
+
checks pinned Rack, Rake, and RSpec production-library sources in both dependency modes, including
|
|
608
|
+
passing and deliberately failing rules, graph baselines, and cold/warm performance. It never runs
|
|
609
|
+
those projects or installs their dependencies, and is not scheduled automatically yet.
|
|
610
|
+
|
|
364
611
|
The implementation conventions and intended dependency directions live in [`AGENTS.md`](AGENTS.md).
|
|
365
612
|
|
|
366
|
-
##
|
|
613
|
+
## 1.0 compatibility and migration
|
|
614
|
+
|
|
615
|
+
The documented entry points, fluent rule methods, options, public data/error types, and report
|
|
616
|
+
formats in the [API guide](https://lukasniessen.github.io/ArchUnitRuby/file.API.html) form the 1.x
|
|
617
|
+
compatibility baseline. Additive capabilities may be introduced in minor versions; incompatible
|
|
618
|
+
public API changes require a new major version and migration notes. Private implementation helpers
|
|
619
|
+
are not part of that contract. Correctness fixes can reveal violations that earlier versions missed;
|
|
620
|
+
review the changelog when updating.
|
|
621
|
+
|
|
622
|
+
When upgrading from 0.0.1:
|
|
367
623
|
|
|
368
|
-
-
|
|
369
|
-
|
|
624
|
+
- The default remains import-only. Explicitly opt into `dependency_analysis: :imports_and_constants`
|
|
625
|
+
when you want the documented static constant-reference model; it may reveal additional violations.
|
|
626
|
+
- Included malformed Ruby now raises `SourceParseError < TechnicalError`, rather than silently
|
|
627
|
+
passing or returning zero AST counts. Fix or explicitly exclude invalid source files.
|
|
628
|
+
- Project `.archignore` exclusions and skipped symlinked source entries can change the discovered
|
|
629
|
+
file set. Empty selections are still violations by default; check selectors rather than blindly
|
|
630
|
+
allowing empty tests.
|
|
631
|
+
- Cached graph results still require explicit clearing after source edits in a long-lived process.
|
|
632
|
+
|
|
633
|
+
Stable API compatibility does not mean complete Ruby interpretation or exhaustive runtime detection.
|
|
634
|
+
The supported static-analysis boundaries below remain part of the contract.
|
|
635
|
+
|
|
636
|
+
## 📅 Plans and Current Limitations
|
|
637
|
+
|
|
638
|
+
- Files are the primary vocabulary; opt-in constant analysis has the lookup limits described above.
|
|
639
|
+
- Runtime-derived import paths cannot be resolved; safely known local literal strings are supported.
|
|
370
640
|
- PlantUML support is a deliberately small component-diagram subset, not a complete UML parser.
|
|
371
|
-
-
|
|
641
|
+
- `.archignore` supports exclusion globs, not negated gitignore patterns or nested ignore files.
|
|
642
|
+
- Cycle traversal is iterative and tested on 10,000-node chains and rings. Enumerating every cycle
|
|
643
|
+
can still be expensive in densely connected graphs; narrow the selected scope when necessary.
|
|
644
|
+
- Method-call/protocol rules, a standalone CLI, presets, and adoption baselines are roadmap work,
|
|
645
|
+
not claims made by the current 1.0 API.
|
|
646
|
+
|
|
647
|
+
## 💟 Community
|
|
648
|
+
|
|
649
|
+
### Maintainers
|
|
650
|
+
|
|
651
|
+
- **[Tristan Kruse](https://github.com/TristanKruse)** — Tech Lead & Maintainer
|
|
652
|
+
- **[Lukas Niessen](https://github.com/LukasNiessen)** — Maintainer
|
|
653
|
+
|
|
654
|
+
### Contributors
|
|
372
655
|
|
|
373
|
-
|
|
656
|
+
See everyone who has contributed on the [GitHub contributors page](https://github.com/LukasNiessen/ArchUnitRuby/graphs/contributors).
|
|
657
|
+
|
|
658
|
+
Questions and feature ideas are welcome in [GitHub Issues](https://github.com/LukasNiessen/ArchUnitRuby/issues).
|
|
659
|
+
For usage help, responsible vulnerability reporting, and community expectations, see
|
|
660
|
+
[SUPPORT.md](SUPPORT.md), [SECURITY.md](SECURITY.md), and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|
661
|
+
|
|
662
|
+
### Star History
|
|
663
|
+
|
|
664
|
+
[](https://star-history.com/#LukasNiessen/ArchUnitRuby&Date)
|
|
665
|
+
|
|
666
|
+
## 📄 License
|
|
374
667
|
|
|
375
668
|
[MIT](LICENSE)
|
|
669
|
+
|
|
670
|
+
<div align="right">
|
|
671
|
+
|
|
672
|
+
[Go back to the top](#top)
|
|
673
|
+
|
|
674
|
+
</div>
|