Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
32233f5
Bump rubocop from 1.89.0 to 1.90.0 in the minor-and-patch group
dependabot[bot] Aug 31, 2026
e30e84a
Complete definition-of-done documentation and branding
TristanKruse Sep 7, 2026
24e9247
Resolve static constant dependencies and local-string imports
TristanKruse Oct 15, 2026
8e5fa99
Preserve import-only edge compatibility and fix CI lint
TristanKruse Oct 15, 2026
cc60de3
Merge remote-tracking branch 'origin/main' into review/dependabot-pr50
TristanKruse Oct 16, 2026
bdd47f3
Adapt suppression directives to RuboCop 1.90
TristanKruse Oct 16, 2026
2da93a6
Sync review PR with merged documentation and diagnostics
TristanKruse Oct 16, 2026
c854b65
Merge branch 'review/dependabot-pr50' into review/dod-pr51
TristanKruse Oct 16, 2026
d9141e8
Merge remote-tracking branch 'origin/main' into fix/issues-54-55-extr…
TristanKruse Oct 16, 2026
894b9fc
Align extraction fixes with merged diagnostics and quality tooling
TristanKruse Oct 16, 2026
fbe5352
Integrate extraction fixes with documentation and quality review base
TristanKruse Oct 16, 2026
ef48253
Keep combined metric documentation within lint limits
TristanKruse Oct 16, 2026
021b5a2
Bump rubocop from 1.89.0 to 1.92.0 in the minor-and-patch group
dependabot[bot] Oct 19, 2026
0d369c8
Merge pull request #56 from LukasNiessen/fix/issues-54-55-extraction
TristanKruse Oct 20, 2026
e57395c
Adapt suppression directives to RuboCop 1.90
TristanKruse Oct 16, 2026
36f662b
Integrate Dependabot RuboCop 1.92 without dropping documentation depe…
TristanKruse Oct 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Bug report
description: Report reproducible incorrect behavior in ArchUnitRuby
title: "[Bug]: "
labels:
- bug
body:
- type: markdown
attributes:
value: >-
Please remove credentials, private source code, and customer data. Security issues must use
private vulnerability reporting instead.
- type: input
id: versions
attributes:
label: Versions
description: ArchUnitRuby version, Ruby version, operating system, and test framework
placeholder: archunit 0.0.1, Ruby 3.4, Ubuntu, RSpec 3.13
validations:
required: true
- type: textarea
id: rule
attributes:
label: Minimal rule and project layout
description: Show the smallest synthetic rule, files, and dependencies that reproduce the problem.
render: ruby
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behavior
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual behavior
description: Include the violation output or stack trace after removing sensitive information.
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional context

9 changes: 9 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
blank_issues_enabled: false
contact_links:
- name: Support guide
url: https://github.com/LukasNiessen/ArchUnitRuby/blob/main/SUPPORT.md
about: Read how to ask a useful usage question.
- name: Private security report
url: https://github.com/LukasNiessen/ArchUnitRuby/security/advisories/new
about: Report vulnerabilities privately instead of opening a public issue.

31 changes: 31 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Feature request
description: Propose a new architecture rule or improvement
title: "[Feature]: "
labels:
- enhancement
body:
- type: textarea
id: problem
attributes:
label: Architectural problem
description: What architecture policy is difficult or impossible to express today?
validations:
required: true
- type: textarea
id: sentence
attributes:
label: Proposed fluent sentence
description: Write how the complete rule should read in Ruby.
render: ruby
validations:
required: true
- type: textarea
id: siblings
attributes:
label: ArchUnit family comparison
description: If known, describe how ArchUnitTS, ArchUnitPython, or another ArchUnit implements it.
- type: textarea
id: alternatives
attributes:
label: Alternatives considered

18 changes: 18 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Summary

Describe the user-facing problem and the chosen solution.

## ArchUnit family alignment

Explain how this matches the shared ArchUnit fluent API, or why idiomatic Ruby requires a deliberate
difference.

## Verification

- [ ] Added or updated unit tests
- [ ] Added or updated an end-to-end fluent API test
- [ ] `bundle exec rake` passes
- [ ] `COVERAGE=true bundle exec rspec` passes
- [ ] `bundle exec rake docs` passes for documentation or public API changes
- [ ] Updated README, API guide, and changelog where relevant
- [ ] No credentials, private source code, or customer data are included
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ jobs:
- name: Test
run: bundle exec rspec

- name: Exercise dependency-analysis reproducers
run: bundle exec ruby scripts/check_dependency_analysis.rb

quality:
name: Quality and package — Ruby 4.0 on Ubuntu
runs-on: ubuntu-latest
Expand Down Expand Up @@ -73,6 +76,12 @@ jobs:
--files 200 --gems 4 --imports-per-file 3
--max-cold-seconds 30 --max-warm-seconds 2

- name: Constant-index performance smoke benchmark
run: >-
bundle exec ruby benchmark/extraction.rb --constants
--files 200 --gems 4 --imports-per-file 3
--max-cold-seconds 30 --max-warm-seconds 2

- name: Build documentation
run: bundle exec rake docs

Expand Down Expand Up @@ -123,6 +132,7 @@ jobs:
GEM_PATH: ${{ runner.temp }}/archunit-gem-home
run: |
gem install pkg/archunit.gem --no-document
ruby scripts/check_dependency_analysis.rb
ruby -e "
require 'json'
require 'stringio'
Expand Down
1 change: 1 addition & 0 deletions .yardopts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
lib/**/*.rb
-
README.md
API.md
236 changes: 236 additions & 0 deletions API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
# ArchUnitRuby API Guide

This guide is the stable map of ArchUnitRuby's public fluent API. The
[class and method reference](class_list.html) is generated from the source, while this page explains
which entry point to choose and how the stages fit together.

All rule builders are immutable and lazy. Constructing a rule does not read the project; `check`,
`measure`, `snapshot`, and export methods are the terminals that perform work.

## Common rule shape

~~~ruby
rule = ArchUnit.project_files
.in_folder('app/api/**')
.should_not.depend_on_files
.in_folder('app/database/**')

violations = rule.check
~~~

| Stage | Public vocabulary | Result |
| --- | --- | --- |
| Entry | `project_files`, `project_layers`, `project_slices`, `project_graph`, `metrics` | Immutable builder |
| Scope | `with_name`, `in_folder`, `in_path`, `in_file`, `for_classes_matching` | Narrower builder |
| Mood | `should`, `should_not` | Positive or negated predicate builder |
| Predicate | File, layer, slice, or metric policy | Checkable rule |
| Terminal | `check`, `measure`, `snapshot`, `to_*`, `export_as_*` | Data or an artifact |

String patterns are anchored globs. `*` stays inside one path segment, `**` crosses zero or more
segments, and `?` matches one non-separator character. A trailing folder glob such as
`app/api/**` includes the `app/api` folder itself and every descendant folder. Regular expressions
are accepted by all pattern selectors except the exact `in_file` selector.

Every pattern selector accepts `except:`. Pass a pattern, an array of patterns, or explicit selector
keys such as `{ in_folder: 'generated', with_name: '*_spec.rb' }`.

## File rules

Start with `ArchUnit.project_files(project_locator = nil)`; `ArchUnit.files` is its short alias.
Without a locator, ArchUnitRuby searches upward for a `Gemfile` or gemspec.

| Scope or predicate | Purpose |
| --- | --- |
| `with_name(pattern)` | Select or require a filename pattern |
| `in_folder(pattern)` | Select or require a project-relative directory |
| `in_path(pattern)` | Select or require a complete project-relative path |
| `in_file(path)` | Select one exact project-relative file |
| `have_no_cycles` | Reject cycles among selected project files |
| `depend_on_files` | Allow or reject dependencies matching target selectors |
| `depend_on_external_modules` | Allow or reject standard-library or gem imports |
| `adhere_to(callable, message)` | Evaluate a custom predicate over immutable `FileInfo` values |

~~~ruby
service_scope = ArchUnit.project_files.in_folder('app/services/**')

rules = [
service_scope.should.have_name('*_service.rb'),
service_scope.should_not.depend_on_files.in_folder('app/controllers/**'),
service_scope.should_not.depend_on_external_modules.matching('net/http'),
service_scope.should.adhere_to(
->(file) { file.lines_of_code < 300 },
'services must stay below 300 non-blank lines'
)
]
~~~

Positive dependency rules are allowlists: every selected dependency must match at least one target.
Negated dependency rules are blocklists: matching dependencies become violations.

## Layer rules

Use `ArchUnit.project_layers(project_locator = nil)` to name architectural layers and then declare
their allowed or forbidden relationships.

~~~ruby
rule = ArchUnit.project_layers
.layer('api').defined_by('app/api/**/*.rb')
.layer('services').defined_by_folder('app/services/**')
.layer('database').defined_by('app/database/**/*.rb')
.where_layer('api').may_only_depend_on_layers('services')
.where_layer('services').may_only_depend_on_layers('database')
.where_layer('database').may_only_depend_on_layers
~~~

`may_only_depend_on_layers` creates an allowlist. Calling it without targets seals the layer.
`may_not_depend_on_layers` creates a blocklist and requires at least one target. Dependencies inside
one layer are allowed; edges with an unassigned endpoint are ignored.

## Slices and diagrams

Use `ArchUnit.project_slices(project_locator = nil)` to capture one segment from project paths.
`(**)` is the glob capture; `defined_by_regex` uses the first regular-expression capture.

~~~ruby
slices = ArchUnit.project_slices.defined_by('lib/my_app/(**)/')

dependency_rule = slices.should_not.contain_dependency('api', 'database')
diagram_rule = slices.should
.ignoring_external_slices
.ignoring_orphan_slices
.adhere_to_diagram_in_file('docs/architecture.puml')

plantuml = slices.to_plantuml
slices.export_as_plantuml('reports/architecture.puml')
~~~

`adhere_to_diagram` accepts inline PlantUML. The supported subset includes components, directed
dependencies, comments, `@startuml`, and `@enduml`.

## Dependency graph reports

Use `ArchUnit.project_graph(project_locator = nil)` for queryable snapshots and reports.

~~~ruby
report = ArchUnit.project_graph
.include_external_dependencies
.focus_on('app/services/**', 2)
.collapse_to_folder_depth(2)
.titled('Service dependencies')

summary = report.summary
snapshot = report.snapshot
report.export_as_mermaid('reports/services.mmd')
report.export_as_html('reports/services.html')
~~~

Query modifiers are `focus_on(pattern, depth)`, `reachable_from(pattern)`, and
`dependents_of(pattern)`. Group nodes with `collapse_to_folder_depth` or
`collapse_by_pattern(pattern, replacement)`. Formats are DOT, Mermaid, D2, CSV, JSON, and
self-contained HTML; each provides both `to_<format>` and `export_as_<format>`.

## Metrics

Start with `ArchUnit.metrics(project_locator = nil)`. Select files with the standard file selectors
and classes with `for_classes_matching`.

~~~ruby
services = ArchUnit.metrics
.in_path('app/services/**/*.rb')
.for_classes_matching('*Service')

rules = [
services.count.method_count.should_be_below_or_equal(20),
services.lcom.lcom4.should_be(1),
services.distance.instability.should_be_below(0.8)
]

measurements = services.count.method_count.measure
services.count.export_as_html('reports/service-counts')
~~~

Count metrics cover methods, fields, lines, statements, imports, classes, and top-level functions.
Cohesion provides LCOM96a, LCOM96b, LCOM1-5, and LCOM*. Distance metrics include abstractness,
instability, main-sequence distance, normalized distance, and coupling factor.

Thresholds use exactly `should_be_below`, `should_be_above`, `should_be`,
`should_be_below_or_equal`, `should_be_above_or_equal`, and `should_satisfy`. Use `custom_metric`
for a calculation over `ClassInfo`; zone guards are `not_in_zone_of_pain` and
`not_in_zone_of_uselessness`.

## Checking and test frameworks

Every architecture rule includes `ArchUnit::Checkable` and exposes `check(options = nil)`. It returns
an array of structured `Violation` values; an architecture disagreement is not an exception until
an assertion adapter translates it at the test boundary.

~~~ruby
violations = rule.check

expect(rule).to pass # RSpec
assert_passes(rule) # Minitest
ArchUnit.assert_passes(rule) # Framework-neutral
~~~

Use `ArchUnit.format_violations` or `ArchUnit::ResultFactory` when integrating with another test
framework or command-line interface.

## Check options and logging

`ArchUnit::CheckOptions` keeps optional execution behavior out of the fluent sentence:

~~~ruby
logging = ArchUnit::LoggingOptions.new(
level: :debug,
output_directory: 'tmp/archunit-logs'
)

options = ArchUnit::CheckOptions.new(
allow_empty_tests: false,
clear_cache: false,
load_paths: ['components/billing/lib'],
dependency_analysis: :imports,
logging: logging
)

violations = rule.check(options)
~~~

Zero selected subjects produce `EmptyTestViolation` by default. Set `allow_empty_tests: true` only
when an empty scope is intentionally valid. Set `clear_cache: true` after modifying the analyzed
project during one process. `load_paths` adds project-contained source roots without evaluating
gemspecs or running application code.

`dependency_analysis` defaults to `:imports`. Imports accept literal arguments and preceding local
literal-string assignments in supported straight-line scopes. Aliases, mutation/escape, closures,
reflection, and unsupported control flow invalidate inferred values; runtime paths are not guessed.

For autoloaded projects, opt into known project constant references alongside imports:

~~~ruby
options = ArchUnit::CheckOptions.new(dependency_analysis: :imports_and_constants)
violations = rule.check(options)
json = ArchUnit.project_graph.with_check_options(options).to_json
values = ArchUnit.metrics.distance.instability.measure(options)
~~~

Expanded analysis resolves supported lexical, compact, and root-qualified references to their
definition files. Explicit superclass and mixin references also contribute edges. Missing or
ambiguous definitions, inherited-member lookup, and dynamic metaprogramming stay unresolved under
the bounded contract in the main guide. Namespaces that fail to stabilize within eight index passes
fall back to import-only results. Unknown third-party constants do not create architecture violations.

Expanded edges include `:constant_reference` in `import_kinds` and retain `DependencyEvidence` with
names and source lines in `edge.evidence` and JSON reports. An import and a constant use supporting
the same file pair merge into one edge. Import-only edge evidence stays empty for compatibility;
`ArchUnit::Extraction.extract_imports` retains import call locations in both modes. The analysis
mode participates in graph-cache identity, and `clear_cache: true` rebuilds the symbol index.

## Errors and support

Invalid API input raises `ArchUnit::UserError` or `ArgumentError`; library or environment failures
raise `ArchUnit::TechnicalError`. Rule failures remain violation data.

See the [main guide](index.html) for installation and complete examples, [SUPPORT.md](https://github.com/LukasNiessen/ArchUnitRuby/blob/main/SUPPORT.md)
for help, and [SECURITY.md](https://github.com/LukasNiessen/ArchUnitRuby/blob/main/SECURITY.md) for
responsible vulnerability reporting.
Loading
Loading