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,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GSC
4
+ class BrandSegmenter
5
+ attr_reader :brand_tokens
6
+
7
+ # Suffixes to strip when deriving brand name from domain (e.g., exampleapp -> example)
8
+ STRIP_SUFFIXES = %w[app apps io co tech hq get try official shop store ltd inc net org].freeze
9
+
10
+ def initialize(domain: nil, custom_brand: nil)
11
+ @brand_tokens = resolve_tokens(domain, custom_brand)
12
+ end
13
+
14
+ def brand?(query)
15
+ return false if @brand_tokens.empty? || query.to_s.strip.empty?
16
+
17
+ q = normalize(query)
18
+ # Check exact token containment or multi-word phrase containment
19
+ @brand_tokens.any? do |token|
20
+ token_clean = normalize(token)
21
+ q.include?(token_clean) || q.gsub(/\s+/, '').include?(token_clean.gsub(/\s+/, ''))
22
+ end
23
+ end
24
+
25
+ def segment(rows)
26
+ brand_rows = []
27
+ non_brand_rows = []
28
+
29
+ rows.each do |row|
30
+ q = row[:query] || row['query'] || ''
31
+ if brand?(q)
32
+ brand_rows << row
33
+ else
34
+ non_brand_rows << row
35
+ end
36
+ end
37
+
38
+ {
39
+ brand: brand_rows,
40
+ non_brand: non_brand_rows,
41
+ summary: calculate_summary(brand_rows, non_brand_rows)
42
+ }
43
+ end
44
+
45
+ private
46
+
47
+ def normalize(str)
48
+ str.to_s.downcase.gsub(/[^a-z0-9\s]/, ' ').squeeze(' ').strip
49
+ end
50
+
51
+ def resolve_tokens(domain, custom_brand)
52
+ tokens = []
53
+
54
+ if custom_brand && !custom_brand.to_s.strip.empty?
55
+ cb = normalize(custom_brand)
56
+ tokens << cb
57
+ tokens << cb.gsub(/\s+/, '')
58
+ cb.split(/\s+/).each do |w|
59
+ tokens << w if w.length >= 4 && !%w[pro app apps the inc llc ltd corp].include?(w)
60
+ end
61
+ end
62
+
63
+ if domain && !domain.to_s.strip.empty?
64
+ # Clean domain
65
+ clean_dom = domain.to_s.sub(/^https?:\/\//i, '').sub(/^sc-domain:/i, '').split('/').first.to_s.downcase
66
+ parts = clean_dom.split('.')
67
+ # Main apex stem (e.g., exampleapp from exampleapp.com)
68
+ stem = parts.first || ''
69
+
70
+ tokens << stem unless stem.empty?
71
+
72
+ # Try stripping known suffixes
73
+ STRIP_SUFFIXES.each do |suffix|
74
+ if stem.end_with?(suffix) && stem.length > (suffix.length + 3)
75
+ base = stem[0...-suffix.length]
76
+ tokens << base
77
+ # Add spaced variant if it looks compound (e.g. quickcart -> quick cart)
78
+ tokens << base.sub(/(super|pack|cart|speed|quick|auto|mega|hyper)/i, '\1 ').strip
79
+ end
80
+ end
81
+
82
+ # Also add compound space variations
83
+ tokens << stem.sub(/(super|pack|cart|speed|quick|auto|mega|hyper)/i, '\1 ').strip
84
+ end
85
+
86
+ tokens.map { |t| normalize(t) }.reject { |t| t.length < 3 }.uniq
87
+ end
88
+
89
+ def calculate_summary(brand_rows, non_brand_rows)
90
+ total_rows = brand_rows.size + non_brand_rows.size
91
+
92
+ b_clicks = brand_rows.sum { |r| r[:clicks] || r['clicks'] || 0 }
93
+ b_imp = brand_rows.sum { |r| r[:impressions] || r['impressions'] || 0 }
94
+ b_ctr = b_imp > 0 ? (b_clicks.to_f / b_imp * 100).round(2) : 0.0
95
+ b_pos = if b_imp > 0
96
+ (brand_rows.sum { |r| (r[:position] || r['position'] || 0.0).to_f * (r[:impressions] || r['impressions'] || 0).to_f } / b_imp.to_f).round(1)
97
+ elsif brand_rows.any?
98
+ (brand_rows.sum { |r| (r[:position] || r['position'] || 0.0).to_f } / brand_rows.size).round(1)
99
+ else
100
+ 0.0
101
+ end
102
+
103
+ nb_clicks = non_brand_rows.sum { |r| r[:clicks] || r['clicks'] || 0 }
104
+ nb_imp = non_brand_rows.sum { |r| r[:impressions] || r['impressions'] || 0 }
105
+ nb_ctr = nb_imp > 0 ? (nb_clicks.to_f / nb_imp * 100).round(2) : 0.0
106
+ nb_pos = if nb_imp > 0
107
+ (non_brand_rows.sum { |r| (r[:position] || r['position'] || 0.0).to_f * (r[:impressions] || r['impressions'] || 0).to_f } / nb_imp.to_f).round(1)
108
+ elsif non_brand_rows.any?
109
+ (non_brand_rows.sum { |r| (r[:position] || r['position'] || 0.0).to_f } / non_brand_rows.size).round(1)
110
+ else
111
+ 0.0
112
+ end
113
+
114
+ total_clicks = b_clicks + nb_clicks
115
+ total_imp = b_imp + nb_imp
116
+
117
+ {
118
+ total_queries: total_rows,
119
+ brand: {
120
+ queries_count: brand_rows.size,
121
+ clicks: b_clicks,
122
+ impressions: b_imp,
123
+ ctr: b_ctr,
124
+ avg_position: b_pos,
125
+ click_share: total_clicks > 0 ? (b_clicks.to_f / total_clicks * 100).round(1) : 0.0,
126
+ impression_share: total_imp > 0 ? (b_imp.to_f / total_imp * 100).round(1) : 0.0
127
+ },
128
+ non_brand: {
129
+ queries_count: non_brand_rows.size,
130
+ clicks: nb_clicks,
131
+ impressions: nb_imp,
132
+ ctr: nb_ctr,
133
+ avg_position: nb_pos,
134
+ click_share: total_clicks > 0 ? (nb_clicks.to_f / total_clicks * 100).round(1) : 0.0,
135
+ impression_share: total_imp > 0 ? (nb_imp.to_f / total_imp * 100).round(1) : 0.0
136
+ }
137
+ }
138
+ end
139
+ end
140
+ end