sigmap 5.1.0 → 5.2.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/AGENTS.md +19 -3
- package/CHANGELOG.md +14 -0
- package/gen-context.js +303 -9
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/README.md +1 -0
- package/packages/core/index.js +1 -0
- package/packages/core/package.json +1 -1
- package/src/judge/judge-engine.js +68 -1
- package/src/learning/weights.js +138 -0
- package/src/mcp/handlers.js +2 -2
- package/src/mcp/server.js +1 -1
- package/src/retrieval/ranker.js +7 -0
package/AGENTS.md
CHANGED
|
@@ -12,18 +12,18 @@ Use this marker block for all appendable context files:
|
|
|
12
12
|
## Auto-generated signatures
|
|
13
13
|
<!-- Updated by gen-context.js -->
|
|
14
14
|
You are a coding assistant with full knowledge of this codebase.
|
|
15
|
-
Below are the code signatures extracted by SigMap v5.
|
|
15
|
+
Below are the code signatures extracted by SigMap v5.2.0 on 2026-04-16T22:22:03.099Z.
|
|
16
16
|
|
|
17
17
|
Use these signatures to answer questions about the code accurately.
|
|
18
18
|
|
|
19
19
|
## Code Signatures
|
|
20
20
|
|
|
21
|
-
<!-- Generated by SigMap gen-context.js v5.
|
|
21
|
+
<!-- Generated by SigMap gen-context.js v5.2.0 -->
|
|
22
22
|
<!-- DO NOT EDIT below the marker line — run gen-context.js to regenerate -->
|
|
23
23
|
|
|
24
24
|
# Code signatures
|
|
25
25
|
|
|
26
|
-
## changes (last 5 commits —
|
|
26
|
+
## changes (last 5 commits — 41 minutes ago)
|
|
27
27
|
```
|
|
28
28
|
src/config/loader.js +loadBaseConfig ~loadConfig ~deepClone
|
|
29
29
|
src/format/dashboard.js ~computeExtractorCoverage ~readBenchmarkTrend
|
|
@@ -183,6 +183,7 @@ function renderHistoryCharts(cwd, health)
|
|
|
183
183
|
module.exports = { groundedness, judge }
|
|
184
184
|
function tokenize(text)
|
|
185
185
|
function groundedness(response, context)
|
|
186
|
+
function extractContextFiles(context, cwd)
|
|
186
187
|
function judge(response, context, opts = {})
|
|
187
188
|
```
|
|
188
189
|
|
|
@@ -603,6 +604,21 @@ module.exports = { score }
|
|
|
603
604
|
function score(cwd) → { * score: number, * grad
|
|
604
605
|
```
|
|
605
606
|
|
|
607
|
+
### src/learning/weights.js
|
|
608
|
+
```
|
|
609
|
+
module.exports = { BASELINE, DECAY, MAX_MULT, MIN_MULT, weightsPath, clampMultiplier, normalizeFile, loadWeights, saveWeights, updateWeights, boostFiles, penalizeFiles, resetWeights }
|
|
610
|
+
function weightsPath(cwd)
|
|
611
|
+
function clampMultiplier(value)
|
|
612
|
+
function normalizeFile(cwd, filePath)
|
|
613
|
+
function sanitizeWeights(cwd, weights)
|
|
614
|
+
function loadWeights(cwd)
|
|
615
|
+
function saveWeights(cwd, weights)
|
|
616
|
+
function updateWeights(cwd, opts = {})
|
|
617
|
+
function boostFiles(cwd, files, amount = 0.15)
|
|
618
|
+
function penalizeFiles(cwd, files, amount = 0.10)
|
|
619
|
+
function resetWeights(cwd)
|
|
620
|
+
```
|
|
621
|
+
|
|
606
622
|
### src/map/class-hierarchy.js
|
|
607
623
|
```
|
|
608
624
|
module.exports = { analyze }
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [5.2.0] — 2026-04-17
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Learning engine** — new local-only weight store at `.context/weights.json` with path-normalized per-file multipliers, clamp safety (`0.30..3.00`), and decay on every non-reset mutation.
|
|
18
|
+
- **`sigmap learn`** — manually boost or penalize ranked files with `--good <files...>`, `--bad <files...>`, and `--reset`. Invalid or out-of-repo paths are skipped with warnings; the command exits non-zero when no valid targets remain.
|
|
19
|
+
- **`sigmap weights [--json]`** — explainability view for learned ranking multipliers. Human mode prints a compact table and reset hint; JSON mode emits the raw learned-weight object.
|
|
20
|
+
- **Opt-in judge learning** — `sigmap judge --response <file> --context <file> --learn` now extracts file headings from query/generated context files and applies small boosts or penalties when groundedness is confidently high or low.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Ranker learned weighting** — `rank(query, sigIndex, { cwd })` now loads `.context/weights.json` and multiplies non-empty-query scores by learned file multipliers. Empty-query fallback ordering is unchanged.
|
|
25
|
+
- **Learning-aware rank call sites** — `sigmap ask`, `sigmap --query`, `sigmap validate --query`, and MCP `query_context` now pass `cwd` into the ranker so learned weights apply consistently across CLI and MCP flows.
|
|
26
|
+
|
|
13
27
|
## [5.1.0] — 2026-04-16
|
|
14
28
|
|
|
15
29
|
### Added
|
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
|
-
|
|
4856
|
+
version: '5.2.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
|
-
|
|
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.
|
|
6574
|
+
const VERSION = '5.2.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)
|
|
@@ -8136,6 +8322,38 @@ function extractQuerySymbols(query) {
|
|
|
8136
8322
|
return (query.match(/\b[A-Z][a-zA-Z]+|[a-z]+(?:[A-Z][a-z]+)+\b/g) || []);
|
|
8137
8323
|
}
|
|
8138
8324
|
|
|
8325
|
+
function collectLearnFiles(args, flag, cwd) {
|
|
8326
|
+
const idx = args.indexOf(flag);
|
|
8327
|
+
if (idx < 0) return { rawCount: 0, files: [], warnings: [] };
|
|
8328
|
+
|
|
8329
|
+
const { normalizeFile } = requireSourceOrBundled('./src/learning/weights');
|
|
8330
|
+
const seen = new Set();
|
|
8331
|
+
const files = [];
|
|
8332
|
+
const warnings = [];
|
|
8333
|
+
let rawCount = 0;
|
|
8334
|
+
|
|
8335
|
+
for (let i = idx + 1; i < args.length; i++) {
|
|
8336
|
+
const value = args[i];
|
|
8337
|
+
if (!value || value.startsWith('--')) break;
|
|
8338
|
+
rawCount++;
|
|
8339
|
+
const normalized = normalizeFile(cwd, value);
|
|
8340
|
+
if (!normalized) {
|
|
8341
|
+
warnings.push(`${value} is outside the repo`);
|
|
8342
|
+
continue;
|
|
8343
|
+
}
|
|
8344
|
+
if (!fs.existsSync(path.join(cwd, normalized))) {
|
|
8345
|
+
warnings.push(`${normalized} does not exist`);
|
|
8346
|
+
continue;
|
|
8347
|
+
}
|
|
8348
|
+
if (!seen.has(normalized)) {
|
|
8349
|
+
seen.add(normalized);
|
|
8350
|
+
files.push(normalized);
|
|
8351
|
+
}
|
|
8352
|
+
}
|
|
8353
|
+
|
|
8354
|
+
return { rawCount, files, warnings };
|
|
8355
|
+
}
|
|
8356
|
+
|
|
8139
8357
|
function main() {
|
|
8140
8358
|
const args = process.argv.slice(2);
|
|
8141
8359
|
|
|
@@ -8236,7 +8454,7 @@ function main() {
|
|
|
8236
8454
|
process.exit(1);
|
|
8237
8455
|
}
|
|
8238
8456
|
|
|
8239
|
-
const ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights });
|
|
8457
|
+
const ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });
|
|
8240
8458
|
const miniCtx = buildMiniContext(ranked, cwd);
|
|
8241
8459
|
const outPath = path.join(cwd, '.context', 'query-context.md');
|
|
8242
8460
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
@@ -8382,6 +8600,75 @@ function main() {
|
|
|
8382
8600
|
process.exit(0);
|
|
8383
8601
|
}
|
|
8384
8602
|
|
|
8603
|
+
// v5.2: `sigmap learn` — manual learning controls for ranking
|
|
8604
|
+
if (args[0] === 'learn') {
|
|
8605
|
+
const doReset = args.includes('--reset');
|
|
8606
|
+
const good = collectLearnFiles(args, '--good', cwd);
|
|
8607
|
+
const bad = collectLearnFiles(args, '--bad', cwd);
|
|
8608
|
+
|
|
8609
|
+
if (doReset && (good.rawCount > 0 || bad.rawCount > 0)) {
|
|
8610
|
+
console.error('[sigmap] --reset cannot be combined with --good or --bad');
|
|
8611
|
+
process.exit(1);
|
|
8612
|
+
}
|
|
8613
|
+
|
|
8614
|
+
if (doReset) {
|
|
8615
|
+
const { resetWeights } = requireSourceOrBundled('./src/learning/weights');
|
|
8616
|
+
resetWeights(cwd);
|
|
8617
|
+
console.log('[sigmap] weights reset — all files back to baseline');
|
|
8618
|
+
process.exit(0);
|
|
8619
|
+
}
|
|
8620
|
+
|
|
8621
|
+
if (good.rawCount === 0 && bad.rawCount === 0) {
|
|
8622
|
+
console.error('[sigmap] Usage: sigmap learn --good <files...> [--bad <files...>] | sigmap learn --reset');
|
|
8623
|
+
process.exit(1);
|
|
8624
|
+
}
|
|
8625
|
+
|
|
8626
|
+
for (const warning of [...good.warnings, ...bad.warnings]) {
|
|
8627
|
+
console.warn(`[sigmap] warning: ${warning}`);
|
|
8628
|
+
}
|
|
8629
|
+
|
|
8630
|
+
if (good.files.length === 0 && bad.files.length === 0) {
|
|
8631
|
+
console.error('[sigmap] No valid files to learn from.');
|
|
8632
|
+
process.exit(1);
|
|
8633
|
+
}
|
|
8634
|
+
|
|
8635
|
+
const { updateWeights } = requireSourceOrBundled('./src/learning/weights');
|
|
8636
|
+
const result = updateWeights(cwd, { goodFiles: good.files, badFiles: bad.files });
|
|
8637
|
+
const parts = [];
|
|
8638
|
+
if (result.good.length) parts.push(`boosted ${result.good.length} file(s)`);
|
|
8639
|
+
if (result.bad.length) parts.push(`penalized ${result.bad.length} file(s)`);
|
|
8640
|
+
console.log(`[sigmap] learned: ${parts.join(', ')}`);
|
|
8641
|
+
process.exit(0);
|
|
8642
|
+
}
|
|
8643
|
+
|
|
8644
|
+
// v5.2: `sigmap weights` — explain learned ranking multipliers
|
|
8645
|
+
if (args[0] === 'weights') {
|
|
8646
|
+
const { loadWeights } = requireSourceOrBundled('./src/learning/weights');
|
|
8647
|
+
const weights = loadWeights(cwd);
|
|
8648
|
+
const entries = Object.entries(weights).sort(([, a], [, b]) => b - a || 0);
|
|
8649
|
+
|
|
8650
|
+
if (args.includes('--json')) {
|
|
8651
|
+
process.stdout.write(JSON.stringify(weights, null, 2) + '\n');
|
|
8652
|
+
process.exit(0);
|
|
8653
|
+
}
|
|
8654
|
+
|
|
8655
|
+
if (entries.length === 0) {
|
|
8656
|
+
console.log('[sigmap] No learned weights yet. Run: sigmap learn --good <file>');
|
|
8657
|
+
process.exit(0);
|
|
8658
|
+
}
|
|
8659
|
+
|
|
8660
|
+
console.log('[sigmap] Learned file weights (xmultiplier vs baseline):');
|
|
8661
|
+
for (const [file, mult] of entries) {
|
|
8662
|
+
const bar = mult >= 1
|
|
8663
|
+
? `+${'█'.repeat(Math.max(1, Math.round((mult - 1) * 10)))}`
|
|
8664
|
+
: `-${'░'.repeat(Math.max(1, Math.round((1 - mult) * 10)))}`;
|
|
8665
|
+
console.log(` ${file.padEnd(50)} x${mult.toFixed(2)} ${bar}`);
|
|
8666
|
+
}
|
|
8667
|
+
console.log(`\n Total files with learned weights: ${entries.length}`);
|
|
8668
|
+
console.log(' To reset: sigmap learn --reset');
|
|
8669
|
+
process.exit(0);
|
|
8670
|
+
}
|
|
8671
|
+
|
|
8385
8672
|
// v4.3: `sigmap validate` — config + coverage + optional query symbol check
|
|
8386
8673
|
if (args[0] === 'validate') {
|
|
8387
8674
|
const issues = [];
|
|
@@ -8414,7 +8701,7 @@ function main() {
|
|
|
8414
8701
|
if (q && !q.startsWith('--')) {
|
|
8415
8702
|
try {
|
|
8416
8703
|
const { rank, buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
8417
|
-
const ranked = rank(q, buildSigIndex(cwd), { topK: 5 });
|
|
8704
|
+
const ranked = rank(q, buildSigIndex(cwd), { topK: 5, cwd });
|
|
8418
8705
|
const symbols = extractQuerySymbols(q);
|
|
8419
8706
|
const missing = symbols.filter((sym) =>
|
|
8420
8707
|
!ranked.some((r) => r.sigs && r.sigs.some((s) => s.toLowerCase().includes(sym.toLowerCase())))
|
|
@@ -8447,7 +8734,7 @@ function main() {
|
|
|
8447
8734
|
const ctxIdx = args.indexOf('--context');
|
|
8448
8735
|
|
|
8449
8736
|
if (respIdx < 0 || ctxIdx < 0) {
|
|
8450
|
-
console.error('[sigmap] Usage: sigmap judge --response <file> --context <file> [--json] [--threshold 0.25]');
|
|
8737
|
+
console.error('[sigmap] Usage: sigmap judge --response <file> --context <file> [--json] [--threshold 0.25] [--learn]');
|
|
8451
8738
|
process.exit(1);
|
|
8452
8739
|
}
|
|
8453
8740
|
|
|
@@ -8467,6 +8754,10 @@ function main() {
|
|
|
8467
8754
|
|
|
8468
8755
|
const thrIdx = args.indexOf('--threshold');
|
|
8469
8756
|
const judgeOpts = thrIdx >= 0 ? { threshold: parseFloat(args[thrIdx + 1]) || 0.25 } : {};
|
|
8757
|
+
if (args.includes('--learn')) {
|
|
8758
|
+
judgeOpts.learn = true;
|
|
8759
|
+
judgeOpts.cwd = cwd;
|
|
8760
|
+
}
|
|
8470
8761
|
|
|
8471
8762
|
const { judge: runJudge } = requireSourceOrBundled('./src/judge/judge-engine');
|
|
8472
8763
|
const result = runJudge(responseText, contextText, judgeOpts);
|
|
@@ -8481,8 +8772,11 @@ function main() {
|
|
|
8481
8772
|
` Score : ${result.score}`,
|
|
8482
8773
|
` Verdict : ${result.verdict}`,
|
|
8483
8774
|
result.reasons.length ? ` Reasons :\n ${result.reasons.join('\n ')}` : ` Reasons : none`,
|
|
8775
|
+
result.learning
|
|
8776
|
+
? ` Learning : ${result.learning.applied ? result.learning.action : 'skipped'}${result.learning.files.length ? ` (${result.learning.files.join(', ')})` : ''}${result.learning.reason ? ` — ${result.learning.reason}` : ''}`
|
|
8777
|
+
: null,
|
|
8484
8778
|
bar,
|
|
8485
|
-
].join('\n'));
|
|
8779
|
+
].filter(Boolean).join('\n'));
|
|
8486
8780
|
}
|
|
8487
8781
|
process.exit(result.verdict === 'pass' ? 0 : 1);
|
|
8488
8782
|
}
|
|
@@ -9009,7 +9303,7 @@ function main() {
|
|
|
9009
9303
|
const topK = topIdx >= 0 ? Math.min(Math.max(1, parseInt(args[topIdx + 1], 10) || 10), 25)
|
|
9010
9304
|
: ((config && config.retrieval && config.retrieval.topK) || 10);
|
|
9011
9305
|
const recencyBoost = (config && config.retrieval && config.retrieval.recencyBoost) || 1.5;
|
|
9012
|
-
const results = rank(query, index, { topK, recencyBoost });
|
|
9306
|
+
const results = rank(query, index, { topK, recencyBoost, cwd });
|
|
9013
9307
|
if (args.includes('--context')) {
|
|
9014
9308
|
const miniCtx = buildMiniContext(results, cwd);
|
|
9015
9309
|
const ctxOut = path.join(cwd, '.context', 'query-context.md');
|
package/package.json
CHANGED
package/packages/core/README.md
CHANGED
|
@@ -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
|
|
package/packages/core/index.js
CHANGED
|
@@ -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,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { boostFiles, normalizeFile, penalizeFiles } = require('../learning/weights');
|
|
6
|
+
|
|
3
7
|
const STOP = new Set([
|
|
4
8
|
'the','a','an','in','on','at','to','of','for','and','or','but',
|
|
5
9
|
'is','are','was','were','be','been','being','have','has','had',
|
|
@@ -30,6 +34,30 @@ const GENERIC_MARKERS = [
|
|
|
30
34
|
'as a general rule',
|
|
31
35
|
];
|
|
32
36
|
|
|
37
|
+
function extractContextFiles(context, cwd) {
|
|
38
|
+
if (!context || !cwd) return [];
|
|
39
|
+
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const files = [];
|
|
42
|
+
const lines = context.split('\n');
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const match = line.match(/^#{2,3}\s+(.+?)\s*$/);
|
|
46
|
+
if (!match) continue;
|
|
47
|
+
|
|
48
|
+
const normalized = normalizeFile(cwd, match[1]);
|
|
49
|
+
if (!normalized) continue;
|
|
50
|
+
|
|
51
|
+
const abs = path.join(cwd, normalized);
|
|
52
|
+
if (!fs.existsSync(abs) || seen.has(normalized)) continue;
|
|
53
|
+
|
|
54
|
+
seen.add(normalized);
|
|
55
|
+
files.push(normalized);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return files;
|
|
59
|
+
}
|
|
60
|
+
|
|
33
61
|
function judge(response, context, opts = {}) {
|
|
34
62
|
const score = groundedness(response, context);
|
|
35
63
|
const threshold = opts.threshold !== undefined ? opts.threshold : 0.25;
|
|
@@ -49,7 +77,46 @@ function judge(response, context, opts = {}) {
|
|
|
49
77
|
}
|
|
50
78
|
|
|
51
79
|
const verdict = score >= threshold && reasons.length === 0 ? 'pass' : 'fail';
|
|
52
|
-
|
|
80
|
+
const result = { score, verdict, reasons };
|
|
81
|
+
|
|
82
|
+
if (opts.learn) {
|
|
83
|
+
const learning = {
|
|
84
|
+
applied: false,
|
|
85
|
+
action: 'none',
|
|
86
|
+
files: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (!opts.cwd) {
|
|
90
|
+
learning.reason = 'cwd is required for learning';
|
|
91
|
+
result.learning = learning;
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const contextFiles = extractContextFiles(context, opts.cwd);
|
|
96
|
+
learning.files = contextFiles;
|
|
97
|
+
|
|
98
|
+
if (contextFiles.length === 0) {
|
|
99
|
+
learning.reason = 'no context files found in context headings';
|
|
100
|
+
result.learning = learning;
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (score > 0.75) {
|
|
105
|
+
boostFiles(opts.cwd, contextFiles, 0.05);
|
|
106
|
+
learning.applied = true;
|
|
107
|
+
learning.action = 'boost';
|
|
108
|
+
} else if (score < 0.40) {
|
|
109
|
+
penalizeFiles(opts.cwd, contextFiles, 0.03);
|
|
110
|
+
learning.applied = true;
|
|
111
|
+
learning.action = 'penalize';
|
|
112
|
+
} else {
|
|
113
|
+
learning.reason = 'groundedness in no-op band (0.40-0.75)';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
result.learning = learning;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
53
120
|
}
|
|
54
121
|
|
|
55
122
|
module.exports = { groundedness, judge };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const DECAY = 0.95;
|
|
7
|
+
const MAX_MULT = 3.0;
|
|
8
|
+
const MIN_MULT = 0.30;
|
|
9
|
+
const BASELINE = 1.0;
|
|
10
|
+
|
|
11
|
+
function weightsPath(cwd) {
|
|
12
|
+
return path.join(cwd, '.context', 'weights.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function clampMultiplier(value) {
|
|
16
|
+
if (!Number.isFinite(value)) return BASELINE;
|
|
17
|
+
if (value > MAX_MULT) return MAX_MULT;
|
|
18
|
+
if (value < MIN_MULT) return MIN_MULT;
|
|
19
|
+
return parseFloat(value.toFixed(6));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeFile(cwd, filePath) {
|
|
23
|
+
if (!cwd || !filePath || typeof filePath !== 'string') return null;
|
|
24
|
+
const cleaned = filePath.trim().replace(/\\/g, '/');
|
|
25
|
+
if (!cleaned) return null;
|
|
26
|
+
|
|
27
|
+
const abs = path.resolve(cwd, cleaned);
|
|
28
|
+
const rel = path.relative(cwd, abs);
|
|
29
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
|
|
30
|
+
return rel.split(path.sep).join('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitizeWeights(cwd, weights) {
|
|
34
|
+
const out = {};
|
|
35
|
+
const entries = weights && typeof weights === 'object' ? Object.entries(weights) : [];
|
|
36
|
+
|
|
37
|
+
for (const [filePath, raw] of entries) {
|
|
38
|
+
const normalized = normalizeFile(cwd, filePath);
|
|
39
|
+
if (!normalized) continue;
|
|
40
|
+
const mult = clampMultiplier(Number(raw));
|
|
41
|
+
if (Math.abs(mult - BASELINE) < 1e-9) continue;
|
|
42
|
+
out[normalized] = mult;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function loadWeights(cwd) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(fs.readFileSync(weightsPath(cwd), 'utf8'));
|
|
51
|
+
return sanitizeWeights(cwd, parsed);
|
|
52
|
+
} catch (_) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function saveWeights(cwd, weights) {
|
|
58
|
+
const cleaned = sanitizeWeights(cwd, weights);
|
|
59
|
+
const outPath = weightsPath(cwd);
|
|
60
|
+
|
|
61
|
+
if (Object.keys(cleaned).length === 0) {
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
|
|
64
|
+
} catch (_) {}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
69
|
+
const sorted = Object.keys(cleaned)
|
|
70
|
+
.sort()
|
|
71
|
+
.reduce((acc, key) => {
|
|
72
|
+
acc[key] = cleaned[key];
|
|
73
|
+
return acc;
|
|
74
|
+
}, {});
|
|
75
|
+
fs.writeFileSync(outPath, JSON.stringify(sorted, null, 2) + '\n', 'utf8');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function updateWeights(cwd, opts = {}) {
|
|
79
|
+
const goodAmount = Number.isFinite(opts.goodAmount) ? opts.goodAmount : 0.15;
|
|
80
|
+
const badAmount = Number.isFinite(opts.badAmount) ? opts.badAmount : 0.10;
|
|
81
|
+
const goodFiles = Array.isArray(opts.goodFiles) ? opts.goodFiles : [];
|
|
82
|
+
const badFiles = Array.isArray(opts.badFiles) ? opts.badFiles : [];
|
|
83
|
+
|
|
84
|
+
const weights = loadWeights(cwd);
|
|
85
|
+
|
|
86
|
+
for (const key of Object.keys(weights)) {
|
|
87
|
+
weights[key] = clampMultiplier(weights[key] * DECAY);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const good = [];
|
|
91
|
+
const bad = [];
|
|
92
|
+
|
|
93
|
+
for (const filePath of goodFiles) {
|
|
94
|
+
const normalized = normalizeFile(cwd, filePath);
|
|
95
|
+
if (!normalized) continue;
|
|
96
|
+
weights[normalized] = clampMultiplier((weights[normalized] || BASELINE) + goodAmount);
|
|
97
|
+
good.push(normalized);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const filePath of badFiles) {
|
|
101
|
+
const normalized = normalizeFile(cwd, filePath);
|
|
102
|
+
if (!normalized) continue;
|
|
103
|
+
weights[normalized] = clampMultiplier((weights[normalized] || BASELINE) - badAmount);
|
|
104
|
+
bad.push(normalized);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
saveWeights(cwd, weights);
|
|
108
|
+
return { good, bad, weights: loadWeights(cwd) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function boostFiles(cwd, files, amount = 0.15) {
|
|
112
|
+
return updateWeights(cwd, { goodFiles: files, goodAmount: amount });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function penalizeFiles(cwd, files, amount = 0.10) {
|
|
116
|
+
return updateWeights(cwd, { badFiles: files, badAmount: amount });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resetWeights(cwd) {
|
|
120
|
+
const outPath = weightsPath(cwd);
|
|
121
|
+
if (fs.existsSync(outPath)) fs.unlinkSync(outPath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
BASELINE,
|
|
126
|
+
DECAY,
|
|
127
|
+
MAX_MULT,
|
|
128
|
+
MIN_MULT,
|
|
129
|
+
weightsPath,
|
|
130
|
+
clampMultiplier,
|
|
131
|
+
normalizeFile,
|
|
132
|
+
loadWeights,
|
|
133
|
+
saveWeights,
|
|
134
|
+
updateWeights,
|
|
135
|
+
boostFiles,
|
|
136
|
+
penalizeFiles,
|
|
137
|
+
resetWeights,
|
|
138
|
+
};
|
package/src/mcp/handlers.js
CHANGED
|
@@ -450,7 +450,7 @@ function queryContext(args, cwd) {
|
|
|
450
450
|
if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
|
|
451
451
|
|
|
452
452
|
const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
|
|
453
|
-
const results = rank(args.query, index, { topK });
|
|
453
|
+
const results = rank(args.query, index, { topK, cwd });
|
|
454
454
|
return formatRankTable(results, args.query);
|
|
455
455
|
} catch (err) {
|
|
456
456
|
return `_query_context failed: ${err.message}_`;
|
|
@@ -477,4 +477,4 @@ function getImpact(args, cwd) {
|
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
479
|
|
|
480
|
-
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact };
|
|
480
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact };
|
package/src/mcp/server.js
CHANGED
package/src/retrieval/ranker.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* // results: [{ file, score, sigs, tokens }]
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
const { loadWeights } = require('../learning/weights');
|
|
20
21
|
const { tokenize, STOP_WORDS } = require('./tokenizer');
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
@@ -97,6 +98,7 @@ function scoreFile(filePath, sigs, queryTokens, weights) {
|
|
|
97
98
|
* @param {number} [opts.recencyBoost=1.5] - multiplier for recent files
|
|
98
99
|
* @param {Set<string>} [opts.recencySet] - set of recently-changed file paths
|
|
99
100
|
* @param {object} [opts.weights] - override scoring weights
|
|
101
|
+
* @param {string} [opts.cwd] - project root for learned ranking weights
|
|
100
102
|
* @returns {{ file: string, score: number, sigs: string[], tokens: number }[]}
|
|
101
103
|
*/
|
|
102
104
|
function rank(query, sigIndex, opts) {
|
|
@@ -107,6 +109,7 @@ function rank(query, sigIndex, opts) {
|
|
|
107
109
|
const recencyMultiplier = (opts && opts.recencyBoost) || DEFAULT_WEIGHTS.recencyBoost;
|
|
108
110
|
const recencySet = (opts && opts.recencySet) || null;
|
|
109
111
|
const weights = (opts && opts.weights) ? Object.assign({}, DEFAULT_WEIGHTS, opts.weights) : DEFAULT_WEIGHTS;
|
|
112
|
+
const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;
|
|
110
113
|
|
|
111
114
|
const queryTokens = tokenize(query);
|
|
112
115
|
if (queryTokens.length === 0) {
|
|
@@ -128,6 +131,10 @@ function rank(query, sigIndex, opts) {
|
|
|
128
131
|
score *= recencyMultiplier;
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
if (learnedWeights && score > 0) {
|
|
135
|
+
score *= learnedWeights[file] || 1.0;
|
|
136
|
+
}
|
|
137
|
+
|
|
131
138
|
scored.push({
|
|
132
139
|
file,
|
|
133
140
|
score,
|