aidp 0.27.0 → 0.28.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/README.md +89 -0
- data/lib/aidp/cli/models_command.rb +5 -6
- data/lib/aidp/cli.rb +10 -8
- data/lib/aidp/config.rb +54 -0
- data/lib/aidp/debug_mixin.rb +23 -1
- data/lib/aidp/execute/agent_signal_parser.rb +22 -0
- data/lib/aidp/execute/repl_macros.rb +2 -2
- data/lib/aidp/execute/steps.rb +94 -1
- data/lib/aidp/execute/work_loop_runner.rb +209 -17
- data/lib/aidp/execute/workflow_selector.rb +2 -25
- data/lib/aidp/firewall/provider_requirements_collector.rb +262 -0
- data/lib/aidp/harness/ai_decision_engine.rb +35 -2
- data/lib/aidp/harness/config_manager.rb +0 -5
- data/lib/aidp/harness/config_schema.rb +8 -0
- data/lib/aidp/harness/configuration.rb +27 -19
- data/lib/aidp/harness/enhanced_runner.rb +1 -4
- data/lib/aidp/harness/error_handler.rb +1 -72
- data/lib/aidp/harness/provider_factory.rb +11 -2
- data/lib/aidp/harness/state_manager.rb +0 -7
- data/lib/aidp/harness/thinking_depth_manager.rb +47 -68
- data/lib/aidp/harness/ui/enhanced_tui.rb +8 -18
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +0 -18
- data/lib/aidp/harness/ui/progress_display.rb +6 -2
- data/lib/aidp/harness/user_interface.rb +0 -58
- data/lib/aidp/init/runner.rb +7 -2
- data/lib/aidp/planning/analyzers/feedback_analyzer.rb +365 -0
- data/lib/aidp/planning/builders/agile_plan_builder.rb +387 -0
- data/lib/aidp/planning/builders/project_plan_builder.rb +193 -0
- data/lib/aidp/planning/generators/gantt_generator.rb +190 -0
- data/lib/aidp/planning/generators/iteration_plan_generator.rb +392 -0
- data/lib/aidp/planning/generators/legacy_research_planner.rb +473 -0
- data/lib/aidp/planning/generators/marketing_report_generator.rb +348 -0
- data/lib/aidp/planning/generators/mvp_scope_generator.rb +310 -0
- data/lib/aidp/planning/generators/user_test_plan_generator.rb +373 -0
- data/lib/aidp/planning/generators/wbs_generator.rb +259 -0
- data/lib/aidp/planning/mappers/persona_mapper.rb +163 -0
- data/lib/aidp/planning/parsers/document_parser.rb +141 -0
- data/lib/aidp/planning/parsers/feedback_data_parser.rb +252 -0
- data/lib/aidp/provider_manager.rb +8 -32
- data/lib/aidp/providers/aider.rb +264 -0
- data/lib/aidp/providers/anthropic.rb +74 -2
- data/lib/aidp/providers/base.rb +25 -1
- data/lib/aidp/providers/codex.rb +26 -3
- data/lib/aidp/providers/cursor.rb +16 -0
- data/lib/aidp/providers/gemini.rb +13 -0
- data/lib/aidp/providers/github_copilot.rb +17 -0
- data/lib/aidp/providers/kilocode.rb +11 -0
- data/lib/aidp/providers/opencode.rb +11 -0
- data/lib/aidp/setup/wizard.rb +249 -39
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +211 -30
- data/lib/aidp/watch/change_request_processor.rb +128 -14
- data/lib/aidp/watch/ci_fix_processor.rb +103 -37
- data/lib/aidp/watch/ci_log_extractor.rb +258 -0
- data/lib/aidp/watch/github_state_extractor.rb +177 -0
- data/lib/aidp/watch/implementation_verifier.rb +284 -0
- data/lib/aidp/watch/plan_generator.rb +7 -43
- data/lib/aidp/watch/plan_processor.rb +7 -6
- data/lib/aidp/watch/repository_client.rb +245 -17
- data/lib/aidp/watch/review_processor.rb +98 -17
- data/lib/aidp/watch/reviewers/base_reviewer.rb +1 -1
- data/lib/aidp/watch/runner.rb +181 -29
- data/lib/aidp/watch/state_store.rb +22 -1
- data/lib/aidp/workflows/definitions.rb +147 -0
- data/lib/aidp/workstream_cleanup.rb +245 -0
- data/lib/aidp/worktree.rb +19 -0
- data/templates/aidp.yml.example +57 -0
- data/templates/implementation/generate_tdd_specs.md +213 -0
- data/templates/implementation/iterative_implementation.md +122 -0
- data/templates/planning/agile/analyze_feedback.md +183 -0
- data/templates/planning/agile/generate_iteration_plan.md +179 -0
- data/templates/planning/agile/generate_legacy_research_plan.md +171 -0
- data/templates/planning/agile/generate_marketing_report.md +162 -0
- data/templates/planning/agile/generate_mvp_scope.md +127 -0
- data/templates/planning/agile/generate_user_test_plan.md +143 -0
- data/templates/planning/agile/ingest_feedback.md +174 -0
- data/templates/planning/assemble_project_plan.md +113 -0
- data/templates/planning/assign_personas.md +108 -0
- data/templates/planning/create_tasks.md +52 -6
- data/templates/planning/generate_gantt.md +86 -0
- data/templates/planning/generate_wbs.md +85 -0
- data/templates/planning/initialize_planning_mode.md +70 -0
- data/templates/skills/README.md +2 -2
- data/templates/skills/marketing_strategist/SKILL.md +279 -0
- data/templates/skills/product_manager/SKILL.md +177 -0
- data/templates/skills/ruby_aidp_planning/SKILL.md +497 -0
- data/templates/skills/ruby_rspec_tdd/SKILL.md +514 -0
- data/templates/skills/ux_researcher/SKILL.md +222 -0
- metadata +39 -1
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../logger"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Planning
|
|
8
|
+
module Mappers
|
|
9
|
+
# Maps tasks to personas using Zero Framework Cognition (ZFC)
|
|
10
|
+
# NO heuristics, NO regex, NO keyword matching - pure AI decision making
|
|
11
|
+
class PersonaMapper
|
|
12
|
+
def initialize(ai_decision_engine:, config: nil, mode: :waterfall)
|
|
13
|
+
@ai_decision_engine = ai_decision_engine
|
|
14
|
+
@config = config
|
|
15
|
+
@mode = mode
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Assign personas to tasks using ZFC
|
|
19
|
+
# @param task_list [Array<Hash>] List of tasks to assign
|
|
20
|
+
# @param available_personas [Array<String>] Available persona names
|
|
21
|
+
# @return [Hash] Persona assignments
|
|
22
|
+
def assign_personas(task_list, available_personas: nil)
|
|
23
|
+
Aidp.log_debug("persona_mapper", "assign_personas", task_count: task_list.size)
|
|
24
|
+
|
|
25
|
+
available_personas ||= default_personas
|
|
26
|
+
|
|
27
|
+
assignments = {}
|
|
28
|
+
|
|
29
|
+
task_list.each do |task|
|
|
30
|
+
persona = assign_task_to_persona(task, available_personas)
|
|
31
|
+
assignments[task[:id] || task[:name]] = {
|
|
32
|
+
persona: persona,
|
|
33
|
+
task: task[:name],
|
|
34
|
+
phase: task[:phase],
|
|
35
|
+
rationale: "AI-determined based on task characteristics"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Aidp.log_debug("persona_mapper", "assigned", task: task[:name], persona: persona)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
{
|
|
42
|
+
assignments: assignments,
|
|
43
|
+
metadata: {
|
|
44
|
+
generated_at: Time.now.iso8601,
|
|
45
|
+
total_assignments: assignments.size,
|
|
46
|
+
personas_used: assignments.values.map { |a| a[:persona] }.uniq
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Generate persona_map.yml configuration
|
|
52
|
+
# @param assignments [Hash] Persona assignments
|
|
53
|
+
# @return [String] YAML configuration
|
|
54
|
+
def generate_persona_map(assignments)
|
|
55
|
+
Aidp.log_debug("persona_mapper", "generate_persona_map")
|
|
56
|
+
|
|
57
|
+
config = {
|
|
58
|
+
"version" => "1.0",
|
|
59
|
+
"generated_at" => Time.now.iso8601,
|
|
60
|
+
"assignments" => format_assignments_for_yaml(assignments[:assignments])
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
YAML.dump(config)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Assign a single task to the best persona using AI decision engine
|
|
69
|
+
# This is the ZFC pattern - meaning/decisions go to AI, not code
|
|
70
|
+
def assign_task_to_persona(task, available_personas)
|
|
71
|
+
Aidp.log_debug("persona_mapper", "assign_task", task: task[:name])
|
|
72
|
+
|
|
73
|
+
# Use AI decision engine to determine best persona
|
|
74
|
+
# NO regex, NO keyword matching, NO heuristics!
|
|
75
|
+
decision = @ai_decision_engine.decide(
|
|
76
|
+
context: "persona assignment",
|
|
77
|
+
prompt: build_assignment_prompt(task, available_personas),
|
|
78
|
+
data: {
|
|
79
|
+
task_name: task[:name],
|
|
80
|
+
task_description: task[:description],
|
|
81
|
+
task_phase: task[:phase],
|
|
82
|
+
task_effort: task[:effort],
|
|
83
|
+
available_personas: available_personas
|
|
84
|
+
},
|
|
85
|
+
schema: {
|
|
86
|
+
type: "string",
|
|
87
|
+
enum: available_personas
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
Aidp.log_debug("persona_mapper", "ai_decision", persona: decision)
|
|
92
|
+
decision
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_assignment_prompt(task, personas)
|
|
96
|
+
<<~PROMPT
|
|
97
|
+
Assign this task to the most appropriate persona based on the task characteristics.
|
|
98
|
+
|
|
99
|
+
Task: #{task[:name]}
|
|
100
|
+
Description: #{task[:description]}
|
|
101
|
+
Phase: #{task[:phase]}
|
|
102
|
+
Effort: #{task[:effort]}
|
|
103
|
+
|
|
104
|
+
Available Personas: #{personas.join(", ")}
|
|
105
|
+
|
|
106
|
+
Consider:
|
|
107
|
+
- Task type and complexity
|
|
108
|
+
- Required skills and expertise
|
|
109
|
+
- Phase of project (requirements, design, implementation, etc.)
|
|
110
|
+
- Technical vs. product focus
|
|
111
|
+
|
|
112
|
+
Return ONLY the persona name, nothing else.
|
|
113
|
+
PROMPT
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def default_personas
|
|
117
|
+
case @mode
|
|
118
|
+
when :agile
|
|
119
|
+
agile_personas
|
|
120
|
+
when :waterfall
|
|
121
|
+
waterfall_personas
|
|
122
|
+
else
|
|
123
|
+
waterfall_personas # Default to waterfall
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def waterfall_personas
|
|
128
|
+
[
|
|
129
|
+
"product_strategist",
|
|
130
|
+
"architect",
|
|
131
|
+
"senior_developer",
|
|
132
|
+
"qa_engineer",
|
|
133
|
+
"devops_engineer",
|
|
134
|
+
"tech_writer"
|
|
135
|
+
]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def agile_personas
|
|
139
|
+
[
|
|
140
|
+
"product_manager",
|
|
141
|
+
"ux_researcher",
|
|
142
|
+
"architect",
|
|
143
|
+
"senior_developer",
|
|
144
|
+
"qa_engineer",
|
|
145
|
+
"devops_engineer",
|
|
146
|
+
"tech_writer",
|
|
147
|
+
"marketing_strategist"
|
|
148
|
+
]
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def format_assignments_for_yaml(assignments)
|
|
152
|
+
assignments.transform_values do |assignment|
|
|
153
|
+
{
|
|
154
|
+
"persona" => assignment[:persona],
|
|
155
|
+
"task" => assignment[:task],
|
|
156
|
+
"phase" => assignment[:phase]
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../logger"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Planning
|
|
7
|
+
module Parsers
|
|
8
|
+
# Parses existing documentation files to extract structured information
|
|
9
|
+
# Uses Zero Framework Cognition (ZFC) for semantic analysis
|
|
10
|
+
class DocumentParser
|
|
11
|
+
def initialize(ai_decision_engine: nil)
|
|
12
|
+
@ai_decision_engine = ai_decision_engine
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Parse a single file and detect its structure
|
|
16
|
+
# @param file_path [String] Path to the markdown file
|
|
17
|
+
# @return [Hash] Parsed document with type and sections
|
|
18
|
+
def parse_file(file_path)
|
|
19
|
+
Aidp.log_debug("document_parser", "parse_file", path: file_path)
|
|
20
|
+
|
|
21
|
+
unless File.exist?(file_path)
|
|
22
|
+
Aidp.log_error("document_parser", "file_not_found", path: file_path)
|
|
23
|
+
raise ArgumentError, "File not found: #{file_path}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
content = File.read(file_path)
|
|
27
|
+
Aidp.log_debug("document_parser", "read_content", size: content.length)
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
path: file_path,
|
|
31
|
+
type: detect_document_type(content),
|
|
32
|
+
sections: extract_sections(content),
|
|
33
|
+
raw_content: content
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Parse all markdown files in a directory
|
|
38
|
+
# @param dir_path [String] Directory path
|
|
39
|
+
# @return [Array<Hash>] Array of parsed documents
|
|
40
|
+
def parse_directory(dir_path)
|
|
41
|
+
Aidp.log_debug("document_parser", "parse_directory", path: dir_path)
|
|
42
|
+
|
|
43
|
+
unless Dir.exist?(dir_path)
|
|
44
|
+
Aidp.log_error("document_parser", "directory_not_found", path: dir_path)
|
|
45
|
+
raise ArgumentError, "Directory not found: #{dir_path}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
markdown_files = Dir.glob(File.join(dir_path, "**", "*.md"))
|
|
49
|
+
Aidp.log_debug("document_parser", "found_files", count: markdown_files.size)
|
|
50
|
+
|
|
51
|
+
markdown_files.map { |file| parse_file(file) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Detect document type using ZFC (AI decision engine)
|
|
57
|
+
# Returns :prd, :design, :adr, :task_list, or :unknown
|
|
58
|
+
def detect_document_type(content)
|
|
59
|
+
Aidp.log_debug("document_parser", "detect_document_type")
|
|
60
|
+
|
|
61
|
+
# Use AI decision engine if available (ZFC pattern)
|
|
62
|
+
if @ai_decision_engine
|
|
63
|
+
decision = @ai_decision_engine.decide(
|
|
64
|
+
context: "document classification",
|
|
65
|
+
prompt: "Classify this document as PRD, technical design, ADR, task list, or unknown",
|
|
66
|
+
data: {content: content.slice(0, 2000)}, # First 2000 chars
|
|
67
|
+
schema: {
|
|
68
|
+
type: "string",
|
|
69
|
+
enum: ["prd", "design", "adr", "task_list", "unknown"]
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
Aidp.log_debug("document_parser", "ai_classification", type: decision)
|
|
73
|
+
return decision.to_sym
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Fallback: simple heuristics when AI not available
|
|
77
|
+
# This is acceptable as fallback, but ZFC is preferred
|
|
78
|
+
type = classify_by_heuristics(content)
|
|
79
|
+
Aidp.log_debug("document_parser", "heuristic_classification", type: type)
|
|
80
|
+
type
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Fallback classification using basic heuristics
|
|
84
|
+
def classify_by_heuristics(content)
|
|
85
|
+
lower_content = content.downcase
|
|
86
|
+
|
|
87
|
+
return :prd if lower_content.include?("product requirements") ||
|
|
88
|
+
lower_content.include?("user stories") ||
|
|
89
|
+
lower_content.include?("success criteria")
|
|
90
|
+
|
|
91
|
+
return :design if lower_content.include?("technical design") ||
|
|
92
|
+
lower_content.include?("system architecture") ||
|
|
93
|
+
lower_content.include?("component design")
|
|
94
|
+
|
|
95
|
+
return :adr if lower_content.include?("decision record") ||
|
|
96
|
+
lower_content.include?("adr") ||
|
|
97
|
+
lower_content.match?(/##?\s+status/i)
|
|
98
|
+
|
|
99
|
+
return :task_list if lower_content.include?("task list") ||
|
|
100
|
+
lower_content.include?("- [ ]") ||
|
|
101
|
+
lower_content.match?(/\d+\.\s+\[[ x]\]/i)
|
|
102
|
+
|
|
103
|
+
:unknown
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Extract markdown sections from content
|
|
107
|
+
# Returns hash of section_name => section_content
|
|
108
|
+
def extract_sections(content)
|
|
109
|
+
Aidp.log_debug("document_parser", "extract_sections")
|
|
110
|
+
|
|
111
|
+
sections = {}
|
|
112
|
+
current_section = nil
|
|
113
|
+
current_content = []
|
|
114
|
+
|
|
115
|
+
content.each_line do |line|
|
|
116
|
+
if line.match?(/^##?\s+(.+)/)
|
|
117
|
+
# Save previous section
|
|
118
|
+
if current_section
|
|
119
|
+
sections[current_section] = current_content.join.strip
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Start new section
|
|
123
|
+
current_section = line.match(/^##?\s+(.+)/)[1].strip.downcase.gsub(/\s+/, "_")
|
|
124
|
+
current_content = []
|
|
125
|
+
elsif current_section
|
|
126
|
+
current_content << line
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Save last section
|
|
131
|
+
if current_section
|
|
132
|
+
sections[current_section] = current_content.join.strip
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Aidp.log_debug("document_parser", "extracted_sections", count: sections.size)
|
|
136
|
+
sections
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../../logger"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module Planning
|
|
9
|
+
module Parsers
|
|
10
|
+
# Parse feedback data from multiple formats (CSV, JSON, markdown)
|
|
11
|
+
# Normalizes data into consistent structure for analysis
|
|
12
|
+
class FeedbackDataParser
|
|
13
|
+
class FeedbackParseError < StandardError; end
|
|
14
|
+
|
|
15
|
+
def initialize(file_path:)
|
|
16
|
+
@file_path = file_path
|
|
17
|
+
@format = detect_format
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Parse feedback file and return normalized structure
|
|
21
|
+
# @return [Hash] Normalized feedback data
|
|
22
|
+
def parse
|
|
23
|
+
Aidp.log_debug("feedback_data_parser", "parsing", file: @file_path, format: @format)
|
|
24
|
+
|
|
25
|
+
case @format
|
|
26
|
+
when :csv
|
|
27
|
+
parse_csv
|
|
28
|
+
when :json
|
|
29
|
+
parse_json
|
|
30
|
+
when :markdown
|
|
31
|
+
parse_markdown
|
|
32
|
+
else
|
|
33
|
+
raise FeedbackParseError, "Unsupported format: #{@format}"
|
|
34
|
+
end
|
|
35
|
+
rescue => e
|
|
36
|
+
Aidp.log_error("feedback_data_parser", "parse_failed", error: e.message, file: @file_path)
|
|
37
|
+
raise FeedbackParseError, "Failed to parse feedback file: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def detect_format
|
|
43
|
+
ext = File.extname(@file_path).downcase
|
|
44
|
+
|
|
45
|
+
case ext
|
|
46
|
+
when ".csv"
|
|
47
|
+
:csv
|
|
48
|
+
when ".json"
|
|
49
|
+
:json
|
|
50
|
+
when ".md", ".markdown"
|
|
51
|
+
:markdown
|
|
52
|
+
else
|
|
53
|
+
raise FeedbackParseError, "Unknown file extension: #{ext}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_csv
|
|
58
|
+
Aidp.log_debug("feedback_data_parser", "parsing_csv")
|
|
59
|
+
|
|
60
|
+
unless File.exist?(@file_path)
|
|
61
|
+
raise FeedbackParseError, "File not found: #{@file_path}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
rows = CSV.read(@file_path, headers: true)
|
|
65
|
+
responses = rows.map { |row| normalize_csv_row(row) }
|
|
66
|
+
|
|
67
|
+
{
|
|
68
|
+
format: :csv,
|
|
69
|
+
source_file: @file_path,
|
|
70
|
+
parsed_at: Time.now.iso8601,
|
|
71
|
+
response_count: responses.size,
|
|
72
|
+
responses: responses,
|
|
73
|
+
metadata: extract_csv_metadata(rows)
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_json
|
|
78
|
+
Aidp.log_debug("feedback_data_parser", "parsing_json")
|
|
79
|
+
|
|
80
|
+
unless File.exist?(@file_path)
|
|
81
|
+
raise FeedbackParseError, "File not found: #{@file_path}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
data = JSON.parse(File.read(@file_path))
|
|
85
|
+
|
|
86
|
+
# Support both array of responses and object with responses key
|
|
87
|
+
responses = if data.is_a?(Array)
|
|
88
|
+
data
|
|
89
|
+
elsif data.is_a?(Hash) && data["responses"]
|
|
90
|
+
data["responses"]
|
|
91
|
+
else
|
|
92
|
+
raise FeedbackParseError, "Invalid JSON structure: expected array or {responses: [...]}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
normalized_responses = responses.map { |r| normalize_json_response(r) }
|
|
96
|
+
|
|
97
|
+
{
|
|
98
|
+
format: :json,
|
|
99
|
+
source_file: @file_path,
|
|
100
|
+
parsed_at: Time.now.iso8601,
|
|
101
|
+
response_count: normalized_responses.size,
|
|
102
|
+
responses: normalized_responses,
|
|
103
|
+
metadata: extract_json_metadata(data)
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parse_markdown
|
|
108
|
+
Aidp.log_debug("feedback_data_parser", "parsing_markdown")
|
|
109
|
+
|
|
110
|
+
unless File.exist?(@file_path)
|
|
111
|
+
raise FeedbackParseError, "File not found: #{@file_path}"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
content = File.read(@file_path)
|
|
115
|
+
responses = extract_markdown_responses(content)
|
|
116
|
+
|
|
117
|
+
{
|
|
118
|
+
format: :markdown,
|
|
119
|
+
source_file: @file_path,
|
|
120
|
+
parsed_at: Time.now.iso8601,
|
|
121
|
+
response_count: responses.size,
|
|
122
|
+
responses: responses,
|
|
123
|
+
metadata: extract_markdown_metadata(content)
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def normalize_csv_row(row)
|
|
128
|
+
{
|
|
129
|
+
respondent_id: row["id"] || row["respondent_id"] || row["user_id"],
|
|
130
|
+
timestamp: row["timestamp"] || row["date"] || row["submitted_at"],
|
|
131
|
+
rating: parse_rating(row["rating"] || row["score"]),
|
|
132
|
+
feedback_text: row["feedback"] || row["comments"] || row["response"],
|
|
133
|
+
feature: row["feature"] || row["area"],
|
|
134
|
+
sentiment: row["sentiment"],
|
|
135
|
+
tags: parse_tags(row["tags"]),
|
|
136
|
+
raw_data: row.to_h
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def normalize_json_response(response)
|
|
141
|
+
{
|
|
142
|
+
respondent_id: response["id"] || response["respondent_id"] || response["user_id"],
|
|
143
|
+
timestamp: response["timestamp"] || response["date"] || response["submitted_at"],
|
|
144
|
+
rating: parse_rating(response["rating"] || response["score"]),
|
|
145
|
+
feedback_text: response["feedback"] || response["comments"] || response["response"] || response["text"],
|
|
146
|
+
feature: response["feature"] || response["area"] || response["category"],
|
|
147
|
+
sentiment: response["sentiment"],
|
|
148
|
+
tags: parse_tags(response["tags"]),
|
|
149
|
+
raw_data: response
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def extract_markdown_responses(content)
|
|
154
|
+
# Simple markdown parser that looks for response sections
|
|
155
|
+
# Format: ## Response N or ### Respondent: ID
|
|
156
|
+
responses = []
|
|
157
|
+
current_response = nil
|
|
158
|
+
|
|
159
|
+
content.each_line do |line|
|
|
160
|
+
if line =~ /^##+ Response (\d+)/i || line =~ /^##+ Respondent:?\s*(.+)/i
|
|
161
|
+
responses << current_response if current_response
|
|
162
|
+
current_response = {text: "", metadata: {}}
|
|
163
|
+
elsif current_response
|
|
164
|
+
# Extract key-value pairs like **Rating:** 5
|
|
165
|
+
if line =~ /\*\*(.+?):\*\*\s*(.+)/
|
|
166
|
+
key = $1.downcase.strip
|
|
167
|
+
value = $2.strip
|
|
168
|
+
current_response[:metadata][key] = value
|
|
169
|
+
else
|
|
170
|
+
current_response[:text] += line
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
responses << current_response if current_response
|
|
176
|
+
|
|
177
|
+
responses.map do |resp|
|
|
178
|
+
{
|
|
179
|
+
respondent_id: resp[:metadata]["id"] || resp[:metadata]["respondent"],
|
|
180
|
+
timestamp: resp[:metadata]["timestamp"] || resp[:metadata]["date"],
|
|
181
|
+
rating: parse_rating(resp[:metadata]["rating"] || resp[:metadata]["score"]),
|
|
182
|
+
feedback_text: resp[:text].strip,
|
|
183
|
+
feature: resp[:metadata]["feature"] || resp[:metadata]["area"],
|
|
184
|
+
sentiment: resp[:metadata]["sentiment"],
|
|
185
|
+
tags: parse_tags(resp[:metadata]["tags"]),
|
|
186
|
+
raw_data: resp[:metadata]
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def parse_rating(value)
|
|
192
|
+
return nil if value.nil? || value.to_s.strip.empty?
|
|
193
|
+
|
|
194
|
+
# Handle numeric ratings, star ratings, etc.
|
|
195
|
+
if value.to_s =~ /^(\d+)(?:\/\d+)?$/
|
|
196
|
+
$1.to_i
|
|
197
|
+
elsif value.to_s =~ /^(\d+)\s*stars?$/i
|
|
198
|
+
$1.to_i
|
|
199
|
+
else
|
|
200
|
+
value.to_s
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def parse_tags(value)
|
|
205
|
+
return [] if value.nil? || value.to_s.strip.empty?
|
|
206
|
+
|
|
207
|
+
if value.is_a?(Array)
|
|
208
|
+
value
|
|
209
|
+
elsif value.is_a?(String)
|
|
210
|
+
value.split(/[,;]/).map(&:strip).reject(&:empty?)
|
|
211
|
+
else
|
|
212
|
+
[]
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def extract_csv_metadata(rows)
|
|
217
|
+
{
|
|
218
|
+
total_rows: rows.size,
|
|
219
|
+
columns: rows.headers,
|
|
220
|
+
has_timestamps: rows.headers.any? { |h| h&.match?(/timestamp|date/i) },
|
|
221
|
+
has_ratings: rows.headers.any? { |h| h&.match?(/rating|score/i) }
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def extract_json_metadata(data)
|
|
226
|
+
metadata = data.is_a?(Hash) ? data.except("responses") : {}
|
|
227
|
+
|
|
228
|
+
{
|
|
229
|
+
survey_name: metadata["survey_name"] || metadata["name"],
|
|
230
|
+
survey_id: metadata["survey_id"] || metadata["id"],
|
|
231
|
+
created_at: metadata["created_at"] || metadata["timestamp"],
|
|
232
|
+
additional_fields: metadata.keys - ["responses", "survey_name", "name", "id", "survey_id", "created_at", "timestamp"]
|
|
233
|
+
}
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def extract_markdown_metadata(content)
|
|
237
|
+
# Extract YAML front matter if present
|
|
238
|
+
if content =~ /^---\s*\n(.*?)\n---\s*\n/m
|
|
239
|
+
begin
|
|
240
|
+
YAML.safe_load($1, permitted_classes: [Date, Time, Symbol]) || {}
|
|
241
|
+
rescue => e
|
|
242
|
+
Aidp.log_debug("feedback_data_parser", "yaml_parse_failed", error: e.message)
|
|
243
|
+
{}
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
{}
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -7,15 +7,10 @@ module Aidp
|
|
|
7
7
|
class ProviderManager
|
|
8
8
|
class << self
|
|
9
9
|
def get_provider(provider_type, options = {})
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
factory = get_harness_factory
|
|
13
|
-
return factory.create_provider(provider_type, options) if factory
|
|
14
|
-
end
|
|
10
|
+
factory = get_harness_factory
|
|
11
|
+
raise "Harness factory not available" unless factory
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
prompt = options[:prompt] || TTY::Prompt.new
|
|
18
|
-
create_legacy_provider(provider_type, prompt: prompt)
|
|
13
|
+
factory.create_provider(provider_type, options)
|
|
19
14
|
end
|
|
20
15
|
|
|
21
16
|
def load_from_config(config = {}, options = {})
|
|
@@ -62,7 +57,7 @@ module Aidp
|
|
|
62
57
|
factory = get_harness_factory
|
|
63
58
|
return [] unless factory
|
|
64
59
|
|
|
65
|
-
enabled_names = factory.
|
|
60
|
+
enabled_names = factory.enabled_providers(options)
|
|
66
61
|
factory.create_providers(enabled_names, options)
|
|
67
62
|
end
|
|
68
63
|
|
|
@@ -71,7 +66,7 @@ module Aidp
|
|
|
71
66
|
factory = get_harness_factory
|
|
72
67
|
return false unless factory
|
|
73
68
|
|
|
74
|
-
factory.
|
|
69
|
+
factory.configured_providers(options).include?(provider_name.to_s)
|
|
75
70
|
end
|
|
76
71
|
|
|
77
72
|
# Check if provider is enabled
|
|
@@ -79,7 +74,7 @@ module Aidp
|
|
|
79
74
|
factory = get_harness_factory
|
|
80
75
|
return false unless factory
|
|
81
76
|
|
|
82
|
-
factory.
|
|
77
|
+
factory.enabled_providers(options).include?(provider_name.to_s)
|
|
83
78
|
end
|
|
84
79
|
|
|
85
80
|
# Get provider capabilities
|
|
@@ -87,7 +82,7 @@ module Aidp
|
|
|
87
82
|
factory = get_harness_factory
|
|
88
83
|
return [] unless factory
|
|
89
84
|
|
|
90
|
-
factory.
|
|
85
|
+
factory.provider_capabilities(provider_name, options)
|
|
91
86
|
end
|
|
92
87
|
|
|
93
88
|
# Check if provider supports feature
|
|
@@ -103,7 +98,7 @@ module Aidp
|
|
|
103
98
|
factory = get_harness_factory
|
|
104
99
|
return [] unless factory
|
|
105
100
|
|
|
106
|
-
factory.
|
|
101
|
+
factory.provider_models(provider_name, options)
|
|
107
102
|
end
|
|
108
103
|
|
|
109
104
|
# Validate provider configuration
|
|
@@ -131,25 +126,6 @@ module Aidp
|
|
|
131
126
|
def reload_config
|
|
132
127
|
@harness_factory&.reload_config
|
|
133
128
|
end
|
|
134
|
-
|
|
135
|
-
private
|
|
136
|
-
|
|
137
|
-
def create_legacy_provider(provider_type, prompt: TTY::Prompt.new)
|
|
138
|
-
case provider_type
|
|
139
|
-
when "cursor"
|
|
140
|
-
Aidp::Providers::Cursor.new(prompt: prompt)
|
|
141
|
-
when "anthropic", "claude"
|
|
142
|
-
Aidp::Providers::Anthropic.new(prompt: prompt)
|
|
143
|
-
when "gemini"
|
|
144
|
-
Aidp::Providers::Gemini.new(prompt: prompt)
|
|
145
|
-
when "kilocode"
|
|
146
|
-
Aidp::Providers::Kilocode.new(prompt: prompt)
|
|
147
|
-
when "github_copilot"
|
|
148
|
-
Aidp::Providers::GithubCopilot.new(prompt: prompt)
|
|
149
|
-
when "codex"
|
|
150
|
-
Aidp::Providers::Codex.new(prompt: prompt)
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
129
|
end
|
|
154
130
|
end
|
|
155
131
|
end
|