archspec 0.3.0 → 0.5.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.
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ArchSpec
4
+ # The checks ArchSpec runs. Each rule responds to +id+ (its stable rule id,
5
+ # used in suppressions and the todo file) and <tt>evaluate(graph)</tt>, which
6
+ # returns ArchSpec::Diagnostic objects. You normally add rules through the
7
+ # ArchSpec::DSL rather than instantiating these directly, but you can pass a
8
+ # custom object of the same shape to ArchSpec::DSL::Context#rule.
4
9
  module Rules
10
+ # Base for the allow and forbid dependency rules. Merges repeated
11
+ # declarations for the same source component.
5
12
  class DependencyRule
6
13
  attr_reader :source, :targets
7
14
 
@@ -34,6 +41,8 @@ module ArchSpec
34
41
  end
35
42
  end
36
43
 
44
+ # Backs ArchSpec::DSL::ComponentProxy#can_only_use. Flags references from the
45
+ # source to any component outside its allowlist.
37
46
  class AllowDependenciesRule < DependencyRule
38
47
  def id
39
48
  'dependencies.allow'
@@ -55,6 +64,8 @@ module ArchSpec
55
64
  end
56
65
  end
57
66
 
67
+ # Backs ArchSpec::DSL::ComponentProxy#cannot_use. Flags references from the
68
+ # source to any of the named components.
58
69
  class ForbidDependenciesRule < DependencyRule
59
70
  def id
60
71
  'dependencies.forbid'
@@ -76,6 +87,60 @@ module ArchSpec
76
87
  end
77
88
  end
78
89
 
90
+ # Backs ArchSpec::DSL::ComponentProxy#can_only_be_used_by. The inverse of an
91
+ # allowlist: flags references to the component from any component that is not
92
+ # an approved consumer. Use it to protect a shared kernel.
93
+ class AllowedConsumersRule
94
+ attr_reader :source, :consumers
95
+
96
+ def initialize(source, consumers)
97
+ @source = source.to_sym
98
+ @consumers = Array(consumers).flatten.map(&:to_sym).to_set
99
+ end
100
+
101
+ def merge_key
102
+ [self.class, source]
103
+ end
104
+
105
+ def merge!(other)
106
+ consumers.merge(other.consumers)
107
+ self
108
+ end
109
+
110
+ def id
111
+ 'dependencies.consumers'
112
+ end
113
+
114
+ def evaluate(graph)
115
+ graph.dependency_edges.flat_map do |edge|
116
+ next [] unless graph.target_components_for(edge).include?(source)
117
+
118
+ offenders = graph.component_names_for_path(edge.from_path).reject do |component|
119
+ component == source || consumers.include?(component)
120
+ end
121
+
122
+ offenders.map do |offender|
123
+ Diagnostic.new(
124
+ rule: id,
125
+ message: message_for(offender),
126
+ location: edge.location,
127
+ evidence: "#{edge.from_constant || edge.from_path} #{edge.type} #{edge.to}"
128
+ )
129
+ end
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ def message_for(offender)
136
+ return "#{source} may not be used by #{offender}" if consumers.empty?
137
+
138
+ "#{source} may only be used by #{consumers.to_a.sort.join(', ')}, not #{offender}"
139
+ end
140
+ end
141
+
142
+ # Backs ArchSpec::DSL::ComponentProxy#cannot_reference_constants. Flags
143
+ # references to the named constants or anything nested under them.
79
144
  class CannotReferenceConstantsRule
80
145
  attr_reader :source, :constants
81
146
 
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module ArchSpec
6
+ module Rules
7
+ # Backs the +methods.matching(...)+ DSL. One rule pairs a selector (which
8
+ # methods it judges) with a constraint (what must hold of them). It judges a
9
+ # component's defined, public methods in the requested scope, or every such
10
+ # method in the project when +source+ is nil. Every finding is name-based and
11
+ # exact, so confidence is always +:high+.
12
+ class NamingRule
13
+ attr_reader :source, :selector, :scope, :except
14
+
15
+ def initialize(source:, selector:, constraint:, scope: :instance, except: [])
16
+ @source = source&.to_sym
17
+ @selector = selector
18
+ @constraint = constraint
19
+ @scope = scope
20
+ @except = Array(except).flatten.map(&:to_sym).to_set
21
+ end
22
+
23
+ def id
24
+ @constraint.id
25
+ end
26
+
27
+ def evaluate(graph)
28
+ selected = candidate_methods(graph).filter_map do |definition|
29
+ next if except.include?(definition.name)
30
+
31
+ match = selector.match(definition)
32
+ [definition, match] if match
33
+ end
34
+
35
+ @constraint.diagnostics(selected, self, graph)
36
+ end
37
+
38
+ private
39
+
40
+ def candidate_methods(graph)
41
+ definitions = source ? graph.method_definitions_for_component(source) : graph.method_definitions
42
+ definitions.select { |definition| definition.scope == scope && definition.visibility == :public }
43
+ end
44
+ end
45
+
46
+ # The selectors and constraints the naming DSL composes, plus the builder the
47
+ # DSL returns.
48
+ module Naming
49
+ # Selects methods whose name matches a regex. A named capture in the regex
50
+ # (such as <tt>(?<base>.+)</tt>) is exposed to the +requires+ constraint.
51
+ class NameSelector
52
+ attr_reader :regex
53
+
54
+ def initialize(regex)
55
+ @regex = regex
56
+ end
57
+
58
+ def match(definition)
59
+ regex.match(definition.name.to_s)
60
+ end
61
+
62
+ def describe
63
+ "matches #{regex.inspect}"
64
+ end
65
+ end
66
+
67
+ # Forbids any selected method from existing. Rule id +naming.forbidden+.
68
+ class Forbidden
69
+ def initialize(because: nil)
70
+ @because = because
71
+ end
72
+
73
+ def id
74
+ 'naming.forbidden'
75
+ end
76
+
77
+ def diagnostics(selected, rule, _graph)
78
+ selected.map do |definition, _match|
79
+ Diagnostic.new(
80
+ rule: id,
81
+ message: message_for(definition),
82
+ location: definition.location,
83
+ evidence: "#{definition.owner} defines #{definition.scope} method #{definition.name} (#{rule.selector.describe})"
84
+ )
85
+ end
86
+ end
87
+
88
+ private
89
+
90
+ def message_for(definition)
91
+ base = "#{definition.owner} must not define ##{definition.name}"
92
+ @because ? "#{base}: #{@because}" : base
93
+ end
94
+ end
95
+
96
+ # Requires each selected method to have a sibling named by a template, in
97
+ # the same component or another (+on:+), at a given scope. The template
98
+ # interpolates the selector's named captures, as in
99
+ # <tt>requires("without_%{base}")</tt>. Rule id +naming.requires+.
100
+ class Requires
101
+ def initialize(template, on: nil, scope: :instance, because: nil)
102
+ @template = template
103
+ @on = on
104
+ @target_scope = scope
105
+ @because = because
106
+ end
107
+
108
+ def id
109
+ 'naming.requires'
110
+ end
111
+
112
+ def diagnostics(selected, rule, graph)
113
+ target = @on || rule.source
114
+ existing = existing_names(graph, target)
115
+
116
+ selected.filter_map do |definition, match|
117
+ sibling = expand(match)
118
+ next if sibling.nil? || existing.include?(sibling.to_sym)
119
+
120
+ Diagnostic.new(
121
+ rule: id,
122
+ message: message_for(definition, sibling, target, rule),
123
+ location: definition.location,
124
+ evidence: "#{definition.owner} defines ##{definition.name}, expected ##{sibling}"
125
+ )
126
+ end
127
+ end
128
+
129
+ private
130
+
131
+ def existing_names(graph, target)
132
+ definitions = target ? graph.method_definitions_for_component(target) : graph.method_definitions
133
+ definitions.select { |definition| definition.scope == @target_scope }.map(&:name).to_set
134
+ end
135
+
136
+ def expand(match)
137
+ @template % captures(match)
138
+ rescue KeyError, ArgumentError
139
+ nil
140
+ end
141
+
142
+ def captures(match)
143
+ match.is_a?(MatchData) ? match.named_captures.transform_keys(&:to_sym) : {}
144
+ end
145
+
146
+ def message_for(definition, sibling, target, rule)
147
+ clause =
148
+ if target && target != rule.source
149
+ "#{target} to define ##{sibling}"
150
+ else
151
+ "a matching ##{sibling}"
152
+ end
153
+ base = "#{definition.owner}##{definition.name} requires #{clause}"
154
+ @because ? "#{base}: #{@because}" : base
155
+ end
156
+ end
157
+
158
+ # Returned by ArchSpec::DSL::ComponentProxy#methods. Starts a selector.
159
+ class Builder
160
+ def initialize(component, scope: :instance)
161
+ @component = component
162
+ @scope = scope
163
+ end
164
+
165
+ def matching(regex)
166
+ Selected.new(@component, NameSelector.new(regex), @scope)
167
+ end
168
+ end
169
+
170
+ # A chosen selector, waiting for a constraint. Each constraint method builds
171
+ # a NamingRule, attaches it, and returns the component proxy so rules chain.
172
+ class Selected
173
+ def initialize(component, selector, scope)
174
+ @component = component
175
+ @selector = selector
176
+ @scope = scope
177
+ end
178
+
179
+ def forbidden(except: [], because: nil)
180
+ add(Forbidden.new(because: because), except)
181
+ end
182
+
183
+ def requires(template, on: nil, scope: :instance, except: [], because: nil)
184
+ add(Requires.new(template, on: component_name(on), scope: scope, because: because), except)
185
+ end
186
+
187
+ private
188
+
189
+ def add(constraint, except)
190
+ rule = NamingRule.new(
191
+ source: @component.name,
192
+ selector: @selector,
193
+ constraint: constraint,
194
+ scope: @scope,
195
+ except: except
196
+ )
197
+ @component.definition.add_rule(rule)
198
+ @component
199
+ end
200
+
201
+ def component_name(target)
202
+ return if target.nil?
203
+
204
+ target.respond_to?(:name) ? target.name : target.to_sym
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArchSpec
4
+ module Rules
5
+ # Backs ArchSpec::DSL::ComponentProxy#public_api. Flags references from
6
+ # outside the component to constants that are not part of its public API.
7
+ class PublicApiRule
8
+ attr_reader :source, :file_patterns, :constants, :namespaces
9
+
10
+ def initialize(source, files: [], constants: nil, namespaces: nil)
11
+ @source = source.to_sym
12
+ @file_patterns = Array(files).flatten.compact.map(&:to_s)
13
+ @constants = Array(constants).compact.map { |value| normalize_constant(value) }
14
+ @namespaces = Array(namespaces).compact.map { |value| normalize_constant(value) }
15
+ end
16
+
17
+ def merge_key
18
+ [self.class, source]
19
+ end
20
+
21
+ def merge!(other)
22
+ @file_patterns |= other.file_patterns
23
+ @constants |= other.constants
24
+ @namespaces |= other.namespaces
25
+ self
26
+ end
27
+
28
+ def id
29
+ 'dependencies.privacy'
30
+ end
31
+
32
+ def evaluate(graph)
33
+ component = graph.components[source]
34
+ return [] unless component
35
+
36
+ public_names = public_constant_names(graph)
37
+
38
+ graph.dependency_edges.filter_map do |edge|
39
+ next if component.files.include?(edge.from_path)
40
+
41
+ resolved = graph.resolve_constant_reference(edge.to, edge.from_constant)
42
+ next unless graph.component_names_for_constant(resolved).include?(source)
43
+ next if public?(resolved, public_names)
44
+
45
+ Diagnostic.new(
46
+ rule: id,
47
+ message: "#{resolved} is private to #{source}",
48
+ location: edge.location,
49
+ evidence: "#{edge.from_constant || edge.from_path} #{edge.type} #{resolved}"
50
+ )
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def public_constant_names(graph)
57
+ paths = file_patterns.flat_map do |pattern|
58
+ Dir.glob(File.absolute_path(pattern, graph.root))
59
+ end.map { |path| File.expand_path(path) }.to_set
60
+
61
+ names = graph.constants.select { |constant| paths.include?(constant.path) }.map(&:name)
62
+ (names + constants).to_set
63
+ end
64
+
65
+ # Names from public files and constants: match exactly. Reopened
66
+ # namespace modules land in public files too, so prefix matching there
67
+ # would make the whole namespace public. Use namespace: for prefixes.
68
+ def public?(name, public_names)
69
+ return true if public_names.include?(name)
70
+
71
+ namespaces.any? do |namespace|
72
+ name == namespace || name.start_with?("#{namespace}::")
73
+ end
74
+ end
75
+
76
+ def normalize_constant(value)
77
+ value.to_s.sub(/\A::/, '')
78
+ end
79
+ end
80
+ end
81
+ end
@@ -2,16 +2,23 @@
2
2
 
3
3
  module ArchSpec
4
4
  module Rules
5
+ # Backs ArchSpec::DSL::ComponentProxy#cannot_call. Flags calls to the named
6
+ # methods, optionally only bare implicit-+self+ calls.
5
7
  class CannotCallRule
6
- attr_reader :source, :method_names
8
+ attr_reader :source, :method_names, :receiver
9
+
10
+ def initialize(source, methods, receiver: :any)
11
+ unless %i[any none].include?(receiver)
12
+ raise Error, "cannot_call receiver: must be :any or :none, got #{receiver.inspect}"
13
+ end
7
14
 
8
- def initialize(source, methods)
9
15
  @source = source.to_sym
10
16
  @method_names = Array(methods).flatten.map(&:to_sym)
17
+ @receiver = receiver
11
18
  end
12
19
 
13
20
  def merge_key
14
- [self.class, source]
21
+ [self.class, source, receiver]
15
22
  end
16
23
 
17
24
  def merge!(other)
@@ -27,7 +34,9 @@ module ArchSpec
27
34
  graph.edges.filter_map do |edge|
28
35
  next unless edge.type == :calls_named_method
29
36
  next unless method_names.include?(edge.to.to_sym)
37
+ next if receiver == :none && edge.receiver != :none
30
38
  next unless graph.component_names_for_path(edge.from_path).include?(source)
39
+ next if own_method_call?(graph, edge)
31
40
 
32
41
  Diagnostic.new(
33
42
  rule: id,
@@ -37,8 +46,22 @@ module ArchSpec
37
46
  )
38
47
  end
39
48
  end
49
+
50
+ private
51
+
52
+ # A receiverless call to a method the class itself defines (directly,
53
+ # inherited, or via attr_*/attribute/delegate) is a call to its own API.
54
+ def own_method_call?(graph, edge)
55
+ return false unless edge.receiver == :none && edge.from_constant
56
+
57
+ methods, = graph.effective_instance_methods(edge.from_constant)
58
+ methods.include?(edge.to.to_sym)
59
+ end
40
60
  end
41
61
 
62
+ # Backs ArchSpec::DSL::ComponentProxy#must_implement. Flags classes in the
63
+ # component that do not implement the method, counting inherited and
64
+ # mixed-in methods.
42
65
  class MustImplementRule
43
66
  attr_reader :source, :method_name
44
67
 
@@ -57,13 +80,15 @@ module ArchSpec
57
80
 
58
81
  def evaluate(graph)
59
82
  constants_for(graph).filter_map do |constant|
60
- next if constant.instance_methods.include?(method_name)
83
+ methods, unresolved = graph.effective_instance_methods(constant.name)
84
+ next if methods.include?(method_name)
61
85
 
62
86
  Diagnostic.new(
63
87
  rule: id,
64
88
  message: "#{constant.name} must implement ##{method_name}",
65
89
  location: constant.location,
66
- evidence: "#{constant.name} methods: #{constant.instance_methods.to_a.sort.join(', ')}"
90
+ evidence: ProtocolEvidence.for(constant, methods, unresolved),
91
+ confidence: unresolved.empty? ? :high : :medium
67
92
  )
68
93
  end
69
94
  end
@@ -71,12 +96,12 @@ module ArchSpec
71
96
  private
72
97
 
73
98
  def constants_for(graph)
74
- graph.components.fetch(source).constants.flat_map { |name| graph.constants_named(name) }.select(&:class?)
75
- rescue KeyError
76
- []
99
+ ProtocolEvidence.constants_for(graph, source)
77
100
  end
78
101
  end
79
102
 
103
+ # Backs ArchSpec::DSL::ComponentProxy#must_implement_one_of. Flags classes
104
+ # that implement none of the named methods.
80
105
  class MustImplementOneOfRule
81
106
  attr_reader :source, :method_names
82
107
 
@@ -100,13 +125,15 @@ module ArchSpec
100
125
 
101
126
  def evaluate(graph)
102
127
  constants_for(graph).filter_map do |constant|
103
- next if method_names.any? { |method_name| constant.instance_methods.include?(method_name) }
128
+ methods, unresolved = graph.effective_instance_methods(constant.name)
129
+ next if method_names.any? { |method_name| methods.include?(method_name) }
104
130
 
105
131
  Diagnostic.new(
106
132
  rule: id,
107
133
  message: "#{constant.name} must implement one of #{method_names.map { |name| "##{name}" }.join(', ')}",
108
134
  location: constant.location,
109
- evidence: "#{constant.name} methods: #{constant.instance_methods.to_a.sort.join(', ')}"
135
+ evidence: ProtocolEvidence.for(constant, methods, unresolved),
136
+ confidence: unresolved.empty? ? :high : :medium
110
137
  )
111
138
  end
112
139
  end
@@ -114,12 +141,32 @@ module ArchSpec
114
141
  private
115
142
 
116
143
  def constants_for(graph)
117
- graph.components.fetch(source).constants.flat_map { |name| graph.constants_named(name) }.select(&:class?)
118
- rescue KeyError
119
- []
144
+ ProtocolEvidence.constants_for(graph, source)
145
+ end
146
+ end
147
+
148
+ # Builds the shared "methods: ..." evidence string for the protocol rules
149
+ # and resolves the classes in a component. Internal helper.
150
+ module ProtocolEvidence
151
+ module_function
152
+
153
+ def constants_for(graph, source)
154
+ component = graph.components[source]
155
+ return [] unless component
156
+
157
+ component.constants.flat_map { |name| graph.constants_named(name) }.select(&:class?).uniq(&:name)
158
+ end
159
+
160
+ def for(constant, methods, unresolved)
161
+ evidence = "#{constant.name} methods: #{methods.empty? ? '(none)' : methods.to_a.sort.join(', ')}"
162
+ return evidence if unresolved.empty?
163
+
164
+ "#{evidence}; unresolved ancestors: #{unresolved.to_a.sort.join(', ')}"
120
165
  end
121
166
  end
122
167
 
168
+ # Backs ArchSpec::DSL::ComponentProxy#cannot_define. Flags method
169
+ # definitions in the component matching the named methods.
123
170
  class CannotDefineMethodRule
124
171
  attr_reader :source, :method_names
125
172
 
@@ -155,6 +202,8 @@ module ArchSpec
155
202
  end
156
203
  end
157
204
 
205
+ # Backs ArchSpec::DSL::ComponentProxy#cannot_instantiate_and_invoke. Flags
206
+ # the one-shot <tt>Thing.new(...).call</tt> pattern.
158
207
  class CannotInstantiateAndInvokeRule
159
208
  attr_reader :source
160
209
 
@@ -3,7 +3,11 @@
3
3
  require 'yaml'
4
4
 
5
5
  module ArchSpec
6
- class Baseline
6
+ # A file of accepted existing violations. ArchSpec still checks the files
7
+ # these come from, but subtracts the recorded violations so you can adopt it
8
+ # in an existing app and burn the list down over time. Matched by
9
+ # ArchSpec::Diagnostic#fingerprint, so entries survive edits that shift lines.
10
+ class Todo
7
11
  def self.empty(root: nil)
8
12
  new(Set.new, root: root)
9
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ArchSpec
4
- VERSION = '0.3.0'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/archspec.rb CHANGED
@@ -7,27 +7,64 @@ require_relative 'archspec/diagnostic'
7
7
  require_relative 'archspec/component_spec'
8
8
  require_relative 'archspec/model'
9
9
  require_relative 'archspec/definition'
10
- require_relative 'archspec/baseline'
10
+ require_relative 'archspec/todo'
11
11
  require_relative 'archspec/dsl'
12
12
  require_relative 'archspec/analyzer'
13
13
  require_relative 'archspec/evaluator'
14
14
  require_relative 'archspec/architectures'
15
- require_relative 'archspec/presets'
16
15
  require_relative 'archspec/rules/component_rules'
16
+ require_relative 'archspec/rules/concern_rules'
17
17
  require_relative 'archspec/rules/dependency_rules'
18
+ require_relative 'archspec/rules/naming_rules'
19
+ require_relative 'archspec/rules/privacy_rule'
18
20
  require_relative 'archspec/rules/protocol_rules'
19
21
  require_relative 'archspec/rules/cycle_rule'
20
- require_relative 'archspec/rules/zeitwerk_rule'
21
22
  require_relative 'archspec/formatters/text'
22
23
  require_relative 'archspec/formatters/json'
24
+ require_relative 'archspec/formatters/explanation'
23
25
  require_relative 'archspec/cli'
24
26
 
27
+ # ArchSpec turns your application's architecture into executable checks.
28
+ #
29
+ # You describe components, dependencies, and boundaries in an +Archspec.rb+
30
+ # file written in the ArchSpec::DSL, then run <tt>archspec check</tt> to verify
31
+ # every change. ArchSpec reads Ruby source with Prism and never boots the app.
32
+ #
33
+ # The DSL is the public API. An +Archspec.rb+ file is evaluated directly:
34
+ #
35
+ # architecture :rails
36
+ #
37
+ # component :services, in: "app/services/**/*.rb"
38
+ # services.cannot_call :render, :redirect_to, receiver: :none
39
+ #
40
+ # See ArchSpec::DSL::Context for the top-level DSL and
41
+ # ArchSpec::DSL::ComponentProxy for per-component rules. See
42
+ # ArchSpec::Architectures for the bundled architecture presets.
43
+ #
44
+ # You can also build a definition in plain Ruby with ArchSpec.define.
25
45
  module ArchSpec
46
+ # Raised for configuration and usage errors, such as an unknown architecture
47
+ # name or a malformed rule option.
26
48
  class Error < StandardError; end
27
49
 
28
50
  class << self
51
+ # The definition produced by the most recent ArchSpec.define call, or by
52
+ # evaluating an +Archspec.rb+ file. The CLI reads this after loading config.
29
53
  attr_accessor :last_definition
30
54
 
55
+ # Builds an architecture definition from a block of DSL calls.
56
+ #
57
+ # ArchSpec.define do
58
+ # component :models, in: "app/models/**/*.rb"
59
+ # component :controllers, in: "app/controllers/**/*.rb"
60
+ # models.cannot_use :controllers
61
+ # end
62
+ #
63
+ # An +Archspec.rb+ file does not need this wrapper. Its top level is already
64
+ # the DSL, so bare +component+ and +architecture+ calls work directly. Use
65
+ # +define+ when constructing a definition from Ruby, such as in a test.
66
+ #
67
+ # Returns the ArchSpec::Definition, which is also stored as last_definition.
31
68
  def define(name = nil, &block)
32
69
  definition = Definition.new(name)
33
70
  definition.extend(DSL::Context)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: archspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-06-21 00:00:00.000000000 Z
11
+ date: 2026-08-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prism
@@ -58,6 +58,20 @@ dependencies:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
60
  version: '13.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rdoc
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '6.6'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '6.6'
61
75
  description: A static architecture linter for Ruby and Rails. Declare your components,
62
76
  dependencies, and boundaries in one file, then check every change in CI. It reads
63
77
  source with Prism and never boots the app.
@@ -74,23 +88,25 @@ files:
74
88
  - lib/archspec.rb
75
89
  - lib/archspec/analyzer.rb
76
90
  - lib/archspec/architectures.rb
77
- - lib/archspec/baseline.rb
78
91
  - lib/archspec/cli.rb
79
92
  - lib/archspec/component_spec.rb
80
93
  - lib/archspec/definition.rb
81
94
  - lib/archspec/diagnostic.rb
82
95
  - lib/archspec/dsl.rb
83
96
  - lib/archspec/evaluator.rb
97
+ - lib/archspec/formatters/explanation.rb
84
98
  - lib/archspec/formatters/json.rb
85
99
  - lib/archspec/formatters/text.rb
86
100
  - lib/archspec/model.rb
87
- - lib/archspec/presets.rb
88
101
  - lib/archspec/rules/component_rules.rb
102
+ - lib/archspec/rules/concern_rules.rb
89
103
  - lib/archspec/rules/cycle_rule.rb
90
104
  - lib/archspec/rules/dependency_rules.rb
105
+ - lib/archspec/rules/naming_rules.rb
106
+ - lib/archspec/rules/privacy_rule.rb
91
107
  - lib/archspec/rules/protocol_rules.rb
92
- - lib/archspec/rules/zeitwerk_rule.rb
93
108
  - lib/archspec/source_location.rb
109
+ - lib/archspec/todo.rb
94
110
  - lib/archspec/value_object.rb
95
111
  - lib/archspec/version.rb
96
112
  homepage: https://archspecrb.dev