sigmap 4.3.0 → 5.0.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/CHANGELOG.md +10 -0
- package/gen-context.js +217 -8
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/loader.js +77 -5
- package/src/judge/judge-engine.js +55 -0
- package/src/mcp/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [5.0.0] — 2026-04-16
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`sigmap judge --response <file> --context <file>`** — rule-based groundedness scoring engine (`src/judge/judge-engine.js`). Computes a 0–1 score from token overlap between an LLM response and its source context. Exits 0 when verdict is `pass`, exits 1 on `fail`. Supports `--json` (emits `{ score, verdict, reasons }`) and `--threshold` override.
|
|
18
|
+
- **Config `extends`** — `gen-context.config.json` now accepts an `"extends"` key pointing to a local JSON file path or HTTPS URL. The base config is deep-merged (DEFAULTS → base → local), with HTTPS responses cached for 1 hour in `.context/config-cache/`.
|
|
19
|
+
- **`sigmap history [--last N] [--json]`** — displays last N usage log entries as a table with a Unicode sparkline (▁▂▃▄▅▆▇█) for the token trend. Reads from `.context/usage.ndjson` (requires `tracking: true` in config).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
13
23
|
## [4.3.0] — 2026-04-16
|
|
14
24
|
|
|
15
25
|
### Added
|
package/gen-context.js
CHANGED
|
@@ -221,6 +221,47 @@ __factories["./src/config/loader"] = function(module, exports) {
|
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
const BASE_CONFIG_TTL_MS = 60 * 60 * 1000;
|
|
225
|
+
|
|
226
|
+
function loadBaseConfig(extendsVal, cwd) {
|
|
227
|
+
if (!extendsVal || typeof extendsVal !== 'string') return {};
|
|
228
|
+
if (extendsVal.startsWith('https://') || extendsVal.startsWith('http://')) {
|
|
229
|
+
const cacheDir = path.join(cwd, '.context', 'config-cache');
|
|
230
|
+
const cacheKey = Buffer.from(extendsVal).toString('base64').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
231
|
+
const cachePath = path.join(cacheDir, `${cacheKey}.json`);
|
|
232
|
+
if (fs.existsSync(cachePath)) {
|
|
233
|
+
const age = Date.now() - fs.statSync(cachePath).mtimeMs;
|
|
234
|
+
if (age < BASE_CONFIG_TTL_MS) {
|
|
235
|
+
try { return JSON.parse(fs.readFileSync(cachePath, 'utf8')); } catch (_) {}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const { execSync } = require('child_process');
|
|
240
|
+
const proto = extendsVal.startsWith('https') ? 'https' : 'http';
|
|
241
|
+
const out = execSync(
|
|
242
|
+
`node -e "const h=require('${proto}');let d='';h.get(${JSON.stringify(extendsVal)},r=>{r.on('data',c=>d+=c);r.on('end',()=>process.stdout.write(d))}).on('error',()=>process.exit(1))"`,
|
|
243
|
+
{ timeout: 10000, encoding: 'utf8' }
|
|
244
|
+
);
|
|
245
|
+
const parsed = JSON.parse(out);
|
|
246
|
+
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
247
|
+
fs.writeFileSync(cachePath, JSON.stringify(parsed), 'utf8');
|
|
248
|
+
return parsed;
|
|
249
|
+
} catch (err) {
|
|
250
|
+
process.stderr.write(`[sigmap] config extends: could not fetch ${extendsVal}: ${err.message}\n`);
|
|
251
|
+
if (fs.existsSync(cachePath)) {
|
|
252
|
+
try { return JSON.parse(fs.readFileSync(cachePath, 'utf8')); } catch (_) {}
|
|
253
|
+
}
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const absPath = path.resolve(cwd, extendsVal);
|
|
258
|
+
try { return JSON.parse(fs.readFileSync(absPath, 'utf8')); }
|
|
259
|
+
catch (err) {
|
|
260
|
+
process.stderr.write(`[sigmap] config extends: could not load ${absPath}: ${err.message}\n`);
|
|
261
|
+
return {};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
224
265
|
/**
|
|
225
266
|
* Load and merge configuration for a given working directory.
|
|
226
267
|
*
|
|
@@ -250,18 +291,31 @@ __factories["./src/config/loader"] = function(module, exports) {
|
|
|
250
291
|
|
|
251
292
|
// Warn on unknown keys (helps catch typos)
|
|
252
293
|
for (const key of Object.keys(userConfig)) {
|
|
253
|
-
if (key.startsWith('_')
|
|
294
|
+
if (key.startsWith('_') || key === 'extends') continue;
|
|
254
295
|
if (!KNOWN_KEYS.has(key)) {
|
|
255
296
|
console.warn(`[sigmap] unknown config key: "${key}" (ignored)`);
|
|
256
297
|
}
|
|
257
298
|
}
|
|
258
299
|
|
|
259
|
-
// Deep merge:
|
|
260
|
-
|
|
300
|
+
// Deep merge: DEFAULTS → base (extends) → user config
|
|
301
|
+
const baseConfig = loadBaseConfig(userConfig.extends, cwd);
|
|
261
302
|
const merged = deepClone(DEFAULTS);
|
|
303
|
+
|
|
304
|
+
for (const key of Object.keys(baseConfig)) {
|
|
305
|
+
if (key.startsWith('_') || key === 'extends') continue;
|
|
306
|
+
if (!KNOWN_KEYS.has(key)) continue;
|
|
307
|
+
const val = baseConfig[key];
|
|
308
|
+
if (val !== null && typeof val === 'object' && !Array.isArray(val) &&
|
|
309
|
+
typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
|
|
310
|
+
merged[key] = Object.assign({}, merged[key], val);
|
|
311
|
+
} else {
|
|
312
|
+
merged[key] = val;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
262
316
|
for (const key of Object.keys(userConfig)) {
|
|
263
|
-
if (key.startsWith('_')) continue;
|
|
264
|
-
if (!KNOWN_KEYS.has(key)) continue;
|
|
317
|
+
if (key.startsWith('_') || key === 'extends') continue;
|
|
318
|
+
if (!KNOWN_KEYS.has(key)) continue;
|
|
265
319
|
const val = userConfig[key];
|
|
266
320
|
if (val !== null && typeof val === 'object' && !Array.isArray(val) &&
|
|
267
321
|
typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
|
|
@@ -292,7 +346,7 @@ __factories["./src/config/loader"] = function(module, exports) {
|
|
|
292
346
|
return JSON.parse(JSON.stringify(obj));
|
|
293
347
|
}
|
|
294
348
|
|
|
295
|
-
module.exports = { loadConfig, detectAutoSrcDirs };
|
|
349
|
+
module.exports = { loadConfig, loadBaseConfig, detectAutoSrcDirs };
|
|
296
350
|
|
|
297
351
|
};
|
|
298
352
|
|
|
@@ -4654,7 +4708,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
4654
4708
|
|
|
4655
4709
|
const SERVER_INFO = {
|
|
4656
4710
|
name: 'sigmap',
|
|
4657
|
-
version: '
|
|
4711
|
+
version: '5.0.0',
|
|
4658
4712
|
description: 'SigMap MCP server — code signatures on demand',
|
|
4659
4713
|
};
|
|
4660
4714
|
|
|
@@ -5252,6 +5306,61 @@ __factories["./src/security/scanner"] = function(module, exports) {
|
|
|
5252
5306
|
|
|
5253
5307
|
};
|
|
5254
5308
|
|
|
5309
|
+
// ── ./src/judge/judge-engine ──
|
|
5310
|
+
__factories["./src/judge/judge-engine"] = function(module, exports) {
|
|
5311
|
+
'use strict';
|
|
5312
|
+
|
|
5313
|
+
const STOP = new Set([
|
|
5314
|
+
'the','a','an','in','on','at','to','of','for','and','or','but',
|
|
5315
|
+
'is','are','was','were','be','been','being','have','has','had',
|
|
5316
|
+
'do','does','did','will','would','could','should','may','might',
|
|
5317
|
+
'shall','can','not','with','from','by','as','this','that','it',
|
|
5318
|
+
]);
|
|
5319
|
+
|
|
5320
|
+
function tokenize(text) {
|
|
5321
|
+
return (text || '').toLowerCase().match(/\b[a-z][a-z0-9_]{2,}\b/g) || [];
|
|
5322
|
+
}
|
|
5323
|
+
|
|
5324
|
+
function groundedness(response, context) {
|
|
5325
|
+
if (!response || !context) return 0;
|
|
5326
|
+
const ctxTokens = new Set(tokenize(context).filter((t) => !STOP.has(t)));
|
|
5327
|
+
if (ctxTokens.size === 0) return 0;
|
|
5328
|
+
const respTokens = tokenize(response).filter((t) => !STOP.has(t));
|
|
5329
|
+
if (respTokens.length === 0) return 0;
|
|
5330
|
+
const matched = respTokens.filter((t) => ctxTokens.has(t));
|
|
5331
|
+
return parseFloat((matched.length / respTokens.length).toFixed(3));
|
|
5332
|
+
}
|
|
5333
|
+
|
|
5334
|
+
const GENERIC_MARKERS = [
|
|
5335
|
+
'however, based on my knowledge',
|
|
5336
|
+
'generally speaking',
|
|
5337
|
+
'in general',
|
|
5338
|
+
'typically,',
|
|
5339
|
+
'usually,',
|
|
5340
|
+
'as a general rule',
|
|
5341
|
+
];
|
|
5342
|
+
|
|
5343
|
+
function judge(response, context, opts) {
|
|
5344
|
+
opts = opts || {};
|
|
5345
|
+
const score = groundedness(response, context);
|
|
5346
|
+
const threshold = opts.threshold !== undefined ? opts.threshold : 0.25;
|
|
5347
|
+
const reasons = [];
|
|
5348
|
+
if (score < threshold) {
|
|
5349
|
+
reasons.push(`score ${score} is below threshold ${threshold} — response may not be grounded in context`);
|
|
5350
|
+
}
|
|
5351
|
+
if (response) {
|
|
5352
|
+
const lower = response.toLowerCase();
|
|
5353
|
+
for (const m of GENERIC_MARKERS) {
|
|
5354
|
+
if (lower.includes(m)) reasons.push(`response contains generic phrase: "${m}"`);
|
|
5355
|
+
}
|
|
5356
|
+
}
|
|
5357
|
+
const verdict = score >= threshold && reasons.length === 0 ? 'pass' : 'fail';
|
|
5358
|
+
return { score, verdict, reasons };
|
|
5359
|
+
}
|
|
5360
|
+
|
|
5361
|
+
module.exports = { groundedness, judge };
|
|
5362
|
+
};
|
|
5363
|
+
|
|
5255
5364
|
// ── ./src/tracking/logger ──
|
|
5256
5365
|
__factories["./src/tracking/logger"] = function(module, exports) {
|
|
5257
5366
|
|
|
@@ -6262,7 +6371,7 @@ const path = require('path');
|
|
|
6262
6371
|
const os = require('os');
|
|
6263
6372
|
const { execSync } = require('child_process');
|
|
6264
6373
|
|
|
6265
|
-
const VERSION = '
|
|
6374
|
+
const VERSION = '5.0.0';
|
|
6266
6375
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
6267
6376
|
|
|
6268
6377
|
function requireSourceOrBundled(key) {
|
|
@@ -8313,6 +8422,106 @@ function main() {
|
|
|
8313
8422
|
process.exit(0);
|
|
8314
8423
|
}
|
|
8315
8424
|
|
|
8425
|
+
// v5.0: `sigmap judge --response <file> --context <file>` — groundedness scoring
|
|
8426
|
+
if (args[0] === 'judge') {
|
|
8427
|
+
const respIdx = args.indexOf('--response');
|
|
8428
|
+
const ctxIdx = args.indexOf('--context');
|
|
8429
|
+
|
|
8430
|
+
if (respIdx < 0 || ctxIdx < 0) {
|
|
8431
|
+
console.error('[sigmap] Usage: sigmap judge --response <file> --context <file> [--json] [--threshold 0.25]');
|
|
8432
|
+
process.exit(1);
|
|
8433
|
+
}
|
|
8434
|
+
|
|
8435
|
+
const respFile = (args[respIdx + 1] || '').trim();
|
|
8436
|
+
const ctxFile = (args[ctxIdx + 1] || '').trim();
|
|
8437
|
+
|
|
8438
|
+
if (!respFile || respFile.startsWith('--') || !ctxFile || ctxFile.startsWith('--')) {
|
|
8439
|
+
console.error('[sigmap] --response and --context require file paths');
|
|
8440
|
+
process.exit(1);
|
|
8441
|
+
}
|
|
8442
|
+
|
|
8443
|
+
let responseText = '', contextText = '';
|
|
8444
|
+
try { responseText = fs.readFileSync(path.resolve(cwd, respFile), 'utf8'); }
|
|
8445
|
+
catch (e) { console.error(`[sigmap] cannot read --response file: ${e.message}`); process.exit(1); }
|
|
8446
|
+
try { contextText = fs.readFileSync(path.resolve(cwd, ctxFile), 'utf8'); }
|
|
8447
|
+
catch (e) { console.error(`[sigmap] cannot read --context file: ${e.message}`); process.exit(1); }
|
|
8448
|
+
|
|
8449
|
+
const thrIdx = args.indexOf('--threshold');
|
|
8450
|
+
const judgeOpts = thrIdx >= 0 ? { threshold: parseFloat(args[thrIdx + 1]) || 0.25 } : {};
|
|
8451
|
+
|
|
8452
|
+
const { judge: runJudge } = requireSourceOrBundled('./src/judge/judge-engine');
|
|
8453
|
+
const result = runJudge(responseText, contextText, judgeOpts);
|
|
8454
|
+
|
|
8455
|
+
if (args.includes('--json')) {
|
|
8456
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
8457
|
+
} else {
|
|
8458
|
+
const bar = '─'.repeat(44);
|
|
8459
|
+
console.log([
|
|
8460
|
+
bar,
|
|
8461
|
+
` sigmap judge`,
|
|
8462
|
+
` Score : ${result.score}`,
|
|
8463
|
+
` Verdict : ${result.verdict}`,
|
|
8464
|
+
result.reasons.length ? ` Reasons :\n ${result.reasons.join('\n ')}` : ` Reasons : none`,
|
|
8465
|
+
bar,
|
|
8466
|
+
].join('\n'));
|
|
8467
|
+
}
|
|
8468
|
+
process.exit(result.verdict === 'pass' ? 0 : 1);
|
|
8469
|
+
}
|
|
8470
|
+
|
|
8471
|
+
// v5.0: `sigmap history` — show last N usage log entries with sparkline
|
|
8472
|
+
if (args[0] === 'history') {
|
|
8473
|
+
const { readLog } = requireSourceOrBundled('./src/tracking/logger');
|
|
8474
|
+
const entries = readLog(cwd);
|
|
8475
|
+
|
|
8476
|
+
const nIdx = args.indexOf('--last');
|
|
8477
|
+
const n = nIdx >= 0 ? (parseInt(args[nIdx + 1], 10) || 10) : 10;
|
|
8478
|
+
const last = entries.slice(-n);
|
|
8479
|
+
|
|
8480
|
+
if (args.includes('--json')) {
|
|
8481
|
+
process.stdout.write(JSON.stringify(last) + '\n');
|
|
8482
|
+
process.exit(0);
|
|
8483
|
+
}
|
|
8484
|
+
|
|
8485
|
+
if (last.length === 0) {
|
|
8486
|
+
console.log('[sigmap] No history found. Run sigmap to generate entries (enable tracking: true in config).');
|
|
8487
|
+
process.exit(0);
|
|
8488
|
+
}
|
|
8489
|
+
|
|
8490
|
+
const SPARK_CHARS = '▁▂▃▄▅▆▇█';
|
|
8491
|
+
function sparkline(values) {
|
|
8492
|
+
if (values.length === 0) return '';
|
|
8493
|
+
const min = Math.min(...values);
|
|
8494
|
+
const max = Math.max(...values);
|
|
8495
|
+
const range = max - min || 1;
|
|
8496
|
+
return values.map((v) => {
|
|
8497
|
+
const idx = Math.round(((v - min) / range) * (SPARK_CHARS.length - 1));
|
|
8498
|
+
return SPARK_CHARS[idx];
|
|
8499
|
+
}).join('');
|
|
8500
|
+
}
|
|
8501
|
+
|
|
8502
|
+
const tokens = last.map((e) => e.finalTokens || 0);
|
|
8503
|
+
const spark = sparkline(tokens);
|
|
8504
|
+
|
|
8505
|
+
const bar = '─'.repeat(62);
|
|
8506
|
+
console.log(bar);
|
|
8507
|
+
console.log(` sigmap history (last ${last.length} runs)`);
|
|
8508
|
+
console.log(bar);
|
|
8509
|
+
console.log(` ${'Date'.padEnd(24)} ${'Files'.padStart(5)} ${'Tokens'.padStart(7)} ${'Reduction'.padStart(9)} ${'Budget?'.padStart(7)}`);
|
|
8510
|
+
console.log(` ${'─'.repeat(24)} ${'─'.repeat(5)} ${'─'.repeat(7)} ${'─'.repeat(9)} ${'─'.repeat(7)}`);
|
|
8511
|
+
for (const e of last) {
|
|
8512
|
+
const date = (e.ts || '').slice(0, 19).replace('T', ' ');
|
|
8513
|
+
const files = String(e.fileCount || 0).padStart(5);
|
|
8514
|
+
const tok = String(e.finalTokens || 0).padStart(7);
|
|
8515
|
+
const red = `${e.reductionPct || 0}%`.padStart(9);
|
|
8516
|
+
const over = (e.overBudget ? ' ⚠ yes' : ' no').padStart(7);
|
|
8517
|
+
console.log(` ${date.padEnd(24)} ${files} ${tok} ${red} ${over}`);
|
|
8518
|
+
}
|
|
8519
|
+
console.log(bar);
|
|
8520
|
+
console.log(` Token trend: ${spark}`);
|
|
8521
|
+
console.log(bar);
|
|
8522
|
+
process.exit(0);
|
|
8523
|
+
}
|
|
8524
|
+
|
|
8316
8525
|
// Feature 6: `sigmap sync` — write all outputs + llms.txt + print compact diff
|
|
8317
8526
|
if (args[0] === 'sync') {
|
|
8318
8527
|
try {
|
package/package.json
CHANGED
package/src/config/loader.js
CHANGED
|
@@ -4,6 +4,65 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { DEFAULTS } = require('./defaults');
|
|
6
6
|
|
|
7
|
+
const BASE_CONFIG_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
8
|
+
|
|
9
|
+
function loadBaseConfig(extendsVal, cwd) {
|
|
10
|
+
if (!extendsVal || typeof extendsVal !== 'string') return {};
|
|
11
|
+
|
|
12
|
+
if (extendsVal.startsWith('https://') || extendsVal.startsWith('http://')) {
|
|
13
|
+
const cacheDir = path.join(cwd, '.context', 'config-cache');
|
|
14
|
+
const cacheKey = Buffer.from(extendsVal).toString('base64url').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
15
|
+
const cachePath = path.join(cacheDir, `${cacheKey}.json`);
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(cachePath)) {
|
|
18
|
+
const age = Date.now() - fs.statSync(cachePath).mtimeMs;
|
|
19
|
+
if (age < BASE_CONFIG_TTL_MS) {
|
|
20
|
+
try { return JSON.parse(fs.readFileSync(cachePath, 'utf8')); } catch (_) {}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const https = require('https');
|
|
26
|
+
const http = require('http');
|
|
27
|
+
const mod = extendsVal.startsWith('https://') ? https : http;
|
|
28
|
+
const raw = (() => {
|
|
29
|
+
let data = '';
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
mod.get(extendsVal, (res) => {
|
|
32
|
+
res.on('data', (c) => { data += c; });
|
|
33
|
+
res.on('end', () => resolve(data));
|
|
34
|
+
}).on('error', reject);
|
|
35
|
+
});
|
|
36
|
+
})();
|
|
37
|
+
// sync fallback: use execSync with node -e
|
|
38
|
+
const { execSync } = require('child_process');
|
|
39
|
+
const out = execSync(
|
|
40
|
+
`node -e "const h=require('${extendsVal.startsWith('https') ? 'https' : 'http'}');let d='';h.get(${JSON.stringify(extendsVal)},r=>{r.on('data',c=>d+=c);r.on('end',()=>process.stdout.write(d))}).on('error',()=>process.exit(1))"`,
|
|
41
|
+
{ timeout: 10000, encoding: 'utf8' }
|
|
42
|
+
);
|
|
43
|
+
const parsed = JSON.parse(out);
|
|
44
|
+
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
|
|
45
|
+
fs.writeFileSync(cachePath, JSON.stringify(parsed), 'utf8');
|
|
46
|
+
return parsed;
|
|
47
|
+
} catch (err) {
|
|
48
|
+
process.stderr.write(`[sigmap] config extends: could not fetch ${extendsVal}: ${err.message}\n`);
|
|
49
|
+
if (fs.existsSync(cachePath)) {
|
|
50
|
+
try { return JSON.parse(fs.readFileSync(cachePath, 'utf8')); } catch (_) {}
|
|
51
|
+
}
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Local file path
|
|
57
|
+
const absPath = path.resolve(cwd, extendsVal);
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(fs.readFileSync(absPath, 'utf8'));
|
|
60
|
+
} catch (err) {
|
|
61
|
+
process.stderr.write(`[sigmap] config extends: could not load ${absPath}: ${err.message}\n`);
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
7
66
|
// Keys that are valid in gen-context.config.json
|
|
8
67
|
const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
|
|
9
68
|
|
|
@@ -173,17 +232,30 @@ function loadConfig(cwd) {
|
|
|
173
232
|
|
|
174
233
|
// Warn on unknown keys (helps catch typos)
|
|
175
234
|
for (const key of Object.keys(userConfig)) {
|
|
176
|
-
if (key.startsWith('_')
|
|
235
|
+
if (key.startsWith('_') || key === 'extends') continue;
|
|
177
236
|
if (!KNOWN_KEYS.has(key)) {
|
|
178
237
|
console.warn(`[sigmap] unknown config key: "${key}" (ignored)`);
|
|
179
238
|
}
|
|
180
239
|
}
|
|
181
240
|
|
|
182
|
-
// Deep merge:
|
|
183
|
-
|
|
241
|
+
// Deep merge: DEFAULTS → base (extends) → user config
|
|
242
|
+
const baseConfig = loadBaseConfig(userConfig.extends, cwd);
|
|
184
243
|
const merged = deepClone(DEFAULTS);
|
|
244
|
+
|
|
245
|
+
for (const key of Object.keys(baseConfig)) {
|
|
246
|
+
if (key.startsWith('_') || key === 'extends') continue;
|
|
247
|
+
if (!KNOWN_KEYS.has(key)) continue;
|
|
248
|
+
const val = baseConfig[key];
|
|
249
|
+
if (val !== null && typeof val === 'object' && !Array.isArray(val) &&
|
|
250
|
+
typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
|
|
251
|
+
merged[key] = Object.assign({}, merged[key], val);
|
|
252
|
+
} else {
|
|
253
|
+
merged[key] = val;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
185
257
|
for (const key of Object.keys(userConfig)) {
|
|
186
|
-
if (key.startsWith('_')) continue;
|
|
258
|
+
if (key.startsWith('_') || key === 'extends') continue;
|
|
187
259
|
if (!KNOWN_KEYS.has(key)) continue; // skip unknown keys
|
|
188
260
|
const val = userConfig[key];
|
|
189
261
|
if (val !== null && typeof val === 'object' && !Array.isArray(val) &&
|
|
@@ -214,4 +286,4 @@ function deepClone(obj) {
|
|
|
214
286
|
return JSON.parse(JSON.stringify(obj));
|
|
215
287
|
}
|
|
216
288
|
|
|
217
|
-
module.exports = { loadConfig };
|
|
289
|
+
module.exports = { loadConfig, loadBaseConfig };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const STOP = new Set([
|
|
4
|
+
'the','a','an','in','on','at','to','of','for','and','or','but',
|
|
5
|
+
'is','are','was','were','be','been','being','have','has','had',
|
|
6
|
+
'do','does','did','will','would','could','should','may','might',
|
|
7
|
+
'shall','can','not','with','from','by','as','this','that','it',
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
function tokenize(text) {
|
|
11
|
+
return (text || '').toLowerCase().match(/\b[a-z][a-z0-9_]{2,}\b/g) || [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function groundedness(response, context) {
|
|
15
|
+
if (!response || !context) return 0;
|
|
16
|
+
const ctxTokens = new Set(tokenize(context).filter((t) => !STOP.has(t)));
|
|
17
|
+
if (ctxTokens.size === 0) return 0;
|
|
18
|
+
const respTokens = tokenize(response).filter((t) => !STOP.has(t));
|
|
19
|
+
if (respTokens.length === 0) return 0;
|
|
20
|
+
const matched = respTokens.filter((t) => ctxTokens.has(t));
|
|
21
|
+
return parseFloat((matched.length / respTokens.length).toFixed(3));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const GENERIC_MARKERS = [
|
|
25
|
+
'however, based on my knowledge',
|
|
26
|
+
'generally speaking',
|
|
27
|
+
'in general',
|
|
28
|
+
'typically,',
|
|
29
|
+
'usually,',
|
|
30
|
+
'as a general rule',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
function judge(response, context, opts = {}) {
|
|
34
|
+
const score = groundedness(response, context);
|
|
35
|
+
const threshold = opts.threshold !== undefined ? opts.threshold : 0.25;
|
|
36
|
+
const reasons = [];
|
|
37
|
+
|
|
38
|
+
if (score < threshold) {
|
|
39
|
+
reasons.push(`score ${score} is below threshold ${threshold} — response may not be grounded in context`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (response) {
|
|
43
|
+
const lower = response.toLowerCase();
|
|
44
|
+
for (const m of GENERIC_MARKERS) {
|
|
45
|
+
if (lower.includes(m)) {
|
|
46
|
+
reasons.push(`response contains generic phrase: "${m}"`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const verdict = score >= threshold && reasons.length === 0 ? 'pass' : 'fail';
|
|
52
|
+
return { score, verdict, reasons };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { groundedness, judge };
|
package/src/mcp/server.js
CHANGED