ace-lint 0.25.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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/lint/config.yml +5 -0
  3. data/.ace-defaults/lint/kramdown.yml +23 -0
  4. data/.ace-defaults/lint/markdown.yml +16 -0
  5. data/.ace-defaults/lint/ruby.yml +67 -0
  6. data/.ace-defaults/lint/skills.yml +138 -0
  7. data/.ace-defaults/nav/protocols/wfi-sources/ace-lint.yml +11 -0
  8. data/CHANGELOG.md +584 -0
  9. data/LICENSE +21 -0
  10. data/README.md +40 -0
  11. data/Rakefile +14 -0
  12. data/exe/ace-lint +14 -0
  13. data/handbook/skills/as-lint-fix-issue-from/SKILL.md +34 -0
  14. data/handbook/skills/as-lint-process-report/SKILL.md +29 -0
  15. data/handbook/skills/as-lint-run/SKILL.md +27 -0
  16. data/handbook/workflow-instructions/lint/process-report.wf.md +175 -0
  17. data/handbook/workflow-instructions/lint/run.wf.md +145 -0
  18. data/lib/ace/lint/atoms/allowed_tools_validator.rb +100 -0
  19. data/lib/ace/lint/atoms/base_runner.rb +239 -0
  20. data/lib/ace/lint/atoms/comment_validator.rb +63 -0
  21. data/lib/ace/lint/atoms/config_locator.rb +162 -0
  22. data/lib/ace/lint/atoms/frontmatter_extractor.rb +74 -0
  23. data/lib/ace/lint/atoms/kramdown_parser.rb +81 -0
  24. data/lib/ace/lint/atoms/pattern_matcher.rb +96 -0
  25. data/lib/ace/lint/atoms/rubocop_runner.rb +67 -0
  26. data/lib/ace/lint/atoms/skill_schema_loader.rb +83 -0
  27. data/lib/ace/lint/atoms/standardrb_runner.rb +45 -0
  28. data/lib/ace/lint/atoms/type_detector.rb +121 -0
  29. data/lib/ace/lint/atoms/validator_registry.rb +113 -0
  30. data/lib/ace/lint/atoms/yaml_parser.rb +11 -0
  31. data/lib/ace/lint/atoms/yaml_validator.rb +69 -0
  32. data/lib/ace/lint/cli/commands/lint.rb +318 -0
  33. data/lib/ace/lint/cli.rb +25 -0
  34. data/lib/ace/lint/models/lint_result.rb +87 -0
  35. data/lib/ace/lint/models/validation_error.rb +31 -0
  36. data/lib/ace/lint/molecules/frontmatter_validator.rb +131 -0
  37. data/lib/ace/lint/molecules/group_resolver.rb +122 -0
  38. data/lib/ace/lint/molecules/kramdown_formatter.rb +66 -0
  39. data/lib/ace/lint/molecules/markdown_linter.rb +249 -0
  40. data/lib/ace/lint/molecules/offense_deduplicator.rb +65 -0
  41. data/lib/ace/lint/molecules/ruby_linter.rb +205 -0
  42. data/lib/ace/lint/molecules/skill_validator.rb +462 -0
  43. data/lib/ace/lint/molecules/validator_chain.rb +150 -0
  44. data/lib/ace/lint/molecules/yaml_linter.rb +53 -0
  45. data/lib/ace/lint/organisms/lint_doctor.rb +289 -0
  46. data/lib/ace/lint/organisms/lint_orchestrator.rb +294 -0
  47. data/lib/ace/lint/organisms/report_generator.rb +213 -0
  48. data/lib/ace/lint/organisms/result_reporter.rb +130 -0
  49. data/lib/ace/lint/version.rb +7 -0
  50. data/lib/ace/lint.rb +141 -0
  51. metadata +248 -0
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../atoms/validator_registry"
4
+ require_relative "../atoms/config_locator"
5
+ require_relative "offense_deduplicator"
6
+
7
+ module Ace
8
+ module Lint
9
+ module Molecules
10
+ # Executes multiple validators sequentially and aggregates results
11
+ # Handles validator availability, fallbacks, and result deduplication
12
+ class ValidatorChain
13
+ attr_reader :validators, :fallback_validators, :warnings
14
+
15
+ # Initialize with validators to run
16
+ # @param validators [Array<Symbol>] Primary validators to run
17
+ # @param fallback_validators [Array<Symbol>] Fallback validators if primary unavailable
18
+ # @param project_root [String, nil] Project root for config lookup
19
+ def initialize(validators, fallback_validators: [], project_root: nil)
20
+ @validators = Array(validators)
21
+ @fallback_validators = Array(fallback_validators)
22
+ @project_root = project_root || Dir.pwd
23
+ @warnings = []
24
+ end
25
+
26
+ # Run validators on file(s)
27
+ # @param file_paths [Array<String>] Paths to lint
28
+ # @param fix [Boolean] Apply autofix
29
+ # @return [Hash] Aggregated result with :success, :errors, :warnings, :runners
30
+ def run(file_paths, fix: false)
31
+ paths = Array(file_paths)
32
+ return empty_result if paths.empty?
33
+
34
+ # Determine which validators to actually run
35
+ active_validators = resolve_validators
36
+
37
+ if active_validators.empty?
38
+ return unavailable_result
39
+ end
40
+
41
+ # Run each validator and collect results
42
+ all_results = []
43
+ runners_used = []
44
+
45
+ active_validators.each do |validator_name|
46
+ runner = Atoms::ValidatorRegistry.runner_for(validator_name)
47
+ next unless runner
48
+
49
+ # Look up config for this validator using ConfigLocator
50
+ config = Atoms::ConfigLocator.locate(validator_name, project_root: @project_root)
51
+ config_path = config[:exists] ? config[:path] : nil
52
+
53
+ result = runner.run(paths, fix: fix, config_path: config_path)
54
+ all_results << result
55
+ runners_used << validator_name
56
+ end
57
+
58
+ # Aggregate and deduplicate results
59
+ aggregate_results(all_results, runners_used)
60
+ end
61
+
62
+ private
63
+
64
+ # Resolve which validators to run based on availability
65
+ # @return [Array<Symbol>] Validators to run
66
+ def resolve_validators
67
+ active = []
68
+
69
+ # Check primary validators
70
+ @validators.each do |name|
71
+ if Atoms::ValidatorRegistry.available?(name)
72
+ active << name
73
+ else
74
+ @warnings << "Validator '#{name}' is not available, skipping"
75
+ end
76
+ end
77
+
78
+ # If no primary validators available, try fallbacks
79
+ if active.empty?
80
+ @fallback_validators.each do |name|
81
+ if Atoms::ValidatorRegistry.available?(name)
82
+ active << name
83
+ @warnings << "Using fallback validator '#{name}'"
84
+ end
85
+ end
86
+ end
87
+
88
+ active
89
+ end
90
+
91
+ # Aggregate results from multiple validators
92
+ # @param results [Array<Hash>] Results from each validator
93
+ # @param runners [Array<Symbol>] Validators that were run
94
+ # @return [Hash] Aggregated result
95
+ def aggregate_results(results, runners)
96
+ return empty_result if results.empty?
97
+
98
+ all_errors = []
99
+ all_warnings = []
100
+
101
+ results.each do |result|
102
+ all_errors.concat(result[:errors] || [])
103
+ all_warnings.concat(result[:warnings] || [])
104
+ end
105
+
106
+ # Deduplicate by line:column:message using OffenseDeduplicator
107
+ deduped_errors = OffenseDeduplicator.deduplicate(all_errors)
108
+ deduped_warnings = OffenseDeduplicator.deduplicate(all_warnings)
109
+
110
+ # Success only if all validators succeeded
111
+ success = results.all? { |r| r[:success] }
112
+
113
+ {
114
+ success: success,
115
+ errors: deduped_errors,
116
+ warnings: deduped_warnings,
117
+ runners: runners,
118
+ chain_warnings: @warnings
119
+ }
120
+ end
121
+
122
+ # Empty result when no files to lint
123
+ # @return [Hash] Empty success result
124
+ def empty_result
125
+ {
126
+ success: true,
127
+ errors: [],
128
+ warnings: [],
129
+ runners: [],
130
+ chain_warnings: @warnings
131
+ }
132
+ end
133
+
134
+ # Result when no validators are available
135
+ # @return [Hash] Error result
136
+ def unavailable_result
137
+ {
138
+ success: false,
139
+ errors: [{
140
+ message: "No validators available. Install StandardRB: gem install standardrb"
141
+ }],
142
+ warnings: [],
143
+ runners: [],
144
+ chain_warnings: @warnings
145
+ }
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../atoms/yaml_validator"
4
+ require_relative "../models/lint_result"
5
+ require_relative "../models/validation_error"
6
+
7
+ module Ace
8
+ module Lint
9
+ module Molecules
10
+ # Validates YAML syntax via Psych
11
+ class YamlLinter
12
+ # Validate YAML file
13
+ # @param file_path [String] Path to YAML file
14
+ # @return [Models::LintResult] Validation result
15
+ def self.lint(file_path)
16
+ content = File.read(file_path)
17
+ lint_content(file_path, content)
18
+ rescue Errno::ENOENT
19
+ Models::LintResult.new(
20
+ file_path: file_path,
21
+ success: false,
22
+ errors: [Models::ValidationError.new(message: "File not found: #{file_path}")]
23
+ )
24
+ rescue => e
25
+ Models::LintResult.new(
26
+ file_path: file_path,
27
+ success: false,
28
+ errors: [Models::ValidationError.new(message: "Error reading file: #{e.message}")]
29
+ )
30
+ end
31
+
32
+ # Validate YAML content
33
+ # @param file_path [String] Path for reference
34
+ # @param content [String] YAML content
35
+ # @return [Models::LintResult] Validation result
36
+ def self.lint_content(file_path, content)
37
+ result = Atoms::YamlValidator.validate(content)
38
+
39
+ errors = result[:errors].map do |msg|
40
+ Models::ValidationError.new(message: msg, severity: :error)
41
+ end
42
+
43
+ Models::LintResult.new(
44
+ file_path: file_path,
45
+ success: result[:valid],
46
+ errors: errors,
47
+ warnings: []
48
+ )
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "yaml"
5
+
6
+ require_relative "../atoms/validator_registry"
7
+ require_relative "../atoms/config_locator"
8
+ require_relative "../molecules/group_resolver"
9
+
10
+ module Ace
11
+ module Lint
12
+ module Organisms
13
+ # Diagnostic tool for checking linting configuration health
14
+ # Checks validator availability, config files, and pattern coverage
15
+ class LintDoctor
16
+ # Diagnostic result structure
17
+ DiagnosticResult = Struct.new(:category, :level, :message, :details, keyword_init: true) do
18
+ def error?
19
+ level == :error
20
+ end
21
+
22
+ def warning?
23
+ level == :warning
24
+ end
25
+
26
+ def info?
27
+ level == :info
28
+ end
29
+ end
30
+
31
+ attr_reader :project_root, :groups, :diagnostics
32
+
33
+ # Initialize doctor with configuration
34
+ # @param project_root [String] Project root directory
35
+ # @param groups [Hash, nil] Ruby validator groups configuration
36
+ def initialize(project_root: Dir.pwd, groups: nil)
37
+ @project_root = project_root
38
+ @groups = groups
39
+ @diagnostics = []
40
+ end
41
+
42
+ # Run all diagnostic checks
43
+ # @return [Array<DiagnosticResult>] All diagnostic results
44
+ def diagnose
45
+ @diagnostics = []
46
+
47
+ check_validator_availability
48
+ check_config_files
49
+ check_pattern_coverage if @groups
50
+
51
+ @diagnostics
52
+ end
53
+
54
+ # Check if all configured validators are available
55
+ # @return [Array<DiagnosticResult>] Validator availability diagnostics
56
+ def check_validator_availability
57
+ results = []
58
+
59
+ # Check registered validators
60
+ Atoms::ValidatorRegistry.registered_validators.each do |name|
61
+ results << if Atoms::ValidatorRegistry.available?(name)
62
+ DiagnosticResult.new(
63
+ category: :validator,
64
+ level: :info,
65
+ message: "#{name}: available",
66
+ details: {validator: name, status: :available}
67
+ )
68
+ else
69
+ DiagnosticResult.new(
70
+ category: :validator,
71
+ level: :warning,
72
+ message: "#{name}: not installed",
73
+ details: {validator: name, status: :unavailable}
74
+ )
75
+ end
76
+ end
77
+
78
+ # Check configured validators in groups
79
+ if @groups
80
+ configured_validators = collect_configured_validators
81
+ configured_validators.each do |name|
82
+ unless Atoms::ValidatorRegistry.available?(name)
83
+ results << DiagnosticResult.new(
84
+ category: :validator,
85
+ level: :warning,
86
+ message: "Configured validator '#{name}' is not available",
87
+ details: {validator: name, status: :configured_unavailable}
88
+ )
89
+ end
90
+ end
91
+ end
92
+
93
+ @diagnostics.concat(results)
94
+ results
95
+ end
96
+
97
+ # Check if configured config files exist
98
+ # @return [Array<DiagnosticResult>] Config file diagnostics
99
+ def check_config_files
100
+ results = []
101
+
102
+ # Check for each validator's config
103
+ Atoms::ValidatorRegistry.registered_validators.each do |name|
104
+ config = Atoms::ConfigLocator.locate(name, project_root: @project_root)
105
+
106
+ case config[:source]
107
+ when :explicit
108
+ if config[:exists]
109
+ results << DiagnosticResult.new(
110
+ category: :config,
111
+ level: :info,
112
+ message: "#{name}: using explicit config at #{config[:path]}",
113
+ details: {validator: name, source: :explicit, path: config[:path]}
114
+ )
115
+ # Validate YAML syntax
116
+ yaml_error = validate_yaml_syntax(config[:path], name)
117
+ results << yaml_error if yaml_error
118
+ else
119
+ results << DiagnosticResult.new(
120
+ category: :config,
121
+ level: :error,
122
+ message: "#{name}: explicit config not found at #{config[:path]}",
123
+ details: {validator: name, source: :explicit, path: config[:path], exists: false}
124
+ )
125
+ end
126
+ when :ace_config
127
+ results << DiagnosticResult.new(
128
+ category: :config,
129
+ level: :info,
130
+ message: "#{name}: using .ace/lint config at #{config[:path]}",
131
+ details: {validator: name, source: :ace_config, path: config[:path]}
132
+ )
133
+ # Validate YAML syntax
134
+ yaml_error = validate_yaml_syntax(config[:path], name)
135
+ results << yaml_error if yaml_error
136
+ when :native
137
+ results << DiagnosticResult.new(
138
+ category: :config,
139
+ level: :info,
140
+ message: "#{name}: using native config at #{config[:path]}",
141
+ details: {validator: name, source: :native, path: config[:path]}
142
+ )
143
+ # Validate YAML syntax
144
+ yaml_error = validate_yaml_syntax(config[:path], name)
145
+ results << yaml_error if yaml_error
146
+ when :gem_defaults
147
+ results << DiagnosticResult.new(
148
+ category: :config,
149
+ level: :info,
150
+ message: "#{name}: using gem default config",
151
+ details: {validator: name, source: :gem_defaults, path: config[:path]}
152
+ )
153
+ when :none
154
+ results << DiagnosticResult.new(
155
+ category: :config,
156
+ level: :info,
157
+ message: "#{name}: using tool defaults (no config file)",
158
+ details: {validator: name, source: :none}
159
+ )
160
+ end
161
+ end
162
+
163
+ @diagnostics.concat(results)
164
+ results
165
+ end
166
+
167
+ # Check pattern coverage in groups configuration
168
+ # @return [Array<DiagnosticResult>] Pattern coverage diagnostics
169
+ def check_pattern_coverage
170
+ return [] unless @groups
171
+
172
+ results = []
173
+
174
+ # Check for default group
175
+ unless @groups.key?(:default) || @groups.key?("default")
176
+ results << DiagnosticResult.new(
177
+ category: :pattern,
178
+ level: :warning,
179
+ message: "No 'default' group defined - some files may not be matched",
180
+ details: {issue: :no_default_group}
181
+ )
182
+ end
183
+
184
+ # Check for overlapping patterns (info only)
185
+ all_patterns = []
186
+ @groups.each do |name, config|
187
+ patterns = config[:patterns] || config["patterns"] || []
188
+ patterns.each do |pattern|
189
+ all_patterns << {group: name, pattern: pattern}
190
+ end
191
+ end
192
+
193
+ # Info about configured groups
194
+ @groups.each do |name, config|
195
+ validators = config[:validators] || config["validators"] || []
196
+ patterns = config[:patterns] || config["patterns"] || []
197
+
198
+ results << DiagnosticResult.new(
199
+ category: :pattern,
200
+ level: :info,
201
+ message: "Group '#{name}': #{validators.join(", ")} for #{patterns.size} pattern(s)",
202
+ details: {group: name, validators: validators, pattern_count: patterns.size}
203
+ )
204
+ end
205
+
206
+ @diagnostics.concat(results)
207
+ results
208
+ end
209
+
210
+ # Check if there are any errors
211
+ # @return [Boolean] True if any errors found
212
+ def errors?
213
+ @diagnostics.any?(&:error?)
214
+ end
215
+
216
+ # Check if there are any warnings
217
+ # @return [Boolean] True if any warnings found
218
+ def warnings?
219
+ @diagnostics.any?(&:warning?)
220
+ end
221
+
222
+ # Get all errors
223
+ # @return [Array<DiagnosticResult>] Error diagnostics
224
+ def errors
225
+ @diagnostics.select(&:error?)
226
+ end
227
+
228
+ # Get all warnings
229
+ # @return [Array<DiagnosticResult>] Warning diagnostics
230
+ def warnings
231
+ @diagnostics.select(&:warning?)
232
+ end
233
+
234
+ private
235
+
236
+ # Collect all validators referenced in groups configuration
237
+ # @return [Array<Symbol>] All configured validator names
238
+ def collect_configured_validators
239
+ return [] unless @groups
240
+
241
+ validators = Set.new
242
+
243
+ @groups.each_value do |config|
244
+ (config[:validators] || config["validators"] || []).each do |v|
245
+ validators << v.to_sym
246
+ end
247
+ (config[:fallback_validators] || config["fallback_validators"] || []).each do |v|
248
+ validators << v.to_sym
249
+ end
250
+ end
251
+
252
+ validators.to_a
253
+ end
254
+
255
+ # Validate YAML syntax of a config file
256
+ # @param path [String] Path to the config file
257
+ # @param validator_name [Symbol, String] Name of the validator
258
+ # @return [DiagnosticResult, nil] Error diagnostic if YAML is invalid, nil otherwise
259
+ def validate_yaml_syntax(path, validator_name)
260
+ return nil unless path && File.exist?(path)
261
+
262
+ YAML.safe_load_file(path, permitted_classes: [Date, Symbol], aliases: true)
263
+ nil
264
+ rescue Psych::SyntaxError => e
265
+ DiagnosticResult.new(
266
+ category: :config,
267
+ level: :error,
268
+ message: "#{validator_name}: YAML syntax error in #{path}: #{e.message}",
269
+ details: {validator: validator_name, path: path, error: e.message, line: e.line, column: e.column}
270
+ )
271
+ rescue Psych::BadAlias => e
272
+ DiagnosticResult.new(
273
+ category: :config,
274
+ level: :error,
275
+ message: "#{validator_name}: YAML alias error in #{path}: #{e.message}",
276
+ details: {validator: validator_name, path: path, error: e.message}
277
+ )
278
+ rescue Errno::ENOENT, Errno::EACCES => e
279
+ DiagnosticResult.new(
280
+ category: :config,
281
+ level: :warning,
282
+ message: "#{validator_name}: Could not read #{path}: #{e.message}",
283
+ details: {validator: validator_name, path: path, error: e.message}
284
+ )
285
+ end
286
+ end
287
+ end
288
+ end
289
+ end