aidp 0.44.0 → 0.45.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 +4 -4
- data/lib/aidp/config.rb +13 -1
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/gantt_synchronizer.rb +273 -0
- data/lib/aidp/watch/plan_processor.rb +22 -0
- data/lib/aidp/watch/prd_parser.rb +395 -0
- data/lib/aidp/watch/projects_processor.rb +88 -11
- data/lib/aidp/watch/repository_client.rb +18 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a86b02df6fbe151abffa67c335c73fc3c724de523065f1f03b083bca305c1c6c
|
|
4
|
+
data.tar.gz: 15cf32cc8d3007f91e7a94a777e1b32d1a10002a63f91f10721bcf7ffe5c3d04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc49aca1c586e8603be81e229ad3ce42663a5d9592995cc593ed7929c48339542cb67d8b9c3056b429d8409b016c2ddcc4619938f9905666d23a3f0ced1cf006
|
|
7
|
+
data.tar.gz: 1dac0b372011909722cc0d139bee899b4b968d3041fba11114f7314a9ce5b5a7b8037799b3484e1c03f8587312aae507bad44469c29bbb9b102c6035ad45dea0
|
data/lib/aidp/config.rb
CHANGED
|
@@ -215,10 +215,17 @@ module Aidp
|
|
|
215
215
|
priority: "Priority",
|
|
216
216
|
skills: "Skills",
|
|
217
217
|
personas: "Personas",
|
|
218
|
-
blocking: "Blocking"
|
|
218
|
+
blocking: "Blocking",
|
|
219
|
+
start_date: "Start Date",
|
|
220
|
+
target_date: "Target Date",
|
|
221
|
+
dependencies: "Dependencies",
|
|
222
|
+
critical_path: "Critical Path"
|
|
219
223
|
},
|
|
220
224
|
auto_create_fields: true,
|
|
221
225
|
sync_interval: 60,
|
|
226
|
+
prd_path: nil,
|
|
227
|
+
auto_sync_gantt: false,
|
|
228
|
+
gantt_format: "auto",
|
|
222
229
|
default_status_values: ["Backlog", "Todo", "In Progress", "In Review", "Done"],
|
|
223
230
|
default_priority_values: ["Low", "Medium", "High", "Critical"]
|
|
224
231
|
},
|
|
@@ -512,6 +519,11 @@ module Aidp
|
|
|
512
519
|
merged[:security] = deep_merge_hash(merged[:security], symbolize_keys(security_section))
|
|
513
520
|
end
|
|
514
521
|
|
|
522
|
+
if config[:watch] || config["watch"]
|
|
523
|
+
watch_section = config[:watch] || config["watch"]
|
|
524
|
+
merged[:watch] = deep_merge_hash(merged[:watch], symbolize_keys(watch_section))
|
|
525
|
+
end
|
|
526
|
+
|
|
515
527
|
merged
|
|
516
528
|
end
|
|
517
529
|
|
data/lib/aidp/version.rb
CHANGED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Watch
|
|
7
|
+
# Synchronizes parsed Gantt task data with GitHub Projects and Mermaid docs.
|
|
8
|
+
class GanttSynchronizer
|
|
9
|
+
CRITICAL_PATH_VALUES = %w[Yes No].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(repository_client:, state_store:, project_id:, field_mappings:, auto_create_fields: true, parser_class: PrdParser)
|
|
12
|
+
@repository_client = repository_client
|
|
13
|
+
@state_store = state_store
|
|
14
|
+
@project_id = project_id
|
|
15
|
+
@field_mappings = field_mappings
|
|
16
|
+
@auto_create_fields = auto_create_fields
|
|
17
|
+
@parser_class = parser_class
|
|
18
|
+
@project_fields_cache = nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def sync_from_prd(prd_path:, format: nil, issue_numbers_by_title: {})
|
|
22
|
+
parsed = @parser_class.new(file_path: prd_path, format: format).parse
|
|
23
|
+
tasks = resolve_issue_numbers(parsed[:tasks], issue_numbers_by_title)
|
|
24
|
+
critical_path = calculate_critical_path(tasks)
|
|
25
|
+
tasks_by_id = tasks.each_with_object({}) { |task, memo| memo[task[:id]] = task }
|
|
26
|
+
|
|
27
|
+
synced = 0
|
|
28
|
+
skipped = 0
|
|
29
|
+
|
|
30
|
+
tasks.each do |task|
|
|
31
|
+
if task[:issue_number].nil?
|
|
32
|
+
skipped += 1
|
|
33
|
+
next
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
item_id = ensure_project_item(task[:issue_number])
|
|
37
|
+
update_date_fields(item_id, task)
|
|
38
|
+
update_dependencies_field(item_id, task, tasks_by_id)
|
|
39
|
+
update_critical_path_field(item_id, critical_path.include?(task[:id]))
|
|
40
|
+
|
|
41
|
+
@state_store.record_project_sync(task[:issue_number], {
|
|
42
|
+
gantt_task_id: task[:id],
|
|
43
|
+
gantt_path: prd_path,
|
|
44
|
+
gantt_format: parsed[:format].to_s,
|
|
45
|
+
critical_path: critical_path.include?(task[:id])
|
|
46
|
+
})
|
|
47
|
+
synced += 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
{synced: synced, skipped: skipped, critical_path: critical_path}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sync_issue_status_to_gantt(prd_path:, issue_number:, status:, format: nil)
|
|
54
|
+
parser = @parser_class.new(file_path: prd_path, format: format)
|
|
55
|
+
parsed = parser.parse
|
|
56
|
+
return false unless parsed[:format] == :mermaid
|
|
57
|
+
|
|
58
|
+
content = File.read(prd_path)
|
|
59
|
+
lines = content.lines
|
|
60
|
+
line_index = mermaid_task_line_index(content, parsed[:tasks], issue_number)
|
|
61
|
+
return false unless line_index
|
|
62
|
+
|
|
63
|
+
lines[line_index] = rewrite_mermaid_status(lines[line_index], status)
|
|
64
|
+
File.write(prd_path, lines.join)
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def resolve_issue_numbers(tasks, issue_numbers_by_title)
|
|
71
|
+
tasks.map do |task|
|
|
72
|
+
next task if task[:issue_number]
|
|
73
|
+
|
|
74
|
+
resolved_issue_number = issue_numbers_by_title[normalize_title(task[:name])]
|
|
75
|
+
resolved_issue_number ? task.merge(issue_number: resolved_issue_number) : task
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def calculate_critical_path(tasks)
|
|
80
|
+
explicit = tasks.select { |task| task[:critical] }.map { |task| task[:id] }
|
|
81
|
+
return explicit if explicit.any?
|
|
82
|
+
|
|
83
|
+
durations = tasks.each_with_object({}) { |task, memo| memo[task[:id]] = task[:duration_days].to_i }
|
|
84
|
+
predecessors = tasks.each_with_object({}) { |task, memo| memo[task[:id]] = task[:dependency_ids] }
|
|
85
|
+
longest = {}
|
|
86
|
+
previous = {}
|
|
87
|
+
|
|
88
|
+
ordered_ids(tasks, predecessors).each do |task_id|
|
|
89
|
+
best_predecessor = predecessors[task_id].max_by { |candidate| longest[candidate].to_i }
|
|
90
|
+
longest[task_id] = durations[task_id] + longest[best_predecessor].to_i
|
|
91
|
+
previous[task_id] = best_predecessor
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
endpoint = longest.max_by { |_task_id, distance| distance }&.first
|
|
95
|
+
unwind_path(endpoint, previous)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def ordered_ids(tasks, predecessors)
|
|
99
|
+
task_ids = tasks.map { |task| task[:id] }
|
|
100
|
+
ordered = []
|
|
101
|
+
remaining = task_ids.dup
|
|
102
|
+
|
|
103
|
+
until remaining.empty?
|
|
104
|
+
progressed = false
|
|
105
|
+
remaining.dup.each do |task_id|
|
|
106
|
+
unmet = predecessors[task_id].reject { |dependency| ordered.include?(dependency) }
|
|
107
|
+
next if unmet.any?
|
|
108
|
+
|
|
109
|
+
ordered << task_id
|
|
110
|
+
remaining.delete(task_id)
|
|
111
|
+
progressed = true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
break unless progressed
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
ordered + remaining
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def unwind_path(endpoint, previous)
|
|
121
|
+
path = []
|
|
122
|
+
current = endpoint
|
|
123
|
+
|
|
124
|
+
while current
|
|
125
|
+
path.unshift(current)
|
|
126
|
+
current = previous[current]
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
path
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def ensure_project_item(issue_number)
|
|
133
|
+
item_id = @state_store.project_item_id(issue_number)
|
|
134
|
+
return item_id if item_id
|
|
135
|
+
|
|
136
|
+
item_id = @repository_client.link_issue_to_project(@project_id, issue_number)
|
|
137
|
+
@state_store.record_project_item_id(issue_number, item_id)
|
|
138
|
+
item_id
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def update_date_fields(item_id, task)
|
|
142
|
+
update_date_field(item_id, @field_mappings[:start_date], task[:start_date])
|
|
143
|
+
update_date_field(item_id, @field_mappings[:target_date], task[:end_date] || task[:start_date])
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def update_date_field(item_id, field_name, date)
|
|
147
|
+
return unless field_name && date
|
|
148
|
+
|
|
149
|
+
field = find_or_create_field(field_name, "DATE")
|
|
150
|
+
return unless field
|
|
151
|
+
|
|
152
|
+
@repository_client.update_project_item_field(
|
|
153
|
+
item_id,
|
|
154
|
+
field[:id],
|
|
155
|
+
{project_id: @project_id, date: date.iso8601}
|
|
156
|
+
)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def update_dependencies_field(item_id, task, tasks_by_id)
|
|
160
|
+
field_name = @field_mappings[:dependencies]
|
|
161
|
+
return unless field_name
|
|
162
|
+
|
|
163
|
+
dependency_issue_numbers = task[:dependency_ids].filter_map do |dependency_id|
|
|
164
|
+
tasks_by_id[dependency_id]&.dig(:issue_number)
|
|
165
|
+
end
|
|
166
|
+
field = find_or_create_field(field_name, "TEXT")
|
|
167
|
+
return unless field
|
|
168
|
+
|
|
169
|
+
text = dependency_issue_numbers.map { |issue_number| "##{issue_number}" }.join(", ")
|
|
170
|
+
@repository_client.update_project_item_field(
|
|
171
|
+
item_id,
|
|
172
|
+
field[:id],
|
|
173
|
+
{project_id: @project_id, text: text}
|
|
174
|
+
)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def update_critical_path_field(item_id, critical)
|
|
178
|
+
field_name = @field_mappings[:critical_path]
|
|
179
|
+
return unless field_name
|
|
180
|
+
|
|
181
|
+
field = find_or_create_field(field_name, "SINGLE_SELECT", CRITICAL_PATH_VALUES)
|
|
182
|
+
return unless field
|
|
183
|
+
|
|
184
|
+
option_id = field[:options]&.find { |option| option[:name].casecmp?(critical ? "Yes" : "No") }&.dig(:id)
|
|
185
|
+
return unless option_id
|
|
186
|
+
|
|
187
|
+
@repository_client.update_project_item_field(
|
|
188
|
+
item_id,
|
|
189
|
+
field[:id],
|
|
190
|
+
{project_id: @project_id, option_id: option_id}
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def rewrite_mermaid_status(line, status)
|
|
195
|
+
match = line.match(/\A(?<name_part>.*?)(?<separator>\s*:\s*)(?<definition>.*?)(?<newline>\r?\n?)\z/)
|
|
196
|
+
return line unless match
|
|
197
|
+
|
|
198
|
+
mapped_status = mermaid_status_for(status)
|
|
199
|
+
|
|
200
|
+
tokens = match[:definition].split(",").map(&:strip).reject(&:empty?)
|
|
201
|
+
tokens.reject! { |token| %w[done active].include?(token) }
|
|
202
|
+
|
|
203
|
+
tokens.unshift(mapped_status) if mapped_status
|
|
204
|
+
|
|
205
|
+
"#{match[:name_part]}#{match[:separator]}#{tokens.join(", ")}#{match[:newline]}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def mermaid_status_for(status)
|
|
209
|
+
case status.to_s.downcase
|
|
210
|
+
when "done", "closed", "completed" then "done"
|
|
211
|
+
when "in progress", "in_progress", "active" then "active"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def mermaid_task_line_index(content, tasks, issue_number)
|
|
216
|
+
line_number = find_mermaid_task(tasks, issue_number)&.dig(:line_number)
|
|
217
|
+
return unless line_number
|
|
218
|
+
|
|
219
|
+
line_number + mermaid_chart_line_offset(content) - 1
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def mermaid_chart_line_offset(content)
|
|
223
|
+
lines = content.lines
|
|
224
|
+
mermaid_start = lines.each_index.find do |index|
|
|
225
|
+
mermaid_gantt_block?(lines, index)
|
|
226
|
+
end
|
|
227
|
+
mermaid_start ? mermaid_start + 1 : 0
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def find_mermaid_task(tasks, issue_number)
|
|
231
|
+
sync_data = @state_store.project_sync_data(issue_number)
|
|
232
|
+
gantt_task_id = sync_data["gantt_task_id"]
|
|
233
|
+
|
|
234
|
+
tasks.find { |task| task[:id] == gantt_task_id } ||
|
|
235
|
+
tasks.find { |task| task[:issue_number] == issue_number }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def mermaid_gantt_block?(lines, start_index)
|
|
239
|
+
return false unless lines[start_index].match?(/\A```mermaid\b/)
|
|
240
|
+
|
|
241
|
+
block_lines = lines[(start_index + 1)..]
|
|
242
|
+
return false unless block_lines
|
|
243
|
+
|
|
244
|
+
block_lines.take_while { |line| !line.match?(/\A\s*```\s*$/) }.any? do |line|
|
|
245
|
+
line.strip == "gantt"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def normalize_title(title)
|
|
250
|
+
title.to_s.gsub(/\s*\(#\d+\)\s*/, " ").strip.downcase
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def project_fields
|
|
254
|
+
@project_fields_cache ||= @repository_client.fetch_project_fields(@project_id)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def invalidate_fields_cache
|
|
258
|
+
@project_fields_cache = nil
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def find_or_create_field(name, field_type, options = nil)
|
|
262
|
+
field = project_fields.find { |entry| entry[:name].casecmp?(name) }
|
|
263
|
+
return field if field
|
|
264
|
+
return nil unless @auto_create_fields
|
|
265
|
+
|
|
266
|
+
formatted_options = options&.map { |option| {name: option} } if field_type == "SINGLE_SELECT"
|
|
267
|
+
field = @repository_client.create_project_field(@project_id, name, field_type, options: formatted_options)
|
|
268
|
+
invalidate_fields_cache
|
|
269
|
+
field
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
@@ -263,6 +263,10 @@ module Aidp
|
|
|
263
263
|
return record_project_setup_failure(issue[:number], "unable to create project sub-issues")
|
|
264
264
|
end
|
|
265
265
|
|
|
266
|
+
if gantt_sync_enabled? && !sync_project_gantt_data(projects_processor, issue[:number], created_issues)
|
|
267
|
+
return record_project_setup_failure(issue[:number], "unable to sync GitHub Project data from the PRD Gantt chart")
|
|
268
|
+
end
|
|
269
|
+
|
|
266
270
|
unless sync_project_issue_statuses(issue[:number], created_issues, projects_processor)
|
|
267
271
|
return record_project_setup_failure(issue[:number], "unable to sync project issues to the GitHub Project")
|
|
268
272
|
end
|
|
@@ -320,6 +324,24 @@ module Aidp
|
|
|
320
324
|
true
|
|
321
325
|
end
|
|
322
326
|
|
|
327
|
+
def sync_project_gantt_data(projects_processor, issue_number, created_issues)
|
|
328
|
+
projects_processor.sync_from_gantt(issue_numbers_by_title: issue_numbers_by_title(created_issues))
|
|
329
|
+
true
|
|
330
|
+
rescue => e
|
|
331
|
+
Aidp.log_error("plan_processor", "project_gantt_sync_failed",
|
|
332
|
+
issue: issue_number, error: e.message)
|
|
333
|
+
false
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def prd_path_configured?
|
|
337
|
+
value = @project_config[:prd_path] || @project_config["prd_path"]
|
|
338
|
+
!value.to_s.empty?
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def gantt_sync_enabled?
|
|
342
|
+
(@project_config[:auto_sync_gantt] || @project_config["auto_sync_gantt"]) == true && prd_path_configured?
|
|
343
|
+
end
|
|
344
|
+
|
|
323
345
|
def existing_sub_issues_for(parent_issue, sub_issues:, creator:)
|
|
324
346
|
parent_number = parent_issue[:number]
|
|
325
347
|
existing_numbers = @state_store.sub_issues(parent_number)
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
require "date"
|
|
5
|
+
require "rexml/document"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module Watch
|
|
9
|
+
# Parses Gantt-oriented planning documents into a normalized task graph.
|
|
10
|
+
class PrdParser
|
|
11
|
+
class ParseError < StandardError; end
|
|
12
|
+
|
|
13
|
+
SUPPORTED_FORMATS = %i[mermaid ms_project_xml csv].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(file_path:, format: nil)
|
|
16
|
+
@file_path = file_path
|
|
17
|
+
@format = normalize_format(format) || detect_format
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse
|
|
21
|
+
raise ParseError, "File not found: #{@file_path}" unless File.exist?(@file_path)
|
|
22
|
+
|
|
23
|
+
tasks = case @format
|
|
24
|
+
when :mermaid then parse_mermaid(File.read(@file_path))
|
|
25
|
+
when :ms_project_xml then parse_ms_project_xml(File.read(@file_path))
|
|
26
|
+
when :csv then parse_csv
|
|
27
|
+
else
|
|
28
|
+
raise ParseError, "Unsupported format: #{@format}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
normalized_tasks = normalize_tasks(tasks)
|
|
32
|
+
|
|
33
|
+
{
|
|
34
|
+
format: @format,
|
|
35
|
+
source_file: @file_path,
|
|
36
|
+
tasks: normalized_tasks,
|
|
37
|
+
metadata: build_metadata(normalized_tasks)
|
|
38
|
+
}
|
|
39
|
+
rescue REXML::ParseException => e
|
|
40
|
+
raise ParseError, "Invalid XML: #{e.message}"
|
|
41
|
+
rescue CSV::MalformedCSVError => e
|
|
42
|
+
raise ParseError, "Invalid CSV: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def normalize_format(format)
|
|
48
|
+
return nil if format.nil?
|
|
49
|
+
|
|
50
|
+
normalized = format.to_s.strip.downcase.tr("-", "_")
|
|
51
|
+
{
|
|
52
|
+
"auto" => nil,
|
|
53
|
+
"mermaid" => :mermaid,
|
|
54
|
+
"ms_project_xml" => :ms_project_xml,
|
|
55
|
+
"msp_xml" => :ms_project_xml,
|
|
56
|
+
"xml" => :ms_project_xml,
|
|
57
|
+
"csv" => :csv
|
|
58
|
+
}.fetch(normalized) { raise ParseError, "Unknown format override: #{format}" }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def detect_format
|
|
62
|
+
ext = File.extname(@file_path).downcase
|
|
63
|
+
content = File.read(@file_path)
|
|
64
|
+
|
|
65
|
+
return :csv if ext == ".csv"
|
|
66
|
+
return :ms_project_xml if ext == ".xml"
|
|
67
|
+
return :mermaid if mermaid_gantt_format?(ext, content)
|
|
68
|
+
|
|
69
|
+
raise ParseError, "Unable to detect Gantt format for #{@file_path}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def mermaid_gantt_format?(ext, content)
|
|
73
|
+
return false unless [".md", ".markdown", ".mmd"].include?(ext) || content.match?(/^\s*gantt\s*$/)
|
|
74
|
+
|
|
75
|
+
content.match?(/```mermaid\b.*?^\s*gantt\s*$/m) || content.match?(/^\s*gantt\s*$/)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_mermaid(content)
|
|
79
|
+
chart = extract_mermaid_chart(content)
|
|
80
|
+
lines = chart.each_line.map(&:rstrip)
|
|
81
|
+
chart_start_date = extract_mermaid_start_date(lines)
|
|
82
|
+
section = nil
|
|
83
|
+
tasks = []
|
|
84
|
+
|
|
85
|
+
lines.each_with_index do |line, index|
|
|
86
|
+
stripped = line.strip
|
|
87
|
+
next if stripped.empty? || stripped == "gantt" || stripped.start_with?("%%", "title ", "dateFormat ", "axisFormat ", "todayMarker ")
|
|
88
|
+
|
|
89
|
+
if stripped.start_with?("section ")
|
|
90
|
+
section = stripped.delete_prefix("section ").strip
|
|
91
|
+
next
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
next unless stripped.include?(":")
|
|
95
|
+
|
|
96
|
+
name_part, definition = stripped.split(":", 2)
|
|
97
|
+
tasks << build_mermaid_task(name_part, definition, section, index + 1, chart_start_date)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
tasks
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def extract_mermaid_chart(content)
|
|
104
|
+
mermaid_blocks(content).each do |block|
|
|
105
|
+
return block if block.each_line.any? { |line| line.strip == "gantt" }
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
content
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def mermaid_blocks(content)
|
|
112
|
+
content.scan(/```mermaid\s*\n(?<chart>.*?^\s*```)/m).flatten.map do |chart|
|
|
113
|
+
chart.sub(/\n?\s*```\s*\z/m, "")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def build_mermaid_task(name_part, definition, section, line_number, chart_start_date)
|
|
118
|
+
tokens = definition.split(",").map(&:strip).reject(&:empty?)
|
|
119
|
+
dependency_ids = []
|
|
120
|
+
milestone = false
|
|
121
|
+
critical = false
|
|
122
|
+
status = nil
|
|
123
|
+
task_id = nil
|
|
124
|
+
explicit_dates = []
|
|
125
|
+
duration_days = nil
|
|
126
|
+
|
|
127
|
+
tokens.each do |token|
|
|
128
|
+
case token
|
|
129
|
+
when "crit"
|
|
130
|
+
critical = true
|
|
131
|
+
when "milestone", "milestone?"
|
|
132
|
+
milestone = true
|
|
133
|
+
when "done", "active"
|
|
134
|
+
status = token
|
|
135
|
+
when /\Aafter\s+(.+)\z/i
|
|
136
|
+
dependency_ids.concat(Regexp.last_match(1).split)
|
|
137
|
+
when /\A\d{4}-\d{2}-\d{2}\z/
|
|
138
|
+
explicit_dates << Date.parse(token)
|
|
139
|
+
when /\A(\d+)d\z/i
|
|
140
|
+
duration_days = Regexp.last_match(1).to_i
|
|
141
|
+
else
|
|
142
|
+
task_id ||= token
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
start_date = explicit_dates.first
|
|
147
|
+
end_date = explicit_dates[1]
|
|
148
|
+
duration_days ||= 0 if milestone
|
|
149
|
+
duration_days ||= 1 unless end_date
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
id: task_id || slugify(name_part),
|
|
153
|
+
name: name_part.strip,
|
|
154
|
+
section: section,
|
|
155
|
+
start_date: start_date,
|
|
156
|
+
end_date: end_date || ((start_date && duration_days) ? start_date + duration_days - 1 : nil),
|
|
157
|
+
duration_days: duration_days,
|
|
158
|
+
dependency_ids: dependency_ids,
|
|
159
|
+
milestone: milestone,
|
|
160
|
+
critical: critical,
|
|
161
|
+
status: status,
|
|
162
|
+
issue_number: extract_issue_number("#{name_part} #{task_id}"),
|
|
163
|
+
line_number: line_number,
|
|
164
|
+
chart_start_date: chart_start_date
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def parse_ms_project_xml(content)
|
|
169
|
+
document = REXML::Document.new(content)
|
|
170
|
+
uid_to_task = {}
|
|
171
|
+
|
|
172
|
+
REXML::XPath.each(document, "//Task") do |node|
|
|
173
|
+
name = text_at(node, "Name")
|
|
174
|
+
next if name.to_s.strip.empty?
|
|
175
|
+
next if text_at(node, "Null") == "1"
|
|
176
|
+
|
|
177
|
+
uid = text_at(node, "UID")
|
|
178
|
+
task = {
|
|
179
|
+
id: uid.to_s.empty? ? slugify(name) : uid.to_s,
|
|
180
|
+
name: name.strip,
|
|
181
|
+
section: nil,
|
|
182
|
+
start_date: parse_date(text_at(node, "Start")),
|
|
183
|
+
end_date: parse_date(text_at(node, "Finish")),
|
|
184
|
+
duration_days: parse_duration(text_at(node, "Duration")),
|
|
185
|
+
dependency_ids: [],
|
|
186
|
+
milestone: text_at(node, "Milestone") == "1",
|
|
187
|
+
critical: text_at(node, "Critical") == "1",
|
|
188
|
+
status: nil,
|
|
189
|
+
issue_number: extract_issue_number(name)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
REXML::XPath.each(node, "PredecessorLink") do |link|
|
|
193
|
+
predecessor_uid = text_at(link, "PredecessorUID")
|
|
194
|
+
task[:dependency_ids] << predecessor_uid if predecessor_uid
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
uid_to_task[uid] = task
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
uid_to_task.values
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def parse_csv
|
|
204
|
+
rows = CSV.read(@file_path, headers: true)
|
|
205
|
+
rows.map do |row|
|
|
206
|
+
name = first_value(row, "name", "task", "title", "summary")
|
|
207
|
+
id = first_value(row, "id", "task_id", "uid") || slugify(name)
|
|
208
|
+
dependencies = first_value(row, "dependencies", "depends_on", "predecessors", "dependency_ids")
|
|
209
|
+
|
|
210
|
+
{
|
|
211
|
+
id: id.to_s,
|
|
212
|
+
name: name.to_s.strip,
|
|
213
|
+
section: first_value(row, "section", "phase", "group"),
|
|
214
|
+
start_date: parse_date(first_value(row, "start_date", "start")),
|
|
215
|
+
end_date: parse_date(first_value(row, "end_date", "finish", "due_date")),
|
|
216
|
+
duration_days: parse_numeric(first_value(row, "duration_days", "duration", "days")),
|
|
217
|
+
dependency_ids: split_dependencies(dependencies),
|
|
218
|
+
milestone: truthy?(first_value(row, "milestone", "is_milestone")),
|
|
219
|
+
critical: truthy?(first_value(row, "critical", "critical_path")),
|
|
220
|
+
status: first_value(row, "status", "state"),
|
|
221
|
+
issue_number: parse_numeric(first_value(row, "issue_number")) || extract_issue_number("#{name} #{id}")
|
|
222
|
+
}
|
|
223
|
+
end.reject { |task| task[:name].empty? }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def normalize_tasks(tasks)
|
|
227
|
+
task_ids = tasks.each_with_object({}) { |task, memo| memo[task[:id].to_s] = task[:id].to_s }
|
|
228
|
+
|
|
229
|
+
normalized_tasks = tasks.map do |task|
|
|
230
|
+
dependency_ids = Array(task[:dependency_ids]).filter_map do |dependency|
|
|
231
|
+
resolve_dependency_id(dependency, tasks, task_ids)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
duration_days = task[:duration_days]
|
|
235
|
+
duration_days = calculate_duration(task[:start_date], task[:end_date]) if duration_days.nil?
|
|
236
|
+
duration_days ||= task[:milestone] ? 0 : 1
|
|
237
|
+
|
|
238
|
+
task.merge(
|
|
239
|
+
id: task[:id].to_s,
|
|
240
|
+
dependency_ids: dependency_ids.uniq,
|
|
241
|
+
duration_days: duration_days,
|
|
242
|
+
end_date: task[:end_date] || infer_end_date(task[:start_date], duration_days)
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
infer_dependency_dates(infer_chart_order_dates(normalized_tasks))
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def infer_chart_order_dates(tasks)
|
|
250
|
+
root_tasks = tasks.select { |task| task[:dependency_ids].empty? }
|
|
251
|
+
return tasks if root_tasks.empty?
|
|
252
|
+
|
|
253
|
+
cursor = tasks.filter_map { |task| task[:chart_start_date] }.min || Date.today
|
|
254
|
+
root_tasks_by_id = root_tasks.each_with_object({}) do |task, memo|
|
|
255
|
+
start_date = task[:start_date] || cursor
|
|
256
|
+
end_date = task[:end_date] || infer_end_date(start_date, task[:duration_days])
|
|
257
|
+
memo[task[:id]] = task.merge(start_date: start_date, end_date: end_date, chart_start_date: nil)
|
|
258
|
+
next unless end_date
|
|
259
|
+
|
|
260
|
+
next_cursor = end_date + 1
|
|
261
|
+
cursor = [cursor, next_cursor].max
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
tasks.map do |task|
|
|
265
|
+
root_tasks_by_id.fetch(task[:id]) { task.merge(chart_start_date: nil) }
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def resolve_dependency_id(dependency, tasks, task_ids)
|
|
270
|
+
key = dependency.to_s.strip
|
|
271
|
+
return if key.empty?
|
|
272
|
+
return task_ids[key] if task_ids[key]
|
|
273
|
+
|
|
274
|
+
task = tasks.find { |candidate| candidate[:name].casecmp?(key) }
|
|
275
|
+
task&.dig(:id)&.to_s
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def build_metadata(tasks)
|
|
279
|
+
{
|
|
280
|
+
task_count: tasks.size,
|
|
281
|
+
milestone_count: tasks.count { |task| task[:milestone] },
|
|
282
|
+
dependency_count: tasks.sum { |task| task[:dependency_ids].size },
|
|
283
|
+
issues_mapped: tasks.count { |task| task[:issue_number] }
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def calculate_duration(start_date, end_date)
|
|
288
|
+
return unless start_date && end_date
|
|
289
|
+
|
|
290
|
+
[(end_date - start_date).to_i + 1, 0].max
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def infer_end_date(start_date, duration_days)
|
|
294
|
+
return unless start_date && duration_days
|
|
295
|
+
return start_date if duration_days.zero?
|
|
296
|
+
|
|
297
|
+
start_date + duration_days - 1
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def infer_dependency_dates(tasks)
|
|
301
|
+
tasks_by_id = tasks.each_with_object({}) { |task, memo| memo[task[:id]] = task }
|
|
302
|
+
|
|
303
|
+
tasks.map do |task|
|
|
304
|
+
resolved_task = resolve_dependency_dates(task, tasks_by_id, {})
|
|
305
|
+
tasks_by_id[task[:id]] = resolved_task
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def resolve_dependency_dates(task, tasks_by_id, visiting)
|
|
310
|
+
return task if task[:start_date] && task[:end_date]
|
|
311
|
+
return task if task[:dependency_ids].empty?
|
|
312
|
+
return task if visiting[task[:id]]
|
|
313
|
+
|
|
314
|
+
visiting[task[:id]] = true
|
|
315
|
+
dependency_end_dates = task[:dependency_ids].filter_map do |dependency_id|
|
|
316
|
+
dependency = tasks_by_id[dependency_id]
|
|
317
|
+
next unless dependency
|
|
318
|
+
|
|
319
|
+
resolved_dependency = resolve_dependency_dates(dependency, tasks_by_id, visiting)
|
|
320
|
+
tasks_by_id[dependency_id] = resolved_dependency
|
|
321
|
+
resolved_dependency[:end_date]
|
|
322
|
+
end
|
|
323
|
+
visiting.delete(task[:id])
|
|
324
|
+
|
|
325
|
+
return task if dependency_end_dates.empty?
|
|
326
|
+
|
|
327
|
+
start_date = task[:start_date] || dependency_end_dates.max + 1
|
|
328
|
+
task.merge(
|
|
329
|
+
start_date: start_date,
|
|
330
|
+
end_date: task[:end_date] || infer_end_date(start_date, task[:duration_days])
|
|
331
|
+
)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def text_at(node, path)
|
|
335
|
+
child = node.elements[path]
|
|
336
|
+
child&.text
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def parse_date(value)
|
|
340
|
+
return if value.to_s.strip.empty?
|
|
341
|
+
|
|
342
|
+
Date.parse(value.to_s)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
def extract_mermaid_start_date(lines)
|
|
346
|
+
value = lines.filter_map do |line|
|
|
347
|
+
line.strip.match(/\A%%\s*start_date:\s*(\d{4}-\d{2}-\d{2})\s*\z/i)&.captures&.first
|
|
348
|
+
end.first
|
|
349
|
+
parse_date(value)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def parse_duration(value)
|
|
353
|
+
return if value.to_s.strip.empty?
|
|
354
|
+
|
|
355
|
+
return value.to_i if value.to_s.match?(/\A\d+\z/)
|
|
356
|
+
|
|
357
|
+
hours = value.to_s.scan(/(\d+)H/i).flatten.first.to_i
|
|
358
|
+
minutes = value.to_s.scan(/(\d+)M/i).flatten.first.to_i
|
|
359
|
+
days = (hours / 8.0) + (minutes.positive? ? 1.0 / 8 : 0)
|
|
360
|
+
days.ceil
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def parse_numeric(value)
|
|
364
|
+
return if value.to_s.strip.empty?
|
|
365
|
+
|
|
366
|
+
value.to_s[/\d+/]&.to_i
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
def first_value(row, *keys)
|
|
370
|
+
keys.each do |key|
|
|
371
|
+
value = row[key] || row[key.to_s] || row[key.to_sym]
|
|
372
|
+
return value unless value.nil?
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
nil
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def split_dependencies(value)
|
|
379
|
+
value.to_s.split(/[,;|]/).map(&:strip).reject(&:empty?)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def truthy?(value)
|
|
383
|
+
%w[true yes y 1].include?(value.to_s.strip.downcase)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def extract_issue_number(value)
|
|
387
|
+
value.to_s.match(/#(\d+)|\bissue[-\s]?(\d+)\b|\(#(\d+)\)/i)&.captures&.compact&.first&.to_i
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def slugify(value)
|
|
391
|
+
value.to_s.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_|_\z/, "")
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
@@ -15,7 +15,11 @@ module Aidp
|
|
|
15
15
|
priority: "Priority",
|
|
16
16
|
skills: "Skills",
|
|
17
17
|
personas: "Personas",
|
|
18
|
-
blocking: "Blocking"
|
|
18
|
+
blocking: "Blocking",
|
|
19
|
+
start_date: "Start Date",
|
|
20
|
+
target_date: "Target Date",
|
|
21
|
+
dependencies: "Dependencies",
|
|
22
|
+
critical_path: "Critical Path"
|
|
19
23
|
}.freeze
|
|
20
24
|
|
|
21
25
|
# Status values for different issue states
|
|
@@ -27,17 +31,25 @@ module Aidp
|
|
|
27
31
|
done: "Done",
|
|
28
32
|
blocked: "Blocked"
|
|
29
33
|
}.freeze
|
|
34
|
+
CRITICAL_PATH_VALUES = %w[Yes No].freeze
|
|
30
35
|
|
|
31
36
|
attr_reader :repository_client, :state_store, :project_id
|
|
32
37
|
|
|
33
|
-
def initialize(repository_client:, state_store:, project_id:, config: {})
|
|
38
|
+
def initialize(repository_client:, state_store:, project_id:, config: {}, gantt_synchronizer: nil)
|
|
34
39
|
@repository_client = repository_client
|
|
35
40
|
@state_store = state_store
|
|
36
41
|
@project_id = project_id
|
|
37
|
-
@config = config
|
|
38
|
-
@field_mappings = config[:field_mappings] ||
|
|
39
|
-
@auto_create_fields = config[:auto_create_fields] != false
|
|
42
|
+
@config = normalize_config(config)
|
|
43
|
+
@field_mappings = DEFAULT_FIELD_MAPPINGS.merge(@config[:field_mappings] || {})
|
|
44
|
+
@auto_create_fields = @config[:auto_create_fields] != false
|
|
40
45
|
@project_fields_cache = nil
|
|
46
|
+
@gantt_synchronizer = gantt_synchronizer || GanttSynchronizer.new(
|
|
47
|
+
repository_client: repository_client,
|
|
48
|
+
state_store: state_store,
|
|
49
|
+
project_id: project_id,
|
|
50
|
+
field_mappings: @field_mappings,
|
|
51
|
+
auto_create_fields: @auto_create_fields
|
|
52
|
+
)
|
|
41
53
|
end
|
|
42
54
|
|
|
43
55
|
# Sync a single issue to the project
|
|
@@ -66,7 +78,7 @@ module Aidp
|
|
|
66
78
|
|
|
67
79
|
# Update status if provided
|
|
68
80
|
if status
|
|
69
|
-
update_issue_status(issue_number, status)
|
|
81
|
+
return false unless update_issue_status(issue_number, status)
|
|
70
82
|
end
|
|
71
83
|
|
|
72
84
|
# Check and update blocking status
|
|
@@ -76,6 +88,7 @@ module Aidp
|
|
|
76
88
|
last_sync: Time.now.utc.iso8601,
|
|
77
89
|
status: status
|
|
78
90
|
})
|
|
91
|
+
sync_issue_status_to_gantt(issue_number, status) if status && gantt_sync_enabled?
|
|
79
92
|
|
|
80
93
|
true
|
|
81
94
|
rescue => e
|
|
@@ -178,6 +191,16 @@ module Aidp
|
|
|
178
191
|
{synced: synced, failed: failed}
|
|
179
192
|
end
|
|
180
193
|
|
|
194
|
+
def sync_from_gantt(prd_path = configured_prd_path, issue_numbers_by_title: {})
|
|
195
|
+
return {synced: 0, skipped: 0, critical_path: []} unless prd_path
|
|
196
|
+
|
|
197
|
+
@gantt_synchronizer.sync_from_prd(
|
|
198
|
+
prd_path: prd_path,
|
|
199
|
+
format: configured_gantt_format,
|
|
200
|
+
issue_numbers_by_title: issue_numbers_by_title
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
181
204
|
# Initialize required project fields if they don't exist
|
|
182
205
|
# @return [Boolean] True if all fields are ready
|
|
183
206
|
def ensure_project_fields
|
|
@@ -185,11 +208,6 @@ module Aidp
|
|
|
185
208
|
|
|
186
209
|
Aidp.log_debug("projects_processor", "ensure_project_fields", project_id: @project_id)
|
|
187
210
|
|
|
188
|
-
required_fields = [
|
|
189
|
-
{name: @field_mappings[:status], type: "SINGLE_SELECT", options: STATUS_VALUES.values},
|
|
190
|
-
{name: @field_mappings[:blocking], type: "TEXT"}
|
|
191
|
-
]
|
|
192
|
-
|
|
193
211
|
all_ready = true
|
|
194
212
|
required_fields.each do |field_spec|
|
|
195
213
|
field = find_or_create_field(field_spec[:name], field_spec[:type], field_spec[:options])
|
|
@@ -210,6 +228,19 @@ module Aidp
|
|
|
210
228
|
end
|
|
211
229
|
end
|
|
212
230
|
|
|
231
|
+
def normalize_config(value)
|
|
232
|
+
case value
|
|
233
|
+
when Hash
|
|
234
|
+
value.each_with_object({}) do |(key, nested_value), normalized|
|
|
235
|
+
normalized[key.to_sym] = normalize_config(nested_value)
|
|
236
|
+
end
|
|
237
|
+
when Array
|
|
238
|
+
value.map { |item| normalize_config(item) }
|
|
239
|
+
else
|
|
240
|
+
value
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
213
244
|
def invalidate_fields_cache
|
|
214
245
|
@project_fields_cache = nil
|
|
215
246
|
end
|
|
@@ -281,6 +312,52 @@ module Aidp
|
|
|
281
312
|
def clear_blocking_field(issue_number)
|
|
282
313
|
update_blocking_field(issue_number, [])
|
|
283
314
|
end
|
|
315
|
+
|
|
316
|
+
def gantt_sync_enabled?
|
|
317
|
+
@config[:auto_sync_gantt] == true && !configured_prd_path.to_s.empty?
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def required_fields
|
|
321
|
+
[
|
|
322
|
+
{name: @field_mappings[:status], type: "SINGLE_SELECT", options: STATUS_VALUES.values},
|
|
323
|
+
{name: @field_mappings[:blocking], type: "TEXT"}
|
|
324
|
+
] + gantt_required_fields
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def gantt_required_fields
|
|
328
|
+
return [] unless gantt_sync_enabled?
|
|
329
|
+
|
|
330
|
+
[
|
|
331
|
+
{name: @field_mappings[:start_date], type: "DATE"},
|
|
332
|
+
{name: @field_mappings[:target_date], type: "DATE"},
|
|
333
|
+
{name: @field_mappings[:dependencies], type: "TEXT"},
|
|
334
|
+
{name: @field_mappings[:critical_path], type: "SINGLE_SELECT", options: CRITICAL_PATH_VALUES}
|
|
335
|
+
]
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def configured_prd_path
|
|
339
|
+
@config[:prd_path]
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def configured_gantt_format
|
|
343
|
+
format = @config[:gantt_format]
|
|
344
|
+
return nil if format.nil? || format.to_s == "auto"
|
|
345
|
+
|
|
346
|
+
format
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def sync_issue_status_to_gantt(issue_number, status)
|
|
350
|
+
@gantt_synchronizer.sync_issue_status_to_gantt(
|
|
351
|
+
prd_path: configured_prd_path,
|
|
352
|
+
issue_number: issue_number,
|
|
353
|
+
status: status,
|
|
354
|
+
format: configured_gantt_format
|
|
355
|
+
)
|
|
356
|
+
rescue => e
|
|
357
|
+
Aidp.log_warn("projects_processor", "gantt_status_sync_failed",
|
|
358
|
+
issue_number: issue_number, error: e.message)
|
|
359
|
+
false
|
|
360
|
+
end
|
|
284
361
|
end
|
|
285
362
|
end
|
|
286
363
|
end
|
|
@@ -1644,6 +1644,21 @@ module Aidp
|
|
|
1644
1644
|
}
|
|
1645
1645
|
}
|
|
1646
1646
|
GRAPHQL
|
|
1647
|
+
elsif value.is_a?(Hash) && value[:date]
|
|
1648
|
+
<<~GRAPHQL
|
|
1649
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {
|
|
1650
|
+
updateProjectV2ItemFieldValue(input: {
|
|
1651
|
+
projectId: $projectId
|
|
1652
|
+
itemId: $itemId
|
|
1653
|
+
fieldId: $fieldId
|
|
1654
|
+
value: {date: $date}
|
|
1655
|
+
}) {
|
|
1656
|
+
projectV2Item {
|
|
1657
|
+
id
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
GRAPHQL
|
|
1647
1662
|
else
|
|
1648
1663
|
# Text field
|
|
1649
1664
|
<<~GRAPHQL
|
|
@@ -1675,8 +1690,10 @@ module Aidp
|
|
|
1675
1690
|
|
|
1676
1691
|
if value.is_a?(Hash) && value[:option_id]
|
|
1677
1692
|
variables[:optionId] = value[:option_id]
|
|
1693
|
+
elsif value.is_a?(Hash) && value[:date]
|
|
1694
|
+
variables[:date] = value[:date]
|
|
1678
1695
|
else
|
|
1679
|
-
variables[:text] = value.to_s
|
|
1696
|
+
variables[:text] = value.is_a?(Hash) ? value[:text].to_s : value.to_s
|
|
1680
1697
|
end
|
|
1681
1698
|
|
|
1682
1699
|
result = execute_graphql_query(mutation, **variables)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: aidp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.45.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -609,11 +609,13 @@ files:
|
|
|
609
609
|
- lib/aidp/watch/ci_fix_processor.rb
|
|
610
610
|
- lib/aidp/watch/ci_log_extractor.rb
|
|
611
611
|
- lib/aidp/watch/feedback_collector.rb
|
|
612
|
+
- lib/aidp/watch/gantt_synchronizer.rb
|
|
612
613
|
- lib/aidp/watch/github_state_extractor.rb
|
|
613
614
|
- lib/aidp/watch/hierarchical_pr_strategy.rb
|
|
614
615
|
- lib/aidp/watch/implementation_verifier.rb
|
|
615
616
|
- lib/aidp/watch/plan_generator.rb
|
|
616
617
|
- lib/aidp/watch/plan_processor.rb
|
|
618
|
+
- lib/aidp/watch/prd_parser.rb
|
|
617
619
|
- lib/aidp/watch/projects_processor.rb
|
|
618
620
|
- lib/aidp/watch/rebase_processor.rb
|
|
619
621
|
- lib/aidp/watch/repository_client.rb
|