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,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ace/compressor"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "tmpdir"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Bundle
|
|
9
|
+
module Molecules
|
|
10
|
+
# Compresses file content within bundle sections using ace-compressor.
|
|
11
|
+
# Each section is compressed independently. Files that are not
|
|
12
|
+
# compressible (non-markdown/text) pass through unchanged.
|
|
13
|
+
#
|
|
14
|
+
# Uses the compressor's file-based API (compress_sources) so that both
|
|
15
|
+
# "exact" and "agent" engines work through the same code path.
|
|
16
|
+
class SectionCompressor
|
|
17
|
+
COMPRESSIBLE_EXTENSIONS = %w[.md .markdown .mdown .mkd .txt .text].freeze
|
|
18
|
+
|
|
19
|
+
# @param default_mode [String] default source scope: "off", "per-source", "merged"
|
|
20
|
+
# @param compressor_mode [String] compressor engine: "exact", "agent"
|
|
21
|
+
# @param cache_store [Ace::Compressor::Molecules::CacheStore, nil] injectable cache store
|
|
22
|
+
def initialize(default_mode: "off", compressor_mode: "exact", cache_store: nil)
|
|
23
|
+
@default_mode = default_mode.to_s
|
|
24
|
+
@compressor_mode = compressor_mode.to_s
|
|
25
|
+
@cache_store = cache_store || Ace::Compressor::Molecules::CacheStore.new
|
|
26
|
+
validate_compressor_mode!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Compress files in all sections of a bundle.
|
|
30
|
+
# @param bundle_data [Models::BundleData] bundle with sections
|
|
31
|
+
# @return [Models::BundleData] same bundle with compressed file content
|
|
32
|
+
def call(bundle_data)
|
|
33
|
+
unless bundle_data.has_sections?
|
|
34
|
+
compress_content(bundle_data) if @default_mode != "off"
|
|
35
|
+
return bundle_data
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
bundle_data.sections.each do |name, section_data|
|
|
39
|
+
section_mode = resolve_mode(section_data)
|
|
40
|
+
next if section_mode == "off"
|
|
41
|
+
|
|
42
|
+
compress_section_files(section_data, section_mode)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
bundle_data
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def validate_compressor_mode!
|
|
51
|
+
return if %w[exact agent].include?(@compressor_mode)
|
|
52
|
+
|
|
53
|
+
raise ArgumentError, "Unknown compressor_mode: #{@compressor_mode.inspect}. Supported: exact, agent"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def resolve_mode(section_data)
|
|
57
|
+
# Section-level params override: check compressor_source_scope first, fall back to compress
|
|
58
|
+
section_scope = section_data.dig(:params, :compressor_source_scope) ||
|
|
59
|
+
section_data.dig(:params, "compressor_source_scope") ||
|
|
60
|
+
section_data.dig("params", "compressor_source_scope") ||
|
|
61
|
+
section_data.dig(:params, :compress) ||
|
|
62
|
+
section_data.dig(:params, "compress") ||
|
|
63
|
+
section_data.dig("params", "compress")
|
|
64
|
+
mode = (section_scope || @default_mode).to_s
|
|
65
|
+
return "off" unless %w[off per-source merged].include?(mode)
|
|
66
|
+
|
|
67
|
+
mode
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_compressor(paths)
|
|
71
|
+
case @compressor_mode
|
|
72
|
+
when "exact"
|
|
73
|
+
Ace::Compressor::Organisms::ExactCompressor.new(paths, mode_label: "exact")
|
|
74
|
+
when "agent"
|
|
75
|
+
Ace::Compressor::Organisms::AgentCompressor.new(paths)
|
|
76
|
+
else
|
|
77
|
+
raise ArgumentError, "Unknown compressor_mode: #{@compressor_mode.inspect}. Supported: exact, agent"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def compress_section_files(section_data, mode)
|
|
82
|
+
files = section_data[:_processed_files]
|
|
83
|
+
return if files.nil? || files.empty?
|
|
84
|
+
|
|
85
|
+
compressible = files.select { |f| compressible?(f[:path]) }
|
|
86
|
+
return if compressible.empty?
|
|
87
|
+
|
|
88
|
+
case mode
|
|
89
|
+
when "per-source"
|
|
90
|
+
compress_per_source(compressible)
|
|
91
|
+
when "merged"
|
|
92
|
+
merged = compress_merged(compressible)
|
|
93
|
+
merged_inserted = false
|
|
94
|
+
section_data[:_processed_files] = files.each_with_object([]) do |file_info, ordered_files|
|
|
95
|
+
if compressible?(file_info[:path])
|
|
96
|
+
next if merged_inserted
|
|
97
|
+
|
|
98
|
+
ordered_files << merged
|
|
99
|
+
merged_inserted = true
|
|
100
|
+
else
|
|
101
|
+
ordered_files << file_info
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def compress_per_source(files)
|
|
108
|
+
Dir.mktmpdir("ace-bundle-compress") do |tmpdir|
|
|
109
|
+
uncached = []
|
|
110
|
+
files.each do |file_info|
|
|
111
|
+
fragment = write_fragment(tmpdir, file_info[:path], file_info[:content])
|
|
112
|
+
source_entry = cache_source_entry(fragment, file_info[:path], source_kind: "file")
|
|
113
|
+
manifest = @cache_store.manifest(mode: @compressor_mode, sources: [source_entry])
|
|
114
|
+
canonical = @cache_store.canonical_paths(mode: @compressor_mode, sources: [source_entry], manifest_key: manifest["key"])
|
|
115
|
+
|
|
116
|
+
if @cache_store.cache_hit?(pack_path: canonical[:pack_path], metadata_path: canonical[:metadata_path])
|
|
117
|
+
file_info[:content] = @cache_store.read_pack(canonical[:pack_path])
|
|
118
|
+
file_info[:compressed] = true
|
|
119
|
+
else
|
|
120
|
+
uncached << {file_info: file_info, fragment: fragment, manifest: manifest, canonical: canonical}
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
uncached.each do |entry|
|
|
125
|
+
fi = entry[:file_info]
|
|
126
|
+
compressor = build_compressor([entry[:fragment]])
|
|
127
|
+
output = compress_with_source_identity(compressor, entry[:fragment] => fi[:path])
|
|
128
|
+
compressed = strip_context_pack_header(output)
|
|
129
|
+
write_cache_entry(entry[:manifest], entry[:canonical], compressed)
|
|
130
|
+
fi[:content] = compressed
|
|
131
|
+
fi[:compressed] = true
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def compress_merged(files)
|
|
137
|
+
Dir.mktmpdir("ace-bundle-compress") do |tmpdir|
|
|
138
|
+
path_map = {}
|
|
139
|
+
source_entries = files.map do |f|
|
|
140
|
+
fragment = write_fragment(tmpdir, f[:path], f[:content])
|
|
141
|
+
path_map[fragment] = f[:path]
|
|
142
|
+
cache_source_entry(fragment, f[:path], source_kind: "file")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
manifest = @cache_store.manifest(mode: @compressor_mode, sources: source_entries)
|
|
146
|
+
canonical = @cache_store.canonical_paths(mode: @compressor_mode, sources: source_entries, manifest_key: manifest["key"])
|
|
147
|
+
|
|
148
|
+
content = if @cache_store.cache_hit?(pack_path: canonical[:pack_path], metadata_path: canonical[:metadata_path])
|
|
149
|
+
@cache_store.read_pack(canonical[:pack_path])
|
|
150
|
+
else
|
|
151
|
+
fragments = source_entries.map { |entry| entry[:content_path] }
|
|
152
|
+
compressor = build_compressor(fragments)
|
|
153
|
+
output = compress_with_source_identity(compressor, path_map)
|
|
154
|
+
compressed = strip_context_pack_header(output)
|
|
155
|
+
write_cache_entry(manifest, canonical, compressed)
|
|
156
|
+
compressed
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
path: files.first[:path],
|
|
161
|
+
content: content,
|
|
162
|
+
compressed: true,
|
|
163
|
+
merged_sources: files.map { |f| f[:path] }
|
|
164
|
+
}
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def write_fragment(tmpdir, path, content)
|
|
169
|
+
fragment = File.join(tmpdir, path)
|
|
170
|
+
FileUtils.mkdir_p(File.dirname(fragment))
|
|
171
|
+
File.write(fragment, content)
|
|
172
|
+
fragment
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def strip_context_pack_header(output)
|
|
176
|
+
lines = output.lines
|
|
177
|
+
lines.shift if lines.first&.start_with?("H|ContextPack/")
|
|
178
|
+
lines.join.strip
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def write_cache_entry(manifest, canonical, compressed)
|
|
182
|
+
metadata = {
|
|
183
|
+
"schema" => Ace::Compressor::Models::ContextPack::SCHEMA,
|
|
184
|
+
"mode" => @compressor_mode,
|
|
185
|
+
"key" => manifest["key"],
|
|
186
|
+
"short_key" => canonical[:short_key],
|
|
187
|
+
"sources" => manifest["sources"],
|
|
188
|
+
"file_count" => manifest["sources"].size,
|
|
189
|
+
"original_bytes" => manifest["original_bytes"],
|
|
190
|
+
"original_lines" => manifest["original_lines"],
|
|
191
|
+
"packed_bytes" => compressed.bytesize,
|
|
192
|
+
"packed_lines" => compressed.lines.count
|
|
193
|
+
}
|
|
194
|
+
@cache_store.write_cache(
|
|
195
|
+
pack_path: canonical[:pack_path],
|
|
196
|
+
metadata_path: canonical[:metadata_path],
|
|
197
|
+
content: compressed,
|
|
198
|
+
metadata: metadata
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def compressible?(path)
|
|
203
|
+
ext = File.extname(path.to_s).downcase
|
|
204
|
+
COMPRESSIBLE_EXTENSIONS.include?(ext)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Compress content-only bundles (no sections) using the real file path.
|
|
208
|
+
# Uses the source metadata path directly — no temp files needed.
|
|
209
|
+
def compress_content(bundle_data)
|
|
210
|
+
source = bundle_data.metadata[:source]&.to_s
|
|
211
|
+
return if source.nil? || source.empty? || !File.exist?(source) || !compressible?(source)
|
|
212
|
+
|
|
213
|
+
manifest = @cache_store.manifest(mode: @compressor_mode, sources: [source])
|
|
214
|
+
canonical = @cache_store.canonical_paths(mode: @compressor_mode, sources: [source], manifest_key: manifest["key"])
|
|
215
|
+
|
|
216
|
+
if @cache_store.cache_hit?(pack_path: canonical[:pack_path], metadata_path: canonical[:metadata_path])
|
|
217
|
+
bundle_data.content = @cache_store.read_pack(canonical[:pack_path])
|
|
218
|
+
else
|
|
219
|
+
compressor = build_compressor([source])
|
|
220
|
+
output = compressor.compress_sources([source])
|
|
221
|
+
compressed = strip_context_pack_header(output)
|
|
222
|
+
write_cache_entry(manifest, canonical, compressed)
|
|
223
|
+
bundle_data.content = compressed
|
|
224
|
+
end
|
|
225
|
+
bundle_data.metadata[:compressed] = true
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def compress_with_source_identity(compressor, path_map)
|
|
229
|
+
parameters = compressor.method(:compress_sources).parameters
|
|
230
|
+
supports_source_paths = parameters.any? do |type, name|
|
|
231
|
+
([:key, :keyreq].include?(type) && name == :source_paths) || type == :keyrest
|
|
232
|
+
end
|
|
233
|
+
fragments = path_map.keys
|
|
234
|
+
return compressor.compress_sources(fragments, source_paths: path_map) if supports_source_paths
|
|
235
|
+
|
|
236
|
+
compressor.compress_sources(fragments)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def cache_source_entry(content_path, source_path, source_kind:)
|
|
240
|
+
{
|
|
241
|
+
content_path: content_path,
|
|
242
|
+
source_path: source_path,
|
|
243
|
+
source_kind: source_kind
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|