ace-docs 0.31.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 (91) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/docs/config.yml +169 -0
  3. data/.ace-defaults/docs/multi-subject-example.md +130 -0
  4. data/.ace-defaults/docs/single-subject-example.md +150 -0
  5. data/.ace-defaults/nav/protocols/guide-sources/ace-docs.yml +10 -0
  6. data/.ace-defaults/nav/protocols/prompt-sources/ace-docs.yml +34 -0
  7. data/.ace-defaults/nav/protocols/tmpl-sources/ace-docs.yml +10 -0
  8. data/.ace-defaults/nav/protocols/wfi-sources/ace-docs.yml +19 -0
  9. data/CHANGELOG.md +1082 -0
  10. data/LICENSE +21 -0
  11. data/README.md +40 -0
  12. data/Rakefile +14 -0
  13. data/exe/ace-docs +14 -0
  14. data/handbook/guides/documentation/ruby.md +16 -0
  15. data/handbook/guides/documentation/rust.md +35 -0
  16. data/handbook/guides/documentation/typescript.md +18 -0
  17. data/handbook/guides/documentation.g.md +437 -0
  18. data/handbook/guides/documents-embedded-sync.g.md +473 -0
  19. data/handbook/guides/documents-embedding.g.md +276 -0
  20. data/handbook/guides/markdown-style.g.md +290 -0
  21. data/handbook/prompts/ace-change-analyzer.system.md +113 -0
  22. data/handbook/prompts/ace-change-analyzer.user.md +95 -0
  23. data/handbook/prompts/document-analysis.md +74 -0
  24. data/handbook/prompts/document-analysis.system.md +129 -0
  25. data/handbook/prompts/markdown-style.system.md +113 -0
  26. data/handbook/skills/as-docs-create-adr/SKILL.md +35 -0
  27. data/handbook/skills/as-docs-create-api/SKILL.md +35 -0
  28. data/handbook/skills/as-docs-create-user/SKILL.md +35 -0
  29. data/handbook/skills/as-docs-maintain-adrs/SKILL.md +35 -0
  30. data/handbook/skills/as-docs-squash-changelog/SKILL.md +42 -0
  31. data/handbook/skills/as-docs-update/SKILL.md +36 -0
  32. data/handbook/skills/as-docs-update-blueprint/SKILL.md +28 -0
  33. data/handbook/skills/as-docs-update-roadmap/SKILL.md +24 -0
  34. data/handbook/skills/as-docs-update-tools/SKILL.md +36 -0
  35. data/handbook/skills/as-docs-update-usage/SKILL.md +26 -0
  36. data/handbook/templates/code-docs/javascript-jsdoc.template.md +102 -0
  37. data/handbook/templates/code-docs/ruby-yard.template.md +85 -0
  38. data/handbook/templates/project-docs/README.template.md +73 -0
  39. data/handbook/templates/project-docs/architecture.template.md +300 -0
  40. data/handbook/templates/project-docs/blueprint.template.md +165 -0
  41. data/handbook/templates/project-docs/context/ownership.yml +160 -0
  42. data/handbook/templates/project-docs/decisions/adr.template.md +60 -0
  43. data/handbook/templates/project-docs/prd.template.md +144 -0
  44. data/handbook/templates/project-docs/roadmap/roadmap.template.md +47 -0
  45. data/handbook/templates/project-docs/vision.template.md +233 -0
  46. data/handbook/templates/user-docs/user-guide.template.md +107 -0
  47. data/handbook/workflow-instructions/docs/create-adr.wf.md +334 -0
  48. data/handbook/workflow-instructions/docs/create-api.wf.md +448 -0
  49. data/handbook/workflow-instructions/docs/create-cookbook.wf.md +434 -0
  50. data/handbook/workflow-instructions/docs/create-user.wf.md +399 -0
  51. data/handbook/workflow-instructions/docs/maintain-adrs.wf.md +589 -0
  52. data/handbook/workflow-instructions/docs/squash-changelog.wf.md +246 -0
  53. data/handbook/workflow-instructions/docs/update-blueprint.wf.md +361 -0
  54. data/handbook/workflow-instructions/docs/update-context.wf.md +336 -0
  55. data/handbook/workflow-instructions/docs/update-roadmap.wf.md +421 -0
  56. data/handbook/workflow-instructions/docs/update-tools.wf.md +307 -0
  57. data/handbook/workflow-instructions/docs/update-usage.wf.md +710 -0
  58. data/handbook/workflow-instructions/docs/update.wf.md +418 -0
  59. data/lib/ace/docs/atoms/diff_filterer.rb +131 -0
  60. data/lib/ace/docs/atoms/frontmatter_free_matcher.rb +20 -0
  61. data/lib/ace/docs/atoms/git_date_resolver.rb +16 -0
  62. data/lib/ace/docs/atoms/readme_metadata_inferrer.rb +60 -0
  63. data/lib/ace/docs/atoms/terminology_extractor.rb +308 -0
  64. data/lib/ace/docs/atoms/time_range_calculator.rb +96 -0
  65. data/lib/ace/docs/atoms/timestamp_parser.rb +106 -0
  66. data/lib/ace/docs/atoms/type_inferrer.rb +70 -0
  67. data/lib/ace/docs/cli/commands/analyze.rb +351 -0
  68. data/lib/ace/docs/cli/commands/analyze_consistency.rb +185 -0
  69. data/lib/ace/docs/cli/commands/discover.rb +75 -0
  70. data/lib/ace/docs/cli/commands/scope_options.rb +71 -0
  71. data/lib/ace/docs/cli/commands/status.rb +241 -0
  72. data/lib/ace/docs/cli/commands/update.rb +198 -0
  73. data/lib/ace/docs/cli/commands/validate.rb +225 -0
  74. data/lib/ace/docs/cli.rb +60 -0
  75. data/lib/ace/docs/models/analysis_report.rb +120 -0
  76. data/lib/ace/docs/models/consistency_report.rb +259 -0
  77. data/lib/ace/docs/models/document.rb +354 -0
  78. data/lib/ace/docs/molecules/change_detector.rb +389 -0
  79. data/lib/ace/docs/molecules/document_loader.rb +133 -0
  80. data/lib/ace/docs/molecules/frontmatter_manager.rb +85 -0
  81. data/lib/ace/docs/molecules/git_date_resolver.rb +30 -0
  82. data/lib/ace/docs/organisms/cross_document_analyzer.rb +274 -0
  83. data/lib/ace/docs/organisms/document_registry.rb +318 -0
  84. data/lib/ace/docs/organisms/validator.rb +164 -0
  85. data/lib/ace/docs/prompts/compact_diff_prompt.rb +119 -0
  86. data/lib/ace/docs/prompts/consistency_prompt.rb +286 -0
  87. data/lib/ace/docs/prompts/document_analysis_prompt.rb +389 -0
  88. data/lib/ace/docs/version.rb +7 -0
  89. data/lib/ace/docs.rb +82 -0
  90. data/lib/test.rb +4 -0
  91. metadata +347 -0
@@ -0,0 +1,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "fileutils"
5
+ require "ace/core"
6
+ require "colorize"
7
+ require "ace/b36ts"
8
+ require_relative "../../organisms/document_registry"
9
+ require_relative "../../molecules/change_detector"
10
+ require_relative "../../prompts/document_analysis_prompt"
11
+
12
+ # Try to load ace-llm
13
+ begin
14
+ require "ace/llm"
15
+ rescue LoadError
16
+ # Will be handled with clear error message during execution
17
+ end
18
+
19
+ module Ace
20
+ module Docs
21
+ module CLI
22
+ module Commands
23
+ # ace-support-cli Command class for the analyze command
24
+ #
25
+ # This command handles document analysis with LLM integration.
26
+ class Analyze < Ace::Support::Cli::Command
27
+ include Ace::Support::Cli::Base
28
+
29
+ # Exit codes
30
+ EXIT_SUCCESS = 0
31
+ EXIT_ERROR = 1
32
+ EXIT_NO_CHANGES = 2
33
+ EXIT_ANALYSIS_ERROR = 3
34
+
35
+ desc <<~DESC.strip
36
+ Analyze changes for a document with LLM
37
+
38
+ Analyze git changes for a document using an LLM to understand what content
39
+ has changed and whether documentation updates are needed.
40
+
41
+ Configuration:
42
+ LLM model configured via ace-llm
43
+ Global config: ~/.ace/docs/config.yml
44
+ Project config: .ace/docs/config.yml
45
+
46
+ Output:
47
+ Analysis results printed to stdout
48
+ Exit codes: 0 (success), 1 (error)
49
+ DESC
50
+
51
+ example [
52
+ "README.md",
53
+ "docs/architecture.md --since '2025-01-01'",
54
+ "file.md --exclude-renames --exclude-moves"
55
+ ]
56
+
57
+ argument :file, required: true, desc: "File to analyze"
58
+
59
+ option :since, type: :string, desc: "Date or commit to analyze from"
60
+ option :exclude_renames, type: :boolean, desc: "Exclude renamed files from diff"
61
+ option :exclude_moves, type: :boolean, desc: "Exclude moved files from diff"
62
+
63
+ # Standard options
64
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
65
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
66
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
67
+
68
+ def call(file:, **options)
69
+ # Handle --help/-h passed as file argument
70
+ if file == "--help" || file == "-h"
71
+ # ace-support-cli will handle help automatically, so we just ignore
72
+ return EXIT_SUCCESS
73
+ end
74
+
75
+ execute_analyze(file, options)
76
+ end
77
+
78
+ private
79
+
80
+ def execute_analyze(file, options)
81
+ # Load document (file argument is enforced as required by ace-support-cli)
82
+ registry = Ace::Docs::Organisms::DocumentRegistry.new
83
+ document = registry.find_by_path(file)
84
+
85
+ unless document
86
+ warn "Error: Document not found or not managed by ace-docs: #{file}"
87
+ warn "Ensure the file has ace-docs frontmatter (doc-type, purpose)"
88
+ return EXIT_ERROR
89
+ end
90
+
91
+ puts "Analyzing changes for: #{document.display_name}".cyan
92
+ puts "Document type: #{document.doc_type}" if document.doc_type
93
+ puts "Purpose: #{document.purpose}" if document.purpose
94
+
95
+ # Show subject configuration
96
+ if document.multi_subject?
97
+ subjects = document.subject_configurations
98
+ puts "\nSubjects configured:".yellow
99
+ subjects.each do |subject|
100
+ filter_desc = subject[:filters].join(", ")
101
+ puts " - #{subject[:name]}: #{filter_desc}"
102
+ end
103
+ else
104
+ # Single subject - show filters if present
105
+ filters = document.subject_diff_filters
106
+ if filters && !filters.empty?
107
+ puts "\nSubject filters (tracking changes in):".yellow
108
+ filters.each { |f| puts " - #{f}" }
109
+ else
110
+ puts "\nNo subject filters defined (tracking all changes)".yellow
111
+ end
112
+ end
113
+
114
+ # Determine time range
115
+ since = determine_since(document, options)
116
+ puts "\nAnalyzing changes since: #{since}".cyan
117
+
118
+ # Generate filtered diff(s)
119
+ puts "Generating git diff...".cyan
120
+ diff_result = Ace::Docs::Molecules::ChangeDetector.get_diff_for_document(
121
+ document,
122
+ since: since,
123
+ options: build_diff_options(options)
124
+ )
125
+
126
+ # Check if there are changes
127
+ unless diff_result[:has_changes]
128
+ puts "\n✅ No changes detected in the specified period.".green
129
+ puts "The document appears to be up to date."
130
+ puts "\nNext steps:"
131
+ puts " • Run with different --since date to check other time periods"
132
+ puts " • Use 'ace-docs status' to see document freshness"
133
+ return EXIT_NO_CHANGES
134
+ end
135
+
136
+ # Display diff statistics
137
+ if diff_result[:multi_subject]
138
+ # Multi-subject: show stats for each subject
139
+ diffs_hash = diff_result[:diffs]
140
+ diffs_hash.each do |subject_name, diff_content|
141
+ next if diff_content.strip.empty?
142
+ lines = count_diff_lines(diff_content)
143
+ puts " ✓ #{subject_name}: #{lines} lines changed"
144
+ end
145
+ else
146
+ # Single subject
147
+ diff = diff_result[:diff]
148
+ puts "Changes detected (#{count_diff_lines(diff)} lines)"
149
+ end
150
+
151
+ # Check if ace-llm is available
152
+ unless defined?(Ace::LLM)
153
+ warn "\nError: ace-llm gem not available"
154
+ warn "Install it with: gem install ace-llm"
155
+ warn "\nOr add to your Gemfile:"
156
+ warn " gem 'ace-llm'"
157
+ return EXIT_ANALYSIS_ERROR
158
+ end
159
+
160
+ # Create session directory for analysis
161
+ cache_dir = Ace::Docs.config["cache_dir"] || ".ace-local/docs"
162
+ compact_id = Ace::B36ts.encode(Time.now)
163
+ session_dir = File.join(cache_dir, "analyze-#{compact_id}")
164
+ FileUtils.mkdir_p(session_dir)
165
+
166
+ # Analyze with LLM
167
+ puts "\nAnalyzing changes with LLM...".cyan
168
+ # Pass the appropriate diff format (single string or hash of diffs)
169
+ diff_for_analysis = diff_result[:multi_subject] ? diff_result[:diffs] : diff_result[:diff]
170
+ analysis = analyze_with_llm(document, diff_for_analysis, since, session_dir: session_dir)
171
+
172
+ unless analysis[:success]
173
+ warn "Error: #{analysis[:error]}"
174
+ return EXIT_ANALYSIS_ERROR
175
+ end
176
+
177
+ # Save results to cache
178
+ puts "\nSaving analysis results...".cyan
179
+ save_to_cache(document, diff_result, analysis, since, session_dir: session_dir)
180
+
181
+ # Display summary with session directory
182
+ display_summary(analysis, session_dir: session_dir)
183
+
184
+ EXIT_SUCCESS
185
+ rescue => e
186
+ warn "Error during analysis: #{e.message}"
187
+ warn e.backtrace.join("\n") if debug?(options)
188
+ EXIT_ANALYSIS_ERROR
189
+ end
190
+
191
+ def determine_since(document, options)
192
+ # Use explicit --since option if provided
193
+ return options[:since] if options[:since]
194
+
195
+ # Use document's last-updated date if available
196
+ if document.last_updated
197
+ return document.last_updated.strftime("%Y-%m-%d")
198
+ end
199
+
200
+ # Default to 7 days ago
201
+ (Date.today - 7).strftime("%Y-%m-%d")
202
+ end
203
+
204
+ def build_diff_options(options)
205
+ {
206
+ exclude_renames: options[:exclude_renames] || false,
207
+ exclude_moves: options[:exclude_moves] || false
208
+ }
209
+ end
210
+
211
+ def count_diff_lines(diff)
212
+ diff.lines.count
213
+ end
214
+
215
+ def analyze_with_llm(document, diff, since, session_dir: nil)
216
+ # Build prompts (returns hash with :system, :user, :context_md, :diff_stats)
217
+ prompts = Ace::Docs::Prompts::DocumentAnalysisPrompt.build(
218
+ document,
219
+ diff,
220
+ since: since,
221
+ cache_dir: session_dir
222
+ )
223
+
224
+ # Save prompts BEFORE calling LLM (for debugging even if LLM fails)
225
+ if session_dir
226
+ # Save system prompt
227
+ system_prompt_path = File.join(session_dir, "prompt-system.md")
228
+ File.write(system_prompt_path, format_prompt(prompts[:system], "System Prompt"))
229
+
230
+ # Save user prompt
231
+ user_prompt_path = File.join(session_dir, "prompt-user.md")
232
+ File.write(user_prompt_path, format_prompt(prompts[:user], "User Prompt"))
233
+ end
234
+
235
+ # Determine model (use config or default to gflash)
236
+ model = Ace::Docs.config["llm_model"] || "gflash"
237
+
238
+ # Get timeout from config (default is 300 seconds from default_config)
239
+ timeout = Ace::Docs.config["llm_timeout"]
240
+
241
+ # Call LLM via QueryInterface with system prompt
242
+ result = Ace::LLM::QueryInterface.query(
243
+ model,
244
+ prompts[:user],
245
+ system: prompts[:system],
246
+ temperature: 0.3,
247
+ timeout: timeout
248
+ )
249
+
250
+ {
251
+ success: true,
252
+ analysis: result[:text],
253
+ model: result[:model],
254
+ provider: result[:provider],
255
+ system_prompt: prompts[:system],
256
+ user_prompt: prompts[:user],
257
+ context_md: prompts[:context_md],
258
+ diff_stats: prompts[:diff_stats],
259
+ timestamp: Time.now.utc.iso8601
260
+ }
261
+ rescue => e
262
+ {
263
+ success: false,
264
+ error: e.message,
265
+ timestamp: Time.now.utc.iso8601
266
+ }
267
+ end
268
+
269
+ def save_to_cache(document, diff_result, analysis, since, session_dir:)
270
+ # session_dir is already created in execute method
271
+ # Note: repo-diff.diff, context.md, and prompts are already saved by analyze_with_llm
272
+
273
+ # Save LLM analysis
274
+ analysis_path = File.join(session_dir, "analysis.md")
275
+ File.write(analysis_path, format_analysis(document, analysis, since))
276
+
277
+ # Save diff statistics
278
+ if analysis[:diff_stats]
279
+ diff_stats_path = File.join(session_dir, "diff-stats.yml")
280
+ File.write(diff_stats_path, analysis[:diff_stats].to_yaml)
281
+ end
282
+
283
+ # Save metadata
284
+ metadata_path = File.join(session_dir, "metadata.yml")
285
+ metadata = {
286
+ "document_path" => document.path,
287
+ "document_type" => document.doc_type,
288
+ "generated" => analysis[:timestamp],
289
+ "since" => since,
290
+ "has_changes" => diff_result[:has_changes],
291
+ "filters_applied" => diff_result[:options][:paths] || [],
292
+ "llm_model" => analysis[:model],
293
+ "llm_provider" => analysis[:provider],
294
+ "prompts_saved" => {
295
+ "system" => "prompt-system.md",
296
+ "user" => "prompt-user.md"
297
+ },
298
+ "context_saved" => "context.md",
299
+ "diff_stats_saved" => analysis[:diff_stats] ? "diff-stats.yml" : nil
300
+ }
301
+ File.write(metadata_path, metadata.to_yaml)
302
+
303
+ analysis_path
304
+ end
305
+
306
+ def format_prompt(prompt_content, prompt_type)
307
+ <<~MARKDOWN
308
+ # #{prompt_type}
309
+
310
+ **Generated**: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}
311
+ **Source**: #{(prompt_type == "System Prompt") ? "ace-nav prompt://document-analysis.system" : "Generated from document context"}
312
+
313
+ ---
314
+
315
+ #{prompt_content}
316
+ MARKDOWN
317
+ end
318
+
319
+ def format_analysis(document, analysis, since)
320
+ <<~MARKDOWN
321
+ # Documentation Analysis Report
322
+
323
+ **Document**: #{document.relative_path || document.path}
324
+ **Type**: #{document.doc_type}
325
+ **Purpose**: #{document.purpose}
326
+ **Generated**: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}
327
+ **Period**: Changes since #{since}
328
+ **Model**: #{analysis[:model]} (#{analysis[:provider]})
329
+
330
+ ---
331
+
332
+ #{analysis[:analysis]}
333
+ MARKDOWN
334
+ end
335
+
336
+ def display_summary(analysis, session_dir:)
337
+ puts "\n" + "=" * 60
338
+ puts "✅ Analysis Complete".bold.green
339
+ puts "=" * 60
340
+ puts "\nModel: #{analysis[:model]} (#{analysis[:provider]})"
341
+ puts "\nResults saved to: #{session_dir}"
342
+ puts "\nNext steps:"
343
+ puts " • Review analysis.md for detailed recommendations"
344
+ puts " • Check prompt-system.md and prompt-user.md to see prompts used"
345
+ puts " • Run 'ace-docs update FILE --set last-updated=today' to mark as reviewed"
346
+ end
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require "colorize"
6
+ require_relative "../../organisms/cross_document_analyzer"
7
+ require_relative "../../models/consistency_report"
8
+ require_relative "scope_options"
9
+
10
+ module Ace
11
+ module Docs
12
+ module CLI
13
+ module Commands
14
+ # ace-support-cli Command class for the analyze-consistency command
15
+ #
16
+ # This command handles cross-document consistency analysis.
17
+ class AnalyzeConsistency < Ace::Support::Cli::Command
18
+ include Ace::Support::Cli::Base
19
+ include ScopeOptions
20
+
21
+ # Exit codes
22
+ EXIT_SUCCESS = 0
23
+ EXIT_ERROR = 1
24
+
25
+ desc <<~DESC.strip
26
+ Analyze cross-document consistency
27
+
28
+ Analyze multiple documents for consistency issues including terminology
29
+ conflicts, duplicate content, and version inconsistencies.
30
+
31
+ Configuration:
32
+ LLM model configured via ace-llm
33
+ Global config: ~/.ace/docs/config.yml
34
+ Project config: .ace/docs/config.yml
35
+
36
+ Output:
37
+ Consistency report in markdown format (default)
38
+ Exit codes: 0 (success), 1 (issues found with --strict), 2 (error)
39
+ DESC
40
+
41
+ example [
42
+ " # Analyze all documents",
43
+ "docs/handbook/ # Analyze specific directory",
44
+ "--terminology # Check terminology conflicts only",
45
+ "--duplicates --threshold 80 # Check duplicates with threshold",
46
+ "--save # Save report to file",
47
+ "--model gpt-4 # Use specific LLM model",
48
+ "--package ace-docs # Scope to one package",
49
+ "--glob 'ace-docs/**/*.md' # Scope by glob"
50
+ ]
51
+
52
+ argument :pattern, required: false, desc: "Pattern to analyze"
53
+
54
+ option :terminology, type: :boolean, desc: "Check terminology conflicts only"
55
+ option :duplicates, type: :boolean, desc: "Find duplicate content only"
56
+ option :versions, type: :boolean, desc: "Check version consistency only"
57
+ option :all, type: :boolean, desc: "All analysis types (default)"
58
+ option :threshold, type: :integer, desc: "Similarity threshold for duplicates (default: 70)"
59
+ option :output, type: :string, desc: "Output format (markdown|json|text)", default: "markdown"
60
+ option :save, type: :boolean, desc: "Save report to cache directory"
61
+ option :model, type: :string, desc: "LLM model to use (default: gflash)"
62
+ option :timeout, type: :integer, desc: "LLM timeout in seconds"
63
+ option :strict, type: :boolean, desc: "Exit with code 1 if issues found"
64
+ option :package, type: :array, desc: "Scope to package(s), e.g. --package ace-docs"
65
+ option :glob, type: :array, desc: "Scope by glob(s), e.g. --glob 'ace-docs/**/*.md'"
66
+
67
+ # Standard options
68
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
69
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
70
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
71
+
72
+ def call(pattern: nil, **options)
73
+ # Handle --help/-h passed as pattern argument
74
+ if pattern == "--help" || pattern == "-h"
75
+ # ace-support-cli will handle help automatically, so we just ignore
76
+ return EXIT_SUCCESS
77
+ end
78
+
79
+ # Type-convert numeric options (ace-support-cli returns strings, Thor converted to integers)
80
+ numeric_options = %i[threshold timeout]
81
+ numeric_options.each do |key|
82
+ options[key] = options[key].to_i if options[key]
83
+ end
84
+
85
+ execute_consistency_analysis(pattern, options)
86
+ end
87
+
88
+ private
89
+
90
+ def execute_consistency_analysis(pattern, options)
91
+ normalized_options = parse_options(options)
92
+ analyzer = Ace::Docs::Organisms::CrossDocumentAnalyzer.new(normalized_options)
93
+
94
+ # Show what we're analyzing
95
+ if pattern
96
+ puts "Analyzing documents matching: #{pattern}".cyan
97
+ else
98
+ puts "Analyzing all managed documents".cyan
99
+ end
100
+
101
+ # Determine focus areas
102
+ focus_areas = determine_focus_areas(normalized_options)
103
+ puts "Focus areas: #{focus_areas.join(", ")}".cyan
104
+
105
+ # Run analysis
106
+ report = analyzer.analyze(pattern)
107
+
108
+ # The report is now a path to the saved file
109
+ # Check if it's nil or file doesn't exist
110
+ if report.nil? || !File.exist?(report)
111
+ warn "No analysis results returned."
112
+ return EXIT_ERROR
113
+ end
114
+
115
+ # Display where the report was saved
116
+ puts "Report saved to: #{report}".cyan
117
+
118
+ # Simple completion message
119
+ puts "\n✅ Analysis complete"
120
+ EXIT_SUCCESS
121
+ rescue => e
122
+ warn "Error: #{e.message}"
123
+ warn e.backtrace.join("\n") if normalized_options[:debug]
124
+ EXIT_ERROR
125
+ end
126
+
127
+ # Parse and normalize options
128
+ def parse_options(options)
129
+ normalized = {}
130
+
131
+ # Output format
132
+ normalized[:output] = options[:output] || "markdown"
133
+
134
+ # Analysis focus
135
+ normalized[:all] = options[:all] ||
136
+ (!options[:terminology] && !options[:duplicates] && !options[:versions])
137
+ normalized[:terminology] = options[:terminology] || false
138
+ normalized[:duplicates] = options[:duplicates] || false
139
+ normalized[:versions] = options[:versions] || false
140
+
141
+ # Threshold for duplicate detection
142
+ normalized[:threshold] = options[:threshold] || 70
143
+
144
+ # Save to cache
145
+ normalized[:save] = options[:save] || false
146
+
147
+ # Verbose mode
148
+ normalized[:verbose] = options[:verbose] || false
149
+
150
+ # Debug mode
151
+ normalized[:debug] = options[:debug] || false
152
+
153
+ # Strict mode (exit 1 if issues found)
154
+ normalized[:strict] = options[:strict] || false
155
+
156
+ # LLM model
157
+ normalized[:model] = options[:model]
158
+
159
+ # Timeout
160
+ normalized[:timeout] = options[:timeout]
161
+ normalized[:project_root] = options[:project_root]
162
+ normalized[:scope_globs] = normalized_scope_globs(options, project_root: options[:project_root])
163
+
164
+ normalized
165
+ end
166
+
167
+ # Determine which analysis areas to focus on
168
+ def determine_focus_areas(options)
169
+ areas = []
170
+
171
+ if options[:all]
172
+ areas = ["terminology", "duplicates", "versions", "consolidation"]
173
+ else
174
+ areas << "terminology" if options[:terminology]
175
+ areas << "duplicates" if options[:duplicates]
176
+ areas << "versions" if options[:versions]
177
+ end
178
+
179
+ areas.empty? ? ["all types"] : areas
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "../../organisms/document_registry"
6
+ require_relative "scope_options"
7
+
8
+ module Ace
9
+ module Docs
10
+ module CLI
11
+ module Commands
12
+ # ace-support-cli Command class for the discover command
13
+ #
14
+ # This wraps the discover logic in a ace-support-cli compatible interface.
15
+ class Discover < Ace::Support::Cli::Command
16
+ include Ace::Support::Cli::Base
17
+ include ScopeOptions
18
+
19
+ # Exit codes
20
+ EXIT_SUCCESS = 0
21
+ EXIT_ERROR = 1
22
+
23
+ desc <<~DESC.strip
24
+ Find and list all managed documents
25
+
26
+ Scan the project for all documents managed by ace-docs and display them.
27
+ Useful for verifying which files are being tracked.
28
+
29
+ Output:
30
+ Shows count and file paths with types
31
+ Exit codes: 0 (success), 1 (error)
32
+ DESC
33
+
34
+ example [
35
+ " # List all managed documents",
36
+ "--package ace-docs # List managed docs in one package",
37
+ "--glob 'ace-docs/**/*.md' # List managed docs by glob"
38
+ ]
39
+
40
+ option :package, type: :array, desc: "Scope to package(s), e.g. --package ace-docs"
41
+ option :glob, type: :array, desc: "Scope by glob(s), e.g. --glob 'ace-docs/**/*.md'"
42
+
43
+ # Standard options
44
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
45
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
46
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
47
+
48
+ def call(**options)
49
+ scope_globs = normalized_scope_globs(options, project_root: options[:project_root])
50
+ registry = Ace::Docs::Organisms::DocumentRegistry.new(
51
+ project_root: options[:project_root],
52
+ scope_globs: scope_globs
53
+ )
54
+ documents = registry.all
55
+
56
+ if documents.empty?
57
+ puts "No managed documents found."
58
+ return EXIT_SUCCESS
59
+ end
60
+
61
+ puts "Found #{documents.size} managed documents:"
62
+ documents.each do |doc|
63
+ puts " #{doc.relative_path || doc.path} (#{doc.doc_type})"
64
+ end
65
+ EXIT_SUCCESS
66
+ rescue => e
67
+ warn "Error discovering documents: #{e.message}"
68
+ warn e.backtrace.join("\n ") if debug?(options)
69
+ EXIT_ERROR
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/fs"
4
+
5
+ module Ace
6
+ module Docs
7
+ module CLI
8
+ module Commands
9
+ # Shared scope parsing helpers for package/glob scoped document selection.
10
+ module ScopeOptions
11
+ private
12
+
13
+ def normalized_scope_globs(options, project_root: nil)
14
+ root = project_root || Ace::Support::Fs::Molecules::ProjectRootFinder.find_or_current
15
+ package_globs = Array(options[:package]).compact.map { |value| normalize_package_scope(value, root) }
16
+ direct_globs = Array(options[:glob]).compact.map { |value| normalize_glob_scope(value, root) }
17
+ (package_globs + direct_globs).uniq
18
+ end
19
+
20
+ def scope_options_present?(options)
21
+ Array(options[:package]).any? || Array(options[:glob]).any?
22
+ end
23
+
24
+ def path_in_scope?(path, scope_globs, project_root:)
25
+ return true if scope_globs.nil? || scope_globs.empty?
26
+
27
+ expanded = File.expand_path(path, project_root)
28
+ relative = begin
29
+ expanded.delete_prefix("#{File.expand_path(project_root)}/")
30
+ rescue
31
+ path.to_s
32
+ end
33
+
34
+ scope_globs.any? do |pattern|
35
+ File.fnmatch?(pattern, relative, File::FNM_PATHNAME | File::FNM_EXTGLOB | File::FNM_DOTMATCH)
36
+ end
37
+ end
38
+
39
+ def normalize_package_scope(raw_value, project_root)
40
+ value = raw_value.to_s.strip
41
+ raise ArgumentError, "--package cannot be blank" if value.empty?
42
+
43
+ path = File.join(project_root, value)
44
+ raise ArgumentError, "Unknown package for --package: #{value}" unless Dir.exist?(path)
45
+
46
+ "#{value.chomp("/")}/**/*.md"
47
+ end
48
+
49
+ def normalize_glob_scope(raw_value, project_root)
50
+ value = raw_value.to_s.strip.sub(%r{\A\./}, "")
51
+ raise ArgumentError, "--glob cannot be blank" if value.empty?
52
+
53
+ return value if wildcard_pattern?(value)
54
+ return value if value.end_with?(".md")
55
+
56
+ directory_path = File.join(project_root, value)
57
+ if value.end_with?("/") || Dir.exist?(directory_path)
58
+ return "#{value.chomp("/")}/**/*.md"
59
+ end
60
+
61
+ "#{value}/**/*.md"
62
+ end
63
+
64
+ def wildcard_pattern?(value)
65
+ value.match?(/[*?\[\]{]/)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end