aidp 0.28.0 → 0.29.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/cli/models_command.rb +75 -117
- data/lib/aidp/cli/tools_command.rb +333 -0
- data/lib/aidp/cli.rb +8 -1
- data/lib/aidp/config.rb +9 -0
- data/lib/aidp/execute/work_loop_runner.rb +6 -3
- data/lib/aidp/harness/capability_registry.rb +4 -4
- data/lib/aidp/harness/provider_manager.rb +5 -3
- data/lib/aidp/harness/ruby_llm_registry.rb +239 -0
- data/lib/aidp/metadata/cache.rb +201 -0
- data/lib/aidp/metadata/compiler.rb +229 -0
- data/lib/aidp/metadata/parser.rb +204 -0
- data/lib/aidp/metadata/query.rb +237 -0
- data/lib/aidp/metadata/scanner.rb +191 -0
- data/lib/aidp/metadata/tool_metadata.rb +245 -0
- data/lib/aidp/metadata/validator.rb +187 -0
- data/lib/aidp/providers/aider.rb +1 -1
- data/lib/aidp/providers/anthropic.rb +6 -4
- data/lib/aidp/providers/base.rb +105 -35
- data/lib/aidp/providers/codex.rb +1 -1
- data/lib/aidp/providers/cursor.rb +1 -1
- data/lib/aidp/providers/gemini.rb +1 -1
- data/lib/aidp/providers/github_copilot.rb +1 -1
- data/lib/aidp/providers/kilocode.rb +1 -1
- data/lib/aidp/providers/opencode.rb +1 -1
- data/lib/aidp/setup/wizard.rb +35 -107
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +11 -0
- data/templates/implementation/implement_features.md +4 -1
- metadata +24 -2
- data/lib/aidp/harness/model_discovery_service.rb +0 -259
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "../errors"
|
|
5
|
+
require_relative "scanner"
|
|
6
|
+
require_relative "validator"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module Metadata
|
|
10
|
+
# Compiles tool metadata into a cached directory structure
|
|
11
|
+
#
|
|
12
|
+
# Aggregates metadata from all tool files, builds indexes, resolves dependencies,
|
|
13
|
+
# and generates a cached tool_directory.json for fast lookups.
|
|
14
|
+
#
|
|
15
|
+
# @example Compiling the tool directory
|
|
16
|
+
# compiler = Compiler.new(directories: [".aidp/skills", ".aidp/templates"])
|
|
17
|
+
# compiler.compile(output_path: ".aidp/cache/tool_directory.json")
|
|
18
|
+
class Compiler
|
|
19
|
+
# Compiled directory structure
|
|
20
|
+
attr_reader :tools, :indexes, :dependency_graph
|
|
21
|
+
|
|
22
|
+
# Initialize compiler
|
|
23
|
+
#
|
|
24
|
+
# @param directories [Array<String>] Directories to scan
|
|
25
|
+
# @param strict [Boolean] Whether to fail on validation errors
|
|
26
|
+
def initialize(directories: [], strict: false)
|
|
27
|
+
@directories = Array(directories)
|
|
28
|
+
@strict = strict
|
|
29
|
+
@tools = []
|
|
30
|
+
@indexes = {}
|
|
31
|
+
@dependency_graph = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Compile tool directory
|
|
35
|
+
#
|
|
36
|
+
# @param output_path [String] Path to output JSON file
|
|
37
|
+
# @return [Hash] Compiled directory structure
|
|
38
|
+
def compile(output_path:)
|
|
39
|
+
Aidp.log_info("metadata", "Compiling tool directory", directories: @directories, output: output_path)
|
|
40
|
+
|
|
41
|
+
# Scan all directories
|
|
42
|
+
scanner = Scanner.new(@directories)
|
|
43
|
+
@tools = scanner.scan_all
|
|
44
|
+
|
|
45
|
+
# Validate tools
|
|
46
|
+
validator = Validator.new(@tools)
|
|
47
|
+
validation_results = validator.validate_all
|
|
48
|
+
|
|
49
|
+
# Handle validation failures
|
|
50
|
+
handle_validation_results(validation_results)
|
|
51
|
+
|
|
52
|
+
# Build indexes and graphs
|
|
53
|
+
build_indexes
|
|
54
|
+
build_dependency_graph
|
|
55
|
+
|
|
56
|
+
# Create directory structure
|
|
57
|
+
directory = create_directory_structure
|
|
58
|
+
|
|
59
|
+
# Write to file
|
|
60
|
+
write_directory(directory, output_path)
|
|
61
|
+
|
|
62
|
+
Aidp.log_info(
|
|
63
|
+
"metadata",
|
|
64
|
+
"Compilation complete",
|
|
65
|
+
tools: @tools.size,
|
|
66
|
+
output: output_path
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
directory
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Build indexes for fast lookups
|
|
73
|
+
def build_indexes
|
|
74
|
+
Aidp.log_debug("metadata", "Building indexes")
|
|
75
|
+
|
|
76
|
+
@indexes = {
|
|
77
|
+
by_id: {},
|
|
78
|
+
by_type: {},
|
|
79
|
+
by_tag: {},
|
|
80
|
+
by_work_unit_type: {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@tools.each do |tool|
|
|
84
|
+
# Index by ID
|
|
85
|
+
@indexes[:by_id][tool.id] = tool
|
|
86
|
+
|
|
87
|
+
# Index by type
|
|
88
|
+
@indexes[:by_type][tool.type] ||= []
|
|
89
|
+
@indexes[:by_type][tool.type] << tool
|
|
90
|
+
|
|
91
|
+
# Index by tags
|
|
92
|
+
tool.applies_to.each do |tag|
|
|
93
|
+
@indexes[:by_tag][tag] ||= []
|
|
94
|
+
@indexes[:by_tag][tag] << tool
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Index by work unit types
|
|
98
|
+
tool.work_unit_types.each do |wut|
|
|
99
|
+
@indexes[:by_work_unit_type][wut] ||= []
|
|
100
|
+
@indexes[:by_work_unit_type][wut] << tool
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
Aidp.log_debug(
|
|
105
|
+
"metadata",
|
|
106
|
+
"Indexes built",
|
|
107
|
+
types: @indexes[:by_type].keys,
|
|
108
|
+
tags: @indexes[:by_tag].keys.size,
|
|
109
|
+
work_unit_types: @indexes[:by_work_unit_type].keys
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Build dependency graph
|
|
114
|
+
def build_dependency_graph
|
|
115
|
+
Aidp.log_debug("metadata", "Building dependency graph")
|
|
116
|
+
|
|
117
|
+
@dependency_graph = {}
|
|
118
|
+
|
|
119
|
+
@tools.each do |tool|
|
|
120
|
+
@dependency_graph[tool.id] = {
|
|
121
|
+
dependencies: tool.dependencies,
|
|
122
|
+
dependents: []
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Build reverse dependencies (dependents)
|
|
127
|
+
@tools.each do |tool|
|
|
128
|
+
tool.dependencies.each do |dep_id|
|
|
129
|
+
next unless @dependency_graph[dep_id]
|
|
130
|
+
|
|
131
|
+
@dependency_graph[dep_id][:dependents] << tool.id
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
Aidp.log_debug(
|
|
136
|
+
"metadata",
|
|
137
|
+
"Dependency graph built",
|
|
138
|
+
nodes: @dependency_graph.size
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Create directory structure for serialization
|
|
143
|
+
#
|
|
144
|
+
# @return [Hash] Directory structure
|
|
145
|
+
def create_directory_structure
|
|
146
|
+
{
|
|
147
|
+
version: "1.0.0",
|
|
148
|
+
compiled_at: Time.now.iso8601,
|
|
149
|
+
tools: @tools.map(&:to_h),
|
|
150
|
+
indexes: {
|
|
151
|
+
by_type: @indexes[:by_type].transform_values { |tools| tools.map(&:id) },
|
|
152
|
+
by_tag: @indexes[:by_tag].transform_values { |tools| tools.map(&:id) },
|
|
153
|
+
by_work_unit_type: @indexes[:by_work_unit_type].transform_values { |tools| tools.map(&:id) }
|
|
154
|
+
},
|
|
155
|
+
dependency_graph: @dependency_graph,
|
|
156
|
+
statistics: {
|
|
157
|
+
total_tools: @tools.size,
|
|
158
|
+
by_type: @tools.group_by(&:type).transform_values(&:size),
|
|
159
|
+
total_tags: @indexes[:by_tag].size,
|
|
160
|
+
total_work_unit_types: @indexes[:by_work_unit_type].size
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Write directory to JSON file
|
|
166
|
+
#
|
|
167
|
+
# @param directory [Hash] Directory structure
|
|
168
|
+
# @param output_path [String] Output file path
|
|
169
|
+
def write_directory(directory, output_path)
|
|
170
|
+
# Ensure output directory exists
|
|
171
|
+
output_dir = File.dirname(output_path)
|
|
172
|
+
FileUtils.mkdir_p(output_dir) unless Dir.exist?(output_dir)
|
|
173
|
+
|
|
174
|
+
# Write with pretty formatting
|
|
175
|
+
File.write(output_path, JSON.pretty_generate(directory))
|
|
176
|
+
|
|
177
|
+
Aidp.log_debug("metadata", "Wrote directory", path: output_path, size: File.size(output_path))
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Handle validation results
|
|
181
|
+
#
|
|
182
|
+
# @param results [Array<ValidationResult>] Validation results
|
|
183
|
+
# @raise [Aidp::Errors::ValidationError] if strict mode and errors found
|
|
184
|
+
def handle_validation_results(results)
|
|
185
|
+
invalid_results = results.reject(&:valid)
|
|
186
|
+
|
|
187
|
+
if invalid_results.any?
|
|
188
|
+
Aidp.log_warn(
|
|
189
|
+
"metadata",
|
|
190
|
+
"Validation errors found",
|
|
191
|
+
count: invalid_results.size
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
invalid_results.each do |result|
|
|
195
|
+
Aidp.log_error(
|
|
196
|
+
"metadata",
|
|
197
|
+
"Tool validation failed",
|
|
198
|
+
tool_id: result.tool_id,
|
|
199
|
+
file: result.file_path,
|
|
200
|
+
errors: result.errors
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if @strict
|
|
205
|
+
raise Aidp::Errors::ValidationError,
|
|
206
|
+
"#{invalid_results.size} tool(s) failed validation (strict mode enabled)"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Remove invalid tools from compilation
|
|
210
|
+
invalid_ids = invalid_results.map(&:tool_id)
|
|
211
|
+
@tools.reject! { |tool| invalid_ids.include?(tool.id) }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Log warnings
|
|
215
|
+
results.each do |result|
|
|
216
|
+
next if result.warnings.empty?
|
|
217
|
+
|
|
218
|
+
Aidp.log_warn(
|
|
219
|
+
"metadata",
|
|
220
|
+
"Tool validation warnings",
|
|
221
|
+
tool_id: result.tool_id,
|
|
222
|
+
file: result.file_path,
|
|
223
|
+
warnings: result.warnings
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "digest"
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "tool_metadata"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
module Metadata
|
|
10
|
+
# Parses tool metadata from markdown files with YAML frontmatter
|
|
11
|
+
#
|
|
12
|
+
# Extracts metadata headers from skill, persona, and template files.
|
|
13
|
+
# Supports both new metadata format and legacy skill format.
|
|
14
|
+
#
|
|
15
|
+
# @example Parsing a file
|
|
16
|
+
# metadata = Parser.parse_file("/path/to/tool.md", type: "skill")
|
|
17
|
+
#
|
|
18
|
+
# @example Parsing with auto-detection
|
|
19
|
+
# metadata = Parser.parse_file("/path/to/SKILL.md")
|
|
20
|
+
class Parser
|
|
21
|
+
# Parse metadata from a file
|
|
22
|
+
#
|
|
23
|
+
# @param file_path [String] Path to .md file
|
|
24
|
+
# @param type [String, nil] Tool type ("skill", "persona", "template") or nil to auto-detect
|
|
25
|
+
# @return [ToolMetadata] Parsed metadata
|
|
26
|
+
# @raise [Aidp::Errors::ValidationError] if file format is invalid
|
|
27
|
+
def self.parse_file(file_path, type: nil)
|
|
28
|
+
Aidp.log_debug("metadata", "Parsing file", file: file_path, type: type)
|
|
29
|
+
|
|
30
|
+
unless File.exist?(file_path)
|
|
31
|
+
raise Aidp::Errors::ValidationError, "File not found: #{file_path}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
content = File.read(file_path, encoding: "UTF-8")
|
|
35
|
+
file_hash = compute_file_hash(content)
|
|
36
|
+
|
|
37
|
+
# Auto-detect type from filename or path if not specified
|
|
38
|
+
type ||= detect_type(file_path)
|
|
39
|
+
|
|
40
|
+
parse_string(content, source_path: file_path, file_hash: file_hash, type: type)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Parse metadata from a string
|
|
44
|
+
#
|
|
45
|
+
# @param content [String] File content with frontmatter
|
|
46
|
+
# @param source_path [String] Source file path for reference
|
|
47
|
+
# @param file_hash [String] SHA256 hash of content
|
|
48
|
+
# @param type [String] Tool type ("skill", "persona", "template")
|
|
49
|
+
# @return [ToolMetadata] Parsed metadata
|
|
50
|
+
# @raise [Aidp::Errors::ValidationError] if format is invalid
|
|
51
|
+
def self.parse_string(content, source_path:, file_hash:, type:)
|
|
52
|
+
metadata_hash, markdown = parse_frontmatter(content, source_path: source_path)
|
|
53
|
+
|
|
54
|
+
# Map legacy skill fields to new metadata schema
|
|
55
|
+
normalized = normalize_metadata(metadata_hash, type: type)
|
|
56
|
+
|
|
57
|
+
ToolMetadata.new(
|
|
58
|
+
type: type,
|
|
59
|
+
id: normalized["id"],
|
|
60
|
+
title: normalized["title"],
|
|
61
|
+
summary: normalized["summary"],
|
|
62
|
+
version: normalized["version"],
|
|
63
|
+
applies_to: normalized["applies_to"] || [],
|
|
64
|
+
work_unit_types: normalized["work_unit_types"] || [],
|
|
65
|
+
priority: normalized["priority"] || ToolMetadata::DEFAULT_PRIORITY,
|
|
66
|
+
capabilities: normalized["capabilities"] || [],
|
|
67
|
+
dependencies: normalized["dependencies"] || [],
|
|
68
|
+
experimental: normalized["experimental"] || false,
|
|
69
|
+
content: markdown,
|
|
70
|
+
source_path: source_path,
|
|
71
|
+
file_hash: file_hash
|
|
72
|
+
)
|
|
73
|
+
rescue Aidp::Errors::ValidationError => e
|
|
74
|
+
Aidp.log_error("metadata", "Metadata validation failed", error: e.message, file: source_path)
|
|
75
|
+
raise
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Compute SHA256 hash of file content
|
|
79
|
+
#
|
|
80
|
+
# @param content [String] File content
|
|
81
|
+
# @return [String] SHA256 hex string
|
|
82
|
+
def self.compute_file_hash(content)
|
|
83
|
+
Digest::SHA256.hexdigest(content)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Detect tool type from file path
|
|
87
|
+
#
|
|
88
|
+
# @param file_path [String] File path
|
|
89
|
+
# @return [String] Detected type ("skill", "persona", or "template")
|
|
90
|
+
def self.detect_type(file_path)
|
|
91
|
+
case file_path
|
|
92
|
+
when %r{/skills/}
|
|
93
|
+
"skill"
|
|
94
|
+
when %r{/personas/}
|
|
95
|
+
"persona"
|
|
96
|
+
when /SKILL\.md$/
|
|
97
|
+
"skill"
|
|
98
|
+
else
|
|
99
|
+
"template"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Parse YAML frontmatter from content
|
|
104
|
+
#
|
|
105
|
+
# @param content [String] File content with frontmatter
|
|
106
|
+
# @param source_path [String] Source path for error messages
|
|
107
|
+
# @return [Array(Hash, String)] Tuple of [metadata, markdown_content]
|
|
108
|
+
# @raise [Aidp::Errors::ValidationError] if frontmatter is invalid
|
|
109
|
+
def self.parse_frontmatter(content, source_path:)
|
|
110
|
+
# Ensure content is UTF-8 encoded
|
|
111
|
+
content = content.encode("UTF-8", invalid: :replace, undef: :replace) unless content.encoding == Encoding::UTF_8
|
|
112
|
+
lines = content.lines
|
|
113
|
+
|
|
114
|
+
unless lines.first&.strip == "---"
|
|
115
|
+
raise Aidp::Errors::ValidationError,
|
|
116
|
+
"Invalid format: missing YAML frontmatter in #{source_path}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
frontmatter_lines = []
|
|
120
|
+
body_start_index = nil
|
|
121
|
+
|
|
122
|
+
lines[1..].each_with_index do |line, index|
|
|
123
|
+
if line.strip == "---"
|
|
124
|
+
body_start_index = index + 2
|
|
125
|
+
break
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
frontmatter_lines << line
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
unless body_start_index
|
|
132
|
+
raise Aidp::Errors::ValidationError,
|
|
133
|
+
"Invalid format: missing closing frontmatter delimiter in #{source_path}"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
markdown_content = lines[body_start_index..]&.join.to_s.strip
|
|
137
|
+
frontmatter_yaml = frontmatter_lines.join
|
|
138
|
+
|
|
139
|
+
begin
|
|
140
|
+
metadata = YAML.safe_load(frontmatter_yaml, permitted_classes: [Symbol])
|
|
141
|
+
rescue Psych::SyntaxError => e
|
|
142
|
+
raise Aidp::Errors::ValidationError,
|
|
143
|
+
"Invalid YAML frontmatter in #{source_path}: #{e.message}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
unless metadata.is_a?(Hash)
|
|
147
|
+
raise Aidp::Errors::ValidationError,
|
|
148
|
+
"YAML frontmatter must be a hash in #{source_path}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
[metadata, markdown_content]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Normalize metadata from various formats to unified schema
|
|
155
|
+
#
|
|
156
|
+
# Handles both legacy skill format and new metadata format.
|
|
157
|
+
#
|
|
158
|
+
# @param metadata [Hash] Raw metadata from frontmatter
|
|
159
|
+
# @param type [String] Tool type
|
|
160
|
+
# @return [Hash] Normalized metadata
|
|
161
|
+
def self.normalize_metadata(metadata, type:)
|
|
162
|
+
normalized = {}
|
|
163
|
+
|
|
164
|
+
# Required fields (map from legacy names)
|
|
165
|
+
normalized["id"] = metadata["id"]
|
|
166
|
+
normalized["title"] = metadata["title"] || metadata["name"]
|
|
167
|
+
normalized["summary"] = metadata["summary"] || metadata["description"]
|
|
168
|
+
normalized["version"] = metadata["version"]
|
|
169
|
+
|
|
170
|
+
# Optional fields (new schema)
|
|
171
|
+
normalized["applies_to"] = extract_applies_to(metadata)
|
|
172
|
+
normalized["work_unit_types"] = metadata["work_unit_types"] || []
|
|
173
|
+
normalized["priority"] = metadata["priority"]&.to_i
|
|
174
|
+
normalized["capabilities"] = metadata["capabilities"] || []
|
|
175
|
+
normalized["dependencies"] = metadata["dependencies"] || []
|
|
176
|
+
normalized["experimental"] = metadata["experimental"] || false
|
|
177
|
+
|
|
178
|
+
normalized
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Extract applies_to tags from various metadata fields
|
|
182
|
+
#
|
|
183
|
+
# Combines keywords, tags, expertise areas, etc. into unified applies_to list
|
|
184
|
+
#
|
|
185
|
+
# @param metadata [Hash] Raw metadata
|
|
186
|
+
# @return [Array<String>] Combined applies_to tags
|
|
187
|
+
def self.extract_applies_to(metadata)
|
|
188
|
+
applies_to = []
|
|
189
|
+
|
|
190
|
+
# New schema
|
|
191
|
+
applies_to.concat(metadata["applies_to"] || [])
|
|
192
|
+
|
|
193
|
+
# Legacy skill schema
|
|
194
|
+
applies_to.concat(metadata["keywords"] || [])
|
|
195
|
+
applies_to.concat(metadata["tags"] || [])
|
|
196
|
+
|
|
197
|
+
# Flatten and deduplicate
|
|
198
|
+
applies_to.flatten.compact.uniq
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private_class_method :parse_frontmatter, :normalize_metadata, :extract_applies_to
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "tool_metadata"
|
|
5
|
+
require_relative "cache"
|
|
6
|
+
|
|
7
|
+
module Aidp
|
|
8
|
+
module Metadata
|
|
9
|
+
# Query interface for tool-choosing agent
|
|
10
|
+
#
|
|
11
|
+
# Provides filtering, ranking, and dependency resolution for tools
|
|
12
|
+
# based on metadata criteria.
|
|
13
|
+
#
|
|
14
|
+
# @example Querying for tools
|
|
15
|
+
# query = Query.new(cache: cache)
|
|
16
|
+
# tools = query.find_by_tags(["ruby", "testing"])
|
|
17
|
+
# ranked = query.rank_by_priority(tools)
|
|
18
|
+
class Query
|
|
19
|
+
# Initialize query interface
|
|
20
|
+
#
|
|
21
|
+
# @param cache [Cache] Metadata cache instance
|
|
22
|
+
def initialize(cache:)
|
|
23
|
+
@cache = cache
|
|
24
|
+
@directory = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Load directory (lazy)
|
|
28
|
+
#
|
|
29
|
+
# @return [Hash] Tool directory
|
|
30
|
+
def directory
|
|
31
|
+
@directory ||= @cache.load
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Reload directory
|
|
35
|
+
def reload
|
|
36
|
+
@directory = @cache.reload
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Find tool by ID
|
|
40
|
+
#
|
|
41
|
+
# @param id [String] Tool ID
|
|
42
|
+
# @return [Hash, nil] Tool metadata or nil
|
|
43
|
+
def find_by_id(id)
|
|
44
|
+
tools = directory["tools"]
|
|
45
|
+
tools.find { |tool| tool["id"] == id }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Find tools by type
|
|
49
|
+
#
|
|
50
|
+
# @param type [String] Tool type ("skill", "persona", "template")
|
|
51
|
+
# @return [Array<Hash>] Matching tools
|
|
52
|
+
def find_by_type(type)
|
|
53
|
+
indexes = directory["indexes"]["by_type"]
|
|
54
|
+
tool_ids = indexes[type] || []
|
|
55
|
+
tools_by_ids(tool_ids)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Find tools by applies_to tags
|
|
59
|
+
#
|
|
60
|
+
# @param tags [Array<String>] Tags to filter by
|
|
61
|
+
# @param match_all [Boolean] Whether to match all tags (AND) or any tag (OR)
|
|
62
|
+
# @return [Array<Hash>] Matching tools
|
|
63
|
+
def find_by_tags(tags, match_all: false)
|
|
64
|
+
Aidp.log_debug("metadata", "Finding by tags", tags: tags, match_all: match_all)
|
|
65
|
+
|
|
66
|
+
tags = Array(tags).map(&:downcase)
|
|
67
|
+
indexes = directory["indexes"]["by_tag"]
|
|
68
|
+
|
|
69
|
+
if match_all
|
|
70
|
+
# Find tools that have ALL specified tags
|
|
71
|
+
tool_ids_sets = tags.map { |tag| indexes[tag] || [] }
|
|
72
|
+
tool_ids = tool_ids_sets.reduce(&:&) || []
|
|
73
|
+
else
|
|
74
|
+
# Find tools that have ANY specified tag
|
|
75
|
+
tool_ids = tags.flat_map { |tag| indexes[tag] || [] }.uniq
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
tools = tools_by_ids(tool_ids)
|
|
79
|
+
|
|
80
|
+
Aidp.log_debug("metadata", "Found tools by tags", count: tools.size)
|
|
81
|
+
|
|
82
|
+
tools
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Find tools by work unit type
|
|
86
|
+
#
|
|
87
|
+
# @param work_unit_type [String] Work unit type (e.g., "implementation", "analysis")
|
|
88
|
+
# @return [Array<Hash>] Matching tools
|
|
89
|
+
def find_by_work_unit_type(work_unit_type)
|
|
90
|
+
Aidp.log_debug("metadata", "Finding by work unit type", type: work_unit_type)
|
|
91
|
+
|
|
92
|
+
indexes = directory["indexes"]["by_work_unit_type"]
|
|
93
|
+
tool_ids = indexes[work_unit_type.downcase] || []
|
|
94
|
+
tools = tools_by_ids(tool_ids)
|
|
95
|
+
|
|
96
|
+
Aidp.log_debug("metadata", "Found tools by work unit type", count: tools.size)
|
|
97
|
+
|
|
98
|
+
tools
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Filter tools by multiple criteria
|
|
102
|
+
#
|
|
103
|
+
# @param type [String, nil] Tool type filter
|
|
104
|
+
# @param tags [Array<String>, nil] Tag filter
|
|
105
|
+
# @param work_unit_type [String, nil] Work unit type filter
|
|
106
|
+
# @param experimental [Boolean, nil] Experimental filter (true/false/nil for all)
|
|
107
|
+
# @return [Array<Hash>] Filtered tools
|
|
108
|
+
def filter(type: nil, tags: nil, work_unit_type: nil, experimental: nil)
|
|
109
|
+
Aidp.log_debug(
|
|
110
|
+
"metadata",
|
|
111
|
+
"Filtering tools",
|
|
112
|
+
type: type,
|
|
113
|
+
tags: tags,
|
|
114
|
+
work_unit_type: work_unit_type,
|
|
115
|
+
experimental: experimental
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
tools = directory["tools"]
|
|
119
|
+
|
|
120
|
+
# Filter by type
|
|
121
|
+
tools = tools.select { |t| t["type"] == type } if type
|
|
122
|
+
|
|
123
|
+
# Filter by tags
|
|
124
|
+
if tags && !tags.empty?
|
|
125
|
+
tags = Array(tags).map(&:downcase)
|
|
126
|
+
tools = tools.select do |t|
|
|
127
|
+
tool_tags = (t["applies_to"] || []).map(&:downcase)
|
|
128
|
+
(tool_tags & tags).any?
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Filter by work unit type
|
|
133
|
+
if work_unit_type
|
|
134
|
+
wut_lower = work_unit_type.downcase
|
|
135
|
+
tools = tools.select do |t|
|
|
136
|
+
(t["work_unit_types"] || []).map(&:downcase).include?(wut_lower)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Filter by experimental flag
|
|
141
|
+
tools = tools.select { |t| t["experimental"] == experimental } unless experimental.nil?
|
|
142
|
+
|
|
143
|
+
Aidp.log_debug("metadata", "Filtered tools", count: tools.size)
|
|
144
|
+
|
|
145
|
+
tools
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Rank tools by priority (highest first)
|
|
149
|
+
#
|
|
150
|
+
# @param tools [Array<Hash>] Tools to rank
|
|
151
|
+
# @return [Array<Hash>] Ranked tools
|
|
152
|
+
def rank_by_priority(tools)
|
|
153
|
+
tools.sort_by { |t| -(t["priority"] || ToolMetadata::DEFAULT_PRIORITY) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Resolve dependencies for a tool
|
|
157
|
+
#
|
|
158
|
+
# Returns all dependencies recursively in dependency order (topological sort)
|
|
159
|
+
#
|
|
160
|
+
# @param tool_id [String] Tool ID
|
|
161
|
+
# @return [Array<String>] Ordered list of dependency IDs
|
|
162
|
+
# @raise [Aidp::Errors::ValidationError] if circular dependency detected
|
|
163
|
+
def resolve_dependencies(tool_id)
|
|
164
|
+
Aidp.log_debug("metadata", "Resolving dependencies", tool_id: tool_id)
|
|
165
|
+
|
|
166
|
+
graph = directory["dependency_graph"]
|
|
167
|
+
resolved = []
|
|
168
|
+
seen = Set.new
|
|
169
|
+
|
|
170
|
+
resolve_recursive(tool_id, graph, resolved, seen)
|
|
171
|
+
|
|
172
|
+
Aidp.log_debug("metadata", "Dependencies resolved", tool_id: tool_id, dependencies: resolved)
|
|
173
|
+
|
|
174
|
+
resolved
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Find dependents of a tool
|
|
178
|
+
#
|
|
179
|
+
# Returns all tools that depend on this tool (directly or indirectly)
|
|
180
|
+
#
|
|
181
|
+
# @param tool_id [String] Tool ID
|
|
182
|
+
# @return [Array<String>] List of dependent tool IDs
|
|
183
|
+
def find_dependents(tool_id)
|
|
184
|
+
graph = directory["dependency_graph"]
|
|
185
|
+
return [] unless graph[tool_id]
|
|
186
|
+
|
|
187
|
+
graph[tool_id]["dependents"] || []
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Get statistics about the tool directory
|
|
191
|
+
#
|
|
192
|
+
# @return [Hash] Statistics
|
|
193
|
+
def statistics
|
|
194
|
+
directory["statistics"]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
# Get tools by IDs
|
|
200
|
+
#
|
|
201
|
+
# @param tool_ids [Array<String>] Tool IDs
|
|
202
|
+
# @return [Array<Hash>] Tools
|
|
203
|
+
def tools_by_ids(tool_ids)
|
|
204
|
+
all_tools = directory["tools"]
|
|
205
|
+
tool_ids.map { |id| all_tools.find { |t| t["id"] == id } }.compact
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Resolve dependencies recursively (topological sort)
|
|
209
|
+
#
|
|
210
|
+
# @param tool_id [String] Tool ID
|
|
211
|
+
# @param graph [Hash] Dependency graph
|
|
212
|
+
# @param resolved [Array<String>] Resolved dependencies (output)
|
|
213
|
+
# @param seen [Set<String>] Seen tools (for cycle detection)
|
|
214
|
+
# @raise [Aidp::Errors::ValidationError] if circular dependency detected
|
|
215
|
+
def resolve_recursive(tool_id, graph, resolved, seen)
|
|
216
|
+
return if resolved.include?(tool_id)
|
|
217
|
+
|
|
218
|
+
if seen.include?(tool_id)
|
|
219
|
+
raise Aidp::Errors::ValidationError, "Circular dependency detected: #{tool_id}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
seen.add(tool_id)
|
|
223
|
+
|
|
224
|
+
node = graph[tool_id]
|
|
225
|
+
if node
|
|
226
|
+
dependencies = node["dependencies"] || []
|
|
227
|
+
dependencies.each do |dep_id|
|
|
228
|
+
resolve_recursive(dep_id, graph, resolved, seen)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
resolved << tool_id unless resolved.include?(tool_id)
|
|
233
|
+
seen.delete(tool_id)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|