appydave-tools 0.70.0 → 0.71.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/brainstorming-agent.md +227 -0
  3. data/.claude/commands/cli-test.md +251 -0
  4. data/.claude/commands/dev.md +234 -0
  5. data/.claude/commands/po.md +227 -0
  6. data/.claude/commands/progress.md +51 -0
  7. data/.claude/commands/uat.md +321 -0
  8. data/.rubocop.yml +9 -0
  9. data/AGENTS.md +43 -0
  10. data/CHANGELOG.md +12 -0
  11. data/CLAUDE.md +26 -3
  12. data/README.md +15 -0
  13. data/bin/dam +21 -1
  14. data/bin/jump.rb +29 -0
  15. data/bin/subtitle_processor.rb +54 -1
  16. data/bin/zsh_history.rb +846 -0
  17. data/docs/README.md +162 -69
  18. data/docs/architecture/cli/exe-bin-convention.md +434 -0
  19. data/docs/architecture/cli-patterns.md +631 -0
  20. data/docs/architecture/gpt-context/gpt-context-architecture.md +325 -0
  21. data/docs/architecture/gpt-context/gpt-context-implementation-guide.md +419 -0
  22. data/docs/architecture/gpt-context/gpt-context-vision.md +179 -0
  23. data/docs/architecture/testing/testing-patterns.md +762 -0
  24. data/docs/backlog.md +120 -0
  25. data/docs/cli-tests/FR-3-jump-location-tool.md +515 -0
  26. data/docs/specs/fr-002-gpt-context-help-system.md +265 -0
  27. data/docs/specs/fr-003-jump-location-tool.md +779 -0
  28. data/docs/specs/zsh-history-tool.md +820 -0
  29. data/docs/uat/FR-3-jump-location-tool.md +741 -0
  30. data/exe/jump +11 -0
  31. data/exe/{subtitle_manager → subtitle_processor} +1 -1
  32. data/exe/zsh_history +11 -0
  33. data/lib/appydave/tools/configuration/openai.rb +1 -1
  34. data/lib/appydave/tools/dam/file_helper.rb +28 -0
  35. data/lib/appydave/tools/dam/project_listing.rb +4 -30
  36. data/lib/appydave/tools/dam/s3_operations.rb +2 -1
  37. data/lib/appydave/tools/dam/ssd_status.rb +226 -0
  38. data/lib/appydave/tools/dam/status.rb +3 -51
  39. data/lib/appydave/tools/jump/cli.rb +561 -0
  40. data/lib/appydave/tools/jump/commands/add.rb +52 -0
  41. data/lib/appydave/tools/jump/commands/base.rb +43 -0
  42. data/lib/appydave/tools/jump/commands/generate.rb +153 -0
  43. data/lib/appydave/tools/jump/commands/remove.rb +58 -0
  44. data/lib/appydave/tools/jump/commands/report.rb +214 -0
  45. data/lib/appydave/tools/jump/commands/update.rb +42 -0
  46. data/lib/appydave/tools/jump/commands/validate.rb +54 -0
  47. data/lib/appydave/tools/jump/config.rb +233 -0
  48. data/lib/appydave/tools/jump/formatters/base.rb +48 -0
  49. data/lib/appydave/tools/jump/formatters/json_formatter.rb +19 -0
  50. data/lib/appydave/tools/jump/formatters/paths_formatter.rb +21 -0
  51. data/lib/appydave/tools/jump/formatters/table_formatter.rb +183 -0
  52. data/lib/appydave/tools/jump/location.rb +134 -0
  53. data/lib/appydave/tools/jump/path_validator.rb +47 -0
  54. data/lib/appydave/tools/jump/search.rb +230 -0
  55. data/lib/appydave/tools/subtitle_processor/transcript.rb +51 -0
  56. data/lib/appydave/tools/version.rb +1 -1
  57. data/lib/appydave/tools/zsh_history/command.rb +37 -0
  58. data/lib/appydave/tools/zsh_history/config.rb +235 -0
  59. data/lib/appydave/tools/zsh_history/filter.rb +184 -0
  60. data/lib/appydave/tools/zsh_history/formatter.rb +75 -0
  61. data/lib/appydave/tools/zsh_history/parser.rb +101 -0
  62. data/lib/appydave/tools.rb +25 -0
  63. data/package.json +1 -1
  64. metadata +51 -4
@@ -0,0 +1,561 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Jump
6
+ # CLI provides the command-line interface for the Jump tool
7
+ #
8
+ # Uses the Method Dispatch (Full) pattern for 10+ commands with
9
+ # hierarchical help system.
10
+ #
11
+ # @example Usage
12
+ # cli = CLI.new
13
+ # cli.run(['search', 'appydave', 'ruby'])
14
+ # cli.run(['add', '--key', 'my-project', '--path', '~/dev/project'])
15
+ class CLI
16
+ EXIT_SUCCESS = 0
17
+ EXIT_NOT_FOUND = 1
18
+ EXIT_INVALID_INPUT = 2
19
+ EXIT_CONFIG_ERROR = 3
20
+ EXIT_PATH_NOT_FOUND = 4
21
+
22
+ attr_reader :config, :path_validator, :output
23
+
24
+ def initialize(config: nil, path_validator: nil, output: $stdout)
25
+ @path_validator = path_validator || PathValidator.new
26
+ @output = output
27
+ @config = config
28
+ end
29
+
30
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
31
+ def run(args = ARGV)
32
+ command = args.shift
33
+
34
+ case command
35
+ when nil, '', '--help', '-h'
36
+ show_main_help
37
+ EXIT_SUCCESS
38
+ when '--version', '-v'
39
+ show_version
40
+ EXIT_SUCCESS
41
+ when 'help'
42
+ show_help(args)
43
+ EXIT_SUCCESS
44
+ when 'search'
45
+ run_search(args)
46
+ when 'get'
47
+ run_get(args)
48
+ when 'list'
49
+ run_list(args)
50
+ when 'add'
51
+ run_add(args)
52
+ when 'update'
53
+ run_update(args)
54
+ when 'remove'
55
+ run_remove(args)
56
+ when 'validate'
57
+ run_validate(args)
58
+ when 'report'
59
+ run_report(args)
60
+ when 'generate'
61
+ run_generate(args)
62
+ when 'info'
63
+ run_info(args)
64
+ else
65
+ output.puts "Unknown command: #{command}"
66
+ output.puts "Run 'jump help' for available commands."
67
+ EXIT_INVALID_INPUT
68
+ end
69
+ rescue StandardError => e
70
+ output.puts "Error: #{e.message}"
71
+ output.puts e.backtrace.first(3).join("\n") if ENV['DEBUG']
72
+ EXIT_CONFIG_ERROR
73
+ end
74
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/AbcSize
75
+
76
+ private
77
+
78
+ def load_config
79
+ @config ||= Config.new
80
+ end
81
+
82
+ def format_option(args)
83
+ format_index = args.index('--format') || args.index('-f')
84
+ return 'table' unless format_index
85
+
86
+ format = args[format_index + 1]
87
+ args.delete_at(format_index + 1)
88
+ args.delete_at(format_index)
89
+ format || 'table'
90
+ end
91
+
92
+ def format_output(result, format)
93
+ formatter = case format
94
+ when 'json'
95
+ Formatters::JsonFormatter.new(result)
96
+ when 'paths'
97
+ Formatters::PathsFormatter.new(result)
98
+ else
99
+ Formatters::TableFormatter.new(result)
100
+ end
101
+
102
+ output.puts formatter.format
103
+ end
104
+
105
+ def exit_code_for(result)
106
+ return EXIT_SUCCESS if result[:success]
107
+
108
+ case result[:code]
109
+ when 'NOT_FOUND'
110
+ EXIT_NOT_FOUND
111
+ when 'INVALID_INPUT', 'DUPLICATE_KEY', 'CONFIRMATION_REQUIRED'
112
+ EXIT_INVALID_INPUT
113
+ when 'PATH_NOT_FOUND'
114
+ EXIT_PATH_NOT_FOUND
115
+ else
116
+ EXIT_CONFIG_ERROR
117
+ end
118
+ end
119
+
120
+ # Command implementations
121
+
122
+ def run_search(args)
123
+ format = format_option(args)
124
+ query = args.join(' ')
125
+
126
+ search = Search.new(load_config)
127
+ result = search.search(query)
128
+
129
+ format_output(result, format)
130
+ exit_code_for(result)
131
+ end
132
+
133
+ def run_get(args)
134
+ format = format_option(args)
135
+ key = args.first
136
+
137
+ unless key
138
+ output.puts 'Usage: jump get <key>'
139
+ return EXIT_INVALID_INPUT
140
+ end
141
+
142
+ search = Search.new(load_config)
143
+ result = search.get(key)
144
+
145
+ format_output(result, format)
146
+ exit_code_for(result)
147
+ end
148
+
149
+ def run_list(args)
150
+ format = format_option(args)
151
+
152
+ search = Search.new(load_config)
153
+ result = search.list
154
+
155
+ format_output(result, format)
156
+ exit_code_for(result)
157
+ end
158
+
159
+ def run_add(args)
160
+ format = format_option(args)
161
+ attrs = parse_location_args(args)
162
+
163
+ if attrs[:key].nil?
164
+ output.puts 'Usage: jump add --key <key> --path <path> [options]'
165
+ return EXIT_INVALID_INPUT
166
+ end
167
+
168
+ cmd = Commands::Add.new(load_config, attrs, path_validator: path_validator)
169
+ result = cmd.run
170
+
171
+ format_output(result, format)
172
+ exit_code_for(result)
173
+ end
174
+
175
+ def run_update(args)
176
+ format = format_option(args)
177
+ key = args.shift
178
+ attrs = parse_location_args(args)
179
+
180
+ unless key
181
+ output.puts 'Usage: jump update <key> [options]'
182
+ return EXIT_INVALID_INPUT
183
+ end
184
+
185
+ cmd = Commands::Update.new(load_config, key, attrs, path_validator: path_validator)
186
+ result = cmd.run
187
+
188
+ format_output(result, format)
189
+ exit_code_for(result)
190
+ end
191
+
192
+ def run_remove(args)
193
+ format = format_option(args)
194
+ force = args.delete('--force')
195
+ key = args.first
196
+
197
+ unless key
198
+ output.puts 'Usage: jump remove <key> [--force]'
199
+ return EXIT_INVALID_INPUT
200
+ end
201
+
202
+ cmd = Commands::Remove.new(load_config, key, force: !force.nil?, path_validator: path_validator)
203
+ result = cmd.run
204
+
205
+ format_output(result, format)
206
+ exit_code_for(result)
207
+ end
208
+
209
+ def run_validate(args)
210
+ format = format_option(args)
211
+ key = args.first
212
+
213
+ cmd = Commands::Validate.new(load_config, key: key, path_validator: path_validator)
214
+ result = cmd.run
215
+
216
+ format_output(result, format)
217
+ exit_code_for(result)
218
+ end
219
+
220
+ def run_report(args)
221
+ format = format_option(args)
222
+ report_type = args.shift
223
+ filter = args.first
224
+
225
+ unless report_type
226
+ output.puts 'Usage: jump report <type> [filter]'
227
+ output.puts 'Types: categories, brands, clients, types, tags, by-brand, by-client, by-type, by-tag, summary'
228
+ return EXIT_INVALID_INPUT
229
+ end
230
+
231
+ cmd = Commands::Report.new(load_config, report_type, filter: filter, path_validator: path_validator)
232
+ result = cmd.run
233
+
234
+ format_output(result, format)
235
+ exit_code_for(result)
236
+ end
237
+
238
+ def run_generate(args)
239
+ format = format_option(args)
240
+ target = args.shift
241
+
242
+ # Parse output options
243
+ output_path = extract_option(args, '--output')
244
+ output_dir = extract_option(args, '--output-dir')
245
+
246
+ unless target
247
+ output.puts 'Usage: jump generate <target> [--output <file>] [--output-dir <dir>]'
248
+ output.puts 'Targets: aliases, help, all'
249
+ return EXIT_INVALID_INPUT
250
+ end
251
+
252
+ cmd = Commands::Generate.new(
253
+ load_config,
254
+ target,
255
+ output_path: output_path,
256
+ output_dir: output_dir,
257
+ path_validator: path_validator
258
+ )
259
+ result = cmd.run
260
+
261
+ # For generate, if content is present (stdout mode), show it directly
262
+ if result[:content] && !output_path && !output_dir
263
+ output.puts result[:content]
264
+ else
265
+ format_output(result, format)
266
+ end
267
+
268
+ exit_code_for(result)
269
+ end
270
+
271
+ def run_info(args)
272
+ format = format_option(args)
273
+ result = {
274
+ success: true,
275
+ **load_config.info
276
+ }
277
+
278
+ format_output(result, format)
279
+ EXIT_SUCCESS
280
+ end
281
+
282
+ # Argument parsing helpers
283
+
284
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
285
+ def parse_location_args(args)
286
+ attrs = {}
287
+
288
+ i = 0
289
+ while i < args.length
290
+ case args[i]
291
+ when '--key', '-k'
292
+ attrs[:key] = args[i + 1]
293
+ i += 2
294
+ when '--path', '-p'
295
+ attrs[:path] = args[i + 1]
296
+ i += 2
297
+ when '--jump', '-j'
298
+ attrs[:jump] = args[i + 1]
299
+ i += 2
300
+ when '--brand', '-b'
301
+ attrs[:brand] = args[i + 1]
302
+ i += 2
303
+ when '--client', '-c'
304
+ attrs[:client] = args[i + 1]
305
+ i += 2
306
+ when '--type', '-t'
307
+ attrs[:type] = args[i + 1]
308
+ i += 2
309
+ when '--tags'
310
+ attrs[:tags] = args[i + 1]&.split(',')&.map(&:strip)
311
+ i += 2
312
+ when '--description', '-d'
313
+ attrs[:description] = args[i + 1]
314
+ i += 2
315
+ else
316
+ i += 1
317
+ end
318
+ end
319
+
320
+ attrs
321
+ end
322
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
323
+
324
+ def extract_option(args, flag)
325
+ index = args.index(flag)
326
+ return nil unless index
327
+
328
+ value = args[index + 1]
329
+ args.delete_at(index + 1)
330
+ args.delete_at(index)
331
+ value
332
+ end
333
+
334
+ # Help display methods
335
+
336
+ def show_version
337
+ output.puts "Jump Location Tool v#{Appydave::Tools::VERSION}"
338
+ output.puts 'Part of appydave-tools gem'
339
+ end
340
+
341
+ # rubocop:disable Metrics/MethodLength
342
+ def show_main_help
343
+ output.puts <<~HELP
344
+ Jump - Development Folder Location Manager
345
+
346
+ Usage: jump <command> [options]
347
+
348
+ Search & Retrieval:
349
+ search <terms> Fuzzy search across all location metadata
350
+ get <key> Get location by exact key
351
+ list List all locations
352
+
353
+ CRUD Operations:
354
+ add Add a new location
355
+ update <key> Update an existing location
356
+ remove <key> Remove a location (requires --force)
357
+
358
+ Validation:
359
+ validate [key] Check if paths exist
360
+
361
+ Reports:
362
+ report <type> Generate reports (brands, clients, types, tags, etc.)
363
+
364
+ Generation:
365
+ generate <target> Generate shell aliases or help content
366
+
367
+ Info:
368
+ info Show configuration info
369
+
370
+ Options:
371
+ --format <fmt> Output format: table (default), json, paths
372
+ --help, -h Show this help
373
+ --version, -v Show version
374
+
375
+ Examples:
376
+ jump search appydave ruby
377
+ jump get ad-tools
378
+ jump add --key my-proj --path ~/dev/my-proj --brand appydave
379
+ jump report brands
380
+ jump generate aliases --output ~/.oh-my-zsh/custom/aliases-jump.zsh
381
+
382
+ Run 'jump help <command>' for detailed command help.
383
+ HELP
384
+ end
385
+ # rubocop:enable Metrics/MethodLength
386
+
387
+ # rubocop:disable Metrics/CyclomaticComplexity
388
+ def show_help(args)
389
+ topic = args.first
390
+
391
+ case topic
392
+ when 'search'
393
+ show_search_help
394
+ when 'add'
395
+ show_add_help
396
+ when 'update'
397
+ show_update_help
398
+ when 'remove'
399
+ show_remove_help
400
+ when 'validate'
401
+ show_validate_help
402
+ when 'report'
403
+ show_report_help
404
+ when 'generate'
405
+ show_generate_help
406
+ else
407
+ show_main_help
408
+ end
409
+ end
410
+ # rubocop:enable Metrics/CyclomaticComplexity
411
+
412
+ def show_search_help
413
+ output.puts <<~HELP
414
+ jump search - Fuzzy search locations
415
+
416
+ Usage: jump search <terms> [--format json|table|paths]
417
+
418
+ Searches across all location metadata:
419
+ - Key, path, brand, client, type, tags, description
420
+
421
+ Scoring:
422
+ - Exact key match: 100 points
423
+ - Key contains term: 50 points
424
+ - Brand/client alias: 40 points
425
+ - Tag match: 30 points
426
+ - Type match: 20 points
427
+ - Description contains: 10 points
428
+ - Path contains: 5 points
429
+
430
+ Examples:
431
+ jump search appydave
432
+ jump search ruby cli
433
+ jump search ss app --format json
434
+ HELP
435
+ end
436
+
437
+ def show_add_help
438
+ output.puts <<~HELP
439
+ jump add - Add a new location
440
+
441
+ Usage: jump add --key <key> --path <path> [options]
442
+
443
+ Required:
444
+ --key, -k <key> Unique identifier (lowercase, alphanumeric, hyphens)
445
+ --path, -p <path> Directory path (must start with ~ or /)
446
+
447
+ Optional:
448
+ --jump, -j <alias> Shell alias (default: j + key)
449
+ --brand, -b <brand> Associated brand
450
+ --client, -c <client> Associated client
451
+ --type, -t <type> Location type (tool, gem, brand, etc.)
452
+ --tags <t1,t2> Comma-separated tags
453
+ --description, -d Human description
454
+
455
+ Examples:
456
+ jump add --key my-project --path ~/dev/my-project
457
+ jump add -k ad-tools -p ~/dev/ad/appydave-tools --brand appydave --type tool --tags ruby,cli
458
+ HELP
459
+ end
460
+
461
+ def show_update_help
462
+ output.puts <<~HELP
463
+ jump update - Update an existing location
464
+
465
+ Usage: jump update <key> [options]
466
+
467
+ Options:
468
+ --path, -p <path> New directory path
469
+ --jump, -j <alias> New shell alias
470
+ --brand, -b <brand> New brand
471
+ --client, -c <client> New client
472
+ --type, -t <type> New type
473
+ --tags <t1,t2> New tags (replaces existing)
474
+ --description, -d New description
475
+
476
+ Examples:
477
+ jump update my-project --description "Updated description"
478
+ jump update ad-tools --tags ruby,cli,youtube
479
+ HELP
480
+ end
481
+
482
+ def show_remove_help
483
+ output.puts <<~HELP
484
+ jump remove - Remove a location
485
+
486
+ Usage: jump remove <key> --force
487
+
488
+ The --force flag is required to prevent accidental deletions.
489
+
490
+ Examples:
491
+ jump remove old-project --force
492
+ HELP
493
+ end
494
+
495
+ def show_validate_help
496
+ output.puts <<~HELP
497
+ jump validate - Check if paths exist
498
+
499
+ Usage: jump validate [key]
500
+
501
+ Without key: validates all locations
502
+ With key: validates specific location
503
+
504
+ Examples:
505
+ jump validate
506
+ jump validate ad-tools
507
+ jump validate --format json
508
+ HELP
509
+ end
510
+
511
+ def show_report_help
512
+ output.puts <<~HELP
513
+ jump report - Generate reports
514
+
515
+ Usage: jump report <type> [filter]
516
+
517
+ Report Types:
518
+ categories List all category definitions
519
+ brands List brands with location counts
520
+ clients List clients with location counts
521
+ types List types with location counts
522
+ tags List tags with location counts
523
+ by-brand Group locations by brand
524
+ by-client Group locations by client
525
+ by-type Group locations by type
526
+ by-tag Group locations by tag
527
+ summary Overview of all data
528
+
529
+ Examples:
530
+ jump report brands
531
+ jump report by-brand appydave
532
+ jump report tags --format json
533
+ HELP
534
+ end
535
+
536
+ def show_generate_help
537
+ output.puts <<~HELP
538
+ jump generate - Generate shell aliases or help content
539
+
540
+ Usage: jump generate <target> [options]
541
+
542
+ Targets:
543
+ aliases Generate shell alias file (aliases-jump.zsh)
544
+ help Generate help content for fzf (jump-help.txt)
545
+ all Generate both files
546
+
547
+ Options:
548
+ --output <file> Write to specific file (for aliases, help)
549
+ --output-dir <dir> Write to directory (for all)
550
+
551
+ Examples:
552
+ jump generate aliases
553
+ jump generate aliases --output ~/.oh-my-zsh/custom/aliases-jump.zsh
554
+ jump generate help --output ~/.oh-my-zsh/custom/data/jump-help.txt
555
+ jump generate all --output-dir ~/.oh-my-zsh/custom/
556
+ HELP
557
+ end
558
+ end
559
+ end
560
+ end
561
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Jump
6
+ module Commands
7
+ # Add command creates a new location entry
8
+ class Add < Base
9
+ attr_reader :attrs
10
+
11
+ def initialize(config, attrs, path_validator: PathValidator.new, **options)
12
+ super(config, path_validator: path_validator, **options)
13
+ @attrs = attrs
14
+ end
15
+
16
+ def run
17
+ # Validate required fields
18
+ return error_result('Key is required', code: 'INVALID_INPUT') if attrs[:key].nil? || attrs[:key].empty?
19
+ return error_result('Path is required', code: 'INVALID_INPUT') if attrs[:path].nil? || attrs[:path].empty?
20
+
21
+ # Check for duplicate
22
+ return error_result("Location '#{attrs[:key]}' already exists", code: 'DUPLICATE_KEY') if config.key_exists?(attrs[:key])
23
+
24
+ # Validate path exists (optional warning)
25
+ unless path_validator.exists?(attrs[:path])
26
+ # Just warn, don't fail - path might be created later
27
+ @path_warning = "Warning: Path '#{attrs[:path]}' does not exist"
28
+ end
29
+
30
+ # Create and validate location
31
+ location = Location.new(attrs)
32
+ errors = location.validate
33
+ return error_result("Invalid location: #{errors.join(', ')}", code: 'INVALID_INPUT') unless errors.empty?
34
+
35
+ # Add to config
36
+ config.add(location)
37
+ config.save
38
+
39
+ result = success_result(
40
+ message: "Location '#{attrs[:key]}' added successfully",
41
+ location: location.to_h
42
+ )
43
+ result[:warning] = @path_warning if @path_warning
44
+ result
45
+ rescue ArgumentError => e
46
+ error_result(e.message, code: 'INVALID_INPUT')
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Jump
6
+ module Commands
7
+ # Base command class for Jump CLI commands
8
+ class Base
9
+ attr_reader :config, :path_validator, :options
10
+
11
+ def initialize(config, path_validator: PathValidator.new, **options)
12
+ @config = config
13
+ @path_validator = path_validator
14
+ @options = options
15
+ end
16
+
17
+ # Execute the command
18
+ #
19
+ # @return [Hash] Result hash with success status
20
+ def run
21
+ raise NotImplementedError, 'Subclasses must implement #run'
22
+ end
23
+
24
+ protected
25
+
26
+ def success_result(data = {})
27
+ { success: true }.merge(data)
28
+ end
29
+
30
+ def error_result(message, code: 'ERROR', suggestion: nil)
31
+ result = {
32
+ success: false,
33
+ error: message,
34
+ code: code
35
+ }
36
+ result[:suggestion] = suggestion if suggestion
37
+ result
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end