aidp 0.7.0 → 0.8.1
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 +60 -214
- data/bin/aidp +1 -1
- data/lib/aidp/analysis/kb_inspector.rb +38 -23
- data/lib/aidp/analysis/seams.rb +2 -31
- data/lib/aidp/analysis/tree_sitter_grammar_loader.rb +1 -13
- data/lib/aidp/analysis/tree_sitter_scan.rb +3 -20
- data/lib/aidp/analyze/error_handler.rb +2 -75
- data/lib/aidp/analyze/json_file_storage.rb +292 -0
- data/lib/aidp/analyze/progress.rb +12 -0
- data/lib/aidp/analyze/progress_visualizer.rb +12 -17
- data/lib/aidp/analyze/ruby_maat_integration.rb +13 -31
- data/lib/aidp/analyze/runner.rb +256 -87
- data/lib/aidp/cli/jobs_command.rb +100 -432
- data/lib/aidp/cli.rb +309 -239
- data/lib/aidp/config.rb +298 -10
- data/lib/aidp/debug_logger.rb +195 -0
- data/lib/aidp/debug_mixin.rb +187 -0
- data/lib/aidp/execute/progress.rb +9 -0
- data/lib/aidp/execute/runner.rb +221 -40
- data/lib/aidp/execute/steps.rb +17 -7
- data/lib/aidp/execute/workflow_selector.rb +211 -0
- data/lib/aidp/harness/completion_checker.rb +268 -0
- data/lib/aidp/harness/condition_detector.rb +1526 -0
- data/lib/aidp/harness/config_loader.rb +373 -0
- data/lib/aidp/harness/config_manager.rb +382 -0
- data/lib/aidp/harness/config_schema.rb +1006 -0
- data/lib/aidp/harness/config_validator.rb +355 -0
- data/lib/aidp/harness/configuration.rb +477 -0
- data/lib/aidp/harness/enhanced_runner.rb +494 -0
- data/lib/aidp/harness/error_handler.rb +616 -0
- data/lib/aidp/harness/provider_config.rb +423 -0
- data/lib/aidp/harness/provider_factory.rb +306 -0
- data/lib/aidp/harness/provider_manager.rb +1269 -0
- data/lib/aidp/harness/provider_type_checker.rb +88 -0
- data/lib/aidp/harness/runner.rb +411 -0
- data/lib/aidp/harness/state/errors.rb +28 -0
- data/lib/aidp/harness/state/metrics.rb +219 -0
- data/lib/aidp/harness/state/persistence.rb +128 -0
- data/lib/aidp/harness/state/provider_state.rb +132 -0
- data/lib/aidp/harness/state/ui_state.rb +68 -0
- data/lib/aidp/harness/state/workflow_state.rb +123 -0
- data/lib/aidp/harness/state_manager.rb +586 -0
- data/lib/aidp/harness/status_display.rb +888 -0
- data/lib/aidp/harness/ui/base.rb +16 -0
- data/lib/aidp/harness/ui/enhanced_tui.rb +545 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +252 -0
- data/lib/aidp/harness/ui/error_handler.rb +132 -0
- data/lib/aidp/harness/ui/frame_manager.rb +361 -0
- data/lib/aidp/harness/ui/job_monitor.rb +500 -0
- data/lib/aidp/harness/ui/navigation/main_menu.rb +311 -0
- data/lib/aidp/harness/ui/navigation/menu_formatter.rb +120 -0
- data/lib/aidp/harness/ui/navigation/menu_item.rb +142 -0
- data/lib/aidp/harness/ui/navigation/menu_state.rb +139 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +202 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +176 -0
- data/lib/aidp/harness/ui/progress_display.rb +280 -0
- data/lib/aidp/harness/ui/question_collector.rb +141 -0
- data/lib/aidp/harness/ui/spinner_group.rb +184 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +152 -0
- data/lib/aidp/harness/ui/status_manager.rb +312 -0
- data/lib/aidp/harness/ui/status_widget.rb +280 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +312 -0
- data/lib/aidp/harness/user_interface.rb +2381 -0
- data/lib/aidp/provider_manager.rb +131 -7
- data/lib/aidp/providers/anthropic.rb +28 -103
- data/lib/aidp/providers/base.rb +170 -0
- data/lib/aidp/providers/cursor.rb +52 -181
- data/lib/aidp/providers/gemini.rb +24 -107
- data/lib/aidp/providers/macos_ui.rb +99 -5
- data/lib/aidp/providers/opencode.rb +194 -0
- data/lib/aidp/storage/csv_storage.rb +172 -0
- data/lib/aidp/storage/file_manager.rb +214 -0
- data/lib/aidp/storage/json_storage.rb +140 -0
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp.rb +54 -39
- data/templates/COMMON/AGENT_BASE.md +11 -0
- data/templates/EXECUTE/00_PRD.md +4 -4
- data/templates/EXECUTE/02_ARCHITECTURE.md +5 -4
- data/templates/EXECUTE/07_TEST_PLAN.md +4 -1
- data/templates/EXECUTE/08_TASKS.md +4 -4
- data/templates/EXECUTE/10_IMPLEMENTATION_AGENT.md +4 -4
- data/templates/README.md +279 -0
- data/templates/aidp-development.yml.example +373 -0
- data/templates/aidp-minimal.yml.example +48 -0
- data/templates/aidp-production.yml.example +475 -0
- data/templates/aidp.yml.example +598 -0
- metadata +93 -69
- data/lib/aidp/analyze/agent_personas.rb +0 -71
- data/lib/aidp/analyze/agent_tool_executor.rb +0 -439
- data/lib/aidp/analyze/data_retention_manager.rb +0 -421
- data/lib/aidp/analyze/database.rb +0 -260
- data/lib/aidp/analyze/dependencies.rb +0 -335
- data/lib/aidp/analyze/export_manager.rb +0 -418
- data/lib/aidp/analyze/focus_guidance.rb +0 -517
- data/lib/aidp/analyze/incremental_analyzer.rb +0 -533
- data/lib/aidp/analyze/language_analysis_strategies.rb +0 -897
- data/lib/aidp/analyze/large_analysis_progress.rb +0 -499
- data/lib/aidp/analyze/memory_manager.rb +0 -339
- data/lib/aidp/analyze/metrics_storage.rb +0 -336
- data/lib/aidp/analyze/parallel_processor.rb +0 -454
- data/lib/aidp/analyze/performance_optimizer.rb +0 -691
- data/lib/aidp/analyze/repository_chunker.rb +0 -697
- data/lib/aidp/analyze/static_analysis_detector.rb +0 -577
- data/lib/aidp/analyze/storage.rb +0 -655
- data/lib/aidp/analyze/tool_configuration.rb +0 -441
- data/lib/aidp/analyze/tool_modernization.rb +0 -750
- data/lib/aidp/database/pg_adapter.rb +0 -148
- data/lib/aidp/database_config.rb +0 -69
- data/lib/aidp/database_connection.rb +0 -72
- data/lib/aidp/job_manager.rb +0 -41
- data/lib/aidp/jobs/base_job.rb +0 -45
- data/lib/aidp/jobs/provider_execution_job.rb +0 -83
- data/lib/aidp/project_detector.rb +0 -117
- data/lib/aidp/providers/agent_supervisor.rb +0 -348
- data/lib/aidp/providers/supervised_base.rb +0 -317
- data/lib/aidp/providers/supervised_cursor.rb +0 -22
- data/lib/aidp/sync.rb +0 -13
- data/lib/aidp/workspace.rb +0 -19
@@ -1,418 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
require "csv"
|
5
|
-
require "yaml"
|
6
|
-
|
7
|
-
module Aidp
|
8
|
-
class ExportManager
|
9
|
-
# Supported export formats
|
10
|
-
SUPPORTED_FORMATS = %w[json csv yaml xml].freeze
|
11
|
-
|
12
|
-
def initialize(project_dir = Dir.pwd, config = {})
|
13
|
-
@project_dir = project_dir
|
14
|
-
@config = config
|
15
|
-
@output_dir = config[:output_dir] || File.join(project_dir, "exports")
|
16
|
-
end
|
17
|
-
|
18
|
-
# Export analysis data to JSON format
|
19
|
-
def export_to_json(data, options = {})
|
20
|
-
export_data(data, "json", options) do |formatted_data|
|
21
|
-
JSON.pretty_generate(formatted_data)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
# Export analysis data to CSV format
|
26
|
-
def export_to_csv(data, options = {})
|
27
|
-
export_data(data, "csv", options) do |formatted_data|
|
28
|
-
generate_csv_content(formatted_data, options)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
# Export analysis data to YAML format
|
33
|
-
def export_to_yaml(data, options = {})
|
34
|
-
export_data(data, "yaml", options) do |formatted_data|
|
35
|
-
YAML.dump(formatted_data)
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
# Export analysis data to XML format
|
40
|
-
def export_to_xml(data, options = {})
|
41
|
-
export_data(data, "xml", options) do |formatted_data|
|
42
|
-
generate_xml_content(formatted_data, options)
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
# Export analysis data to multiple formats
|
47
|
-
def export_to_multiple_formats(data, formats, options = {})
|
48
|
-
results = {}
|
49
|
-
|
50
|
-
formats.each do |format|
|
51
|
-
case format.downcase
|
52
|
-
when "json"
|
53
|
-
results[:json] = export_to_json(data, options)
|
54
|
-
when "csv"
|
55
|
-
results[:csv] = export_to_csv(data, options)
|
56
|
-
when "yaml"
|
57
|
-
results[:yaml] = export_to_yaml(data, options)
|
58
|
-
when "xml"
|
59
|
-
results[:xml] = export_to_xml(data, options)
|
60
|
-
else
|
61
|
-
results[format] = {error: "Unsupported format: #{format}"}
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
results
|
66
|
-
end
|
67
|
-
|
68
|
-
# Export specific analysis components
|
69
|
-
def export_analysis_component(component_name, data, format, options = {})
|
70
|
-
component_data = extract_component_data(component_name, data)
|
71
|
-
|
72
|
-
case format.downcase
|
73
|
-
when "json"
|
74
|
-
export_to_json(component_data, options.merge(component: component_name))
|
75
|
-
when "csv"
|
76
|
-
export_to_csv(component_data, options.merge(component: component_name))
|
77
|
-
when "yaml"
|
78
|
-
export_to_yaml(component_data, options.merge(component: component_name))
|
79
|
-
when "xml"
|
80
|
-
export_to_xml(component_data, options.merge(component: component_name))
|
81
|
-
else
|
82
|
-
{error: "Unsupported format: #{format}"}
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# Export metrics data
|
87
|
-
def export_metrics(metrics_data, format, options = {})
|
88
|
-
formatted_metrics = format_metrics_data(metrics_data)
|
89
|
-
|
90
|
-
case format.downcase
|
91
|
-
when "json"
|
92
|
-
export_to_json(formatted_metrics, options.merge(type: "metrics"))
|
93
|
-
when "csv"
|
94
|
-
export_to_csv(formatted_metrics, options.merge(type: "metrics"))
|
95
|
-
when "yaml"
|
96
|
-
export_to_yaml(formatted_metrics, options.merge(type: "metrics"))
|
97
|
-
when "xml"
|
98
|
-
export_to_xml(formatted_metrics, options.merge(type: "metrics"))
|
99
|
-
else
|
100
|
-
{error: "Unsupported format: #{format}"}
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# Export comparison data
|
105
|
-
def export_comparison(before_data, after_data, format, options = {})
|
106
|
-
comparison_data = prepare_comparison_data(before_data, after_data)
|
107
|
-
|
108
|
-
case format.downcase
|
109
|
-
when "json"
|
110
|
-
export_to_json(comparison_data, options.merge(type: "comparison"))
|
111
|
-
when "csv"
|
112
|
-
export_to_csv(comparison_data, options.merge(type: "comparison"))
|
113
|
-
when "yaml"
|
114
|
-
export_to_yaml(comparison_data, options.merge(type: "comparison"))
|
115
|
-
when "xml"
|
116
|
-
export_to_xml(comparison_data, options.merge(type: "comparison"))
|
117
|
-
else
|
118
|
-
{error: "Unsupported format: #{format}"}
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
# Generate export summary
|
123
|
-
def generate_export_summary(exports)
|
124
|
-
{
|
125
|
-
total_exports: exports.length,
|
126
|
-
successful_exports: exports.count { |_, result| result[:success] },
|
127
|
-
failed_exports: exports.count { |_, result| !result[:success] },
|
128
|
-
formats_used: exports.keys.uniq,
|
129
|
-
files_generated: exports.values.filter_map { |result| result[:path] },
|
130
|
-
generated_at: Time.now
|
131
|
-
}
|
132
|
-
end
|
133
|
-
|
134
|
-
private
|
135
|
-
|
136
|
-
def export_data(data, format, options)
|
137
|
-
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
138
|
-
component = options[:component] || "analysis"
|
139
|
-
type = options[:type] || "data"
|
140
|
-
|
141
|
-
filename = options[:filename] || "#{component}_#{type}_#{timestamp}.#{format}"
|
142
|
-
output_path = File.join(@output_dir, filename)
|
143
|
-
|
144
|
-
# Ensure output directory exists
|
145
|
-
FileUtils.mkdir_p(@output_dir)
|
146
|
-
|
147
|
-
# Format and write data
|
148
|
-
formatted_data = format_export_data(data, options)
|
149
|
-
content = yield(formatted_data)
|
150
|
-
|
151
|
-
File.write(output_path, content)
|
152
|
-
|
153
|
-
{
|
154
|
-
success: true,
|
155
|
-
path: output_path,
|
156
|
-
format: format,
|
157
|
-
filename: filename,
|
158
|
-
size: File.size(output_path),
|
159
|
-
generated_at: Time.now
|
160
|
-
}
|
161
|
-
end
|
162
|
-
|
163
|
-
def format_export_data(data, options)
|
164
|
-
formatted = {
|
165
|
-
metadata: {
|
166
|
-
exported_at: Time.now.iso8601,
|
167
|
-
project_name: File.basename(@project_dir),
|
168
|
-
project_path: @project_dir,
|
169
|
-
export_type: options[:type] || "analysis",
|
170
|
-
component: options[:component]
|
171
|
-
},
|
172
|
-
data: data
|
173
|
-
}
|
174
|
-
|
175
|
-
# Add configuration if requested
|
176
|
-
formatted[:config] = @config if options[:include_config]
|
177
|
-
|
178
|
-
formatted
|
179
|
-
end
|
180
|
-
|
181
|
-
def generate_csv_content(data, options)
|
182
|
-
# Flatten nested data for CSV export
|
183
|
-
flattened_data = flatten_data_for_csv(data)
|
184
|
-
|
185
|
-
return "" if flattened_data.empty?
|
186
|
-
|
187
|
-
# Generate CSV content
|
188
|
-
CSV.generate do |csv|
|
189
|
-
# Add headers
|
190
|
-
headers = flattened_data.first.keys
|
191
|
-
csv << headers
|
192
|
-
|
193
|
-
# Add data rows
|
194
|
-
flattened_data.each do |row|
|
195
|
-
csv << headers.map { |header| row[header] }
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
def flatten_data_for_csv(data)
|
201
|
-
flattened = []
|
202
|
-
|
203
|
-
if data[:data].is_a?(Array)
|
204
|
-
# Handle array data
|
205
|
-
data[:data].each_with_index do |item, index|
|
206
|
-
flattened_item = flatten_hash(item)
|
207
|
-
flattened_item["index"] = index
|
208
|
-
flattened << flattened_item
|
209
|
-
end
|
210
|
-
elsif data[:data].is_a?(Hash)
|
211
|
-
# Handle hash data
|
212
|
-
flattened << flatten_hash(data[:data])
|
213
|
-
end
|
214
|
-
|
215
|
-
flattened
|
216
|
-
end
|
217
|
-
|
218
|
-
def flatten_hash(hash, prefix = "")
|
219
|
-
flattened = {}
|
220
|
-
|
221
|
-
hash.each do |key, value|
|
222
|
-
new_key = prefix.empty? ? key.to_s : "#{prefix}_#{key}"
|
223
|
-
|
224
|
-
case value
|
225
|
-
when Hash
|
226
|
-
flattened.merge!(flatten_hash(value, new_key))
|
227
|
-
when Array
|
228
|
-
if value.all?(Hash)
|
229
|
-
# Array of hashes - create separate rows
|
230
|
-
value.each_with_index do |item, index|
|
231
|
-
item_flattened = flatten_hash(item, "#{new_key}_#{index}")
|
232
|
-
flattened.merge!(item_flattened)
|
233
|
-
end
|
234
|
-
else
|
235
|
-
# Simple array - join with semicolon
|
236
|
-
flattened[new_key] = value.join("; ")
|
237
|
-
end
|
238
|
-
else
|
239
|
-
flattened[new_key] = value
|
240
|
-
end
|
241
|
-
end
|
242
|
-
|
243
|
-
flattened
|
244
|
-
end
|
245
|
-
|
246
|
-
def generate_xml_content(data, options)
|
247
|
-
require "rexml/document"
|
248
|
-
|
249
|
-
doc = REXML::Document.new
|
250
|
-
doc.add_element("analysis_export")
|
251
|
-
|
252
|
-
# Add metadata
|
253
|
-
metadata_elem = doc.root.add_element("metadata")
|
254
|
-
data[:metadata].each do |key, value|
|
255
|
-
elem = metadata_elem.add_element(key.to_s)
|
256
|
-
elem.text = value.to_s
|
257
|
-
end
|
258
|
-
|
259
|
-
# Add data
|
260
|
-
data_elem = doc.root.add_element("data")
|
261
|
-
add_xml_element(data_elem, data[:data])
|
262
|
-
|
263
|
-
# Format XML
|
264
|
-
formatter = REXML::Formatters::Pretty.new(2)
|
265
|
-
formatter.compact = true
|
266
|
-
output = ""
|
267
|
-
formatter.write(doc, output)
|
268
|
-
|
269
|
-
output
|
270
|
-
end
|
271
|
-
|
272
|
-
def add_xml_element(parent, data)
|
273
|
-
case data
|
274
|
-
when Hash
|
275
|
-
data.each do |key, value|
|
276
|
-
elem = parent.add_element(key.to_s)
|
277
|
-
add_xml_element(elem, value)
|
278
|
-
end
|
279
|
-
when Array
|
280
|
-
data.each_with_index do |item, index|
|
281
|
-
elem = parent.add_element("item_#{index}")
|
282
|
-
add_xml_element(elem, item)
|
283
|
-
end
|
284
|
-
else
|
285
|
-
parent.text = data.to_s
|
286
|
-
end
|
287
|
-
end
|
288
|
-
|
289
|
-
def extract_component_data(component_name, data)
|
290
|
-
case component_name
|
291
|
-
when "repository_analysis"
|
292
|
-
data[:repository_analysis] || {}
|
293
|
-
when "architecture_analysis"
|
294
|
-
data[:architecture_analysis] || {}
|
295
|
-
when "static_analysis"
|
296
|
-
data[:static_analysis] || {}
|
297
|
-
when "security_analysis"
|
298
|
-
data[:security_analysis] || {}
|
299
|
-
when "performance_analysis"
|
300
|
-
data[:performance_analysis] || {}
|
301
|
-
when "test_coverage"
|
302
|
-
data[:test_coverage] || {}
|
303
|
-
when "refactoring_recommendations"
|
304
|
-
data[:refactoring_recommendations] || {}
|
305
|
-
when "modernization_recommendations"
|
306
|
-
data[:modernization_recommendations] || {}
|
307
|
-
else
|
308
|
-
data[component_name.to_sym] || {}
|
309
|
-
end
|
310
|
-
end
|
311
|
-
|
312
|
-
def format_metrics_data(metrics_data)
|
313
|
-
formatted = {
|
314
|
-
metrics: {},
|
315
|
-
summary: {},
|
316
|
-
trends: {}
|
317
|
-
}
|
318
|
-
|
319
|
-
if metrics_data.is_a?(Hash)
|
320
|
-
metrics_data.each do |category, metrics|
|
321
|
-
if metrics.is_a?(Hash)
|
322
|
-
formatted[:metrics][category] = metrics
|
323
|
-
else
|
324
|
-
formatted[:summary][category] = metrics
|
325
|
-
end
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
|
-
formatted
|
330
|
-
end
|
331
|
-
|
332
|
-
def prepare_comparison_data(before_data, after_data)
|
333
|
-
{
|
334
|
-
before: before_data,
|
335
|
-
after: after_data,
|
336
|
-
changes: calculate_changes(before_data, after_data),
|
337
|
-
improvements: identify_improvements(before_data, after_data),
|
338
|
-
regressions: identify_regressions(before_data, after_data)
|
339
|
-
}
|
340
|
-
end
|
341
|
-
|
342
|
-
def calculate_changes(before_data, after_data)
|
343
|
-
changes = {}
|
344
|
-
|
345
|
-
# Compare metrics
|
346
|
-
before_metrics = extract_metrics(before_data)
|
347
|
-
after_metrics = extract_metrics(after_data)
|
348
|
-
|
349
|
-
all_metrics = (before_metrics.keys + after_metrics.keys).uniq
|
350
|
-
|
351
|
-
all_metrics.each do |metric|
|
352
|
-
before_value = before_metrics[metric]
|
353
|
-
after_value = after_metrics[metric]
|
354
|
-
|
355
|
-
next unless before_value && after_value
|
356
|
-
|
357
|
-
changes[metric] = {
|
358
|
-
before: before_value,
|
359
|
-
after: after_value,
|
360
|
-
change: after_value - before_value,
|
361
|
-
percentage_change: ((after_value - before_value) / before_value * 100).round(2)
|
362
|
-
}
|
363
|
-
end
|
364
|
-
|
365
|
-
changes
|
366
|
-
end
|
367
|
-
|
368
|
-
def identify_improvements(before_data, after_data)
|
369
|
-
improvements = []
|
370
|
-
|
371
|
-
changes = calculate_changes(before_data, after_data)
|
372
|
-
changes.each do |metric, change|
|
373
|
-
next unless change[:change] > 0
|
374
|
-
|
375
|
-
improvements << {
|
376
|
-
metric: metric,
|
377
|
-
improvement: change[:change],
|
378
|
-
percentage: change[:percentage_change]
|
379
|
-
}
|
380
|
-
end
|
381
|
-
|
382
|
-
improvements
|
383
|
-
end
|
384
|
-
|
385
|
-
def identify_regressions(before_data, after_data)
|
386
|
-
regressions = []
|
387
|
-
|
388
|
-
changes = calculate_changes(before_data, after_data)
|
389
|
-
changes.each do |metric, change|
|
390
|
-
next unless change[:change] < 0
|
391
|
-
|
392
|
-
regressions << {
|
393
|
-
metric: metric,
|
394
|
-
regression: change[:change].abs,
|
395
|
-
percentage: change[:percentage_change].abs
|
396
|
-
}
|
397
|
-
end
|
398
|
-
|
399
|
-
regressions
|
400
|
-
end
|
401
|
-
|
402
|
-
def extract_metrics(data)
|
403
|
-
metrics = {}
|
404
|
-
|
405
|
-
if data.is_a?(Hash)
|
406
|
-
data.each do |key, value|
|
407
|
-
if value.is_a?(Numeric)
|
408
|
-
metrics[key] = value
|
409
|
-
elsif value.is_a?(Hash)
|
410
|
-
metrics.merge!(extract_metrics(value))
|
411
|
-
end
|
412
|
-
end
|
413
|
-
end
|
414
|
-
|
415
|
-
metrics
|
416
|
-
end
|
417
|
-
end
|
418
|
-
end
|