ace-idea 0.18.5 → 0.21.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf7ef23ac4a38f4d26294f5fb596febbb00a78b4279043a2909a47e510bb5387
4
- data.tar.gz: 6f533a3b40614a87f532c0a14bf0060a577c825409b00b90aa4fd26b71644cb8
3
+ metadata.gz: '0669d1f1ea0f1d9e894bf29551ae7575a81983bffe85fb0de6225edca8a3b6de'
4
+ data.tar.gz: 2cfa1bd44ac20a02f462d35f8bafd27374bb4533a2719e0192a783707437fcaa
5
5
  SHA512:
6
- metadata.gz: ffc4926286559c5df48894692633cb977c72228c893595213ceb4dcff52a1c3788b78f5d58c2509449b054c540f5937b2723fcffb9fa5586ca95544108536114
7
- data.tar.gz: 0f3927c864fb1a43ab51f5930b883daaa943168e67917ba836329352c613dc24570805a308543945f52887f3d39af90c930cd3ab9e06b8dbcd3c781b8b900776
6
+ metadata.gz: ebdfaaf5c40128b9cc02c0aad4afee4428fca994bf5c859ca0d61ecfeb9c81921e8450cab9ff9b6a91f326c509b9ccc44922d247c7e5298c6d36bd5bacd73342
7
+ data.tar.gz: 1e448f583770a87ee3169173bf23b9dbffdfd27541e66fcbb35b1a0d61bdb5f5340e423f1ab2fbf06f9a886e4c4e9d93b52ca0dd91477ba31955df828cabd92e
data/CHANGELOG.md CHANGED
@@ -7,6 +7,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.21.3] - 2026-04-20
11
+
12
+ ### Fixed
13
+ - Updated stale idea workflow and root quick-start docs so `next` is consistently documented as the root-scope queue alias rather than a physical `_next` folder.
14
+
15
+ ### Technical
16
+ - Tightened `TS-IDEA-001` move-to-next E2E contracts so Goal 3 requires raw update/list artifacts and verifies the `next` transition before the later archive step mutates final state.
17
+
18
+ ## [0.21.2] - 2026-04-18
19
+
20
+ ### Fixed
21
+ - Pinned `TS-IDEA-001` to a sandbox-local `.ace-ideas` root so archive-listing lifecycle coverage no longer depends on ambient project-root discovery.
22
+
23
+ ### Technical
24
+ - Added a CLI regression proving `ace-idea list --in archive` surfaces an idea immediately after `update --move-to archive`.
25
+
26
+ ## [0.21.1] - 2026-04-16
27
+
28
+ ### Fixed
29
+ - Updated `TS-IDEA-001` sandbox setup to source `mise.toml` from `${ACE_E2E_SOURCE_ROOT:-$PROJECT_ROOT_PATH}`, keeping idea lifecycle runs compatible with read-only source-root mounts.
30
+
31
+ ## [0.21.0] - 2026-04-14
32
+
33
+ ### Changed
34
+ - Reworked `TS-IDEA-001` lifecycle runner instructions around public command-output ID flow (`create` output to `update`/archive transitions) instead of hidden frontmatter ID recipes.
35
+
36
+ ### Technical
37
+ - Reduced duplicated list assertions across lifecycle scenario goals and added lightweight scenario freshness metadata (`last-verified` + freshness policy) for E2E maintenance.
38
+
39
+ ## [0.20.2] - 2026-04-13
40
+
41
+ ### Fixed
42
+ - Made create-time collision retries deterministic by reusing one precomputed payload across retries so clipboard capture and LLM enhancement are not rerun with divergent inputs.
43
+
44
+ ### Technical
45
+ - Added manager-level regression coverage to enforce payload reuse semantics across collision retries.
46
+
47
+ ## [0.20.1] - 2026-04-13
48
+
49
+ ### Fixed
50
+ - Added atomic idea-ID reservation during create so concurrent creates fail fast with retryable collision semantics instead of racing through non-atomic prechecks.
51
+
52
+ ### Technical
53
+ - Narrowed idea ID-existence scans to ID-prefixed glob matches and added regression coverage for reservation-lock collision handling.
54
+
55
+ ## [0.20.0] - 2026-04-13
56
+
57
+ ### Fixed
58
+ - Added duplicate persisted idea-ID detection to doctor health checks and made frontmatter-only doctor validation fail when duplicates are present.
59
+ - Added bounded retry handling for `ace-idea create` ID collisions, including retry-exhaustion failures and enforcement that same-ID/different-slug creates are not treated as success.
60
+
61
+ ### Technical
62
+ - Added regression coverage for duplicate-ID doctor failures, create-time retry semantics, and orphan-artifact cleanup guarantees.
63
+
64
+ ## [0.19.3] - 2026-04-13
65
+
66
+ ### Fixed
67
+ - Added bounded retry handling for `ace-idea create` ID collisions so success is emitted only after a unique persisted idea ID exists.
68
+ - Enforced create-time collision semantics where "same ID, different folder slug" is no longer treated as successful creation.
69
+ - Added clear retry-exhaustion failure messaging for create-time ID collisions.
70
+
71
+ ### Technical
72
+ - Updated molecule, command, and feature regression coverage to validate ID-retry semantics and no-orphan cleanup behavior.
73
+
74
+ ## [0.19.2] - 2026-04-13
75
+
76
+ ### Fixed
77
+ - Report duplicate persisted idea IDs as explicit doctor health-check errors with file-path context.
78
+ - Include duplicate ID detection in `ace-idea doctor --check frontmatter` and fail health status when duplicates are present.
79
+
80
+ ### Technical
81
+ - Added organism and CLI regression coverage for duplicate ID failures and frontmatter-only doctor checks.
82
+
83
+ ## [0.19.1] - 2026-04-13
84
+
85
+ ### Changed
86
+ - Completed the batch i05 migration follow-through for this package and aligned it with the restarted `fast` / `feat` / `e2e` verification model.
87
+
88
+ ### Technical
89
+ - Included in the coordinated assignment-driven patch release for batch i05 package updates.
90
+
91
+
92
+ ### Changed
93
+ - Migrated deterministic package coverage to `test/fast` and `test/feat`, retained lifecycle workflow coverage in `test/e2e`, and updated E2E scenario metadata to the restarted `fast`/`feat`/`e2e` contract.
94
+
10
95
  ## [0.18.5] - 2026-03-31
11
96
 
12
97
  ### Changed
data/docs/usage.md CHANGED
@@ -3,8 +3,8 @@ doc-type: user
3
3
  title: ace-idea CLI Usage Reference
4
4
  purpose: Command reference for ace-idea
5
5
  ace-docs:
6
- last-updated: 2026-03-22
7
- last-checked: 2026-03-22
6
+ last-updated: '2026-04-13'
7
+ last-checked: '2026-04-13'
8
8
  ---
9
9
 
10
10
  # ace-idea CLI Usage Reference
@@ -92,6 +92,11 @@ ace-idea status --up-next-limit 5
92
92
 
93
93
  Create a new idea from direct text or clipboard input.
94
94
 
95
+ If the generated idea ID collides with an existing persisted idea, `ace-idea create`
96
+ automatically retries with a new ID. If retries are exhausted, the command fails
97
+ without leaving behind a partial idea artifact. A same-ID create under a different
98
+ folder slug is treated as a collision, not a successful create.
99
+
95
100
  **Syntax:**
96
101
 
97
102
  ```bash
@@ -210,6 +215,9 @@ ace-idea update q7w --move-to archive
210
215
 
211
216
  Run health checks across the idea store.
212
217
 
218
+ Duplicate persisted idea IDs are reported as errors. `ace-idea doctor --check frontmatter`
219
+ also fails when duplicate IDs are present in idea frontmatter.
220
+
213
221
  **Syntax:**
214
222
 
215
223
  ```bash
@@ -317,4 +325,4 @@ ID if needed.
317
325
 
318
326
  **Symptom:** The configured ideas root does not exist yet.
319
327
 
320
- **Solution:** Create an idea first or set `idea.root_dir` in config before running `doctor`.
328
+ **Solution:** Create an idea first or set `idea.root_dir` in config before running `doctor`.
@@ -178,8 +178,8 @@ ace-idea update q7w --move-to archive
178
178
  * **Note**: Clipboard functionality requires `ace-support-mac-clipboard` gem
179
179
 
180
180
  **"Invalid target folder" Error:**
181
- * **Cause**: `--move-to` was given a virtual filter (e.g., `next`) instead of a physical folder
182
- * **Solution**: Use a physical folder: `maybe`, `anytime`, `archive`, or any custom name. Note: `next` is a virtual filter for listing, not a physical folder — omit `--move-to` to place ideas in root
181
+ * **Cause**: `--move-to` was given an unsupported or invalid folder value
182
+ * **Solution**: Use a physical folder such as `maybe`, `anytime`, `archive`, or any custom name. Use `--move-to next` or `--move-to root` to place ideas in the root-scope next queue.
183
183
 
184
184
  **LLM Enhancement Failures:**
185
185
  * **Cause**: API issues, model unavailability, or `--no-llm-enhance` flag
@@ -94,7 +94,7 @@ Systematically organize, prioritize, and align queued ideas with current project
94
94
  ```
95
95
 
96
96
  **How it works:**
97
- * Updates move idea files between folders (`_next`, `_maybe`, `_anytime`)
97
+ * Updates move idea files between physical folders (`_maybe`, `_anytime`) or back to the root-scope `next` queue
98
98
  * Ideas are listed per folder when using `ace-idea list --in <folder>`
99
99
  * The same command can also update metadata such as `status` when needed
100
100
 
@@ -71,14 +71,18 @@ module Ace
71
71
  end
72
72
 
73
73
  manager = Ace::Idea::Organisms::IdeaManager.new
74
- idea = manager.create(
75
- content,
76
- title: title,
77
- tags: tags,
78
- move_to: move_to,
79
- clipboard: clipboard || false,
80
- llm_enhance: llm_enhance || false
81
- )
74
+ idea = begin
75
+ manager.create(
76
+ content,
77
+ title: title,
78
+ tags: tags,
79
+ move_to: move_to,
80
+ clipboard: clipboard || false,
81
+ llm_enhance: llm_enhance || false
82
+ )
83
+ rescue Ace::Idea::Organisms::IdeaManager::CreateRetriesExhaustedError => e
84
+ raise Ace::Support::Cli::Error, e.message
85
+ end
82
86
 
83
87
  folder_info = idea.special_folder ? " (#{idea.special_folder})" : ""
84
88
  puts "Idea created: #{idea.id} #{idea.title}#{folder_info}"
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Clipboard reader adapted from ace-taskflow's ClipboardReader.
4
- # Kept independent (no ace-taskflow dependency) as per task spec:
5
- # "Shared utilities from ace-taskflow are duplicated rather than centralized."
3
+ # Clipboard reader adapted from ace-task's ClipboardReader.
4
+ # Kept independent (no ace-task dependency) as per task spec:
5
+ # "Shared utilities from ace-task are duplicated rather than centralized."
6
6
 
7
7
  begin
8
8
  require "clipboard"
@@ -16,6 +16,8 @@ module Ace
16
16
  # Creates new ideas with b36ts IDs, folder/file creation.
17
17
  # Supports --clipboard, --llm-enhance, and --move-to options.
18
18
  class IdeaCreator
19
+ class IdCollisionError < StandardError; end
20
+
19
21
  # @param root_dir [String] Root directory for ideas
20
22
  # @param config [Hash] Configuration hash
21
23
  def initialize(root_dir:, config: {})
@@ -33,66 +35,102 @@ module Ace
33
35
  # @param time [Time] Creation time (default: now)
34
36
  # @return [Idea] Created idea object
35
37
  def create(content = nil, title: nil, tags: [], move_to: nil,
36
- clipboard: false, llm_enhance: false, time: Time.now.utc)
37
- # Step 1: Gather content
38
+ clipboard: false, llm_enhance: false, time: Time.now.utc, prepared_payload: nil)
39
+ payload = prepared_payload || prepare_create_payload(content, clipboard: clipboard, llm_enhance: llm_enhance)
40
+ enhanced_body = payload.fetch(:enhanced_body)
41
+ attachments_to_save = payload.fetch(:attachments_to_save)
42
+
43
+ # Step 3: Generate ID and slugs
44
+ id = Atoms::IdeaIdFormatter.generate(time)
45
+ with_id_reservation(id) do
46
+ raise IdCollisionError, "Idea ID collision detected for #{id}" if idea_id_exists?(id)
47
+
48
+ slug_title = title || extract_title(enhanced_body)
49
+ folder_slug = generate_folder_slug(slug_title)
50
+ file_slug = generate_file_slug(slug_title)
51
+
52
+ # Step 4: Determine target directory
53
+ target_dir = determine_target_dir(move_to)
54
+ FileUtils.mkdir_p(target_dir)
55
+
56
+ # Step 5: Create idea folder
57
+ folder_name, _ = unique_folder_name(id, folder_slug, target_dir)
58
+ idea_dir = File.join(target_dir, folder_name)
59
+ FileUtils.mkdir_p(idea_dir)
60
+
61
+ begin
62
+ # Step 6: Handle attachments
63
+ if attachments_to_save.any?
64
+ enhanced_body = save_attachments_and_inject_refs(attachments_to_save, idea_dir, enhanced_body)
65
+ end
66
+
67
+ # Step 7: Write spec file
68
+ effective_title = title || extract_title(enhanced_body) || "Untitled Idea"
69
+ frontmatter = Atoms::IdeaFrontmatterDefaults.build(
70
+ id: id,
71
+ title: effective_title,
72
+ tags: tags,
73
+ status: "pending",
74
+ created_at: time
75
+ )
76
+
77
+ file_content = build_file_content(frontmatter, enhanced_body, effective_title)
78
+ spec_filename = Atoms::IdeaFilePattern.spec_filename(id, file_slug)
79
+ spec_file = File.join(idea_dir, spec_filename)
80
+ File.write(spec_file, file_content)
81
+
82
+ # Step 8: Load and return the created idea
83
+ loader = IdeaLoader.new
84
+ special_folder = Ace::Support::Items::Atoms::SpecialFolderDetector.detect_in_path(
85
+ idea_dir, root: @root_dir
86
+ )
87
+ loader.load(idea_dir, id: id, special_folder: special_folder)
88
+ rescue StandardError
89
+ FileUtils.rm_rf(idea_dir) if Dir.exist?(idea_dir)
90
+ raise
91
+ end
92
+ end
93
+ end
94
+
95
+ def prepare_create_payload(content, clipboard: false, llm_enhance: false)
38
96
  body, attachments_to_save = gather_content(content, clipboard: clipboard)
39
97
 
40
98
  if body.nil? || body.strip.empty?
41
99
  raise ArgumentError, "No content provided. Provide text or use --clipboard."
42
100
  end
43
101
 
44
- # Step 2: Optionally enhance with LLM
45
102
  enhanced_body = if llm_enhance
46
103
  enhance_with_llm(body, config: @config)
47
104
  else
48
105
  body
49
106
  end
50
107
 
51
- # Step 3: Generate ID and slugs
52
- id = Atoms::IdeaIdFormatter.generate(time)
53
- slug_title = title || extract_title(enhanced_body)
54
- folder_slug = generate_folder_slug(slug_title)
55
- file_slug = generate_file_slug(slug_title)
56
-
57
- # Step 4: Determine target directory
58
- target_dir = determine_target_dir(move_to)
59
- FileUtils.mkdir_p(target_dir)
60
-
61
- # Step 5: Create idea folder (ensure unique name if ID collision occurs)
62
- folder_name, _ = unique_folder_name(id, folder_slug, target_dir)
63
- idea_dir = File.join(target_dir, folder_name)
64
- FileUtils.mkdir_p(idea_dir)
65
-
66
- # Step 6: Handle attachments
67
- if attachments_to_save.any?
68
- enhanced_body = save_attachments_and_inject_refs(attachments_to_save, idea_dir, enhanced_body)
69
- end
70
-
71
- # Step 7: Write spec file
72
- effective_title = title || extract_title(enhanced_body) || "Untitled Idea"
73
- frontmatter = Atoms::IdeaFrontmatterDefaults.build(
74
- id: id,
75
- title: effective_title,
76
- tags: tags,
77
- status: "pending",
78
- created_at: time
79
- )
80
-
81
- file_content = build_file_content(frontmatter, enhanced_body, effective_title)
82
- spec_filename = Atoms::IdeaFilePattern.spec_filename(id, file_slug)
83
- spec_file = File.join(idea_dir, spec_filename)
84
- File.write(spec_file, file_content)
85
-
86
- # Step 8: Load and return the created idea
87
- loader = IdeaLoader.new
88
- special_folder = Ace::Support::Items::Atoms::SpecialFolderDetector.detect_in_path(
89
- idea_dir, root: @root_dir
90
- )
91
- loader.load(idea_dir, id: id, special_folder: special_folder)
108
+ {
109
+ enhanced_body: enhanced_body,
110
+ attachments_to_save: attachments_to_save
111
+ }
92
112
  end
93
113
 
94
114
  private
95
115
 
116
+ def idea_id_exists?(id)
117
+ Dir.glob(File.join(@root_dir, "**", "#{id}-*")).any? do |path|
118
+ File.directory?(path)
119
+ end
120
+ end
121
+
122
+ def with_id_reservation(id)
123
+ reservation_path = File.join(@root_dir, ".ace-idea-id-lock-#{id}")
124
+ acquired = false
125
+ Dir.mkdir(reservation_path)
126
+ acquired = true
127
+ yield
128
+ rescue Errno::EEXIST
129
+ raise IdCollisionError, "Idea ID collision detected for #{id}"
130
+ ensure
131
+ FileUtils.rm_rf(reservation_path) if acquired && reservation_path && Dir.exist?(reservation_path)
132
+ end
133
+
96
134
  def gather_content(content, clipboard: false)
97
135
  attachments = []
98
136
 
@@ -117,11 +117,18 @@ module Ace
117
117
 
118
118
  scan_results = scanner.scan
119
119
  @stats[:ideas_scanned] = scan_results.size
120
+ id_locations = Hash.new { |hash, key| hash[key] = [] }
120
121
 
121
122
  scan_results.each do |scan_result|
122
123
  spec_file = scan_result.file_path
123
124
  next unless spec_file && File.exist?(spec_file)
124
125
 
126
+ content = File.read(spec_file)
127
+ frontmatter, _body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
128
+ if frontmatter.is_a?(Hash) && frontmatter["id"] && !frontmatter["id"].to_s.strip.empty?
129
+ id_locations[frontmatter["id"]] << spec_file
130
+ end
131
+
125
132
  issues = Molecules::IdeaFrontmatterValidator.validate(
126
133
  spec_file,
127
134
  special_folder: scan_result.special_folder
@@ -134,6 +141,8 @@ module Ace
134
141
  add_issue(issue[:type], issue[:message], issue[:location])
135
142
  end
136
143
  end
144
+
145
+ add_duplicate_id_issues(id_locations)
137
146
  end
138
147
 
139
148
  def run_scope_check
@@ -183,6 +192,18 @@ module Ace
183
192
  count
184
193
  end
185
194
 
195
+ def add_duplicate_id_issues(id_locations)
196
+ id_locations.each do |id, locations|
197
+ next unless locations.size > 1
198
+
199
+ add_issue(
200
+ :error,
201
+ "Duplicate idea ID '#{id}' found in: #{locations.sort.join(', ')}",
202
+ locations.first
203
+ )
204
+ end
205
+ end
206
+
186
207
  def add_issue(type, message, location = nil)
187
208
  issue = {type: type, message: message}
188
209
  issue[:location] = location if location
@@ -15,6 +15,9 @@ module Ace
15
15
  # Orchestrates all idea CRUD operations.
16
16
  # Entry point for idea management with config-driven root directory.
17
17
  class IdeaManager
18
+ CREATE_RETRY_LIMIT = 3
19
+ class CreateRetriesExhaustedError < StandardError; end
20
+
18
21
  attr_reader :last_list_total, :last_folder_counts
19
22
 
20
23
  # @param root_dir [String, nil] Override root directory for ideas
@@ -36,8 +39,30 @@ module Ace
36
39
  clipboard: false, llm_enhance: false)
37
40
  ensure_root_dir
38
41
  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)
42
+ prepared_payload = creator.prepare_create_payload(
43
+ content,
44
+ clipboard: clipboard,
45
+ llm_enhance: llm_enhance
46
+ )
47
+ attempts = 0
48
+
49
+ begin
50
+ attempts += 1
51
+ creator.create(
52
+ nil,
53
+ title: title,
54
+ tags: tags,
55
+ move_to: move_to,
56
+ clipboard: false,
57
+ llm_enhance: false,
58
+ time: Time.now.utc + ((attempts - 1) * 2),
59
+ prepared_payload: prepared_payload
60
+ )
61
+ rescue Molecules::IdeaCreator::IdCollisionError
62
+ retry if attempts < CREATE_RETRY_LIMIT
63
+ raise CreateRetriesExhaustedError,
64
+ "Failed to create idea: unable to generate a unique ID after #{CREATE_RETRY_LIMIT} attempts"
65
+ end
41
66
  end
42
67
 
43
68
  # Create an idea from clipboard
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Idea
5
- VERSION = '0.18.5'
5
+ VERSION = '0.21.3'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-idea
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.5
4
+ version: 0.21.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-04-01 00:00:00.000000000 Z
10
+ date: 2026-04-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-support-core