aidp 0.28.0 → 0.30.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/deprecation_cache.rb +177 -0
- data/lib/aidp/harness/provider_manager.rb +5 -3
- data/lib/aidp/harness/ruby_llm_registry.rb +327 -0
- data/lib/aidp/harness/thinking_depth_manager.rb +47 -5
- 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 +189 -13
- 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 +25 -2
- data/lib/aidp/harness/model_discovery_service.rb +0 -259
|
@@ -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
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "parser"
|
|
5
|
+
|
|
6
|
+
module Aidp
|
|
7
|
+
module Metadata
|
|
8
|
+
# Scans directories for tool files and extracts metadata
|
|
9
|
+
#
|
|
10
|
+
# Recursively finds all .md files in configured directories,
|
|
11
|
+
# parses their metadata, and returns a collection of ToolMetadata objects.
|
|
12
|
+
#
|
|
13
|
+
# @example Scanning directories
|
|
14
|
+
# scanner = Scanner.new([".aidp/skills", ".aidp/templates"])
|
|
15
|
+
# tools = scanner.scan_all
|
|
16
|
+
class Scanner
|
|
17
|
+
# Initialize scanner with directory paths
|
|
18
|
+
#
|
|
19
|
+
# @param directories [Array<String>] Directories to scan
|
|
20
|
+
def initialize(directories = [])
|
|
21
|
+
@directories = Array(directories)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Scan all configured directories
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<ToolMetadata>] All discovered tool metadata
|
|
27
|
+
def scan_all
|
|
28
|
+
Aidp.log_debug("metadata", "Scanning directories", directories: @directories)
|
|
29
|
+
|
|
30
|
+
all_tools = []
|
|
31
|
+
@directories.each do |dir|
|
|
32
|
+
tools = scan_directory(dir)
|
|
33
|
+
all_tools.concat(tools)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Aidp.log_info(
|
|
37
|
+
"metadata",
|
|
38
|
+
"Scan complete",
|
|
39
|
+
directories: @directories.size,
|
|
40
|
+
tools_found: all_tools.size
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
all_tools
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Scan a single directory
|
|
47
|
+
#
|
|
48
|
+
# @param directory [String] Directory path to scan
|
|
49
|
+
# @param type [String, nil] Tool type filter or nil for all
|
|
50
|
+
# @return [Array<ToolMetadata>] Discovered tool metadata
|
|
51
|
+
def scan_directory(directory, type: nil)
|
|
52
|
+
unless Dir.exist?(directory)
|
|
53
|
+
Aidp.log_warn("metadata", "Directory not found", directory: directory)
|
|
54
|
+
return []
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Aidp.log_debug("metadata", "Scanning directory", directory: directory, type: type)
|
|
58
|
+
|
|
59
|
+
tools = []
|
|
60
|
+
md_files = find_markdown_files(directory)
|
|
61
|
+
|
|
62
|
+
md_files.each do |file_path|
|
|
63
|
+
tool = Parser.parse_file(file_path, type: type)
|
|
64
|
+
tools << tool if type.nil? || tool.type == type
|
|
65
|
+
rescue Aidp::Errors::ValidationError => e
|
|
66
|
+
Aidp.log_warn(
|
|
67
|
+
"metadata",
|
|
68
|
+
"Failed to parse file",
|
|
69
|
+
file: file_path,
|
|
70
|
+
error: e.message
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Aidp.log_debug(
|
|
75
|
+
"metadata",
|
|
76
|
+
"Directory scan complete",
|
|
77
|
+
directory: directory,
|
|
78
|
+
files_found: md_files.size,
|
|
79
|
+
tools_parsed: tools.size
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
tools
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Find all markdown files in directory recursively
|
|
86
|
+
#
|
|
87
|
+
# @param directory [String] Directory path
|
|
88
|
+
# @return [Array<String>] Paths to .md files
|
|
89
|
+
def find_markdown_files(directory)
|
|
90
|
+
pattern = File.join(directory, "**", "*.md")
|
|
91
|
+
files = Dir.glob(pattern)
|
|
92
|
+
|
|
93
|
+
Aidp.log_debug(
|
|
94
|
+
"metadata",
|
|
95
|
+
"Found markdown files",
|
|
96
|
+
directory: directory,
|
|
97
|
+
count: files.size
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
files
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Scan with file filtering
|
|
104
|
+
#
|
|
105
|
+
# @param directory [String] Directory path
|
|
106
|
+
# @param filter [Proc] Filter proc that receives file_path and returns boolean
|
|
107
|
+
# @return [Array<ToolMetadata>] Filtered tool metadata
|
|
108
|
+
def scan_with_filter(directory, &filter)
|
|
109
|
+
unless Dir.exist?(directory)
|
|
110
|
+
Aidp.log_warn("metadata", "Directory not found", directory: directory)
|
|
111
|
+
return []
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
tools = []
|
|
115
|
+
md_files = find_markdown_files(directory)
|
|
116
|
+
|
|
117
|
+
md_files.each do |file_path|
|
|
118
|
+
next unless filter.call(file_path)
|
|
119
|
+
|
|
120
|
+
begin
|
|
121
|
+
tool = Parser.parse_file(file_path)
|
|
122
|
+
tools << tool
|
|
123
|
+
rescue Aidp::Errors::ValidationError => e
|
|
124
|
+
Aidp.log_warn(
|
|
125
|
+
"metadata",
|
|
126
|
+
"Failed to parse file",
|
|
127
|
+
file: file_path,
|
|
128
|
+
error: e.message
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
tools
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Scan for changes since last scan
|
|
137
|
+
#
|
|
138
|
+
# Compares file hashes to detect changes
|
|
139
|
+
#
|
|
140
|
+
# @param directory [String] Directory path
|
|
141
|
+
# @param previous_hashes [Hash<String, String>] Map of file_path => file_hash
|
|
142
|
+
# @return [Hash] Hash with :added, :modified, :removed keys
|
|
143
|
+
def scan_changes(directory, previous_hashes = {})
|
|
144
|
+
Aidp.log_debug("metadata", "Scanning for changes", directory: directory)
|
|
145
|
+
|
|
146
|
+
current_files = find_markdown_files(directory)
|
|
147
|
+
current_hashes = {}
|
|
148
|
+
|
|
149
|
+
changes = {
|
|
150
|
+
added: [],
|
|
151
|
+
modified: [],
|
|
152
|
+
removed: [],
|
|
153
|
+
unchanged: []
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
# Check for added and modified files
|
|
157
|
+
current_files.each do |file_path|
|
|
158
|
+
content = File.read(file_path, encoding: "UTF-8")
|
|
159
|
+
file_hash = Parser.compute_file_hash(content)
|
|
160
|
+
current_hashes[file_path] = file_hash
|
|
161
|
+
|
|
162
|
+
if previous_hashes.key?(file_path)
|
|
163
|
+
if previous_hashes[file_path] != file_hash
|
|
164
|
+
changes[:modified] << file_path
|
|
165
|
+
else
|
|
166
|
+
changes[:unchanged] << file_path
|
|
167
|
+
end
|
|
168
|
+
else
|
|
169
|
+
changes[:added] << file_path
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check for removed files
|
|
174
|
+
previous_hashes.keys.each do |file_path|
|
|
175
|
+
changes[:removed] << file_path unless current_hashes.key?(file_path)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
Aidp.log_info(
|
|
179
|
+
"metadata",
|
|
180
|
+
"Change detection complete",
|
|
181
|
+
added: changes[:added].size,
|
|
182
|
+
modified: changes[:modified].size,
|
|
183
|
+
removed: changes[:removed].size,
|
|
184
|
+
unchanged: changes[:unchanged].size
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
changes
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
|
|
5
|
+
module Aidp
|
|
6
|
+
module Metadata
|
|
7
|
+
# Base class for tool metadata (skills, personas, templates)
|
|
8
|
+
#
|
|
9
|
+
# Represents metadata extracted from YAML frontmatter in .md files.
|
|
10
|
+
# Provides validation and query capabilities for the tool directory.
|
|
11
|
+
#
|
|
12
|
+
# @example Creating tool metadata
|
|
13
|
+
# metadata = ToolMetadata.new(
|
|
14
|
+
# type: "skill",
|
|
15
|
+
# id: "ruby_rspec_tdd",
|
|
16
|
+
# title: "Ruby RSpec TDD Implementer",
|
|
17
|
+
# summary: "Expert in Test-Driven Development",
|
|
18
|
+
# version: "1.0.0",
|
|
19
|
+
# applies_to: ["ruby", "testing"],
|
|
20
|
+
# work_unit_types: ["implementation", "testing"],
|
|
21
|
+
# priority: 10,
|
|
22
|
+
# content: "You are a TDD expert...",
|
|
23
|
+
# source_path: "/path/to/SKILL.md"
|
|
24
|
+
# )
|
|
25
|
+
class ToolMetadata
|
|
26
|
+
attr_reader :type, :id, :title, :summary, :version,
|
|
27
|
+
:applies_to, :work_unit_types, :priority,
|
|
28
|
+
:capabilities, :dependencies, :experimental,
|
|
29
|
+
:content, :source_path, :file_hash
|
|
30
|
+
|
|
31
|
+
# Valid tool types
|
|
32
|
+
VALID_TYPES = %w[skill persona template].freeze
|
|
33
|
+
|
|
34
|
+
# Default priority (medium)
|
|
35
|
+
DEFAULT_PRIORITY = 5
|
|
36
|
+
|
|
37
|
+
# Initialize new tool metadata
|
|
38
|
+
#
|
|
39
|
+
# @param type [String] Tool type: "skill", "persona", or "template"
|
|
40
|
+
# @param id [String] Unique identifier (lowercase, alphanumeric, underscores)
|
|
41
|
+
# @param title [String] Human-readable title
|
|
42
|
+
# @param summary [String] Brief one-line summary
|
|
43
|
+
# @param version [String] Semantic version (e.g., "1.0.0")
|
|
44
|
+
# @param applies_to [Array<String>] Tags indicating applicability
|
|
45
|
+
# @param work_unit_types [Array<String>] Work unit types this tool supports
|
|
46
|
+
# @param priority [Integer] Priority for ranking (1-10, default 5)
|
|
47
|
+
# @param capabilities [Array<String>] Capabilities provided by this tool
|
|
48
|
+
# @param dependencies [Array<String>] IDs of required tools
|
|
49
|
+
# @param experimental [Boolean] Whether this is experimental
|
|
50
|
+
# @param content [String] The tool content (markdown)
|
|
51
|
+
# @param source_path [String] Path to source .md file
|
|
52
|
+
# @param file_hash [String] SHA256 hash of source file for cache invalidation
|
|
53
|
+
def initialize(
|
|
54
|
+
type:,
|
|
55
|
+
id:,
|
|
56
|
+
title:,
|
|
57
|
+
summary:,
|
|
58
|
+
version:,
|
|
59
|
+
content:,
|
|
60
|
+
source_path:,
|
|
61
|
+
file_hash:,
|
|
62
|
+
applies_to: [],
|
|
63
|
+
work_unit_types: [],
|
|
64
|
+
priority: DEFAULT_PRIORITY,
|
|
65
|
+
capabilities: [],
|
|
66
|
+
dependencies: [],
|
|
67
|
+
experimental: false
|
|
68
|
+
)
|
|
69
|
+
@type = type
|
|
70
|
+
@id = id
|
|
71
|
+
@title = title
|
|
72
|
+
@summary = summary
|
|
73
|
+
@version = version
|
|
74
|
+
@applies_to = Array(applies_to)
|
|
75
|
+
@work_unit_types = Array(work_unit_types)
|
|
76
|
+
@priority = priority
|
|
77
|
+
@capabilities = Array(capabilities)
|
|
78
|
+
@dependencies = Array(dependencies)
|
|
79
|
+
@experimental = experimental
|
|
80
|
+
@content = content
|
|
81
|
+
@source_path = source_path
|
|
82
|
+
@file_hash = file_hash
|
|
83
|
+
|
|
84
|
+
validate!
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Check if this tool applies to given tags
|
|
88
|
+
#
|
|
89
|
+
# @param tags [Array<String>] Tags to check against
|
|
90
|
+
# @return [Boolean] True if any tag matches applies_to
|
|
91
|
+
def applies_to?(tags)
|
|
92
|
+
return true if applies_to.empty?
|
|
93
|
+
tags = Array(tags).map(&:downcase)
|
|
94
|
+
applies_to.map(&:downcase).any? { |tag| tags.include?(tag) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check if this tool supports a given work unit type
|
|
98
|
+
#
|
|
99
|
+
# @param type [String] Work unit type (e.g., "implementation", "analysis")
|
|
100
|
+
# @return [Boolean] True if type is supported
|
|
101
|
+
def supports_work_unit?(type)
|
|
102
|
+
return true if work_unit_types.empty?
|
|
103
|
+
work_unit_types.map(&:downcase).include?(type.to_s.downcase)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get a summary of this tool for display
|
|
107
|
+
#
|
|
108
|
+
# @return [Hash] Summary with key metadata
|
|
109
|
+
def summary_hash
|
|
110
|
+
{
|
|
111
|
+
type: type,
|
|
112
|
+
id: id,
|
|
113
|
+
title: title,
|
|
114
|
+
summary: summary,
|
|
115
|
+
version: version,
|
|
116
|
+
priority: priority,
|
|
117
|
+
tags: applies_to,
|
|
118
|
+
experimental: experimental
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get full details of this tool for display
|
|
123
|
+
#
|
|
124
|
+
# @return [Hash] Detailed tool information
|
|
125
|
+
def details
|
|
126
|
+
{
|
|
127
|
+
type: type,
|
|
128
|
+
id: id,
|
|
129
|
+
title: title,
|
|
130
|
+
summary: summary,
|
|
131
|
+
version: version,
|
|
132
|
+
applies_to: applies_to,
|
|
133
|
+
work_unit_types: work_unit_types,
|
|
134
|
+
priority: priority,
|
|
135
|
+
capabilities: capabilities,
|
|
136
|
+
dependencies: dependencies,
|
|
137
|
+
experimental: experimental,
|
|
138
|
+
source: source_path,
|
|
139
|
+
file_hash: file_hash,
|
|
140
|
+
content_length: content.length
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Convert to hash for serialization
|
|
145
|
+
#
|
|
146
|
+
# @return [Hash] Metadata as hash (for JSON)
|
|
147
|
+
def to_h
|
|
148
|
+
{
|
|
149
|
+
type: type,
|
|
150
|
+
id: id,
|
|
151
|
+
title: title,
|
|
152
|
+
summary: summary,
|
|
153
|
+
version: version,
|
|
154
|
+
applies_to: applies_to,
|
|
155
|
+
work_unit_types: work_unit_types,
|
|
156
|
+
priority: priority,
|
|
157
|
+
capabilities: capabilities,
|
|
158
|
+
dependencies: dependencies,
|
|
159
|
+
experimental: experimental,
|
|
160
|
+
source_path: source_path,
|
|
161
|
+
file_hash: file_hash
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Return string representation
|
|
166
|
+
#
|
|
167
|
+
# @return [String] Tool representation
|
|
168
|
+
def to_s
|
|
169
|
+
"#{type.capitalize}[#{id}](#{title} v#{version})"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Return inspection string
|
|
173
|
+
#
|
|
174
|
+
# @return [String] Detailed inspection
|
|
175
|
+
def inspect
|
|
176
|
+
"#<Aidp::Metadata::ToolMetadata type=#{type} id=#{id} title=\"#{title}\" " \
|
|
177
|
+
"version=#{version} source=#{source_path}>"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
# Validate required fields and types
|
|
183
|
+
#
|
|
184
|
+
# @raise [Aidp::Errors::ValidationError] if validation fails
|
|
185
|
+
def validate!
|
|
186
|
+
validate_required_fields!
|
|
187
|
+
validate_types!
|
|
188
|
+
validate_formats!
|
|
189
|
+
validate_ranges!
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Validate required fields are present
|
|
193
|
+
def validate_required_fields!
|
|
194
|
+
raise Aidp::Errors::ValidationError, "type is required" if type.nil? || type.strip.empty?
|
|
195
|
+
raise Aidp::Errors::ValidationError, "id is required" if id.nil? || id.strip.empty?
|
|
196
|
+
raise Aidp::Errors::ValidationError, "title is required" if title.nil? || title.strip.empty?
|
|
197
|
+
raise Aidp::Errors::ValidationError, "summary is required" if summary.nil? || summary.strip.empty?
|
|
198
|
+
raise Aidp::Errors::ValidationError, "version is required" if version.nil? || version.strip.empty?
|
|
199
|
+
raise Aidp::Errors::ValidationError, "content is required" if content.nil? || content.strip.empty?
|
|
200
|
+
raise Aidp::Errors::ValidationError, "source_path is required" if source_path.nil? || source_path.strip.empty?
|
|
201
|
+
raise Aidp::Errors::ValidationError, "file_hash is required" if file_hash.nil? || file_hash.strip.empty?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Validate field types
|
|
205
|
+
def validate_types!
|
|
206
|
+
unless VALID_TYPES.include?(type)
|
|
207
|
+
raise Aidp::Errors::ValidationError, "type must be one of: #{VALID_TYPES.join(", ")}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
unless priority.is_a?(Integer)
|
|
211
|
+
raise Aidp::Errors::ValidationError, "priority must be an integer"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
unless [true, false].include?(experimental)
|
|
215
|
+
raise Aidp::Errors::ValidationError, "experimental must be boolean"
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Validate field formats
|
|
220
|
+
def validate_formats!
|
|
221
|
+
# Validate version format (simple semantic version check)
|
|
222
|
+
unless version.match?(/^\d+\.\d+\.\d+/)
|
|
223
|
+
raise Aidp::Errors::ValidationError, "version must be in format X.Y.Z (e.g., 1.0.0)"
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Validate id format (lowercase, alphanumeric, underscores only)
|
|
227
|
+
unless id.match?(/^[a-z0-9_]+$/)
|
|
228
|
+
raise Aidp::Errors::ValidationError, "id must be lowercase alphanumeric with underscores only"
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Validate file_hash format (SHA256 hex)
|
|
232
|
+
unless file_hash.match?(/^[a-f0-9]{64}$/)
|
|
233
|
+
raise Aidp::Errors::ValidationError, "file_hash must be a valid SHA256 hex string"
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Validate value ranges
|
|
238
|
+
def validate_ranges!
|
|
239
|
+
unless priority.between?(1, 10)
|
|
240
|
+
raise Aidp::Errors::ValidationError, "priority must be between 1 and 10"
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|