ace-lint 0.25.0 → 0.27.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3b28e15301bd4cac4a828be1dbe5a7c13bb09dc8014d0f0d803fcd53fe0e7e88
4
- data.tar.gz: 8f8fe4b4948fd41aa12af6588a4a6e90a1a6eaa357208aaab54f997ebfbd69f1
3
+ metadata.gz: 8d08aca270559c7f2cb97ffab6f7d0cc28fab077a58b51763d7d9ac0e223a22b
4
+ data.tar.gz: 305b0142f0f4b89104559f0079f98f3286e41fd8876ec236c24e3bce838f5df7
5
5
  SHA512:
6
- metadata.gz: 24cf2c6f78e9df0663cab9c9f073d4e1ab07d9206146b38d8c579b7874d285f07423b5d38602b6dafa683b957866f2f04a79be91dccac8cbcf6c67054c9fe0a5
7
- data.tar.gz: e72a57f50f8fa8ef5a9ed5593134c4c72aa29119c66023596d50b573edd6bf44ca88cbfa5c7fba76f6f0b967476cd2ff3050b3548fbc09a4cac1a9d53bd44e48
6
+ metadata.gz: 033a583ec905b507983508780c806a13bbb054ac6423c3c7619279c84aebf85bb60db054b53ad7d7f4f887b9a4f004006de67d1edf90952aeaa706bedeacc5ef
7
+ data.tar.gz: b444718179323bb8e2c1b6617605a54349e22e170c02b81add84b76c282c036ccad901edba5aa0386b85515a95d29ea63331c13d50fb0da5f9d7b77b6e9b1fc8
@@ -1,5 +1,6 @@
1
1
  # ace-lint general configuration
2
2
  # Place this in .ace/lint/config.yml in your project
3
3
 
4
- # General ace-lint settings (currently using defaults)
5
- # Future: enable/disable specific linters, custom rules, etc.
4
+ lint:
5
+ # Default model for --auto-fix-with-agent when --model is not provided
6
+ doctor_agent_model: "gemini:flash-latest@yolo"
data/CHANGELOG.md CHANGED
@@ -6,6 +6,84 @@ The format is based on [Keep a Changelog][1], and this project adheres to [Seman
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.27.3] - 2026-03-26
10
+
11
+ ### Fixed
12
+ - Corrected `--auto-fix --dry-run` file-count reporting to count unique lint result file paths instead of parsing formatted issue strings.
13
+
14
+ ### Changed
15
+ - Extracted deterministic and agent-assisted auto-fix orchestration from the lint CLI command into `Organisms::AutoFixOrchestrator`.
16
+ - Centralized `ace-llm` test load-path setup in `test_helper` for agent-fix command tests.
17
+
18
+ ### Technical
19
+ - Documented the frontmatter-prefix reconstruction contract in `MarkdownSurgicalFixer`.
20
+ - Marked internal structural-change guardrail helpers in `KramdownFormatter` as private class methods.
21
+
22
+ ## [0.27.2] - 2026-03-26
23
+
24
+ ### Fixed
25
+ - Made `--auto-fix-with-agent` apply concrete file edits from model output and fail fast when no editable changes are returned.
26
+ - Prevented stale pre-agent lint errors from being carried into post-agent validation results.
27
+ - Hardened markdown formatting guardrails to treat any HTML attribute-count change (increase or decrease) as structural risk.
28
+
29
+ ### Changed
30
+ - Added an explicit warning before agent-assisted fixes that full file contents are sent to the selected model.
31
+ - Added payload-size limits for agent-fix prompt file content to avoid oversized requests.
32
+ - Refactored markdown fence-state handling to use a shared fence-aware line iterator across linter and fixer paths.
33
+
34
+ ### Technical
35
+ - Added non-dry-run command tests for agent fix mode success and no-edit failure behavior.
36
+ - Added formatter coverage for HTML attribute-removal structural change detection.
37
+
38
+ ## [0.27.1] - 2026-03-26
39
+
40
+ ### Fixed
41
+ - Preserved markdown link destinations containing nested parentheses during typography-safe surgical fixes.
42
+ - Hardened agent prompt construction for `--auto-fix-with-agent` by using dynamic code fences that remain valid when file content already contains fenced blocks.
43
+
44
+ ### Changed
45
+ - Updated `--auto-fix --dry-run` preview wording to avoid implying every issue is deterministically fixable.
46
+ - Aligned `--auto-fix` exit behavior with normal lint semantics: warning-only results now exit successfully while error results still fail.
47
+
48
+ ### Technical
49
+ - Optimized `run_auto_fix` to skip redundant fix-mode passes for non-fixable file types while preserving final lint validation.
50
+
51
+ ## [0.27.0] - 2026-03-26
52
+
53
+ ### Added
54
+ - Added deterministic repair flags to `ace-lint`: `--auto-fix`, `--auto-fix-with-agent`, `--dry-run` (`-n`), and `--model`.
55
+ - Added agent-assisted lint repair flow that builds a structured prompt with remaining violations and full file content for affected files.
56
+ - Added command-level coverage for auto-fix dry-run, alias parity, warning precedence, and exit semantics.
57
+
58
+ ### Fixed
59
+ - Fixed auto-fix exit behavior to return non-zero when violations remain after deterministic repair.
60
+ - Fixed help and docs drift by aligning CLI docs/examples and E2E fix-mode guidance with the new auto-fix contract.
61
+
62
+ ### Changed
63
+ - Changed `--fix` semantics to an alias of `--auto-fix` (deterministic fix then re-lint).
64
+ - Changed auto-fix modes to ignore `--format` with an explicit warning.
65
+ - Added `lint.doctor_agent_model` default configuration for agent-assisted repair model selection.
66
+
67
+ ### Technical
68
+ - Expanded task specification verification checklist evidence for the auto-fix and agent-assisted workflow implementation.
69
+
70
+ ## [0.26.0] - 2026-03-26
71
+
72
+ ### Added
73
+ - Added `MarkdownSurgicalFixer` for markdown-family `--fix` operations that apply targeted line edits without full document reserialization.
74
+ - Added structural guardrails for markdown `--format` to skip unsafe kramdown rewrites when frontmatter, code-block, table, or HTML-attribute drift is detected.
75
+
76
+ ### Fixed
77
+ - Fixed markdown style checks to ignore heading/list spacing checks inside fenced code blocks.
78
+ - Added trailing whitespace detection for markdown style validation.
79
+
80
+ ### Changed
81
+ - Changed markdown-family `--fix` behavior (`markdown`, `skill`, `workflow`, `agent`) to surgical edits, with `--fix --format` executing surgical fix first then guarded format.
82
+ - Updated CLI/help and usage docs to describe surgical `--fix` semantics and guarded `--format` behavior.
83
+
84
+ ### Technical
85
+ - Expanded ace-lint tests for surgical fixer behavior, formatter guardrails, and orchestrator ordering.
86
+
9
87
  ## [0.25.0] - 2026-03-23
10
88
 
11
89
  ### Fixed
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  Ruby-native linting for markdown, YAML, and Ruby with no Node.js or Python runtime required.
5
5
 
6
- <img src="https://raw.githubusercontent.com/cs3b/ace/main/docs/brand/AgenticCodingEnvironment.Logo.XS.jpg" alt="ACE Logo" width="480">
6
+ <img src="../docs/brand/AgenticCodingEnvironment.Logo.XS.jpg" alt="ACE Logo" width="480">
7
7
  <br><br>
8
8
 
9
9
  <a href="https://rubygems.org/gems/ace-lint"><img alt="Gem Version" src="https://img.shields.io/gem/v/ace-lint.svg" /></a>
@@ -24,13 +24,15 @@
24
24
 
25
25
  1. Run lint checks across markdown, YAML, Ruby, and frontmatter validators in one pass.
26
26
  2. Apply safe auto-fixes with `--fix` for markdown and Ruby style issues.
27
+ For markdown-family files, `--fix` is surgical (line-level) and does not reserialize structure.
28
+ Use `--format` when you explicitly want a guarded kramdown rewrite.
27
29
  3. Report results with colorized pass/fail output, using configuration cascade from CLI flags through project and user defaults.
28
30
 
29
31
  ## Use Cases
30
32
 
31
33
  **Validate markdown, YAML, and Ruby in one pass** - run [`ace-lint`](docs/usage.md) for doc and code lint checks without mixing Node/Python tooling into Ruby projects. Use `/as-lint-run` for the full agent-driven lint workflow.
32
34
 
33
- **Apply low-risk formatting fixes before review** - use `--fix` to automatically clean markdown and Ruby style issues prior to manual review. Run `/as-lint-fix-issue-from` to fix specific issues identified in lint reports.
35
+ **Apply low-risk formatting fixes before review** - use `--fix` to automatically clean markdown and Ruby style issues prior to manual review. For markdown, fixes are surgical to preserve frontmatter/code blocks/tables/HTML. Run `/as-lint-fix-issue-from` to fix specific issues identified in lint reports.
34
36
 
35
37
  **Standardize lint behavior across teams and repos** - rely on user/project/default cascade settings from [ace-support-config](../ace-support-config) for consistent validator sets and output modes.
36
38
 
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ace/support/cli"
4
+ require_relative "../../atoms/type_detector"
4
5
  require_relative "../../atoms/validator_registry"
5
6
  require_relative "../../organisms/lint_orchestrator"
7
+ require_relative "../../organisms/auto_fix_orchestrator"
6
8
  require_relative "../../organisms/result_reporter"
7
9
  require_relative "../../organisms/report_generator"
8
10
  require_relative "../../organisms/lint_doctor"
@@ -18,6 +20,12 @@ module Ace
18
20
  class Lint < Ace::Support::Cli::Command
19
21
  include Ace::Support::Cli::Base
20
22
 
23
+ AGENT_FIX_FILE_BLOCK_START = "<<ACE_FILE:".freeze
24
+ AGENT_FIX_FILE_BLOCK_END = "<<ACE_END_FILE>>".freeze
25
+ AGENT_FIX_NO_CHANGES = "<<ACE_NO_CHANGES>>".freeze
26
+ AGENT_FIX_MAX_FILE_BYTES = 200_000
27
+ AGENT_FIX_MAX_TOTAL_BYTES = 1_000_000
28
+
21
29
  desc <<~DESC.strip
22
30
  Lint markdown, YAML, Ruby, and frontmatter files
23
31
 
@@ -42,8 +50,11 @@ module Ace
42
50
  # Examples shown in help output
43
51
  example [
44
52
  "README.md # Auto-detect type from extension",
45
- "--fix README.md # Auto-fix and format",
46
- "docs/**/*.md --format # Format with kramdown",
53
+ "--auto-fix README.md # Deterministic auto-fix then re-lint",
54
+ "--fix README.md # Alias for --auto-fix",
55
+ "--auto-fix --dry-run README.md # Preview fixes without writing",
56
+ "--auto-fix-with-agent README.md # Auto-fix then agent for remaining issues",
57
+ "docs/**/*.md --format # Format with guarded kramdown",
47
58
  "**/*.rb --validators standardrb,rubocop # Multiple validators",
48
59
  "--doctor # Diagnose lint configuration",
49
60
  "--doctor-verbose # Diagnose with all details"
@@ -54,8 +65,13 @@ module Ace
54
65
  argument :files, required: false, type: :array, desc: "Files to lint"
55
66
 
56
67
  # Method options (maintaining parity with Thor implementation)
57
- option :fix, type: :boolean, aliases: %w[-f], desc: "Auto-fix/format files"
58
- option :format, type: :boolean, desc: "Format files with kramdown"
68
+ option :auto_fix, type: :boolean, aliases: %w[-f --fix], desc: "Deterministic auto-fix, then re-lint"
69
+ option :auto_fix_with_agent, type: :boolean,
70
+ desc: "Run --auto-fix, then launch agent for remaining issues"
71
+ option :dry_run, type: :boolean, aliases: %w[-n],
72
+ desc: "Preview auto-fixes without modifying files"
73
+ option :model, type: :string, desc: "Provider:model for agent-assisted fix"
74
+ option :format, type: :boolean, desc: "Format markdown with guarded kramdown"
59
75
  option :type, type: :string, aliases: %w[-t], desc: "File type (markdown, yaml, ruby, frontmatter)"
60
76
  option :line_width, type: :integer, desc: "Line width for formatting (default: 120)"
61
77
  option :validators, type: :string, desc: "Comma-separated list of validators (e.g., standardrb,rubocop)"
@@ -116,7 +132,22 @@ module Ace
116
132
  lint_options = prepare_options(clean_options)
117
133
 
118
134
  # Lint files
119
- results = orchestrator.lint_files(expanded_paths, options: lint_options)
135
+ if auto_fix_mode?(clean_options)
136
+ warn_format_ignored_if_needed(clean_options)
137
+ auto_fix_orchestrator = build_auto_fix_orchestrator(
138
+ orchestrator,
139
+ lint_options: lint_options,
140
+ options: clean_options
141
+ )
142
+ if clean_options[:dry_run]
143
+ auto_fix_orchestrator.run_dry_run(expanded_paths)
144
+ return
145
+ end
146
+
147
+ results = auto_fix_orchestrator.run(expanded_paths)
148
+ else
149
+ results = orchestrator.lint_files(expanded_paths, options: lint_options)
150
+ end
120
151
 
121
152
  # Generate report unless --no-report flag is set
122
153
  report_dir = nil
@@ -138,8 +169,16 @@ module Ace
138
169
  verbose = !clean_options[:quiet]
139
170
  Organisms::ResultReporter.report(results, verbose: verbose, report_dir: report_dir, report_files: report_files)
140
171
 
141
- # Raise on lint failures
142
- if results.any?(&:failed?)
172
+ # Raise on remaining issues
173
+ if auto_fix_mode?(clean_options)
174
+ remaining_errors = results.sum(&:error_count)
175
+ if remaining_errors.positive?
176
+ raise Ace::Support::Cli::Error.new(
177
+ "#{remaining_errors} error violation(s) remain after auto-fix",
178
+ exit_code: 1
179
+ )
180
+ end
181
+ elsif results.any?(&:failed?)
143
182
  failed_count = results.count(&:failed?)
144
183
  exit_code = Organisms::ResultReporter.exit_code(results)
145
184
  raise Ace::Support::Cli::Error.new("#{failed_count} file(s) had lint errors", exit_code: exit_code)
@@ -289,7 +328,7 @@ module Ace
289
328
  prepared[:type] = options[:type].to_sym if options[:type]
290
329
 
291
330
  # Fix/format options
292
- prepared[:fix] = options[:fix] if options[:fix]
331
+ prepared[:fix] = true if auto_fix_mode?(options)
293
332
  prepared[:format] = options[:format] if options[:format]
294
333
 
295
334
  # Validators option (comma-separated list)
@@ -311,6 +350,30 @@ module Ace
311
350
  def parse_validators(validators_str)
312
351
  validators_str.split(",").map { |v| v.strip.downcase.to_sym }
313
352
  end
353
+
354
+ def auto_fix_mode?(options)
355
+ options[:auto_fix] || options[:auto_fix_with_agent]
356
+ end
357
+
358
+ def warn_format_ignored_if_needed(options)
359
+ return unless options[:format]
360
+
361
+ warn "[ace-lint] Warning: --format is ignored when using --auto-fix or --auto-fix-with-agent"
362
+ end
363
+
364
+ def build_auto_fix_orchestrator(orchestrator, lint_options:, options:)
365
+ Organisms::AutoFixOrchestrator.new(
366
+ orchestrator: orchestrator,
367
+ lint_options: lint_options,
368
+ with_agent: options[:auto_fix_with_agent],
369
+ model: options[:model],
370
+ file_block_start: AGENT_FIX_FILE_BLOCK_START,
371
+ file_block_end: AGENT_FIX_FILE_BLOCK_END,
372
+ no_changes_marker: AGENT_FIX_NO_CHANGES,
373
+ max_file_bytes: AGENT_FIX_MAX_FILE_BYTES,
374
+ max_total_bytes: AGENT_FIX_MAX_TOTAL_BYTES
375
+ )
376
+ end
314
377
  end
315
378
  end
316
379
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../atoms/kramdown_parser"
4
+ require_relative "../atoms/frontmatter_extractor"
5
+ require_relative "markdown_linter"
4
6
 
5
7
  module Ace
6
8
  module Lint
@@ -10,21 +12,35 @@ module Ace
10
12
  # Format markdown file in place
11
13
  # @param file_path [String] Path to markdown file
12
14
  # @param options [Hash] Kramdown options
13
- # @return [Hash] Result with :success, :formatted, :errors
14
- def self.format_file(file_path, options: {})
15
+ # @param guardrails [Boolean] Enable structural safety checks before write
16
+ # @return [Hash] Result with :success, :formatted, :errors, :warnings
17
+ def self.format_file(file_path, options: {}, guardrails: false)
15
18
  content = File.read(file_path)
16
19
  result = format_content(content, options: options)
17
20
 
18
- if result[:success] && result[:formatted_content]
19
- File.write(file_path, result[:formatted_content])
20
- {success: true, formatted: true, errors: []}
21
- else
22
- {success: false, formatted: false, errors: result[:errors]}
21
+ return {success: false, formatted: false, errors: result[:errors], warnings: []} unless result[:success]
22
+
23
+ formatted_content = result[:formatted_content]
24
+ return {success: true, formatted: false, errors: [], warnings: []} if formatted_content == content
25
+
26
+ if guardrails
27
+ structural_changes = detect_structural_changes(content, formatted_content)
28
+ if structural_changes.any?
29
+ return {
30
+ success: true,
31
+ formatted: false,
32
+ errors: [],
33
+ warnings: ["Skipped formatting due to structural change risk: #{structural_changes.join(", ")}"]
34
+ }
35
+ end
23
36
  end
37
+
38
+ File.write(file_path, formatted_content)
39
+ {success: true, formatted: true, errors: [], warnings: []}
24
40
  rescue Errno::ENOENT
25
- {success: false, formatted: false, errors: ["File not found: #{file_path}"]}
41
+ {success: false, formatted: false, errors: ["File not found: #{file_path}"], warnings: []}
26
42
  rescue => e
27
- {success: false, formatted: false, errors: ["Error formatting file: #{e.message}"]}
43
+ {success: false, formatted: false, errors: ["Error formatting file: #{e.message}"], warnings: []}
28
44
  end
29
45
 
30
46
  # Format markdown content
@@ -60,6 +76,57 @@ module Ace
60
76
 
61
77
  result[:formatted_content] != content
62
78
  end
79
+
80
+ def self.detect_structural_changes(original, formatted)
81
+ changes = []
82
+ changes << "frontmatter" if frontmatter_changed?(original, formatted)
83
+ changes << "code blocks" if fence_count_changed?(original, formatted)
84
+ changes << "tables" if table_row_count_changed?(original, formatted)
85
+ changes << "html attributes" if html_attributes_changed?(original, formatted)
86
+ changes
87
+ end
88
+
89
+ def self.frontmatter_changed?(original, formatted)
90
+ original_frontmatter = Atoms::FrontmatterExtractor.extract(original)
91
+ formatted_frontmatter = Atoms::FrontmatterExtractor.extract(formatted)
92
+
93
+ original_frontmatter[:has_frontmatter] != formatted_frontmatter[:has_frontmatter] ||
94
+ original_frontmatter[:frontmatter].to_s != formatted_frontmatter[:frontmatter].to_s
95
+ end
96
+
97
+ def self.fence_count_changed?(original, formatted)
98
+ fence_count(original) != fence_count(formatted)
99
+ end
100
+
101
+ def self.fence_count(content)
102
+ content.lines.count { |line| line.match?(MarkdownLinter::FENCE_PATTERN) }
103
+ end
104
+
105
+ def self.table_row_count_changed?(original, formatted)
106
+ table_row_count(original) != table_row_count(formatted)
107
+ end
108
+
109
+ def self.table_row_count(content)
110
+ content.lines.count { |line| line.match?(/^\s*\|.*\|\s*$/) }
111
+ end
112
+
113
+ def self.html_attributes_changed?(original, formatted)
114
+ html_attribute_count(formatted) != html_attribute_count(original)
115
+ end
116
+
117
+ def self.html_attribute_count(content)
118
+ content.scan(/<([A-Za-z][A-Za-z0-9:-]*)([^>]*)>/).sum do |_tag_name, attributes|
119
+ attributes.scan(/\s+[A-Za-z_:][\w:.-]*(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?/).size
120
+ end
121
+ end
122
+
123
+ private_class_method :frontmatter_changed?,
124
+ :fence_count_changed?,
125
+ :fence_count,
126
+ :table_row_count_changed?,
127
+ :table_row_count,
128
+ :html_attributes_changed?,
129
+ :html_attribute_count
63
130
  end
64
131
  end
65
132
  end