ace-bundle 0.40.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/bundle/config.yml +28 -0
- data/.ace-defaults/bundle/presets/base.md +15 -0
- data/.ace-defaults/bundle/presets/code-review.md +61 -0
- data/.ace-defaults/bundle/presets/development.md +16 -0
- data/.ace-defaults/bundle/presets/documentation-review.md +52 -0
- data/.ace-defaults/bundle/presets/mixed-content-example.md +94 -0
- data/.ace-defaults/bundle/presets/project-context.md +79 -0
- data/.ace-defaults/bundle/presets/project.md +35 -0
- data/.ace-defaults/bundle/presets/section-example-simple.md +27 -0
- data/.ace-defaults/bundle/presets/security-review.md +53 -0
- data/.ace-defaults/bundle/presets/simple-project.md +43 -0
- data/.ace-defaults/bundle/presets/team.md +18 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-bundle.yml +19 -0
- data/CHANGELOG.md +384 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +22 -0
- data/exe/ace-bundle +14 -0
- data/handbook/skills/as-bundle/SKILL.md +28 -0
- data/handbook/skills/as-onboard/SKILL.md +33 -0
- data/handbook/workflow-instructions/bundle.wf.md +111 -0
- data/handbook/workflow-instructions/onboard.wf.md +20 -0
- data/lib/ace/bundle/atoms/boundary_finder.rb +122 -0
- data/lib/ace/bundle/atoms/bundle_normalizer.rb +128 -0
- data/lib/ace/bundle/atoms/content_checker.rb +46 -0
- data/lib/ace/bundle/atoms/line_counter.rb +37 -0
- data/lib/ace/bundle/atoms/preset_list_formatter.rb +44 -0
- data/lib/ace/bundle/atoms/preset_validator.rb +69 -0
- data/lib/ace/bundle/atoms/section_validator.rb +215 -0
- data/lib/ace/bundle/atoms/typo_detector.rb +76 -0
- data/lib/ace/bundle/cli/commands/load.rb +347 -0
- data/lib/ace/bundle/cli.rb +26 -0
- data/lib/ace/bundle/models/bundle_data.rb +75 -0
- data/lib/ace/bundle/molecules/bundle_chunker.rb +280 -0
- data/lib/ace/bundle/molecules/bundle_file_writer.rb +269 -0
- data/lib/ace/bundle/molecules/bundle_merger.rb +248 -0
- data/lib/ace/bundle/molecules/preset_manager.rb +331 -0
- data/lib/ace/bundle/molecules/section_compressor.rb +249 -0
- data/lib/ace/bundle/molecules/section_formatter.rb +580 -0
- data/lib/ace/bundle/molecules/section_processor.rb +460 -0
- data/lib/ace/bundle/organisms/bundle_loader.rb +1436 -0
- data/lib/ace/bundle/organisms/pr_bundle_loader.rb +147 -0
- data/lib/ace/bundle/version.rb +7 -0
- data/lib/ace/bundle.rb +251 -0
- metadata +190 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../atoms/boundary_finder"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Bundle
|
|
7
|
+
module Molecules
|
|
8
|
+
# BundleChunker splits large content into manageable chunks
|
|
9
|
+
# Configuration is loaded from Ace::Bundle.max_lines (ADR-022)
|
|
10
|
+
#
|
|
11
|
+
# Performance Note: Current implementation loads full content into memory.
|
|
12
|
+
# For very large files (>100MB), consider implementing streaming chunking
|
|
13
|
+
# to reduce memory footprint. This optimization can be added in future versions.
|
|
14
|
+
class BundleChunker
|
|
15
|
+
# Fallback value if config is not available
|
|
16
|
+
DEFAULT_MAX_LINES = 2_000
|
|
17
|
+
DEFAULT_CHUNK_SUFFIX = "_chunk"
|
|
18
|
+
|
|
19
|
+
attr_reader :max_lines
|
|
20
|
+
|
|
21
|
+
# @param max_lines [Integer, nil] Override max lines per chunk (nil uses config)
|
|
22
|
+
def initialize(max_lines = nil)
|
|
23
|
+
@max_lines = max_lines || config_max_lines || DEFAULT_MAX_LINES
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
# Load max_lines from configuration
|
|
29
|
+
# Falls back to DEFAULT_MAX_LINES if config is unavailable
|
|
30
|
+
# @return [Integer] Configured max lines per chunk
|
|
31
|
+
def config_max_lines
|
|
32
|
+
Ace::Bundle.max_lines
|
|
33
|
+
rescue
|
|
34
|
+
DEFAULT_MAX_LINES
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
public
|
|
38
|
+
|
|
39
|
+
# Check if content needs chunking
|
|
40
|
+
def needs_chunking?(content)
|
|
41
|
+
return false if content.nil? || content.empty?
|
|
42
|
+
|
|
43
|
+
line_count = content.lines.size
|
|
44
|
+
line_count > @max_lines
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Split content into chunks
|
|
48
|
+
def chunk_content(content, base_path, options = {})
|
|
49
|
+
opts = {
|
|
50
|
+
chunk_suffix: DEFAULT_CHUNK_SUFFIX,
|
|
51
|
+
include_metadata: true
|
|
52
|
+
}.merge(options)
|
|
53
|
+
|
|
54
|
+
return single_file_result(content, base_path) unless needs_chunking?(content)
|
|
55
|
+
|
|
56
|
+
lines = content.lines
|
|
57
|
+
chunks = split_into_chunks(lines)
|
|
58
|
+
|
|
59
|
+
chunk_files = generate_chunk_files(chunks, base_path, opts)
|
|
60
|
+
index_content = generate_index_content(chunk_files, base_path, opts)
|
|
61
|
+
|
|
62
|
+
{
|
|
63
|
+
chunked: true,
|
|
64
|
+
total_chunks: chunks.size,
|
|
65
|
+
max_lines: @max_lines,
|
|
66
|
+
total_lines: lines.size,
|
|
67
|
+
index_file: "#{base_path}.md",
|
|
68
|
+
index_content: index_content,
|
|
69
|
+
chunk_files: chunk_files,
|
|
70
|
+
total_size: calculate_total_size(chunk_files)
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Split content and write all files (index + chunks)
|
|
75
|
+
def chunk_and_write(content, base_path, file_writer, options = {})
|
|
76
|
+
chunk_result = chunk_content(content, base_path, options)
|
|
77
|
+
|
|
78
|
+
unless chunk_result[:chunked]
|
|
79
|
+
# Write single file
|
|
80
|
+
return {
|
|
81
|
+
chunked: false,
|
|
82
|
+
files_written: 1,
|
|
83
|
+
results: [
|
|
84
|
+
file_writer.write(content, "#{base_path}.md", options)
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Write index file and all chunks
|
|
90
|
+
write_results = []
|
|
91
|
+
|
|
92
|
+
# Write index file
|
|
93
|
+
index_result = file_writer.write(
|
|
94
|
+
chunk_result[:index_content],
|
|
95
|
+
chunk_result[:index_file],
|
|
96
|
+
options
|
|
97
|
+
)
|
|
98
|
+
write_results << index_result.merge(file_type: "index")
|
|
99
|
+
|
|
100
|
+
# Write chunk files
|
|
101
|
+
chunk_result[:chunk_files].each do |chunk_info|
|
|
102
|
+
chunk_result_write = file_writer.write(
|
|
103
|
+
chunk_info[:content],
|
|
104
|
+
chunk_info[:path],
|
|
105
|
+
options
|
|
106
|
+
)
|
|
107
|
+
write_results << chunk_result_write.merge(file_type: "chunk", chunk_number: chunk_info[:chunk_number])
|
|
108
|
+
|
|
109
|
+
# Progress callback if provided
|
|
110
|
+
if options[:progress_callback]
|
|
111
|
+
options[:progress_callback].call("Wrote chunk #{chunk_info[:chunk_number]} of #{chunk_result[:total_chunks]}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
{
|
|
116
|
+
chunked: true,
|
|
117
|
+
total_chunks: chunk_result[:total_chunks],
|
|
118
|
+
files_written: write_results.size,
|
|
119
|
+
results: write_results
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
# Generate result for single file (no chunking needed)
|
|
126
|
+
def single_file_result(content, base_path)
|
|
127
|
+
{
|
|
128
|
+
chunked: false,
|
|
129
|
+
total_chunks: 1,
|
|
130
|
+
total_lines: content.lines.size,
|
|
131
|
+
file_path: "#{base_path}.md",
|
|
132
|
+
content: content,
|
|
133
|
+
total_size: content.bytesize
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Split lines into chunks using semantic boundaries when possible
|
|
138
|
+
# Semantic boundaries ensure XML elements like <file> and <output> are never split
|
|
139
|
+
def split_into_chunks(lines)
|
|
140
|
+
content = lines.join
|
|
141
|
+
|
|
142
|
+
# Check if content has semantic elements (XML tags we shouldn't split)
|
|
143
|
+
if Atoms::BoundaryFinder.has_semantic_elements?(content)
|
|
144
|
+
split_by_semantic_boundaries(content)
|
|
145
|
+
else
|
|
146
|
+
split_by_line_count(lines)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Split content using semantic boundaries (never splits <file> or <output> elements)
|
|
151
|
+
# Falls back to keeping large single elements whole rather than splitting them
|
|
152
|
+
def split_by_semantic_boundaries(content)
|
|
153
|
+
blocks = Atoms::BoundaryFinder.parse_blocks(content)
|
|
154
|
+
|
|
155
|
+
chunks = []
|
|
156
|
+
current_chunk_blocks = []
|
|
157
|
+
current_line_count = 0
|
|
158
|
+
|
|
159
|
+
blocks.each do |block|
|
|
160
|
+
block_lines = block[:lines]
|
|
161
|
+
|
|
162
|
+
# If adding this block would exceed limit and we have content, flush current chunk
|
|
163
|
+
if current_line_count + block_lines > @max_lines && current_chunk_blocks.any?
|
|
164
|
+
chunks << current_chunk_blocks.map { |b| b[:content] }.join
|
|
165
|
+
current_chunk_blocks = []
|
|
166
|
+
current_line_count = 0
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Add block to current chunk (even if it exceeds limit - we don't split elements)
|
|
170
|
+
current_chunk_blocks << block
|
|
171
|
+
current_line_count += block_lines
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Add remaining blocks
|
|
175
|
+
chunks << current_chunk_blocks.map { |b| b[:content] }.join if current_chunk_blocks.any?
|
|
176
|
+
|
|
177
|
+
chunks
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Original line-based splitting for content without semantic elements
|
|
181
|
+
def split_by_line_count(lines)
|
|
182
|
+
chunks = []
|
|
183
|
+
current_chunk = []
|
|
184
|
+
|
|
185
|
+
lines.each do |line|
|
|
186
|
+
current_chunk << line
|
|
187
|
+
|
|
188
|
+
if current_chunk.size >= @max_lines
|
|
189
|
+
chunks << current_chunk.join
|
|
190
|
+
current_chunk = []
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Add remaining lines
|
|
195
|
+
chunks << current_chunk.join unless current_chunk.empty?
|
|
196
|
+
|
|
197
|
+
chunks
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Generate chunk file information
|
|
201
|
+
def generate_chunk_files(chunks, base_path, options)
|
|
202
|
+
chunk_files = []
|
|
203
|
+
|
|
204
|
+
chunks.each_with_index do |chunk_content, index|
|
|
205
|
+
chunk_number = index + 1
|
|
206
|
+
chunk_path = "#{base_path}#{options[:chunk_suffix]}_#{chunk_number.to_s.rjust(3, "0")}.md"
|
|
207
|
+
|
|
208
|
+
chunk_files << {
|
|
209
|
+
chunk_number: chunk_number,
|
|
210
|
+
path: chunk_path,
|
|
211
|
+
content: chunk_content,
|
|
212
|
+
lines: chunk_content.lines.size,
|
|
213
|
+
size: chunk_content.bytesize
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
chunk_files
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Generate index file content
|
|
221
|
+
def generate_index_content(chunk_files, base_path, options)
|
|
222
|
+
index_lines = []
|
|
223
|
+
|
|
224
|
+
index_lines << "# Bundle Index"
|
|
225
|
+
index_lines << ""
|
|
226
|
+
index_lines << "This content has been split into #{chunk_files.size} chunks due to size constraints."
|
|
227
|
+
index_lines << ""
|
|
228
|
+
index_lines << "## Summary"
|
|
229
|
+
index_lines << ""
|
|
230
|
+
index_lines << "- Total chunks: #{chunk_files.size}"
|
|
231
|
+
index_lines << "- Max lines per chunk: #{@max_lines}"
|
|
232
|
+
index_lines << "- Total size: #{format_bytes(calculate_total_size(chunk_files))}"
|
|
233
|
+
index_lines << ""
|
|
234
|
+
index_lines << "## Chunks"
|
|
235
|
+
index_lines << ""
|
|
236
|
+
|
|
237
|
+
chunk_files.each do |chunk_info|
|
|
238
|
+
relative_path = chunk_info[:path].sub(%r{^.*/}, "")
|
|
239
|
+
index_lines << "### Chunk #{chunk_info[:chunk_number]}"
|
|
240
|
+
index_lines << ""
|
|
241
|
+
index_lines << "- File: [#{relative_path}](#{relative_path})"
|
|
242
|
+
index_lines << "- Lines: #{chunk_info[:lines]}"
|
|
243
|
+
index_lines << "- Size: #{format_bytes(chunk_info[:size])}"
|
|
244
|
+
index_lines << ""
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if options[:include_metadata]
|
|
248
|
+
index_lines << "## Metadata"
|
|
249
|
+
index_lines << ""
|
|
250
|
+
index_lines << "- Generated at: #{Time.now.iso8601}"
|
|
251
|
+
index_lines << "- Base path: #{base_path}"
|
|
252
|
+
index_lines << "- Chunk suffix: #{options[:chunk_suffix]}"
|
|
253
|
+
index_lines << ""
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
index_lines.join("\n")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Calculate total size of all chunks
|
|
260
|
+
def calculate_total_size(chunk_files)
|
|
261
|
+
chunk_files.sum { |chunk| chunk[:size] }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Format bytes for human readability
|
|
265
|
+
def format_bytes(bytes)
|
|
266
|
+
units = ["B", "KB", "MB", "GB"]
|
|
267
|
+
size = bytes.to_f
|
|
268
|
+
unit_index = 0
|
|
269
|
+
|
|
270
|
+
while size >= 1024 && unit_index < units.size - 1
|
|
271
|
+
size /= 1024
|
|
272
|
+
unit_index += 1
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
"#{size.round(2)} #{units[unit_index]}"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "bundle_chunker"
|
|
6
|
+
require_relative "section_formatter"
|
|
7
|
+
|
|
8
|
+
module Ace
|
|
9
|
+
module Bundle
|
|
10
|
+
module Molecules
|
|
11
|
+
# BundleFileWriter handles writing bundle to files with caching and chunking
|
|
12
|
+
# Configuration values (cache_dir, max_lines) are loaded from Ace::Bundle.config
|
|
13
|
+
# following ADR-022 pattern.
|
|
14
|
+
class BundleFileWriter
|
|
15
|
+
def initialize(cache_dir: nil, max_lines: nil)
|
|
16
|
+
@cache_dir = cache_dir || Ace::Bundle.cache_dir
|
|
17
|
+
@max_lines = max_lines || Ace::Bundle.max_lines
|
|
18
|
+
@chunker = BundleChunker.new(@max_lines)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Write bundle with optional chunking
|
|
22
|
+
def write_with_chunking(bundle, output_path, options = {})
|
|
23
|
+
# Check if we should organize by sections
|
|
24
|
+
if options[:organize_by_sections] && bundle.respond_to?(:has_sections?) && bundle.has_sections?
|
|
25
|
+
write_sections_organized(bundle, output_path, options)
|
|
26
|
+
else
|
|
27
|
+
content = format_content(bundle, options[:format])
|
|
28
|
+
|
|
29
|
+
# Determine actual output path
|
|
30
|
+
path = resolve_output_path(output_path)
|
|
31
|
+
|
|
32
|
+
# Check if chunking is needed
|
|
33
|
+
if @chunker.needs_chunking?(content)
|
|
34
|
+
write_chunked_content(content, path, options)
|
|
35
|
+
else
|
|
36
|
+
write_single_file(content, path, options)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Write single file
|
|
42
|
+
def write(content, path, options = {})
|
|
43
|
+
# Ensure directory exists
|
|
44
|
+
dir = File.dirname(path)
|
|
45
|
+
unless File.directory?(dir)
|
|
46
|
+
FileUtils.mkdir_p(dir)
|
|
47
|
+
# Validate that directory was created successfully
|
|
48
|
+
unless File.directory?(dir)
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: "Failed to create directory: #{dir}",
|
|
52
|
+
path: path
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Write file
|
|
58
|
+
File.write(path, content)
|
|
59
|
+
|
|
60
|
+
# Calculate statistics
|
|
61
|
+
lines = content.lines.size
|
|
62
|
+
size = content.bytesize
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
success: true,
|
|
66
|
+
path: path,
|
|
67
|
+
lines: lines,
|
|
68
|
+
size: size,
|
|
69
|
+
size_formatted: format_bytes(size)
|
|
70
|
+
}
|
|
71
|
+
rescue => e
|
|
72
|
+
{
|
|
73
|
+
success: false,
|
|
74
|
+
error: e.message,
|
|
75
|
+
path: path
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# Format content based on bundle data
|
|
82
|
+
def format_content(bundle, format = nil)
|
|
83
|
+
if bundle.respond_to?(:content)
|
|
84
|
+
bundle.content
|
|
85
|
+
elsif bundle.is_a?(Hash)
|
|
86
|
+
formatter = Ace::Core::Molecules::OutputFormatter.new(format || "markdown-xml")
|
|
87
|
+
formatter.format(bundle)
|
|
88
|
+
else
|
|
89
|
+
bundle.to_s
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Write bundle organized by sections
|
|
94
|
+
def write_sections_organized(bundle, output_path, options = {})
|
|
95
|
+
base_path = resolve_output_path(output_path)
|
|
96
|
+
format = options[:format] || "markdown-xml"
|
|
97
|
+
|
|
98
|
+
# Create section formatter
|
|
99
|
+
section_formatter = SectionFormatter.new(format)
|
|
100
|
+
|
|
101
|
+
# Write main index file
|
|
102
|
+
index_content = create_section_index(bundle, section_formatter)
|
|
103
|
+
index_result = write_single_file(index_content, base_path, options)
|
|
104
|
+
|
|
105
|
+
return index_result unless index_result[:success]
|
|
106
|
+
|
|
107
|
+
# Write individual section files
|
|
108
|
+
section_files = write_individual_sections(bundle, base_path, section_formatter, options)
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
success: true,
|
|
112
|
+
chunked: true,
|
|
113
|
+
index_file: base_path,
|
|
114
|
+
section_files: section_files,
|
|
115
|
+
total_files: section_files.size + 1,
|
|
116
|
+
sections_written: section_files.size,
|
|
117
|
+
lines: index_result[:lines],
|
|
118
|
+
size_formatted: index_result[:size_formatted]
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Create section index content
|
|
123
|
+
def create_section_index(bundle, section_formatter)
|
|
124
|
+
content = []
|
|
125
|
+
|
|
126
|
+
content << "# Bundle Sections Index"
|
|
127
|
+
content << ""
|
|
128
|
+
content << "This bundle is organized into the following sections:"
|
|
129
|
+
content << ""
|
|
130
|
+
|
|
131
|
+
# Add section links
|
|
132
|
+
bundle.sorted_sections.each do |section_name, section_data|
|
|
133
|
+
title = section_data[:title] || section_data["title"] || section_name.to_s.humanize
|
|
134
|
+
content << "- [#{title}](#{section_name}.md)"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
content << ""
|
|
138
|
+
content << "---"
|
|
139
|
+
content << ""
|
|
140
|
+
content << "## Complete Bundle"
|
|
141
|
+
content << ""
|
|
142
|
+
|
|
143
|
+
# Add complete formatted bundle
|
|
144
|
+
content << section_formatter.format_with_sections(bundle)
|
|
145
|
+
|
|
146
|
+
content.join("\n")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Write individual section files
|
|
150
|
+
def write_individual_sections(bundle, base_path, section_formatter, options)
|
|
151
|
+
section_files = []
|
|
152
|
+
base_dir = File.dirname(base_path)
|
|
153
|
+
base_name = File.basename(base_path, ".*")
|
|
154
|
+
|
|
155
|
+
bundle.sorted_sections.each do |section_name, section_data|
|
|
156
|
+
# Create section filename
|
|
157
|
+
section_filename = File.join(base_dir, "#{base_name}-#{section_name}.md")
|
|
158
|
+
|
|
159
|
+
# Create section content
|
|
160
|
+
section_content = create_section_content(section_name, section_data, bundle, section_formatter)
|
|
161
|
+
|
|
162
|
+
# Write section file
|
|
163
|
+
result = write_single_file(section_content, section_filename, options)
|
|
164
|
+
|
|
165
|
+
if result[:success]
|
|
166
|
+
section_files << {
|
|
167
|
+
section: section_name,
|
|
168
|
+
title: section_data[:title] || section_data["title"] || section_name.to_s.humanize,
|
|
169
|
+
file: section_filename,
|
|
170
|
+
lines: result[:lines],
|
|
171
|
+
size: result[:size]
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
section_files
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Create content for an individual section
|
|
180
|
+
def create_section_content(section_name, section_data, bundle, section_formatter)
|
|
181
|
+
content = []
|
|
182
|
+
|
|
183
|
+
title = section_data[:title] || section_data["title"] || section_name.to_s.humanize
|
|
184
|
+
|
|
185
|
+
content << "# #{title}"
|
|
186
|
+
content << ""
|
|
187
|
+
|
|
188
|
+
# Add section metadata
|
|
189
|
+
content << "**Section:** #{section_name}"
|
|
190
|
+
content << "**Content Type:** #{section_data[:content_type] || section_data["content_type"]}"
|
|
191
|
+
content << "**Priority:** #{section_data[:priority] || section_data["priority"] || "N/A"}"
|
|
192
|
+
content << ""
|
|
193
|
+
|
|
194
|
+
# Add section description if available
|
|
195
|
+
if section_data[:description] || section_data["description"]
|
|
196
|
+
content << "## Description"
|
|
197
|
+
content << section_data[:description] || section_data["description"]
|
|
198
|
+
content << ""
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Add section content
|
|
202
|
+
content << "## Content"
|
|
203
|
+
content << ""
|
|
204
|
+
|
|
205
|
+
# Format just this section
|
|
206
|
+
single_section = {section_name => section_data}
|
|
207
|
+
content << section_formatter.format_sections_only(single_section)
|
|
208
|
+
|
|
209
|
+
# Add navigation back to index
|
|
210
|
+
content << ""
|
|
211
|
+
content << "---"
|
|
212
|
+
content << ""
|
|
213
|
+
content << "[← Back to Index](#{File.basename(bundle.metadata[:output_file] || "bundle.md")})"
|
|
214
|
+
|
|
215
|
+
content.join("\n")
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Resolve output path with cache directory
|
|
219
|
+
def resolve_output_path(output_path)
|
|
220
|
+
# If output path is explicitly provided, use it as-is
|
|
221
|
+
output_path
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Write content in chunks
|
|
225
|
+
def write_chunked_content(content, base_path, options)
|
|
226
|
+
# Remove extension from base path for chunking
|
|
227
|
+
base_path_no_ext = base_path.sub(/\.[^.]+$/, "")
|
|
228
|
+
|
|
229
|
+
# Chunk and write content
|
|
230
|
+
result = @chunker.chunk_and_write(content, base_path_no_ext, self, options)
|
|
231
|
+
|
|
232
|
+
# Add formatted output path
|
|
233
|
+
result[:index_file] = "#{base_path_no_ext}.md"
|
|
234
|
+
result[:success] = true
|
|
235
|
+
|
|
236
|
+
result
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Write single file (no chunking)
|
|
240
|
+
def write_single_file(content, path, options)
|
|
241
|
+
result = write(content, path, options)
|
|
242
|
+
|
|
243
|
+
{
|
|
244
|
+
success: result[:success],
|
|
245
|
+
chunked: false,
|
|
246
|
+
files_written: result[:success] ? 1 : 0,
|
|
247
|
+
lines: result[:lines],
|
|
248
|
+
size_formatted: result[:size_formatted],
|
|
249
|
+
error: result[:error]
|
|
250
|
+
}
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Format bytes for human readability
|
|
254
|
+
def format_bytes(bytes)
|
|
255
|
+
units = ["B", "KB", "MB", "GB"]
|
|
256
|
+
size = bytes.to_f
|
|
257
|
+
unit_index = 0
|
|
258
|
+
|
|
259
|
+
while size >= 1024 && unit_index < units.size - 1
|
|
260
|
+
size /= 1024
|
|
261
|
+
unit_index += 1
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
"#{size.round(2)} #{units[unit_index]}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|