gsc-cli 2.1.0 → 2.2.2

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 (101) hide show
  1. checksums.yaml +4 -4
  2. data/AUTH.md +4 -1
  3. data/README.md +448 -408
  4. data/bin/gsc +28067 -5661
  5. data/dist/gsc +29121 -5046
  6. data/lib/gsc/aio_hunter.rb +343 -0
  7. data/lib/gsc/answer_synthesizer.rb +157 -0
  8. data/lib/gsc/api.rb +53 -1
  9. data/lib/gsc/auth.rb +26 -0
  10. data/lib/gsc/brand_segmenter.rb +140 -0
  11. data/lib/gsc/cache_manager.rb +806 -0
  12. data/lib/gsc/cannibalization_analyzer.rb +141 -0
  13. data/lib/gsc/canonical_chains.rb +367 -0
  14. data/lib/gsc/citation_simulator.rb +339 -0
  15. data/lib/gsc/cli/aio_hunter.rb +154 -0
  16. data/lib/gsc/cli/analytics.rb +788 -0
  17. data/lib/gsc/cli/audit.rb +1976 -0
  18. data/lib/gsc/cli/base.rb +384 -0
  19. data/lib/gsc/cli/cache.rb +266 -0
  20. data/lib/gsc/cli/canonical.rb +223 -0
  21. data/lib/gsc/cli/citation_simulator.rb +152 -0
  22. data/lib/gsc/cli/dashboard.rb +354 -0
  23. data/lib/gsc/cli/doctor.rb +129 -0
  24. data/lib/gsc/cli/eeat.rb +125 -0
  25. data/lib/gsc/cli/ga4.rb +852 -0
  26. data/lib/gsc/cli/growth.rb +650 -0
  27. data/lib/gsc/cli/hreflang.rb +164 -0
  28. data/lib/gsc/cli/image_seo.rb +162 -0
  29. data/lib/gsc/cli/indexing.rb +458 -0
  30. data/lib/gsc/cli/intent_shift.rb +125 -0
  31. data/lib/gsc/cli/keyword_value.rb +134 -0
  32. data/lib/gsc/cli/keywords.rb +795 -0
  33. data/lib/gsc/cli/landing_roi.rb +308 -0
  34. data/lib/gsc/cli/low_ctr.rb +213 -0
  35. data/lib/gsc/cli/mobile_parity.rb +150 -0
  36. data/lib/gsc/cli/report.rb +100 -0
  37. data/lib/gsc/cli/rich_results.rb +172 -0
  38. data/lib/gsc/cli/schema_generate.rb +149 -0
  39. data/lib/gsc/cli/seasonal.rb +232 -0
  40. data/lib/gsc/cli/security.rb +153 -0
  41. data/lib/gsc/cli/setup.rb +1291 -0
  42. data/lib/gsc/cli/sitemap_tree.rb +143 -0
  43. data/lib/gsc/cli/skill_pack.rb +62 -0
  44. data/lib/gsc/cli/soft_404.rb +199 -0
  45. data/lib/gsc/cli/sparkline.rb +227 -0
  46. data/lib/gsc/cli/watchdog.rb +150 -0
  47. data/lib/gsc/cli/zombie_purger.rb +208 -0
  48. data/lib/gsc/cli.rb +707 -5265
  49. data/lib/gsc/cli_advanced.rb +987 -44
  50. data/lib/gsc/client.rb +17 -2
  51. data/lib/gsc/color.rb +16 -1
  52. data/lib/gsc/command_registry.rb +47 -9
  53. data/lib/gsc/config.rb +2 -2
  54. data/lib/gsc/ctr_curve.rb +115 -0
  55. data/lib/gsc/decay_predictor.rb +322 -0
  56. data/lib/gsc/doctor.rb +434 -0
  57. data/lib/gsc/eeat_auditor.rb +428 -0
  58. data/lib/gsc/entity_auditor.rb +229 -0
  59. data/lib/gsc/firewall_scanner.rb +733 -0
  60. data/lib/gsc/geo_auditor.rb +368 -0
  61. data/lib/gsc/google_trends.rb +8 -1
  62. data/lib/gsc/heading_validator.rb +283 -0
  63. data/lib/gsc/hreflang_validator.rb +412 -0
  64. data/lib/gsc/image_seo.rb +286 -0
  65. data/lib/gsc/indexing_queue.rb +179 -0
  66. data/lib/gsc/indexnow.rb +93 -0
  67. data/lib/gsc/intent_shift.rb +188 -0
  68. data/lib/gsc/internal_links.rb +153 -36
  69. data/lib/gsc/keyword_value.rb +191 -0
  70. data/lib/gsc/landing_roi.rb +195 -0
  71. data/lib/gsc/llms_generator.rb +343 -22
  72. data/lib/gsc/low_ctr_rewriter.rb +408 -0
  73. data/lib/gsc/mobile_parity.rb +222 -0
  74. data/lib/gsc/network_tracer.rb +8 -1
  75. data/lib/gsc/page_analyzer.rb +47 -7
  76. data/lib/gsc/prompts.rb +38 -29
  77. data/lib/gsc/questions_harvester.rb +178 -0
  78. data/lib/gsc/report_generator.rb +461 -0
  79. data/lib/gsc/rich_results.rb +388 -0
  80. data/lib/gsc/robots_checker.rb +46 -15
  81. data/lib/gsc/schema_generator.rb +788 -0
  82. data/lib/gsc/schema_validator.rb +36 -38
  83. data/lib/gsc/seasonal_predictor.rb +381 -0
  84. data/lib/gsc/security_scanner.rb +496 -0
  85. data/lib/gsc/serp_feature_detector.rb +359 -0
  86. data/lib/gsc/serp_preview.rb +108 -22
  87. data/lib/gsc/site_crawler.rb +113 -21
  88. data/lib/gsc/sitemap_loader.rb +15 -4
  89. data/lib/gsc/sitemap_tree.rb +301 -0
  90. data/lib/gsc/skill_pack.rb +195 -0
  91. data/lib/gsc/soft_404_analyzer.rb +385 -0
  92. data/lib/gsc/sparkline.rb +171 -0
  93. data/lib/gsc/speed_correlator.rb +416 -0
  94. data/lib/gsc/striking_playbook.rb +190 -0
  95. data/lib/gsc/title_optimizer.rb +420 -0
  96. data/lib/gsc/vault.rb +260 -0
  97. data/lib/gsc/version.rb +1 -1
  98. data/lib/gsc/watchdog.rb +235 -0
  99. data/lib/gsc/zombie_purger.rb +366 -0
  100. data/lib/gsc.rb +118 -0
  101. metadata +75 -1
@@ -0,0 +1,223 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+ require_relative 'base'
6
+ require_relative '../canonical_chains'
7
+
8
+ module GSC
9
+ class CLI
10
+ module Canonical
11
+ module_function
12
+
13
+ def run(command, target, extra, options, api = nil, site_url = nil, hostname = nil)
14
+ target_url = target || options[:url]
15
+
16
+ if target_url && target_url =~ %r{^https?://}
17
+ # Single URL audit
18
+ handle_single_url_audit(target_url, options, api, site_url)
19
+ else
20
+ # Batch audit domain
21
+ domain = target_url || hostname || (options[:domain] rescue nil) || (site_url ? site_url.sub(%r{^https?://}, '').sub(%r{/$}, '') : nil)
22
+ handle_batch_domain_audit(domain, options, api, site_url)
23
+ end
24
+ end
25
+
26
+ def handle_single_url_audit(target, options, api = nil, site_url = nil)
27
+ unless options[:json]
28
+ puts "\n" + Color.cyan("=" * 80)
29
+ puts Color.bold(" CANONICAL LOOP & REDIRECT CHAIN BREAKER")
30
+ puts Color.cyan("=" * 80)
31
+ puts Color.gray(" Auditing URL: ") + Color.bold(target)
32
+ puts ""
33
+ end
34
+
35
+ # Fetch GSC rows if authenticated domain matches
36
+ gsc_rows = nil
37
+ if api && site_url
38
+ begin
39
+ res = api.query_analytics(
40
+ site_url: site_url,
41
+ start_date: (Date.today - 28).to_s,
42
+ end_date: Date.today.to_s,
43
+ dimensions: ['page', 'query'],
44
+ row_limit: 1000
45
+ )
46
+ gsc_rows = res.dig(:data, 'rows') || res['rows']
47
+ rescue StandardError
48
+ # Non-fatal if GSC offline
49
+ end
50
+ end
51
+
52
+ breaker = GSC::CanonicalChains.new
53
+ result = breaker.audit_url(target, gsc_rows: gsc_rows)
54
+
55
+ if options[:json]
56
+ puts JSON.pretty_generate(result)
57
+ return
58
+ end
59
+
60
+ render_canonical_audit(result)
61
+ end
62
+
63
+ def handle_batch_domain_audit(domain, options, api = nil, site_url = nil)
64
+ domain = domain || (Config.default_domain rescue nil)
65
+ if domain.nil? || domain.to_s.strip.empty?
66
+ if options[:json]
67
+ puts JSON.pretty_generate({ error: 'Target domain required. Example: gsc canonical-chains mysite.com' })
68
+ else
69
+ puts Color.c("❌ Error: Target domain required. Example: gsc canonical-chains mysite.com", Color::RED, Color::BOLD)
70
+ end
71
+ return
72
+ end
73
+
74
+ unless options[:json]
75
+ puts "\n" + Color.cyan("=" * 80)
76
+ puts Color.bold(" PORTFOLIO CANONICAL & REDIRECT CHAIN AUDIT: ") + Color.yellow(domain)
77
+ puts Color.cyan("=" * 80)
78
+ end
79
+
80
+ limit = (options[:limit] || 20).to_i
81
+ concurrency = (options[:concurrency] || 4).to_i
82
+
83
+ urls = []
84
+ rows = []
85
+ if api && site_url
86
+ print Color.gray(" Fetching top #{limit} pages from GSC... ")
87
+ begin
88
+ res = api.query_analytics(
89
+ site_url: site_url,
90
+ start_date: (Date.today - 28).to_s,
91
+ end_date: Date.today.to_s,
92
+ dimensions: ['page'],
93
+ row_limit: limit
94
+ )
95
+ rows = res.dig(:data, 'rows') || res['rows'] || []
96
+ urls = rows.map { |r| (r['keys'] || []).first }.compact
97
+ puts Color.green("✓ (#{rows.size} pages)")
98
+ rescue StandardError => e
99
+ puts Color.yellow("(GSC query failed: #{e.message})")
100
+ end
101
+ end
102
+
103
+ if urls.empty?
104
+ urls = [
105
+ "https://#{domain}/",
106
+ "http://#{domain}/",
107
+ "https://#{domain}"
108
+ ]
109
+ end
110
+
111
+ puts Color.gray(" Tracing redirect graphs & canonical targets (concurrency: #{concurrency})...\n")
112
+
113
+ breaker = GSC::CanonicalChains.new
114
+ batch_summary = breaker.audit_batch(urls, gsc_rows: rows, concurrency: concurrency)
115
+
116
+ if options[:json]
117
+ puts JSON.pretty_generate(batch_summary)
118
+ return
119
+ end
120
+
121
+ render_batch_summary(batch_summary, domain)
122
+ end
123
+
124
+ def render_canonical_audit(r)
125
+ status_badge = case r[:severity]
126
+ when :critical then Color.red(" CRITICAL DEFECT ")
127
+ when :warning then Color.yellow(" WARNING ")
128
+ else Color.green(" HEALTHY ")
129
+ end
130
+
131
+ puts " Status: #{status_badge}"
132
+ puts " Redirect Hops: " + (r[:redirect_hops] > 0 ? Color.yellow(r[:redirect_hops].to_s) : Color.green("0"))
133
+ puts " Equity Retained: " + (r[:equity_retention_pct] < 75 ? Color.red("#{r[:equity_retention_pct]}%") : Color.green("#{r[:equity_retention_pct]}%")) + Color.gray(" (Loss: #{r[:equity_loss_pct]}%)")
134
+ puts " Total Latency: " + Color.cyan("#{r[:total_duration_ms]}ms")
135
+
136
+ if r.dig(:traffic_at_risk, :total_clicks) && r[:traffic_at_risk][:total_clicks] > 0
137
+ puts " Traffic at Risk: " + Color.red("#{r[:traffic_at_risk][:total_clicks]} clicks / #{r[:traffic_at_risk][:total_impressions]} imps (last 28d)")
138
+ end
139
+
140
+ # Hops graph
141
+ puts "\n" + Color.bold(" REDIRECT & CANONICAL GRAPH:")
142
+ puts " " + Color.gray("-" * 76)
143
+ r[:hops].each do |h|
144
+ code_str = case h[:status_code]
145
+ when 200 then Color.green("200 OK")
146
+ when 301 then Color.yellow("301 Perm")
147
+ when 302 then Color.yellow("302 Temp")
148
+ when 307, 308 then Color.yellow("#{h[:status_code]} Redir")
149
+ else Color.red("#{h[:status_code]} Err")
150
+ end
151
+ puts " [Hop #{h[:hop]}] #{code_str} (#{h[:duration_ms]}ms) -> #{Color.cyan(h[:url])}"
152
+ if h[:canonical_url]
153
+ puts " ↳ #{Color.magenta("Canonical:")} #{h[:canonical_url]} (#{h[:canonical_source]})"
154
+ end
155
+ if h[:error]
156
+ puts " ↳ #{Color.red("ERROR:")} #{h[:error]}"
157
+ end
158
+ end
159
+ puts " " + Color.gray("-" * 76)
160
+
161
+ # Issues
162
+ if r[:issues].any?
163
+ puts "\n" + Color.bold(" IDENTIFIED ISSUES:")
164
+ r[:issues].each do |iss|
165
+ prefix = iss[:code] == :loop || iss[:code] == :canonical_redirects ? Color.red(" ✖ ") : Color.yellow(" ⚠ ")
166
+ puts "#{prefix}#{iss[:message]}"
167
+ end
168
+ end
169
+
170
+ # Server Collapse Rules
171
+ if r[:redirect_hops] >= 1 || r[:canonical_loop]
172
+ puts "\n" + Color.bold(" 1-CLICK SERVER COLLAPSE RULES (Bypass All Intermediate Hops):")
173
+ puts " " + Color.gray("-" * 76)
174
+ puts Color.yellow(" [Nginx]")
175
+ puts " " + Color.cyan(r[:server_rules][:nginx].to_s)
176
+ puts Color.yellow("\n [Apache .htaccess]")
177
+ puts " " + Color.cyan(r[:server_rules][:apache].to_s)
178
+ puts Color.yellow("\n [Cloudflare Redirect Expression]")
179
+ puts " " + Color.cyan(r[:server_rules][:cloudflare].to_s)
180
+ puts Color.yellow("\n [Vercel / Netlify _redirects]")
181
+ puts " " + Color.cyan(r[:server_rules][:vercel_netlify].to_s)
182
+ puts " " + Color.gray("-" * 76)
183
+ end
184
+ puts ""
185
+ end
186
+
187
+ def render_batch_summary(bs, domain)
188
+ puts "\n Total Pages Audited: " + Color.bold(bs[:total_audited].to_s)
189
+ puts " Redirect Chains (>=2): " + (bs[:redirect_chains_count] > 0 ? Color.red(bs[:redirect_chains_count].to_s) : Color.green("0"))
190
+ puts " Canonical / Dir Loops: " + (bs[:loops_count] > 0 ? Color.red(bs[:loops_count].to_s) : Color.green("0"))
191
+ puts " Average Equity Preserved: " + (bs[:avg_equity_retention_pct] < 85 ? Color.yellow("#{bs[:avg_equity_retention_pct]}%") : Color.green("#{bs[:avg_equity_retention_pct]}%"))
192
+ if bs[:total_clicks_at_risk] > 0
193
+ puts " Total GSC Clicks at Risk: " + Color.red("#{bs[:total_clicks_at_risk]} clicks")
194
+ end
195
+
196
+ puts "\n" + Color.bold(" TOP CHAIN & CANONICAL RISKS:")
197
+ puts " " + Color.gray("-" * 76)
198
+ printf " %-40s %-8s %-12s %-12s\n", "URL", "Hops", "Equity Ret.", "Status"
199
+ puts " " + Color.gray("-" * 76)
200
+
201
+ bs[:results].first(15).each do |r|
202
+ status_str = if r[:loop_detected]
203
+ Color.red("LOOP")
204
+ elsif r[:redirect_hops] >= 2
205
+ Color.yellow("CHAIN")
206
+ elsif r[:redirect_hops] == 1
207
+ Color.cyan("REDIRECT")
208
+ else
209
+ Color.green("DIRECT")
210
+ end
211
+ short_url = r[:start_url].gsub(%r{^https?://(www\.)?}, '')[0..38]
212
+ printf " %-40s %-8s %-12s %-12s\n",
213
+ short_url,
214
+ r[:redirect_hops],
215
+ "#{r[:equity_retention_pct]}%",
216
+ status_str
217
+ end
218
+ puts " " + Color.gray("-" * 76)
219
+ puts Color.gray("\n Tip: Run `gsc canonical-chains <url>` on any flagged URL to copy direct Nginx/Cloudflare collapse rules.\n")
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,152 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'json'
5
+ require_relative 'base'
6
+ require_relative '../citation_simulator'
7
+ require_relative '../color'
8
+
9
+ module GSC
10
+ class CLI
11
+ module CitationSimulator
12
+ module_function
13
+
14
+ def run(command, target, extra, options, api = nil, site_url = nil, hostname = nil)
15
+ target_url = target || (hostname ? "https://#{hostname}" : nil)
16
+ unless target_url
17
+ msg = "Please provide a URL or HTML file: gsc cite-sim <url|file> [query]"
18
+ if options[:json]
19
+ puts JSON.pretty_generate({ error: msg })
20
+ else
21
+ puts Color.red("❌ Error: #{msg}")
22
+ end
23
+ return
24
+ end
25
+
26
+ # Resolve query from extra arguments or options
27
+ query_str = if extra && !extra.to_s.strip.empty?
28
+ extra.is_a?(Array) ? extra.join(' ').strip : extra.to_s.strip
29
+ elsif options[:query]
30
+ options[:query].to_s.strip
31
+ elsif options[:keyword]
32
+ options[:keyword].to_s.strip
33
+ elsif api && site_url
34
+ fetch_top_gsc_query(api, site_url)
35
+ else
36
+ nil
37
+ end
38
+
39
+ puts "🤖 Simulating AI search citation for #{Color.cyan(target_url)}..." unless options[:json]
40
+
41
+ sim_opts = options.dup
42
+ sim_opts[:model] = options[:model] if options[:model]
43
+
44
+ res = GSC::CitationSimulator.simulate(target_url, query_str, sim_opts)
45
+
46
+ if options[:json]
47
+ puts JSON.pretty_generate(res)
48
+ return
49
+ end
50
+
51
+ render_terminal(res)
52
+
53
+ if options[:csv]
54
+ export_csv(res, options[:csv])
55
+ end
56
+ end
57
+
58
+ def fetch_top_gsc_query(api, site_url)
59
+ res = api.query_analytics(site_url, days: 30, dimensions: ['query'], row_limit: 1)
60
+ if res[:ok] && res.dig(:data, 'rows')&.first
61
+ res[:data]['rows'].first['keys'].first
62
+ else
63
+ nil
64
+ end
65
+ rescue StandardError
66
+ nil
67
+ end
68
+
69
+ def render_terminal(res)
70
+ puts "\n#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
71
+ puts "#{Color.bold("🤖 AI SEARCH CITATION SIMULATOR (ChatGPT • Perplexity • Claude • AIO)")}"
72
+ puts "#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}"
73
+
74
+ score = res[:citation_likelihood_score]
75
+ grade_color = case res[:grade]
76
+ when 'A+', 'A' then Color.green(res[:grade])
77
+ when 'B' then Color.cyan(res[:grade])
78
+ when 'C' then Color.yellow(res[:grade])
79
+ else Color.red(res[:grade])
80
+ end
81
+
82
+ status_color = score >= 70 ? Color.green(res[:status]) : (score >= 45 ? Color.yellow(res[:status]) : Color.red(res[:status]))
83
+
84
+ puts " • Citation Likelihood Score: [ #{Color.bold(grade_color)} ] #{Color.bold("#{score}/100")} (#{status_color})"
85
+ puts " • Target Search Query: #{Color.bold("\"#{res[:target_query]}\"")}"
86
+ puts " • Evaluated Model Profile: #{Color.cyan(res[:model_profile].upcase)}"
87
+ puts " • Content Source: #{res[:url]}"
88
+ if res[:http_status] && res[:http_status] >= 400
89
+ puts Color.red(" ⚠️ HTTP STATUS #{res[:http_status]} (PAGE NOT FOUND / ERROR):")
90
+ puts Color.yellow(" Target URL returned HTTP #{res[:http_status]}. The analysis below evaluated the server's 404 error page.")
91
+ end
92
+ puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}"
93
+
94
+ sig = res[:signals]
95
+ puts " Citability Signals Detected:"
96
+ puts " 📊 Facts / Numerical Data: #{Color.bold(sig[:facts_detected].to_s)} facts found in core passage"
97
+ puts " 🏷️ Schema.org JSON-LD: #{sig[:has_schema] ? Color.green("YES") : Color.yellow("MISSING")}"
98
+ puts " ✍️ Author E-E-A-T Byline: #{sig[:has_author] ? Color.green("YES") : Color.yellow("MISSING")}"
99
+ puts " 📅 Verified Published Date: #{sig[:has_published_date] ? Color.green("YES") : Color.yellow("MISSING")}"
100
+ puts " 📑 Comparative Data Table: #{sig[:has_comparative_table] ? Color.green("YES") : Color.gray("NO")}"
101
+ puts "#{Color.bold("───────────────────────────────────────────────────────────────────────────")}\n"
102
+
103
+ puts "#{Color.bold("EMULATED AI SEARCH SYNTHESIZED RESPONSE:")}"
104
+ puts "┌──────────────────────────────────────────────────────────────────────────┐"
105
+ res[:emulated_ai_response].each_line do |line|
106
+ puts "│ #{line.strip.ljust(72)} │"
107
+ end
108
+ puts "└──────────────────────────────────────────────────────────────────────────┘\n"
109
+
110
+ if res[:extracted_quotes].any?
111
+ puts "#{Color.bold("EXTRACTED VERIFIABLE CITATION QUOTES:")}"
112
+ res[:extracted_quotes].each_with_index do |q, idx|
113
+ puts " [#{idx + 1}] #{Color.green("\"#{q}\"")}"
114
+ end
115
+ puts
116
+ end
117
+
118
+ top_c = res[:top_cited_chunk]
119
+ puts "#{Color.bold("TOP CITED CONTENT CHUNK (Section: \"#{top_c[:heading]}\"):")}"
120
+ puts " • Word Count: #{top_c[:word_count]} words | Fact Density: #{top_c[:fact_density]}% | Relevance: #{top_c[:relevance_score]}/100"
121
+ short_text = top_c[:text].length > 180 ? "#{top_c[:text][0..177]}..." : top_c[:text]
122
+ puts " • Preview: #{Color.gray(short_text)}\n\n"
123
+
124
+ puts "#{Color.bold("ACTIONABLE INFORMATION-GAIN PRESCRIPTIONS (TO REACH 100/100):")}"
125
+ res[:prescriptions].each_with_index do |p, idx|
126
+ puts " #{idx + 1}. 💡 #{p}"
127
+ end
128
+
129
+ puts "\n#{Color.bold("═══════════════════════════════════════════════════════════════════════════")}\n"
130
+ end
131
+
132
+ def export_csv(res, file_path)
133
+ headers = %w[URL Query Model CLS Grade Status FactsCount Quotes Prescriptions]
134
+ rows = [
135
+ [
136
+ res[:url],
137
+ res[:target_query],
138
+ res[:model_profile],
139
+ res[:citation_likelihood_score],
140
+ res[:grade],
141
+ res[:status],
142
+ res.dig(:signals, :facts_detected),
143
+ res[:extracted_quotes].join(' | '),
144
+ res[:prescriptions].join(' | ')
145
+ ]
146
+ ]
147
+ Base.write_csv(file_path, headers, rows)
148
+ puts Color.cyan("📁 Exported citation simulation to #{file_path}")
149
+ end
150
+ end
151
+ end
152
+ end