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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/bundle/config.yml +28 -0
  3. data/.ace-defaults/bundle/presets/base.md +15 -0
  4. data/.ace-defaults/bundle/presets/code-review.md +61 -0
  5. data/.ace-defaults/bundle/presets/development.md +16 -0
  6. data/.ace-defaults/bundle/presets/documentation-review.md +52 -0
  7. data/.ace-defaults/bundle/presets/mixed-content-example.md +94 -0
  8. data/.ace-defaults/bundle/presets/project-context.md +79 -0
  9. data/.ace-defaults/bundle/presets/project.md +35 -0
  10. data/.ace-defaults/bundle/presets/section-example-simple.md +27 -0
  11. data/.ace-defaults/bundle/presets/security-review.md +53 -0
  12. data/.ace-defaults/bundle/presets/simple-project.md +43 -0
  13. data/.ace-defaults/bundle/presets/team.md +18 -0
  14. data/.ace-defaults/nav/protocols/wfi-sources/ace-bundle.yml +19 -0
  15. data/CHANGELOG.md +384 -0
  16. data/LICENSE +21 -0
  17. data/README.md +40 -0
  18. data/Rakefile +22 -0
  19. data/exe/ace-bundle +14 -0
  20. data/handbook/skills/as-bundle/SKILL.md +28 -0
  21. data/handbook/skills/as-onboard/SKILL.md +33 -0
  22. data/handbook/workflow-instructions/bundle.wf.md +111 -0
  23. data/handbook/workflow-instructions/onboard.wf.md +20 -0
  24. data/lib/ace/bundle/atoms/boundary_finder.rb +122 -0
  25. data/lib/ace/bundle/atoms/bundle_normalizer.rb +128 -0
  26. data/lib/ace/bundle/atoms/content_checker.rb +46 -0
  27. data/lib/ace/bundle/atoms/line_counter.rb +37 -0
  28. data/lib/ace/bundle/atoms/preset_list_formatter.rb +44 -0
  29. data/lib/ace/bundle/atoms/preset_validator.rb +69 -0
  30. data/lib/ace/bundle/atoms/section_validator.rb +215 -0
  31. data/lib/ace/bundle/atoms/typo_detector.rb +76 -0
  32. data/lib/ace/bundle/cli/commands/load.rb +347 -0
  33. data/lib/ace/bundle/cli.rb +26 -0
  34. data/lib/ace/bundle/models/bundle_data.rb +75 -0
  35. data/lib/ace/bundle/molecules/bundle_chunker.rb +280 -0
  36. data/lib/ace/bundle/molecules/bundle_file_writer.rb +269 -0
  37. data/lib/ace/bundle/molecules/bundle_merger.rb +248 -0
  38. data/lib/ace/bundle/molecules/preset_manager.rb +331 -0
  39. data/lib/ace/bundle/molecules/section_compressor.rb +249 -0
  40. data/lib/ace/bundle/molecules/section_formatter.rb +580 -0
  41. data/lib/ace/bundle/molecules/section_processor.rb +460 -0
  42. data/lib/ace/bundle/organisms/bundle_loader.rb +1436 -0
  43. data/lib/ace/bundle/organisms/pr_bundle_loader.rb +147 -0
  44. data/lib/ace/bundle/version.rb +7 -0
  45. data/lib/ace/bundle.rb +251 -0
  46. metadata +190 -0
@@ -0,0 +1,580 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Bundle
5
+ module Molecules
6
+ # Formats sections with XML-style tags for different output formats
7
+ class SectionFormatter
8
+ def initialize(format = "markdown-xml")
9
+ @format = format
10
+ end
11
+
12
+ # Formats bundle data with sections
13
+ # @param bundle_data [BundleData] bundle data with sections
14
+ # @return [String] formatted output
15
+ def format_with_sections(bundle_data)
16
+ if bundle_data.has_sections?
17
+ format_sections_output(bundle_data)
18
+ else
19
+ # Fallback to regular formatting
20
+ format_legacy_output(bundle_data)
21
+ end
22
+ end
23
+
24
+ # Formats only the sections part (for inclusion in larger documents)
25
+ # @param sections [Hash] sections hash
26
+ # @return [String] formatted sections
27
+ def format_sections_only(sections)
28
+ return "" if sections.nil? || sections.empty?
29
+
30
+ sorted_sections = sections.sort_by { |name, data| data[:priority] || data["priority"] || 999 }
31
+
32
+ case @format
33
+ when "markdown-xml"
34
+ format_sections_markdown_xml(sorted_sections)
35
+ when "markdown"
36
+ format_sections_markdown(sorted_sections)
37
+ when "yaml"
38
+ format_sections_yaml(sorted_sections)
39
+ when "json"
40
+ format_sections_json(sorted_sections)
41
+ else
42
+ format_sections_markdown_xml(sorted_sections)
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ # Formats bundle data with sections based on format
49
+ def format_sections_output(bundle_data)
50
+ case @format
51
+ when "markdown-xml"
52
+ format_sections_markdown_xml_full(bundle_data)
53
+ when "markdown"
54
+ format_sections_markdown_full(bundle_data)
55
+ when "yaml"
56
+ format_sections_yaml_full(bundle_data)
57
+ when "json"
58
+ format_sections_json_full(bundle_data)
59
+ else
60
+ format_sections_markdown_xml_full(bundle_data)
61
+ end
62
+ end
63
+
64
+ # Formats full bundle data with sections in markdown-xml format
65
+ def format_sections_markdown_xml_full(bundle_data)
66
+ output = []
67
+
68
+ # Add any additional content FIRST (before sections)
69
+ if bundle_data.content && !bundle_data.content.empty?
70
+ output << bundle_data.content
71
+ output << "" # Empty line after content
72
+ end
73
+
74
+ # Add sections with XML tags
75
+ output << format_sections_markdown_xml(bundle_data.sorted_sections)
76
+
77
+ output.join("\n")
78
+ end
79
+
80
+ # Formats sections in markdown-xml format with XML tags
81
+ def format_sections_markdown_xml(sections)
82
+ output = []
83
+
84
+ sections.each do |name, section_data|
85
+ title = section_data[:title] || section_data["title"] || name.to_s.humanize
86
+ output << "# #{title}"
87
+
88
+ # Add description as plain paragraph if present
89
+ description = section_data[:description] || section_data["description"]
90
+ output << description if description && !description.empty?
91
+
92
+ output << "<#{name}>"
93
+
94
+ # Format all content types that are present in the section
95
+ if has_files_content?(section_data)
96
+ output << format_files_section(section_data)
97
+ end
98
+
99
+ if has_commands_content?(section_data)
100
+ output << format_commands_section(section_data)
101
+ end
102
+
103
+ if has_diffs_content?(section_data)
104
+ output << format_diffs_section(section_data)
105
+ end
106
+
107
+ if has_content_content?(section_data)
108
+ output << format_content_section(section_data)
109
+ end
110
+
111
+ output << "</#{name}>"
112
+ output << "" # Empty line between sections
113
+ end
114
+
115
+ output.join("\n")
116
+ end
117
+
118
+ # Formats files section with XML file tags
119
+ def format_files_section(section_data)
120
+ output = []
121
+
122
+ files = section_data[:_processed_files] || section_data["_processed_files"] || []
123
+ files.each do |file_info|
124
+ language = detect_language(file_info[:path])
125
+ output << " <file path=\"#{file_info[:path]}\" language=\"#{language}\">"
126
+ output << format_file_content(file_info[:content])
127
+ output << " </file>"
128
+ end
129
+
130
+ output.join("\n")
131
+ end
132
+
133
+ # Formats commands section with output tags
134
+ def format_commands_section(section_data)
135
+ output = []
136
+
137
+ commands = section_data[:_processed_commands] || section_data["_processed_commands"] || []
138
+ commands.each do |command_data|
139
+ output << " <output command=\"#{command_data[:command]}\">"
140
+ output << format_command_output(command_data[:output])
141
+ output << " </output>"
142
+ end
143
+
144
+ output.join("\n")
145
+ end
146
+
147
+ # Formats diffs section with output tags
148
+ def format_diffs_section(section_data)
149
+ output = []
150
+
151
+ diffs = section_data[:_processed_diffs] || section_data["_processed_diffs"] || []
152
+ diffs.each do |diff_data|
153
+ command = diff_command_for(diff_data)
154
+ output << " <output command=\"#{command}\">"
155
+ output << format_diff_output(diff_data[:output])
156
+ output << " </output>"
157
+ end
158
+
159
+ output.join("\n")
160
+ end
161
+
162
+ # Returns the appropriate command string for a diff based on its source
163
+ def diff_command_for(diff_data)
164
+ source = diff_data[:source] || diff_data["source"]
165
+ range = diff_data[:range] || diff_data["range"]
166
+
167
+ case source
168
+ when :pr, "pr"
169
+ "gh pr diff #{range.to_s.sub(/^pr:/, "")}"
170
+ else
171
+ "git diff #{range}"
172
+ end
173
+ end
174
+
175
+ # Formats inline content section
176
+ def format_content_section(section_data)
177
+ content = section_data[:_processed_content] || section_data["_processed_content"] || ""
178
+ format_inline_content(content)
179
+ end
180
+
181
+ # Formats full bundle data with sections in markdown format
182
+ def format_sections_markdown_full(bundle_data)
183
+ output = []
184
+
185
+ # Add any additional content FIRST (before sections)
186
+ if bundle_data.content && !bundle_data.content.empty?
187
+ output << bundle_data.content
188
+ output << "" # Empty line after content
189
+ end
190
+
191
+ # Add sections without XML tags
192
+ output << format_sections_markdown(bundle_data.sorted_sections)
193
+
194
+ output.join("\n")
195
+ end
196
+
197
+ # Formats sections in markdown format (no XML tags)
198
+ def format_sections_markdown(sections)
199
+ output = []
200
+
201
+ sections.each do |name, section_data|
202
+ title = section_data[:title] || section_data["title"] || name.to_s.humanize
203
+ output << "# #{title}"
204
+
205
+ # Add description as plain paragraph if present
206
+ description = section_data[:description] || section_data["description"]
207
+ output << description if description && !description.empty?
208
+
209
+ # Format all content types that are present in the section
210
+ if has_files_content?(section_data)
211
+ output << format_files_section_markdown(section_data)
212
+ end
213
+
214
+ if has_commands_content?(section_data)
215
+ output << format_commands_section_markdown(section_data)
216
+ end
217
+
218
+ if has_diffs_content?(section_data)
219
+ output << format_diffs_section_markdown(section_data)
220
+ end
221
+
222
+ if has_content_content?(section_data)
223
+ output << format_content_section_markdown(section_data)
224
+ end
225
+
226
+ output << "" # Empty line between sections
227
+ end
228
+
229
+ output.join("\n")
230
+ end
231
+
232
+ # Formats files section in markdown format
233
+ def format_files_section_markdown(section_data)
234
+ output = []
235
+
236
+ files = section_data[:_processed_files] || section_data["_processed_files"] || []
237
+ files.each do |file_info|
238
+ language = detect_language(file_info[:path])
239
+ output << "### #{file_info[:path]}"
240
+ output << "```#{language}"
241
+ output << file_info[:content]
242
+ output << "```"
243
+ output << ""
244
+ end
245
+
246
+ output.join("\n")
247
+ end
248
+
249
+ # Formats commands section in markdown format
250
+ def format_commands_section_markdown(section_data)
251
+ output = []
252
+
253
+ commands = section_data[:_processed_commands] || section_data["_processed_commands"] || []
254
+ commands.each do |command_data|
255
+ output << "### Command: `#{command_data[:command]}`"
256
+ output << "```"
257
+ output << command_data[:output]
258
+ output << "```"
259
+ output << ""
260
+ end
261
+
262
+ output.join("\n")
263
+ end
264
+
265
+ # Formats diffs section in markdown format
266
+ def format_diffs_section_markdown(section_data)
267
+ output = []
268
+
269
+ diffs = section_data[:_processed_diffs] || section_data["_processed_diffs"] || []
270
+ diffs.each do |diff_data|
271
+ output << "### Diff: `#{diff_data[:range]}`"
272
+ output << "```diff"
273
+ output << diff_data[:output]
274
+ output << "```"
275
+ output << ""
276
+ end
277
+
278
+ output.join("\n")
279
+ end
280
+
281
+ # Formats content section in markdown format
282
+ def format_content_section_markdown(section_data)
283
+ section_data[:_processed_content] || section_data["_processed_content"] || ""
284
+ end
285
+
286
+ # Formats sections in YAML format
287
+ def format_sections_yaml(sections)
288
+ require "yaml"
289
+
290
+ yaml_data = {}
291
+ sections.each do |name, section_data|
292
+ yaml_data[name] = {
293
+ "title" => section_data[:title] || section_data["title"],
294
+ "content_type" => section_data[:content_type] || section_data["content_type"],
295
+ "priority" => section_data[:priority] || section_data["priority"]
296
+ }
297
+
298
+ # Add processed content
299
+ case section_data[:content_type] || section_data["content_type"]
300
+ when "files"
301
+ yaml_data[name]["files"] = format_files_for_yaml(section_data)
302
+ when "commands"
303
+ yaml_data[name]["commands"] = format_commands_for_yaml(section_data)
304
+ when "diffs"
305
+ yaml_data[name]["diffs"] = format_diffs_for_yaml(section_data)
306
+ when "content"
307
+ yaml_data[name]["content"] = section_data[:_processed_content] || section_data["_processed_content"]
308
+ end
309
+ end
310
+
311
+ YAML.dump({"sections" => yaml_data})
312
+ end
313
+
314
+ # Formats full bundle data in YAML format
315
+ def format_sections_yaml_full(bundle_data)
316
+ require "yaml"
317
+
318
+ yaml_data = {
319
+ "preset_name" => bundle_data.preset_name,
320
+ "sections" => format_sections_for_yaml(bundle_data.sections),
321
+ "metadata" => bundle_data.metadata
322
+ }
323
+
324
+ if bundle_data.content && !bundle_data.content.empty?
325
+ yaml_data["content"] = bundle_data.content
326
+ end
327
+
328
+ YAML.dump(yaml_data)
329
+ end
330
+
331
+ # Formats sections in JSON format
332
+ def format_sections_json(sections)
333
+ json_data = {}
334
+ sections.each do |name, section_data|
335
+ json_data[name] = {
336
+ "title" => section_data[:title] || section_data["title"],
337
+ "content_type" => section_data[:content_type] || section_data["content_type"],
338
+ "priority" => section_data[:priority] || section_data["priority"]
339
+ }
340
+
341
+ # Add processed content
342
+ case section_data[:content_type] || section_data["content_type"]
343
+ when "files"
344
+ json_data[name]["files"] = format_files_for_json(section_data)
345
+ when "commands"
346
+ json_data[name]["commands"] = format_commands_for_json(section_data)
347
+ when "diffs"
348
+ json_data[name]["diffs"] = format_diffs_for_json(section_data)
349
+ when "content"
350
+ json_data[name]["content"] = section_data[:_processed_content] || section_data["_processed_content"]
351
+ end
352
+ end
353
+
354
+ JSON.pretty_generate({"sections" => json_data})
355
+ end
356
+
357
+ # Formats full bundle data in JSON format
358
+ def format_sections_json_full(bundle_data)
359
+ require "json"
360
+
361
+ json_data = {
362
+ "preset_name" => bundle_data.preset_name,
363
+ "sections" => format_sections_for_json(bundle_data.sections),
364
+ "metadata" => bundle_data.metadata
365
+ }
366
+
367
+ if bundle_data.content && !bundle_data.content.empty?
368
+ json_data["content"] = bundle_data.content
369
+ end
370
+
371
+ JSON.pretty_generate(json_data)
372
+ end
373
+
374
+ # Fallback formatting for non-section bundle data
375
+ def format_legacy_output(bundle_data)
376
+ # Use ace-core OutputFormatter as fallback
377
+ require "ace/core/molecules/output_formatter"
378
+ formatter = Ace::Core::Molecules::OutputFormatter.new(@format)
379
+
380
+ data = {
381
+ files: bundle_data.files,
382
+ metadata: bundle_data.metadata,
383
+ commands: bundle_data.commands,
384
+ content: bundle_data.content
385
+ }
386
+
387
+ formatter.format(data)
388
+ end
389
+
390
+ # Helper methods for formatting specific content types
391
+
392
+ def format_files_for_yaml(section_data)
393
+ files = section_data[:_processed_files] || section_data["_processed_files"] || []
394
+ files.map { |f| {"path" => f[:path], "content" => f[:content]} }
395
+ end
396
+
397
+ def format_commands_for_yaml(section_data)
398
+ commands = section_data[:_processed_commands] || section_data["_processed_commands"] || []
399
+ commands.map { |c| {"command" => c[:command], "output" => c[:output]} }
400
+ end
401
+
402
+ def format_diffs_for_yaml(section_data)
403
+ diffs = section_data[:_processed_diffs] || section_data["_processed_diffs"] || []
404
+ diffs.map { |d| {"range" => d[:range], "output" => d[:output]} }
405
+ end
406
+
407
+ def format_files_for_json(section_data)
408
+ format_files_for_yaml(section_data)
409
+ end
410
+
411
+ def format_commands_for_json(section_data)
412
+ format_commands_for_yaml(section_data)
413
+ end
414
+
415
+ def format_diffs_for_json(section_data)
416
+ format_diffs_for_yaml(section_data)
417
+ end
418
+
419
+ def format_sections_for_yaml(sections)
420
+ return {} if sections.nil? || sections.empty?
421
+
422
+ yaml_sections = {}
423
+ sections.each do |name, section_data|
424
+ yaml_sections[name] = {
425
+ "title" => section_data[:title] || section_data["title"],
426
+ "content_type" => section_data[:content_type] || section_data["content_type"],
427
+ "priority" => section_data[:priority] || section_data["priority"]
428
+ }
429
+
430
+ # Add processed content
431
+ case section_data[:content_type] || section_data["content_type"]
432
+ when "files"
433
+ yaml_sections[name]["files"] = format_files_for_yaml(section_data)
434
+ when "commands"
435
+ yaml_sections[name]["commands"] = format_commands_for_yaml(section_data)
436
+ when "diffs"
437
+ yaml_sections[name]["diffs"] = format_diffs_for_yaml(section_data)
438
+ when "content"
439
+ yaml_sections[name]["content"] = section_data[:_processed_content] || section_data["_processed_content"]
440
+ end
441
+ end
442
+ yaml_sections
443
+ end
444
+
445
+ def format_sections_for_json(sections)
446
+ return {} if sections.nil? || sections.empty?
447
+
448
+ json_sections = {}
449
+ sections.each do |name, section_data|
450
+ json_sections[name] = {
451
+ "title" => section_data[:title] || section_data["title"],
452
+ "content_type" => section_data[:content_type] || section_data["content_type"],
453
+ "priority" => section_data[:priority] || section_data["priority"]
454
+ }
455
+
456
+ # Add processed content
457
+ case section_data[:content_type] || section_data["content_type"]
458
+ when "files"
459
+ json_sections[name]["files"] = format_files_for_json(section_data)
460
+ when "commands"
461
+ json_sections[name]["commands"] = format_commands_for_json(section_data)
462
+ when "diffs"
463
+ json_sections[name]["diffs"] = format_diffs_for_json(section_data)
464
+ when "content"
465
+ json_sections[name]["content"] = section_data[:_processed_content] || section_data["_processed_content"]
466
+ end
467
+ end
468
+ json_sections
469
+ end
470
+
471
+ # Content formatting helpers
472
+ def format_file_content(content)
473
+ return "" if content.nil? || content.empty?
474
+
475
+ # Indent content for XML formatting
476
+ content.lines.map { |line| " #{line}" }.join.rstrip
477
+ end
478
+
479
+ def format_command_output(output)
480
+ return "" if output.nil? || output.empty?
481
+
482
+ # Indent output for XML formatting
483
+ output.lines.map { |line| " #{line}" }.join.rstrip
484
+ end
485
+
486
+ def format_diff_output(output)
487
+ return "" if output.nil? || output.empty?
488
+
489
+ # Indent diff output for XML formatting
490
+ output.lines.map { |line| " #{line}" }.join.rstrip
491
+ end
492
+
493
+ def format_inline_content(content)
494
+ return "" if content.nil? || content.empty?
495
+
496
+ content
497
+ end
498
+
499
+ # Language detection for file syntax highlighting
500
+ def detect_language(file_path)
501
+ LANGUAGE_MAP[File.extname(file_path).downcase] || "text"
502
+ end
503
+
504
+ # Language mapping for file extensions
505
+ LANGUAGE_MAP = {
506
+ ".rb" => "ruby",
507
+ ".py" => "python",
508
+ ".js" => "javascript",
509
+ ".ts" => "typescript",
510
+ ".jsx" => "jsx",
511
+ ".tsx" => "tsx",
512
+ ".java" => "java",
513
+ ".c" => "c",
514
+ ".cpp" => "cpp",
515
+ ".cc" => "cpp",
516
+ ".cxx" => "cpp",
517
+ ".h" => "c",
518
+ ".hpp" => "cpp",
519
+ ".cs" => "csharp",
520
+ ".php" => "php",
521
+ ".swift" => "swift",
522
+ ".kt" => "kotlin",
523
+ ".go" => "go",
524
+ ".rs" => "rust",
525
+ ".sh" => "bash",
526
+ ".bash" => "bash",
527
+ ".zsh" => "bash",
528
+ ".sql" => "sql",
529
+ ".html" => "html",
530
+ ".htm" => "html",
531
+ ".css" => "css",
532
+ ".scss" => "scss",
533
+ ".sass" => "scss",
534
+ ".less" => "less",
535
+ ".xml" => "xml",
536
+ ".json" => "json",
537
+ ".yaml" => "yaml",
538
+ ".yml" => "yaml",
539
+ ".toml" => "toml",
540
+ ".md" => "markdown",
541
+ ".markdown" => "markdown",
542
+ ".txt" => "text",
543
+ ".dockerfile" => "dockerfile",
544
+ ".gitignore" => "git",
545
+ ".gitattributes" => "git",
546
+ ".env" => "env",
547
+ ".ini" => "ini",
548
+ ".conf" => "apache"
549
+ }.freeze
550
+
551
+ # Helper methods to detect content types in sections
552
+
553
+ # Checks if section has files content
554
+ def has_files_content?(section_data)
555
+ !!(section_data[:files] || section_data["files"] ||
556
+ section_data[:_processed_files] || section_data["_processed_files"])
557
+ end
558
+
559
+ # Checks if section has commands content
560
+ def has_commands_content?(section_data)
561
+ !!(section_data[:commands] || section_data["commands"] ||
562
+ section_data[:_processed_commands] || section_data["_processed_commands"])
563
+ end
564
+
565
+ # Checks if section has diffs content
566
+ def has_diffs_content?(section_data)
567
+ !!(section_data[:ranges] || section_data["ranges"] ||
568
+ section_data[:diffs] || section_data["diffs"] ||
569
+ section_data[:_processed_diffs] || section_data["_processed_diffs"])
570
+ end
571
+
572
+ # Checks if section has inline content
573
+ def has_content_content?(section_data)
574
+ !!(section_data[:content] || section_data["content"] ||
575
+ section_data[:_processed_content] || section_data["_processed_content"])
576
+ end
577
+ end
578
+ end
579
+ end
580
+ end