sigmap 5.1.0 → 5.3.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/gen-context.js CHANGED
@@ -4684,7 +4684,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
4684
4684
  const index = buildSigIndex(cwd);
4685
4685
  if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
4686
4686
  const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
4687
- const results = rank(args.query, index, { topK });
4687
+ const results = rank(args.query, index, { topK, cwd });
4688
4688
  return formatRankTable(results, args.query);
4689
4689
  } catch (err) {
4690
4690
  return `_query_context failed: ${err.message}_`;
@@ -4706,6 +4706,132 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
4706
4706
  module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact };
4707
4707
  };
4708
4708
 
4709
+ // ── ./src/learning/weights ──
4710
+ __factories["./src/learning/weights"] = function(module, exports) {
4711
+ 'use strict';
4712
+
4713
+ const fs = require('fs');
4714
+ const path = require('path');
4715
+
4716
+ const DECAY = 0.95;
4717
+ const MAX_MULT = 3.0;
4718
+ const MIN_MULT = 0.30;
4719
+ const BASELINE = 1.0;
4720
+
4721
+ function weightsPath(cwd) {
4722
+ return path.join(cwd, '.context', 'weights.json');
4723
+ }
4724
+
4725
+ function clampMultiplier(value) {
4726
+ if (!Number.isFinite(value)) return BASELINE;
4727
+ if (value > MAX_MULT) return MAX_MULT;
4728
+ if (value < MIN_MULT) return MIN_MULT;
4729
+ return parseFloat(value.toFixed(6));
4730
+ }
4731
+
4732
+ function normalizeFile(cwd, filePath) {
4733
+ if (!cwd || !filePath || typeof filePath !== 'string') return null;
4734
+ const cleaned = filePath.trim().replace(/\\/g, '/');
4735
+ if (!cleaned) return null;
4736
+ const abs = path.resolve(cwd, cleaned);
4737
+ const rel = path.relative(cwd, abs);
4738
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
4739
+ return rel.split(path.sep).join('/');
4740
+ }
4741
+
4742
+ function sanitizeWeights(cwd, weights) {
4743
+ const out = {};
4744
+ const entries = weights && typeof weights === 'object' ? Object.entries(weights) : [];
4745
+ for (const [filePath, raw] of entries) {
4746
+ const normalized = normalizeFile(cwd, filePath);
4747
+ if (!normalized) continue;
4748
+ const mult = clampMultiplier(Number(raw));
4749
+ if (Math.abs(mult - BASELINE) < 1e-9) continue;
4750
+ out[normalized] = mult;
4751
+ }
4752
+ return out;
4753
+ }
4754
+
4755
+ function loadWeights(cwd) {
4756
+ try {
4757
+ const parsed = JSON.parse(fs.readFileSync(weightsPath(cwd), 'utf8'));
4758
+ return sanitizeWeights(cwd, parsed);
4759
+ } catch (_) {
4760
+ return {};
4761
+ }
4762
+ }
4763
+
4764
+ function saveWeights(cwd, weights) {
4765
+ const cleaned = sanitizeWeights(cwd, weights);
4766
+ const outPath = weightsPath(cwd);
4767
+ if (Object.keys(cleaned).length === 0) {
4768
+ try {
4769
+ if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
4770
+ } catch (_) {}
4771
+ return;
4772
+ }
4773
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
4774
+ const sorted = Object.keys(cleaned).sort().reduce((acc, key) => {
4775
+ acc[key] = cleaned[key];
4776
+ return acc;
4777
+ }, {});
4778
+ fs.writeFileSync(outPath, JSON.stringify(sorted, null, 2) + '\n', 'utf8');
4779
+ }
4780
+
4781
+ function updateWeights(cwd, opts) {
4782
+ opts = opts || {};
4783
+ const goodAmount = Number.isFinite(opts.goodAmount) ? opts.goodAmount : 0.15;
4784
+ const badAmount = Number.isFinite(opts.badAmount) ? opts.badAmount : 0.10;
4785
+ const goodFiles = Array.isArray(opts.goodFiles) ? opts.goodFiles : [];
4786
+ const badFiles = Array.isArray(opts.badFiles) ? opts.badFiles : [];
4787
+ const weights = loadWeights(cwd);
4788
+
4789
+ for (const key of Object.keys(weights)) {
4790
+ weights[key] = clampMultiplier(weights[key] * DECAY);
4791
+ }
4792
+
4793
+ const good = [];
4794
+ const bad = [];
4795
+
4796
+ for (const filePath of goodFiles) {
4797
+ const normalized = normalizeFile(cwd, filePath);
4798
+ if (!normalized) continue;
4799
+ weights[normalized] = clampMultiplier((weights[normalized] || BASELINE) + goodAmount);
4800
+ good.push(normalized);
4801
+ }
4802
+
4803
+ for (const filePath of badFiles) {
4804
+ const normalized = normalizeFile(cwd, filePath);
4805
+ if (!normalized) continue;
4806
+ weights[normalized] = clampMultiplier((weights[normalized] || BASELINE) - badAmount);
4807
+ bad.push(normalized);
4808
+ }
4809
+
4810
+ saveWeights(cwd, weights);
4811
+ return { good, bad, weights: loadWeights(cwd) };
4812
+ }
4813
+
4814
+ function boostFiles(cwd, files, amount) {
4815
+ return updateWeights(cwd, { goodFiles: files, goodAmount: amount });
4816
+ }
4817
+
4818
+ function penalizeFiles(cwd, files, amount) {
4819
+ return updateWeights(cwd, { badFiles: files, badAmount: amount });
4820
+ }
4821
+
4822
+ function resetWeights(cwd) {
4823
+ const outPath = weightsPath(cwd);
4824
+ if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
4825
+ }
4826
+
4827
+ module.exports = {
4828
+ BASELINE, DECAY, MAX_MULT, MIN_MULT,
4829
+ weightsPath, clampMultiplier, normalizeFile,
4830
+ loadWeights, saveWeights, updateWeights,
4831
+ boostFiles, penalizeFiles, resetWeights,
4832
+ };
4833
+ };
4834
+
4709
4835
  // ── ./src/mcp/server ──
4710
4836
  __factories["./src/mcp/server"] = function(module, exports) {
4711
4837
 
@@ -4727,7 +4853,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
4727
4853
 
4728
4854
  const SERVER_INFO = {
4729
4855
  name: 'sigmap',
4730
- version: '5.1.0',
4856
+ version: '5.3.0',
4731
4857
  description: 'SigMap MCP server — code signatures on demand',
4732
4858
  };
4733
4859
 
@@ -5329,6 +5455,10 @@ __factories["./src/security/scanner"] = function(module, exports) {
5329
5455
  __factories["./src/judge/judge-engine"] = function(module, exports) {
5330
5456
  'use strict';
5331
5457
 
5458
+ const fs = require('fs');
5459
+ const path = require('path');
5460
+ const { boostFiles, normalizeFile, penalizeFiles } = __require('./src/learning/weights');
5461
+
5332
5462
  const STOP = new Set([
5333
5463
  'the','a','an','in','on','at','to','of','for','and','or','but',
5334
5464
  'is','are','was','were','be','been','being','have','has','had',
@@ -5359,6 +5489,24 @@ __factories["./src/judge/judge-engine"] = function(module, exports) {
5359
5489
  'as a general rule',
5360
5490
  ];
5361
5491
 
5492
+ function extractContextFiles(context, cwd) {
5493
+ if (!context || !cwd) return [];
5494
+ const seen = new Set();
5495
+ const files = [];
5496
+ const lines = context.split('\n');
5497
+ for (const line of lines) {
5498
+ const match = line.match(/^#{2,3}\s+(.+?)\s*$/);
5499
+ if (!match) continue;
5500
+ const normalized = normalizeFile(cwd, match[1]);
5501
+ if (!normalized) continue;
5502
+ const abs = path.join(cwd, normalized);
5503
+ if (!fs.existsSync(abs) || seen.has(normalized)) continue;
5504
+ seen.add(normalized);
5505
+ files.push(normalized);
5506
+ }
5507
+ return files;
5508
+ }
5509
+
5362
5510
  function judge(response, context, opts) {
5363
5511
  opts = opts || {};
5364
5512
  const score = groundedness(response, context);
@@ -5374,7 +5522,37 @@ __factories["./src/judge/judge-engine"] = function(module, exports) {
5374
5522
  }
5375
5523
  }
5376
5524
  const verdict = score >= threshold && reasons.length === 0 ? 'pass' : 'fail';
5377
- return { score, verdict, reasons };
5525
+ const result = { score, verdict, reasons };
5526
+
5527
+ if (opts.learn) {
5528
+ const learning = { applied: false, action: 'none', files: [] };
5529
+ if (!opts.cwd) {
5530
+ learning.reason = 'cwd is required for learning';
5531
+ result.learning = learning;
5532
+ return result;
5533
+ }
5534
+ const contextFiles = extractContextFiles(context, opts.cwd);
5535
+ learning.files = contextFiles;
5536
+ if (contextFiles.length === 0) {
5537
+ learning.reason = 'no context files found in context headings';
5538
+ result.learning = learning;
5539
+ return result;
5540
+ }
5541
+ if (score > 0.75) {
5542
+ boostFiles(opts.cwd, contextFiles, 0.05);
5543
+ learning.applied = true;
5544
+ learning.action = 'boost';
5545
+ } else if (score < 0.40) {
5546
+ penalizeFiles(opts.cwd, contextFiles, 0.03);
5547
+ learning.applied = true;
5548
+ learning.action = 'penalize';
5549
+ } else {
5550
+ learning.reason = 'groundedness in no-op band (0.40-0.75)';
5551
+ }
5552
+ result.learning = learning;
5553
+ }
5554
+
5555
+ return result;
5378
5556
  }
5379
5557
 
5380
5558
  module.exports = { groundedness, judge };
@@ -5529,6 +5707,7 @@ __factories["./src/retrieval/tokenizer"] = function(module, exports) {
5529
5707
  // ── ./src/retrieval/ranker ──
5530
5708
  __factories["./src/retrieval/ranker"] = function(module, exports) {
5531
5709
  'use strict';
5710
+ const { loadWeights } = __require('./src/learning/weights');
5532
5711
  const { tokenize, STOP_WORDS } = __require('./src/retrieval/tokenizer');
5533
5712
  const DEFAULT_WEIGHTS = {
5534
5713
  exactToken: 1.0, symbolMatch: 0.5, prefixMatch: 0.3, pathMatch: 0.8, recencyBoost: 1.5,
@@ -5561,6 +5740,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5561
5740
  const recencyMultiplier = (opts && opts.recencyBoost) || DEFAULT_WEIGHTS.recencyBoost;
5562
5741
  const recencySet = (opts && opts.recencySet) || null;
5563
5742
  const weights = (opts && opts.weights) ? Object.assign({}, DEFAULT_WEIGHTS, opts.weights) : DEFAULT_WEIGHTS;
5743
+ const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;
5564
5744
  const queryTokens = tokenize(query);
5565
5745
  if (queryTokens.length === 0) {
5566
5746
  const all = [];
@@ -5572,6 +5752,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5572
5752
  for (const [file, sigs] of sigIndex.entries()) {
5573
5753
  let score = scoreFile(file, sigs, queryTokens, weights);
5574
5754
  if (recencySet && recencySet.has(file) && score > 0) score *= recencyMultiplier;
5755
+ if (learnedWeights && score > 0) score *= learnedWeights[file] || 1.0;
5575
5756
  scored.push({ file, score, sigs, tokens: Math.ceil(sigs.join('\n').length / 4) });
5576
5757
  }
5577
5758
  scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
@@ -6390,7 +6571,7 @@ const path = require('path');
6390
6571
  const os = require('os');
6391
6572
  const { execSync } = require('child_process');
6392
6573
 
6393
- const VERSION = '5.1.0';
6574
+ const VERSION = '5.3.0';
6394
6575
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
6395
6576
 
6396
6577
  function requireSourceOrBundled(key) {
@@ -8021,6 +8202,11 @@ Usage:
8021
8202
  ${cmd} --query "<text>" Rank files by relevance to a query
8022
8203
  ${cmd} --query "<text>" --json Ranked results as JSON
8023
8204
  ${cmd} --query "<text>" --top <n> Limit results to top N files (default 10)
8205
+ ${cmd} learn --good <files...> Boost files in .context/weights.json
8206
+ ${cmd} learn --bad <files...> Penalize files in .context/weights.json
8207
+ ${cmd} learn --reset Delete learned file weights
8208
+ ${cmd} weights Show learned file multipliers
8209
+ ${cmd} weights --json Learned weights as JSON
8024
8210
  ${cmd} --impact <file> Show every file impacted by changing <file>
8025
8211
  ${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
8026
8212
  ${cmd} --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
@@ -8055,9 +8241,13 @@ function registerMcp(cwd, scriptPath) {
8055
8241
  args: [path.resolve(scriptPath), '--mcp'],
8056
8242
  };
8057
8243
 
8244
+ // mcpServers shape: Claude (.claude/settings.json), Cursor (.cursor/mcp.json),
8245
+ // Windsurf project (.windsurf/mcp.json) and global (~/.codeium/windsurf/mcp_config.json)
8058
8246
  const targets = [
8059
8247
  path.join(cwd, '.claude', 'settings.json'),
8060
8248
  path.join(cwd, '.cursor', 'mcp.json'),
8249
+ path.join(cwd, '.windsurf', 'mcp.json'),
8250
+ path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
8061
8251
  ];
8062
8252
 
8063
8253
  for (const settingsPath of targets) {
@@ -8069,15 +8259,37 @@ function registerMcp(cwd, scriptPath) {
8069
8259
  if (settings.mcpServers['sigmap']) continue; // already registered
8070
8260
  settings.mcpServers['sigmap'] = serverEntry;
8071
8261
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
8072
- console.warn(`[sigmap] registered MCP server in ${path.relative(cwd, settingsPath)}`);
8262
+ console.warn(`[sigmap] registered MCP server in ${settingsPath.startsWith(os.homedir()) ? '~' + settingsPath.slice(os.homedir().length) : path.relative(cwd, settingsPath)}`);
8073
8263
  } catch (err) {
8074
8264
  console.warn(`[sigmap] could not update ${path.relative(cwd, settingsPath)}: ${err.message}`);
8075
8265
  }
8076
8266
  }
8077
8267
 
8078
- // Always print the manual snippet so users can configure other tools
8079
- console.warn('[sigmap] MCP server config snippet:');
8080
- console.warn(JSON.stringify({ mcpServers: { 'sigmap': serverEntry } }, null, 2));
8268
+ // Zed uses context_servers (different shape from mcpServers)
8269
+ const zedSettingsPath = path.join(os.homedir(), '.config', 'zed', 'settings.json');
8270
+ if (fs.existsSync(zedSettingsPath)) {
8271
+ try {
8272
+ const raw = fs.readFileSync(zedSettingsPath, 'utf8');
8273
+ const settings = JSON.parse(raw);
8274
+ if (!settings.context_servers) settings.context_servers = {};
8275
+ if (!settings.context_servers['sigmap']) {
8276
+ settings.context_servers['sigmap'] = {
8277
+ command: { path: 'node', args: [path.resolve(scriptPath), '--mcp'] },
8278
+ };
8279
+ fs.writeFileSync(zedSettingsPath, JSON.stringify(settings, null, 2) + '\n');
8280
+ console.warn('[sigmap] registered context server in ~/.config/zed/settings.json');
8281
+ }
8282
+ } catch (err) {
8283
+ console.warn(`[sigmap] could not update ~/.config/zed/settings.json: ${err.message}`);
8284
+ }
8285
+ }
8286
+
8287
+ // Print manual snippets for all 4 tools
8288
+ console.warn('[sigmap] MCP / context server config snippets:');
8289
+ console.warn(' Claude / Cursor / Windsurf (.claude/settings.json | .cursor/mcp.json | .windsurf/mcp.json):');
8290
+ console.warn(JSON.stringify({ mcpServers: { sigmap: serverEntry } }, null, 2));
8291
+ console.warn(' Zed (~/.config/zed/settings.json):');
8292
+ console.warn(JSON.stringify({ context_servers: { sigmap: { command: { path: 'node', args: [path.resolve(scriptPath), '--mcp'] } } } }, null, 2));
8081
8293
  }
8082
8294
 
8083
8295
  // ---------------------------------------------------------------------------
@@ -8136,6 +8348,38 @@ function extractQuerySymbols(query) {
8136
8348
  return (query.match(/\b[A-Z][a-zA-Z]+|[a-z]+(?:[A-Z][a-z]+)+\b/g) || []);
8137
8349
  }
8138
8350
 
8351
+ function collectLearnFiles(args, flag, cwd) {
8352
+ const idx = args.indexOf(flag);
8353
+ if (idx < 0) return { rawCount: 0, files: [], warnings: [] };
8354
+
8355
+ const { normalizeFile } = requireSourceOrBundled('./src/learning/weights');
8356
+ const seen = new Set();
8357
+ const files = [];
8358
+ const warnings = [];
8359
+ let rawCount = 0;
8360
+
8361
+ for (let i = idx + 1; i < args.length; i++) {
8362
+ const value = args[i];
8363
+ if (!value || value.startsWith('--')) break;
8364
+ rawCount++;
8365
+ const normalized = normalizeFile(cwd, value);
8366
+ if (!normalized) {
8367
+ warnings.push(`${value} is outside the repo`);
8368
+ continue;
8369
+ }
8370
+ if (!fs.existsSync(path.join(cwd, normalized))) {
8371
+ warnings.push(`${normalized} does not exist`);
8372
+ continue;
8373
+ }
8374
+ if (!seen.has(normalized)) {
8375
+ seen.add(normalized);
8376
+ files.push(normalized);
8377
+ }
8378
+ }
8379
+
8380
+ return { rawCount, files, warnings };
8381
+ }
8382
+
8139
8383
  function main() {
8140
8384
  const args = process.argv.slice(2);
8141
8385
 
@@ -8236,7 +8480,7 @@ function main() {
8236
8480
  process.exit(1);
8237
8481
  }
8238
8482
 
8239
- const ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights });
8483
+ const ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });
8240
8484
  const miniCtx = buildMiniContext(ranked, cwd);
8241
8485
  const outPath = path.join(cwd, '.context', 'query-context.md');
8242
8486
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
@@ -8382,6 +8626,75 @@ function main() {
8382
8626
  process.exit(0);
8383
8627
  }
8384
8628
 
8629
+ // v5.2: `sigmap learn` — manual learning controls for ranking
8630
+ if (args[0] === 'learn') {
8631
+ const doReset = args.includes('--reset');
8632
+ const good = collectLearnFiles(args, '--good', cwd);
8633
+ const bad = collectLearnFiles(args, '--bad', cwd);
8634
+
8635
+ if (doReset && (good.rawCount > 0 || bad.rawCount > 0)) {
8636
+ console.error('[sigmap] --reset cannot be combined with --good or --bad');
8637
+ process.exit(1);
8638
+ }
8639
+
8640
+ if (doReset) {
8641
+ const { resetWeights } = requireSourceOrBundled('./src/learning/weights');
8642
+ resetWeights(cwd);
8643
+ console.log('[sigmap] weights reset — all files back to baseline');
8644
+ process.exit(0);
8645
+ }
8646
+
8647
+ if (good.rawCount === 0 && bad.rawCount === 0) {
8648
+ console.error('[sigmap] Usage: sigmap learn --good <files...> [--bad <files...>] | sigmap learn --reset');
8649
+ process.exit(1);
8650
+ }
8651
+
8652
+ for (const warning of [...good.warnings, ...bad.warnings]) {
8653
+ console.warn(`[sigmap] warning: ${warning}`);
8654
+ }
8655
+
8656
+ if (good.files.length === 0 && bad.files.length === 0) {
8657
+ console.error('[sigmap] No valid files to learn from.');
8658
+ process.exit(1);
8659
+ }
8660
+
8661
+ const { updateWeights } = requireSourceOrBundled('./src/learning/weights');
8662
+ const result = updateWeights(cwd, { goodFiles: good.files, badFiles: bad.files });
8663
+ const parts = [];
8664
+ if (result.good.length) parts.push(`boosted ${result.good.length} file(s)`);
8665
+ if (result.bad.length) parts.push(`penalized ${result.bad.length} file(s)`);
8666
+ console.log(`[sigmap] learned: ${parts.join(', ')}`);
8667
+ process.exit(0);
8668
+ }
8669
+
8670
+ // v5.2: `sigmap weights` — explain learned ranking multipliers
8671
+ if (args[0] === 'weights') {
8672
+ const { loadWeights } = requireSourceOrBundled('./src/learning/weights');
8673
+ const weights = loadWeights(cwd);
8674
+ const entries = Object.entries(weights).sort(([, a], [, b]) => b - a || 0);
8675
+
8676
+ if (args.includes('--json')) {
8677
+ process.stdout.write(JSON.stringify(weights, null, 2) + '\n');
8678
+ process.exit(0);
8679
+ }
8680
+
8681
+ if (entries.length === 0) {
8682
+ console.log('[sigmap] No learned weights yet. Run: sigmap learn --good <file>');
8683
+ process.exit(0);
8684
+ }
8685
+
8686
+ console.log('[sigmap] Learned file weights (xmultiplier vs baseline):');
8687
+ for (const [file, mult] of entries) {
8688
+ const bar = mult >= 1
8689
+ ? `+${'█'.repeat(Math.max(1, Math.round((mult - 1) * 10)))}`
8690
+ : `-${'░'.repeat(Math.max(1, Math.round((1 - mult) * 10)))}`;
8691
+ console.log(` ${file.padEnd(50)} x${mult.toFixed(2)} ${bar}`);
8692
+ }
8693
+ console.log(`\n Total files with learned weights: ${entries.length}`);
8694
+ console.log(' To reset: sigmap learn --reset');
8695
+ process.exit(0);
8696
+ }
8697
+
8385
8698
  // v4.3: `sigmap validate` — config + coverage + optional query symbol check
8386
8699
  if (args[0] === 'validate') {
8387
8700
  const issues = [];
@@ -8414,7 +8727,7 @@ function main() {
8414
8727
  if (q && !q.startsWith('--')) {
8415
8728
  try {
8416
8729
  const { rank, buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
8417
- const ranked = rank(q, buildSigIndex(cwd), { topK: 5 });
8730
+ const ranked = rank(q, buildSigIndex(cwd), { topK: 5, cwd });
8418
8731
  const symbols = extractQuerySymbols(q);
8419
8732
  const missing = symbols.filter((sym) =>
8420
8733
  !ranked.some((r) => r.sigs && r.sigs.some((s) => s.toLowerCase().includes(sym.toLowerCase())))
@@ -8447,7 +8760,7 @@ function main() {
8447
8760
  const ctxIdx = args.indexOf('--context');
8448
8761
 
8449
8762
  if (respIdx < 0 || ctxIdx < 0) {
8450
- console.error('[sigmap] Usage: sigmap judge --response <file> --context <file> [--json] [--threshold 0.25]');
8763
+ console.error('[sigmap] Usage: sigmap judge --response <file> --context <file> [--json] [--threshold 0.25] [--learn]');
8451
8764
  process.exit(1);
8452
8765
  }
8453
8766
 
@@ -8467,6 +8780,10 @@ function main() {
8467
8780
 
8468
8781
  const thrIdx = args.indexOf('--threshold');
8469
8782
  const judgeOpts = thrIdx >= 0 ? { threshold: parseFloat(args[thrIdx + 1]) || 0.25 } : {};
8783
+ if (args.includes('--learn')) {
8784
+ judgeOpts.learn = true;
8785
+ judgeOpts.cwd = cwd;
8786
+ }
8470
8787
 
8471
8788
  const { judge: runJudge } = requireSourceOrBundled('./src/judge/judge-engine');
8472
8789
  const result = runJudge(responseText, contextText, judgeOpts);
@@ -8481,8 +8798,11 @@ function main() {
8481
8798
  ` Score : ${result.score}`,
8482
8799
  ` Verdict : ${result.verdict}`,
8483
8800
  result.reasons.length ? ` Reasons :\n ${result.reasons.join('\n ')}` : ` Reasons : none`,
8801
+ result.learning
8802
+ ? ` Learning : ${result.learning.applied ? result.learning.action : 'skipped'}${result.learning.files.length ? ` (${result.learning.files.join(', ')})` : ''}${result.learning.reason ? ` — ${result.learning.reason}` : ''}`
8803
+ : null,
8484
8804
  bar,
8485
- ].join('\n'));
8805
+ ].filter(Boolean).join('\n'));
8486
8806
  }
8487
8807
  process.exit(result.verdict === 'pass' ? 0 : 1);
8488
8808
  }
@@ -9009,7 +9329,7 @@ function main() {
9009
9329
  const topK = topIdx >= 0 ? Math.min(Math.max(1, parseInt(args[topIdx + 1], 10) || 10), 25)
9010
9330
  : ((config && config.retrieval && config.retrieval.topK) || 10);
9011
9331
  const recencyBoost = (config && config.retrieval && config.retrieval.recencyBoost) || 1.5;
9012
- const results = rank(query, index, { topK, recencyBoost });
9332
+ const results = rank(query, index, { topK, recencyBoost, cwd });
9013
9333
  if (args.includes('--context')) {
9014
9334
  const miniCtx = buildMiniContext(results, cwd);
9015
9335
  const ctxOut = path.join(cwd, '.context', 'query-context.md');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -71,6 +71,7 @@ Rank all files in a signature index against a natural-language query.
71
71
  | `opts.topK` | `number` | Max files to return (default: `10`) |
72
72
  | `opts.weights` | `object` | Override default scoring weights |
73
73
  | `opts.recencySet` | `Set<string>` | Files to boost with `recencyBoost` multiplier |
74
+ | `opts.cwd` | `string` | Project root used to load learned ranking weights from `.context/weights.json` |
74
75
 
75
76
  Each result: `{ file: string, score: number, sigs: string[], tokens: number }`
76
77
 
@@ -130,6 +130,7 @@ function extract(src, language) {
130
130
  * @param {number} [opts.recencyBoost] - Score multiplier for recent files
131
131
  * @param {Set<string>} [opts.recencySet] - Set of file paths considered recent
132
132
  * @param {object} [opts.weights] - Override default scoring weights
133
+ * @param {string} [opts.cwd] - Project root for learned ranking weights
133
134
  * @returns {{ file: string, score: number, sigs: string[], tokens: number }[]}
134
135
  *
135
136
  * @example
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "5.1.0",
3
+ "version": "5.3.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [