ace-idea 0.18.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 (52) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/idea/config.yml +21 -0
  3. data/.ace-defaults/nav/protocols/wfi-sources/ace-idea.yml +19 -0
  4. data/CHANGELOG.md +387 -0
  5. data/README.md +42 -0
  6. data/Rakefile +13 -0
  7. data/docs/demo/ace-idea-getting-started.gif +0 -0
  8. data/docs/demo/ace-idea-getting-started.tape.yml +44 -0
  9. data/docs/demo/fixtures/README.md +3 -0
  10. data/docs/demo/fixtures/sample.txt +1 -0
  11. data/docs/getting-started.md +102 -0
  12. data/docs/handbook.md +39 -0
  13. data/docs/usage.md +320 -0
  14. data/exe/ace-idea +22 -0
  15. data/handbook/skills/as-idea-capture/SKILL.md +25 -0
  16. data/handbook/skills/as-idea-capture-features/SKILL.md +26 -0
  17. data/handbook/skills/as-idea-review/SKILL.md +26 -0
  18. data/handbook/workflow-instructions/idea/capture-features.wf.md +243 -0
  19. data/handbook/workflow-instructions/idea/capture.wf.md +270 -0
  20. data/handbook/workflow-instructions/idea/prioritize.wf.md +223 -0
  21. data/handbook/workflow-instructions/idea/review.wf.md +93 -0
  22. data/lib/ace/idea/atoms/idea_file_pattern.rb +40 -0
  23. data/lib/ace/idea/atoms/idea_frontmatter_defaults.rb +39 -0
  24. data/lib/ace/idea/atoms/idea_id_formatter.rb +37 -0
  25. data/lib/ace/idea/atoms/idea_validation_rules.rb +89 -0
  26. data/lib/ace/idea/atoms/slug_sanitizer_adapter.rb +6 -0
  27. data/lib/ace/idea/cli/commands/create.rb +98 -0
  28. data/lib/ace/idea/cli/commands/doctor.rb +206 -0
  29. data/lib/ace/idea/cli/commands/list.rb +62 -0
  30. data/lib/ace/idea/cli/commands/show.rb +55 -0
  31. data/lib/ace/idea/cli/commands/status.rb +61 -0
  32. data/lib/ace/idea/cli/commands/update.rb +118 -0
  33. data/lib/ace/idea/cli.rb +75 -0
  34. data/lib/ace/idea/models/idea.rb +39 -0
  35. data/lib/ace/idea/molecules/idea_clipboard_reader.rb +117 -0
  36. data/lib/ace/idea/molecules/idea_config_loader.rb +93 -0
  37. data/lib/ace/idea/molecules/idea_creator.rb +248 -0
  38. data/lib/ace/idea/molecules/idea_display_formatter.rb +165 -0
  39. data/lib/ace/idea/molecules/idea_doctor_fixer.rb +504 -0
  40. data/lib/ace/idea/molecules/idea_doctor_reporter.rb +264 -0
  41. data/lib/ace/idea/molecules/idea_frontmatter_validator.rb +137 -0
  42. data/lib/ace/idea/molecules/idea_llm_enhancer.rb +177 -0
  43. data/lib/ace/idea/molecules/idea_loader.rb +124 -0
  44. data/lib/ace/idea/molecules/idea_mover.rb +78 -0
  45. data/lib/ace/idea/molecules/idea_resolver.rb +57 -0
  46. data/lib/ace/idea/molecules/idea_scanner.rb +56 -0
  47. data/lib/ace/idea/molecules/idea_structure_validator.rb +157 -0
  48. data/lib/ace/idea/organisms/idea_doctor.rb +207 -0
  49. data/lib/ace/idea/organisms/idea_manager.rb +251 -0
  50. data/lib/ace/idea/version.rb +7 -0
  51. data/lib/ace/idea.rb +37 -0
  52. metadata +166 -0
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "../atoms/idea_frontmatter_defaults"
5
+ require_relative "../molecules/idea_config_loader"
6
+ require_relative "../molecules/idea_scanner"
7
+ require_relative "../molecules/idea_resolver"
8
+ require_relative "../molecules/idea_loader"
9
+ require_relative "../molecules/idea_creator"
10
+ require_relative "../molecules/idea_mover"
11
+
12
+ module Ace
13
+ module Idea
14
+ module Organisms
15
+ # Orchestrates all idea CRUD operations.
16
+ # Entry point for idea management with config-driven root directory.
17
+ class IdeaManager
18
+ attr_reader :last_list_total, :last_folder_counts
19
+
20
+ # @param root_dir [String, nil] Override root directory for ideas
21
+ # @param config [Hash, nil] Override configuration
22
+ def initialize(root_dir: nil, config: nil)
23
+ @config = config || load_config
24
+ @root_dir = root_dir || resolve_root_dir
25
+ end
26
+
27
+ # Create a new idea
28
+ # @param content [String, nil] Idea content
29
+ # @param title [String, nil] Optional explicit title
30
+ # @param tags [Array<String>] Tags
31
+ # @param move_to [String, nil] Target folder
32
+ # @param clipboard [Boolean] Capture from clipboard
33
+ # @param llm_enhance [Boolean] Enhance with LLM
34
+ # @return [Idea] Created idea
35
+ def create(content = nil, title: nil, tags: [], move_to: nil,
36
+ clipboard: false, llm_enhance: false)
37
+ ensure_root_dir
38
+ creator = Molecules::IdeaCreator.new(root_dir: @root_dir, config: @config)
39
+ creator.create(content, title: title, tags: tags, move_to: move_to,
40
+ clipboard: clipboard, llm_enhance: llm_enhance)
41
+ end
42
+
43
+ # Create an idea from clipboard
44
+ # @param llm_enhance [Boolean] Enhance with LLM after clipboard capture
45
+ # @param move_to [String, nil] Target folder
46
+ # @return [Idea] Created idea
47
+ def create_from_clipboard(llm_enhance: false, move_to: nil)
48
+ create(nil, clipboard: true, llm_enhance: llm_enhance, move_to: move_to)
49
+ end
50
+
51
+ # Show (load) a single idea by reference
52
+ # @param ref [String] Full ID (6 chars) or suffix shortcut (3 chars)
53
+ # @return [Idea, nil] Loaded idea or nil if not found
54
+ def show(ref)
55
+ resolver = Molecules::IdeaResolver.new(@root_dir)
56
+ scan_result = resolver.resolve(ref)
57
+ return nil unless scan_result
58
+
59
+ loader = Molecules::IdeaLoader.new
60
+ loader.load(scan_result.dir_path,
61
+ id: scan_result.id,
62
+ special_folder: scan_result.special_folder)
63
+ end
64
+
65
+ # List ideas with optional filtering
66
+ # @param status [String, nil] Filter by status
67
+ # @param in_folder [String, nil] Filter by special folder (default: "next" = root items only)
68
+ # @param tags [Array<String>] Filter by tags (any match)
69
+ # @param root [String, nil] Override root path (subpath within root_dir)
70
+ # @param filters [Array<String>, nil] Generic filter strings (e.g., ["status:pending", "tags:ux|design"])
71
+ # @return [Array<Idea>] List of ideas
72
+ def list(status: nil, in_folder: "next", tags: [], root: nil, filters: nil)
73
+ scan_root = if root
74
+ candidate = File.expand_path(File.join(@root_dir, root))
75
+ root_real = File.expand_path(@root_dir)
76
+ unless candidate.start_with?(root_real + File::SEPARATOR) || candidate == root_real
77
+ raise ArgumentError, "Path traversal detected in --root option"
78
+ end
79
+ candidate
80
+ else
81
+ @root_dir
82
+ end
83
+ scanner = Molecules::IdeaScanner.new(scan_root)
84
+ scan_results = scanner.scan_in_folder(in_folder)
85
+ @last_list_total = scanner.last_scan_total
86
+ @last_folder_counts = scanner.last_folder_counts
87
+
88
+ loader = Molecules::IdeaLoader.new
89
+ ideas = scan_results.filter_map do |sr|
90
+ loader.load(sr.dir_path, id: sr.id, special_folder: sr.special_folder)
91
+ end
92
+
93
+ # Apply legacy filters (backward-compatible)
94
+ ideas = ideas.select { |i| i.status == status } if status
95
+ ideas = filter_by_tags(ideas, tags) if tags.any?
96
+
97
+ # Apply generic --filter specs via FilterApplier
98
+ if filters && !filters.empty?
99
+ filter_specs = Ace::Support::Items::Atoms::FilterParser.parse(filters)
100
+ ideas = Ace::Support::Items::Molecules::FilterApplier.apply(ideas, filter_specs, value_accessor: method(:idea_value_accessor))
101
+ end
102
+
103
+ ideas
104
+ end
105
+
106
+ # Update an idea's fields and optionally move to a folder.
107
+ # @param ref [String] Idea reference
108
+ # @param set [Hash] Fields to set (key => value)
109
+ # @param add [Hash] Fields to add to (for arrays like tags)
110
+ # @param remove [Hash] Fields to remove from (for arrays)
111
+ # @param move_to [String, nil] Target folder to move to (archive, maybe, anytime, next/root//)
112
+ # @return [Idea, nil] Updated idea or nil if not found
113
+ def update(ref, set: {}, add: {}, remove: {}, move_to: nil)
114
+ scan_result = resolve_scan_result(ref)
115
+ return nil unless scan_result
116
+
117
+ loader = Molecules::IdeaLoader.new
118
+ idea = loader.load(scan_result.dir_path,
119
+ id: scan_result.id,
120
+ special_folder: scan_result.special_folder)
121
+ return nil unless idea
122
+
123
+ # Apply field updates if any
124
+ has_field_updates = [set, add, remove].any? { |h| h && !h.empty? }
125
+ update_idea_file(idea, set: set, add: add, remove: remove) if has_field_updates
126
+
127
+ # Apply move if requested
128
+ current_path = idea.path
129
+ current_special = idea.special_folder
130
+ if move_to
131
+ mover = Molecules::IdeaMover.new(@root_dir)
132
+ new_path = if Ace::Support::Items::Atoms::SpecialFolderDetector.move_to_root?(move_to)
133
+ mover.move_to_root(idea)
134
+ else
135
+ archive_date = parse_archive_date(idea)
136
+ mover.move(idea, to: move_to, date: archive_date)
137
+ end
138
+ current_path = new_path
139
+ current_special = Ace::Support::Items::Atoms::SpecialFolderDetector.detect_in_path(
140
+ new_path, root: @root_dir
141
+ )
142
+ end
143
+
144
+ # Reload and return updated idea
145
+ loader.load(current_path, id: idea.id, special_folder: current_special)
146
+ end
147
+
148
+ # Get the root directory
149
+ # @return [String] Absolute path to ideas root
150
+ attr_reader :root_dir
151
+
152
+ private
153
+
154
+ def load_config
155
+ gem_root = File.expand_path("../../../..", __dir__)
156
+ Molecules::IdeaConfigLoader.load(gem_root: gem_root)
157
+ end
158
+
159
+ def resolve_root_dir
160
+ Molecules::IdeaConfigLoader.root_dir(@config)
161
+ end
162
+
163
+ def ensure_root_dir
164
+ require "fileutils"
165
+ FileUtils.mkdir_p(@root_dir) unless Dir.exist?(@root_dir)
166
+ end
167
+
168
+ def resolve_scan_result(ref)
169
+ resolver = Molecules::IdeaResolver.new(@root_dir)
170
+ resolver.resolve(ref)
171
+ end
172
+
173
+ def filter_by_tags(ideas, tags)
174
+ return ideas if tags.empty?
175
+
176
+ ideas.select do |idea|
177
+ tags.any? { |tag| idea.tags.include?(tag) }
178
+ end
179
+ end
180
+
181
+ def update_idea_file(idea, set:, add:, remove:)
182
+ content = File.read(idea.file_path)
183
+ frontmatter, body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
184
+ # Strip leading newline from body so rebuild doesn't double-space
185
+ body = body.sub(/\A\n/, "")
186
+
187
+ # Apply set operations
188
+ set.each { |k, v| frontmatter[k.to_s] = v }
189
+
190
+ # Apply add operations (for arrays)
191
+ add.each do |k, v|
192
+ key = k.to_s
193
+ current = Array(frontmatter[key])
194
+ values = Array(v)
195
+ frontmatter[key] = (current + values).uniq
196
+ end
197
+
198
+ # Apply remove operations (for arrays)
199
+ remove.each do |k, v|
200
+ key = k.to_s
201
+ next unless frontmatter[key].is_a?(Array)
202
+
203
+ values = Array(v)
204
+ frontmatter[key] = frontmatter[key] - values
205
+ end
206
+
207
+ # Write back atomically (temp + rename to avoid partial writes)
208
+ new_content = Ace::Support::Items::Atoms::FrontmatterSerializer.rebuild(frontmatter, body)
209
+ tmp_path = "#{idea.file_path}.tmp.#{Process.pid}"
210
+ File.write(tmp_path, new_content)
211
+ File.rename(tmp_path, idea.file_path)
212
+ ensure
213
+ begin
214
+ File.unlink(tmp_path) if tmp_path && File.exist?(tmp_path)
215
+ rescue
216
+ nil
217
+ end
218
+ end
219
+
220
+ # Extract archive date from idea frontmatter, falling back to Time.now
221
+ def parse_archive_date(idea)
222
+ raw = idea.metadata["completed_at"] || idea.metadata["created_at"]
223
+ return nil unless raw
224
+
225
+ case raw
226
+ when Time then raw
227
+ when DateTime then raw.to_time
228
+ else begin
229
+ Time.parse(raw.to_s)
230
+ rescue
231
+ nil
232
+ end
233
+ end
234
+ end
235
+
236
+ # Value accessor for FilterApplier — reads from Idea model attributes and metadata
237
+ def idea_value_accessor(item, key)
238
+ case key
239
+ when "status" then item.status
240
+ when "title" then item.title
241
+ when "tags" then item.tags
242
+ when "id" then item.id
243
+ when "special_folder" then item.special_folder
244
+ else
245
+ item.metadata[key] || item.metadata[key.to_sym] if item.respond_to?(:metadata) && item.metadata
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Idea
5
+ VERSION = "0.18.0"
6
+ end
7
+ end
data/lib/ace/idea.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "idea/version"
4
+
5
+ # External dependencies
6
+ require "ace/b36ts"
7
+ require "ace/support/items"
8
+
9
+ # Atoms
10
+ require_relative "idea/atoms/idea_id_formatter"
11
+ require_relative "idea/atoms/idea_file_pattern"
12
+ require_relative "idea/atoms/idea_frontmatter_defaults"
13
+
14
+ # Models
15
+ require_relative "idea/models/idea"
16
+
17
+ # Molecules
18
+ require_relative "idea/molecules/idea_config_loader"
19
+ require_relative "idea/molecules/idea_scanner"
20
+ require_relative "idea/molecules/idea_resolver"
21
+ require_relative "idea/molecules/idea_loader"
22
+ require_relative "idea/molecules/idea_creator"
23
+ require_relative "idea/molecules/idea_llm_enhancer"
24
+ require_relative "idea/molecules/idea_clipboard_reader"
25
+ require_relative "idea/molecules/idea_mover"
26
+ require_relative "idea/molecules/idea_display_formatter"
27
+
28
+ # Organisms
29
+ require_relative "idea/organisms/idea_manager"
30
+
31
+ module Ace
32
+ # Idea management gem for ACE.
33
+ # Manages ideas in .ace-ideas/ using raw 6-char b36ts IDs.
34
+ module Idea
35
+ class Error < StandardError; end
36
+ end
37
+ end
metadata ADDED
@@ -0,0 +1,166 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ace-idea
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.18.0
5
+ platform: ruby
6
+ authors:
7
+ - Michal Czyz
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ace-support-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.25'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.25'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ace-support-fs
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.2'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ace-support-items
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.3'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.3'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ace-b36ts
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.7'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.7'
68
+ - !ruby/object:Gem::Dependency
69
+ name: ace-support-cli
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '0.3'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.3'
82
+ description: Capture rough ideas quickly, store them as structured files, organize
83
+ them with GTD-style folders, and manage them through a focused six-command CLI.
84
+ email:
85
+ - mc@cs3b.com
86
+ executables:
87
+ - ace-idea
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".ace-defaults/idea/config.yml"
92
+ - ".ace-defaults/nav/protocols/wfi-sources/ace-idea.yml"
93
+ - CHANGELOG.md
94
+ - README.md
95
+ - Rakefile
96
+ - docs/demo/ace-idea-getting-started.gif
97
+ - docs/demo/ace-idea-getting-started.tape.yml
98
+ - docs/demo/fixtures/README.md
99
+ - docs/demo/fixtures/sample.txt
100
+ - docs/getting-started.md
101
+ - docs/handbook.md
102
+ - docs/usage.md
103
+ - exe/ace-idea
104
+ - handbook/skills/as-idea-capture-features/SKILL.md
105
+ - handbook/skills/as-idea-capture/SKILL.md
106
+ - handbook/skills/as-idea-review/SKILL.md
107
+ - handbook/workflow-instructions/idea/capture-features.wf.md
108
+ - handbook/workflow-instructions/idea/capture.wf.md
109
+ - handbook/workflow-instructions/idea/prioritize.wf.md
110
+ - handbook/workflow-instructions/idea/review.wf.md
111
+ - lib/ace/idea.rb
112
+ - lib/ace/idea/atoms/idea_file_pattern.rb
113
+ - lib/ace/idea/atoms/idea_frontmatter_defaults.rb
114
+ - lib/ace/idea/atoms/idea_id_formatter.rb
115
+ - lib/ace/idea/atoms/idea_validation_rules.rb
116
+ - lib/ace/idea/atoms/slug_sanitizer_adapter.rb
117
+ - lib/ace/idea/cli.rb
118
+ - lib/ace/idea/cli/commands/create.rb
119
+ - lib/ace/idea/cli/commands/doctor.rb
120
+ - lib/ace/idea/cli/commands/list.rb
121
+ - lib/ace/idea/cli/commands/show.rb
122
+ - lib/ace/idea/cli/commands/status.rb
123
+ - lib/ace/idea/cli/commands/update.rb
124
+ - lib/ace/idea/models/idea.rb
125
+ - lib/ace/idea/molecules/idea_clipboard_reader.rb
126
+ - lib/ace/idea/molecules/idea_config_loader.rb
127
+ - lib/ace/idea/molecules/idea_creator.rb
128
+ - lib/ace/idea/molecules/idea_display_formatter.rb
129
+ - lib/ace/idea/molecules/idea_doctor_fixer.rb
130
+ - lib/ace/idea/molecules/idea_doctor_reporter.rb
131
+ - lib/ace/idea/molecules/idea_frontmatter_validator.rb
132
+ - lib/ace/idea/molecules/idea_llm_enhancer.rb
133
+ - lib/ace/idea/molecules/idea_loader.rb
134
+ - lib/ace/idea/molecules/idea_mover.rb
135
+ - lib/ace/idea/molecules/idea_resolver.rb
136
+ - lib/ace/idea/molecules/idea_scanner.rb
137
+ - lib/ace/idea/molecules/idea_structure_validator.rb
138
+ - lib/ace/idea/organisms/idea_doctor.rb
139
+ - lib/ace/idea/organisms/idea_manager.rb
140
+ - lib/ace/idea/version.rb
141
+ homepage: https://github.com/cs3b/ace
142
+ licenses:
143
+ - MIT
144
+ metadata:
145
+ allowed_push_host: https://rubygems.org
146
+ homepage_uri: https://github.com/cs3b/ace
147
+ source_code_uri: https://github.com/cs3b/ace/tree/main/ace-idea/
148
+ changelog_uri: https://github.com/cs3b/ace/blob/main/ace-idea/CHANGELOG.md
149
+ rdoc_options: []
150
+ require_paths:
151
+ - lib
152
+ required_ruby_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: 3.2.0
157
+ required_rubygems_version: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ requirements: []
163
+ rubygems_version: 3.6.9
164
+ specification_version: 4
165
+ summary: Capture ideas quickly, then shape and organize them for execution
166
+ test_files: []