sigmap 4.2.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 +24 -0
- package/gen-context.js +307 -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,30 @@ 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
|
+
|
|
23
|
+
## [4.3.0] — 2026-04-16
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`sigmap validate`** — validates config (srcDirs exist, exclude patterns, maxTokens range), computes coverage as sig-index size / total source files, warns when coverage < 70%, exits 1 on hard errors. Optional `--query "<q>"` checks that PascalCase/camelCase symbols in the query appear in top-5 ranked context. Supports `--json`.
|
|
28
|
+
- **`sigmap --ci [--min-coverage N] [--json]`** — GitHub Actions exit gate: exits 0 when coverage ≥ threshold (default 80%), exits 1 otherwise. Uses sig-index vs source file count for a budget-aware coverage metric. Ready for `npx sigmap --ci` in CI workflows.
|
|
29
|
+
- **`extractQuerySymbols(query)`** — internal helper that extracts PascalCase and camelCase identifiers from a query string for symbol-level coverage checks in `sigmap validate`.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **`sigmap ask`** — now emits a stderr warning when coverage < 70%, pointing users to `sigmap validate` for diagnosis.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
13
37
|
## [4.2.0] — 2026-04-16
|
|
14
38
|
|
|
15
39
|
### 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) {
|
|
@@ -8004,6 +8113,10 @@ function getIntentWeights(intent) {
|
|
|
8004
8113
|
return base;
|
|
8005
8114
|
}
|
|
8006
8115
|
|
|
8116
|
+
function extractQuerySymbols(query) {
|
|
8117
|
+
return (query.match(/\b[A-Z][a-zA-Z]+|[a-z]+(?:[A-Z][a-z]+)+\b/g) || []);
|
|
8118
|
+
}
|
|
8119
|
+
|
|
8007
8120
|
function main() {
|
|
8008
8121
|
const args = process.argv.slice(2);
|
|
8009
8122
|
|
|
@@ -8132,6 +8245,9 @@ function main() {
|
|
|
8132
8245
|
riskLevel, contextPath: path.relative(cwd, outPath),
|
|
8133
8246
|
}) + '\n');
|
|
8134
8247
|
} else {
|
|
8248
|
+
if (coveragePct < 70) {
|
|
8249
|
+
process.stderr.write(`[sigmap] ⚠ coverage ${coveragePct}% — consider running: sigmap validate\n`);
|
|
8250
|
+
}
|
|
8135
8251
|
const bar = '─'.repeat(44);
|
|
8136
8252
|
console.log([
|
|
8137
8253
|
bar,
|
|
@@ -8247,6 +8363,165 @@ function main() {
|
|
|
8247
8363
|
process.exit(0);
|
|
8248
8364
|
}
|
|
8249
8365
|
|
|
8366
|
+
// v4.3: `sigmap validate` — config + coverage + optional query symbol check
|
|
8367
|
+
if (args[0] === 'validate') {
|
|
8368
|
+
const issues = [];
|
|
8369
|
+
const warnings = [];
|
|
8370
|
+
|
|
8371
|
+
// Config checks
|
|
8372
|
+
for (const d of (config.srcDirs || [])) {
|
|
8373
|
+
if (!fs.existsSync(path.join(cwd, d)))
|
|
8374
|
+
issues.push(`srcDir '${d}' does not exist`);
|
|
8375
|
+
}
|
|
8376
|
+
if ((config.exclude || []).some((p) => p === 'src/**'))
|
|
8377
|
+
issues.push(`exclude pattern 'src/**' will exclude all source files`);
|
|
8378
|
+
if ((config.maxTokens || 0) < 1000)
|
|
8379
|
+
warnings.push(`maxTokens ${config.maxTokens} is very low — consider ≥ 4000`);
|
|
8380
|
+
if ((config.maxTokens || 0) > 50000)
|
|
8381
|
+
warnings.push(`maxTokens ${config.maxTokens} is very high — may exceed LLM context windows`);
|
|
8382
|
+
|
|
8383
|
+
// Coverage check: files actually in context vs total source files
|
|
8384
|
+
const { buildSigIndex: valBuildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
8385
|
+
const valSigIndex = valBuildSigIndex(cwd);
|
|
8386
|
+
const valTotal = buildFileList(cwd, config).length;
|
|
8387
|
+
const coveragePct = valTotal > 0 ? Math.round((valSigIndex.size / valTotal) * 100) : 0;
|
|
8388
|
+
if (coveragePct < 70)
|
|
8389
|
+
warnings.push(`coverage ${coveragePct}% is below recommended 70% — increase maxTokens or expand srcDirs`);
|
|
8390
|
+
|
|
8391
|
+
// Optional query symbol check
|
|
8392
|
+
const valQueryIdx = args.indexOf('--query');
|
|
8393
|
+
if (valQueryIdx !== -1) {
|
|
8394
|
+
const q = (args[valQueryIdx + 1] || '').trim();
|
|
8395
|
+
if (q && !q.startsWith('--')) {
|
|
8396
|
+
try {
|
|
8397
|
+
const { rank, buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
8398
|
+
const ranked = rank(q, buildSigIndex(cwd), { topK: 5 });
|
|
8399
|
+
const symbols = extractQuerySymbols(q);
|
|
8400
|
+
const missing = symbols.filter((sym) =>
|
|
8401
|
+
!ranked.some((r) => r.sigs && r.sigs.some((s) => s.toLowerCase().includes(sym.toLowerCase())))
|
|
8402
|
+
);
|
|
8403
|
+
if (missing.length > 0)
|
|
8404
|
+
warnings.push(`query "${q}" references symbols not in top-5 context: ${missing.join(', ')}`);
|
|
8405
|
+
else if (symbols.length > 0)
|
|
8406
|
+
console.log(`[sigmap] ✓ query coverage OK — all ${symbols.length} symbols found`);
|
|
8407
|
+
} catch (_) {}
|
|
8408
|
+
}
|
|
8409
|
+
}
|
|
8410
|
+
|
|
8411
|
+
if (args.includes('--json')) {
|
|
8412
|
+
process.stdout.write(JSON.stringify({ valid: issues.length === 0, issues, warnings, coverage: coveragePct }) + '\n');
|
|
8413
|
+
} else {
|
|
8414
|
+
for (const w of warnings) console.warn(`[sigmap] ⚠ ${w}`);
|
|
8415
|
+
if (issues.length === 0) {
|
|
8416
|
+
console.log(`[sigmap] ✓ config valid coverage: ${coveragePct}%`);
|
|
8417
|
+
} else {
|
|
8418
|
+
for (const iss of issues) console.error(`[sigmap] ✗ ${iss}`);
|
|
8419
|
+
process.exit(1);
|
|
8420
|
+
}
|
|
8421
|
+
}
|
|
8422
|
+
process.exit(0);
|
|
8423
|
+
}
|
|
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
|
+
|
|
8250
8525
|
// Feature 6: `sigmap sync` — write all outputs + llms.txt + print compact diff
|
|
8251
8526
|
if (args[0] === 'sync') {
|
|
8252
8527
|
try {
|
|
@@ -8912,6 +9187,30 @@ function main() {
|
|
|
8912
9187
|
process.exit(0);
|
|
8913
9188
|
}
|
|
8914
9189
|
|
|
9190
|
+
// v4.3: `--ci [--min-coverage N] [--json]` — GitHub Actions exit gate
|
|
9191
|
+
if (args.includes('--ci')) {
|
|
9192
|
+
const minCovIdx = args.indexOf('--min-coverage');
|
|
9193
|
+
const minCoverage = minCovIdx !== -1 ? Math.max(0, Math.min(100, parseInt(args[minCovIdx + 1], 10) || 80)) : 80;
|
|
9194
|
+
|
|
9195
|
+
// Coverage = files actually in context / total source files
|
|
9196
|
+
const { buildSigIndex: ciBuildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
9197
|
+
const ciSigIndex = ciBuildSigIndex(cwd);
|
|
9198
|
+
const ciTotal = buildFileList(cwd, config).length;
|
|
9199
|
+
const coveragePct = ciTotal > 0 ? Math.round((ciSigIndex.size / ciTotal) * 100) : 0;
|
|
9200
|
+
|
|
9201
|
+
const pass = coveragePct >= minCoverage;
|
|
9202
|
+
|
|
9203
|
+
if (args.includes('--json')) {
|
|
9204
|
+
process.stdout.write(JSON.stringify({ pass, coverage: coveragePct, threshold: minCoverage }) + '\n');
|
|
9205
|
+
} else if (pass) {
|
|
9206
|
+
console.log(`[sigmap] ✓ CI gate passed — coverage ${coveragePct}% ≥ ${minCoverage}%`);
|
|
9207
|
+
} else {
|
|
9208
|
+
console.error(`[sigmap] ✗ CI gate FAILED — coverage ${coveragePct}% < ${minCoverage}%`);
|
|
9209
|
+
console.error(` Fix: increase maxTokens or expand srcDirs in gen-context.config.json`);
|
|
9210
|
+
}
|
|
9211
|
+
process.exit(pass ? 0 : 1);
|
|
9212
|
+
}
|
|
9213
|
+
|
|
8915
9214
|
// Default: generate once
|
|
8916
9215
|
runGenerate(cwd, config, false);
|
|
8917
9216
|
}
|
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