aidp 0.41.0 → 0.42.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/execute/project_knowledge_manager.rb +300 -0
- data/lib/aidp/execute/work_loop_runner.rb +292 -15
- data/lib/aidp/prompt_optimization/context_composer.rb +5 -0
- data/lib/aidp/prompt_optimization/optimizer.rb +11 -0
- data/lib/aidp/prompt_optimization/project_knowledge_indexer.rb +98 -0
- data/lib/aidp/prompt_optimization/prompt_builder.rb +22 -0
- data/lib/aidp/prompt_optimization/relevance_scorer.rb +17 -0
- data/lib/aidp/version.rb +1 -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: f44b47ad0d31015794ec7b2286dc659e85061eb3e6dad8c89ae018976e0d9b1d
|
|
4
|
+
data.tar.gz: 6d8b28a431c3d55e037e32d0e54e583313af4d495982578b72ab3374155323c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2f838a10c7789f69a253858d0d71aee754a24020722a845eba38cd29eb764be247fee615427afe67f13618cec384565c04cf0f25a25ddd2493962df0df3e0af
|
|
7
|
+
data.tar.gz: 616bf1a6910461b907c5ab99bccb8dddc3312ee7816550d22d31c395b9faecb90e97bd4e5a032ba594a5d78538d9199a0ffbee203a8fe5c08ae8f784ae47b84e
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Execute
|
|
8
|
+
# Creates and refreshes concise feature/tool notes under docs/
|
|
9
|
+
class ProjectKnowledgeManager
|
|
10
|
+
MAX_TOOL_NOTES = 5
|
|
11
|
+
FEATURE_ROOT_SEGMENTS = %w[app client lib packages server src].freeze
|
|
12
|
+
|
|
13
|
+
def initialize(project_dir)
|
|
14
|
+
@project_dir = project_dir
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def sync!(step_name:, task_description:, affected_files:, tool_commands:, feature_identifier: nil)
|
|
18
|
+
sync_feature_note(
|
|
19
|
+
feature_identifier: feature_identifier,
|
|
20
|
+
task_description: task_description,
|
|
21
|
+
affected_files: affected_files
|
|
22
|
+
)
|
|
23
|
+
sync_tool_notes(step_name: step_name, affected_files: affected_files, tool_commands: tool_commands)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def sync_feature_note(feature_identifier:, task_description:, affected_files:)
|
|
29
|
+
return if affected_files.empty?
|
|
30
|
+
|
|
31
|
+
feature_id = resolve_feature_id(
|
|
32
|
+
feature_identifier: feature_identifier,
|
|
33
|
+
affected_files: affected_files
|
|
34
|
+
)
|
|
35
|
+
return if feature_id.empty?
|
|
36
|
+
|
|
37
|
+
path = File.join(@project_dir, "docs", "features", "#{feature_id}.md")
|
|
38
|
+
markers = affected_files.map { |file| "<#{feature_id}:#{file}>" }
|
|
39
|
+
learning = bullet_line("#{Time.now.utc.iso8601}: #{learning_summary(task_description)}")
|
|
40
|
+
|
|
41
|
+
content = if File.exist?(path)
|
|
42
|
+
update_existing_note(File.read(path, encoding: "UTF-8"), markers: markers, learning: learning)
|
|
43
|
+
else
|
|
44
|
+
build_feature_note(feature_id: feature_id, markers: markers, learning: learning)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
write_note(path, content)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def sync_tool_notes(step_name:, affected_files:, tool_commands:)
|
|
51
|
+
tool_commands.first(MAX_TOOL_NOTES).each do |command|
|
|
52
|
+
tool_id = tool_identifier(command)
|
|
53
|
+
next if tool_id.empty?
|
|
54
|
+
|
|
55
|
+
path = File.join(@project_dir, "docs", "tools", "#{tool_id}.md")
|
|
56
|
+
commands = [bullet_line("`#{command}`")]
|
|
57
|
+
markers = affected_files.map { |file| "<#{tool_id}:#{file}>" }
|
|
58
|
+
learning = bullet_line("#{Time.now.utc.iso8601}: Used `#{command}` during `#{step_name}`.")
|
|
59
|
+
|
|
60
|
+
content = if File.exist?(path)
|
|
61
|
+
update_existing_note(File.read(path, encoding: "UTF-8"), markers: markers, learning: learning, commands: commands)
|
|
62
|
+
else
|
|
63
|
+
build_tool_note(commands: commands, tool_id: tool_id, markers: markers, learning: learning)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
write_note(path, content)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_feature_note(feature_id:, markers:, learning:)
|
|
71
|
+
<<~MARKDOWN
|
|
72
|
+
# #{humanize(feature_id)}
|
|
73
|
+
|
|
74
|
+
## Paths
|
|
75
|
+
#{markers.join("\n")}
|
|
76
|
+
|
|
77
|
+
## Purpose
|
|
78
|
+
- Concise feature context for work touching these paths.
|
|
79
|
+
|
|
80
|
+
## Users
|
|
81
|
+
- Add the end users or personas when they become clear.
|
|
82
|
+
|
|
83
|
+
## Known Gaps
|
|
84
|
+
- Add concrete gaps when discovered.
|
|
85
|
+
|
|
86
|
+
## Key Decisions
|
|
87
|
+
- Add behavior-affecting decisions when they become clear.
|
|
88
|
+
|
|
89
|
+
## Recent Learnings
|
|
90
|
+
#{learning}
|
|
91
|
+
MARKDOWN
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def build_tool_note(commands:, tool_id:, markers:, learning:)
|
|
95
|
+
paths_section = markers.empty? ? "- Add affected paths as they become known." : markers.join("\n")
|
|
96
|
+
|
|
97
|
+
<<~MARKDOWN
|
|
98
|
+
# #{humanize(tool_id)}
|
|
99
|
+
|
|
100
|
+
## Commands
|
|
101
|
+
#{commands.join("\n")}
|
|
102
|
+
|
|
103
|
+
## Paths
|
|
104
|
+
#{paths_section}
|
|
105
|
+
|
|
106
|
+
## Intended Use
|
|
107
|
+
- Run this tool while validating or updating related paths.
|
|
108
|
+
|
|
109
|
+
## Known Gaps
|
|
110
|
+
- Add environment quirks or failure modes when discovered.
|
|
111
|
+
|
|
112
|
+
## Recent Learnings
|
|
113
|
+
#{learning}
|
|
114
|
+
MARKDOWN
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def update_existing_note(content, markers:, learning:, commands: [])
|
|
118
|
+
updated = ensure_section_entries(content, "Commands", commands)
|
|
119
|
+
updated = ensure_section_entries(updated, "Paths", markers)
|
|
120
|
+
ensure_section_entries(updated, "Recent Learnings", [learning], normalizer: method(:normalize_learning_entry))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ensure_section_entries(content, section_name, entries, normalizer: nil)
|
|
124
|
+
return content if entries.empty?
|
|
125
|
+
|
|
126
|
+
existing_section = content.match(/(## #{Regexp.escape(section_name)}\n)(.*?)(?=\n## |\z)/m)
|
|
127
|
+
return append_new_section(content, section_name, entries) unless existing_section
|
|
128
|
+
|
|
129
|
+
body = existing_section[2].to_s
|
|
130
|
+
existing_entries = body.lines.map(&:strip).reject(&:empty?)
|
|
131
|
+
normalized_existing = normalize_section_entries(existing_entries, normalizer)
|
|
132
|
+
missing = entries.reject do |entry|
|
|
133
|
+
normalized_existing.include?(normalize_section_entry(entry, normalizer))
|
|
134
|
+
end
|
|
135
|
+
return content if missing.empty?
|
|
136
|
+
|
|
137
|
+
replacement = "#{existing_section[1]}#{body.rstrip}\n#{missing.join("\n")}\n"
|
|
138
|
+
content.sub(existing_section[0], replacement)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def append_new_section(content, section_name, entries)
|
|
142
|
+
[content.rstrip, "", "## #{section_name}", entries.join("\n"), ""].join("\n")
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def write_note(path, content)
|
|
146
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
147
|
+
File.write(path, content)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def resolve_feature_id(feature_identifier:, affected_files:)
|
|
151
|
+
explicit_id = normalize_id(feature_identifier)
|
|
152
|
+
return explicit_id unless explicit_id.empty?
|
|
153
|
+
|
|
154
|
+
derived_id = normalize_id(feature_path_identifier(affected_files))
|
|
155
|
+
return derived_id unless derived_id.empty?
|
|
156
|
+
|
|
157
|
+
""
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def feature_path_identifier(affected_files)
|
|
161
|
+
feature_files = affected_files.select { |file| feature_path?(file) }
|
|
162
|
+
segments = feature_files.map { |file| trimmed_feature_segments(file) }
|
|
163
|
+
return "" if segments.empty?
|
|
164
|
+
|
|
165
|
+
shared_segments = meaningful_segments(common_path_segments(segments))
|
|
166
|
+
return shared_segments.join("_") unless shared_segments.empty?
|
|
167
|
+
|
|
168
|
+
meaningful_segments(segments.first).join("_")
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def feature_path?(file)
|
|
172
|
+
path_segments = file.to_s.split("/")
|
|
173
|
+
FEATURE_ROOT_SEGMENTS.include?(path_segments.first) && path_segments.length > 1
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def common_path_segments(all_segments)
|
|
177
|
+
shared = []
|
|
178
|
+
max_length = all_segments.map(&:length).min
|
|
179
|
+
|
|
180
|
+
max_length.times do |index|
|
|
181
|
+
segment = all_segments.first[index]
|
|
182
|
+
break unless all_segments.all? { |segments| segments[index] == segment }
|
|
183
|
+
|
|
184
|
+
shared << segment
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
shared
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def meaningful_segments(path_segments)
|
|
191
|
+
path_segments.map { |segment| segment.sub(/\.[^.]+\z/, "") }
|
|
192
|
+
.reject { |segment| generic_path_segment?(segment) }
|
|
193
|
+
.reject(&:empty?)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def trimmed_feature_segments(file)
|
|
197
|
+
segments = file.to_s.split("/")
|
|
198
|
+
return segments unless generic_path_segment?(segments.first)
|
|
199
|
+
|
|
200
|
+
segments.drop(1)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def generic_path_segment?(segment)
|
|
204
|
+
%w[app client docs lib packages server spec src test].include?(segment)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def normalize_id(value)
|
|
208
|
+
value.to_s.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_|_\z/, "")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def learning_summary(task_description)
|
|
212
|
+
single_line = task_description.to_s.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
213
|
+
.lines
|
|
214
|
+
.map(&:strip)
|
|
215
|
+
.reject(&:empty?)
|
|
216
|
+
.join(" ")
|
|
217
|
+
.gsub(/\s+/, " ")
|
|
218
|
+
|
|
219
|
+
return "Updated feature context." if single_line.empty?
|
|
220
|
+
return single_line if single_line.length <= 200
|
|
221
|
+
|
|
222
|
+
"#{single_line[0, 197].rstrip}..."
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def normalize_section_entries(entries, normalizer)
|
|
226
|
+
entries.map { |entry| normalize_section_entry(entry, normalizer) }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def normalize_section_entry(entry, normalizer)
|
|
230
|
+
return entry unless normalizer
|
|
231
|
+
|
|
232
|
+
normalizer.call(entry)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def normalize_learning_entry(entry)
|
|
236
|
+
entry.to_s.sub(/\A-\s*/, "").sub(/\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z:\s*/, "")
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def tool_identifier(command)
|
|
240
|
+
raw_command = command.to_s.strip
|
|
241
|
+
tokens = Shellwords.split(raw_command)
|
|
242
|
+
executable = unwrap_tool_command(strip_env_assignments(tokens))
|
|
243
|
+
normalize_id(executable)
|
|
244
|
+
rescue ArgumentError
|
|
245
|
+
normalize_id(raw_command.split(/\s+/).first)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def unwrap_tool_command(tokens)
|
|
249
|
+
command_tokens = tokens.dup
|
|
250
|
+
|
|
251
|
+
loop do
|
|
252
|
+
return "" if command_tokens.empty?
|
|
253
|
+
|
|
254
|
+
case command_tokens.first
|
|
255
|
+
when "mise"
|
|
256
|
+
command_tokens = unwrap_mise_exec(command_tokens)
|
|
257
|
+
when "env", "/usr/bin/env"
|
|
258
|
+
command_tokens = unwrap_env(command_tokens)
|
|
259
|
+
when "bundle"
|
|
260
|
+
return File.basename(command_tokens.first, ".rb") unless command_tokens[1] == "exec"
|
|
261
|
+
|
|
262
|
+
command_tokens = command_tokens.drop(2)
|
|
263
|
+
when "ruby"
|
|
264
|
+
return File.basename(command_tokens[1], ".rb") if command_tokens[1]
|
|
265
|
+
|
|
266
|
+
command_tokens = []
|
|
267
|
+
else
|
|
268
|
+
return File.basename(command_tokens.first, ".rb")
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def strip_env_assignments(tokens)
|
|
274
|
+
tokens.drop_while { |token| token.include?("=") && !token.start_with?("/", "./", "../") }
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def unwrap_mise_exec(tokens)
|
|
278
|
+
return tokens unless tokens[1] == "exec"
|
|
279
|
+
|
|
280
|
+
separator_index = tokens.index("--")
|
|
281
|
+
remaining = separator_index ? tokens.drop(separator_index + 1) : tokens.drop(2)
|
|
282
|
+
strip_env_assignments(remaining)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def unwrap_env(tokens)
|
|
286
|
+
remaining = tokens.drop(1)
|
|
287
|
+
remaining = remaining.drop_while { |token| token.start_with?("-") }
|
|
288
|
+
strip_env_assignments(remaining)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def humanize(value)
|
|
292
|
+
value.to_s.split(/[_\s]+/).map(&:capitalize).join(" ")
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def bullet_line(value)
|
|
296
|
+
"- #{value}"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -8,6 +8,7 @@ require_relative "guard_policy"
|
|
|
8
8
|
require_relative "work_loop_unit_scheduler"
|
|
9
9
|
require_relative "deterministic_unit"
|
|
10
10
|
require_relative "agent_signal_parser"
|
|
11
|
+
require_relative "project_knowledge_manager"
|
|
11
12
|
require_relative "steps"
|
|
12
13
|
require_relative "../harness/test_runner"
|
|
13
14
|
require_relative "../errors"
|
|
@@ -61,7 +62,10 @@ module Aidp
|
|
|
61
62
|
@checkpoint = Checkpoint.new(project_dir)
|
|
62
63
|
@checkpoint_display = CheckpointDisplay.new(prompt: @prompt)
|
|
63
64
|
@guard_policy = GuardPolicy.new(project_dir, config.guards_config)
|
|
65
|
+
@project_knowledge_manager = options[:project_knowledge_manager] || ProjectKnowledgeManager.new(project_dir)
|
|
64
66
|
@work_context = {}
|
|
67
|
+
@executed_tool_commands = []
|
|
68
|
+
@agentic_execution_results = []
|
|
65
69
|
@persistent_tasklist = PersistentTasklist.new(project_dir)
|
|
66
70
|
@iteration_count = 0
|
|
67
71
|
@step_name = nil
|
|
@@ -100,6 +104,8 @@ module Aidp
|
|
|
100
104
|
def execute_step(step_name, step_spec, context = {})
|
|
101
105
|
@step_name = step_name
|
|
102
106
|
@work_context = context
|
|
107
|
+
@executed_tool_commands = []
|
|
108
|
+
@agentic_execution_results = []
|
|
103
109
|
@iteration_count = 0
|
|
104
110
|
transition_to(:ready)
|
|
105
111
|
|
|
@@ -572,6 +578,7 @@ module Aidp
|
|
|
572
578
|
tier: @thinking_depth_manager.current_tier
|
|
573
579
|
}
|
|
574
580
|
)
|
|
581
|
+
record_agentic_execution_result(agent_result)
|
|
575
582
|
|
|
576
583
|
requested = AgentSignalParser.extract_next_unit(agent_result[:output])
|
|
577
584
|
|
|
@@ -605,6 +612,7 @@ module Aidp
|
|
|
605
612
|
tier: @thinking_depth_manager.current_tier
|
|
606
613
|
}
|
|
607
614
|
)
|
|
615
|
+
record_agentic_execution_result(agent_result)
|
|
608
616
|
|
|
609
617
|
requested = AgentSignalParser.extract_next_unit(agent_result[:output])
|
|
610
618
|
|
|
@@ -778,11 +786,13 @@ module Aidp
|
|
|
778
786
|
if @config.respond_to?(:commands) && @config.commands.any?
|
|
779
787
|
# New phase-based execution
|
|
780
788
|
each_unit_results = @test_runner.run_commands_for_phase(:each_unit)
|
|
789
|
+
record_phase_commands(:each_unit, each_unit_results)
|
|
781
790
|
all_results[:each_unit] = each_unit_results
|
|
782
791
|
|
|
783
792
|
# Run on_completion commands only if agent marked work complete
|
|
784
793
|
if agent_marked_complete?(agent_result)
|
|
785
794
|
on_completion_results = @test_runner.run_commands_for_phase(:on_completion)
|
|
795
|
+
record_phase_commands(:on_completion, on_completion_results)
|
|
786
796
|
all_results[:on_completion] = on_completion_results
|
|
787
797
|
else
|
|
788
798
|
all_results[:on_completion] = {
|
|
@@ -810,6 +820,7 @@ module Aidp
|
|
|
810
820
|
return {success: true, output: "", failures: [], required_failures: []} unless @config.respond_to?(:commands)
|
|
811
821
|
|
|
812
822
|
full_loop_results = @test_runner.run_commands_for_phase(:full_loop)
|
|
823
|
+
record_phase_commands(:full_loop, full_loop_results)
|
|
813
824
|
|
|
814
825
|
Aidp.log_debug("work_loop", "ran_full_loop_commands",
|
|
815
826
|
success: full_loop_results[:success],
|
|
@@ -821,13 +832,17 @@ module Aidp
|
|
|
821
832
|
# Run commands using legacy category-based approach (backwards compatibility)
|
|
822
833
|
def run_legacy_category_commands(agent_result)
|
|
823
834
|
test_results = @test_runner.run_tests
|
|
835
|
+
record_legacy_category_commands(:tests)
|
|
824
836
|
lint_results = @test_runner.run_linters
|
|
837
|
+
record_legacy_category_commands(:lints)
|
|
825
838
|
build_results = @test_runner.run_builds
|
|
839
|
+
record_legacy_category_commands(:builds)
|
|
826
840
|
doc_results = @test_runner.run_documentation
|
|
841
|
+
record_legacy_category_commands(:docs)
|
|
827
842
|
|
|
828
843
|
# Run formatters only if agent marked work complete (per issue #234)
|
|
829
844
|
formatter_results = if agent_marked_complete?(agent_result)
|
|
830
|
-
@test_runner.run_formatters
|
|
845
|
+
@test_runner.run_formatters.tap { record_legacy_category_commands(:formatters) }
|
|
831
846
|
else
|
|
832
847
|
{success: true, output: "Formatters: Skipped (work not complete)", failures: [], required_failures: []}
|
|
833
848
|
end
|
|
@@ -959,20 +974,8 @@ module Aidp
|
|
|
959
974
|
|
|
960
975
|
# Extract files that will be affected by this work
|
|
961
976
|
def extract_affected_files(context, user_input)
|
|
962
|
-
files =
|
|
963
|
-
|
|
964
|
-
# From user input (e.g., "update lib/user.rb")
|
|
965
|
-
user_input&.scan(/[\w\/]+\.rb/)&.each do |file|
|
|
966
|
-
files << file
|
|
967
|
-
end
|
|
968
|
-
|
|
969
|
-
# From deterministic outputs
|
|
970
|
-
context[:deterministic_outputs]&.each do |output|
|
|
971
|
-
if output[:output_path]&.end_with?(".rb")
|
|
972
|
-
files << output[:output_path]
|
|
973
|
-
end
|
|
974
|
-
end
|
|
975
|
-
|
|
977
|
+
files = extract_paths_from_text(user_input)
|
|
978
|
+
files.concat(extract_output_paths(context[:deterministic_outputs]))
|
|
976
979
|
files.uniq
|
|
977
980
|
end
|
|
978
981
|
|
|
@@ -1110,6 +1113,7 @@ module Aidp
|
|
|
1110
1113
|
execute_block.call
|
|
1111
1114
|
end
|
|
1112
1115
|
end
|
|
1116
|
+
.tap { |result| record_agentic_execution_result(result) }
|
|
1113
1117
|
end
|
|
1114
1118
|
|
|
1115
1119
|
def display_iteration_overview(provider_name, model_name, prompt_length, checks_summary = nil)
|
|
@@ -1515,10 +1519,283 @@ module Aidp
|
|
|
1515
1519
|
end
|
|
1516
1520
|
|
|
1517
1521
|
def archive_and_cleanup
|
|
1522
|
+
sync_project_knowledge
|
|
1518
1523
|
@prompt_manager.archive(@step_name)
|
|
1519
1524
|
@prompt_manager.delete
|
|
1520
1525
|
end
|
|
1521
1526
|
|
|
1527
|
+
def sync_project_knowledge
|
|
1528
|
+
affected_files = knowledge_affected_files
|
|
1529
|
+
return if affected_files.empty?
|
|
1530
|
+
|
|
1531
|
+
@project_knowledge_manager.sync!(
|
|
1532
|
+
step_name: @step_name,
|
|
1533
|
+
feature_identifier: knowledge_feature_identifier,
|
|
1534
|
+
task_description: build_task_description(format_user_input(@work_context[:user_input]), @work_context),
|
|
1535
|
+
affected_files: affected_files,
|
|
1536
|
+
tool_commands: knowledge_tool_commands
|
|
1537
|
+
)
|
|
1538
|
+
rescue => e
|
|
1539
|
+
Aidp.logger.warn("work_loop", "project_knowledge_sync_failed",
|
|
1540
|
+
step: @step_name,
|
|
1541
|
+
error: e.message)
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
def knowledge_affected_files
|
|
1545
|
+
files = Array(@work_context[:affected_files])
|
|
1546
|
+
files.concat(extract_affected_files(@work_context, format_user_input(@work_context[:user_input])))
|
|
1547
|
+
files.concat(metadata_affected_files)
|
|
1548
|
+
files.concat(get_changed_files) if files.empty?
|
|
1549
|
+
files.map(&:to_s).select { |path| knowledge_path_candidate?(path) }.uniq
|
|
1550
|
+
end
|
|
1551
|
+
|
|
1552
|
+
def extract_paths_from_text(text)
|
|
1553
|
+
return [] if text.nil?
|
|
1554
|
+
|
|
1555
|
+
text.to_s.split.filter_map do |token|
|
|
1556
|
+
path = trim_path_token(token)
|
|
1557
|
+
path if valid_path_candidate?(path)
|
|
1558
|
+
end
|
|
1559
|
+
end
|
|
1560
|
+
|
|
1561
|
+
def extract_output_paths(outputs)
|
|
1562
|
+
Array(outputs).filter_map do |output|
|
|
1563
|
+
path = output[:output_path].to_s.strip
|
|
1564
|
+
path unless path.empty?
|
|
1565
|
+
end
|
|
1566
|
+
end
|
|
1567
|
+
|
|
1568
|
+
def knowledge_tool_commands
|
|
1569
|
+
metadata_tool_commands.concat(@executed_tool_commands).uniq
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
def record_agentic_execution_result(result)
|
|
1573
|
+
return unless result.is_a?(Hash)
|
|
1574
|
+
|
|
1575
|
+
@agentic_execution_results << result
|
|
1576
|
+
end
|
|
1577
|
+
|
|
1578
|
+
def knowledge_feature_identifier
|
|
1579
|
+
user_input = @work_context[:user_input]
|
|
1580
|
+
|
|
1581
|
+
@work_context[:feature_identifier] ||
|
|
1582
|
+
@work_context[:feature_id] ||
|
|
1583
|
+
extract_feature_identifier(user_input)
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
def record_phase_commands(phase, results)
|
|
1587
|
+
commands_by_name = Array(@config.commands_for_phase(phase)).each_with_object({}) do |entry, indexed|
|
|
1588
|
+
indexed[entry[:name]] = entry
|
|
1589
|
+
end
|
|
1590
|
+
executed = results.fetch(:results_by_command, {}).keys.filter_map do |name|
|
|
1591
|
+
commands_by_name[name]&.fetch(:command, nil)
|
|
1592
|
+
end
|
|
1593
|
+
executed = commands_by_name.values.map { |entry| entry[:command] } if executed.empty? && commands_by_name.any?
|
|
1594
|
+
@executed_tool_commands.concat(executed.compact)
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
def record_legacy_category_commands(category)
|
|
1598
|
+
return unless @test_runner.respond_to?(:planned_commands)
|
|
1599
|
+
|
|
1600
|
+
commands = Array(@test_runner.planned_commands[category]).filter_map do |entry|
|
|
1601
|
+
entry.is_a?(String) ? entry : entry[:command]
|
|
1602
|
+
end
|
|
1603
|
+
@executed_tool_commands.concat(commands)
|
|
1604
|
+
end
|
|
1605
|
+
|
|
1606
|
+
def extract_feature_identifier(user_input)
|
|
1607
|
+
return unless user_input.is_a?(Hash)
|
|
1608
|
+
|
|
1609
|
+
user_input["feature_identifier"] ||
|
|
1610
|
+
user_input[:feature_identifier] ||
|
|
1611
|
+
user_input["feature_id"] ||
|
|
1612
|
+
user_input[:feature_id]
|
|
1613
|
+
end
|
|
1614
|
+
|
|
1615
|
+
def metadata_affected_files
|
|
1616
|
+
metadata_entries = metadata_value_objects
|
|
1617
|
+
files = extract_file_entries(metadata_entries, :affected_files)
|
|
1618
|
+
files.concat(extract_file_entries(metadata_entries, :changed_files))
|
|
1619
|
+
files.concat(extract_file_entries(metadata_entries, :edited_files))
|
|
1620
|
+
files.concat(extract_file_entries(metadata_entries, :files_changed))
|
|
1621
|
+
files.concat(extract_file_entries(metadata_entries, :modified_files))
|
|
1622
|
+
files.concat(extract_file_entries(metadata_entries, :touched_files))
|
|
1623
|
+
files.uniq
|
|
1624
|
+
end
|
|
1625
|
+
|
|
1626
|
+
def metadata_tool_commands
|
|
1627
|
+
metadata_entries = metadata_value_objects
|
|
1628
|
+
commands = extract_command_entries(metadata_entries, :tool_commands)
|
|
1629
|
+
commands.concat(extract_command_entries(metadata_entries, :tool_usage))
|
|
1630
|
+
commands.concat(extract_command_entries(metadata_entries, :tool_calls))
|
|
1631
|
+
commands.concat(extract_command_entries(metadata_entries, :tools_used))
|
|
1632
|
+
commands.concat(extract_command_entries(metadata_entries, :commands))
|
|
1633
|
+
commands.uniq
|
|
1634
|
+
end
|
|
1635
|
+
|
|
1636
|
+
def metadata_value_objects
|
|
1637
|
+
Array(@agentic_execution_results).flat_map do |result|
|
|
1638
|
+
[
|
|
1639
|
+
result,
|
|
1640
|
+
indifferent_hash_value(result, :metadata),
|
|
1641
|
+
indifferent_hash_value(result, :output)
|
|
1642
|
+
]
|
|
1643
|
+
end.compact.select { |entry| entry.is_a?(Hash) }
|
|
1644
|
+
end
|
|
1645
|
+
|
|
1646
|
+
def extract_file_entries(entries, key)
|
|
1647
|
+
entries.flat_map do |entry|
|
|
1648
|
+
Array(indifferent_hash_value(entry, key)).flat_map do |value|
|
|
1649
|
+
case value
|
|
1650
|
+
when String
|
|
1651
|
+
[value]
|
|
1652
|
+
when Hash
|
|
1653
|
+
candidate = file_candidate_from_hash(value)
|
|
1654
|
+
candidate ? [candidate] : []
|
|
1655
|
+
else
|
|
1656
|
+
[]
|
|
1657
|
+
end
|
|
1658
|
+
end
|
|
1659
|
+
end
|
|
1660
|
+
end
|
|
1661
|
+
|
|
1662
|
+
def extract_command_entries(entries, key)
|
|
1663
|
+
entries.flat_map do |entry|
|
|
1664
|
+
Array(indifferent_hash_value(entry, key)).flat_map do |value|
|
|
1665
|
+
case value
|
|
1666
|
+
when String
|
|
1667
|
+
[value]
|
|
1668
|
+
when Hash
|
|
1669
|
+
candidate = command_candidate_from_hash(value)
|
|
1670
|
+
candidate ? [candidate] : []
|
|
1671
|
+
else
|
|
1672
|
+
[]
|
|
1673
|
+
end
|
|
1674
|
+
end
|
|
1675
|
+
end.reject(&:empty?)
|
|
1676
|
+
end
|
|
1677
|
+
|
|
1678
|
+
def indifferent_hash_value(hash, key)
|
|
1679
|
+
hash[key] || hash[key.to_s]
|
|
1680
|
+
end
|
|
1681
|
+
|
|
1682
|
+
def file_candidate_from_hash(hash)
|
|
1683
|
+
%i[path file file_path output_path relative_path].each do |key|
|
|
1684
|
+
candidate = indifferent_hash_value(hash, key).to_s.strip
|
|
1685
|
+
return candidate unless candidate.empty?
|
|
1686
|
+
end
|
|
1687
|
+
|
|
1688
|
+
nil
|
|
1689
|
+
end
|
|
1690
|
+
|
|
1691
|
+
def command_candidate_from_hash(hash)
|
|
1692
|
+
%i[command tool name identifier].each do |key|
|
|
1693
|
+
candidate = indifferent_hash_value(hash, key).to_s.strip
|
|
1694
|
+
return candidate unless candidate.empty?
|
|
1695
|
+
end
|
|
1696
|
+
|
|
1697
|
+
nil
|
|
1698
|
+
end
|
|
1699
|
+
|
|
1700
|
+
def trim_path_token(token)
|
|
1701
|
+
text = token.to_s
|
|
1702
|
+
start_index = 0
|
|
1703
|
+
end_index = text.length - 1
|
|
1704
|
+
|
|
1705
|
+
start_index += 1 while start_index <= end_index && !path_character?(text[start_index])
|
|
1706
|
+
end_index -= 1 while end_index >= start_index && !path_character?(text[end_index])
|
|
1707
|
+
|
|
1708
|
+
return "" if start_index > end_index
|
|
1709
|
+
|
|
1710
|
+
text[start_index..end_index]
|
|
1711
|
+
end
|
|
1712
|
+
|
|
1713
|
+
def knowledge_path_candidate?(path)
|
|
1714
|
+
return false if path.empty?
|
|
1715
|
+
return false if internal_generated_path?(path)
|
|
1716
|
+
|
|
1717
|
+
true
|
|
1718
|
+
end
|
|
1719
|
+
|
|
1720
|
+
def internal_generated_path?(path)
|
|
1721
|
+
path == ".aidp" || path.start_with?(".aidp/")
|
|
1722
|
+
end
|
|
1723
|
+
|
|
1724
|
+
def path_character?(char)
|
|
1725
|
+
alphanumeric_character?(char) || ["_", ".", "/", "-"].include?(char)
|
|
1726
|
+
end
|
|
1727
|
+
|
|
1728
|
+
def valid_path_candidate?(path)
|
|
1729
|
+
return false if path.empty?
|
|
1730
|
+
return false if standalone_version_token?(path)
|
|
1731
|
+
|
|
1732
|
+
segments = path.split("/")
|
|
1733
|
+
return false if segments.empty? || segments.any?(&:empty?)
|
|
1734
|
+
|
|
1735
|
+
filename = segments.last
|
|
1736
|
+
return false unless valid_filename_candidate?(filename, path.include?("/"))
|
|
1737
|
+
|
|
1738
|
+
segments.all? { |segment| valid_path_segment?(segment) }
|
|
1739
|
+
end
|
|
1740
|
+
|
|
1741
|
+
def standalone_version_token?(path)
|
|
1742
|
+
return false if path.include?("/")
|
|
1743
|
+
|
|
1744
|
+
components = path.split(".")
|
|
1745
|
+
return false if components.length < 2
|
|
1746
|
+
|
|
1747
|
+
components.all? { |component| version_component?(component) }
|
|
1748
|
+
end
|
|
1749
|
+
|
|
1750
|
+
def version_component?(component)
|
|
1751
|
+
component.match?(/\Av?\d+\z/i)
|
|
1752
|
+
end
|
|
1753
|
+
|
|
1754
|
+
def valid_path_segment?(segment)
|
|
1755
|
+
return false if segment == "." || segment == ".."
|
|
1756
|
+
|
|
1757
|
+
segment.each_char.all? do |char|
|
|
1758
|
+
alphanumeric_character?(char) || ["_", ".", "-"].include?(char)
|
|
1759
|
+
end
|
|
1760
|
+
end
|
|
1761
|
+
|
|
1762
|
+
def valid_filename_candidate?(filename, nested_path)
|
|
1763
|
+
return valid_extension_filename?(filename) if filename.include?(".")
|
|
1764
|
+
|
|
1765
|
+
return true if nested_path
|
|
1766
|
+
|
|
1767
|
+
known_extensionless_filename?(filename)
|
|
1768
|
+
end
|
|
1769
|
+
|
|
1770
|
+
def valid_extension_filename?(filename)
|
|
1771
|
+
name, _separator, extension = filename.rpartition(".")
|
|
1772
|
+
return false if name.to_s.empty? || extension.to_s.empty?
|
|
1773
|
+
|
|
1774
|
+
extension.each_char.all? { |char| alphanumeric_character?(char) }
|
|
1775
|
+
end
|
|
1776
|
+
|
|
1777
|
+
def known_extensionless_filename?(filename)
|
|
1778
|
+
%w[
|
|
1779
|
+
Dockerfile
|
|
1780
|
+
Gemfile
|
|
1781
|
+
Procfile
|
|
1782
|
+
Rakefile
|
|
1783
|
+
Makefile
|
|
1784
|
+
Brewfile
|
|
1785
|
+
Podfile
|
|
1786
|
+
Fastfile
|
|
1787
|
+
Appfile
|
|
1788
|
+
Guardfile
|
|
1789
|
+
Vagrantfile
|
|
1790
|
+
].include?(filename)
|
|
1791
|
+
end
|
|
1792
|
+
|
|
1793
|
+
def alphanumeric_character?(char)
|
|
1794
|
+
char.between?("a", "z") ||
|
|
1795
|
+
char.between?("A", "Z") ||
|
|
1796
|
+
char.between?("0", "9")
|
|
1797
|
+
end
|
|
1798
|
+
|
|
1522
1799
|
def load_template(template_name)
|
|
1523
1800
|
return "" unless template_name
|
|
1524
1801
|
|
|
@@ -115,6 +115,8 @@ module Aidp
|
|
|
115
115
|
def meets_threshold?(fragment, score, thresholds)
|
|
116
116
|
threshold = if fragment.class.name.include?("Fragment") && fragment.respond_to?(:heading)
|
|
117
117
|
thresholds[:style_guide] || 0.75
|
|
118
|
+
elsif fragment.respond_to?(:linked_paths)
|
|
119
|
+
thresholds[:knowledge] || 0.72
|
|
118
120
|
elsif fragment.respond_to?(:category)
|
|
119
121
|
thresholds[:templates] || 0.8
|
|
120
122
|
elsif fragment.respond_to?(:file_path) && fragment.respond_to?(:type)
|
|
@@ -248,6 +250,8 @@ module Aidp
|
|
|
248
250
|
case type
|
|
249
251
|
when :style_guide
|
|
250
252
|
item[:fragment].class.name.include?("Fragment") && item[:fragment].respond_to?(:heading)
|
|
253
|
+
when :project_knowledge
|
|
254
|
+
item[:fragment].respond_to?(:linked_paths)
|
|
251
255
|
when :template
|
|
252
256
|
item[:fragment].respond_to?(:category) && !item[:fragment].respond_to?(:type)
|
|
253
257
|
when :code
|
|
@@ -272,6 +276,7 @@ module Aidp
|
|
|
272
276
|
over_budget: over_budget?,
|
|
273
277
|
by_type: {
|
|
274
278
|
style_guide: fragments_by_type(:style_guide).count,
|
|
279
|
+
project_knowledge: fragments_by_type(:project_knowledge).count,
|
|
275
280
|
templates: fragments_by_type(:template).count,
|
|
276
281
|
code: fragments_by_type(:code).count
|
|
277
282
|
}
|
|
@@ -6,6 +6,7 @@ require_relative "source_code_fragmenter"
|
|
|
6
6
|
require_relative "relevance_scorer"
|
|
7
7
|
require_relative "context_composer"
|
|
8
8
|
require_relative "prompt_builder"
|
|
9
|
+
require_relative "project_knowledge_indexer"
|
|
9
10
|
|
|
10
11
|
module Aidp
|
|
11
12
|
module PromptOptimization
|
|
@@ -38,6 +39,7 @@ module Aidp
|
|
|
38
39
|
@style_guide_indexer = nil
|
|
39
40
|
@template_indexer = nil
|
|
40
41
|
@fragmenter = nil
|
|
42
|
+
@project_knowledge_indexer = nil
|
|
41
43
|
@scorer = nil
|
|
42
44
|
@composer = nil
|
|
43
45
|
@builder = nil
|
|
@@ -100,6 +102,7 @@ module Aidp
|
|
|
100
102
|
@style_guide_indexer = nil
|
|
101
103
|
@template_indexer = nil
|
|
102
104
|
@fragmenter = nil
|
|
105
|
+
@project_knowledge_indexer = nil
|
|
103
106
|
@stats.reset!
|
|
104
107
|
end
|
|
105
108
|
|
|
@@ -127,6 +130,10 @@ module Aidp
|
|
|
127
130
|
template_indexer.index!
|
|
128
131
|
fragments.concat(template_indexer.templates)
|
|
129
132
|
|
|
133
|
+
# Project knowledge fragments
|
|
134
|
+
project_knowledge_indexer.index!
|
|
135
|
+
fragments.concat(project_knowledge_indexer.fragments)
|
|
136
|
+
|
|
130
137
|
# Source code fragments (only for affected files)
|
|
131
138
|
if affected_files && !affected_files.empty?
|
|
132
139
|
code_fragments = fragmenter.fragment_files(affected_files)
|
|
@@ -180,6 +187,10 @@ module Aidp
|
|
|
180
187
|
@fragmenter ||= SourceCodeFragmenter.new(project_dir: @project_dir)
|
|
181
188
|
end
|
|
182
189
|
|
|
190
|
+
def project_knowledge_indexer
|
|
191
|
+
@project_knowledge_indexer ||= ProjectKnowledgeIndexer.new(project_dir: @project_dir)
|
|
192
|
+
end
|
|
193
|
+
|
|
183
194
|
# Get or create relevance scorer (cached)
|
|
184
195
|
def scorer
|
|
185
196
|
@scorer ||= RelevanceScorer.new
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module PromptOptimization
|
|
5
|
+
# Indexes concise feature and tool notes stored under docs/
|
|
6
|
+
#
|
|
7
|
+
# These notes use inline path markers like <feature_id:lib/file.rb> so
|
|
8
|
+
# they can be found quickly by ripgrep and loaded for matching files.
|
|
9
|
+
class ProjectKnowledgeIndexer
|
|
10
|
+
KNOWLEDGE_DIRS = {
|
|
11
|
+
feature: File.join("docs", "features"),
|
|
12
|
+
tool: File.join("docs", "tools")
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
MARKER_PATTERN = /<([a-z0-9_]+):([^>\n]+)>/
|
|
16
|
+
|
|
17
|
+
attr_reader :fragments, :project_dir
|
|
18
|
+
|
|
19
|
+
def initialize(project_dir:)
|
|
20
|
+
@project_dir = project_dir
|
|
21
|
+
@fragments = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def index!
|
|
25
|
+
@fragments = []
|
|
26
|
+
|
|
27
|
+
KNOWLEDGE_DIRS.each do |note_type, relative_dir|
|
|
28
|
+
index_directory(note_type, File.join(project_dir, relative_dir))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@fragments
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def index_directory(note_type, directory)
|
|
37
|
+
return unless Dir.exist?(directory)
|
|
38
|
+
|
|
39
|
+
Dir.glob(File.join(directory, "**", "*.md")).sort.each do |file_path|
|
|
40
|
+
@fragments << parse_file(file_path, note_type)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_file(file_path, note_type)
|
|
45
|
+
content = File.read(file_path, encoding: "UTF-8")
|
|
46
|
+
title = extract_title(content, file_path)
|
|
47
|
+
markers = content.scan(MARKER_PATTERN)
|
|
48
|
+
note_id = markers.first&.first || File.basename(file_path, ".md")
|
|
49
|
+
|
|
50
|
+
ProjectKnowledgeFragment.new(
|
|
51
|
+
id: note_id,
|
|
52
|
+
title: title,
|
|
53
|
+
note_type: note_type,
|
|
54
|
+
file_path: file_path,
|
|
55
|
+
content: content,
|
|
56
|
+
linked_paths: markers.map { |(_, path)| path.strip }.uniq,
|
|
57
|
+
tags: extract_tags(note_type, content)
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def extract_title(content, file_path)
|
|
62
|
+
heading = content.lines.find { |line| line.start_with?("# ") }
|
|
63
|
+
return heading.sub(/^#\s+/, "").strip if heading
|
|
64
|
+
|
|
65
|
+
File.basename(file_path, ".md").split("_").map(&:capitalize).join(" ")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def extract_tags(note_type, content)
|
|
69
|
+
text = content.downcase
|
|
70
|
+
tags = [note_type.to_s, "project_knowledge"]
|
|
71
|
+
tags << "testing" if text.match?(/rspec|rubocop|test|lint|spec/)
|
|
72
|
+
tags << "documentation" if text.match?(/doc|guide|markdown/)
|
|
73
|
+
tags << "implementation" if text.match?(/implement|feature|behavior/)
|
|
74
|
+
tags << "filesystem" if text.match?(/file|path|directory|workspace/)
|
|
75
|
+
tags << "external_service" if text.match?(/api|provider|service|github|slack/)
|
|
76
|
+
tags.uniq
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class ProjectKnowledgeFragment
|
|
81
|
+
attr_reader :id, :title, :note_type, :file_path, :content, :linked_paths, :tags
|
|
82
|
+
|
|
83
|
+
def initialize(id:, title:, note_type:, file_path:, content:, linked_paths:, tags:)
|
|
84
|
+
@id = id
|
|
85
|
+
@title = title
|
|
86
|
+
@note_type = note_type
|
|
87
|
+
@file_path = file_path
|
|
88
|
+
@content = content
|
|
89
|
+
@linked_paths = linked_paths
|
|
90
|
+
@tags = tags
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def estimated_tokens
|
|
94
|
+
(content.length / 4.0).ceil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -31,11 +31,13 @@ module Aidp
|
|
|
31
31
|
|
|
32
32
|
# Group fragments by type
|
|
33
33
|
style_guide_fragments = composition_result.fragments_by_type(:style_guide)
|
|
34
|
+
project_knowledge_fragments = composition_result.fragments_by_type(:project_knowledge)
|
|
34
35
|
template_fragments = composition_result.fragments_by_type(:template)
|
|
35
36
|
code_fragments = composition_result.fragments_by_type(:code)
|
|
36
37
|
|
|
37
38
|
# Add relevant sections if they have content
|
|
38
39
|
sections << build_style_guide_section(style_guide_fragments) unless style_guide_fragments.empty?
|
|
40
|
+
sections << build_project_knowledge_section(project_knowledge_fragments) unless project_knowledge_fragments.empty?
|
|
39
41
|
sections << build_template_section(template_fragments) unless template_fragments.empty?
|
|
40
42
|
sections << build_code_section(code_fragments) unless code_fragments.empty?
|
|
41
43
|
|
|
@@ -139,6 +141,25 @@ module Aidp
|
|
|
139
141
|
lines.join("\n")
|
|
140
142
|
end
|
|
141
143
|
|
|
144
|
+
def build_project_knowledge_section(fragments)
|
|
145
|
+
lines = ["# Project Knowledge"]
|
|
146
|
+
lines << ""
|
|
147
|
+
lines << "Concise notes associated with the affected files:"
|
|
148
|
+
lines << ""
|
|
149
|
+
|
|
150
|
+
fragments.each do |item|
|
|
151
|
+
fragment = item[:fragment]
|
|
152
|
+
lines << "## #{fragment.title}"
|
|
153
|
+
lines << ""
|
|
154
|
+
lines << "**Type**: #{fragment.note_type}"
|
|
155
|
+
lines << ""
|
|
156
|
+
lines << fragment.content
|
|
157
|
+
lines << ""
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
lines.join("\n")
|
|
161
|
+
end
|
|
162
|
+
|
|
142
163
|
# Build code context section
|
|
143
164
|
#
|
|
144
165
|
# @param fragments [Array<Hash>] Code fragments
|
|
@@ -198,6 +219,7 @@ module Aidp
|
|
|
198
219
|
lines << "## Fragments by Type"
|
|
199
220
|
lines << ""
|
|
200
221
|
lines << "- **Style Guide Sections**: #{summary[:by_type][:style_guide]}"
|
|
222
|
+
lines << "- **Project Knowledge Notes**: #{summary[:by_type][:project_knowledge]}"
|
|
201
223
|
lines << "- **Templates**: #{summary[:by_type][:templates]}"
|
|
202
224
|
lines << "- **Code Fragments**: #{summary[:by_type][:code]}"
|
|
203
225
|
lines << ""
|
|
@@ -105,6 +105,13 @@ module Aidp
|
|
|
105
105
|
def score_file_location_match(fragment, context)
|
|
106
106
|
return 0.5 unless context.affected_files && !context.affected_files.empty?
|
|
107
107
|
|
|
108
|
+
if fragment.respond_to?(:linked_paths) && fragment.linked_paths.any?
|
|
109
|
+
matches_affected_file = context.affected_files.any? do |affected_file|
|
|
110
|
+
knowledge_path_match?(fragment.linked_paths, affected_file)
|
|
111
|
+
end
|
|
112
|
+
return matches_affected_file ? 1.0 : 0.1
|
|
113
|
+
end
|
|
114
|
+
|
|
108
115
|
# Only code fragments have file_path
|
|
109
116
|
return 0.5 unless fragment.respond_to?(:file_path)
|
|
110
117
|
|
|
@@ -114,6 +121,16 @@ module Aidp
|
|
|
114
121
|
end) ? 1.0 : 0.1
|
|
115
122
|
end
|
|
116
123
|
|
|
124
|
+
def knowledge_path_match?(linked_paths, affected_file)
|
|
125
|
+
linked_paths.any? do |linked_path|
|
|
126
|
+
normalized_link = linked_path.sub(%r{\A\./}, "")
|
|
127
|
+
normalized_file = affected_file.to_s.sub(%r{\A\./}, "")
|
|
128
|
+
normalized_file == normalized_link ||
|
|
129
|
+
normalized_file.start_with?("#{normalized_link}/") ||
|
|
130
|
+
normalized_link.start_with?("#{normalized_file}/")
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
117
134
|
# Score based on work loop step
|
|
118
135
|
#
|
|
119
136
|
# @param fragment [Fragment] Fragment to score
|
data/lib/aidp/version.rb
CHANGED
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.42.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -407,6 +407,7 @@ files:
|
|
|
407
407
|
- lib/aidp/execute/interactive_repl.rb
|
|
408
408
|
- lib/aidp/execute/persistent_tasklist.rb
|
|
409
409
|
- lib/aidp/execute/progress.rb
|
|
410
|
+
- lib/aidp/execute/project_knowledge_manager.rb
|
|
410
411
|
- lib/aidp/execute/prompt_evaluator.rb
|
|
411
412
|
- lib/aidp/execute/prompt_manager.rb
|
|
412
413
|
- lib/aidp/execute/repl_macros.rb
|
|
@@ -526,6 +527,7 @@ files:
|
|
|
526
527
|
- lib/aidp/pr_worktree_manager.rb
|
|
527
528
|
- lib/aidp/prompt_optimization/context_composer.rb
|
|
528
529
|
- lib/aidp/prompt_optimization/optimizer.rb
|
|
530
|
+
- lib/aidp/prompt_optimization/project_knowledge_indexer.rb
|
|
529
531
|
- lib/aidp/prompt_optimization/prompt_builder.rb
|
|
530
532
|
- lib/aidp/prompt_optimization/relevance_scorer.rb
|
|
531
533
|
- lib/aidp/prompt_optimization/source_code_fragmenter.rb
|