thumbgate 1.1.0 → 1.3.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 (63) hide show
  1. package/.claude-plugin/README.md +4 -4
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.well-known/mcp/server-card.json +1 -1
  5. package/README.md +48 -16
  6. package/adapters/README.md +1 -1
  7. package/adapters/claude/.mcp.json +2 -2
  8. package/adapters/codex/config.toml +2 -2
  9. package/adapters/mcp/server-stdio.js +11 -8
  10. package/adapters/opencode/opencode.json +1 -1
  11. package/bin/cli.js +20 -11
  12. package/config/github-about.json +1 -1
  13. package/config/model-tiers.json +11 -0
  14. package/package.json +22 -11
  15. package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +1 -1
  16. package/plugins/claude-codex-bridge/.mcp.json +1 -1
  17. package/plugins/codex-profile/.codex-plugin/plugin.json +1 -1
  18. package/plugins/codex-profile/.mcp.json +1 -1
  19. package/plugins/codex-profile/INSTALL.md +1 -1
  20. package/plugins/codex-profile/README.md +1 -1
  21. package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +1 -1
  22. package/plugins/cursor-marketplace/README.md +2 -2
  23. package/plugins/cursor-marketplace/commands/capture-feedback.md +2 -2
  24. package/plugins/cursor-marketplace/rules/feedback-capture.mdc +3 -3
  25. package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +3 -2
  26. package/plugins/opencode-profile/INSTALL.md +1 -1
  27. package/public/compare.html +302 -0
  28. package/public/guide.html +4 -4
  29. package/public/index.html +77 -38
  30. package/public/learn/ai-agent-persistent-memory.html +1 -0
  31. package/public/lessons.html +325 -17
  32. package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
  33. package/scripts/ai-search-visibility.js +142 -0
  34. package/scripts/audit-trail.js +6 -0
  35. package/scripts/capture-railway-diagnostics.sh +97 -0
  36. package/scripts/changeset-check.js +372 -0
  37. package/scripts/check-congruence.js +8 -5
  38. package/scripts/claude-feedback-sync.js +320 -0
  39. package/scripts/cli-telemetry.js +4 -1
  40. package/scripts/computer-use-firewall.js +45 -15
  41. package/scripts/contextfs.js +32 -23
  42. package/scripts/dashboard.js +84 -0
  43. package/scripts/docker-sandbox-planner.js +208 -0
  44. package/scripts/feedback-loop.js +16 -0
  45. package/scripts/github-about.js +56 -0
  46. package/scripts/intervention-policy.js +696 -0
  47. package/scripts/local-model-profile.js +18 -2
  48. package/scripts/model-tier-router.js +10 -1
  49. package/scripts/operational-integrity.js +361 -32
  50. package/scripts/prove-adapters.js +1 -0
  51. package/scripts/prove-automation.js +2 -2
  52. package/scripts/prove-packaged-runtime.js +260 -0
  53. package/scripts/prove-runtime.js +13 -0
  54. package/scripts/published-cli.js +10 -1
  55. package/scripts/rate-limiter.js +3 -3
  56. package/scripts/statusline-links.js +238 -0
  57. package/scripts/statusline-local-stats.js +2 -0
  58. package/scripts/statusline.sh +200 -10
  59. package/scripts/sync-github-about.js +7 -4
  60. package/scripts/tool-registry.js +2 -2
  61. package/scripts/workflow-sentinel.js +197 -39
  62. package/skills/thumbgate/SKILL.md +1 -1
  63. package/src/api/server.js +12 -1
@@ -42,6 +42,49 @@
42
42
  .stat-value.purple { color: var(--purple); }
43
43
  .stat-sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
44
44
 
45
+ /* IMPROVEMENT METRICS */
46
+ .metrics-panel { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 22px; margin: 0 0 24px; }
47
+ .metrics-header { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; margin-bottom: 18px; }
48
+ .metrics-header h2 { margin-bottom: 6px; }
49
+ .metrics-header p { font-size: 13px; color: var(--text-muted); max-width: 720px; }
50
+ .metrics-summary-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 18px; }
51
+ .metric-tile { background: var(--bg-raised); border: 1px solid var(--border); border-radius: 10px; padding: 16px; min-height: 94px; }
52
+ .metric-kicker { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-muted); margin-bottom: 6px; }
53
+ .metric-number { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; }
54
+ .metric-number.green { color: var(--green); }
55
+ .metric-number.red { color: var(--red); }
56
+ .metric-number.cyan { color: var(--cyan); }
57
+ .metric-number.purple { color: var(--purple); }
58
+ .metric-note { font-size: 12px; color: var(--text-muted); margin-top: 6px; }
59
+ .metrics-chart-wrap { background: var(--bg-raised); border: 1px solid var(--border); border-radius: 10px; padding: 16px; }
60
+ .metrics-chart-title { display: flex; justify-content: space-between; gap: 12px; align-items: center; margin-bottom: 12px; }
61
+ .metrics-chart-title h3 { font-size: 14px; margin: 0; }
62
+ .metrics-chart-title p { font-size: 12px; color: var(--text-muted); }
63
+ .metrics-legend { display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; color: var(--text-muted); margin-bottom: 12px; }
64
+ .metrics-legend span { display: inline-flex; align-items: center; gap: 6px; }
65
+ .metrics-legend-dot { width: 8px; height: 8px; border-radius: 999px; display: inline-block; }
66
+ .metrics-legend-dot.up { background: var(--green); }
67
+ .metrics-legend-dot.down { background: var(--red); }
68
+ .metrics-legend-dot.deny { background: var(--cyan); }
69
+ .metrics-legend-dot.warn { background: var(--yellow); }
70
+ .metrics-chart { display: grid; grid-template-columns: repeat(14, minmax(0, 1fr)); gap: 8px; align-items: end; min-height: 180px; }
71
+ .metrics-bar { background: transparent; border: 1px solid transparent; border-radius: 10px; padding: 8px 4px; color: inherit; cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 8px; min-width: 0; }
72
+ .metrics-bar:hover { border-color: rgba(34,211,238,0.35); background: rgba(34,211,238,0.04); }
73
+ .metrics-bar.selected { border-color: rgba(34,211,238,0.65); background: rgba(34,211,238,0.08); }
74
+ .metrics-bar-body { width: 100%; display: flex; justify-content: center; align-items: flex-end; gap: 6px; }
75
+ .metrics-bar-stack { width: 100%; max-width: 28px; height: 120px; display: flex; flex-direction: column; justify-content: flex-end; gap: 3px; }
76
+ .metrics-bar-stack.gate { max-width: 16px; height: 84px; }
77
+ .metrics-bar-empty { width: 100%; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px dashed rgba(255,255,255,0.05); flex: 1; }
78
+ .metrics-segment { width: 100%; border-radius: 8px; min-height: 0; }
79
+ .metrics-segment.up { background: linear-gradient(180deg, rgba(74,222,128,0.95), rgba(74,222,128,0.55)); }
80
+ .metrics-segment.down { background: linear-gradient(180deg, rgba(248,113,113,0.95), rgba(248,113,113,0.55)); }
81
+ .metrics-segment.deny { background: linear-gradient(180deg, rgba(34,211,238,0.95), rgba(34,211,238,0.55)); }
82
+ .metrics-segment.warn { background: linear-gradient(180deg, rgba(251,191,36,0.95), rgba(251,191,36,0.55)); }
83
+ .metrics-bar-total { font-size: 12px; font-weight: 600; color: var(--text); }
84
+ .metrics-bar-subtotal { font-size: 10px; color: var(--text-muted); line-height: 1; }
85
+ .metrics-bar-label { font-size: 11px; color: var(--text-muted); white-space: nowrap; }
86
+ .metrics-chart-note { font-size: 12px; color: var(--text-muted); margin-top: 12px; }
87
+
45
88
  /* TABS */
46
89
  .tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 24px; }
47
90
  .tab { padding: 12px 20px; font-size: 14px; font-weight: 500; color: var(--text-muted); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; }
@@ -114,6 +157,9 @@
114
157
 
115
158
  @media (max-width: 700px) {
116
159
  .stats-grid { grid-template-columns: repeat(2, 1fr); }
160
+ .metrics-header { flex-direction: column; }
161
+ .metrics-summary-grid { grid-template-columns: repeat(2, 1fr); }
162
+ .metrics-chart { grid-template-columns: repeat(7, minmax(0, 1fr)); row-gap: 12px; }
117
163
  .filter-row { flex-wrap: wrap; }
118
164
  .rule-meta { flex-direction: column; gap: 8px; }
119
165
  }
@@ -152,7 +198,7 @@
152
198
  <div class="container">
153
199
  <div style="margin:32px 0 24px;padding:24px;background:linear-gradient(135deg,rgba(167,139,250,0.08),rgba(34,211,238,0.05));border:1px solid rgba(167,139,250,0.2);border-radius:12px;">
154
200
  <h1 style="font-size:22px;font-weight:700;margin-bottom:8px;letter-spacing:-0.02em;">📚 Lessons Learned</h1>
155
- <p style="font-size:14px;color:var(--text-muted);line-height:1.6;max-width:700px;">Is your AI getting smarter? See every rule auto-generated from your feedback, how many mistakes each one prevented, and which patterns keep recurring. <span style="color:var(--purple);font-weight:600;">This is the proof your feedback works.</span></p>
201
+ <p style="font-size:14px;color:var(--text-muted);line-height:1.6;max-width:700px;">See what ThumbGate learned from your feedback, which failure patterns keep repeating, and how many actions the gate layer actually blocked. <span style="color:var(--purple);font-weight:600;">This view separates repeated failures from recorded gate blocks so the proof stays honest.</span></p>
156
202
  <div style="display:flex;gap:16px;margin-top:12px;font-size:12px;color:var(--text-muted);">
157
203
  <span>📋 <strong style="color:var(--text);">Active Rules</strong> — what was learned</span>
158
204
  <span>📊 <strong style="color:var(--text);">Timeline</strong> — when it was learned</span>
@@ -171,9 +217,9 @@
171
217
  <div class="stat-sub">Highest severity rules</div>
172
218
  </div>
173
219
  <div class="stat-card" onclick="switchTab('timeline')">
174
- <div class="stat-label">Mistakes Prevented</div>
175
- <div class="stat-value green" id="statPrevented">0</div>
176
- <div class="stat-sub">Gate blocks from learned rules</div>
220
+ <div class="stat-label">Actions Blocked</div>
221
+ <div class="stat-value green" id="statBlocked">0</div>
222
+ <div class="stat-sub">Recorded gate denies, not inferred repeats</div>
177
223
  </div>
178
224
  <div class="stat-card" onclick="switchTab('insights')">
179
225
  <div class="stat-label">Approval Trend</div>
@@ -186,6 +232,54 @@
186
232
  Loading lessons view...
187
233
  </div>
188
234
 
235
+ <div class="metrics-panel" id="metricsPanel">
236
+ <div class="metrics-header">
237
+ <div>
238
+ <h2>Improvement Over Time</h2>
239
+ <p id="metricsSummary">Loading defensible live metrics...</p>
240
+ </div>
241
+ <button class="filter-btn" id="metricsClearBtn" type="button" style="display:none;" onclick="clearTimelineDayFilter()">Clear day filter</button>
242
+ </div>
243
+ <div class="metrics-summary-grid" id="metricsSummaryGrid">
244
+ <div class="metric-tile">
245
+ <div class="metric-kicker">7d approval</div>
246
+ <div class="metric-number purple">--</div>
247
+ <div class="metric-note">Waiting for live feedback</div>
248
+ </div>
249
+ <div class="metric-tile">
250
+ <div class="metric-kicker">This week negatives</div>
251
+ <div class="metric-number red">--</div>
252
+ <div class="metric-note">Waiting for live feedback</div>
253
+ </div>
254
+ <div class="metric-tile">
255
+ <div class="metric-kicker">Actions blocked</div>
256
+ <div class="metric-number green">--</div>
257
+ <div class="metric-note">Recorded gate denies</div>
258
+ </div>
259
+ <div class="metric-tile">
260
+ <div class="metric-kicker">Repeat pressure</div>
261
+ <div class="metric-number cyan">--</div>
262
+ <div class="metric-note">Share of negatives that repeat</div>
263
+ </div>
264
+ </div>
265
+ <div class="metrics-chart-wrap">
266
+ <div class="metrics-chart-title">
267
+ <h3>Recent Feedback + Gate Activity</h3>
268
+ <p>Click any day to inspect the recorded feedback behind the trend and compare it with gate interceptions.</p>
269
+ </div>
270
+ <div class="metrics-legend">
271
+ <span><i class="metrics-legend-dot up"></i> Positive feedback</span>
272
+ <span><i class="metrics-legend-dot down"></i> Negative feedback</span>
273
+ <span><i class="metrics-legend-dot deny"></i> Gate deny</span>
274
+ <span><i class="metrics-legend-dot warn"></i> Gate warn</span>
275
+ </div>
276
+ <div class="metrics-chart" id="metricsChart">
277
+ <div class="loading" style="grid-column:1 / -1;">Loading chart...</div>
278
+ </div>
279
+ <div class="metrics-chart-note" id="metricsChartNote">The chart uses recorded feedback events, not inferred rule strength.</div>
280
+ </div>
281
+ </div>
282
+
189
283
  <div class="tabs">
190
284
  <div class="tab active" onclick="switchTab('rules')">📋 Active Rules</div>
191
285
  <div class="tab" onclick="switchTab('timeline')">📊 Feedback Timeline</div>
@@ -224,13 +318,13 @@
224
318
  <!-- TAB 3: INSIGHTS (PRO) -->
225
319
  <div class="tab-content" id="tab-insights">
226
320
  <div class="insight-card">
227
- <h3>🔥 Top Recurring Mistakes This Week</h3>
321
+ <h3>🔥 Top Recurring Failure Patterns</h3>
228
322
  <ul class="insight-list" id="insightMistakes">
229
323
  <li><span>Loading...</span></li>
230
324
  </ul>
231
325
  </div>
232
326
  <div class="insight-card">
233
- <h3>🛡️ Most Effective Rules</h3>
327
+ <h3>🛡️ Most Reinforced Rules</h3>
234
328
  <ul class="insight-list" id="insightEffective">
235
329
  <li><span>Loading...</span></li>
236
330
  </ul>
@@ -243,7 +337,7 @@
243
337
  </div>
244
338
  <div class="pro-badge" id="proBadge" style="display:none;">
245
339
  <h3>Unlock Full Insights</h3>
246
- <p>DPO training data export, effectiveness charts, pattern clustering, and weekly auto-digests.</p>
340
+ <p>DPO training data export, effectiveness charts, pattern clustering, weekly auto-digests, and a learned intervention policy trained from feedback, audits, and diagnostics.</p>
247
341
  <a href="/#pricing">Get Pro — $19/mo</a>
248
342
  </div>
249
343
  </div>
@@ -255,10 +349,14 @@ var allTimeline = [];
255
349
  var isDemo = true;
256
350
  var API_KEY = '';
257
351
  var dashboardSnapshot = null;
352
+ var activeTimelineSignal = 'all';
353
+ var activeTimelineDay = null;
354
+ var currentImprovementSeries = [];
258
355
  const BOOTSTRAP_API_KEY = __LESSONS_BOOTSTRAP_KEY__;
259
356
  const LOCAL_PRO_BOOTSTRAP = __LESSONS_BOOTSTRAP_ENABLED__;
260
357
 
261
358
  function escHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
359
+ function escAttr(s) { return escHtml(String(s || '')).replace(/"/g, '&quot;'); }
262
360
 
263
361
  function getHeaders() {
264
362
  return { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' };
@@ -284,6 +382,29 @@ function formatDate(value) {
284
382
  return ts.toLocaleString(undefined, { year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: '2-digit', second: '2-digit', timeZoneName: 'short' });
285
383
  }
286
384
 
385
+ function formatShortDay(dayKey) {
386
+ if (!dayKey) return '';
387
+ var ts = new Date(dayKey + 'T00:00:00');
388
+ if (Number.isNaN(ts.getTime())) return dayKey;
389
+ return ts.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
390
+ }
391
+
392
+ function normalizeSignal(value) {
393
+ var signal = String(value || '').toLowerCase();
394
+ if (signal === 'up' || signal === 'positive' || signal === 'thumbs_up') return 'up';
395
+ return 'down';
396
+ }
397
+
398
+ function toDayKey(value) {
399
+ if (!value) return '';
400
+ var ts = new Date(value);
401
+ if (Number.isNaN(ts.getTime())) return '';
402
+ var year = ts.getFullYear();
403
+ var month = String(ts.getMonth() + 1).padStart(2, '0');
404
+ var day = String(ts.getDate()).padStart(2, '0');
405
+ return year + '-' + month + '-' + day;
406
+ }
407
+
287
408
  function highlightCard(index) {
288
409
  document.querySelectorAll('.stat-card').forEach(function(c, i) {
289
410
  c.classList.toggle('selected', i === index);
@@ -325,12 +446,54 @@ function filterSeverity(level, el) {
325
446
  }
326
447
 
327
448
  function filterTimeline(signal, el) {
449
+ activeTimelineSignal = signal;
328
450
  if (el) {
329
451
  document.querySelectorAll('#tab-timeline .filter-btn').forEach(function(b) { b.classList.remove('active'); });
330
452
  el.classList.add('active');
331
453
  }
332
- var filtered = signal === 'all' ? allTimeline : allTimeline.filter(function(r) { return r.signal === signal; });
333
- renderTimeline(filtered);
454
+ renderTimeline(getFilteredTimelineItems());
455
+ updateMetricsSelection();
456
+ }
457
+
458
+ function getFilteredTimelineItems() {
459
+ return allTimeline.filter(function(item) {
460
+ if (activeTimelineSignal !== 'all' && item.signal !== activeTimelineSignal) return false;
461
+ if (activeTimelineDay && item.dayKey !== activeTimelineDay) return false;
462
+ return true;
463
+ });
464
+ }
465
+
466
+ function focusTimelineDay(dayKey) {
467
+ activeTimelineDay = dayKey;
468
+ switchTab('timeline');
469
+ renderTimeline(getFilteredTimelineItems());
470
+ updateMetricsSelection();
471
+ }
472
+
473
+ function clearTimelineDayFilter() {
474
+ activeTimelineDay = null;
475
+ renderTimeline(getFilteredTimelineItems());
476
+ updateMetricsSelection();
477
+ }
478
+
479
+ function updateMetricsSelection() {
480
+ document.querySelectorAll('.metrics-bar').forEach(function(bar) {
481
+ bar.classList.toggle('selected', activeTimelineDay && bar.getAttribute('data-day-key') === activeTimelineDay);
482
+ });
483
+ var clearBtn = document.getElementById('metricsClearBtn');
484
+ if (clearBtn) clearBtn.style.display = activeTimelineDay ? 'inline-flex' : 'none';
485
+ var note = document.getElementById('metricsChartNote');
486
+ if (!note) return;
487
+ if (activeTimelineDay) {
488
+ var selected = currentImprovementSeries.find(function(item) { return item.dayKey === activeTimelineDay; }) || null;
489
+ if (selected) {
490
+ note.textContent = 'Showing timeline events for ' + formatShortDay(activeTimelineDay) + '. Feedback: ' + selected.total + ' (' + selected.up + ' positive, ' + selected.down + ' negative). Gate audit: ' + selected.intercepted + ' intercepted (' + selected.deny + ' denies, ' + selected.warn + ' warns).';
491
+ } else {
492
+ note.textContent = 'Showing timeline events for ' + formatShortDay(activeTimelineDay) + '. Clear the day filter to return to the full timeline.';
493
+ }
494
+ } else {
495
+ note.textContent = 'The chart combines recorded feedback events with daily gate-audit activity.';
496
+ }
334
497
  }
335
498
 
336
499
  function searchRules() {
@@ -362,7 +525,7 @@ function renderRules(rules) {
362
525
  '</div>' +
363
526
  (r.context ? '<div class="rule-context">' + escHtml(r.context) + '</div>' : '') +
364
527
  '<div class="rule-meta">' +
365
- '<div class="rule-meta-item">Triggered <span class="value">' + (r.occurrences || 0) + '</span> times</div>' +
528
+ '<div class="rule-meta-item">Seen in feedback <span class="value">' + (r.occurrences || 0) + '</span> times</div>' +
366
529
  (r.lastTriggered ? '<div class="rule-meta-item">Last: <span class="value">' + escHtml(r.lastTriggered) + '</span></div>' : '') +
367
530
  '</div>' +
368
531
  (r.tags && r.tags.length ? '<div class="rule-tags">' + r.tags.map(function(t) { return '<span class="tag">' + escHtml(t) + '</span>'; }).join('') + '</div>' : '') +
@@ -420,17 +583,154 @@ function renderInsights(data) {
420
583
 
421
584
  mistakes.innerHTML = topMistakes.length ? topMistakes.map(function(r) {
422
585
  return insightRow(r, '<span style="color:var(--red);font-weight:600">' + (r.occurrences || 0) + 'x</span>');
423
- }).join('') : '<li><span style="color:var(--green)">No critical mistakes this week</span></li>';
586
+ }).join('') : '<li><span style="color:var(--green)">No critical repeated failures this week</span></li>';
424
587
 
425
588
  effective.innerHTML = topEffective.length ? topEffective.map(function(r) {
426
- return insightRow(r, '<span class="rule-effectiveness">Triggered ' + (r.occurrences || 0) + 'x</span>');
427
- }).join('') : '<li><span>No high-frequency rules yet</span></li>';
589
+ return insightRow(r, '<span class="rule-effectiveness">Seen ' + (r.occurrences || 0) + 'x</span>');
590
+ }).join('') : '<li><span>No reinforced rules yet</span></li>';
428
591
 
429
592
  stale.innerHTML = staleRules.length ? staleRules.map(function(r) {
430
593
  return insightRow(r, '<span style="color:var(--text-muted)">Archive?</span>');
431
594
  }).join('') : '<li><span style="color:var(--green)">All rules are active</span></li>';
432
595
  }
433
596
 
597
+ function buildFeedbackSeries(items, dayCount) {
598
+ var series = [];
599
+ var today = new Date();
600
+ today.setHours(0, 0, 0, 0);
601
+ var counts = {};
602
+ items.forEach(function(item) {
603
+ if (!item.dayKey) return;
604
+ if (!counts[item.dayKey]) counts[item.dayKey] = { up: 0, down: 0 };
605
+ counts[item.dayKey][item.signal] = (counts[item.dayKey][item.signal] || 0) + 1;
606
+ });
607
+ for (var idx = dayCount - 1; idx >= 0; idx--) {
608
+ var day = new Date(today);
609
+ day.setDate(today.getDate() - idx);
610
+ var dayKey = toDayKey(day.toISOString());
611
+ var entry = counts[dayKey] || { up: 0, down: 0 };
612
+ series.push({
613
+ dayKey: dayKey,
614
+ label: formatShortDay(dayKey),
615
+ up: entry.up || 0,
616
+ down: entry.down || 0,
617
+ total: (entry.up || 0) + (entry.down || 0)
618
+ });
619
+ }
620
+ return series;
621
+ }
622
+
623
+ function renderImprovementMetrics(stats, data) {
624
+ var summary = document.getElementById('metricsSummary');
625
+ var grid = document.getElementById('metricsSummaryGrid');
626
+ var chart = document.getElementById('metricsChart');
627
+ if (!summary || !grid || !chart) return;
628
+
629
+ if (isDemo) {
630
+ summary.textContent = 'Live improvement trends appear here once local Pro is connected. The chart is based on recorded feedback, gate denials, and repeat-failure pressure.';
631
+ grid.innerHTML = [
632
+ { label: '7d approval', value: '12.5%', tone: 'purple', note: 'Demo preview only' },
633
+ { label: 'This week negatives', value: '4', tone: 'red', note: 'Sample trend only' },
634
+ { label: 'Actions blocked', value: '42', tone: 'green', note: 'Sample gate denies' },
635
+ { label: 'Repeat pressure', value: '38%', tone: 'cyan', note: 'Sample repeated failures' }
636
+ ].map(function(tile) {
637
+ return '<div class="metric-tile"><div class="metric-kicker">' + escHtml(tile.label) + '</div><div class="metric-number ' + escHtml(tile.tone) + '">' + escHtml(tile.value) + '</div><div class="metric-note">' + escHtml(tile.note) + '</div></div>';
638
+ }).join('');
639
+ currentImprovementSeries = [];
640
+ chart.innerHTML = '<div class="empty" style="grid-column:1 / -1;">Upgrade to Pro to see the clickable live feedback and gate-audit timeline.</div>';
641
+ updateMetricsSelection();
642
+ return;
643
+ }
644
+
645
+ var gateStats = (data && data.gateStats) || {};
646
+ var gateAudit = (data && data.gateAudit) || {};
647
+ var liveMetrics = (data && data.liveMetrics) || {};
648
+ var harness = (data && data.harness) || {};
649
+ var approvalRate = Math.round(((stats && (stats.recentRate || stats.approvalRate)) || 0) * 100);
650
+ var errorTrend = liveMetrics.errorTrend || {};
651
+ var repeatPressure = Math.round(((harness.repeatFailureRate || 0) * 100));
652
+ var repeatedCount = harness.repeatedFailureCount || 0;
653
+ var thisWeekNeg = errorTrend.thisWeek || 0;
654
+ var lastWeekNeg = errorTrend.lastWeek || 0;
655
+ var blocked = gateStats.blocked || 0;
656
+
657
+ summary.textContent = '7-day approval is ' + approvalRate + '%. This week logged ' + thisWeekNeg + ' negative signal' + (thisWeekNeg === 1 ? '' : 's')
658
+ + ' versus ' + lastWeekNeg + ' last week, while gates recorded ' + blocked + ' deny decision' + (blocked === 1 ? '' : 's') + '.';
659
+
660
+ grid.innerHTML = [
661
+ { label: '7d approval', value: approvalRate + '%', tone: 'purple', note: 'Recent approval rate from feedback signals' },
662
+ { label: 'This week negatives', value: String(thisWeekNeg), tone: 'red', note: 'Vs ' + lastWeekNeg + ' last week' },
663
+ { label: 'Actions blocked', value: String(blocked), tone: 'green', note: 'Recorded gate denies from the audit trail' },
664
+ { label: 'Repeat pressure', value: repeatPressure + '%', tone: 'cyan', note: repeatedCount + ' repeated failure pattern' + (repeatedCount === 1 ? '' : 's') }
665
+ ].map(function(tile) {
666
+ return '<div class="metric-tile"><div class="metric-kicker">' + escHtml(tile.label) + '</div><div class="metric-number ' + escHtml(tile.tone) + '">' + escHtml(tile.value) + '</div><div class="metric-note">' + escHtml(tile.note) + '</div></div>';
667
+ }).join('');
668
+
669
+ var gateDays = {};
670
+ ((gateAudit.days) || []).forEach(function(day) {
671
+ gateDays[day.dayKey] = day;
672
+ });
673
+ var series = buildFeedbackSeries(allTimeline, 14).map(function(item) {
674
+ var gateDay = gateDays[item.dayKey] || { deny: 0, warn: 0, intercepted: 0 };
675
+ return {
676
+ dayKey: item.dayKey,
677
+ label: item.label,
678
+ up: item.up,
679
+ down: item.down,
680
+ total: item.total,
681
+ deny: gateDay.deny || 0,
682
+ warn: gateDay.warn || 0,
683
+ intercepted: gateDay.intercepted || 0
684
+ };
685
+ });
686
+ currentImprovementSeries = series;
687
+ var maxFeedbackTotal = series.reduce(function(max, item) { return Math.max(max, item.total); }, 0);
688
+ var maxGateTotal = series.reduce(function(max, item) { return Math.max(max, item.intercepted); }, 0);
689
+ if (maxFeedbackTotal === 0 && maxGateTotal === 0) {
690
+ currentImprovementSeries = [];
691
+ chart.innerHTML = '<div class="empty" style="grid-column:1 / -1;">No recent feedback events to chart yet.</div>';
692
+ updateMetricsSelection();
693
+ return;
694
+ }
695
+
696
+ chart.innerHTML = series.map(function(item) {
697
+ if (!item.total && !item.intercepted) {
698
+ return '<button class="metrics-bar" type="button" data-day-key="' + escAttr(item.dayKey) + '" onclick="focusTimelineDay(\'' + escAttr(item.dayKey) + '\')">' +
699
+ '<div class="metrics-bar-body"><div class="metrics-bar-stack"><div class="metrics-bar-empty"></div></div><div class="metrics-bar-stack gate"><div class="metrics-bar-empty"></div></div></div>' +
700
+ '<div class="metrics-bar-total">F0</div>' +
701
+ '<div class="metrics-bar-subtotal">G0</div>' +
702
+ '<div class="metrics-bar-label">' + escHtml(item.label) + '</div>' +
703
+ '</button>';
704
+ }
705
+ var feedbackHeight = item.total > 0 ? Math.max(14, Math.round((item.total / Math.max(maxFeedbackTotal, 1)) * 120)) : 0;
706
+ var upHeight = item.up > 0 ? Math.max(8, Math.round((item.up / Math.max(item.total, 1)) * feedbackHeight)) : 0;
707
+ var downHeight = item.down > 0 ? Math.max(8, feedbackHeight - upHeight) : 0;
708
+ if (item.up > 0 && item.down > 0 && upHeight + downHeight > 120) downHeight = Math.max(8, 120 - upHeight);
709
+ var gateHeight = item.intercepted > 0 ? Math.max(12, Math.round((item.intercepted / Math.max(maxGateTotal, 1)) * 84)) : 0;
710
+ var denyHeight = item.deny > 0 ? Math.max(8, Math.round((item.deny / Math.max(item.intercepted, 1)) * gateHeight)) : 0;
711
+ var warnHeight = item.warn > 0 ? Math.max(8, gateHeight - denyHeight) : 0;
712
+ if (item.deny > 0 && item.warn > 0 && denyHeight + warnHeight > 84) warnHeight = Math.max(8, 84 - denyHeight);
713
+ return '<button class="metrics-bar" type="button" data-day-key="' + escAttr(item.dayKey) + '" onclick="focusTimelineDay(\'' + escAttr(item.dayKey) + '\')">' +
714
+ '<div class="metrics-bar-body">' +
715
+ '<div class="metrics-bar-stack">' +
716
+ (item.up ? '<div class="metrics-segment up" style="height:' + upHeight + 'px" title="' + item.up + ' positive feedback"></div>' : '') +
717
+ (item.down ? '<div class="metrics-segment down" style="height:' + downHeight + 'px" title="' + item.down + ' negative feedback"></div>' : '') +
718
+ (!item.total ? '<div class="metrics-bar-empty"></div>' : '') +
719
+ '</div>' +
720
+ '<div class="metrics-bar-stack gate">' +
721
+ (item.deny ? '<div class="metrics-segment deny" style="height:' + denyHeight + 'px" title="' + item.deny + ' gate denies"></div>' : '') +
722
+ (item.warn ? '<div class="metrics-segment warn" style="height:' + warnHeight + 'px" title="' + item.warn + ' gate warns"></div>' : '') +
723
+ (!item.intercepted ? '<div class="metrics-bar-empty"></div>' : '') +
724
+ '</div>' +
725
+ '</div>' +
726
+ '<div class="metrics-bar-total">F' + item.total + '</div>' +
727
+ '<div class="metrics-bar-subtotal">G' + item.intercepted + '</div>' +
728
+ '<div class="metrics-bar-label">' + escHtml(item.label) + '</div>' +
729
+ '</button>';
730
+ }).join('');
731
+ updateMetricsSelection();
732
+ }
733
+
434
734
  function searchAndHighlight(ruleId) {
435
735
  var el = document.querySelector('[data-rule-id="' + ruleId + '"]');
436
736
  if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'center' }); el.style.borderColor = 'var(--cyan)'; setTimeout(function() { el.style.borderColor = ''; }, 2000); }
@@ -452,7 +752,6 @@ function mapLiveRules(payload) {
452
752
  title: result.title || ((result.lesson || {}).summary) || 'Lesson',
453
753
  context: ((result.lesson || {}).howToAvoid) || ((result.lesson || {}).summary) || (sourceFeedback && sourceFeedback.context) || '',
454
754
  occurrences: linkedGate && linkedGate.occurrences ? linkedGate.occurrences : 1,
455
- prevented: linkedGate && linkedGate.occurrences ? linkedGate.occurrences : 0,
456
755
  severity: deriveSeverity(result),
457
756
  tags: Array.isArray(result.tags) ? result.tags : [],
458
757
  lastTriggered: sourceFeedback && sourceFeedback.timestamp ? formatDate(sourceFeedback.timestamp) : formatDate(result.timestamp),
@@ -464,10 +763,13 @@ function mapLiveRules(payload) {
464
763
  function mapTimelineItems(payload, lessonMap) {
465
764
  return (payload.results || []).map(function(entry) {
466
765
  var linkedLesson = lessonMap.get(entry.id) || null;
766
+ var timestamp = entry.timestamp || null;
467
767
  return {
468
- signal: entry.signal || 'down',
768
+ signal: normalizeSignal(entry.signal || entry.feedback),
469
769
  context: entry.context || entry.title || '',
470
770
  date: formatDate(entry.timestamp),
771
+ timestamp: timestamp,
772
+ dayKey: toDayKey(timestamp),
471
773
  learned: linkedLesson ? linkedLesson.title : '',
472
774
  feedbackId: entry.id || ''
473
775
  };
@@ -517,16 +819,19 @@ function loadDemo() {
517
819
  allRules = demoRules.slice(0, 3);
518
820
  allTimeline = demoTimeline.slice(0, 3);
519
821
  dashboardSnapshot = null;
822
+ activeTimelineSignal = 'all';
823
+ activeTimelineDay = null;
520
824
 
521
825
  document.getElementById('statRules').textContent = demoRules.length;
522
826
  document.getElementById('statCritical').textContent = demoRules.filter(function(r) { return r.severity === 'critical'; }).length;
523
- document.getElementById('statPrevented').textContent = demoRules.reduce(function(sum, r) { return sum + (r.prevented || 0); }, 0);
827
+ document.getElementById('statBlocked').textContent = demoRules.reduce(function(sum, r) { return sum + (r.prevented || 0); }, 0);
524
828
  document.getElementById('statTrend').textContent = '12.5%';
525
829
  document.getElementById('statTrendSub').textContent = '7-day approval rate';
526
830
 
527
831
  renderRules(allRules);
528
832
  renderTimeline(allTimeline);
529
833
  renderInsights();
834
+ renderImprovementMetrics({ recentRate: 0.125 }, null);
530
835
  renderUpgradeWall('rulesList');
531
836
  renderUpgradeWall('timelineList');
532
837
  setMode('Demo preview — upgrade to Pro to see your live lessons, timeline, and insights.', true);
@@ -560,16 +865,19 @@ async function loadLive() {
560
865
  .filter(function(rule) { return rule.sourceFeedbackId; })
561
866
  .map(function(rule) { return [rule.sourceFeedbackId, rule]; }));
562
867
  allTimeline = mapTimelineItems(feedbackPayload, lessonMap);
868
+ activeTimelineSignal = 'all';
869
+ activeTimelineDay = null;
563
870
 
564
871
  document.getElementById('statRules').textContent = allRules.length;
565
872
  document.getElementById('statCritical').textContent = allRules.filter(function(r) { return r.severity === 'critical'; }).length;
566
- document.getElementById('statPrevented').textContent = allRules.reduce(function(sum, r) { return sum + (r.prevented || 0); }, 0);
873
+ document.getElementById('statBlocked').textContent = ((dashboardSnapshot.gateStats || {}).blocked || 0);
567
874
  document.getElementById('statTrend').textContent = ((stats.recentRate || stats.approvalRate || 0) * 100).toFixed(1) + '%';
568
875
  document.getElementById('statTrendSub').textContent = 'Live approval rate';
569
876
 
570
877
  renderRules(allRules);
571
878
  renderTimeline(allTimeline);
572
879
  renderInsights(dashboardSnapshot);
880
+ renderImprovementMetrics(stats, dashboardSnapshot);
573
881
  document.getElementById('proBadge').style.display = 'none';
574
882
  setMode('Local Pro connected — this lessons view is reading your live rules, feedback timeline, and insights.', false);
575
883
  } catch (_err) {
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+
3
+ const https = require('node:https');
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+
7
+ const PROMPTS = [
8
+ 'best pre-action gate tools for AI coding agents',
9
+ 'how to prevent AI coding agent from making mistakes',
10
+ 'Claude Code safety tools',
11
+ 'npm packages for AI agent guardrails',
12
+ 'how to block bad tool calls in AI agents',
13
+ 'alternatives to thumbgate',
14
+ 'pre-tool-use hooks for AI agents',
15
+ 'AI coding agent memory and learning',
16
+ ];
17
+
18
+ function queryPerplexity(prompt, apiKey) {
19
+ return new Promise((resolve, reject) => {
20
+ const body = JSON.stringify({
21
+ model: 'sonar',
22
+ messages: [{ role: 'user', content: prompt }],
23
+ });
24
+ const req = https.request(
25
+ {
26
+ hostname: 'api.perplexity.ai',
27
+ path: '/chat/completions',
28
+ method: 'POST',
29
+ headers: {
30
+ Authorization: `Bearer ${apiKey}`,
31
+ 'Content-Type': 'application/json',
32
+ 'Content-Length': Buffer.byteLength(body),
33
+ },
34
+ },
35
+ (res) => {
36
+ const chunks = [];
37
+ res.on('data', (c) => chunks.push(c));
38
+ res.on('end', () => {
39
+ try {
40
+ const json = JSON.parse(Buffer.concat(chunks).toString());
41
+ const content = json.choices?.[0]?.message?.content || '';
42
+ resolve(content);
43
+ } catch (e) {
44
+ reject(new Error(`Failed to parse Perplexity response: ${e.message}`));
45
+ }
46
+ });
47
+ }
48
+ );
49
+ req.on('error', reject);
50
+ req.write(body);
51
+ req.end();
52
+ });
53
+ }
54
+
55
+ async function runVisibilityCheck(opts = {}) {
56
+ const apiKey = opts.apiKey || process.env.PERPLEXITY_API_KEY;
57
+ const queryFn = opts.queryFn || (apiKey ? (p) => queryPerplexity(p, apiKey) : null);
58
+
59
+ const results = [];
60
+ for (const prompt of PROMPTS) {
61
+ if (!queryFn) {
62
+ results.push({ prompt, status: 'MANUAL', response: null });
63
+ continue;
64
+ }
65
+ try {
66
+ const response = await queryFn(prompt);
67
+ const found = /thumbgate/i.test(response);
68
+ results.push({ prompt, status: found ? 'FOUND' : 'MISSING', response });
69
+ } catch (err) {
70
+ results.push({ prompt, status: 'ERROR', response: null, error: err.message });
71
+ }
72
+ }
73
+ return results;
74
+ }
75
+
76
+ function formatReport(results) {
77
+ const date = new Date().toISOString().slice(0, 10);
78
+ const lines = [`AI Search Visibility Report — ${date}`, '='.repeat(42)];
79
+
80
+ for (const r of results) {
81
+ const tag = `[${r.status}]`.padEnd(10);
82
+ const shortPrompt =
83
+ r.prompt.length > 60 ? r.prompt.slice(0, 57) + '...' : r.prompt;
84
+ const suffix =
85
+ r.status === 'FOUND'
86
+ ? '— mentioned in response'
87
+ : r.status === 'MISSING'
88
+ ? '— not found'
89
+ : r.status === 'MANUAL'
90
+ ? '— check manually'
91
+ : `— ${r.error || 'error'}`;
92
+ lines.push(`${tag} "${shortPrompt}" ${suffix}`);
93
+ }
94
+
95
+ const hasApi = results.some((r) => r.status !== 'MANUAL');
96
+ if (hasApi) {
97
+ const found = results.filter((r) => r.status === 'FOUND').length;
98
+ const total = results.filter((r) => r.status !== 'MANUAL').length;
99
+ lines.push('', `Score: ${found}/${total} prompts mention ThumbGate`);
100
+ } else {
101
+ lines.push('', `Manual checklist: ${results.length} prompts to test`);
102
+ }
103
+ return lines.join('\n');
104
+ }
105
+
106
+ function saveReport(results, opts = {}) {
107
+ const date = new Date().toISOString().slice(0, 10);
108
+ const dir = opts.dir || path.join(process.cwd(), '.thumbgate', 'ai-visibility');
109
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
110
+
111
+ const filePath = path.join(dir, `${date}.json`);
112
+ const found = results.filter((r) => r.status === 'FOUND').length;
113
+ const total = results.filter((r) => r.status !== 'MANUAL').length;
114
+
115
+ const report = {
116
+ date,
117
+ score: total > 0 ? `${found}/${total}` : 'manual',
118
+ results: results.map((r) => ({
119
+ prompt: r.prompt,
120
+ status: r.status,
121
+ ...(r.error ? { error: r.error } : {}),
122
+ })),
123
+ };
124
+
125
+ fs.writeFileSync(filePath, JSON.stringify(report, null, 2));
126
+ return filePath;
127
+ }
128
+
129
+ module.exports = { PROMPTS, queryPerplexity, runVisibilityCheck, formatReport, saveReport };
130
+
131
+ if (require.main === module) {
132
+ (async () => {
133
+ const results = await runVisibilityCheck();
134
+ const report = formatReport(results);
135
+ console.log(report);
136
+ const filePath = saveReport(results);
137
+ console.log(`\nReport saved to ${filePath}`);
138
+ })().catch((err) => {
139
+ console.error('Error:', err.message);
140
+ process.exit(1);
141
+ });
142
+ }
@@ -64,6 +64,12 @@ function recordAuditEvent(params = {}) {
64
64
  };
65
65
 
66
66
  fs.appendFileSync(logPath, JSON.stringify(record) + '\n');
67
+ try {
68
+ const { trainAndPersistInterventionPolicy } = require('./intervention-policy');
69
+ trainAndPersistInterventionPolicy(path.dirname(logPath));
70
+ } catch {
71
+ // Keep audit recording resilient even if the learned policy refresh fails.
72
+ }
67
73
  return record;
68
74
  }
69
75