thumbgate 1.27.18 → 1.27.19

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 (96) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +4 -3
  3. package/.well-known/agentic-verify.txt +1 -0
  4. package/.well-known/llms.txt +33 -12
  5. package/.well-known/mcp/server-card.json +8 -8
  6. package/README.md +249 -30
  7. package/adapters/chatgpt/openapi.yaml +12 -0
  8. package/adapters/claude/.mcp.json +2 -2
  9. package/adapters/codex/config.toml +2 -2
  10. package/adapters/gemini/function-declarations.json +1 -0
  11. package/adapters/mcp/server-stdio.js +263 -11
  12. package/adapters/opencode/opencode.json +1 -1
  13. package/bench/thumbgate-bench.json +2 -2
  14. package/bin/cli.js +1429 -121
  15. package/bin/postinstall.js +1 -8
  16. package/config/gate-classifier-routing.json +98 -0
  17. package/config/gate-templates.json +216 -0
  18. package/config/gates/claim-verification.json +12 -0
  19. package/config/gates/default.json +31 -2
  20. package/config/github-about.json +2 -2
  21. package/config/mcp-allowlists.json +23 -13
  22. package/config/merge-quality-checks.json +0 -1
  23. package/config/model-candidates.json +121 -6
  24. package/config/post-deploy-marketing-pages.json +80 -0
  25. package/config/tessl-tiles.json +1 -3
  26. package/openapi/openapi.yaml +12 -0
  27. package/package.json +1 -1
  28. package/public/blog.html +4 -4
  29. package/public/codex-plugin.html +72 -20
  30. package/public/compare.html +31 -8
  31. package/public/dashboard.html +930 -166
  32. package/public/federal.html +2 -2
  33. package/public/guide.html +33 -13
  34. package/public/index.html +469 -111
  35. package/public/learn.html +183 -18
  36. package/public/lessons.html +168 -10
  37. package/public/numbers.html +7 -7
  38. package/public/pro.html +34 -11
  39. package/scripts/agent-memory-lifecycle.js +211 -0
  40. package/scripts/agent-readiness.js +20 -3
  41. package/scripts/agent-reward-model.js +53 -1
  42. package/scripts/auto-promote-gates.js +82 -10
  43. package/scripts/auto-wire-hooks.js +14 -0
  44. package/scripts/billing.js +93 -1
  45. package/scripts/bot-detection.js +61 -3
  46. package/scripts/build-metadata.js +50 -10
  47. package/scripts/cli-feedback.js +4 -2
  48. package/scripts/cli-schema.js +97 -0
  49. package/scripts/cli-telemetry.js +6 -1
  50. package/scripts/commercial-offer.js +82 -2
  51. package/scripts/context-manager.js +74 -6
  52. package/scripts/dashboard.js +68 -2
  53. package/scripts/export-databricks-bundle.js +5 -1
  54. package/scripts/export-dpo-pairs.js +7 -2
  55. package/scripts/feedback-loop.js +123 -1
  56. package/scripts/feedback-quality.js +87 -0
  57. package/scripts/filesystem-search.js +35 -10
  58. package/scripts/gate-stats.js +89 -0
  59. package/scripts/gates-engine.js +1176 -85
  60. package/scripts/gemini-embedding-policy.js +2 -1
  61. package/scripts/hook-runtime.js +20 -14
  62. package/scripts/hook-thumbgate-cache-updater.js +18 -2
  63. package/scripts/hybrid-feedback-context.js +142 -7
  64. package/scripts/lesson-inference.js +8 -3
  65. package/scripts/lesson-search.js +17 -1
  66. package/scripts/license.js +10 -10
  67. package/scripts/llm-client.js +169 -4
  68. package/scripts/local-model-profile.js +15 -8
  69. package/scripts/mcp-config.js +7 -1
  70. package/scripts/memory-scope-readiness.js +159 -0
  71. package/scripts/meta-agent-loop.js +36 -0
  72. package/scripts/operational-integrity.js +39 -5
  73. package/scripts/oss-pr-opportunity-scout.js +35 -5
  74. package/scripts/plausible-server-events.js +9 -6
  75. package/scripts/pro-local-dashboard.js +4 -4
  76. package/scripts/proxy-pointer-rag-guardrails.js +42 -1
  77. package/scripts/published-cli.js +0 -8
  78. package/scripts/rate-limiter.js +64 -13
  79. package/scripts/secret-scanner.js +44 -5
  80. package/scripts/security-scanner.js +260 -10
  81. package/scripts/self-distill-agent.js +3 -1
  82. package/scripts/seo-gsd.js +916 -7
  83. package/scripts/statusline-cache-path.js +17 -2
  84. package/scripts/statusline-local-stats.js +9 -1
  85. package/scripts/statusline-meta.js +28 -2
  86. package/scripts/statusline.sh +20 -4
  87. package/scripts/telemetry-analytics.js +357 -0
  88. package/scripts/thompson-sampling.js +31 -10
  89. package/scripts/thumbgate-bench.js +16 -1
  90. package/scripts/thumbgate-search.js +85 -19
  91. package/scripts/tool-registry.js +169 -1
  92. package/scripts/vector-store.js +45 -0
  93. package/scripts/workflow-sentinel.js +286 -53
  94. package/scripts/workspace-evolver.js +62 -2
  95. package/src/api/server.js +2683 -319
  96. package/scripts/bot-detector.js +0 -50
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- const path = require('path');
4
+ const path = require('node:path');
5
+ const os = require('node:os');
6
+ const {
7
+ getAggregateStatuslineCachePath,
8
+ shouldAggregateFeedback,
9
+ } = require('./feedback-aggregate');
5
10
  const {
6
11
  listFeedbackArtifactPaths,
7
12
  resolveFeedbackDir,
@@ -12,18 +17,28 @@ function unique(values = []) {
12
17
  return [...new Set(values.filter(Boolean).map((value) => path.resolve(value)))];
13
18
  }
14
19
 
20
+ function hasIsolatedTempFeedbackDir(env = process.env) {
21
+ if (!env.THUMBGATE_FEEDBACK_DIR) return false;
22
+ try {
23
+ return path.resolve(env.THUMBGATE_FEEDBACK_DIR).startsWith(path.resolve(os.tmpdir()) + path.sep);
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
15
29
  function getStatuslineCacheCandidates(options = {}) {
16
30
  const env = options.env || process.env;
17
31
  const projectDir = resolveProjectDir({ cwd: options.cwd, env });
18
32
  const feedbackDir = resolveFeedbackDir({ projectDir, env });
19
33
 
20
34
  return unique([
35
+ shouldAggregateFeedback({ env }) && !hasIsolatedTempFeedbackDir(env) && getAggregateStatuslineCachePath({ env }),
21
36
  ...listFeedbackArtifactPaths('statusline_cache.json', { projectDir, env }),
22
37
  path.join(feedbackDir, 'statusline_cache.json'),
23
38
  ]);
24
39
  }
25
40
 
26
- if (require.main === module) {
41
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(__filename)) {
27
42
  process.stdout.write(JSON.stringify({ candidates: getStatuslineCacheCandidates() }));
28
43
  }
29
44
 
@@ -2,6 +2,10 @@
2
2
  'use strict';
3
3
 
4
4
  const { analyzeFeedback } = require('./feedback-loop');
5
+ const {
6
+ computeAggregateFeedbackStats,
7
+ shouldAggregateFeedback,
8
+ } = require('./feedback-aggregate');
5
9
  const { normalizeStatsPayload } = require('./hook-thumbgate-cache-updater');
6
10
  const { syncClaudeHistoryFeedback } = require('./claude-feedback-sync');
7
11
  const { resolveProjectDir } = require('./feedback-paths');
@@ -9,9 +13,13 @@ const { resolveProjectDir } = require('./feedback-paths');
9
13
  try {
10
14
  const projectDir = resolveProjectDir({ cwd: process.cwd(), env: process.env });
11
15
  syncClaudeHistoryFeedback({ projectDir });
12
- const stats = analyzeFeedback();
16
+ const scope = String(process.env.THUMBGATE_STATUSLINE_SCOPE || 'global').toLowerCase();
17
+ const stats = scope !== 'project' && shouldAggregateFeedback({ env: process.env })
18
+ ? computeAggregateFeedbackStats({ projectDir, env: process.env })
19
+ : analyzeFeedback();
13
20
  const payload = {
14
21
  ...normalizeStatsPayload(stats),
22
+ aggregate: stats.aggregate || { enabled: false },
15
23
  updated_at: String(Math.floor(Date.now() / 1000)),
16
24
  };
17
25
  process.stdout.write(JSON.stringify(payload));
@@ -2,16 +2,42 @@
2
2
  'use strict';
3
3
 
4
4
  const path = require('path');
5
- const { isProLicensed } = require('./license');
5
+ const { isProLicensed, isValidKey } = require('./license');
6
6
 
7
7
  function getStatuslineMeta(options = {}) {
8
8
  const pkg = require(path.join(__dirname, '..', 'package.json'));
9
9
  const env = options.env || process.env;
10
10
  const homeDir = options.homeDir || env.HOME || env.USERPROFILE || '.';
11
+ const fs = require('fs');
12
+
13
+ // Enterprise detection based on key prefix
14
+ let apiKey = env.THUMBGATE_OPERATOR_KEY || env.THUMBGATE_API_KEY || '';
15
+ if (apiKey && !isValidKey(apiKey)) {
16
+ apiKey = '';
17
+ }
18
+
19
+ // Fallback to reading from disk if not in env
20
+ if (!apiKey) {
21
+ try {
22
+ const configPath = path.join(homeDir, '.config', 'thumbgate', 'operator.json');
23
+ if (fs.existsSync(configPath)) {
24
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
25
+ apiKey = config.operatorKey || config.apiKey || '';
26
+ }
27
+ } catch (_) { /* ignore disk read errors */ }
28
+ }
29
+
30
+ let activeTier = 'Free';
31
+
32
+ if (apiKey.startsWith('tg_op_') || apiKey.startsWith('tg_creator_')) {
33
+ activeTier = 'Enterprise';
34
+ } else if (isProLicensed({ homeDir }) || apiKey.startsWith('tg_pro_')) {
35
+ activeTier = 'Pro';
36
+ }
11
37
 
12
38
  return {
13
39
  version: String(pkg.version || '').trim() || 'unknown',
14
- tier: isProLicensed({ homeDir }) ? 'Pro' : 'Free',
40
+ tier: activeTier,
15
41
  };
16
42
  }
17
43
 
@@ -7,6 +7,7 @@
7
7
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
8
8
  case "$SCRIPT_DIR" in *[!a-zA-Z0-9/_.-]*) echo "ThumbGate: invalid script path"; exit 1;; esac
9
9
  LOCAL_API_ORIGIN="${THUMBGATE_LOCAL_API_ORIGIN:-http://localhost:3456}"
10
+ STATUSLINE_VERBOSE="${THUMBGATE_STATUSLINE_VERBOSE:-0}"
10
11
 
11
12
  # ── Parse Claude Code session JSON from stdin ─────────────────────
12
13
  eval "$(cat | jq -r '
@@ -44,7 +45,19 @@ if [ -z "$THUMBGATE_CACHE" ]; then
44
45
  fi
45
46
 
46
47
  UP="0"; DOWN="0"; LESSONS="0"; TREND="?"; CACHE_TS="0"
47
- if [ -f "$THUMBGATE_CACHE" ]; then
48
+ # Display reads aggregate across every per-folder cache so the statusline
49
+ # reflects true totals, not whichever folder happens to be cwd. Writes still
50
+ # target $THUMBGATE_CACHE (per-folder), so attribution is preserved.
51
+ _RESOLVED_CACHE_JSON=$(node "${SCRIPT_DIR}/statusline-cache-read.js" 2>/dev/null)
52
+ if [[ -n "$_RESOLVED_CACHE_JSON" ]]; then
53
+ eval "$(echo "$_RESOLVED_CACHE_JSON" | jq -r '
54
+ @sh "UP=\(.thumbs_up // "0")",
55
+ @sh "DOWN=\(.thumbs_down // "0")",
56
+ @sh "LESSONS=\(.lessons // "0")",
57
+ @sh "TREND=\(.trend // "?")",
58
+ @sh "CACHE_TS=\(.updated_at // "0")"
59
+ ' 2>/dev/null)"
60
+ elif [[ -f "$THUMBGATE_CACHE" ]]; then
48
61
  eval "$(jq -r '
49
62
  @sh "UP=\(.thumbs_up // "0")",
50
63
  @sh "DOWN=\(.thumbs_down // "0")",
@@ -199,8 +212,10 @@ LINE="${LINE:+${LINE} · }ThumbGate v${TG_VERSION} · ${TG_TIER}"
199
212
  if [[ "$UP" = "0" && "$DOWN" = "0" ]]; then
200
213
  LINE="${D}${LINE}${RST} · no feedback yet"
201
214
  [[ -n "$PR_LABEL" ]] && LINE="${LINE} · ${D}${PR_LABEL}${RST}"
202
- LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST} · ${M}${LESSONS_LINK}${RST}"
215
+
216
+ LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST}"
203
217
  [[ -n "$LATEST_LESSON_LINK" ]] && LINE="${LINE} · ${D}${LATEST_LESSON_LINK}${RST}"
218
+
204
219
  printf '%b\n' "$LINE"
205
220
  else
206
221
  LINE="${LINE} · ${G}${BD}${UP}${RST}${UP_LINK} ${R}${BD}${DOWN}${RST}${DOWN_LINK} ${ARROW}"
@@ -210,8 +225,9 @@ else
210
225
  [[ "${AT_RISK:-0}" -gt 0 ]] && LINE="${LINE} ${R}${AT_RISK}⚠${RST}"
211
226
  [[ "${ANOMALIES:-0}" -gt 0 ]] && LINE="${LINE} ${R}${ANOMALIES}☠${RST}"
212
227
  [[ -n "$PR_LABEL" ]] && LINE="${LINE} · ${D}${PR_LABEL}${RST}"
213
- LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST} · ${M}${LESSONS_LINK}${RST}"
228
+
229
+ LINE="${LINE} · ${C}${DASHBOARD_LINK}${RST}"
214
230
  [[ -n "$LATEST_LESSON_LINK" ]] && LINE="${LINE} · ${D}${LATEST_LESSON_LINK}${RST}"
215
-
231
+
216
232
  printf '%b\n' "$LINE"
217
233
  fi
@@ -75,6 +75,13 @@ function normalizeInteger(value) {
75
75
  return Number.isFinite(parsed) ? Math.trunc(parsed) : null;
76
76
  }
77
77
 
78
+ function normalizeRate(value) {
79
+ if (value === undefined || value === null || value === '') return null;
80
+ const parsed = Number(value);
81
+ if (!Number.isFinite(parsed)) return null;
82
+ return Math.max(0, Math.min(parsed, 1));
83
+ }
84
+
78
85
  function safeRate(num, den) {
79
86
  if (!den) return 0;
80
87
  return Number((num / den).toFixed(4));
@@ -255,6 +262,149 @@ function inferTrafficChannel(raw = {}, referrerHost = null) {
255
262
  return 'referral';
256
263
  }
257
264
 
265
+ function normalizeBoolean(value) {
266
+ if (value === true || value === false) return value;
267
+ const text = normalizeText(value, 32);
268
+ if (!text) return false;
269
+ return ['1', 'true', 'yes', 'y', 'bot'].includes(text.toLowerCase());
270
+ }
271
+
272
+ function includesAnyToken(values, tokens) {
273
+ const haystack = values
274
+ .map((value) => normalizeText(value, 512))
275
+ .filter(Boolean)
276
+ .join(' ')
277
+ .toLowerCase();
278
+ if (!haystack) return false;
279
+ return tokens.some((token) => haystack.includes(token));
280
+ }
281
+
282
+ function addAudienceReason(reasons, code) {
283
+ if (!reasons.includes(code)) reasons.push(code);
284
+ }
285
+
286
+ function classifyTelemetryAudience(entry = {}, raw = {}) {
287
+ const reasons = [];
288
+ const identityValues = [
289
+ raw.email,
290
+ raw.customerEmail,
291
+ raw.buyerEmail,
292
+ raw.userEmail,
293
+ entry.visitorId,
294
+ entry.sessionId,
295
+ entry.traceId,
296
+ entry.acquisitionId,
297
+ entry.installId,
298
+ ];
299
+ const attributionValues = [
300
+ entry.source,
301
+ entry.utmSource,
302
+ entry.utmMedium,
303
+ entry.utmCampaign,
304
+ entry.utmContent,
305
+ entry.creator,
306
+ entry.offerCode,
307
+ entry.campaignVariant,
308
+ entry.referrerHost,
309
+ ];
310
+ const eventValues = [
311
+ entry.eventType,
312
+ entry.event,
313
+ entry.page,
314
+ entry.landingPath,
315
+ entry.ctaId,
316
+ entry.reasonCode,
317
+ entry.reasonDetail,
318
+ ];
319
+ const userAgent = normalizeText(entry.userAgent || raw.userAgent, 512) || '';
320
+ const hostValues = [raw.host, raw.hostname, raw.origin, raw.url, raw.pageUrl, entry.referrerHost];
321
+
322
+ if (normalizeBoolean(entry.isBot || raw.isBot) || includesAnyToken([userAgent], [
323
+ 'bot',
324
+ 'crawler',
325
+ 'spider',
326
+ 'headless',
327
+ 'playwright',
328
+ 'puppeteer',
329
+ 'curl/',
330
+ 'wget/',
331
+ 'lighthouse',
332
+ ])) {
333
+ addAudienceReason(reasons, 'bot_or_automation_user_agent');
334
+ }
335
+
336
+ if (includesAnyToken(identityValues, [
337
+ '@example.com',
338
+ 'buyer@example.com',
339
+ 'test@example.com',
340
+ 'codex-verification',
341
+ 'audit@thumbgate.ai',
342
+ ])) {
343
+ addAudienceReason(reasons, 'test_identity');
344
+ }
345
+
346
+ if (includesAnyToken([...attributionValues, ...eventValues], [
347
+ 'codex',
348
+ 'audit',
349
+ 'verification',
350
+ 'synthetic',
351
+ 'smoke',
352
+ 'probe',
353
+ 'test',
354
+ ])) {
355
+ addAudienceReason(reasons, 'test_or_audit_attribution');
356
+ }
357
+
358
+ if (includesAnyToken(hostValues, [
359
+ 'localhost',
360
+ '127.0.0.1',
361
+ '::1',
362
+ 'thumbgate-production.up.railway.app',
363
+ ])) {
364
+ addAudienceReason(reasons, 'internal_host');
365
+ }
366
+
367
+ if (includesAnyToken([userAgent, ...identityValues], [
368
+ 'codex',
369
+ 'github-actions',
370
+ 'node-fetch',
371
+ 'thumbgate-analytics',
372
+ 'thumbgate-verification',
373
+ ])) {
374
+ addAudienceReason(reasons, 'internal_operator_or_ci');
375
+ }
376
+
377
+ if (
378
+ (entry.clientType || entry.client) === 'unknown' &&
379
+ !pickFirstText(entry.visitorId, entry.sessionId, entry.acquisitionId, entry.installId, entry.traceId) &&
380
+ !userAgent &&
381
+ includesAnyToken([entry.source, entry.utmSource], ['direct', 'website'])
382
+ ) {
383
+ addAudienceReason(reasons, 'unknown_direct_no_identity');
384
+ }
385
+
386
+ const audience = reasons.includes('test_identity') || reasons.includes('test_or_audit_attribution')
387
+ ? 'test'
388
+ : (reasons.includes('bot_or_automation_user_agent')
389
+ ? 'bot'
390
+ : (
391
+ reasons.includes('internal_host') ||
392
+ reasons.includes('internal_operator_or_ci') ||
393
+ reasons.includes('unknown_direct_no_identity')
394
+ ? 'internal'
395
+ : 'external'
396
+ ));
397
+
398
+ return {
399
+ audience,
400
+ reasons,
401
+ isExternal: audience === 'external',
402
+ isInternal: audience === 'internal',
403
+ isTest: audience === 'test',
404
+ isBot: audience === 'bot',
405
+ };
406
+ }
407
+
258
408
  function sanitizeTelemetryPayload(payload = {}, headers = {}) {
259
409
  const raw = payload && typeof payload === 'object' ? payload : {};
260
410
  const clientType = inferClientType(raw);
@@ -313,6 +463,12 @@ function sanitizeTelemetryPayload(payload = {}, headers = {}) {
313
463
  pipelineStatus: pickFirstText(raw.pipelineStatus, raw.workflowSprintStatus, raw.status),
314
464
  reasonCode,
315
465
  reasonDetail: pickFirstText(raw.reasonDetail, raw.reasonText, raw.otherReason, raw.notes),
466
+ integration: pickFirstText(raw.integration, raw.actionIntegration),
467
+ actionOperation: pickFirstText(raw.actionOperation, raw.operationId),
468
+ endpoint: pickFirstText(raw.endpoint, raw.apiPath),
469
+ decisionMode: pickFirstText(raw.decisionMode, raw.executionMode),
470
+ actionStatus: pickFirstText(raw.actionStatus, raw.status),
471
+ accepted: raw.accepted === undefined || raw.accepted === null ? null : Boolean(raw.accepted),
316
472
  pricingInterest: pickFirstText(raw.pricingInterest, raw.interestLevel),
317
473
  seoQuery: pickFirstText(raw.seoQuery, raw.query),
318
474
  seoSurface: pickFirstText(raw.seoSurface, raw.searchSurface, raw.surface),
@@ -331,11 +487,22 @@ function sanitizeTelemetryPayload(payload = {}, headers = {}) {
331
487
  httpStatus: normalizeInteger(raw.httpStatus),
332
488
  userAgent: pickFirstText(raw.userAgent, headers['user-agent']),
333
489
  isBot: pickFirstText(raw.isBot),
490
+ interstitialSampled: pickFirstText(raw.interstitialSampled),
491
+ interstitialSampleRate: normalizeRate(raw.interstitialSampleRate),
334
492
  attributionTagged: Boolean(
335
493
  pickFirstText(raw.utmSource, raw.utmMedium, raw.utmCampaign, raw.utmContent, raw.utmTerm)
336
494
  ),
337
495
  };
338
496
 
497
+ const audience = classifyTelemetryAudience(entry, raw);
498
+ entry.audience = audience.audience;
499
+ entry.trafficAudience = audience.audience;
500
+ entry.audienceReasons = audience.reasons;
501
+ entry.isExternal = audience.isExternal;
502
+ entry.isInternal = audience.isInternal;
503
+ entry.isTest = audience.isTest;
504
+ entry.isBotTraffic = audience.isBot;
505
+
339
506
  return entry;
340
507
  }
341
508
 
@@ -426,9 +593,162 @@ function summarizeRecentEvents(events) {
426
593
  community: entry.community || null,
427
594
  offerCode: entry.offerCode || null,
428
595
  campaignVariant: entry.campaignVariant || null,
596
+ audience: entry.audience || 'external',
597
+ audienceReasons: entry.audienceReasons || [],
429
598
  }));
430
599
  }
431
600
 
601
+ function summarizeExternalTrafficQuality(events) {
602
+ const byAudience = {};
603
+ const byExclusionReason = {};
604
+ const externalVisitors = new Set();
605
+ const externalSessions = new Set();
606
+ const externalCheckoutStarters = new Set();
607
+ const pageViewsBySource = {};
608
+ const pageViewsByPath = {};
609
+ const pageViewsByTrafficChannel = {};
610
+ const checkoutStartsBySource = {};
611
+ const checkoutStartsByTrafficChannel = {};
612
+ const buyerLossReasons = {};
613
+ const visitorPaths = new Map();
614
+ let externalEvents = 0;
615
+ let excludedEvents = 0;
616
+ let externalWebEvents = 0;
617
+ let externalPageViews = 0;
618
+ let externalCtaClicks = 0;
619
+ let externalCheckoutStarts = 0;
620
+ let externalBuyerLossSignals = 0;
621
+
622
+ for (const entry of events) {
623
+ const audience = entry.audience || 'external';
624
+ incrementCounter(byAudience, audience);
625
+ if (audience !== 'external') {
626
+ excludedEvents += 1;
627
+ const reasons = Array.isArray(entry.audienceReasons) && entry.audienceReasons.length > 0
628
+ ? entry.audienceReasons
629
+ : [`${audience}_traffic`];
630
+ for (const reason of reasons) incrementCounter(byExclusionReason, reason);
631
+ continue;
632
+ }
633
+
634
+ externalEvents += 1;
635
+ if ((entry.clientType || entry.client) !== 'web') continue;
636
+
637
+ externalWebEvents += 1;
638
+ const visitorKey = pickFirstText(entry.visitorId, entry.installId, entry.sessionId);
639
+ if (visitorKey) externalVisitors.add(visitorKey);
640
+ if (entry.sessionId) externalSessions.add(entry.sessionId);
641
+
642
+ const eventType = entry.eventType || entry.event;
643
+ if (visitorKey) {
644
+ const pathRow = visitorPaths.get(visitorKey) || {
645
+ visitorKey,
646
+ firstSeenAt: entry.receivedAt || null,
647
+ lastSeenAt: entry.receivedAt || null,
648
+ source: entry.source || null,
649
+ trafficChannel: entry.trafficChannel || null,
650
+ events: [],
651
+ pages: [],
652
+ checkoutStarts: 0,
653
+ };
654
+ if (!pathRow.firstSeenAt || String(entry.receivedAt || '') < pathRow.firstSeenAt) {
655
+ pathRow.firstSeenAt = entry.receivedAt || null;
656
+ }
657
+ if (!pathRow.lastSeenAt || String(entry.receivedAt || '') > pathRow.lastSeenAt) {
658
+ pathRow.lastSeenAt = entry.receivedAt || null;
659
+ }
660
+ pathRow.events.push({
661
+ at: entry.receivedAt || null,
662
+ eventType,
663
+ page: entry.page || entry.landingPath || null,
664
+ ctaId: entry.ctaId || null,
665
+ });
666
+ if ((entry.page || entry.landingPath) && !pathRow.pages.includes(entry.page || entry.landingPath)) {
667
+ pathRow.pages.push(entry.page || entry.landingPath);
668
+ }
669
+ if (eventType === 'checkout_start' || eventType === 'checkout_bootstrap') {
670
+ pathRow.checkoutStarts += 1;
671
+ }
672
+ visitorPaths.set(visitorKey, pathRow);
673
+ }
674
+
675
+ if (eventType === 'landing_page_view') {
676
+ externalPageViews += 1;
677
+ incrementCounter(pageViewsBySource, entry.source);
678
+ incrementCounter(pageViewsByPath, entry.page);
679
+ incrementCounter(pageViewsByTrafficChannel, entry.trafficChannel);
680
+ }
681
+
682
+ if (isMarketingClickEvent(eventType)) {
683
+ externalCtaClicks += 1;
684
+ }
685
+
686
+ if (eventType === 'checkout_start' || eventType === 'checkout_bootstrap') {
687
+ externalCheckoutStarts += 1;
688
+ incrementCounter(checkoutStartsBySource, entry.source);
689
+ incrementCounter(checkoutStartsByTrafficChannel, entry.trafficChannel);
690
+ const starterKey = pickFirstText(
691
+ entry.acquisitionId,
692
+ entry.visitorId,
693
+ entry.sessionId,
694
+ entry.installId,
695
+ entry.traceId
696
+ );
697
+ if (starterKey) externalCheckoutStarters.add(starterKey);
698
+ }
699
+
700
+ if (eventType === 'checkout_cancelled' || eventType === 'checkout_abandoned' || eventType === 'reason_not_buying') {
701
+ externalBuyerLossSignals += 1;
702
+ incrementCounter(buyerLossReasons, entry.reasonCode);
703
+ }
704
+ }
705
+
706
+ const topPath = getTopCounterEntry(pageViewsByPath);
707
+ const topSource = getTopCounterEntry(pageViewsBySource);
708
+ const topTrafficChannel = getTopCounterEntry(pageViewsByTrafficChannel);
709
+ const topExclusionReason = getTopCounterEntry(byExclusionReason);
710
+
711
+ return {
712
+ rawEvents: events.length,
713
+ externalEvents,
714
+ excludedEvents,
715
+ internalEvents: byAudience.internal || 0,
716
+ testEvents: byAudience.test || 0,
717
+ botEvents: byAudience.bot || 0,
718
+ exclusionRate: safeRate(excludedEvents, events.length),
719
+ byAudience,
720
+ byExclusionReason,
721
+ topExclusionReason: topExclusionReason ? { key: topExclusionReason[0], count: topExclusionReason[1] } : null,
722
+ external: {
723
+ totalEvents: externalWebEvents,
724
+ uniqueVisitors: externalVisitors.size,
725
+ uniqueSessions: externalSessions.size,
726
+ uniqueCheckoutStarters: externalCheckoutStarters.size,
727
+ pageViews: externalPageViews,
728
+ ctaClicks: externalCtaClicks,
729
+ checkoutStarts: externalCheckoutStarts,
730
+ buyerLossSignals: externalBuyerLossSignals,
731
+ pageViewToCheckoutRate: safeRate(externalCheckoutStarts, externalPageViews),
732
+ visitorToCheckoutRate: safeRate(externalCheckoutStarts, externalVisitors.size),
733
+ bySource: pageViewsBySource,
734
+ byPath: pageViewsByPath,
735
+ byTrafficChannel: pageViewsByTrafficChannel,
736
+ checkoutStartsBySource,
737
+ checkoutStartsByTrafficChannel,
738
+ buyerLossReasons,
739
+ topSource: topSource ? { key: topSource[0], count: topSource[1] } : null,
740
+ topPath: topPath ? { key: topPath[0], count: topPath[1] } : null,
741
+ topTrafficChannel: topTrafficChannel ? { key: topTrafficChannel[0], count: topTrafficChannel[1] } : null,
742
+ visitorPaths: Array.from(visitorPaths.values())
743
+ .sort((a, b) => String(a.firstSeenAt || '').localeCompare(String(b.firstSeenAt || '')))
744
+ .slice(0, 50),
745
+ },
746
+ verdict: externalEvents > 0
747
+ ? (excludedEvents > externalEvents ? 'polluted_but_usable' : 'usable')
748
+ : (events.length > 0 ? 'polluted_no_external_signal' : 'missing'),
749
+ };
750
+ }
751
+
432
752
  function getTelemetrySummary(feedbackDir, options = {}) {
433
753
  const analyticsWindow = resolveAnalyticsWindow(options);
434
754
  const telemetryLoadOptions = analyticsWindow.bounded
@@ -439,6 +759,7 @@ function getTelemetrySummary(feedbackDir, options = {}) {
439
759
  analyticsWindow,
440
760
  (entry) => entry && (entry.receivedAt || entry.timestamp)
441
761
  );
762
+ const trafficQuality = summarizeExternalTrafficQuality(events);
442
763
  const byClientType = {};
443
764
  const byEventType = {};
444
765
  const webVisitors = new Set();
@@ -526,6 +847,12 @@ function getTelemetrySummary(feedbackDir, options = {}) {
526
847
  let exitEngagementMsCount = 0;
527
848
  let exitScrollPercentTotal = 0;
528
849
  let exitScrollPercentCount = 0;
850
+ let chatgptActionCalls = 0;
851
+ let chatgptActionAccepted = 0;
852
+ const chatgptActionsByOperation = {};
853
+ const chatgptActionsByEndpoint = {};
854
+ const chatgptActionsByStatus = {};
855
+ const chatgptActionsByDecisionMode = {};
529
856
 
530
857
  for (const entry of events) {
531
858
  incrementCounter(byClientType, entry.clientType || entry.client || 'unknown');
@@ -714,6 +1041,18 @@ function getTelemetrySummary(feedbackDir, options = {}) {
714
1041
  }
715
1042
  }
716
1043
 
1044
+ if (
1045
+ String(entry.eventType || entry.event || '').startsWith('chatgpt_action_') ||
1046
+ entry.integration === 'chatgpt_gpt'
1047
+ ) {
1048
+ chatgptActionCalls += 1;
1049
+ if (entry.accepted === true) chatgptActionAccepted += 1;
1050
+ incrementCounter(chatgptActionsByOperation, entry.actionOperation);
1051
+ incrementCounter(chatgptActionsByEndpoint, entry.endpoint);
1052
+ incrementCounter(chatgptActionsByStatus, entry.actionStatus);
1053
+ incrementCounter(chatgptActionsByDecisionMode, entry.decisionMode);
1054
+ }
1055
+
717
1056
  if ((entry.clientType || entry.client) === 'cli') {
718
1057
  if (entry.installId) cliInstalls.add(entry.installId);
719
1058
  incrementCounter(cliByPlatform, entry.platform);
@@ -764,12 +1103,14 @@ function getTelemetrySummary(feedbackDir, options = {}) {
764
1103
  window: serializeAnalyticsWindow(analyticsWindow),
765
1104
  totalEvents: events.length,
766
1105
  latestSeenAt,
1106
+ trafficQuality,
767
1107
  byClientType,
768
1108
  byEventType,
769
1109
  conversionFunnel: {
770
1110
  landingViews: pageViews,
771
1111
  installCopies,
772
1112
  gptOpens,
1113
+ gptActionCalls: chatgptActionCalls,
773
1114
  checkoutStarts,
774
1115
  checkoutInterstitialViews,
775
1116
  checkoutInterstitialClicks,
@@ -777,6 +1118,7 @@ function getTelemetrySummary(feedbackDir, options = {}) {
777
1118
  proConversions,
778
1119
  landingToInstallCopyRate: safeRate(installCopies, pageViews),
779
1120
  landingToGptOpenRate: safeRate(gptOpens, pageViews),
1121
+ gptOpenToActionRate: safeRate(chatgptActionCalls, gptOpens),
780
1122
  landingToCheckoutRate: safeRate(checkoutStarts, pageViews),
781
1123
  checkoutInterstitialClickRate: safeRate(checkoutInterstitialClicks, checkoutInterstitialViews),
782
1124
  checkoutInterstitialProConfirmRate: safeRate(checkoutInterstitialProConfirms, checkoutInterstitialViews),
@@ -835,6 +1177,17 @@ function getTelemetrySummary(feedbackDir, options = {}) {
835
1177
  byPlatform: cliByPlatform,
836
1178
  byVersion: cliByVersion,
837
1179
  },
1180
+ chatgpt: {
1181
+ gptOpens,
1182
+ actionCalls: chatgptActionCalls,
1183
+ acceptedActionCalls: chatgptActionAccepted,
1184
+ openToActionRate: safeRate(chatgptActionCalls, gptOpens),
1185
+ acceptedActionRate: safeRate(chatgptActionAccepted, chatgptActionCalls),
1186
+ byOperation: chatgptActionsByOperation,
1187
+ byEndpoint: chatgptActionsByEndpoint,
1188
+ byStatus: chatgptActionsByStatus,
1189
+ byDecisionMode: chatgptActionsByDecisionMode,
1190
+ },
838
1191
  marketing: {
839
1192
  pageViewsBySource,
840
1193
  pageViewsByCampaign,
@@ -926,9 +1279,12 @@ function getTelemetryAnalytics(feedbackDir, options = {}) {
926
1279
  window: summary.window,
927
1280
  totalEvents: summary.totalEvents,
928
1281
  latestSeenAt: summary.latestSeenAt,
1282
+ trafficQuality: summary.trafficQuality,
1283
+ qualified: summary.trafficQuality.external,
929
1284
  byClientType: summary.byClientType,
930
1285
  byEventType: summary.byEventType,
931
1286
  conversionFunnel: summary.conversionFunnel,
1287
+ chatgpt: summary.chatgpt,
932
1288
  visitors: {
933
1289
  totalEvents: summary.web.totalEvents,
934
1290
  uniqueVisitors: summary.web.uniqueVisitors,
@@ -1104,6 +1460,7 @@ const appendTelemetryPing = appendTelemetryEvent;
1104
1460
  module.exports = {
1105
1461
  TELEMETRY_FILE_NAME,
1106
1462
  sanitizeTelemetryPayload,
1463
+ classifyTelemetryAudience,
1107
1464
  appendTelemetryPing,
1108
1465
  appendTelemetryEvent,
1109
1466
  getTelemetrySourceDiagnostics,
@@ -302,24 +302,44 @@ function getCalibration(model) {
302
302
  // ---------------------------------------------------------------------------
303
303
 
304
304
  /**
305
- * Draw one sample from the Beta posterior for each category via the
306
- * Marsaglia-Tsang (2000) gamma ratio method. No external library needed.
305
+ * Return the Beta posterior parameters after applying Thompson temperature
306
+ * scaling. The posterior mean is preserved while precision changes:
307
+ * lower temperatures sharpen the posterior, higher temperatures flatten it.
307
308
  *
308
- * betaSample(alpha, beta) = gammaSample(alpha) / (gammaSample(alpha) + gammaSample(beta))
309
+ * @param {Object} params - Category posterior parameters
310
+ * @param {number} temperature - Scaling factor (default 1.0)
311
+ * @returns {{ alpha: number, beta: number }}
312
+ */
313
+ function getTemperatureScaledPosteriorParams(params, temperature = 1.0) {
314
+ const T = Math.max(0.01, Number(temperature) || 1.0);
315
+ const invT = 1.0 / T;
316
+ return {
317
+ alpha: Math.max(params.alpha * invT, 0.01),
318
+ beta: Math.max(params.beta * invT, 0.01),
319
+ };
320
+ }
321
+
322
+ /**
323
+ * Draw one sample from the Beta posterior for each category.
324
+ * Supports temperature scaling to adjust exploitation vs exploration.
309
325
  *
310
- * This is the JS equivalent of Python's random.betavariate(alpha, beta).
311
- * Used for Thompson Sampling action selection (explore via uncertainty).
326
+ * Temperature (T):
327
+ * T = 1.0 (default) Standard Thompson Sampling.
328
+ * T < 1.0 — Sharper distribution, favors high-reliability categories (exploit).
329
+ * T > 1.0 — Flatter distribution, increases uncertainty and exploration.
330
+ *
331
+ * Implementation: Scales alpha and beta by 1/T.
312
332
  *
313
333
  * @param {Object} model - Model object containing categories
334
+ * @param {number} temperature - Scaling factor (default 1.0)
314
335
  * @returns {Object} Map of category → float sample in [0, 1]
315
336
  */
316
- function samplePosteriors(model) {
337
+ function samplePosteriors(model, temperature = 1.0) {
317
338
  const samples = {};
339
+
318
340
  for (const [cat, params] of Object.entries(model.categories || {})) {
319
- samples[cat] = betaSample(
320
- Math.max(params.alpha, 0.01),
321
- Math.max(params.beta, 0.01),
322
- );
341
+ const { alpha, beta } = getTemperatureScaledPosteriorParams(params, temperature);
342
+ samples[cat] = betaSample(alpha, beta);
323
343
  }
324
344
  return samples;
325
345
  }
@@ -451,6 +471,7 @@ module.exports = {
451
471
  getReliability,
452
472
  isCalibrated,
453
473
  getCalibration,
474
+ getTemperatureScaledPosteriorParams,
454
475
  samplePosteriors,
455
476
  argmaxPosteriors,
456
477
  pickBestCategory,