ace-task 0.38.1 → 0.39.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.
@@ -76,9 +76,9 @@ module Ace
76
76
  end
77
77
 
78
78
  def run_ace_bundle(input, output_path)
79
- _stdout, status = Open3.capture2(
80
- "ace-bundle", input, "--format", "markdown-xml", "--output", output_path
81
- )
79
+ checkout_binstub = File.expand_path("../../../../../bin/ace-bundle", __dir__)
80
+ executable = File.file?(checkout_binstub) ? checkout_binstub : Gem.bin_path("ace-bundle", "ace-bundle")
81
+ _stdout, status = Open3.capture2(executable, input, "--format", "markdown-xml", "--output", output_path)
82
82
  unless status.success?
83
83
  raise Ace::Support::Cli::Error.new("ace-bundle failed for: #{input}")
84
84
  end
@@ -75,6 +75,11 @@ module Ace
75
75
  fm.delete("parent")
76
76
  end
77
77
 
78
+ # Promoted descendants rebase from the standalone ID.
79
+ id_map = {task.id => new_id}
80
+ rewrite_descendants(new_dir, old_parent_id: task.id, new_parent_id: new_id, id_map: id_map)
81
+ rewrite_dependency_references(id_map)
82
+
78
83
  loader = TaskLoader.new
79
84
  loader.load(new_dir, id: new_id)
80
85
  end
@@ -164,10 +169,71 @@ module Ace
164
169
  fm["parent"] = parent_task.id
165
170
  end
166
171
 
172
+ # Descendant identities hang off the parent ID: rewrite their
173
+ # frontmatter and spec filenames so persisted IDs stay authoritative.
174
+ id_map = {task.id => new_id}
175
+ rewrite_descendants(new_dir, old_parent_id: task.id, new_parent_id: new_id, id_map: id_map)
176
+ rewrite_dependency_references(id_map)
177
+
167
178
  loader = TaskLoader.new
168
179
  loader.load(new_dir, id: new_id)
169
180
  end
170
181
 
182
+ # Rewrite dependency references in every task spec after an ID map
183
+ # changes (stale references would read as unmet dependencies).
184
+ def rewrite_dependency_references(id_map)
185
+ return if id_map.empty?
186
+
187
+ Dir.glob(File.join(@root_dir, "**", "*.s.md")).sort.each do |spec|
188
+ content = File.read(spec)
189
+ frontmatter, body = Ace::Support::Items::Atoms::FrontmatterParser.parse(content)
190
+ deps = frontmatter["dependencies"]
191
+ next unless deps.is_a?(Array)
192
+
193
+ mapped = deps.map { |dep| id_map.fetch(dep, dep) }
194
+ next if mapped == deps
195
+
196
+ frontmatter["dependencies"] = mapped
197
+ tmp_path = "#{spec}.tmp.#{Process.pid}"
198
+ File.write(tmp_path, Ace::Support::Items::Atoms::FrontmatterSerializer.rebuild(frontmatter, body))
199
+ File.rename(tmp_path, spec)
200
+ end
201
+ end
202
+
203
+ # Recursively rewrite descendant frontmatter IDs, parent references and
204
+ # spec filenames after their ancestor's ID changed.
205
+ def rewrite_descendants(dir, old_parent_id:, new_parent_id:, id_map: {})
206
+ return unless Dir.exist?(dir)
207
+
208
+ Dir.entries(dir).sort.each do |entry|
209
+ next if entry.start_with?(".")
210
+ next unless (short_match = entry.match(/\A([a-z0-9])-(.+)/))
211
+
212
+ char = short_match[1]
213
+ slug = short_match[2]
214
+ child_dir = File.join(dir, entry)
215
+ next unless File.directory?(child_dir)
216
+
217
+ old_child_id = "#{old_parent_id}.#{char}"
218
+ new_child_id = "#{new_parent_id}.#{char}"
219
+ id_map[old_child_id] = new_child_id
220
+
221
+ spec = Dir.glob(File.join(child_dir, "*.s.md")).sort.first
222
+ if spec
223
+ new_spec = File.join(child_dir, "#{new_child_id}-#{slug}.s.md")
224
+ File.rename(spec, new_spec) if spec != new_spec && !File.exist?(new_spec)
225
+ if File.exist?(new_spec)
226
+ update_frontmatter(new_spec) do |fm|
227
+ fm["id"] = new_child_id
228
+ fm["parent"] = new_parent_id
229
+ end
230
+ end
231
+ end
232
+
233
+ rewrite_descendants(child_dir, old_parent_id: old_child_id, new_parent_id: new_child_id, id_map: id_map)
234
+ end
235
+ end
236
+
171
237
  # Extract the base task ID (without subtask char) from a potentially dotted ID
172
238
  # "8pp.t.q7w.a" => "8pp.t.q7w"
173
239
  def base_task_id(id)
@@ -19,8 +19,9 @@ module Ace
19
19
  # Short reference pattern: "t.q7w" → extract suffix "q7w"
20
20
  SHORT_REF_PATTERN = /^[a-z]\.([0-9a-z]{3})$/
21
21
 
22
- # Subtask reference pattern: "8pp.t.q7w.a" (parent ID + dot + single char)
23
- SUBTASK_REF_PATTERN = /^([0-9a-z]{3}\.[a-z]\.[0-9a-z]{3})\.([a-z0-9])$/
22
+ # Subtask reference pattern: "8pp.t.q7w.a" or nested "8pp.t.q7w.0.a"
23
+ # (parent ID + one or more dot-separated subtask chars)
24
+ SUBTASK_REF_PATTERN = /^([0-9a-z]{3}\.[a-z]\.[0-9a-z]{3}(?:\.[a-z0-9])*)\.([a-z0-9])$/
24
25
 
25
26
  # Short subtask reference: "q7w.a" or "t.q7w.a" (suffix + subtask char)
26
27
  SHORT_SUBTASK_REF_PATTERN = /^(?:[a-z]\.)?([0-9a-z]{3})\.([a-z0-9])$/
@@ -76,7 +77,17 @@ module Ace
76
77
  # Resolve a subtask reference by finding the parent's scan result,
77
78
  # then looking for the subtask folder within that parent's directory.
78
79
  def resolve_subtask(parent_id, subtask_char)
79
- parent_result = @scan_results.find { |sr| sr.id == parent_id }
80
+ parent_result = if parent_id.count(".") >= 3
81
+ # Nested parent: resolve level by level from the base task.
82
+ base_id = parent_id.split(".")[0, 3].join(".")
83
+ nested_chars = parent_id.split(".")[3..]
84
+ current = resolve_subtask_chain(base_id, nested_chars)
85
+ return nil unless current
86
+
87
+ current
88
+ else
89
+ @scan_results.find { |sr| sr.id == parent_id }
90
+ end
80
91
  return nil unless parent_result
81
92
 
82
93
  subtask_id = "#{parent_id}.#{subtask_char}"
@@ -109,6 +120,23 @@ module Ace
109
120
 
110
121
  nil
111
122
  end
123
+
124
+ # Walk nested subtask chars from the base task, returning the final
125
+ # scan result (ids are rebuilt per level exactly like the folders).
126
+ def resolve_subtask_chain(base_id, nested_chars)
127
+ current = @scan_results.find { |sr| sr.id == base_id }
128
+ return nil unless current
129
+
130
+ current_id = base_id
131
+ nested_chars.each do |char|
132
+ next_level = resolve_subtask(current_id, char)
133
+ return nil unless next_level
134
+
135
+ current = next_level
136
+ current_id = next_level.id
137
+ end
138
+ current
139
+ end
112
140
  end
113
141
  end
114
142
  end