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
data/lib/ace/docs/cli.rb
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/support/cli"
|
|
4
|
+
require "ace/core"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Docs
|
|
8
|
+
# ace-support-cli based CLI registry for ace-docs
|
|
9
|
+
#
|
|
10
|
+
# This replaces the Thor-based CLI with ace-support-cli while maintaining
|
|
11
|
+
# complete command parity and user-facing behavior.
|
|
12
|
+
module CLI
|
|
13
|
+
extend Ace::Support::Cli::RegistryDsl
|
|
14
|
+
|
|
15
|
+
PROGRAM_NAME = "ace-docs"
|
|
16
|
+
|
|
17
|
+
REGISTERED_COMMANDS = [
|
|
18
|
+
["status", "Show documentation status"],
|
|
19
|
+
["discover", "Discover undocumented items"],
|
|
20
|
+
["update", "Update documentation metadata"],
|
|
21
|
+
["analyze", "Analyze documentation quality"],
|
|
22
|
+
["validate", "Validate documentation structure"],
|
|
23
|
+
["analyze-consistency", "Check documentation consistency"]
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
HELP_EXAMPLES = [
|
|
27
|
+
"ace-docs status --needs-update # Docs due for refresh",
|
|
28
|
+
"ace-docs update docs/architecture.md # Refresh metadata",
|
|
29
|
+
"ace-docs analyze-consistency # Cross-doc link check"
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
# Register all commands (Hanami pattern: CLI::Commands::*)
|
|
33
|
+
register "status", Commands::Status.new
|
|
34
|
+
register "discover", Commands::Discover.new
|
|
35
|
+
register "update", Commands::Update.new
|
|
36
|
+
register "analyze", Commands::Analyze.new
|
|
37
|
+
register "validate", Commands::Validate.new
|
|
38
|
+
register "analyze-consistency", Commands::AnalyzeConsistency.new
|
|
39
|
+
|
|
40
|
+
# Register version command
|
|
41
|
+
version_cmd = Ace::Support::Cli::VersionCommand.build(
|
|
42
|
+
gem_name: "ace-docs",
|
|
43
|
+
version: Ace::Docs::VERSION
|
|
44
|
+
)
|
|
45
|
+
register "version", version_cmd
|
|
46
|
+
register "--version", version_cmd
|
|
47
|
+
|
|
48
|
+
# Register help command
|
|
49
|
+
help_cmd = Ace::Support::Cli::HelpCommand.build(
|
|
50
|
+
program_name: PROGRAM_NAME,
|
|
51
|
+
version: Ace::Docs::VERSION,
|
|
52
|
+
commands: REGISTERED_COMMANDS,
|
|
53
|
+
examples: HELP_EXAMPLES
|
|
54
|
+
)
|
|
55
|
+
register "help", help_cmd
|
|
56
|
+
register "--help", help_cmd
|
|
57
|
+
register "-h", help_cmd
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "ace/b36ts"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Docs
|
|
9
|
+
module Models
|
|
10
|
+
# Data model for analysis reports
|
|
11
|
+
class AnalysisReport
|
|
12
|
+
attr_accessor :generated, :since, :documents, :analysis, :statistics
|
|
13
|
+
|
|
14
|
+
def initialize(attributes = {})
|
|
15
|
+
@generated = attributes[:generated] || Time.now.utc.iso8601
|
|
16
|
+
@since = attributes[:since]
|
|
17
|
+
@documents = attributes[:documents] || []
|
|
18
|
+
@analysis = attributes[:analysis]
|
|
19
|
+
@statistics = attributes[:statistics] || {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Convert report to markdown format
|
|
23
|
+
# @return [String] Markdown-formatted report
|
|
24
|
+
def to_markdown
|
|
25
|
+
frontmatter = build_frontmatter
|
|
26
|
+
body = @analysis || "No analysis available"
|
|
27
|
+
|
|
28
|
+
<<~MARKDOWN
|
|
29
|
+
---
|
|
30
|
+
#{frontmatter.to_yaml.strip}
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
#{body}
|
|
34
|
+
MARKDOWN
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Save report to cache directory
|
|
38
|
+
# @param cache_dir [String] Cache directory path
|
|
39
|
+
# @return [String] Path to saved file
|
|
40
|
+
def save_to_cache(cache_dir = nil)
|
|
41
|
+
cache_dir ||= Ace::Docs.config["cache_dir"] || ".ace-local/docs"
|
|
42
|
+
FileUtils.mkdir_p(cache_dir)
|
|
43
|
+
|
|
44
|
+
compact_id = Ace::B36ts.encode(Time.now)
|
|
45
|
+
filename = "analysis-#{compact_id}.md"
|
|
46
|
+
filepath = File.join(cache_dir, filename)
|
|
47
|
+
|
|
48
|
+
File.write(filepath, to_markdown)
|
|
49
|
+
filepath
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Convert to hash
|
|
53
|
+
# @return [Hash] Report as hash
|
|
54
|
+
def to_h
|
|
55
|
+
{
|
|
56
|
+
generated: @generated,
|
|
57
|
+
since: @since,
|
|
58
|
+
documents: documents_list,
|
|
59
|
+
analysis: @analysis,
|
|
60
|
+
statistics: @statistics
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Load report from file
|
|
65
|
+
# @param filepath [String] Path to report file
|
|
66
|
+
# @return [AnalysisReport] Loaded report
|
|
67
|
+
def self.load_from_file(filepath)
|
|
68
|
+
content = File.read(filepath)
|
|
69
|
+
|
|
70
|
+
# Parse YAML frontmatter
|
|
71
|
+
if content =~ /\A---\n(.*?)\n---\n(.*)/m
|
|
72
|
+
frontmatter = YAML.safe_load(Regexp.last_match(1))
|
|
73
|
+
body = Regexp.last_match(2)
|
|
74
|
+
|
|
75
|
+
new(
|
|
76
|
+
generated: frontmatter["generated"],
|
|
77
|
+
since: frontmatter["since"],
|
|
78
|
+
documents: parse_documents(frontmatter["document_list"]),
|
|
79
|
+
analysis: body.strip,
|
|
80
|
+
statistics: frontmatter["statistics"] || {}
|
|
81
|
+
)
|
|
82
|
+
else
|
|
83
|
+
# No frontmatter, treat entire content as analysis
|
|
84
|
+
new(analysis: content)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def build_frontmatter
|
|
91
|
+
{
|
|
92
|
+
"generated" => @generated,
|
|
93
|
+
"since" => @since,
|
|
94
|
+
"documents" => @documents.size,
|
|
95
|
+
"document_list" => documents_list,
|
|
96
|
+
"statistics" => @statistics
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def documents_list
|
|
101
|
+
@documents.map do |doc|
|
|
102
|
+
if doc.respond_to?(:relative_path)
|
|
103
|
+
doc.relative_path || doc.path
|
|
104
|
+
elsif doc.is_a?(Hash)
|
|
105
|
+
doc["path"] || doc[:path]
|
|
106
|
+
else
|
|
107
|
+
doc.to_s
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.parse_documents(doc_list)
|
|
113
|
+
return [] unless doc_list
|
|
114
|
+
|
|
115
|
+
doc_list.map { |path| {path: path} }
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Docs
|
|
8
|
+
module Models
|
|
9
|
+
# Model for storing and formatting consistency analysis results
|
|
10
|
+
class ConsistencyReport
|
|
11
|
+
attr_reader :terminology_conflicts, :duplicate_content,
|
|
12
|
+
:version_inconsistencies, :consolidation_opportunities,
|
|
13
|
+
:generated_at, :document_count, :raw_response
|
|
14
|
+
|
|
15
|
+
def initialize(data = {})
|
|
16
|
+
@terminology_conflicts = data[:terminology_conflicts] || []
|
|
17
|
+
@duplicate_content = data[:duplicate_content] || []
|
|
18
|
+
@version_inconsistencies = data[:version_inconsistencies] || []
|
|
19
|
+
@consolidation_opportunities = data[:consolidation_opportunities] || []
|
|
20
|
+
@generated_at = data[:generated_at] || Time.now
|
|
21
|
+
@document_count = data[:document_count] || 0
|
|
22
|
+
@raw_response = data[:raw_response]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Parse LLM response into ConsistencyReport
|
|
26
|
+
# @param response [String] JSON response from LLM
|
|
27
|
+
# @param document_count [Integer] number of documents analyzed
|
|
28
|
+
# @return [ConsistencyReport] parsed report
|
|
29
|
+
def self.parse(response, document_count = 0)
|
|
30
|
+
data = JSON.parse(response, symbolize_names: true)
|
|
31
|
+
|
|
32
|
+
new(
|
|
33
|
+
terminology_conflicts: data[:terminology_conflicts] || [],
|
|
34
|
+
duplicate_content: data[:duplicate_content] || [],
|
|
35
|
+
version_inconsistencies: data[:version_inconsistencies] || [],
|
|
36
|
+
consolidation_opportunities: data[:consolidation_opportunities] || [],
|
|
37
|
+
document_count: document_count,
|
|
38
|
+
generated_at: Time.now,
|
|
39
|
+
raw_response: response
|
|
40
|
+
)
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
# If parsing fails, create a report with the raw response
|
|
43
|
+
new(
|
|
44
|
+
raw_response: response,
|
|
45
|
+
document_count: document_count,
|
|
46
|
+
generated_at: Time.now
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if the report has any issues
|
|
51
|
+
def has_issues?
|
|
52
|
+
total_issues > 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Get total number of issues found
|
|
56
|
+
def total_issues
|
|
57
|
+
terminology_conflicts.size +
|
|
58
|
+
duplicate_content.size +
|
|
59
|
+
version_inconsistencies.size +
|
|
60
|
+
consolidation_opportunities.size
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Check if parsing was successful
|
|
64
|
+
def parsing_successful?
|
|
65
|
+
!raw_response.nil? && total_issues >= 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Convert report to markdown format
|
|
69
|
+
def to_markdown
|
|
70
|
+
output = []
|
|
71
|
+
|
|
72
|
+
output << "# Cross-Document Consistency Report"
|
|
73
|
+
output << ""
|
|
74
|
+
output << "Generated: #{generated_at.strftime("%Y-%m-%d %H:%M:%S")}"
|
|
75
|
+
output << "Documents analyzed: #{document_count}"
|
|
76
|
+
output << "Issues found: #{total_issues}"
|
|
77
|
+
output << ""
|
|
78
|
+
|
|
79
|
+
if !parsing_successful? && raw_response
|
|
80
|
+
output << "## Warning: Could not parse LLM response"
|
|
81
|
+
output << ""
|
|
82
|
+
output << "Raw output:"
|
|
83
|
+
output << "```"
|
|
84
|
+
output << raw_response
|
|
85
|
+
output << "```"
|
|
86
|
+
return output.join("\n")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if terminology_conflicts.any?
|
|
90
|
+
output << "## Terminology Conflicts (#{terminology_conflicts.size})"
|
|
91
|
+
output << ""
|
|
92
|
+
|
|
93
|
+
terminology_conflicts.each do |conflict|
|
|
94
|
+
output << format_terminology_conflict(conflict)
|
|
95
|
+
output << ""
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if duplicate_content.any?
|
|
100
|
+
output << "## Duplicate Content (#{duplicate_content.size})"
|
|
101
|
+
output << ""
|
|
102
|
+
|
|
103
|
+
duplicate_content.each do |duplicate|
|
|
104
|
+
output << format_duplicate_content(duplicate)
|
|
105
|
+
output << ""
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
if version_inconsistencies.any?
|
|
110
|
+
output << "## Version Inconsistencies (#{version_inconsistencies.size})"
|
|
111
|
+
output << ""
|
|
112
|
+
|
|
113
|
+
version_inconsistencies.each do |version_issue|
|
|
114
|
+
output << format_version_inconsistency(version_issue)
|
|
115
|
+
output << ""
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if consolidation_opportunities.any?
|
|
120
|
+
output << "## Consolidation Opportunities (#{consolidation_opportunities.size})"
|
|
121
|
+
output << ""
|
|
122
|
+
|
|
123
|
+
consolidation_opportunities.each do |opportunity|
|
|
124
|
+
output << format_consolidation_opportunity(opportunity)
|
|
125
|
+
output << ""
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
if total_issues == 0
|
|
130
|
+
output << "## No Issues Found"
|
|
131
|
+
output << ""
|
|
132
|
+
output << "✅ All documents appear to be consistent!"
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
output.join("\n")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Convert report to JSON format
|
|
139
|
+
def to_json(*args)
|
|
140
|
+
{
|
|
141
|
+
generated_at: generated_at.iso8601,
|
|
142
|
+
document_count: document_count,
|
|
143
|
+
total_issues: total_issues,
|
|
144
|
+
terminology_conflicts: terminology_conflicts,
|
|
145
|
+
duplicate_content: duplicate_content,
|
|
146
|
+
version_inconsistencies: version_inconsistencies,
|
|
147
|
+
consolidation_opportunities: consolidation_opportunities
|
|
148
|
+
}.to_json(*args)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
# Format a terminology conflict for markdown output
|
|
154
|
+
def format_terminology_conflict(conflict)
|
|
155
|
+
lines = []
|
|
156
|
+
|
|
157
|
+
terms = conflict[:terms] || conflict["terms"] || []
|
|
158
|
+
lines << "### \"#{terms[0]}\" vs \"#{terms[1]}\""
|
|
159
|
+
|
|
160
|
+
occurrences = conflict[:occurrences] || conflict["occurrences"] || {}
|
|
161
|
+
|
|
162
|
+
terms.each do |term|
|
|
163
|
+
term_occurrences = occurrences[term.to_sym] || occurrences[term.to_s] || []
|
|
164
|
+
next if term_occurrences.empty?
|
|
165
|
+
|
|
166
|
+
term_occurrences.each do |occurrence|
|
|
167
|
+
file = occurrence[:file] || occurrence["file"]
|
|
168
|
+
count = occurrence[:count] || occurrence["count"]
|
|
169
|
+
examples = occurrence[:examples] || occurrence["examples"] || []
|
|
170
|
+
|
|
171
|
+
lines << "- #{file}: uses \"#{term}\" (#{count} occurrences)"
|
|
172
|
+
if examples.any?
|
|
173
|
+
lines << " Examples: #{examples.first(2).join("; ")}"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
recommendation = conflict[:recommendation] || conflict["recommendation"]
|
|
179
|
+
lines << "**Recommendation**: #{recommendation}" if recommendation
|
|
180
|
+
|
|
181
|
+
lines.join("\n")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Format duplicate content for markdown output
|
|
185
|
+
def format_duplicate_content(duplicate)
|
|
186
|
+
lines = []
|
|
187
|
+
|
|
188
|
+
description = duplicate[:description] || duplicate["description"] || "Duplicate content"
|
|
189
|
+
similarity = duplicate[:similarity_percentage] || duplicate["similarity_percentage"]
|
|
190
|
+
|
|
191
|
+
lines << "### #{description}"
|
|
192
|
+
lines << "Files with duplicate content (#{similarity}% similarity):" if similarity
|
|
193
|
+
|
|
194
|
+
locations = duplicate[:locations] || duplicate["locations"] || []
|
|
195
|
+
locations.each do |location|
|
|
196
|
+
file = location[:file] || location["file"]
|
|
197
|
+
line_range = location[:lines] || location["lines"]
|
|
198
|
+
excerpt = location[:excerpt] || location["excerpt"]
|
|
199
|
+
|
|
200
|
+
lines << "- #{file} (lines #{line_range})"
|
|
201
|
+
lines << " \"#{excerpt}\"" if excerpt
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
recommendation = duplicate[:recommendation] || duplicate["recommendation"]
|
|
205
|
+
lines << "**Recommendation**: #{recommendation}" if recommendation
|
|
206
|
+
|
|
207
|
+
lines.join("\n")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Format version inconsistency for markdown output
|
|
211
|
+
def format_version_inconsistency(version_issue)
|
|
212
|
+
lines = []
|
|
213
|
+
|
|
214
|
+
item = version_issue[:item] || version_issue["item"] || "Version"
|
|
215
|
+
lines << "### #{item}"
|
|
216
|
+
|
|
217
|
+
versions = version_issue[:versions_found] || version_issue["versions_found"] || []
|
|
218
|
+
versions.each do |version_info|
|
|
219
|
+
version = version_info[:version] || version_info["version"]
|
|
220
|
+
file = version_info[:file] || version_info["file"]
|
|
221
|
+
line = version_info[:line] || version_info["line"]
|
|
222
|
+
|
|
223
|
+
lines << "- #{file}: \"#{version}\""
|
|
224
|
+
lines << " (line #{line})" if line
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
recommendation = version_issue[:recommendation] || version_issue["recommendation"]
|
|
228
|
+
lines << "**Recommendation**: #{recommendation}" if recommendation
|
|
229
|
+
|
|
230
|
+
lines.join("\n")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Format consolidation opportunity for markdown output
|
|
234
|
+
def format_consolidation_opportunity(opportunity)
|
|
235
|
+
lines = []
|
|
236
|
+
|
|
237
|
+
topic = opportunity[:topic] || opportunity["topic"] || "Related content"
|
|
238
|
+
lines << "### #{topic}"
|
|
239
|
+
|
|
240
|
+
lines << "Multiple documents explain similar content:"
|
|
241
|
+
|
|
242
|
+
documents = opportunity[:documents] || opportunity["documents"] || []
|
|
243
|
+
documents.each do |doc|
|
|
244
|
+
file = doc[:file] || doc["file"]
|
|
245
|
+
coverage = doc[:coverage] || doc["coverage"]
|
|
246
|
+
|
|
247
|
+
lines << "- #{file}"
|
|
248
|
+
lines << " Coverage: #{coverage}" if coverage
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
recommendation = opportunity[:recommendation] || opportunity["recommendation"]
|
|
252
|
+
lines << "**Recommendation**: #{recommendation}" if recommendation
|
|
253
|
+
|
|
254
|
+
lines.join("\n")
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|