thumbgate 1.27.18 → 1.27.20
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.
- package/.claude/commands/dashboard.md +15 -0
- package/.claude/commands/thumbgate-blocked.md +27 -0
- package/.claude/commands/thumbgate-dashboard.md +15 -0
- package/.claude/commands/thumbgate-doctor.md +30 -0
- package/.claude/commands/thumbgate-guard.md +36 -0
- package/.claude/commands/thumbgate-protect.md +30 -0
- package/.claude/commands/thumbgate-rules.md +30 -0
- package/.claude-plugin/plugin.json +4 -3
- package/.well-known/agentic-verify.txt +1 -0
- package/.well-known/llms.txt +33 -12
- package/.well-known/mcp/server-card.json +8 -8
- package/README.md +246 -30
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/codex/config.toml +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/gemini/function-declarations.json +1 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +263 -11
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/policy-engine/ethicore-guardian-client.js +68 -0
- package/adapters/policy-engine/thumbgate-policy-engine-adapter.js +260 -0
- package/bench/observability-eval-suite.json +26 -0
- package/bench/thumbgate-bench.json +2 -2
- package/bin/cli.js +1456 -122
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +11 -27
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -0
- package/config/gate-classifier-routing.json +98 -0
- package/config/gate-templates.json +216 -0
- package/config/gates/claim-verification.json +12 -0
- package/config/gates/default.json +31 -2
- package/config/github-about.json +2 -2
- package/config/mcp-allowlists.json +23 -13
- package/config/merge-quality-checks.json +0 -1
- package/config/model-candidates.json +121 -6
- package/config/post-deploy-marketing-pages.json +80 -0
- package/config/tessl-tiles.json +1 -3
- package/openapi/openapi.yaml +12 -0
- package/package.json +225 -100
- package/public/about.html +162 -0
- package/public/agent-manager.html +179 -0
- package/public/agents-cost-savings.html +153 -0
- package/public/ai-malpractice-prevention.html +818 -0
- package/public/assets/brand/github-social-preview.png +0 -0
- package/public/assets/brand/thumbgate-icon-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-pro-512.png +0 -0
- package/public/assets/brand/thumbgate-icon-team-512.png +0 -0
- package/public/assets/brand/thumbgate-logo-1200x360.png +0 -0
- package/public/assets/brand/thumbgate-logo-transparent.svg +28 -0
- package/public/assets/brand/thumbgate-mark-inline-v3.svg +18 -0
- package/public/assets/brand/thumbgate-mark-pro.svg +23 -0
- package/public/assets/brand/thumbgate-mark-team.svg +26 -0
- package/public/assets/brand/thumbgate-mark.svg +21 -0
- package/public/assets/brand/thumbgate-wordmark.svg +20 -0
- package/public/assets/claude-thumbgate-statusbar.svg +8 -0
- package/public/assets/codex-thumbgate-statusbar-test.svg +9 -0
- package/public/assets/legal-intake-control-flow.svg +66 -0
- package/public/blog.html +4 -4
- package/public/brand/thumbgate-mark.svg +19 -0
- package/public/brand/thumbgate-og.svg +16 -0
- package/public/chatgpt-app.html +330 -0
- package/public/codex-enterprise.html +123 -0
- package/public/codex-plugin.html +72 -20
- package/public/compare.html +31 -8
- package/public/dashboard.html +930 -166
- package/public/diagnostic.html +345 -0
- package/public/federal.html +2 -2
- package/public/guide.html +33 -13
- package/public/index.html +469 -111
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/learn.html +183 -18
- package/public/lessons.html +168 -10
- package/public/numbers.html +7 -7
- package/public/pricing.html +399 -0
- package/public/pro.html +34 -11
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-memory-lifecycle.js +211 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/agent-readiness.js +20 -3
- package/scripts/agent-reward-model.js +53 -1
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/auto-promote-gates.js +82 -10
- package/scripts/auto-wire-hooks.js +14 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/billing.js +93 -1
- package/scripts/bot-detection.js +61 -3
- package/scripts/build-metadata.js +50 -10
- package/scripts/classifier-routing.js +130 -0
- package/scripts/cli-feedback.js +4 -2
- package/scripts/cli-schema.js +97 -0
- package/scripts/cli-telemetry.js +6 -1
- package/scripts/commercial-offer.js +82 -2
- package/scripts/context-manager.js +74 -6
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/dashboard.js +68 -2
- package/scripts/export-databricks-bundle.js +5 -1
- package/scripts/export-dpo-pairs.js +7 -2
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-loop.js +123 -1
- package/scripts/feedback-quality.js +87 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/filesystem-search.js +35 -10
- package/scripts/gate-stats.js +89 -0
- package/scripts/gates-engine.js +1176 -85
- package/scripts/gemini-embedding-policy.js +2 -1
- package/scripts/hook-runtime.js +20 -14
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/hook-thumbgate-cache-updater.js +18 -2
- package/scripts/hybrid-feedback-context.js +142 -7
- package/scripts/install-shim.js +87 -0
- package/scripts/lesson-inference.js +8 -3
- package/scripts/lesson-search.js +17 -1
- package/scripts/license.js +10 -10
- package/scripts/llm-client.js +169 -4
- package/scripts/local-model-profile.js +15 -8
- package/scripts/mcp-config.js +7 -1
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/memory-scope-readiness.js +159 -0
- package/scripts/meta-agent-loop.js +36 -0
- package/scripts/noop-detect.js +285 -0
- package/scripts/operational-integrity.js +39 -5
- package/scripts/oss-pr-opportunity-scout.js +35 -5
- package/scripts/parallel-workflow-orchestrator.js +293 -0
- package/scripts/plan-gate.js +243 -0
- package/scripts/plausible-domain-config.js +99 -0
- package/scripts/plausible-server-events.js +9 -6
- package/scripts/pro-local-dashboard.js +4 -4
- package/scripts/proxy-pointer-rag-guardrails.js +42 -1
- package/scripts/published-cli.js +0 -8
- package/scripts/qa-scenario-planner.js +136 -0
- package/scripts/rate-limiter.js +64 -13
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- package/scripts/secret-scanner.js +44 -5
- package/scripts/security-scanner.js +260 -10
- package/scripts/self-distill-agent.js +3 -1
- package/scripts/self-harness-optimizer.js +141 -0
- package/scripts/self-healing-check.js +193 -0
- package/scripts/self-protection.js +90 -0
- package/scripts/seo-gsd.js +916 -7
- package/scripts/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-path.js +17 -2
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/statusline-local-stats.js +9 -1
- package/scripts/statusline-meta.js +28 -2
- package/scripts/statusline.sh +20 -4
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/telemetry-analytics.js +357 -0
- package/scripts/thompson-sampling.js +31 -10
- package/scripts/thumbgate-bench.js +16 -1
- package/scripts/thumbgate-search.js +85 -19
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/tool-registry.js +169 -1
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/vector-store.js +45 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/scripts/workflow-sentinel.js +286 -53
- package/scripts/workspace-evolver.js +62 -2
- package/src/api/server.js +2683 -319
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1695
- package/scripts/bot-detector.js +0 -50
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Context Manager — Unified Context-Augmented Generation (CAG) Orchestrator
|
|
6
9
|
*
|
|
@@ -27,8 +30,9 @@ const {
|
|
|
27
30
|
recordProvenance,
|
|
28
31
|
} = require('./contextfs');
|
|
29
32
|
const { loadOptionalModule } = require('./private-core-boundary');
|
|
30
|
-
const { retrieveRelevantLessons } = loadOptionalModule('./lesson-retrieval', () => ({
|
|
33
|
+
const { retrieveRelevantLessons, calculateRetrievalEntropy } = loadOptionalModule('./lesson-retrieval', () => ({
|
|
31
34
|
retrieveRelevantLessons: () => [],
|
|
35
|
+
calculateRetrievalEntropy: () => 0,
|
|
32
36
|
}));
|
|
33
37
|
const { evaluatePretool } = require('./hybrid-feedback-context');
|
|
34
38
|
const { loadProfile } = require('./user-profile');
|
|
@@ -93,13 +97,21 @@ function assembleSession() {
|
|
|
93
97
|
|
|
94
98
|
function assembleLessons(query, agentProfile, options = {}) {
|
|
95
99
|
try {
|
|
96
|
-
|
|
100
|
+
const lessons = retrieveRelevantLessons(
|
|
97
101
|
options.toolName || '',
|
|
98
102
|
query,
|
|
99
103
|
{ maxResults: agentProfile.maxLessons, feedbackDir: options.feedbackDir },
|
|
100
104
|
);
|
|
105
|
+
|
|
106
|
+
const entropy = calculateRetrievalEntropy(lessons);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
items: lessons,
|
|
110
|
+
entropy,
|
|
111
|
+
highConflict: entropy > 0.7
|
|
112
|
+
};
|
|
101
113
|
} catch {
|
|
102
|
-
return [];
|
|
114
|
+
return { items: [], entropy: 0, highConflict: false };
|
|
103
115
|
}
|
|
104
116
|
}
|
|
105
117
|
|
|
@@ -111,13 +123,41 @@ function assembleGuards(toolName, toolInput) {
|
|
|
111
123
|
}
|
|
112
124
|
}
|
|
113
125
|
|
|
114
|
-
function assembleContextPack(query, agentProfile) {
|
|
126
|
+
function assembleContextPack(query, agentProfile, options = {}) {
|
|
127
|
+
const { guards } = options;
|
|
115
128
|
try {
|
|
116
129
|
ensureContextFs();
|
|
130
|
+
|
|
131
|
+
// 1. Proactive Governance: Filter what the agent sees based on prevention rules
|
|
132
|
+
let structuredQuery = query;
|
|
133
|
+
if (guards && guards.mode === 'block') {
|
|
134
|
+
structuredQuery = `${query} (Active Block Policy: ${guards.reason})`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 2. Elevate Thompson Sampling to Architecture Level
|
|
138
|
+
let strategy = null;
|
|
139
|
+
try {
|
|
140
|
+
const ts = require('./thompson-sampling');
|
|
141
|
+
const model = ts.loadModel();
|
|
142
|
+
const bestCategory = ts.argmaxPosteriors(model);
|
|
143
|
+
|
|
144
|
+
// Route between context-building strategies based on TS posterior mean
|
|
145
|
+
if (bestCategory === 'architecture' || bestCategory === 'infra') {
|
|
146
|
+
strategy = 'hierarchical';
|
|
147
|
+
} else if (bestCategory === 'observability' || bestCategory === 'debugging') {
|
|
148
|
+
strategy = 'summarize-then-expand';
|
|
149
|
+
} else {
|
|
150
|
+
strategy = 'semantic';
|
|
151
|
+
}
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// Fallback to default routing
|
|
154
|
+
}
|
|
155
|
+
|
|
117
156
|
return constructContextPack({
|
|
118
|
-
query,
|
|
157
|
+
query: structuredQuery,
|
|
119
158
|
maxItems: Math.min(8, Math.ceil(agentProfile.contextBudget / 1000)),
|
|
120
159
|
maxChars: agentProfile.contextBudget,
|
|
160
|
+
strategy
|
|
121
161
|
});
|
|
122
162
|
} catch {
|
|
123
163
|
return null;
|
|
@@ -196,7 +236,8 @@ function assembleUnifiedContext(params = {}) {
|
|
|
196
236
|
// Assemble all components — each is fault-tolerant
|
|
197
237
|
const session = assembleSession();
|
|
198
238
|
const userProfile = assembleUserProfile();
|
|
199
|
-
const
|
|
239
|
+
const lessonData = assembleLessons(query, agentProfile, { toolName, feedbackDir });
|
|
240
|
+
const lessons = lessonData.items;
|
|
200
241
|
const guards = assembleGuards(toolName, toolInput);
|
|
201
242
|
const contextPack = assembleContextPack(query, agentProfile);
|
|
202
243
|
const codeGraph = assembleCodeGraph(query, repoPath, agentProfile);
|
|
@@ -204,9 +245,24 @@ function assembleUnifiedContext(params = {}) {
|
|
|
204
245
|
const components = { session, userProfile, lessons, guards, contextPack, codeGraph };
|
|
205
246
|
const tier = classifyTier(components);
|
|
206
247
|
|
|
248
|
+
// v4.2: Entropy-Aware Reliability Directive
|
|
249
|
+
let reliabilityDirective = null;
|
|
250
|
+
if (lessonData.highConflict) {
|
|
251
|
+
reliabilityDirective = 'CAUTION: Conflicting past patterns detected for this action. Prioritize absolute ground truth verification over rapid completion.';
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// v1.26.0: CodeRabbit Planning Directive
|
|
255
|
+
const planPath = path.join(repoPath || process.cwd(), 'PLAN.md');
|
|
256
|
+
if (!fs.existsSync(planPath) && ['Bash', 'Write', 'Edit', 'Deploy'].includes(toolName)) {
|
|
257
|
+
const planReminder = 'ORCHESTRATION: High-risk action detected without a PLAN.md. Please document your intent, assumptions, and verification steps before proceeding.';
|
|
258
|
+
reliabilityDirective = reliabilityDirective ? `${reliabilityDirective}\n\n${planReminder}` : planReminder;
|
|
259
|
+
}
|
|
260
|
+
|
|
207
261
|
const result = {
|
|
208
262
|
tier,
|
|
209
263
|
agentType: agentType || 'default',
|
|
264
|
+
reliabilityDirective,
|
|
265
|
+
entropy: lessonData.entropy,
|
|
210
266
|
agentProfile: {
|
|
211
267
|
maxLessons: agentProfile.maxLessons,
|
|
212
268
|
contextBudget: agentProfile.contextBudget,
|
|
@@ -228,6 +284,12 @@ function assembleUnifiedContext(params = {}) {
|
|
|
228
284
|
})),
|
|
229
285
|
visibility: contextPack.visibility || null,
|
|
230
286
|
cached: !!(contextPack.cache && contextPack.cache.hit),
|
|
287
|
+
layers: {
|
|
288
|
+
localState: session || null,
|
|
289
|
+
graphState: codeGraph || null,
|
|
290
|
+
policyState: guards || null,
|
|
291
|
+
sessionState: contextPack.items ? contextPack.items.filter(i => i.namespace === 'session') : []
|
|
292
|
+
}
|
|
231
293
|
} : null,
|
|
232
294
|
codeGraph: codeGraph || null,
|
|
233
295
|
assembledAt: new Date().toISOString(),
|
|
@@ -306,6 +368,12 @@ function formatUnifiedContext(ctx) {
|
|
|
306
368
|
|
|
307
369
|
// Context pack
|
|
308
370
|
if (ctx.contextPack) {
|
|
371
|
+
lines.push(`### Context Architecture Layers`);
|
|
372
|
+
lines.push(`- Local State: ${ctx.contextPack.layers.localState ? 'Active' : 'Empty'}`);
|
|
373
|
+
lines.push(`- Graph State: ${ctx.contextPack.layers.graphState ? 'Active' : 'Empty'}`);
|
|
374
|
+
lines.push(`- Policy State: ${ctx.contextPack.layers.policyState ? ctx.contextPack.layers.policyState.mode : 'Empty'}`);
|
|
375
|
+
lines.push(`- Session State: ${ctx.contextPack.layers.sessionState.length} items`);
|
|
376
|
+
lines.push('');
|
|
309
377
|
lines.push(`### Context Pack (${ctx.contextPack.itemCount} items)`);
|
|
310
378
|
ctx.contextPack.items.forEach((item) => {
|
|
311
379
|
lines.push(`- [${item.namespace}] ${item.title} (score: ${item.score})`);
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// scripts/dashboard-chat.js
|
|
4
|
+
// -----------------------------------------------------------------------------
|
|
5
|
+
// "Chat with your data" — the dashboard chat backend. Local-first RAG over
|
|
6
|
+
// this install's ThumbGate data (lessons, raw feedback memories via LanceDB
|
|
7
|
+
// vectors, receipts, gate stats). Retrieval is local (lesson search + optional
|
|
8
|
+
// vector-store.searchSimilar). Generation uses your configured LLM: a local
|
|
9
|
+
// OpenAI-compatible endpoint first, then Gemini or Perplexity when explicitly
|
|
10
|
+
// configured.
|
|
11
|
+
//
|
|
12
|
+
// Dialogflow/Google is not the dashboard chatbot brain. It remains an optional
|
|
13
|
+
// guard-adapter path for buyers who already run their own Google agent tenancy.
|
|
14
|
+
// -----------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const GEMINI_ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models';
|
|
19
|
+
const PERPLEXITY_ENDPOINT = 'https://api.perplexity.ai/chat/completions';
|
|
20
|
+
const DEFAULT_MODEL = 'gemini-2.5-flash';
|
|
21
|
+
const MAX_QUESTION_CHARS = 2000;
|
|
22
|
+
const MAX_CONTEXT_LESSONS = 8;
|
|
23
|
+
|
|
24
|
+
// Allowlist the model so a user-supplied `model` cannot route the call to an
|
|
25
|
+
// arbitrary / unexpected (or more expensive) endpoint. Anything not on the list
|
|
26
|
+
// falls back to the default.
|
|
27
|
+
const ALLOWED_MODELS = new Set([
|
|
28
|
+
'gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro',
|
|
29
|
+
'gemini-2.0-flash', 'gemini-2.0-flash-lite',
|
|
30
|
+
'gemini-flash-latest', 'gemini-flash-lite-latest', 'gemini-pro-latest',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function resolveModel(requested) {
|
|
34
|
+
const r = String(requested || '').trim();
|
|
35
|
+
if (r && ALLOWED_MODELS.has(r)) return r;
|
|
36
|
+
const envModel = String(process.env.THUMBGATE_GEMINI_MODEL || '').trim();
|
|
37
|
+
if (envModel && ALLOWED_MODELS.has(envModel)) return envModel;
|
|
38
|
+
return DEFAULT_MODEL;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveApiKey(opts = {}) {
|
|
42
|
+
let key = '';
|
|
43
|
+
if (Object.hasOwn(opts, 'apiKey')) {
|
|
44
|
+
key = opts.apiKey || '';
|
|
45
|
+
} else {
|
|
46
|
+
key = opts.apiKey || process.env.GEMINI_API_KEY || process.env.THUMBGATE_GEMINI_API_KEY || process.env.GOOGLE_API_KEY || process.env.PERPLEXITY_API_KEY || process.env.THUMBGATE_PERPLEXITY_API_KEY || '';
|
|
47
|
+
}
|
|
48
|
+
if (!key) return '';
|
|
49
|
+
return key.trim().replace(/^["']|["']$/g, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function debugChatFallback(label, err) {
|
|
53
|
+
if (process.env.THUMBGATE_DEBUG_CHAT !== '1') return;
|
|
54
|
+
const detail = err?.message ? err.message : String(err);
|
|
55
|
+
console.warn(`[dashboard-chat] ${label}: ${detail}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadLessonSearcher() {
|
|
59
|
+
try {
|
|
60
|
+
return require(path.join(__dirname, 'lesson-search')).searchLessons;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
debugChatFallback('lesson search unavailable', err);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function lessonToContextItem(lesson) {
|
|
68
|
+
return {
|
|
69
|
+
id: lesson.id,
|
|
70
|
+
signal: lesson.signal || lesson.feedback || '',
|
|
71
|
+
title: (lesson.title || '').replace(/^(?:MISTAKE|SUCCESS):\s*/i, '').slice(0, 160),
|
|
72
|
+
content: String(lesson.content || lesson.context || '').replace(/\s+/g, ' ').trim().slice(0, 600),
|
|
73
|
+
tags: lesson.tags || [],
|
|
74
|
+
source: 'lessons',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function vectorMatchToContextItem(match, index) {
|
|
79
|
+
return {
|
|
80
|
+
id: match.id || `vec-${index}`,
|
|
81
|
+
signal: match.signal || '',
|
|
82
|
+
title: String(match.context || match.text || '').slice(0, 100),
|
|
83
|
+
content: match.text || match.context || '',
|
|
84
|
+
tags: match.tags ? String(match.tags).split(',').filter(Boolean) : [],
|
|
85
|
+
source: 'lancedb-vector',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function dedupeContextItems(items, limit = MAX_CONTEXT_LESSONS + 3) {
|
|
90
|
+
const seen = new Set();
|
|
91
|
+
return items.filter((item) => {
|
|
92
|
+
if (!(item.content || item.title)) return false;
|
|
93
|
+
const key = item.id || item.content.slice(0, 80);
|
|
94
|
+
if (seen.has(key)) return false;
|
|
95
|
+
seen.add(key);
|
|
96
|
+
return true;
|
|
97
|
+
}).slice(0, limit);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function retrieveLessonContext(question, opts = {}) {
|
|
101
|
+
const searchLessons = loadLessonSearcher();
|
|
102
|
+
if (!searchLessons) return [];
|
|
103
|
+
try {
|
|
104
|
+
const res = searchLessons(String(question || ''), {
|
|
105
|
+
limit: MAX_CONTEXT_LESSONS,
|
|
106
|
+
feedbackDir: opts.feedbackDir,
|
|
107
|
+
});
|
|
108
|
+
const rows = res?.results || res?.lessons || [];
|
|
109
|
+
return rows.slice(0, MAX_CONTEXT_LESSONS).map(lessonToContextItem);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
debugChatFallback('lesson retrieval failed', err);
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function retrieveVectorContext(question, opts = {}) {
|
|
117
|
+
if (opts.useVectorSearch === false) return [];
|
|
118
|
+
try {
|
|
119
|
+
const vectorStore = require(path.join(__dirname, 'vector-store'));
|
|
120
|
+
const vecResults = vectorStore.searchSimilar
|
|
121
|
+
? await vectorStore.searchSimilar(String(question || ''), opts.vectorLimit || 4)
|
|
122
|
+
: [];
|
|
123
|
+
return vecResults
|
|
124
|
+
.filter((match) => match?.text)
|
|
125
|
+
.map(vectorMatchToContextItem);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
debugChatFallback('vector retrieval failed', err);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Live numeric snapshot from gate-stats + feedback analyzer. Always injected
|
|
133
|
+
// (~120 tokens) so the LLM can answer count/quantity questions like
|
|
134
|
+
// "how many were blocked today" without hallucinating.
|
|
135
|
+
function retrieveMetricsContext() {
|
|
136
|
+
const snapshot = {};
|
|
137
|
+
try {
|
|
138
|
+
const gs = require(path.join(__dirname, 'gate-stats'));
|
|
139
|
+
const s = gs.calculateStats();
|
|
140
|
+
snapshot.gates = {
|
|
141
|
+
total: s.totalGates,
|
|
142
|
+
blockRules: s.blockGates,
|
|
143
|
+
warnRules: s.warnGates,
|
|
144
|
+
totalBlockedEvents: s.totalBlocked,
|
|
145
|
+
totalWarnedEvents: s.totalWarned,
|
|
146
|
+
estimatedHoursSaved: s.estimatedHoursSaved,
|
|
147
|
+
topBlockedTrigger: s.topBlocked?.trigger || null,
|
|
148
|
+
topBlockedOccurrences: s.topBlocked?.occurrences || 0,
|
|
149
|
+
};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
debugChatFallback('gate-stats unavailable', err);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const fl = require(path.join(__dirname, 'feedback-loop'));
|
|
155
|
+
const a = fl.analyzeFeedback();
|
|
156
|
+
snapshot.feedback = {
|
|
157
|
+
total: a.total,
|
|
158
|
+
positive: a.totalPositive,
|
|
159
|
+
negative: a.totalNegative,
|
|
160
|
+
approvalRate: a.approvalRate,
|
|
161
|
+
last7d: a.windows?.['7d'],
|
|
162
|
+
last30d: a.windows?.['30d'],
|
|
163
|
+
trend: a.trend,
|
|
164
|
+
};
|
|
165
|
+
} catch (err) {
|
|
166
|
+
debugChatFallback('feedback-loop analyze unavailable', err);
|
|
167
|
+
}
|
|
168
|
+
return snapshot;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Retrieve relevant stored lessons and optional raw feedback vector matches.
|
|
172
|
+
async function retrieveContext(question, opts = {}) {
|
|
173
|
+
const lessons = retrieveLessonContext(question, opts);
|
|
174
|
+
const vectors = await retrieveVectorContext(question, opts);
|
|
175
|
+
return dedupeContextItems([...lessons, ...vectors]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Build a grounded RAG prompt. Pure function (testable).
|
|
179
|
+
function buildChatPrompt(question, lessons, metrics) {
|
|
180
|
+
const q = String(question || '').slice(0, MAX_QUESTION_CHARS).trim();
|
|
181
|
+
const context = (lessons || []).map((l, i) => {
|
|
182
|
+
const mark = /pos|up/i.test(l.signal) ? 'WORKED' : (/neg|down/i.test(l.signal) ? 'MISTAKE' : 'NOTE');
|
|
183
|
+
const tags = (l.tags || []).length ? ` [tags: ${l.tags.join(', ')}]` : '';
|
|
184
|
+
return `(${i + 1}) [${mark}] ${l.title || ''}${tags}\n ${l.content}`;
|
|
185
|
+
}).join('\n');
|
|
186
|
+
|
|
187
|
+
const metricsBlock = metrics && Object.keys(metrics).length
|
|
188
|
+
? `\n=== Live numeric snapshot (your data, current) ===\n${JSON.stringify(metrics, null, 2)}\n`
|
|
189
|
+
: '';
|
|
190
|
+
|
|
191
|
+
const system = [
|
|
192
|
+
'You are ThumbGate\'s "chat with your data" assistant. Answer the user\'s question',
|
|
193
|
+
'using ONLY the captured lessons and the live numeric snapshot below (this team\'s real data).',
|
|
194
|
+
'For count/quantity questions ("how many X", "what\'s our rate"), use the numeric snapshot.',
|
|
195
|
+
'For pattern/why questions, cite the lesson numbers like [1], [3].',
|
|
196
|
+
'If neither source contains the answer, say so plainly — do not invent facts.',
|
|
197
|
+
].join(' ');
|
|
198
|
+
|
|
199
|
+
return `${system}\n\n=== Captured lessons (your data) ===\n${context || '(no relevant lessons found)'}\n${metricsBlock}\n=== Question ===\n${q}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Parse the Gemini generateContent response into plain text. Pure (testable).
|
|
203
|
+
function parseGeminiAnswer(body) {
|
|
204
|
+
const parts = body?.candidates?.[0]?.content?.parts;
|
|
205
|
+
if (!Array.isArray(parts)) return '';
|
|
206
|
+
return parts.map((p) => (p && typeof p.text === 'string' ? p.text : '')).join('').trim();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function buildOpenAiChatPayload(prompt, model) {
|
|
210
|
+
return JSON.stringify({
|
|
211
|
+
model,
|
|
212
|
+
messages: [{ role: 'user', content: prompt }],
|
|
213
|
+
temperature: 0.2,
|
|
214
|
+
max_tokens: 1024,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function parseOpenAiChatAnswer(json) {
|
|
219
|
+
return json?.choices?.[0]?.message?.content || '';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseModelError(json, status) {
|
|
223
|
+
return json?.error?.message ? String(json.error.message).split('\n')[0] : `HTTP ${status}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function trimTrailingSlashes(value) {
|
|
227
|
+
let text = String(value || '');
|
|
228
|
+
while (text.endsWith('/')) {
|
|
229
|
+
text = text.slice(0, -1);
|
|
230
|
+
}
|
|
231
|
+
return text;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function callLocalOpenAiEndpoint({ endpoint, apiKey, model, prompt, fetchImpl, sources }) {
|
|
235
|
+
const url = endpoint.includes('/chat/completions')
|
|
236
|
+
? endpoint
|
|
237
|
+
: `${trimTrailingSlashes(endpoint)}/chat/completions`;
|
|
238
|
+
const res = await fetchImpl(url, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: {
|
|
241
|
+
'content-type': 'application/json',
|
|
242
|
+
'Authorization': `Bearer ${apiKey || 'local'}`
|
|
243
|
+
},
|
|
244
|
+
body: buildOpenAiChatPayload(prompt, model),
|
|
245
|
+
});
|
|
246
|
+
const json = await res.json().catch(() => ({}));
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
return { ok: false, error: 'local_llm_error', status: res.status, message: parseModelError(json, res.status), sources };
|
|
249
|
+
}
|
|
250
|
+
const answer = parseOpenAiChatAnswer(json);
|
|
251
|
+
return { ok: true, answer: answer.trim() || '(no answer returned)', sources, model: json.model || model };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function callPerplexityEndpoint({ apiKey, prompt, fetchImpl, sources }) {
|
|
255
|
+
const res = await fetchImpl(PERPLEXITY_ENDPOINT, {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
headers: { 'content-type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
|
|
258
|
+
body: buildOpenAiChatPayload(prompt, 'sonar'),
|
|
259
|
+
});
|
|
260
|
+
const json = await res.json().catch(() => ({}));
|
|
261
|
+
if (!res.ok) {
|
|
262
|
+
return { ok: false, error: 'perplexity_error', status: res.status, message: parseModelError(json, res.status), sources };
|
|
263
|
+
}
|
|
264
|
+
const answer = parseOpenAiChatAnswer(json);
|
|
265
|
+
return { ok: true, answer: answer.trim() || '(no answer returned)', sources, model: json.model || 'perplexity-hybrid' };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function callGeminiEndpoint({ apiKey, model, prompt, fetchImpl, sources }) {
|
|
269
|
+
const res = await fetchImpl(`${GEMINI_ENDPOINT}/${encodeURIComponent(model)}:generateContent`, {
|
|
270
|
+
method: 'POST',
|
|
271
|
+
headers: { 'content-type': 'application/json', 'x-goog-api-key': apiKey },
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
contents: [{ role: 'user', parts: [{ text: prompt }] }],
|
|
274
|
+
generationConfig: { temperature: 0.2, maxOutputTokens: 1024 },
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
const json = await res.json().catch(() => ({}));
|
|
278
|
+
if (!res.ok) {
|
|
279
|
+
return { ok: false, error: 'gemini_error', status: res.status, message: parseModelError(json, res.status), sources };
|
|
280
|
+
}
|
|
281
|
+
const answer = parseGeminiAnswer(json);
|
|
282
|
+
return { ok: true, answer: answer || '(no answer returned)', sources, model: json.modelVersion || model };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Answer a question grounded in this install's lessons. Returns
|
|
286
|
+
// { ok, answer, sources, model } or { ok:false, error, ... }.
|
|
287
|
+
async function answerDataQuestion(question, opts = {}) {
|
|
288
|
+
const q = String(question || '').trim();
|
|
289
|
+
if (!q) return { ok: false, error: 'empty_question', message: 'Ask a question about your data.' };
|
|
290
|
+
if (q.length > MAX_QUESTION_CHARS) {
|
|
291
|
+
return { ok: false, error: 'question_too_long', message: `Question exceeds ${MAX_QUESTION_CHARS} characters.` };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const localEndpoint = opts.localEndpoint || process.env.THUMBGATE_LOCAL_LLM_ENDPOINT || '';
|
|
295
|
+
const localModel = opts.localModel || process.env.THUMBGATE_LOCAL_LLM_MODEL || 'llama3';
|
|
296
|
+
const apiKey = resolveApiKey(opts);
|
|
297
|
+
const lessons = await retrieveContext(q, opts);
|
|
298
|
+
const sources = lessons.map((l) => ({ id: l.id, title: l.title, signal: l.signal }));
|
|
299
|
+
|
|
300
|
+
if (!apiKey && !localEndpoint) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
error: 'no_api_key',
|
|
304
|
+
message: 'Chat is not configured. Set a valid GEMINI_API_KEY, PERPLEXITY_API_KEY, or THUMBGATE_LOCAL_LLM_ENDPOINT in the project .env.',
|
|
305
|
+
sources,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const model = resolveModel(opts.model);
|
|
310
|
+
const metrics = retrieveMetricsContext();
|
|
311
|
+
const prompt = buildChatPrompt(q, lessons, metrics);
|
|
312
|
+
const fetchImpl = opts.fetch || globalThis.fetch;
|
|
313
|
+
const isPerplexity = apiKey && (apiKey.startsWith('pplx-') || apiKey.includes('perplexity'));
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
if (localEndpoint) return await callLocalOpenAiEndpoint({ endpoint: localEndpoint, apiKey, model: localModel, prompt, fetchImpl, sources });
|
|
317
|
+
if (isPerplexity) return await callPerplexityEndpoint({ apiKey, prompt, fetchImpl, sources });
|
|
318
|
+
return await callGeminiEndpoint({ apiKey, model, prompt, fetchImpl, sources });
|
|
319
|
+
} catch (err) {
|
|
320
|
+
const safeMessage = (err && err.message) ? String(err.message).split('\n')[0].slice(0, 100) : 'An unexpected error occurred.';
|
|
321
|
+
return { ok: false, error: 'network', message: safeMessage, sources };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
answerDataQuestion,
|
|
327
|
+
buildChatPrompt,
|
|
328
|
+
parseGeminiAnswer,
|
|
329
|
+
retrieveContext,
|
|
330
|
+
DEFAULT_MODEL,
|
|
331
|
+
MAX_QUESTION_CHARS,
|
|
332
|
+
};
|
package/scripts/dashboard.js
CHANGED
|
@@ -17,6 +17,7 @@ const { filterEntriesForWindow, resolveAnalyticsWindow } = require('./analytics-
|
|
|
17
17
|
const { resolveHostedBillingConfig } = require('./hosted-config');
|
|
18
18
|
const { generateAgentReadinessReport } = require('./agent-readiness');
|
|
19
19
|
const { summarizeGateTemplates } = require('./gate-templates');
|
|
20
|
+
const { mergeRepeatMetricIntoGateStats } = require('./repeat-metric');
|
|
20
21
|
const { buildPredictiveInsights } = loadOptionalModule('./predictive-insights', () => ({
|
|
21
22
|
buildPredictiveInsights: () => ({
|
|
22
23
|
upgradePropensity: {
|
|
@@ -48,6 +49,10 @@ const {
|
|
|
48
49
|
readDecisionLog,
|
|
49
50
|
} = require('./decision-journal');
|
|
50
51
|
const { analyzeFeedback } = require('./feedback-loop');
|
|
52
|
+
const {
|
|
53
|
+
collectAggregateLogEntries,
|
|
54
|
+
shouldAggregateFeedback,
|
|
55
|
+
} = require('./feedback-aggregate');
|
|
51
56
|
|
|
52
57
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
53
58
|
const DEFAULT_GATES_PATH = path.join(PROJECT_ROOT, 'config', 'gates', 'default.json');
|
|
@@ -967,6 +972,20 @@ function computeAnalyticsSummary(feedbackDir, options = {}) {
|
|
|
967
972
|
return {
|
|
968
973
|
window: telemetry.window || analyticsWindow,
|
|
969
974
|
telemetry,
|
|
975
|
+
firstPartyTrafficQuality: telemetry.trafficQuality || {
|
|
976
|
+
rawEvents: telemetry.totalEvents || 0,
|
|
977
|
+
externalEvents: 0,
|
|
978
|
+
excludedEvents: 0,
|
|
979
|
+
exclusionRate: 0,
|
|
980
|
+
byAudience: {},
|
|
981
|
+
byExclusionReason: {},
|
|
982
|
+
external: {
|
|
983
|
+
uniqueVisitors: 0,
|
|
984
|
+
pageViews: 0,
|
|
985
|
+
checkoutStarts: 0,
|
|
986
|
+
},
|
|
987
|
+
verdict: 'missing',
|
|
988
|
+
},
|
|
970
989
|
funnel: {
|
|
971
990
|
visitors: uniqueVisitors,
|
|
972
991
|
sessions: telemetry.visitors ? telemetry.visitors.uniqueSessions || 0 : 0,
|
|
@@ -1149,16 +1168,42 @@ function computeInstrumentationReadiness(analytics, billing) {
|
|
|
1149
1168
|
const coverage = billing && billing.coverage ? billing.coverage : {};
|
|
1150
1169
|
const telemetry = analytics.telemetry || {};
|
|
1151
1170
|
const visitors = telemetry.visitors || {};
|
|
1171
|
+
const quality = telemetry.trafficQuality || analytics.firstPartyTrafficQuality || {};
|
|
1172
|
+
const external = quality.external || {};
|
|
1152
1173
|
const cli = telemetry.cli || {};
|
|
1174
|
+
const plausibleExportConfigured = Boolean(
|
|
1175
|
+
process.env.PLAUSIBLE_API_KEY && (process.env.PLAUSIBLE_SITE_ID || process.env.PLAUSIBLE_DOMAIN)
|
|
1176
|
+
);
|
|
1177
|
+
const posthogExportConfigured = Boolean(
|
|
1178
|
+
(process.env.POSTHOG_PERSONAL_API_KEY || process.env.POSTHOG_API_KEY) && process.env.POSTHOG_PROJECT_ID
|
|
1179
|
+
);
|
|
1180
|
+
const ga4ExportConfigured = Boolean(
|
|
1181
|
+
process.env.GA4_PROPERTY_ID && (process.env.GOOGLE_APPLICATION_CREDENTIALS || process.env.GOOGLE_CLIENT_EMAIL)
|
|
1182
|
+
);
|
|
1183
|
+
const dashboardGradeExportReady = plausibleExportConfigured || posthogExportConfigured || ga4ExportConfigured;
|
|
1153
1184
|
|
|
1154
1185
|
return {
|
|
1155
|
-
plausibleConfigured: /plausible\.io\/js\/script
|
|
1186
|
+
plausibleConfigured: /plausible\.io\/js\/script(?:\.tagged-events)?\.js|\/js\/analytics\.js/.test(landingPage),
|
|
1156
1187
|
ga4Configured: Boolean(runtimeConfig.gaMeasurementId),
|
|
1157
1188
|
googleSearchConsoleConfigured: Boolean(runtimeConfig.googleSiteVerification),
|
|
1158
1189
|
softwareApplicationSchemaPresent: /"@type": "SoftwareApplication"/.test(landingPage),
|
|
1159
1190
|
faqSchemaPresent: /"@type": "FAQPage"/.test(landingPage),
|
|
1160
1191
|
telemetryEventsPresent: (telemetry.totalEvents || 0) > 0,
|
|
1161
1192
|
uniqueVisitorsTracked: visitors.uniqueVisitors || 0,
|
|
1193
|
+
rawTelemetryEvents: quality.rawEvents || telemetry.totalEvents || 0,
|
|
1194
|
+
externalTelemetryEvents: quality.externalEvents || 0,
|
|
1195
|
+
excludedTelemetryEvents: quality.excludedEvents || 0,
|
|
1196
|
+
externalVisitorsTracked: external.uniqueVisitors || 0,
|
|
1197
|
+
externalPageViewsTracked: external.pageViews || 0,
|
|
1198
|
+
externalCheckoutStartsTracked: external.checkoutStarts || 0,
|
|
1199
|
+
externalVisitorPathsTracked: Array.isArray(external.visitorPaths) ? external.visitorPaths.length : 0,
|
|
1200
|
+
internalTestPollutionRate: quality.exclusionRate || 0,
|
|
1201
|
+
trafficQualityVerdict: quality.verdict || 'missing',
|
|
1202
|
+
topExcludedTrafficReason: quality.topExclusionReason || null,
|
|
1203
|
+
plausibleExportConfigured,
|
|
1204
|
+
posthogExportConfigured,
|
|
1205
|
+
ga4ExportConfigured,
|
|
1206
|
+
dashboardGradeExportReady,
|
|
1162
1207
|
cliInstallsTracked: cli.uniqueInstalls || 0,
|
|
1163
1208
|
funnelEventsPresent: (analytics.reconciliation.telemetryCheckoutStarts || 0) > 0,
|
|
1164
1209
|
seoSignalsPresent: (analytics.seo.landingViews || 0) > 0,
|
|
@@ -1504,6 +1549,10 @@ function resolveTeamWindowHours(analyticsWindow) {
|
|
|
1504
1549
|
// ---------------------------------------------------------------------------
|
|
1505
1550
|
|
|
1506
1551
|
function collectAllFeedbackEntries(feedbackDir) {
|
|
1552
|
+
if (shouldAggregateFeedback()) {
|
|
1553
|
+
return collectAggregateLogEntries('feedback-log.jsonl', { feedbackDir }).entries;
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1507
1556
|
const entries = [];
|
|
1508
1557
|
const seen = new Set();
|
|
1509
1558
|
|
|
@@ -1573,7 +1622,11 @@ function generateDashboard(feedbackDir, options = {}) {
|
|
|
1573
1622
|
const billingSummary = options.billingSummary || getBillingSummary(analyticsWindow);
|
|
1574
1623
|
|
|
1575
1624
|
const approval = computeApprovalStats(entries);
|
|
1576
|
-
|
|
1625
|
+
// Surface the "repeat-attempts blocked before execution" metric on the
|
|
1626
|
+
// dashboard JSON and the /v1/dashboard HTTP route. Use the non-mutating
|
|
1627
|
+
// helper (mirrors server-stdio.js) instead of mutating computeGateStats()'s
|
|
1628
|
+
// return value. (mergeRepeatMetricIntoGateStats is imported at top of file.)
|
|
1629
|
+
const gateStats = mergeRepeatMetricIntoGateStats(computeGateStats());
|
|
1577
1630
|
const prevention = computePreventionImpact(feedbackDir, gateStats);
|
|
1578
1631
|
const trend = computeSessionTrend(entries, 10);
|
|
1579
1632
|
const health = computeSystemHealth(feedbackDir, gateStats);
|
|
@@ -1865,8 +1918,10 @@ function printDashboard(data) {
|
|
|
1865
1918
|
console.log('');
|
|
1866
1919
|
console.log('\uD83D\uDCBC Growth Analytics');
|
|
1867
1920
|
console.log(` Unique Visitors : ${analytics.trafficMetrics.visitors}`);
|
|
1921
|
+
console.log(` External Visitors: ${instrumentation.externalVisitorsTracked}`);
|
|
1868
1922
|
console.log(` Sessions : ${analytics.trafficMetrics.sessions}`);
|
|
1869
1923
|
console.log(` Page Views : ${analytics.trafficMetrics.pageViews}`);
|
|
1924
|
+
console.log(` External Views : ${instrumentation.externalPageViewsTracked}`);
|
|
1870
1925
|
console.log(` CTA Clicks : ${analytics.trafficMetrics.ctaClicks}`);
|
|
1871
1926
|
console.log(` Leads : ${analytics.funnel.acquisitionLeads}`);
|
|
1872
1927
|
console.log(` Sprint Leads : ${analytics.pipeline.workflowSprintLeads.total}`);
|
|
@@ -1877,6 +1932,7 @@ function printDashboard(data) {
|
|
|
1877
1932
|
console.log(` Booked Revenue : $${(analytics.revenue.bookedRevenueCents / 100).toFixed(2)}`);
|
|
1878
1933
|
console.log(` Matched Journeys : ${analytics.reconciliation.matchedPaidOrders}/${analytics.reconciliation.telemetryCheckoutStarts}`);
|
|
1879
1934
|
console.log(` Buyer Loss : ${analytics.buyerLoss.totalSignals}`);
|
|
1935
|
+
console.log(` Data Quality : ${analytics.firstPartyTrafficQuality.verdict} (${instrumentation.excludedTelemetryEvents}/${instrumentation.rawTelemetryEvents} excluded)`);
|
|
1880
1936
|
if (analytics.telemetry.visitors.topSource) {
|
|
1881
1937
|
console.log(` Top Source : ${analytics.telemetry.visitors.topSource.key} (${analytics.telemetry.visitors.topSource.count}\u00D7)`);
|
|
1882
1938
|
}
|
|
@@ -1896,6 +1952,15 @@ function printDashboard(data) {
|
|
|
1896
1952
|
console.log(` GA4 : ${instrumentation.ga4Configured ? 'configured' : 'missing'}`);
|
|
1897
1953
|
console.log(` Search Console : ${instrumentation.googleSearchConsoleConfigured ? 'configured' : 'missing'}`);
|
|
1898
1954
|
console.log(` Telemetry Events : ${instrumentation.telemetryEventsPresent ? instrumentation.uniqueVisitorsTracked : 0} visitors`);
|
|
1955
|
+
console.log(` Clean Visitors : ${instrumentation.externalVisitorsTracked} external (${Math.round((instrumentation.internalTestPollutionRate || 0) * 100)}% internal/test/bot events)`);
|
|
1956
|
+
console.log(` Clean Paths : ${instrumentation.externalVisitorPathsTracked} first-party paths`);
|
|
1957
|
+
if (instrumentation.topExcludedTrafficReason) {
|
|
1958
|
+
console.log(` Top Exclusion : ${instrumentation.topExcludedTrafficReason.key} (${instrumentation.topExcludedTrafficReason.count}\u00D7)`);
|
|
1959
|
+
}
|
|
1960
|
+
console.log(` Plausible Export : ${instrumentation.plausibleExportConfigured ? 'configured' : 'missing API credentials'}`);
|
|
1961
|
+
console.log(` PostHog Export : ${instrumentation.posthogExportConfigured ? 'configured' : 'missing API credentials'}`);
|
|
1962
|
+
console.log(` GA4 Export : ${instrumentation.ga4ExportConfigured ? 'configured' : 'missing API credentials'}`);
|
|
1963
|
+
console.log(` Visitor Paths : ${instrumentation.dashboardGradeExportReady ? 'export-ready' : 'not dashboard-grade in repo'}`);
|
|
1899
1964
|
console.log(` SEO Signals : ${instrumentation.seoSignalsPresent ? analytics.seo.landingViews : 0}`);
|
|
1900
1965
|
console.log(` Buyer Loss : ${instrumentation.buyerLossSignalsPresent ? analytics.buyerLoss.totalSignals : 0}`);
|
|
1901
1966
|
console.log(` Attribution : ${Math.round((instrumentation.trafficAttributionCoverage || 0) * 100)}% page-view coverage`);
|
|
@@ -2025,6 +2090,7 @@ module.exports = {
|
|
|
2025
2090
|
computeObservabilityStats,
|
|
2026
2091
|
readJSONL,
|
|
2027
2092
|
readJsonFile,
|
|
2093
|
+
collectAllFeedbackEntries,
|
|
2028
2094
|
};
|
|
2029
2095
|
|
|
2030
2096
|
if (require.main === module) {
|
|
@@ -6,6 +6,7 @@ const path = require('path');
|
|
|
6
6
|
|
|
7
7
|
const { getFeedbackPaths } = require('./feedback-loop');
|
|
8
8
|
const { ensureDir } = require('./fs-utils');
|
|
9
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
9
10
|
|
|
10
11
|
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
11
12
|
const DEFAULT_PROOF_DIR = process.env.THUMBGATE_PROOF_DIR
|
|
@@ -47,7 +48,10 @@ function readJSON(filePath) {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function writeJSONL(filePath, rows) {
|
|
50
|
-
|
|
51
|
+
// Redact secrets from every bundle row — this is the single choke point for all bundle tables
|
|
52
|
+
// (feedback_events, memory_records, sequences, attributions, proof_reports). A shared/published
|
|
53
|
+
// dataset must never ship a captured credential. See scripts/secret-redaction.js.
|
|
54
|
+
const content = rows.map((row) => JSON.stringify(redactSecretsDeep(row))).join('\n');
|
|
51
55
|
fs.writeFileSync(filePath, content ? `${content}\n` : '');
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -9,6 +9,7 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { traceForDpoPair, aggregateTraces } = require('./code-reasoning');
|
|
11
11
|
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
12
|
+
const { redactSecretsDeep } = require('./secret-redaction');
|
|
12
13
|
|
|
13
14
|
const DEFAULT_LOCAL_MEMORY_LOG = path.join(resolveFeedbackDir(), 'memory-log.jsonl');
|
|
14
15
|
|
|
@@ -201,14 +202,18 @@ function exportDpoFromMemories(memories) {
|
|
|
201
202
|
},
|
|
202
203
|
}));
|
|
203
204
|
|
|
205
|
+
// Redact secrets before the pairs leave this module — they are derived from memory content and
|
|
206
|
+
// are shipped to disk here AND consumed by export-hf-dataset.js. See scripts/secret-redaction.js.
|
|
207
|
+
const redactedPairs = pairsWithTraces.map((pair) => redactSecretsDeep(pair));
|
|
208
|
+
|
|
204
209
|
return {
|
|
205
|
-
pairs:
|
|
210
|
+
pairs: redactedPairs,
|
|
206
211
|
unpairedErrors: result.unpairedErrors,
|
|
207
212
|
unpairedLearnings: result.unpairedLearnings,
|
|
208
213
|
errors,
|
|
209
214
|
learnings,
|
|
210
215
|
reasoning,
|
|
211
|
-
jsonl: toJSONL(
|
|
216
|
+
jsonl: toJSONL(redactedPairs),
|
|
212
217
|
};
|
|
213
218
|
}
|
|
214
219
|
|