gsc-cli 2.1.0 → 2.2.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/README.md +410 -414
- data/bin/gsc +28067 -5661
- data/dist/gsc +29121 -5046
- data/lib/gsc/aio_hunter.rb +343 -0
- data/lib/gsc/answer_synthesizer.rb +157 -0
- data/lib/gsc/api.rb +53 -1
- data/lib/gsc/auth.rb +26 -0
- data/lib/gsc/brand_segmenter.rb +140 -0
- data/lib/gsc/cache_manager.rb +806 -0
- data/lib/gsc/cannibalization_analyzer.rb +141 -0
- data/lib/gsc/canonical_chains.rb +367 -0
- data/lib/gsc/citation_simulator.rb +339 -0
- data/lib/gsc/cli/aio_hunter.rb +154 -0
- data/lib/gsc/cli/analytics.rb +788 -0
- data/lib/gsc/cli/audit.rb +1976 -0
- data/lib/gsc/cli/base.rb +384 -0
- data/lib/gsc/cli/cache.rb +266 -0
- data/lib/gsc/cli/canonical.rb +223 -0
- data/lib/gsc/cli/citation_simulator.rb +152 -0
- data/lib/gsc/cli/dashboard.rb +354 -0
- data/lib/gsc/cli/doctor.rb +129 -0
- data/lib/gsc/cli/eeat.rb +125 -0
- data/lib/gsc/cli/ga4.rb +852 -0
- data/lib/gsc/cli/growth.rb +650 -0
- data/lib/gsc/cli/hreflang.rb +164 -0
- data/lib/gsc/cli/image_seo.rb +162 -0
- data/lib/gsc/cli/indexing.rb +458 -0
- data/lib/gsc/cli/intent_shift.rb +125 -0
- data/lib/gsc/cli/keyword_value.rb +134 -0
- data/lib/gsc/cli/keywords.rb +795 -0
- data/lib/gsc/cli/landing_roi.rb +308 -0
- data/lib/gsc/cli/low_ctr.rb +213 -0
- data/lib/gsc/cli/mobile_parity.rb +150 -0
- data/lib/gsc/cli/report.rb +100 -0
- data/lib/gsc/cli/rich_results.rb +172 -0
- data/lib/gsc/cli/schema_generate.rb +149 -0
- data/lib/gsc/cli/seasonal.rb +232 -0
- data/lib/gsc/cli/security.rb +153 -0
- data/lib/gsc/cli/setup.rb +1291 -0
- data/lib/gsc/cli/sitemap_tree.rb +143 -0
- data/lib/gsc/cli/skill_pack.rb +62 -0
- data/lib/gsc/cli/soft_404.rb +199 -0
- data/lib/gsc/cli/sparkline.rb +227 -0
- data/lib/gsc/cli/watchdog.rb +150 -0
- data/lib/gsc/cli/zombie_purger.rb +208 -0
- data/lib/gsc/cli.rb +707 -5265
- data/lib/gsc/cli_advanced.rb +987 -44
- data/lib/gsc/client.rb +17 -2
- data/lib/gsc/color.rb +16 -1
- data/lib/gsc/command_registry.rb +47 -9
- data/lib/gsc/config.rb +2 -2
- data/lib/gsc/ctr_curve.rb +115 -0
- data/lib/gsc/decay_predictor.rb +322 -0
- data/lib/gsc/doctor.rb +434 -0
- data/lib/gsc/eeat_auditor.rb +428 -0
- data/lib/gsc/entity_auditor.rb +229 -0
- data/lib/gsc/firewall_scanner.rb +733 -0
- data/lib/gsc/geo_auditor.rb +368 -0
- data/lib/gsc/google_trends.rb +8 -1
- data/lib/gsc/heading_validator.rb +283 -0
- data/lib/gsc/hreflang_validator.rb +412 -0
- data/lib/gsc/image_seo.rb +286 -0
- data/lib/gsc/indexing_queue.rb +179 -0
- data/lib/gsc/indexnow.rb +93 -0
- data/lib/gsc/intent_shift.rb +188 -0
- data/lib/gsc/internal_links.rb +153 -36
- data/lib/gsc/keyword_value.rb +191 -0
- data/lib/gsc/landing_roi.rb +195 -0
- data/lib/gsc/llms_generator.rb +343 -22
- data/lib/gsc/low_ctr_rewriter.rb +408 -0
- data/lib/gsc/mobile_parity.rb +222 -0
- data/lib/gsc/network_tracer.rb +8 -1
- data/lib/gsc/page_analyzer.rb +47 -7
- data/lib/gsc/prompts.rb +38 -29
- data/lib/gsc/questions_harvester.rb +178 -0
- data/lib/gsc/report_generator.rb +461 -0
- data/lib/gsc/rich_results.rb +388 -0
- data/lib/gsc/robots_checker.rb +46 -15
- data/lib/gsc/schema_generator.rb +788 -0
- data/lib/gsc/schema_validator.rb +36 -38
- data/lib/gsc/seasonal_predictor.rb +381 -0
- data/lib/gsc/security_scanner.rb +496 -0
- data/lib/gsc/serp_feature_detector.rb +359 -0
- data/lib/gsc/serp_preview.rb +108 -22
- data/lib/gsc/site_crawler.rb +113 -21
- data/lib/gsc/sitemap_loader.rb +15 -4
- data/lib/gsc/sitemap_tree.rb +301 -0
- data/lib/gsc/skill_pack.rb +195 -0
- data/lib/gsc/soft_404_analyzer.rb +385 -0
- data/lib/gsc/sparkline.rb +171 -0
- data/lib/gsc/speed_correlator.rb +416 -0
- data/lib/gsc/striking_playbook.rb +190 -0
- data/lib/gsc/title_optimizer.rb +420 -0
- data/lib/gsc/vault.rb +260 -0
- data/lib/gsc/version.rb +1 -1
- data/lib/gsc/watchdog.rb +235 -0
- data/lib/gsc/zombie_purger.rb +366 -0
- data/lib/gsc.rb +118 -0
- 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
|