thumbgate 1.30.0 → 1.34.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.
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +54 -16
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +105 -10
- package/adapters/opencode/opencode.json +1 -1
- package/bench/observability-eval-suite.json +2 -2
- package/bin/cli.js +168 -31
- package/config/evals/generation-quality-golden.json +95 -0
- package/config/evals/rag-answer-quality-golden.json +91 -0
- package/config/evals/retrieval-hybrid-ablation.json +66 -0
- package/config/evals/retrieval-ranking-golden.json +522 -0
- package/config/gates/claim-verifiers.example.json +42 -0
- package/config/gates/claim-verifiers.json +25 -0
- package/config/gates/default.json +217 -50
- package/config/mcp-allowlists.json +233 -206
- package/config/model-tiers.json +7 -2
- package/glama.json +6 -0
- package/hooks/hooks.json +1 -1
- package/package.json +69 -12
- package/public/assets/diagrams/before-after.svg +17 -16
- package/public/assets/diagrams/hero-thumbs.svg +68 -0
- package/public/assets/diagrams/loop.svg +19 -13
- package/public/assets/diagrams/self-improving-thumbs-loop.svg +105 -0
- package/public/compare.html +1 -0
- package/public/dashboard.html +126 -28
- package/public/evaluations.html +1 -1
- package/public/index.html +142 -13
- package/public/numbers.html +3 -2
- package/public/pricing.html +143 -30
- package/scripts/a-plus-evidence-scorecard.js +303 -0
- package/scripts/agent-readiness.js +110 -0
- package/scripts/async-eval-observability.js +36 -11
- package/scripts/audit-trail.js +37 -1
- package/scripts/auto-promote-gates.js +149 -34
- package/scripts/auto-wire-hooks.js +20 -8
- package/scripts/cli-schema.js +14 -0
- package/scripts/colbert-style-maxsim.js +236 -0
- package/scripts/cross-encoder-reranker.js +356 -126
- package/scripts/dashboard-chat.js +350 -17
- package/scripts/document-intake.js +283 -7
- package/scripts/eval-quality-suite.js +204 -0
- package/scripts/feedback-loop.js +115 -7
- package/scripts/feedback-paths.js +32 -13
- package/scripts/feedback-quality.js +53 -0
- package/scripts/feedback-schema.js +3 -0
- package/scripts/file-ledger-lock.js +130 -0
- package/scripts/filesystem-search.js +17 -7
- package/scripts/financial-control-plane.js +1514 -0
- package/scripts/gates-engine.js +202 -7
- package/scripts/gemini-embedding-policy.js +1 -0
- package/scripts/harness-tool-names.js +70 -0
- package/scripts/hook-runtime.js +15 -3
- package/scripts/hook-stop-anti-claim.js +63 -3
- package/scripts/human-escalation.js +353 -41
- package/scripts/lesson-db.js +16 -5
- package/scripts/lesson-embedding-index.js +67 -20
- package/scripts/lesson-embedding-maintenance.js +177 -0
- package/scripts/lesson-reranker.js +55 -9
- package/scripts/lesson-retrieval.js +305 -29
- package/scripts/lesson-search.js +22 -8
- package/scripts/llm-client.js +304 -15
- package/scripts/model-tier-router.js +593 -0
- package/scripts/pragmatic-hybrid-search.js +379 -0
- package/scripts/provider-action-normalizer.js +11 -4
- package/scripts/rag-document-pipeline.js +461 -0
- package/scripts/rag-structured-output.js +441 -0
- package/scripts/ragas-style-metrics.js +351 -0
- package/scripts/request-envelope.js +178 -0
- package/scripts/rerank-pipeline.js +370 -0
- package/scripts/rerank-quality-eval.js +155 -0
- package/scripts/retrieval-hybrid-ablation.js +120 -0
- package/scripts/retrieval-quality-tier.js +118 -0
- package/scripts/secret-scanner.js +395 -4
- package/scripts/self-distill-agent.js +7 -1
- package/scripts/self-healing-check.js +25 -0
- package/scripts/skill-packs.js +183 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/statusline-links.js +1 -1
- package/scripts/statusline.sh +8 -1
- package/scripts/telemetry-analytics.js +13 -1
- package/scripts/thumbgate-search.js +98 -6
- package/scripts/tier-budget-guard.js +186 -0
- package/scripts/tool-registry.js +141 -5
- package/scripts/universal-claim-evaluator.js +767 -0
- package/scripts/vector-store.js +154 -17
- package/scripts/verify-marketing-pages-deployed.js +85 -3
- package/scripts/workflow-sentinel.js +77 -11
- package/server.json +44 -0
- package/smithery.yaml +17 -0
- package/src/api/server.js +196 -13
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const {
|
|
7
|
+
semanticRank,
|
|
8
|
+
isEmbedderAvailable,
|
|
9
|
+
lessonText,
|
|
10
|
+
hashText,
|
|
11
|
+
readCache,
|
|
12
|
+
} = require('./lesson-embedding-index');
|
|
13
|
+
const {
|
|
14
|
+
selectRetrievalMemories,
|
|
15
|
+
MAX_RETRIEVAL_MEMORY_LINES,
|
|
16
|
+
} = require('./lesson-retrieval');
|
|
17
|
+
|
|
18
|
+
function loadLessonCorpus(feedbackDir) {
|
|
19
|
+
const { readJSONL, getFeedbackPaths } = require('./feedback-loop');
|
|
20
|
+
const memoryPath = feedbackDir
|
|
21
|
+
? path.join(feedbackDir, 'memory-log.jsonl')
|
|
22
|
+
: getFeedbackPaths().MEMORY_LOG_PATH;
|
|
23
|
+
const lessons = selectRetrievalMemories(
|
|
24
|
+
readJSONL(memoryPath, { maxLines: MAX_RETRIEVAL_MEMORY_LINES }),
|
|
25
|
+
);
|
|
26
|
+
return { lessons, memoryPath };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function evaluateEmbeddingIndexDrift(options = {}) {
|
|
30
|
+
const feedbackDir = options.feedbackDir || process.env.THUMBGATE_FEEDBACK_DIR;
|
|
31
|
+
const { lessons, memoryPath } = loadLessonCorpus(feedbackDir);
|
|
32
|
+
const cachePath = path.join(
|
|
33
|
+
feedbackDir || path.dirname(memoryPath),
|
|
34
|
+
options.cacheFile || 'lesson-embeddings.json',
|
|
35
|
+
);
|
|
36
|
+
const cache = readCache(cachePath);
|
|
37
|
+
let indexedCount = 0;
|
|
38
|
+
const missingIds = [];
|
|
39
|
+
const staleIds = [];
|
|
40
|
+
const providers = {};
|
|
41
|
+
const dimensions = {};
|
|
42
|
+
const corpusIds = new Set(lessons.map((lesson) => lesson.id));
|
|
43
|
+
|
|
44
|
+
for (const lesson of lessons) {
|
|
45
|
+
const entry = cache[lesson.id];
|
|
46
|
+
const valid = entry
|
|
47
|
+
&& entry.hash === hashText(lessonText(lesson))
|
|
48
|
+
&& Array.isArray(entry.vector)
|
|
49
|
+
&& entry.vector.length > 0
|
|
50
|
+
&& entry.dimension === entry.vector.length
|
|
51
|
+
&& typeof entry.provider === 'string';
|
|
52
|
+
if (valid) {
|
|
53
|
+
indexedCount += 1;
|
|
54
|
+
providers[entry.provider] = (providers[entry.provider] || 0) + 1;
|
|
55
|
+
dimensions[entry.dimension] = (dimensions[entry.dimension] || 0) + 1;
|
|
56
|
+
} else if (entry) {
|
|
57
|
+
staleIds.push(lesson.id);
|
|
58
|
+
} else {
|
|
59
|
+
missingIds.push(lesson.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const orphanedIds = Object.keys(cache)
|
|
64
|
+
.filter((id) => !corpusIds.has(id))
|
|
65
|
+
.sort((left, right) => left.localeCompare(right));
|
|
66
|
+
missingIds.sort((left, right) => left.localeCompare(right));
|
|
67
|
+
staleIds.sort((left, right) => left.localeCompare(right));
|
|
68
|
+
|
|
69
|
+
const corpusCount = lessons.length;
|
|
70
|
+
const coverage = corpusCount ? indexedCount / corpusCount : 1;
|
|
71
|
+
const semanticProviderAvailable = options.embedder
|
|
72
|
+
? true
|
|
73
|
+
: isEmbedderAvailable();
|
|
74
|
+
const minCoverage = Math.max(0, Math.min(1, Number(options.minCoverage) || 0.95));
|
|
75
|
+
const requireExact = options.requireExact !== false;
|
|
76
|
+
const enabled = semanticProviderAvailable || Object.keys(cache).length > 0;
|
|
77
|
+
const exactIdSet = missingIds.length === 0
|
|
78
|
+
&& staleIds.length === 0
|
|
79
|
+
&& orphanedIds.length === 0;
|
|
80
|
+
const status = !enabled
|
|
81
|
+
? 'not_configured'
|
|
82
|
+
: coverage >= minCoverage
|
|
83
|
+
&& staleIds.length === 0
|
|
84
|
+
&& orphanedIds.length === 0
|
|
85
|
+
&& (!requireExact || exactIdSet)
|
|
86
|
+
? 'healthy'
|
|
87
|
+
: 'unhealthy';
|
|
88
|
+
return {
|
|
89
|
+
ok: status !== 'unhealthy',
|
|
90
|
+
status,
|
|
91
|
+
semanticProviderAvailable,
|
|
92
|
+
corpusCount,
|
|
93
|
+
indexedCount,
|
|
94
|
+
missingCount: missingIds.length,
|
|
95
|
+
missingIds,
|
|
96
|
+
staleCount: staleIds.length,
|
|
97
|
+
staleIds,
|
|
98
|
+
orphanedCount: orphanedIds.length,
|
|
99
|
+
orphanedIds,
|
|
100
|
+
exactIdSet,
|
|
101
|
+
requireExact,
|
|
102
|
+
coverage: Number(coverage.toFixed(4)),
|
|
103
|
+
minCoverage,
|
|
104
|
+
providers,
|
|
105
|
+
dimensions,
|
|
106
|
+
memoryPath,
|
|
107
|
+
cachePath,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function backfillLessonEmbeddings(options = {}) {
|
|
112
|
+
const feedbackDir = options.feedbackDir || process.env.THUMBGATE_FEEDBACK_DIR;
|
|
113
|
+
if (!options.embedder && !isEmbedderAvailable()) {
|
|
114
|
+
const error = new Error(
|
|
115
|
+
'No semantic embedding provider configured. Set THUMBGATE_OLLAMA_EMBED_MODEL or configure another real provider.',
|
|
116
|
+
);
|
|
117
|
+
error.code = 'THUMBGATE_SEMANTIC_PROVIDER_UNAVAILABLE';
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const { lessons } = loadLessonCorpus(feedbackDir);
|
|
121
|
+
if (lessons.length > 0) {
|
|
122
|
+
await semanticRank('ThumbGate lesson embedding backfill health query', lessons, {
|
|
123
|
+
feedbackDir,
|
|
124
|
+
embedder: options.embedder,
|
|
125
|
+
embedderId: options.embedderId,
|
|
126
|
+
cacheFile: options.cacheFile,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return evaluateEmbeddingIndexDrift({
|
|
130
|
+
...options,
|
|
131
|
+
feedbackDir,
|
|
132
|
+
embedder: options.embedder,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseArgs(argv) {
|
|
137
|
+
const args = { json: false, backfill: false, requireSemantic: false };
|
|
138
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
139
|
+
const arg = argv[index];
|
|
140
|
+
if (arg === '--json') args.json = true;
|
|
141
|
+
else if (arg === '--backfill') args.backfill = true;
|
|
142
|
+
else if (arg === '--require-semantic') args.requireSemantic = true;
|
|
143
|
+
else if (arg === '--feedback-dir') args.feedbackDir = argv[++index];
|
|
144
|
+
else if (arg === '--min-coverage') args.minCoverage = Number(argv[++index]);
|
|
145
|
+
}
|
|
146
|
+
return args;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function runCli() {
|
|
150
|
+
const options = parseArgs(process.argv.slice(2));
|
|
151
|
+
let report;
|
|
152
|
+
try {
|
|
153
|
+
report = options.backfill
|
|
154
|
+
? await backfillLessonEmbeddings(options)
|
|
155
|
+
: evaluateEmbeddingIndexDrift(options);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
report = { ok: false, status: 'error', error: error.message, code: error.code || 'ERROR' };
|
|
158
|
+
}
|
|
159
|
+
const output = options.json
|
|
160
|
+
? JSON.stringify(report, null, 2)
|
|
161
|
+
: `Lesson embeddings: ${report.status}; ${report.indexedCount || 0}/${report.corpusCount || 0} indexed; coverage=${report.coverage || 0}`;
|
|
162
|
+
process.stdout.write(`${output}\n`);
|
|
163
|
+
if (!report.ok || (options.requireSemantic && !report.semanticProviderAvailable)) {
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!module.parent) {
|
|
169
|
+
runCli();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
loadLessonCorpus,
|
|
174
|
+
evaluateEmbeddingIndexDrift,
|
|
175
|
+
backfillLessonEmbeddings,
|
|
176
|
+
parseArgs,
|
|
177
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Field-aware BM25F reranker for lesson retrieval.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* This is a deterministic lexical second stage, not a neural cross-encoder.
|
|
7
|
+
* It scores query terms against lesson fields jointly and catches relevance
|
|
8
|
+
* signals that the first-stage overlap score can miss:
|
|
9
9
|
*
|
|
10
10
|
* - Field-weighted BM25: a query term in `whatWentWrong` is worth more
|
|
11
11
|
* than the same term in `tags`
|
|
@@ -55,6 +55,11 @@ const SYNONYM_GROUPS = [
|
|
|
55
55
|
['auth', 'authentication', 'authorization', 'token', 'api key', 'credential'],
|
|
56
56
|
['delete', 'remove', 'rm', 'drop', 'destroy', 'wipe'],
|
|
57
57
|
['merge', 'pull request', 'pr', 'rebase', 'squash'],
|
|
58
|
+
['tablet', 'ipad', 'ios device'],
|
|
59
|
+
['overlay', 'tailscale', 'mesh network', 'vpn'],
|
|
60
|
+
['laptop', 'macbook', 'mac'],
|
|
61
|
+
['phone', 'mobile', 'android', 'device'],
|
|
62
|
+
['conversation', 'session', 'thread', 'chat'],
|
|
58
63
|
];
|
|
59
64
|
|
|
60
65
|
// Regex patterns that indicate the query is about a failure/mistake.
|
|
@@ -83,7 +88,13 @@ function expandTerms(terms) {
|
|
|
83
88
|
const expanded = new Set(terms);
|
|
84
89
|
for (const term of terms) {
|
|
85
90
|
for (const group of SYNONYM_GROUPS) {
|
|
86
|
-
if (group.some((syn) => syn
|
|
91
|
+
if (group.some((syn) => tokenize(syn).some((word) => (
|
|
92
|
+
word === term
|
|
93
|
+
|| (word.length >= 4 && term.length >= 4 && (
|
|
94
|
+
word.startsWith(term)
|
|
95
|
+
|| term.startsWith(word)
|
|
96
|
+
))
|
|
97
|
+
)))) {
|
|
87
98
|
group.forEach((syn) => tokenize(syn).forEach((t) => expanded.add(t)));
|
|
88
99
|
}
|
|
89
100
|
}
|
|
@@ -103,6 +114,28 @@ function getField(candidate, field) {
|
|
|
103
114
|
return String(val);
|
|
104
115
|
}
|
|
105
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Score exact entity-channel overlap separately from prose relevance.
|
|
119
|
+
*
|
|
120
|
+
* Tags, categories, and tool identities are low-cardinality metadata. Keeping
|
|
121
|
+
* this score visible prevents a tag-only hit from being swallowed by BM25
|
|
122
|
+
* field-length normalization and makes reranking traces explainable.
|
|
123
|
+
*/
|
|
124
|
+
function scoreEntityOverlap(queryTerms, candidate) {
|
|
125
|
+
const entityText = [
|
|
126
|
+
getField(candidate, 'tags'),
|
|
127
|
+
getField(candidate, 'category'),
|
|
128
|
+
getField(candidate, 'toolUsed'),
|
|
129
|
+
getField(candidate, 'toolName'),
|
|
130
|
+
...(candidate.metadata?.toolsUsed || []),
|
|
131
|
+
].filter(Boolean).join(' ');
|
|
132
|
+
const entityTokens = new Set(expandTerms(tokenize(entityText)));
|
|
133
|
+
const uniqueQueryTerms = [...new Set(queryTerms)];
|
|
134
|
+
if (uniqueQueryTerms.length === 0 || entityTokens.size === 0) return 0;
|
|
135
|
+
const hits = uniqueQueryTerms.filter((term) => entityTokens.has(term)).length;
|
|
136
|
+
return Number((hits / uniqueQueryTerms.length).toFixed(6));
|
|
137
|
+
}
|
|
138
|
+
|
|
106
139
|
/**
|
|
107
140
|
* Compute field-weighted BM25 scores for a list of candidates (BM25F variant).
|
|
108
141
|
*
|
|
@@ -189,7 +222,7 @@ function fieldWeightedBM25(queryTerms, candidates) {
|
|
|
189
222
|
}
|
|
190
223
|
|
|
191
224
|
/**
|
|
192
|
-
* Rerank a list of lesson candidates using
|
|
225
|
+
* Rerank a list of lesson candidates using field-aware BM25F.
|
|
193
226
|
*
|
|
194
227
|
* @param {string} query - The original retrieval query / action context
|
|
195
228
|
* @param {Array} candidates - Lesson objects from the bi-encoder stage
|
|
@@ -224,12 +257,15 @@ function rerankLessons(query, candidates, options = {}) {
|
|
|
224
257
|
|
|
225
258
|
const reranked = bm25Results.map(({ candidate, bm25Score }) => {
|
|
226
259
|
const normBm25 = bm25Score / maxBm25;
|
|
260
|
+
const entityScore = scoreEntityOverlap(queryTerms, candidate);
|
|
227
261
|
|
|
228
262
|
// Original bi-encoder score (field name differs between retrieval paths)
|
|
229
263
|
const origScore = candidate.relevanceScore ?? candidate.score ?? 0;
|
|
230
264
|
|
|
231
265
|
// Blend BM25 with original score
|
|
232
|
-
let finalScore =
|
|
266
|
+
let finalScore = 0.85 * (
|
|
267
|
+
blendWeight * normBm25 + (1 - blendWeight) * origScore
|
|
268
|
+
) + 0.15 * entityScore;
|
|
233
269
|
|
|
234
270
|
// Signal coherence bonus: failure queries → negative lessons rank higher
|
|
235
271
|
const candidateSignal =
|
|
@@ -252,7 +288,11 @@ function rerankLessons(query, candidates, options = {}) {
|
|
|
252
288
|
}
|
|
253
289
|
}
|
|
254
290
|
|
|
255
|
-
return {
|
|
291
|
+
return {
|
|
292
|
+
...candidate,
|
|
293
|
+
entityScore,
|
|
294
|
+
rerankedScore: Number(finalScore.toFixed(6)),
|
|
295
|
+
};
|
|
256
296
|
});
|
|
257
297
|
|
|
258
298
|
return reranked
|
|
@@ -260,4 +300,10 @@ function rerankLessons(query, candidates, options = {}) {
|
|
|
260
300
|
.slice(0, topK);
|
|
261
301
|
}
|
|
262
302
|
|
|
263
|
-
module.exports = {
|
|
303
|
+
module.exports = {
|
|
304
|
+
rerankLessons,
|
|
305
|
+
fieldWeightedBM25,
|
|
306
|
+
scoreEntityOverlap,
|
|
307
|
+
tokenize,
|
|
308
|
+
expandTerms,
|
|
309
|
+
};
|