thumbgate 1.27.19 → 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 +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/README.md +0 -3
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/gcp/dfcx-webhook-gate.js +295 -0
- package/adapters/letta/README.md +41 -0
- package/adapters/letta/thumbgate-letta-adapter.js +133 -0
- package/adapters/mcp/server-stdio.js +1 -1
- 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/bin/cli.js +27 -1
- package/bin/dashboard-cli.js +7 -0
- package/bin/postinstall.js +14 -23
- package/commands/dashboard.md +15 -0
- package/commands/thumbgate-dashboard.md +15 -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/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/diagnostic.html +345 -0
- package/public/index.html +2 -2
- package/public/install.html +193 -0
- package/public/js/buyer-intent.js +672 -0
- package/public/numbers.html +2 -2
- package/public/pricing.html +399 -0
- package/scripts/action-receipts.js +324 -0
- package/scripts/activation-quickstart.js +187 -0
- package/scripts/agent-operations-planner.js +621 -0
- package/scripts/ai-component-inventory.js +367 -0
- package/scripts/async-eval-observability.js +236 -0
- package/scripts/audit.js +65 -0
- package/scripts/aws-blocks-guardrails.js +272 -0
- package/scripts/classifier-routing.js +130 -0
- package/scripts/dashboard-chat.js +332 -0
- package/scripts/feedback-aggregate.js +281 -0
- package/scripts/feedback-sanitizer.js +105 -0
- package/scripts/hook-stop-anti-claim.js +301 -0
- package/scripts/install-shim.js +87 -0
- package/scripts/mcp-oauth.js +293 -0
- package/scripts/noop-detect.js +285 -0
- 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/qa-scenario-planner.js +136 -0
- package/scripts/repeat-metric.js +137 -0
- package/scripts/secret-fixture-tokens.js +61 -0
- package/scripts/secret-redaction.js +166 -0
- 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/silent-failure-cluster.js +531 -0
- package/scripts/statusline-cache-read.js +57 -0
- package/scripts/sync-telemetry-from-prod.js +374 -0
- package/scripts/tool-contract-validator.js +76 -0
- package/scripts/trajectory-scorer.js +63 -0
- package/scripts/verify-marketing-pages-deployed.js +212 -0
- package/scripts/visitor-journey.js +172 -0
- package/.claude-plugin/marketplace.json +0 -85
- package/adapters/chatgpt/openapi.yaml +0 -1707
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_MAX_FILES = 2500;
|
|
9
|
+
const DEFAULT_MAX_BYTES = 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
const IGNORE_DIRS = new Set([
|
|
12
|
+
'.git',
|
|
13
|
+
'.hg',
|
|
14
|
+
'.svn',
|
|
15
|
+
'.thumbgate',
|
|
16
|
+
'.venv',
|
|
17
|
+
'venv',
|
|
18
|
+
'__pycache__',
|
|
19
|
+
'node_modules',
|
|
20
|
+
'dist',
|
|
21
|
+
'build',
|
|
22
|
+
'coverage',
|
|
23
|
+
'.next',
|
|
24
|
+
'.turbo',
|
|
25
|
+
'.cache',
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const TEXT_EXTENSIONS = new Set([
|
|
29
|
+
'.js',
|
|
30
|
+
'.jsx',
|
|
31
|
+
'.ts',
|
|
32
|
+
'.tsx',
|
|
33
|
+
'.mjs',
|
|
34
|
+
'.cjs',
|
|
35
|
+
'.py',
|
|
36
|
+
'.ipynb',
|
|
37
|
+
'.go',
|
|
38
|
+
'.java',
|
|
39
|
+
'.rb',
|
|
40
|
+
'.rs',
|
|
41
|
+
'.php',
|
|
42
|
+
'.cs',
|
|
43
|
+
'.swift',
|
|
44
|
+
'.kt',
|
|
45
|
+
'.scala',
|
|
46
|
+
'.sh',
|
|
47
|
+
'.yaml',
|
|
48
|
+
'.yml',
|
|
49
|
+
'.toml',
|
|
50
|
+
'.json',
|
|
51
|
+
'.ini',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const MODEL_EXTENSIONS = new Map([
|
|
55
|
+
['.gguf', { name: 'GGUF model artifact', category: 'model_artifact', ecosystem: 'local-model' }],
|
|
56
|
+
['.safetensors', { name: 'SafeTensors model artifact', category: 'model_artifact', ecosystem: 'local-model' }],
|
|
57
|
+
['.onnx', { name: 'ONNX model artifact', category: 'model_artifact', ecosystem: 'onnx' }],
|
|
58
|
+
['.pt', { name: 'PyTorch model artifact', category: 'model_artifact', ecosystem: 'pytorch' }],
|
|
59
|
+
['.pth', { name: 'PyTorch model artifact', category: 'model_artifact', ecosystem: 'pytorch' }],
|
|
60
|
+
['.tflite', { name: 'TensorFlow Lite model artifact', category: 'model_artifact', ecosystem: 'tensorflow' }],
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const SOURCE_PATTERNS = [
|
|
64
|
+
component('openai', 'OpenAI SDK/API', 'provider_sdk', 'openai', /\b(from\s+openai\s+import|import\s+openai\b|require\(['"]openai['"]\)|from\s+['"]openai['"]|@openai\/agents|new\s+OpenAI\s*\()/i),
|
|
65
|
+
component('anthropic', 'Anthropic Claude SDK/API', 'provider_sdk', 'anthropic', /\b(@anthropic-ai\/sdk|from\s+anthropic\s+import|import\s+anthropic\b|require\(['"]@anthropic-ai\/sdk['"]\)|new\s+Anthropic\s*\()/i),
|
|
66
|
+
component('google-gemini', 'Google Gemini SDK/API', 'provider_sdk', 'google', /\b(@google\/generative-ai|google-genai|from\s+google\s+import\s+genai|GoogleGenerativeAI|GenerativeModel)/i),
|
|
67
|
+
component('vertex-ai', 'Google Vertex AI', 'ai_platform', 'google-cloud', /\b(vertexai|aiplatform|@google-cloud\/vertexai|PredictionServiceClient|projects\.locations\.publishers\.models)/i),
|
|
68
|
+
component('dialogflow-cx', 'Google Dialogflow CX', 'conversation_ai', 'google-cloud', /\b(dialogflowcx|dialogflow-cx|@google-cloud\/dialogflow-cx|SessionsClient|DetectIntentRequest)/i),
|
|
69
|
+
component('langchain', 'LangChain', 'agent_framework', 'langchain', /\b(@langchain\/|langchain\b|from\s+langchain(_community|_core)?\b)/i),
|
|
70
|
+
component('llamaindex', 'LlamaIndex', 'agent_framework', 'llamaindex', /\b(llama_index|llamaindex|from\s+llama_index\b)/i),
|
|
71
|
+
component('semantic-kernel', 'Semantic Kernel', 'agent_framework', 'microsoft', /\b(semantic-kernel|semantic_kernel|Microsoft\.SemanticKernel)/i),
|
|
72
|
+
component('crewai', 'CrewAI', 'agent_framework', 'crewai', /\b(crewai|from\s+crewai\b)/i),
|
|
73
|
+
component('autogen', 'AutoGen', 'agent_framework', 'microsoft', /\b(autogen|pyautogen|@microsoft\/autogen)/i),
|
|
74
|
+
component('transformers', 'Hugging Face Transformers', 'ml_framework', 'huggingface', /\b(transformers|AutoModel|AutoTokenizer|pipeline\s*\()/i),
|
|
75
|
+
component('sentence-transformers', 'Sentence Transformers', 'embedding_model', 'huggingface', /\b(sentence_transformers|SentenceTransformer)/i),
|
|
76
|
+
component('pytorch', 'PyTorch', 'ml_framework', 'pytorch', /\b(import\s+torch\b|from\s+torch\b|torch\.)/i),
|
|
77
|
+
component('tensorflow', 'TensorFlow/Keras', 'ml_framework', 'tensorflow', /\b(import\s+tensorflow\b|from\s+tensorflow\b|import\s+keras\b|from\s+keras\b|tf\.keras)/i),
|
|
78
|
+
component('scikit-learn', 'scikit-learn', 'ml_framework', 'scikit-learn', /\b(sklearn|scikit-learn|from\s+sklearn\b)/i),
|
|
79
|
+
component('onnxruntime', 'ONNX Runtime', 'ml_runtime', 'onnx', /\b(onnxruntime|InferenceSession)/i),
|
|
80
|
+
component('pinecone', 'Pinecone vector database', 'vector_database', 'pinecone', /\b(pinecone|@pinecone-database\/pinecone)/i),
|
|
81
|
+
component('weaviate', 'Weaviate vector database', 'vector_database', 'weaviate', /\b(weaviate|weaviate-client)/i),
|
|
82
|
+
component('qdrant', 'Qdrant vector database', 'vector_database', 'qdrant', /\b(qdrant|@qdrant\/js-client-rest|qdrant-client)/i),
|
|
83
|
+
component('chroma', 'Chroma vector database', 'vector_database', 'chroma', /\b(chromadb|chroma-client|ChromaClient)/i),
|
|
84
|
+
component('lancedb', 'LanceDB vector database', 'vector_database', 'lancedb', /\b(lancedb|@lancedb\/lancedb)/i),
|
|
85
|
+
component('faiss', 'FAISS vector index', 'vector_database', 'faiss', /\b(faiss|faiss-cpu|faiss-gpu)/i),
|
|
86
|
+
component('pgvector', 'Postgres pgvector', 'vector_database', 'postgres', /\b(pgvector|vector\(\d+\)|CREATE\s+EXTENSION\s+vector)/i),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const MANIFEST_FILES = new Set([
|
|
90
|
+
'package.json',
|
|
91
|
+
'requirements.txt',
|
|
92
|
+
'pyproject.toml',
|
|
93
|
+
'poetry.lock',
|
|
94
|
+
'Pipfile',
|
|
95
|
+
'Gemfile',
|
|
96
|
+
'go.mod',
|
|
97
|
+
'Cargo.toml',
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
function component(id, name, category, ecosystem, pattern) {
|
|
101
|
+
return { id, name, category, ecosystem, pattern };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function relativePath(rootDir, filePath) {
|
|
105
|
+
return path.relative(rootDir, filePath).split(path.sep).join('/');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function shouldIgnoreDir(name) {
|
|
109
|
+
return IGNORE_DIRS.has(name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isTextLike(filePath) {
|
|
113
|
+
const base = path.basename(filePath);
|
|
114
|
+
if (MANIFEST_FILES.has(base)) return true;
|
|
115
|
+
return TEXT_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function walkFiles(rootDir, options = {}) {
|
|
119
|
+
const maxFiles = Number(options.maxFiles || DEFAULT_MAX_FILES);
|
|
120
|
+
const files = [];
|
|
121
|
+
const queue = [rootDir];
|
|
122
|
+
|
|
123
|
+
while (queue.length && files.length < maxFiles) {
|
|
124
|
+
const dir = queue.shift();
|
|
125
|
+
let entries = [];
|
|
126
|
+
try {
|
|
127
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
128
|
+
} catch (_) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const fullPath = path.join(dir, entry.name);
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
if (!shouldIgnoreDir(entry.name)) queue.push(fullPath);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (entry.isFile()) {
|
|
139
|
+
files.push(fullPath);
|
|
140
|
+
if (files.length >= maxFiles) break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return files;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function readLines(filePath, maxBytes = DEFAULT_MAX_BYTES) {
|
|
149
|
+
let stat;
|
|
150
|
+
try {
|
|
151
|
+
stat = fs.statSync(filePath);
|
|
152
|
+
} catch (_) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (!stat.isFile() || stat.size > maxBytes) return null;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
return fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
|
159
|
+
} catch (_) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function addEvidence(map, componentDef, evidence, maxEvidencePerComponent) {
|
|
165
|
+
const current = map.get(componentDef.id) || {
|
|
166
|
+
id: componentDef.id,
|
|
167
|
+
name: componentDef.name,
|
|
168
|
+
category: componentDef.category,
|
|
169
|
+
ecosystem: componentDef.ecosystem,
|
|
170
|
+
evidence: [],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
if (current.evidence.length < maxEvidencePerComponent) {
|
|
174
|
+
const duplicate = current.evidence.some((item) => (
|
|
175
|
+
item.file === evidence.file && item.line === evidence.line && item.kind === evidence.kind
|
|
176
|
+
));
|
|
177
|
+
if (!duplicate) current.evidence.push(evidence);
|
|
178
|
+
}
|
|
179
|
+
map.set(componentDef.id, current);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function scanSourceFile(rootDir, filePath, map, options) {
|
|
183
|
+
const lines = readLines(filePath, options.maxBytes);
|
|
184
|
+
if (!lines) return;
|
|
185
|
+
|
|
186
|
+
const rel = relativePath(rootDir, filePath);
|
|
187
|
+
lines.forEach((line, idx) => {
|
|
188
|
+
for (const def of SOURCE_PATTERNS) {
|
|
189
|
+
if (!def.pattern.test(line)) continue;
|
|
190
|
+
addEvidence(map, def, {
|
|
191
|
+
kind: 'source',
|
|
192
|
+
file: rel,
|
|
193
|
+
line: idx + 1,
|
|
194
|
+
snippet: options.includeSnippets === false ? undefined : line.trim().slice(0, 220),
|
|
195
|
+
}, options.maxEvidencePerComponent);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function scanManifestFile(rootDir, filePath, map, options) {
|
|
201
|
+
const lines = readLines(filePath, options.maxBytes);
|
|
202
|
+
if (!lines) return;
|
|
203
|
+
const rel = relativePath(rootDir, filePath);
|
|
204
|
+
|
|
205
|
+
lines.forEach((line, idx) => {
|
|
206
|
+
for (const def of SOURCE_PATTERNS) {
|
|
207
|
+
if (!def.pattern.test(line)) continue;
|
|
208
|
+
addEvidence(map, def, {
|
|
209
|
+
kind: 'manifest',
|
|
210
|
+
file: rel,
|
|
211
|
+
line: idx + 1,
|
|
212
|
+
snippet: options.includeSnippets === false ? undefined : line.trim().slice(0, 220),
|
|
213
|
+
}, options.maxEvidencePerComponent);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function scanModelArtifact(rootDir, filePath, map, options) {
|
|
219
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
220
|
+
const def = MODEL_EXTENSIONS.get(ext);
|
|
221
|
+
if (!def) return;
|
|
222
|
+
|
|
223
|
+
let stat = null;
|
|
224
|
+
try {
|
|
225
|
+
stat = fs.statSync(filePath);
|
|
226
|
+
} catch (_) {
|
|
227
|
+
stat = null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
addEvidence(map, {
|
|
231
|
+
id: `${def.ecosystem}-${ext.slice(1)}-artifact`,
|
|
232
|
+
...def,
|
|
233
|
+
}, {
|
|
234
|
+
kind: 'artifact',
|
|
235
|
+
file: relativePath(rootDir, filePath),
|
|
236
|
+
line: null,
|
|
237
|
+
bytes: stat ? stat.size : undefined,
|
|
238
|
+
}, options.maxEvidencePerComponent);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function summarizeComponents(components) {
|
|
242
|
+
const byCategory = {};
|
|
243
|
+
const byEcosystem = {};
|
|
244
|
+
for (const item of components) {
|
|
245
|
+
byCategory[item.category] = (byCategory[item.category] || 0) + 1;
|
|
246
|
+
byEcosystem[item.ecosystem] = (byEcosystem[item.ecosystem] || 0) + 1;
|
|
247
|
+
}
|
|
248
|
+
return { byCategory, byEcosystem };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function scanAiComponents(options = {}) {
|
|
252
|
+
const rootDir = path.resolve(options.rootDir || process.cwd());
|
|
253
|
+
const maxEvidencePerComponent = Number(options.maxEvidencePerComponent || 10);
|
|
254
|
+
const scanOptions = {
|
|
255
|
+
maxFiles: options.maxFiles || DEFAULT_MAX_FILES,
|
|
256
|
+
maxBytes: options.maxBytes || DEFAULT_MAX_BYTES,
|
|
257
|
+
includeSnippets: options.includeSnippets !== false,
|
|
258
|
+
maxEvidencePerComponent,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const files = walkFiles(rootDir, scanOptions);
|
|
262
|
+
const map = new Map();
|
|
263
|
+
|
|
264
|
+
for (const filePath of files) {
|
|
265
|
+
scanModelArtifact(rootDir, filePath, map, scanOptions);
|
|
266
|
+
const base = path.basename(filePath);
|
|
267
|
+
if (MANIFEST_FILES.has(base)) {
|
|
268
|
+
scanManifestFile(rootDir, filePath, map, scanOptions);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (isTextLike(filePath)) scanSourceFile(rootDir, filePath, map, scanOptions);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const components = Array.from(map.values()).sort((a, b) => a.id.localeCompare(b.id));
|
|
275
|
+
const summary = summarizeComponents(components);
|
|
276
|
+
return {
|
|
277
|
+
schemaVersion: 'thumbgate.ai-inventory.v1',
|
|
278
|
+
generatedAt: new Date().toISOString(),
|
|
279
|
+
rootDir,
|
|
280
|
+
filesScanned: files.length,
|
|
281
|
+
componentCount: components.length,
|
|
282
|
+
summary,
|
|
283
|
+
components,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function buildCycloneDxMlBom(inventory, options = {}) {
|
|
288
|
+
const serialHash = crypto
|
|
289
|
+
.createHash('sha256')
|
|
290
|
+
.update(JSON.stringify({
|
|
291
|
+
rootDir: inventory.rootDir,
|
|
292
|
+
components: inventory.components.map((item) => [item.id, item.evidence.map((e) => e.file)]),
|
|
293
|
+
}))
|
|
294
|
+
.digest('hex')
|
|
295
|
+
.slice(0, 32);
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
bomFormat: 'CycloneDX',
|
|
299
|
+
specVersion: '1.5',
|
|
300
|
+
serialNumber: `urn:uuid:${serialHash.slice(0, 8)}-${serialHash.slice(8, 12)}-${serialHash.slice(12, 16)}-${serialHash.slice(16, 20)}-${serialHash.slice(20, 32)}`,
|
|
301
|
+
version: 1,
|
|
302
|
+
metadata: {
|
|
303
|
+
timestamp: inventory.generatedAt,
|
|
304
|
+
tools: [
|
|
305
|
+
{
|
|
306
|
+
vendor: 'ThumbGate',
|
|
307
|
+
name: 'AI Component Inventory',
|
|
308
|
+
version: options.version || 'local',
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
properties: [
|
|
312
|
+
{ name: 'thumbgate:rootDir', value: inventory.rootDir },
|
|
313
|
+
{ name: 'thumbgate:filesScanned', value: String(inventory.filesScanned) },
|
|
314
|
+
{ name: 'thumbgate:componentCount', value: String(inventory.componentCount) },
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
components: inventory.components.map((item) => ({
|
|
318
|
+
type: item.category === 'model_artifact' ? 'machine-learning-model' : 'library',
|
|
319
|
+
name: item.name,
|
|
320
|
+
group: item.ecosystem,
|
|
321
|
+
bomRef: `thumbgate:${item.id}`,
|
|
322
|
+
properties: [
|
|
323
|
+
{ name: 'thumbgate:category', value: item.category },
|
|
324
|
+
{ name: 'thumbgate:evidenceCount', value: String(item.evidence.length) },
|
|
325
|
+
{ name: 'thumbgate:evidence', value: JSON.stringify(item.evidence.map((e) => ({ file: e.file, line: e.line, kind: e.kind }))) },
|
|
326
|
+
],
|
|
327
|
+
})),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function formatInventoryText(inventory) {
|
|
332
|
+
const lines = [];
|
|
333
|
+
lines.push('ThumbGate AI Component Inventory');
|
|
334
|
+
lines.push(` Root : ${inventory.rootDir}`);
|
|
335
|
+
lines.push(` Files scanned : ${inventory.filesScanned}`);
|
|
336
|
+
lines.push(` AI components : ${inventory.componentCount}`);
|
|
337
|
+
lines.push('');
|
|
338
|
+
lines.push('By category:');
|
|
339
|
+
const categories = Object.entries(inventory.summary.byCategory).sort((a, b) => a[0].localeCompare(b[0]));
|
|
340
|
+
if (!categories.length) lines.push(' none detected');
|
|
341
|
+
for (const [category, count] of categories) lines.push(` ${category}: ${count}`);
|
|
342
|
+
lines.push('');
|
|
343
|
+
lines.push('Evidence:');
|
|
344
|
+
if (!inventory.components.length) lines.push(' none detected');
|
|
345
|
+
for (const item of inventory.components) {
|
|
346
|
+
lines.push(` - ${item.name} (${item.category}, ${item.ecosystem})`);
|
|
347
|
+
for (const evidence of item.evidence.slice(0, 3)) {
|
|
348
|
+
const loc = evidence.line ? `${evidence.file}:${evidence.line}` : evidence.file;
|
|
349
|
+
lines.push(` ${loc} [${evidence.kind}]`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return lines.join('\n');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function writeOutput(filePath, data) {
|
|
356
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
357
|
+
fs.writeFileSync(filePath, data);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
SOURCE_PATTERNS,
|
|
362
|
+
MODEL_EXTENSIONS,
|
|
363
|
+
scanAiComponents,
|
|
364
|
+
buildCycloneDxMlBom,
|
|
365
|
+
formatInventoryText,
|
|
366
|
+
writeOutput,
|
|
367
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_THRESHOLDS = {
|
|
8
|
+
faithfulness: 0.72,
|
|
9
|
+
answerRelevance: 0.45,
|
|
10
|
+
contextPrecision: 0.5,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function tokenize(value) {
|
|
14
|
+
return String(value || '')
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.split(/[^a-z0-9]+/)
|
|
17
|
+
.filter((token) => token.length > 2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function unique(values) {
|
|
21
|
+
return [...new Set(values.filter(Boolean))];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function overlapScore(left, right) {
|
|
25
|
+
const leftTokens = unique(tokenize(left));
|
|
26
|
+
const rightSet = new Set(tokenize(right));
|
|
27
|
+
if (leftTokens.length === 0) return 0;
|
|
28
|
+
const matches = leftTokens.filter((token) => rightSet.has(token)).length;
|
|
29
|
+
return matches / leftTokens.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function splitClaims(response) {
|
|
33
|
+
return String(response || '')
|
|
34
|
+
.split(/(?:[.!?]\s+|\n+)/)
|
|
35
|
+
.map((claim) => claim.trim())
|
|
36
|
+
.filter((claim) => claim.length > 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizeContexts(contexts) {
|
|
40
|
+
if (Array.isArray(contexts)) return contexts.map(String).filter(Boolean);
|
|
41
|
+
if (contexts) return [String(contexts)];
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function scoreFaithfulness(response, contexts) {
|
|
46
|
+
const claims = splitClaims(response);
|
|
47
|
+
const contextText = normalizeContexts(contexts).join('\n');
|
|
48
|
+
if (claims.length === 0) return { score: 0, supportedClaims: 0, totalClaims: 0 };
|
|
49
|
+
const supportedClaims = claims.filter((claim) => {
|
|
50
|
+
const normalized = claim.toLowerCase();
|
|
51
|
+
return contextText.toLowerCase().includes(normalized) || overlapScore(claim, contextText) >= 0.58;
|
|
52
|
+
}).length;
|
|
53
|
+
return {
|
|
54
|
+
score: Number((supportedClaims / claims.length).toFixed(4)),
|
|
55
|
+
supportedClaims,
|
|
56
|
+
totalClaims: claims.length,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function scoreAnswerRelevance(question, response) {
|
|
61
|
+
const score = overlapScore(question, response);
|
|
62
|
+
return {
|
|
63
|
+
score: Number(score.toFixed(4)),
|
|
64
|
+
matchedQuestionTerms: unique(tokenize(question).filter((token) => tokenize(response).includes(token))),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scoreContextPrecision(question, contexts, reference = '') {
|
|
69
|
+
const normalizedContexts = normalizeContexts(contexts);
|
|
70
|
+
const target = [question, reference].filter(Boolean).join('\n');
|
|
71
|
+
if (normalizedContexts.length === 0) return { score: 0, relevantContexts: 0, totalContexts: 0 };
|
|
72
|
+
|
|
73
|
+
let precisionSum = 0;
|
|
74
|
+
let relevantContexts = 0;
|
|
75
|
+
normalizedContexts.forEach((context, index) => {
|
|
76
|
+
const relevant = overlapScore(target, context) >= 0.22 || overlapScore(context, target) >= 0.22;
|
|
77
|
+
if (relevant) relevantContexts += 1;
|
|
78
|
+
const precisionAtK = relevantContexts / (index + 1);
|
|
79
|
+
if (relevant) precisionSum += precisionAtK;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const score = relevantContexts === 0 ? 0 : precisionSum / relevantContexts;
|
|
83
|
+
return {
|
|
84
|
+
score: Number(score.toFixed(4)),
|
|
85
|
+
relevantContexts,
|
|
86
|
+
totalContexts: normalizedContexts.length,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function evaluateGeneration(testCase, options = {}) {
|
|
91
|
+
const thresholds = { ...DEFAULT_THRESHOLDS, ...(options.thresholds || {}) };
|
|
92
|
+
const contexts = normalizeContexts(testCase.retrievedContexts || testCase.contexts || testCase.retrieved_contexts);
|
|
93
|
+
const faithfulness = scoreFaithfulness(testCase.response || testCase.answer, contexts);
|
|
94
|
+
const answerRelevance = scoreAnswerRelevance(testCase.question || testCase.user_input, testCase.response || testCase.answer);
|
|
95
|
+
const contextPrecision = scoreContextPrecision(
|
|
96
|
+
testCase.question || testCase.user_input,
|
|
97
|
+
contexts,
|
|
98
|
+
testCase.reference || testCase.groundTruth || ''
|
|
99
|
+
);
|
|
100
|
+
const scores = {
|
|
101
|
+
faithfulness: faithfulness.score,
|
|
102
|
+
answerRelevance: answerRelevance.score,
|
|
103
|
+
contextPrecision: contextPrecision.score,
|
|
104
|
+
};
|
|
105
|
+
const passed = scores.faithfulness >= thresholds.faithfulness
|
|
106
|
+
&& scores.answerRelevance >= thresholds.answerRelevance
|
|
107
|
+
&& scores.contextPrecision >= thresholds.contextPrecision;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
id: String(testCase.id || testCase.traceId || 'case'),
|
|
111
|
+
traceId: String(testCase.traceId || testCase.id || ''),
|
|
112
|
+
passed,
|
|
113
|
+
scores,
|
|
114
|
+
thresholds,
|
|
115
|
+
details: {
|
|
116
|
+
faithfulness,
|
|
117
|
+
answerRelevance,
|
|
118
|
+
contextPrecision,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildRagasCompatibleRows(cases) {
|
|
124
|
+
return cases.map((testCase) => ({
|
|
125
|
+
user_input: testCase.question || testCase.user_input || '',
|
|
126
|
+
response: testCase.response || testCase.answer || '',
|
|
127
|
+
retrieved_contexts: normalizeContexts(testCase.retrievedContexts || testCase.contexts || testCase.retrieved_contexts),
|
|
128
|
+
reference: testCase.reference || testCase.groundTruth || '',
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildLangSmithCompatibleRuns(cases, results) {
|
|
133
|
+
return cases.map((testCase, index) => ({
|
|
134
|
+
id: testCase.traceId || testCase.id || `case-${index + 1}`,
|
|
135
|
+
name: 'thumbgate_async_rag_eval',
|
|
136
|
+
inputs: { question: testCase.question || testCase.user_input || '' },
|
|
137
|
+
outputs: { response: testCase.response || testCase.answer || '' },
|
|
138
|
+
metadata: {
|
|
139
|
+
evaluator: 'thumbgate-async-eval-observability',
|
|
140
|
+
caseId: testCase.id || null,
|
|
141
|
+
},
|
|
142
|
+
feedback: Object.entries(results[index].scores).map(([key, score]) => ({
|
|
143
|
+
key,
|
|
144
|
+
score,
|
|
145
|
+
})),
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildEvalReport(cases, options = {}) {
|
|
150
|
+
const normalizedCases = Array.isArray(cases) ? cases : [];
|
|
151
|
+
const results = normalizedCases.map((testCase) => evaluateGeneration(testCase, options));
|
|
152
|
+
const passed = results.filter((result) => result.passed).length;
|
|
153
|
+
const failed = results.length - passed;
|
|
154
|
+
const aggregate = {
|
|
155
|
+
faithfulness: average(results.map((result) => result.scores.faithfulness)),
|
|
156
|
+
answerRelevance: average(results.map((result) => result.scores.answerRelevance)),
|
|
157
|
+
contextPrecision: average(results.map((result) => result.scores.contextPrecision)),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
generatedAt: new Date().toISOString(),
|
|
162
|
+
mode: 'async-post-generation',
|
|
163
|
+
total: results.length,
|
|
164
|
+
passed,
|
|
165
|
+
failed,
|
|
166
|
+
passRate: results.length === 0 ? 0 : Number(((passed / results.length) * 100).toFixed(2)),
|
|
167
|
+
aggregate,
|
|
168
|
+
passedThreshold: failed === 0,
|
|
169
|
+
metrics: ['faithfulness', 'answerRelevance', 'contextPrecision'],
|
|
170
|
+
sinks: {
|
|
171
|
+
ci: true,
|
|
172
|
+
langsmithCompatible: true,
|
|
173
|
+
ragasCompatible: true,
|
|
174
|
+
},
|
|
175
|
+
results,
|
|
176
|
+
ragasDataset: buildRagasCompatibleRows(normalizedCases),
|
|
177
|
+
langsmithRuns: buildLangSmithCompatibleRuns(normalizedCases, results),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function average(values) {
|
|
182
|
+
const numeric = values.filter((value) => Number.isFinite(value));
|
|
183
|
+
if (numeric.length === 0) return 0;
|
|
184
|
+
return Number((numeric.reduce((sum, value) => sum + value, 0) / numeric.length).toFixed(4));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function runAsyncEvaluation(cases, options = {}) {
|
|
188
|
+
const report = await new Promise((resolve) => {
|
|
189
|
+
setImmediate(() => resolve(buildEvalReport(cases, options)));
|
|
190
|
+
});
|
|
191
|
+
if (options.outputPath) {
|
|
192
|
+
fs.mkdirSync(path.dirname(options.outputPath), { recursive: true });
|
|
193
|
+
fs.writeFileSync(options.outputPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
194
|
+
}
|
|
195
|
+
return report;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function loadCases(inputPath) {
|
|
199
|
+
const payload = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
|
|
200
|
+
return Array.isArray(payload) ? payload : payload.cases || [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function main(argv = process.argv.slice(2)) {
|
|
204
|
+
const inputIndex = argv.indexOf('--input');
|
|
205
|
+
const outputIndex = argv.indexOf('--output');
|
|
206
|
+
const inputPath = inputIndex >= 0 ? argv[inputIndex + 1] : 'bench/observability-eval-suite.json';
|
|
207
|
+
const outputPath = outputIndex >= 0 ? argv[outputIndex + 1] : 'proof/async-eval-observability-report.json';
|
|
208
|
+
const report = await runAsyncEvaluation(loadCases(inputPath), { outputPath });
|
|
209
|
+
process.stdout.write(`${JSON.stringify({
|
|
210
|
+
outputPath,
|
|
211
|
+
total: report.total,
|
|
212
|
+
passed: report.passed,
|
|
213
|
+
failed: report.failed,
|
|
214
|
+
passRate: report.passRate,
|
|
215
|
+
}, null, 2)}\n`);
|
|
216
|
+
if (!report.passedThreshold) process.exitCode = 1;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
DEFAULT_THRESHOLDS,
|
|
221
|
+
buildEvalReport,
|
|
222
|
+
buildLangSmithCompatibleRuns,
|
|
223
|
+
buildRagasCompatibleRows,
|
|
224
|
+
evaluateGeneration,
|
|
225
|
+
runAsyncEvaluation,
|
|
226
|
+
scoreAnswerRelevance,
|
|
227
|
+
scoreContextPrecision,
|
|
228
|
+
scoreFaithfulness,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (require.main === module) {
|
|
232
|
+
main().catch((err) => {
|
|
233
|
+
console.error(err.stack || err.message);
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
});
|
|
236
|
+
}
|
package/scripts/audit.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/audit.js
|
|
5
|
+
*
|
|
6
|
+
* Heuristic-based AI bill auditor. Finds repeated mistakes in agent transcripts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
|
|
11
|
+
const PATTERNS = [
|
|
12
|
+
{
|
|
13
|
+
id: 'force-push-retry',
|
|
14
|
+
name: 'git push --force after correction',
|
|
15
|
+
regex: /git\s+push.*--force/gi,
|
|
16
|
+
tokenEstimate: 6000,
|
|
17
|
+
costPerRepeat: 0.44,
|
|
18
|
+
why: 'Full diff context reload on error.'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'import-loop',
|
|
22
|
+
name: 'Hallucinated import retry',
|
|
23
|
+
regex: /(Cannot find module|Module not found|import .* from .*error)/gi,
|
|
24
|
+
tokenEstimate: 4000,
|
|
25
|
+
costPerRepeat: 0.12,
|
|
26
|
+
why: 'Re-indexing and path searching.'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'apology-loop',
|
|
30
|
+
name: '"I apologize" retry cycle',
|
|
31
|
+
regex: /(I apologize|Let me try a different approach|I will now attempt)/gi,
|
|
32
|
+
tokenEstimate: 5000,
|
|
33
|
+
costPerRepeat: 0.15,
|
|
34
|
+
why: 'Reasoning chain reset.'
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function runAudit(filePath) {
|
|
39
|
+
if (!fs.existsSync(filePath)) {
|
|
40
|
+
return { error: 'File not found: ' + filePath };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
44
|
+
const results = [];
|
|
45
|
+
let totalWaste = 0;
|
|
46
|
+
|
|
47
|
+
PATTERNS.forEach(p => {
|
|
48
|
+
const matches = (content.match(p.regex) || []).length;
|
|
49
|
+
if (matches > 1) { // It's only a "repeat" if it happens more than once
|
|
50
|
+
const repeats = matches - 1;
|
|
51
|
+
const waste = repeats * p.costPerRepeat;
|
|
52
|
+
totalWaste += waste;
|
|
53
|
+
results.push({
|
|
54
|
+
pattern: p.name,
|
|
55
|
+
occurrences: repeats,
|
|
56
|
+
waste: waste.toFixed(2),
|
|
57
|
+
why: p.why
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return { results, totalWaste: totalWaste.toFixed(2) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { runAudit, PATTERNS };
|