feature_map 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +269 -0
- data/bin/featuremap +5 -0
- data/lib/feature_map/cli.rb +243 -0
- data/lib/feature_map/code_features/plugin.rb +79 -0
- data/lib/feature_map/code_features/plugins/identity.rb +39 -0
- data/lib/feature_map/code_features.rb +152 -0
- data/lib/feature_map/configuration.rb +43 -0
- data/lib/feature_map/constants.rb +11 -0
- data/lib/feature_map/mapper.rb +78 -0
- data/lib/feature_map/output_color.rb +42 -0
- data/lib/feature_map/private/assignment_mappers/directory_assignment.rb +150 -0
- data/lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb +68 -0
- data/lib/feature_map/private/assignment_mappers/feature_globs.rb +138 -0
- data/lib/feature_map/private/assignment_mappers/file_annotations.rb +158 -0
- data/lib/feature_map/private/assignments_file.rb +190 -0
- data/lib/feature_map/private/code_cov.rb +96 -0
- data/lib/feature_map/private/cyclomatic_complexity_calculator.rb +46 -0
- data/lib/feature_map/private/docs/index.html +247 -0
- data/lib/feature_map/private/documentation_site.rb +128 -0
- data/lib/feature_map/private/extension_loader.rb +24 -0
- data/lib/feature_map/private/feature_assigner.rb +22 -0
- data/lib/feature_map/private/feature_metrics_calculator.rb +76 -0
- data/lib/feature_map/private/feature_plugins/assignment.rb +17 -0
- data/lib/feature_map/private/glob_cache.rb +80 -0
- data/lib/feature_map/private/lines_of_code_calculator.rb +49 -0
- data/lib/feature_map/private/metrics_file.rb +86 -0
- data/lib/feature_map/private/test_coverage_file.rb +97 -0
- data/lib/feature_map/private/test_pyramid_file.rb +151 -0
- data/lib/feature_map/private/todo_inspector.rb +57 -0
- data/lib/feature_map/private/validations/features_up_to_date.rb +78 -0
- data/lib/feature_map/private/validations/files_have_features.rb +45 -0
- data/lib/feature_map/private/validations/files_have_unique_features.rb +34 -0
- data/lib/feature_map/private.rb +204 -0
- data/lib/feature_map/validator.rb +29 -0
- data/lib/feature_map.rb +212 -0
- metadata +253 -0
data/lib/feature_map.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# typed: strict
|
4
|
+
|
5
|
+
require 'set'
|
6
|
+
require 'sorbet-runtime'
|
7
|
+
require 'json'
|
8
|
+
require 'yaml'
|
9
|
+
require 'feature_map/code_features'
|
10
|
+
require 'feature_map/mapper'
|
11
|
+
require 'feature_map/validator'
|
12
|
+
require 'feature_map/private'
|
13
|
+
require 'feature_map/cli'
|
14
|
+
require 'feature_map/configuration'
|
15
|
+
|
16
|
+
module FeatureMap
|
17
|
+
module_function
|
18
|
+
|
19
|
+
extend T::Sig
|
20
|
+
extend T::Helpers
|
21
|
+
|
22
|
+
requires_ancestor { Kernel }
|
23
|
+
GlobsToAssignedFeatureMap = T.type_alias { T::Hash[String, CodeFeatures::Feature] }
|
24
|
+
|
25
|
+
sig { params(file: String).returns(T.nilable(CodeFeatures::Feature)) }
|
26
|
+
def for_file(file)
|
27
|
+
@for_file ||= T.let(@for_file, T.nilable(T::Hash[String, T.nilable(CodeFeatures::Feature)]))
|
28
|
+
@for_file ||= {}
|
29
|
+
|
30
|
+
return nil if file.start_with?('./')
|
31
|
+
return @for_file[file] if @for_file.key?(file)
|
32
|
+
|
33
|
+
Private.load_configuration!
|
34
|
+
|
35
|
+
feature = T.let(nil, T.nilable(CodeFeatures::Feature))
|
36
|
+
|
37
|
+
Mapper.all.each do |mapper|
|
38
|
+
feature = mapper.map_file_to_feature(file)
|
39
|
+
break if feature # TODO: what if there are multiple features? Should we respond with an error instead of the first match?
|
40
|
+
end
|
41
|
+
|
42
|
+
@for_file[file] = feature
|
43
|
+
end
|
44
|
+
|
45
|
+
sig { params(feature: T.any(CodeFeatures::Feature, String)).returns(String) }
|
46
|
+
def for_feature(feature)
|
47
|
+
feature = T.must(CodeFeatures.find(feature)) if feature.is_a?(String)
|
48
|
+
feature_report = T.let([], T::Array[String])
|
49
|
+
|
50
|
+
feature_report << "# Report for `#{feature.name}` Feature"
|
51
|
+
|
52
|
+
Private.glob_cache.raw_cache_contents.each do |mapper_description, glob_to_assigned_feature_map|
|
53
|
+
feature_report << "## #{mapper_description}"
|
54
|
+
file_assignments_for_mapper = []
|
55
|
+
glob_to_assigned_feature_map.each do |glob, assigned_feature|
|
56
|
+
next if assigned_feature != feature
|
57
|
+
|
58
|
+
file_assignments_for_mapper << "- #{glob}"
|
59
|
+
end
|
60
|
+
|
61
|
+
if file_assignments_for_mapper.empty?
|
62
|
+
feature_report << 'This feature does not have any files in this category.'
|
63
|
+
else
|
64
|
+
feature_report += file_assignments_for_mapper.sort
|
65
|
+
end
|
66
|
+
|
67
|
+
feature_report << ''
|
68
|
+
end
|
69
|
+
|
70
|
+
feature_report.join("\n")
|
71
|
+
end
|
72
|
+
|
73
|
+
class InvalidFeatureMapConfigurationError < StandardError
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { params(filename: String).void }
|
77
|
+
def self.remove_file_annotation!(filename)
|
78
|
+
Private::AssignmentMappers::FileAnnotations.new.remove_file_annotation!(filename)
|
79
|
+
end
|
80
|
+
|
81
|
+
sig do
|
82
|
+
params(
|
83
|
+
autocorrect: T::Boolean,
|
84
|
+
stage_changes: T::Boolean,
|
85
|
+
files: T.nilable(T::Array[String])
|
86
|
+
).void
|
87
|
+
end
|
88
|
+
def validate!(
|
89
|
+
autocorrect: true,
|
90
|
+
stage_changes: true,
|
91
|
+
files: nil
|
92
|
+
)
|
93
|
+
Private.load_configuration!
|
94
|
+
|
95
|
+
tracked_file_subset = if files
|
96
|
+
files.select { |f| Private.file_tracked?(f) }
|
97
|
+
else
|
98
|
+
Private.tracked_files
|
99
|
+
end
|
100
|
+
|
101
|
+
Private.validate!(files: tracked_file_subset, autocorrect: autocorrect, stage_changes: stage_changes)
|
102
|
+
end
|
103
|
+
|
104
|
+
sig { params(git_ref: T.nilable(String)).void }
|
105
|
+
def generate_docs!(git_ref)
|
106
|
+
Private.generate_docs!(git_ref)
|
107
|
+
end
|
108
|
+
|
109
|
+
sig do
|
110
|
+
params(
|
111
|
+
unit_path: String,
|
112
|
+
integration_path: String,
|
113
|
+
regression_path: String,
|
114
|
+
regression_assignments_path: String
|
115
|
+
).void
|
116
|
+
end
|
117
|
+
def generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
|
118
|
+
Private.generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
|
119
|
+
end
|
120
|
+
|
121
|
+
sig { params(commit_sha: String, code_cov_token: String).void }
|
122
|
+
def gather_test_coverage!(commit_sha, code_cov_token)
|
123
|
+
Private.gather_test_coverage!(commit_sha, code_cov_token)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Given a backtrace from either `Exception#backtrace` or `caller`, find the
|
127
|
+
# first line that corresponds to a file with an assigned feature
|
128
|
+
sig { params(backtrace: T.nilable(T::Array[String]), excluded_features: T::Array[CodeFeatures::Feature]).returns(T.nilable(CodeFeatures::Feature)) }
|
129
|
+
def for_backtrace(backtrace, excluded_features: [])
|
130
|
+
first_assigned_file_for_backtrace(backtrace, excluded_features: excluded_features)&.first
|
131
|
+
end
|
132
|
+
|
133
|
+
# Given a backtrace from either `Exception#backtrace` or `caller`, find the
|
134
|
+
# first assigned file in it, useful for figuring out which file is being blamed.
|
135
|
+
sig { params(backtrace: T.nilable(T::Array[String]), excluded_features: T::Array[CodeFeatures::Feature]).returns(T.nilable([CodeFeatures::Feature, String])) }
|
136
|
+
def first_assigned_file_for_backtrace(backtrace, excluded_features: [])
|
137
|
+
backtrace_with_feature_assignments(backtrace).each do |(feature, file)|
|
138
|
+
if feature && !excluded_features.include?(feature)
|
139
|
+
return [feature, file]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
nil
|
144
|
+
end
|
145
|
+
|
146
|
+
sig { params(backtrace: T.nilable(T::Array[String])).returns(T::Enumerable[[T.nilable(CodeFeatures::Feature), String]]) }
|
147
|
+
def backtrace_with_feature_assignments(backtrace)
|
148
|
+
return [] unless backtrace
|
149
|
+
|
150
|
+
# The pattern for a backtrace hasn't changed in forever and is considered
|
151
|
+
# stable: https://github.com/ruby/ruby/blob/trunk/vm_backtrace.c#L303-L317
|
152
|
+
#
|
153
|
+
# This pattern matches a line like the following:
|
154
|
+
#
|
155
|
+
# ./app/controllers/some_controller.rb:43:in `block (3 levels) in create'
|
156
|
+
#
|
157
|
+
backtrace_line = %r{\A(#{Pathname.pwd}/|\./)?
|
158
|
+
(?<file>.+) # Matches 'app/controllers/some_controller.rb'
|
159
|
+
:
|
160
|
+
(?<line>\d+) # Matches '43'
|
161
|
+
:in\s
|
162
|
+
`(?<function>.*)' # Matches "`block (3 levels) in create'"
|
163
|
+
\z}x
|
164
|
+
|
165
|
+
backtrace.lazy.filter_map do |line|
|
166
|
+
match = line.match(backtrace_line)
|
167
|
+
next unless match
|
168
|
+
|
169
|
+
file = T.must(match[:file])
|
170
|
+
|
171
|
+
[
|
172
|
+
FeatureMap.for_file(file),
|
173
|
+
file
|
174
|
+
]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
private_class_method(:backtrace_with_feature_assignments)
|
178
|
+
|
179
|
+
sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(CodeFeatures::Feature)) }
|
180
|
+
def for_class(klass)
|
181
|
+
@memoized_values ||= T.let(@memoized_values, T.nilable(T::Hash[String, T.nilable(CodeFeatures::Feature)]))
|
182
|
+
@memoized_values ||= {}
|
183
|
+
# We use key because the memoized value could be `nil`
|
184
|
+
if @memoized_values.key?(klass.to_s)
|
185
|
+
@memoized_values[klass.to_s]
|
186
|
+
else
|
187
|
+
path = Private.path_from_klass(klass)
|
188
|
+
return nil if path.nil?
|
189
|
+
|
190
|
+
value_to_memoize = for_file(path)
|
191
|
+
@memoized_values[klass.to_s] = value_to_memoize
|
192
|
+
value_to_memoize
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change.
|
197
|
+
# Namely, the set of files, and directories which are tracked for feature assignment should not change.
|
198
|
+
# The primary reason this is helpful is for clients of FeatureMap who want to test their code, and each test context
|
199
|
+
# has different feature assignments and tracked files.
|
200
|
+
sig { void }
|
201
|
+
def self.bust_caches!
|
202
|
+
@for_file = nil
|
203
|
+
@memoized_values = nil
|
204
|
+
Private.bust_caches!
|
205
|
+
Mapper.all.each(&:bust_caches!)
|
206
|
+
end
|
207
|
+
|
208
|
+
sig { returns(Configuration) }
|
209
|
+
def self.configuration
|
210
|
+
Private.configuration
|
211
|
+
end
|
212
|
+
end
|
metadata
ADDED
@@ -0,0 +1,253 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: feature_map
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Beyond Finance
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-01-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: code_ownership
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.34'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.34'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: faraday
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.7'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: packs-specification
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: sorbet-runtime
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.5'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.5'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: debug
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.9'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.9'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: railties
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '7.2'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '7.2'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rake
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '13.2'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '13.2'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: rspec
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '3.0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '3.0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: sorbet
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0.5'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0.5'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: tapioca
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - "~>"
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0.16'
|
160
|
+
type: :development
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - "~>"
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '0.16'
|
167
|
+
- !ruby/object:Gem::Dependency
|
168
|
+
name: webmock
|
169
|
+
requirement: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - "~>"
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '3.24'
|
174
|
+
type: :development
|
175
|
+
prerelease: false
|
176
|
+
version_requirements: !ruby/object:Gem::Requirement
|
177
|
+
requirements:
|
178
|
+
- - "~>"
|
179
|
+
- !ruby/object:Gem::Version
|
180
|
+
version: '3.24'
|
181
|
+
description: FeatureMap helps identify and manage features within large Ruby and Rails
|
182
|
+
applications. This gem works best in large, usually monolithic code bases for applications
|
183
|
+
that incorporate a wide range of features with various dependencies.
|
184
|
+
email:
|
185
|
+
- engineering@beyondfinance.com
|
186
|
+
executables:
|
187
|
+
- featuremap
|
188
|
+
extensions: []
|
189
|
+
extra_rdoc_files: []
|
190
|
+
files:
|
191
|
+
- README.md
|
192
|
+
- bin/featuremap
|
193
|
+
- lib/feature_map.rb
|
194
|
+
- lib/feature_map/cli.rb
|
195
|
+
- lib/feature_map/code_features.rb
|
196
|
+
- lib/feature_map/code_features/plugin.rb
|
197
|
+
- lib/feature_map/code_features/plugins/identity.rb
|
198
|
+
- lib/feature_map/configuration.rb
|
199
|
+
- lib/feature_map/constants.rb
|
200
|
+
- lib/feature_map/mapper.rb
|
201
|
+
- lib/feature_map/output_color.rb
|
202
|
+
- lib/feature_map/private.rb
|
203
|
+
- lib/feature_map/private/assignment_mappers/directory_assignment.rb
|
204
|
+
- lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb
|
205
|
+
- lib/feature_map/private/assignment_mappers/feature_globs.rb
|
206
|
+
- lib/feature_map/private/assignment_mappers/file_annotations.rb
|
207
|
+
- lib/feature_map/private/assignments_file.rb
|
208
|
+
- lib/feature_map/private/code_cov.rb
|
209
|
+
- lib/feature_map/private/cyclomatic_complexity_calculator.rb
|
210
|
+
- lib/feature_map/private/docs/index.html
|
211
|
+
- lib/feature_map/private/documentation_site.rb
|
212
|
+
- lib/feature_map/private/extension_loader.rb
|
213
|
+
- lib/feature_map/private/feature_assigner.rb
|
214
|
+
- lib/feature_map/private/feature_metrics_calculator.rb
|
215
|
+
- lib/feature_map/private/feature_plugins/assignment.rb
|
216
|
+
- lib/feature_map/private/glob_cache.rb
|
217
|
+
- lib/feature_map/private/lines_of_code_calculator.rb
|
218
|
+
- lib/feature_map/private/metrics_file.rb
|
219
|
+
- lib/feature_map/private/test_coverage_file.rb
|
220
|
+
- lib/feature_map/private/test_pyramid_file.rb
|
221
|
+
- lib/feature_map/private/todo_inspector.rb
|
222
|
+
- lib/feature_map/private/validations/features_up_to_date.rb
|
223
|
+
- lib/feature_map/private/validations/files_have_features.rb
|
224
|
+
- lib/feature_map/private/validations/files_have_unique_features.rb
|
225
|
+
- lib/feature_map/validator.rb
|
226
|
+
homepage: https://github.com/Beyond-Finance/feature_map
|
227
|
+
licenses:
|
228
|
+
- MIT
|
229
|
+
metadata:
|
230
|
+
homepage_uri: https://github.com/Beyond-Finance/feature_map
|
231
|
+
source_code_uri: https://github.com/Beyond-Finance/feature_map
|
232
|
+
changelog_uri: https://github.com/Beyond-Finance/feature_map/releases
|
233
|
+
allowed_push_host: https://rubygems.org
|
234
|
+
post_install_message:
|
235
|
+
rdoc_options: []
|
236
|
+
require_paths:
|
237
|
+
- lib
|
238
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
239
|
+
requirements:
|
240
|
+
- - ">="
|
241
|
+
- !ruby/object:Gem::Version
|
242
|
+
version: '2.6'
|
243
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
244
|
+
requirements:
|
245
|
+
- - ">="
|
246
|
+
- !ruby/object:Gem::Version
|
247
|
+
version: '0'
|
248
|
+
requirements: []
|
249
|
+
rubygems_version: 3.4.10
|
250
|
+
signing_key:
|
251
|
+
specification_version: 4
|
252
|
+
summary: A gem to help identify and manage features within large Ruby and Rails applications
|
253
|
+
test_files: []
|