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.
- checksums.yaml +7 -0
- data/.ace-defaults/docs/config.yml +169 -0
- data/.ace-defaults/docs/multi-subject-example.md +130 -0
- data/.ace-defaults/docs/single-subject-example.md +150 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-docs.yml +10 -0
- data/.ace-defaults/nav/protocols/prompt-sources/ace-docs.yml +34 -0
- data/.ace-defaults/nav/protocols/tmpl-sources/ace-docs.yml +10 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-docs.yml +19 -0
- data/CHANGELOG.md +1082 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +14 -0
- data/exe/ace-docs +14 -0
- data/handbook/guides/documentation/ruby.md +16 -0
- data/handbook/guides/documentation/rust.md +35 -0
- data/handbook/guides/documentation/typescript.md +18 -0
- data/handbook/guides/documentation.g.md +437 -0
- data/handbook/guides/documents-embedded-sync.g.md +473 -0
- data/handbook/guides/documents-embedding.g.md +276 -0
- data/handbook/guides/markdown-style.g.md +290 -0
- data/handbook/prompts/ace-change-analyzer.system.md +113 -0
- data/handbook/prompts/ace-change-analyzer.user.md +95 -0
- data/handbook/prompts/document-analysis.md +74 -0
- data/handbook/prompts/document-analysis.system.md +129 -0
- data/handbook/prompts/markdown-style.system.md +113 -0
- data/handbook/skills/as-docs-create-adr/SKILL.md +35 -0
- data/handbook/skills/as-docs-create-api/SKILL.md +35 -0
- data/handbook/skills/as-docs-create-user/SKILL.md +35 -0
- data/handbook/skills/as-docs-maintain-adrs/SKILL.md +35 -0
- data/handbook/skills/as-docs-squash-changelog/SKILL.md +42 -0
- data/handbook/skills/as-docs-update/SKILL.md +36 -0
- data/handbook/skills/as-docs-update-blueprint/SKILL.md +28 -0
- data/handbook/skills/as-docs-update-roadmap/SKILL.md +24 -0
- data/handbook/skills/as-docs-update-tools/SKILL.md +36 -0
- data/handbook/skills/as-docs-update-usage/SKILL.md +26 -0
- data/handbook/templates/code-docs/javascript-jsdoc.template.md +102 -0
- data/handbook/templates/code-docs/ruby-yard.template.md +85 -0
- data/handbook/templates/project-docs/README.template.md +73 -0
- data/handbook/templates/project-docs/architecture.template.md +300 -0
- data/handbook/templates/project-docs/blueprint.template.md +165 -0
- data/handbook/templates/project-docs/context/ownership.yml +160 -0
- data/handbook/templates/project-docs/decisions/adr.template.md +60 -0
- data/handbook/templates/project-docs/prd.template.md +144 -0
- data/handbook/templates/project-docs/roadmap/roadmap.template.md +47 -0
- data/handbook/templates/project-docs/vision.template.md +233 -0
- data/handbook/templates/user-docs/user-guide.template.md +107 -0
- data/handbook/workflow-instructions/docs/create-adr.wf.md +334 -0
- data/handbook/workflow-instructions/docs/create-api.wf.md +448 -0
- data/handbook/workflow-instructions/docs/create-cookbook.wf.md +434 -0
- data/handbook/workflow-instructions/docs/create-user.wf.md +399 -0
- data/handbook/workflow-instructions/docs/maintain-adrs.wf.md +589 -0
- data/handbook/workflow-instructions/docs/squash-changelog.wf.md +246 -0
- data/handbook/workflow-instructions/docs/update-blueprint.wf.md +361 -0
- data/handbook/workflow-instructions/docs/update-context.wf.md +336 -0
- data/handbook/workflow-instructions/docs/update-roadmap.wf.md +421 -0
- data/handbook/workflow-instructions/docs/update-tools.wf.md +307 -0
- data/handbook/workflow-instructions/docs/update-usage.wf.md +710 -0
- data/handbook/workflow-instructions/docs/update.wf.md +418 -0
- data/lib/ace/docs/atoms/diff_filterer.rb +131 -0
- data/lib/ace/docs/atoms/frontmatter_free_matcher.rb +20 -0
- data/lib/ace/docs/atoms/git_date_resolver.rb +16 -0
- data/lib/ace/docs/atoms/readme_metadata_inferrer.rb +60 -0
- data/lib/ace/docs/atoms/terminology_extractor.rb +308 -0
- data/lib/ace/docs/atoms/time_range_calculator.rb +96 -0
- data/lib/ace/docs/atoms/timestamp_parser.rb +106 -0
- data/lib/ace/docs/atoms/type_inferrer.rb +70 -0
- data/lib/ace/docs/cli/commands/analyze.rb +351 -0
- data/lib/ace/docs/cli/commands/analyze_consistency.rb +185 -0
- data/lib/ace/docs/cli/commands/discover.rb +75 -0
- data/lib/ace/docs/cli/commands/scope_options.rb +71 -0
- data/lib/ace/docs/cli/commands/status.rb +241 -0
- data/lib/ace/docs/cli/commands/update.rb +198 -0
- data/lib/ace/docs/cli/commands/validate.rb +225 -0
- data/lib/ace/docs/cli.rb +60 -0
- data/lib/ace/docs/models/analysis_report.rb +120 -0
- data/lib/ace/docs/models/consistency_report.rb +259 -0
- data/lib/ace/docs/models/document.rb +354 -0
- data/lib/ace/docs/molecules/change_detector.rb +389 -0
- data/lib/ace/docs/molecules/document_loader.rb +133 -0
- data/lib/ace/docs/molecules/frontmatter_manager.rb +85 -0
- data/lib/ace/docs/molecules/git_date_resolver.rb +30 -0
- data/lib/ace/docs/organisms/cross_document_analyzer.rb +274 -0
- data/lib/ace/docs/organisms/document_registry.rb +318 -0
- data/lib/ace/docs/organisms/validator.rb +164 -0
- data/lib/ace/docs/prompts/compact_diff_prompt.rb +119 -0
- data/lib/ace/docs/prompts/consistency_prompt.rb +286 -0
- data/lib/ace/docs/prompts/document_analysis_prompt.rb +389 -0
- data/lib/ace/docs/version.rb +7 -0
- data/lib/ace/docs.rb +82 -0
- data/lib/test.rb +4 -0
- 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
|