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,767 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Universal Claim Evaluator
|
|
5
|
+
*
|
|
6
|
+
* Parses free-text factual claims (e.g. "the row count is 1,284") and rechecks
|
|
7
|
+
* them against configured verifiers (SQLite, filesystem, JSON). Fail-closed:
|
|
8
|
+
* - mismatch → verified=false
|
|
9
|
+
* - parseable claim with no matching verifier → verified=false (unconfigured)
|
|
10
|
+
* - no parseable factual claims → neutral (empty checks; session-action gates still apply)
|
|
11
|
+
*
|
|
12
|
+
* Verifier queries/paths come only from operator config — never from claim text —
|
|
13
|
+
* so agents cannot inject SQL or path traversal through the claim string.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const { resolveFeedbackDir } = require('./feedback-paths');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_VERIFIERS_FILENAME = 'claim-verifiers.json';
|
|
21
|
+
// Package install root (node_modules/thumbgate), not the consumer project cwd.
|
|
22
|
+
const PACKAGE_ROOT = path.join(__dirname, '..');
|
|
23
|
+
const CLAIM_VALUE_MARKER = '{{value}}';
|
|
24
|
+
const NUMBER_PATTERN_SOURCE = String.raw`[-+]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?`;
|
|
25
|
+
const INTEGER_PATTERN_SOURCE = String.raw`[-+]?(?:\d{1,3}(?:,\d{3})+|\d+)`;
|
|
26
|
+
const FILE_PATTERN_SOURCE = String.raw`([^\s,]+\.[A-Za-z0-9]+)`;
|
|
27
|
+
|
|
28
|
+
function parseNumberToken(raw) {
|
|
29
|
+
if (raw == null) return null;
|
|
30
|
+
const cleaned = String(raw).replaceAll(',', '').trim();
|
|
31
|
+
if (!/^[+-]?\d+(?:\.\d+)?$/.test(cleaned)) return null;
|
|
32
|
+
const value = Number(cleaned);
|
|
33
|
+
return Number.isFinite(value) ? value : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeSubject(value) {
|
|
37
|
+
return String(value || '')
|
|
38
|
+
.toLowerCase()
|
|
39
|
+
.replaceAll(/[_./\\-]+/g, ' ')
|
|
40
|
+
.replaceAll(/\s+/g, ' ')
|
|
41
|
+
.trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function escapeRegExp(value) {
|
|
45
|
+
return String(value).replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function configuredClaimTemplates(verifier) {
|
|
49
|
+
const templates = [];
|
|
50
|
+
if (typeof verifier?.claimTemplate === 'string') templates.push(verifier.claimTemplate);
|
|
51
|
+
if (Array.isArray(verifier?.claimTemplates)) templates.push(...verifier.claimTemplates);
|
|
52
|
+
return templates;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compile an operator-authored literal template containing exactly one numeric
|
|
57
|
+
* {{value}} slot. Everything outside the slot is regex-escaped, so a template
|
|
58
|
+
* cannot smuggle in executable regex or redirect the configured verifier.
|
|
59
|
+
*/
|
|
60
|
+
function compileConfiguredClaimTemplate(template) {
|
|
61
|
+
if (typeof template !== 'string' || template.length === 0 || template.length > 500) {
|
|
62
|
+
throw new Error('claim template must be a non-empty string of at most 500 characters');
|
|
63
|
+
}
|
|
64
|
+
const parts = template.split(CLAIM_VALUE_MARKER);
|
|
65
|
+
if (parts.length !== 2) {
|
|
66
|
+
throw new Error(`claim template must contain exactly one ${CLAIM_VALUE_MARKER} marker`);
|
|
67
|
+
}
|
|
68
|
+
if (parts.join('').replaceAll(/\s+/g, '').length < 3) {
|
|
69
|
+
throw new Error('claim template needs at least three literal characters outside the value marker');
|
|
70
|
+
}
|
|
71
|
+
const literalPattern = parts
|
|
72
|
+
.map((part) => escapeRegExp(part).replaceAll(/\s+/g, String.raw`\s+`))
|
|
73
|
+
.join(`(?<value>${NUMBER_PATTERN_SOURCE})`);
|
|
74
|
+
return new RegExp(
|
|
75
|
+
String.raw`(?<![\p{L}\p{N}_])${literalPattern}(?![\p{L}\p{N}_])`,
|
|
76
|
+
'giu',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateConfiguredClaimTemplates(verifiers = []) {
|
|
81
|
+
for (const verifier of verifiers) {
|
|
82
|
+
const templates = configuredClaimTemplates(verifier);
|
|
83
|
+
if (templates.length === 0) continue;
|
|
84
|
+
const verifierId = typeof verifier?.id === 'string' ? verifier.id.trim() : '';
|
|
85
|
+
if (!verifierId) {
|
|
86
|
+
throw new Error('a verifier with claimTemplate or claimTemplates requires a unique id');
|
|
87
|
+
}
|
|
88
|
+
const matchingIds = verifiers.filter((candidate) => candidate?.id === verifierId);
|
|
89
|
+
if (matchingIds.length !== 1) {
|
|
90
|
+
throw new Error(`duplicate configured claim-template verifier id: ${verifierId}`);
|
|
91
|
+
}
|
|
92
|
+
for (const template of templates) compileConfiguredClaimTemplate(template);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function parseConfiguredClaimTemplates(source, verifiers, push) {
|
|
97
|
+
for (const verifier of verifiers) {
|
|
98
|
+
for (const template of configuredClaimTemplates(verifier)) {
|
|
99
|
+
const pattern = compileConfiguredClaimTemplate(template);
|
|
100
|
+
for (const match of source.matchAll(pattern)) {
|
|
101
|
+
const expected = parseNumberToken(match.groups?.value);
|
|
102
|
+
if (expected == null) continue;
|
|
103
|
+
push({
|
|
104
|
+
kind: 'configured_value',
|
|
105
|
+
subject: normalizeSubject(verifier.id),
|
|
106
|
+
expected,
|
|
107
|
+
raw: match[0],
|
|
108
|
+
verifierId: verifier.id,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveRepoRoot(cwd) {
|
|
116
|
+
return path.resolve(cwd || process.cwd());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function pathExistsOrIsSymlink(targetPath) {
|
|
120
|
+
try {
|
|
121
|
+
fs.lstatSync(targetPath);
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function pathIsWithin(rootPath, candidatePath) {
|
|
129
|
+
const rootWithSep = rootPath.endsWith(path.sep) ? rootPath : `${rootPath}${path.sep}`;
|
|
130
|
+
return candidatePath === rootPath || candidatePath.startsWith(rootWithSep);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function resolveSafePath(repoRoot, targetPath) {
|
|
134
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
135
|
+
throw new Error('path is required');
|
|
136
|
+
}
|
|
137
|
+
if (path.isAbsolute(targetPath)) {
|
|
138
|
+
throw new Error('absolute paths are not allowed in claim verifiers');
|
|
139
|
+
}
|
|
140
|
+
if (targetPath.includes('\0')) {
|
|
141
|
+
throw new Error('invalid path');
|
|
142
|
+
}
|
|
143
|
+
const lexicalRoot = path.resolve(repoRoot);
|
|
144
|
+
const resolved = path.resolve(lexicalRoot, targetPath);
|
|
145
|
+
if (!pathIsWithin(lexicalRoot, resolved)) {
|
|
146
|
+
throw new Error(`path escapes repo root: ${targetPath}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// A lexical prefix check is insufficient when an in-root symlink points
|
|
150
|
+
// outside the root. Resolve the target when it exists, or its closest
|
|
151
|
+
// existing ancestor for not-yet-created paths, and enforce the real root.
|
|
152
|
+
const realRoot = fs.realpathSync(lexicalRoot);
|
|
153
|
+
let existing = resolved;
|
|
154
|
+
while (!pathExistsOrIsSymlink(existing)) {
|
|
155
|
+
const parent = path.dirname(existing);
|
|
156
|
+
if (parent === existing) break;
|
|
157
|
+
existing = parent;
|
|
158
|
+
}
|
|
159
|
+
let realExisting;
|
|
160
|
+
try {
|
|
161
|
+
realExisting = fs.realpathSync(existing);
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error(`path cannot be resolved safely: ${targetPath}`);
|
|
164
|
+
}
|
|
165
|
+
if (!pathIsWithin(realRoot, realExisting)) {
|
|
166
|
+
throw new Error(`path resolves outside repo root through a symlink: ${targetPath}`);
|
|
167
|
+
}
|
|
168
|
+
return resolved;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createClaimCollector() {
|
|
172
|
+
const claims = [];
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
const push = (claim) => {
|
|
175
|
+
const key = `${claim.kind}|${claim.subject}|${claim.path || ''}|${String(claim.expected)}`;
|
|
176
|
+
if (seen.has(key)) return;
|
|
177
|
+
seen.add(key);
|
|
178
|
+
claims.push(claim);
|
|
179
|
+
};
|
|
180
|
+
return { claims, push };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function collectPatternClaims(source, pattern, buildClaim, push) {
|
|
184
|
+
for (const match of source.matchAll(pattern)) {
|
|
185
|
+
const claim = buildClaim(match);
|
|
186
|
+
if (claim) push(claim);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function numericClaim(match, { kind, subjectIndex, valueIndex, subject }) {
|
|
191
|
+
const expected = parseNumberToken(match[valueIndex]);
|
|
192
|
+
if (expected == null) return null;
|
|
193
|
+
return {
|
|
194
|
+
kind,
|
|
195
|
+
subject: subject || normalizeSubject(match[subjectIndex]),
|
|
196
|
+
expected,
|
|
197
|
+
raw: match[0],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function collectCountClaims(source, push) {
|
|
202
|
+
const directCount = new RegExp(
|
|
203
|
+
String.raw`\b([A-Za-z][\w.-]*(?:\s+[A-Za-z][\w.-]*){0,7}\s+counts?)\s*(?:is|are|=|:|equals?)\s*(${NUMBER_PATTERN_SOURCE})\b`,
|
|
204
|
+
'giu',
|
|
205
|
+
);
|
|
206
|
+
const thereAreCount = new RegExp(
|
|
207
|
+
String.raw`\bthere\s+(?:is|are)\s+(${INTEGER_PATTERN_SOURCE})\s+(rows?|records?|entries|items?|orders?|users?|lessons?|lines?)\b`,
|
|
208
|
+
'giu',
|
|
209
|
+
);
|
|
210
|
+
const sqlCount = new RegExp(
|
|
211
|
+
String.raw`\bCOUNT\s*\(\s*\*\s*\)\s*(?:=|is|:)\s*(${INTEGER_PATTERN_SOURCE})\b`,
|
|
212
|
+
'giu',
|
|
213
|
+
);
|
|
214
|
+
collectPatternClaims(source, directCount, (match) => numericClaim(match, {
|
|
215
|
+
kind: 'count', subjectIndex: 1, valueIndex: 2,
|
|
216
|
+
}), push);
|
|
217
|
+
collectPatternClaims(source, thereAreCount, (match) => numericClaim(match, {
|
|
218
|
+
kind: 'count', subjectIndex: 2, valueIndex: 1,
|
|
219
|
+
}), push);
|
|
220
|
+
collectPatternClaims(source, sqlCount, (match) => numericClaim(match, {
|
|
221
|
+
kind: 'count', valueIndex: 1, subject: 'count',
|
|
222
|
+
}), push);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function fileNumericClaim(match, kind, subject) {
|
|
226
|
+
const claim = numericClaim(match, { kind, valueIndex: 2, subject });
|
|
227
|
+
return claim ? { ...claim, path: match[1] } : null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function collectFileClaims(source, push) {
|
|
231
|
+
const fileLines = new RegExp(
|
|
232
|
+
String.raw`\b(?:file\s+)?${FILE_PATTERN_SOURCE}\s+(?:has|contains)\s+(${INTEGER_PATTERN_SOURCE})\s+lines?\b`,
|
|
233
|
+
'giu',
|
|
234
|
+
);
|
|
235
|
+
const fileBytes = new RegExp(
|
|
236
|
+
String.raw`\b(?:file\s+)?${FILE_PATTERN_SOURCE}\s+(?:is|has)\s+(${INTEGER_PATTERN_SOURCE})\s+bytes?\b`,
|
|
237
|
+
'giu',
|
|
238
|
+
);
|
|
239
|
+
const fileExists = new RegExp(
|
|
240
|
+
String.raw`\b(?:file\s+)?${FILE_PATTERN_SOURCE}\s+(?:exists|is present|is on disk)\b`,
|
|
241
|
+
'giu',
|
|
242
|
+
);
|
|
243
|
+
const fileMissing = new RegExp(
|
|
244
|
+
String.raw`\b(?:file\s+)?${FILE_PATTERN_SOURCE}\s+(?:does not exist|is missing|is absent)\b`,
|
|
245
|
+
'giu',
|
|
246
|
+
);
|
|
247
|
+
collectPatternClaims(source, fileLines, (match) => fileNumericClaim(match, 'file_lines', 'lines'), push);
|
|
248
|
+
collectPatternClaims(source, fileBytes, (match) => fileNumericClaim(match, 'file_bytes', 'bytes'), push);
|
|
249
|
+
collectPatternClaims(source, fileExists, (match) => ({
|
|
250
|
+
kind: 'file_exists', subject: 'exists', path: match[1], expected: true, raw: match[0],
|
|
251
|
+
}), push);
|
|
252
|
+
collectPatternClaims(source, fileMissing, (match) => ({
|
|
253
|
+
kind: 'file_exists', subject: 'exists', path: match[1], expected: false, raw: match[0],
|
|
254
|
+
}), push);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function collectVersionClaims(source, push) {
|
|
258
|
+
const versionPattern = new RegExp(
|
|
259
|
+
String.raw`\b((?:package\s+)?version)\s*(?:is|=|:|equals?)\s*([vV]?\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?)\b`,
|
|
260
|
+
'giu',
|
|
261
|
+
);
|
|
262
|
+
collectPatternClaims(source, versionPattern, (match) => {
|
|
263
|
+
const rawVersion = String(match[2]);
|
|
264
|
+
const expected = /^[vV]/.test(rawVersion) ? rawVersion.slice(1) : rawVersion;
|
|
265
|
+
return {
|
|
266
|
+
kind: 'value',
|
|
267
|
+
subject: normalizeSubject(match[1]),
|
|
268
|
+
expected,
|
|
269
|
+
raw: match[0],
|
|
270
|
+
};
|
|
271
|
+
}, push);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function addConfiguredClaim(claim, claims, push) {
|
|
275
|
+
const alreadyParsed = claims.find((existing) => (
|
|
276
|
+
existing.raw.toLowerCase() === claim.raw.toLowerCase()
|
|
277
|
+
&& valuesEqual(existing.expected, claim.expected)
|
|
278
|
+
));
|
|
279
|
+
if (!alreadyParsed) {
|
|
280
|
+
push(claim);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (alreadyParsed.verifierId && alreadyParsed.verifierId !== claim.verifierId) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`claim matched multiple configured verifiers: ${alreadyParsed.verifierId}, ${claim.verifierId}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
alreadyParsed.verifierId = claim.verifierId;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Extract structured factual claims from free text.
|
|
293
|
+
* @returns {Array<{kind:string, subject:string, expected:*, path?:string, raw:string}>}
|
|
294
|
+
*/
|
|
295
|
+
function parseFactualClaims(text, options = {}) {
|
|
296
|
+
const source = String(text || '');
|
|
297
|
+
if (!source.trim()) return [];
|
|
298
|
+
|
|
299
|
+
let verifiers = [];
|
|
300
|
+
if (Array.isArray(options)) verifiers = options;
|
|
301
|
+
else if (Array.isArray(options.verifiers)) verifiers = options.verifiers;
|
|
302
|
+
validateConfiguredClaimTemplates(verifiers);
|
|
303
|
+
|
|
304
|
+
const { claims, push } = createClaimCollector();
|
|
305
|
+
collectCountClaims(source, push);
|
|
306
|
+
collectFileClaims(source, push);
|
|
307
|
+
collectVersionClaims(source, push);
|
|
308
|
+
parseConfiguredClaimTemplates(source, verifiers, (claim) => addConfiguredClaim(claim, claims, push));
|
|
309
|
+
|
|
310
|
+
return claims;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function inlineVerifierConfig(options) {
|
|
314
|
+
const candidates = [
|
|
315
|
+
{ verifiers: options.verifiers, source: 'options' },
|
|
316
|
+
{ verifiers: options.claimVerifiers, source: 'options.claimVerifiers' },
|
|
317
|
+
{ verifiers: options.config?.verifiers, source: 'options.config' },
|
|
318
|
+
{ verifiers: options.claimVerifiers?.verifiers, source: 'options.claimVerifiers' },
|
|
319
|
+
];
|
|
320
|
+
return candidates.find((candidate) => Array.isArray(candidate.verifiers)) || null;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function verifierConfigPaths(options, repoRoot) {
|
|
324
|
+
const candidates = [];
|
|
325
|
+
const explicitConfigPath = options.configPath || process.env.THUMBGATE_CLAIM_VERIFIERS_PATH;
|
|
326
|
+
if (explicitConfigPath) {
|
|
327
|
+
const resolvedExplicit = path.isAbsolute(explicitConfigPath)
|
|
328
|
+
? explicitConfigPath
|
|
329
|
+
: path.resolve(repoRoot, explicitConfigPath);
|
|
330
|
+
if (!fs.existsSync(resolvedExplicit)) {
|
|
331
|
+
throw new Error(`claim verifier config not found: ${resolvedExplicit}`);
|
|
332
|
+
}
|
|
333
|
+
candidates.push(resolvedExplicit);
|
|
334
|
+
}
|
|
335
|
+
const feedbackDir = options.feedbackDir || resolveFeedbackDir({ cwd: repoRoot });
|
|
336
|
+
candidates.push(
|
|
337
|
+
path.join(feedbackDir, DEFAULT_VERIFIERS_FILENAME),
|
|
338
|
+
path.join(repoRoot, '.thumbgate', DEFAULT_VERIFIERS_FILENAME),
|
|
339
|
+
path.join(repoRoot, 'config', 'gates', DEFAULT_VERIFIERS_FILENAME),
|
|
340
|
+
// Shipped defaults from the installed package so npm consumers get dogfood
|
|
341
|
+
// without copying config into every project cwd.
|
|
342
|
+
path.join(PACKAGE_ROOT, 'config', 'gates', DEFAULT_VERIFIERS_FILENAME),
|
|
343
|
+
);
|
|
344
|
+
return new Set(candidates);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function readVerifierConfig(candidate) {
|
|
348
|
+
try {
|
|
349
|
+
const raw = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
350
|
+
let verifiers = null;
|
|
351
|
+
if (Array.isArray(raw?.verifiers)) verifiers = raw.verifiers;
|
|
352
|
+
else if (Array.isArray(raw)) verifiers = raw;
|
|
353
|
+
if (!verifiers) {
|
|
354
|
+
throw new Error('expected an array or an object with a verifiers array');
|
|
355
|
+
}
|
|
356
|
+
return { verifiers, source: candidate, path: candidate };
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw new Error(`invalid claim verifier config ${candidate}: ${error.message}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function loadVerifierConfig(options = {}) {
|
|
363
|
+
const inline = inlineVerifierConfig(options);
|
|
364
|
+
if (inline) return inline;
|
|
365
|
+
|
|
366
|
+
const repoRoot = resolveRepoRoot(options.cwd);
|
|
367
|
+
for (const candidate of verifierConfigPaths(options, repoRoot)) {
|
|
368
|
+
if (!candidate || !fs.existsSync(candidate)) continue;
|
|
369
|
+
return readVerifierConfig(candidate);
|
|
370
|
+
}
|
|
371
|
+
return { verifiers: [], source: 'none' };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function subjectMatches(claimSubject, matcherSubjects = []) {
|
|
375
|
+
const claim = normalizeSubject(claimSubject);
|
|
376
|
+
if (!claim) return false;
|
|
377
|
+
for (const subject of matcherSubjects) {
|
|
378
|
+
const needle = normalizeSubject(subject);
|
|
379
|
+
if (!needle) continue;
|
|
380
|
+
if (claim === needle || claim.includes(needle) || needle.includes(claim)) return true;
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function normalizeVerifierPath(value) {
|
|
386
|
+
const normalized = path.posix.normalize(String(value || '').replaceAll('\\', '/'));
|
|
387
|
+
return normalized.startsWith('./') ? normalized.slice(2).toLowerCase() : normalized.toLowerCase();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function pathMatches(claimPath, matcherPaths = []) {
|
|
391
|
+
if (!claimPath) return matcherPaths.length === 0;
|
|
392
|
+
const normalized = normalizeVerifierPath(claimPath);
|
|
393
|
+
for (const candidate of matcherPaths) {
|
|
394
|
+
const c = normalizeVerifierPath(candidate);
|
|
395
|
+
if (!c) continue;
|
|
396
|
+
if (normalized === c) return true;
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function verifierSupportsClaim(verifier, claim) {
|
|
402
|
+
const match = verifier.match || {};
|
|
403
|
+
const kinds = Array.isArray(match.kinds) && match.kinds.length > 0
|
|
404
|
+
? match.kinds
|
|
405
|
+
: [verifier.kind];
|
|
406
|
+
if (kinds.includes(claim.kind)) return true;
|
|
407
|
+
if (claim.kind === 'count') {
|
|
408
|
+
return kinds.includes('sqlite_count') || verifier.kind === 'sqlite_count';
|
|
409
|
+
}
|
|
410
|
+
const compatibleVerifierKinds = {
|
|
411
|
+
value: ['json_path', 'value'],
|
|
412
|
+
file_lines: ['file_lines'],
|
|
413
|
+
file_bytes: ['file_bytes'],
|
|
414
|
+
file_exists: ['file_exists'],
|
|
415
|
+
};
|
|
416
|
+
return compatibleVerifierKinds[claim.kind]?.includes(verifier.kind) || false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function verifierMatchesClaim(verifier, claim) {
|
|
420
|
+
if (!verifier || typeof verifier !== 'object' || !verifierSupportsClaim(verifier, claim)) {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
const match = verifier.match || {};
|
|
424
|
+
const subjects = match.subjects || verifier.subjects || [];
|
|
425
|
+
const paths = match.paths || (verifier.path ? [verifier.path] : []);
|
|
426
|
+
if (claim.path) {
|
|
427
|
+
if (paths.length > 0 && !pathMatches(claim.path, paths)) return false;
|
|
428
|
+
const countSubjectMismatch = claim.kind === 'count'
|
|
429
|
+
&& subjects.length > 0
|
|
430
|
+
&& claim.subject
|
|
431
|
+
&& !subjectMatches(claim.subject, subjects);
|
|
432
|
+
return !countSubjectMismatch;
|
|
433
|
+
}
|
|
434
|
+
return subjects.length > 0 && subjectMatches(claim.subject, subjects);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function findVerifierForClaim(claim, verifiers) {
|
|
438
|
+
if (claim.verifierId) {
|
|
439
|
+
return verifiers.find((verifier) => verifier?.id === claim.verifierId) || null;
|
|
440
|
+
}
|
|
441
|
+
return verifiers.find((verifier) => verifierMatchesClaim(verifier, claim)) || null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function assertSelectOnly(query) {
|
|
445
|
+
const normalized = String(query || '').trim();
|
|
446
|
+
if (!normalized) throw new Error('sqlite_count verifier requires query');
|
|
447
|
+
// Strip trailing semicolon and require a single SELECT statement.
|
|
448
|
+
let body = normalized;
|
|
449
|
+
while (body.endsWith(';')) body = body.slice(0, -1).trimEnd();
|
|
450
|
+
if (/;/.test(body)) throw new Error('sqlite_count query must be a single statement');
|
|
451
|
+
if (!/^\s*select\b/i.test(body)) throw new Error('sqlite_count query must be SELECT-only');
|
|
452
|
+
if (/\b(insert|update|delete|drop|alter|attach|pragma|create|replace|vacuum|reindex)\b/i.test(body)) {
|
|
453
|
+
throw new Error('sqlite_count query rejects non-SELECT keywords');
|
|
454
|
+
}
|
|
455
|
+
return body;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function readSqliteCount(repoRoot, verifier) {
|
|
459
|
+
const Database = require('better-sqlite3');
|
|
460
|
+
const dbPath = resolveSafePath(repoRoot, verifier.dbPath || verifier.path);
|
|
461
|
+
if (!fs.existsSync(dbPath)) {
|
|
462
|
+
throw new Error(`sqlite database not found: ${verifier.dbPath || verifier.path}`);
|
|
463
|
+
}
|
|
464
|
+
const query = assertSelectOnly(verifier.query);
|
|
465
|
+
const db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
466
|
+
try {
|
|
467
|
+
const row = db.prepare(query).get();
|
|
468
|
+
if (!row || typeof row !== 'object') {
|
|
469
|
+
throw new Error('sqlite_count query returned no row');
|
|
470
|
+
}
|
|
471
|
+
const values = Object.values(row);
|
|
472
|
+
const first = values[0];
|
|
473
|
+
const numeric = typeof first === 'number' ? first : parseNumberToken(first);
|
|
474
|
+
if (numeric == null) {
|
|
475
|
+
throw new Error('sqlite_count query did not return a numeric value');
|
|
476
|
+
}
|
|
477
|
+
return numeric;
|
|
478
|
+
} finally {
|
|
479
|
+
db.close();
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function readFileLines(repoRoot, filePath) {
|
|
484
|
+
const resolved = resolveSafePath(repoRoot, filePath);
|
|
485
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
486
|
+
if (content.length === 0) return 0;
|
|
487
|
+
// Count newline-terminated lines; trailing content without newline still counts as a line.
|
|
488
|
+
const parts = content.split(/\r?\n/);
|
|
489
|
+
return parts.length > 0 && parts.at(-1) === '' ? parts.length - 1 : parts.length;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function readFileBytes(repoRoot, filePath) {
|
|
493
|
+
const resolved = resolveSafePath(repoRoot, filePath);
|
|
494
|
+
return fs.statSync(resolved).size;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function readFileExists(repoRoot, filePath) {
|
|
498
|
+
const resolved = resolveSafePath(repoRoot, filePath);
|
|
499
|
+
return fs.existsSync(resolved);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function readJsonPath(repoRoot, verifier) {
|
|
503
|
+
const resolved = resolveSafePath(repoRoot, verifier.path);
|
|
504
|
+
const data = JSON.parse(fs.readFileSync(resolved, 'utf8'));
|
|
505
|
+
const pointer = String(verifier.jsonPath || verifier.pointer || '').replace(/^\./, '');
|
|
506
|
+
if (!pointer) {
|
|
507
|
+
throw new Error('json_path verifier requires jsonPath');
|
|
508
|
+
}
|
|
509
|
+
const segments = pointer.split('.').filter(Boolean);
|
|
510
|
+
let cursor = data;
|
|
511
|
+
for (const segment of segments) {
|
|
512
|
+
if (cursor == null
|
|
513
|
+
|| typeof cursor !== 'object'
|
|
514
|
+
|| !Object.hasOwn(cursor, segment)) {
|
|
515
|
+
throw new Error(`json path not found: ${pointer}`);
|
|
516
|
+
}
|
|
517
|
+
cursor = cursor[segment];
|
|
518
|
+
}
|
|
519
|
+
return cursor;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function runVerifier(claim, verifier, repoRoot) {
|
|
523
|
+
const kind = verifier.kind;
|
|
524
|
+
if (kind === 'sqlite_count') {
|
|
525
|
+
return readSqliteCount(repoRoot, verifier);
|
|
526
|
+
}
|
|
527
|
+
if (kind === 'file_lines') {
|
|
528
|
+
return readFileLines(repoRoot, verifier.path);
|
|
529
|
+
}
|
|
530
|
+
if (kind === 'file_bytes') {
|
|
531
|
+
return readFileBytes(repoRoot, verifier.path);
|
|
532
|
+
}
|
|
533
|
+
if (kind === 'file_exists') {
|
|
534
|
+
return readFileExists(repoRoot, verifier.path);
|
|
535
|
+
}
|
|
536
|
+
if (kind === 'json_path' || kind === 'value') {
|
|
537
|
+
return readJsonPath(repoRoot, verifier);
|
|
538
|
+
}
|
|
539
|
+
throw new Error(`unsupported verifier kind: ${kind}`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function valuesEqual(expected, actual) {
|
|
543
|
+
if (typeof expected === 'boolean') {
|
|
544
|
+
return Boolean(actual) === expected;
|
|
545
|
+
}
|
|
546
|
+
if (typeof expected === 'number') {
|
|
547
|
+
const actualNumber = typeof actual === 'number' ? actual : parseNumberToken(actual);
|
|
548
|
+
return actualNumber != null && actualNumber === expected;
|
|
549
|
+
}
|
|
550
|
+
return String(actual) === String(expected);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function claimCheckBase(claim) {
|
|
554
|
+
return {
|
|
555
|
+
claim: claim.raw,
|
|
556
|
+
kind: claim.kind,
|
|
557
|
+
subject: claim.subject,
|
|
558
|
+
expected: claim.expected,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function unconfiguredClaimCheck(claim, failUnconfigured) {
|
|
563
|
+
let message = `Parsed factual claim "${claim.raw}" with no verifier (advisory).`;
|
|
564
|
+
if (failUnconfigured) {
|
|
565
|
+
message = `Parsed factual claim "${claim.raw}" but no matching verifier is configured. Add one under .thumbgate/claim-verifiers.json (or config/gates/claim-verifiers.json).`;
|
|
566
|
+
}
|
|
567
|
+
return {
|
|
568
|
+
...claimCheckBase(claim),
|
|
569
|
+
path: claim.path || null,
|
|
570
|
+
passed: !failUnconfigured,
|
|
571
|
+
status: 'unconfigured',
|
|
572
|
+
missing: failUnconfigured ? ['claim_verifier_configured'] : [],
|
|
573
|
+
message,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function verifierClaimCheck(claim, verifier, repoRoot) {
|
|
578
|
+
const verifierLabel = verifier.id || verifier.kind;
|
|
579
|
+
try {
|
|
580
|
+
const actual = runVerifier(claim, verifier, repoRoot);
|
|
581
|
+
const passed = valuesEqual(claim.expected, actual);
|
|
582
|
+
const status = passed ? 'match' : 'mismatch';
|
|
583
|
+
const message = `Claim ${passed ? 'verified' : 'mismatch'} via ${verifierLabel}: expected ${String(claim.expected)}, observed ${String(actual)}`;
|
|
584
|
+
return {
|
|
585
|
+
...claimCheckBase(claim),
|
|
586
|
+
claimedPath: claim.path || null,
|
|
587
|
+
path: verifier.path || null,
|
|
588
|
+
actual,
|
|
589
|
+
verifierId: verifier.id || null,
|
|
590
|
+
verifierKind: verifier.kind,
|
|
591
|
+
passed,
|
|
592
|
+
status,
|
|
593
|
+
missing: passed ? [] : ['claim_value_match'],
|
|
594
|
+
message,
|
|
595
|
+
};
|
|
596
|
+
} catch (error) {
|
|
597
|
+
return {
|
|
598
|
+
...claimCheckBase(claim),
|
|
599
|
+
claimedPath: claim.path || null,
|
|
600
|
+
path: verifier.path || null,
|
|
601
|
+
verifierId: verifier.id || null,
|
|
602
|
+
verifierKind: verifier.kind,
|
|
603
|
+
passed: false,
|
|
604
|
+
status: 'verifier_error',
|
|
605
|
+
missing: ['claim_verifier_success'],
|
|
606
|
+
message: `Verifier ${verifierLabel} failed: ${error?.message || 'unknown error'}`,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function evaluateClaim(claim, verifiers, repoRoot, failUnconfigured) {
|
|
612
|
+
const verifier = findVerifierForClaim(claim, verifiers);
|
|
613
|
+
return verifier
|
|
614
|
+
? verifierClaimCheck(claim, verifier, repoRoot)
|
|
615
|
+
: unconfiguredClaimCheck(claim, failUnconfigured);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Evaluate free-text claims against configured verifiers.
|
|
620
|
+
*
|
|
621
|
+
* @param {string} claimText
|
|
622
|
+
* @param {object} [options]
|
|
623
|
+
* @param {Array<object>} [options.verifiers]
|
|
624
|
+
* @param {string} [options.cwd]
|
|
625
|
+
* @param {string} [options.configPath]
|
|
626
|
+
* @param {boolean} [options.failUnconfigured=true]
|
|
627
|
+
* @returns {{
|
|
628
|
+
* verified: boolean,
|
|
629
|
+
* claims: object[],
|
|
630
|
+
* checks: object[],
|
|
631
|
+
* configSource: string,
|
|
632
|
+
* verifierCount: number,
|
|
633
|
+
* }}
|
|
634
|
+
*/
|
|
635
|
+
function evaluateUniversalClaims(claimText, options = {}) {
|
|
636
|
+
const repoRoot = resolveRepoRoot(options.cwd);
|
|
637
|
+
const failUnconfigured = options.failUnconfigured !== false;
|
|
638
|
+
const { verifiers, source: configSource } = loadVerifierConfig(options);
|
|
639
|
+
validateConfiguredClaimTemplates(verifiers);
|
|
640
|
+
const parsed = parseFactualClaims(claimText, { verifiers });
|
|
641
|
+
if (parsed.length === 0) {
|
|
642
|
+
return {
|
|
643
|
+
verified: true,
|
|
644
|
+
claims: [],
|
|
645
|
+
checks: [],
|
|
646
|
+
configSource,
|
|
647
|
+
verifierCount: verifiers.length,
|
|
648
|
+
parsedCount: 0,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const checks = parsed.map((claim) => evaluateClaim(
|
|
652
|
+
claim,
|
|
653
|
+
verifiers,
|
|
654
|
+
repoRoot,
|
|
655
|
+
failUnconfigured,
|
|
656
|
+
));
|
|
657
|
+
const claimResults = parsed.map((claim, index) => ({ ...claim, ...checks[index] }));
|
|
658
|
+
|
|
659
|
+
return {
|
|
660
|
+
verified: checks.every((check) => check.passed),
|
|
661
|
+
claims: claimResults,
|
|
662
|
+
checks,
|
|
663
|
+
configSource,
|
|
664
|
+
verifierCount: verifiers.length,
|
|
665
|
+
parsedCount: parsed.length,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function evaluateUniversalClaimsAsGateChecks(claimText, options = {}) {
|
|
670
|
+
const result = evaluateUniversalClaims(claimText, options);
|
|
671
|
+
return {
|
|
672
|
+
...result,
|
|
673
|
+
checks: result.checks.map((check) => ({
|
|
674
|
+
claim: `universal:${check.kind}`,
|
|
675
|
+
passed: check.passed,
|
|
676
|
+
missing: check.missing || [],
|
|
677
|
+
message: check.message,
|
|
678
|
+
universal: check,
|
|
679
|
+
})),
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function parseCliArgs(argv = []) {
|
|
684
|
+
const options = {};
|
|
685
|
+
const positional = [];
|
|
686
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
687
|
+
const arg = argv[index];
|
|
688
|
+
if (arg === '--json') options.json = true;
|
|
689
|
+
else if (arg === '--claim') options.claim = argv[++index];
|
|
690
|
+
else if (arg.startsWith('--claim=')) options.claim = arg.slice('--claim='.length);
|
|
691
|
+
else if (arg === '--config') options.configPath = argv[++index];
|
|
692
|
+
else if (arg.startsWith('--config=')) options.configPath = arg.slice('--config='.length);
|
|
693
|
+
else if (arg === '--cwd') options.cwd = argv[++index];
|
|
694
|
+
else if (arg.startsWith('--cwd=')) options.cwd = arg.slice('--cwd='.length);
|
|
695
|
+
else if (arg === '--advisory') options.failUnconfigured = false;
|
|
696
|
+
else if (!arg.startsWith('--')) positional.push(arg);
|
|
697
|
+
}
|
|
698
|
+
if (!options.claim && positional.length > 0) options.claim = positional.join(' ');
|
|
699
|
+
return options;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function formatCliSummary(report) {
|
|
703
|
+
const lines = [
|
|
704
|
+
report.verified ? 'ThumbGate claim verification: PASS' : 'ThumbGate claim verification: BLOCK',
|
|
705
|
+
`Parsed claims: ${report.parsedCount}`,
|
|
706
|
+
`Verifier config: ${report.configSource}`,
|
|
707
|
+
];
|
|
708
|
+
for (const check of report.checks) {
|
|
709
|
+
lines.push(`- ${check.status}: ${check.message}`);
|
|
710
|
+
}
|
|
711
|
+
return `${lines.join('\n')}\n`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function runCli(argv = process.argv.slice(2), io = {}) {
|
|
715
|
+
const stdout = io.stdout || process.stdout;
|
|
716
|
+
const stderr = io.stderr || process.stderr;
|
|
717
|
+
const options = parseCliArgs(argv);
|
|
718
|
+
let claim = String(options.claim || '').trim();
|
|
719
|
+
if (!claim && !process.stdin.isTTY) {
|
|
720
|
+
try {
|
|
721
|
+
claim = fs.readFileSync(0, 'utf8').trim();
|
|
722
|
+
} catch {
|
|
723
|
+
claim = '';
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (!claim) {
|
|
727
|
+
stderr.write('Usage: thumbgate verify-claims --claim "the row count is 1,284" [--config path] [--json]\n');
|
|
728
|
+
return 2;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
try {
|
|
732
|
+
const report = evaluateUniversalClaims(claim, options);
|
|
733
|
+
stdout.write(options.json ? `${JSON.stringify(report, null, 2)}\n` : formatCliSummary(report));
|
|
734
|
+
return report.verified ? 0 : 1;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
const failure = {
|
|
737
|
+
verified: false,
|
|
738
|
+
status: 'evaluator_error',
|
|
739
|
+
message: error.message,
|
|
740
|
+
};
|
|
741
|
+
if (options.json) stdout.write(`${JSON.stringify(failure, null, 2)}\n`);
|
|
742
|
+
else stderr.write(`ThumbGate claim verification failed closed: ${error.message}\n`);
|
|
743
|
+
return 1;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (path.resolve(process.argv[1] || '') === path.resolve(__filename)) {
|
|
748
|
+
process.exitCode = runCli();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
module.exports = {
|
|
752
|
+
parseFactualClaims,
|
|
753
|
+
parseNumberToken,
|
|
754
|
+
loadVerifierConfig,
|
|
755
|
+
findVerifierForClaim,
|
|
756
|
+
evaluateUniversalClaims,
|
|
757
|
+
evaluateUniversalClaimsAsGateChecks,
|
|
758
|
+
parseCliArgs,
|
|
759
|
+
formatCliSummary,
|
|
760
|
+
runCli,
|
|
761
|
+
assertSelectOnly,
|
|
762
|
+
resolveSafePath,
|
|
763
|
+
pathMatches,
|
|
764
|
+
compileConfiguredClaimTemplate,
|
|
765
|
+
validateConfiguredClaimTemplates,
|
|
766
|
+
DEFAULT_VERIFIERS_FILENAME,
|
|
767
|
+
};
|