sigmap 5.0.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 +61 -32
- package/CHANGELOG.md +24 -0
- package/gen-context.js +360 -29
- 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/format/dashboard.js +20 -0
- 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,20 +12,23 @@ 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
|
|
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
|
|
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
|
-
src/
|
|
28
|
+
src/config/loader.js +loadBaseConfig ~loadConfig ~deepClone
|
|
29
|
+
src/format/dashboard.js ~computeExtractorCoverage ~readBenchmarkTrend
|
|
30
|
+
src/judge/judge-engine.js +tokenize +groundedness +judge
|
|
31
|
+
src/retrieval/ranker.js +detectIntent ~formatRankJSON
|
|
29
32
|
```
|
|
30
33
|
|
|
31
34
|
## packages
|
|
@@ -146,9 +149,42 @@ function adapt(context, adapterName, opts = {}) → string
|
|
|
146
149
|
|
|
147
150
|
## src
|
|
148
151
|
|
|
149
|
-
### src/config/
|
|
152
|
+
### src/config/loader.js
|
|
150
153
|
```
|
|
151
|
-
module.exports = {
|
|
154
|
+
module.exports = { loadConfig, loadBaseConfig }
|
|
155
|
+
function loadBaseConfig(extendsVal, cwd)
|
|
156
|
+
function detectAutoSrcDirs(cwd, excludeList) → string[]
|
|
157
|
+
function loadConfig(cwd) → object
|
|
158
|
+
function deepClone(obj)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### src/format/dashboard.js
|
|
162
|
+
```
|
|
163
|
+
module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak }
|
|
164
|
+
function toNumber(v)
|
|
165
|
+
function percentile(values, p)
|
|
166
|
+
function overBudgetStreak(entries)
|
|
167
|
+
function loadConfig(cwd)
|
|
168
|
+
function shouldExclude(rel, excludeSet)
|
|
169
|
+
function detectLanguage(filePath)
|
|
170
|
+
function walkFiles(dir, maxDepth, depth, out, excludeSet)
|
|
171
|
+
function computeExtractorCoverage(cwd)
|
|
172
|
+
function readBenchmarkTrend(cwd)
|
|
173
|
+
function lineChartSvg(values, title, ySuffix)
|
|
174
|
+
function barChartSvg(perLanguage)
|
|
175
|
+
function sparkline(values)
|
|
176
|
+
function buildDashboardData(cwd, health)
|
|
177
|
+
function generateDashboardHtml(cwd, health)
|
|
178
|
+
function renderHistoryCharts(cwd, health)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### src/judge/judge-engine.js
|
|
182
|
+
```
|
|
183
|
+
module.exports = { groundedness, judge }
|
|
184
|
+
function tokenize(text)
|
|
185
|
+
function groundedness(response, context)
|
|
186
|
+
function extractContextFiles(context, cwd)
|
|
187
|
+
function judge(response, context, opts = {})
|
|
152
188
|
```
|
|
153
189
|
|
|
154
190
|
### src/mcp/server.js
|
|
@@ -162,13 +198,14 @@ function start(cwd)
|
|
|
162
198
|
|
|
163
199
|
### src/retrieval/ranker.js
|
|
164
200
|
```
|
|
165
|
-
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS }
|
|
201
|
+
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent }
|
|
166
202
|
function scoreFile(filePath, sigs, queryTokens, weights) → number
|
|
167
203
|
function rank(query, sigIndex, opts) → { file: string, score: nu
|
|
168
204
|
function _parseContextFile(contextPath) → Map<string, string[]>
|
|
169
205
|
function buildSigIndex(cwd, opts) → Map<string, string[]>
|
|
170
206
|
function formatRankTable(results, query) → string
|
|
171
207
|
function formatRankJSON(results, query) → object
|
|
208
|
+
function detectIntent(query)
|
|
172
209
|
```
|
|
173
210
|
|
|
174
211
|
### src/analysis/coverage-score.js
|
|
@@ -178,12 +215,9 @@ function coverageScore(cwd, fileEntries, config) → { * score: number, * grad
|
|
|
178
215
|
function _walk(dir, excludeSet, out)
|
|
179
216
|
```
|
|
180
217
|
|
|
181
|
-
### src/config/
|
|
218
|
+
### src/config/defaults.js
|
|
182
219
|
```
|
|
183
|
-
module.exports = {
|
|
184
|
-
function detectAutoSrcDirs(cwd, excludeList) → string[]
|
|
185
|
-
function loadConfig(cwd) → object
|
|
186
|
-
function deepClone(obj)
|
|
220
|
+
module.exports = { DEFAULTS }
|
|
187
221
|
```
|
|
188
222
|
|
|
189
223
|
### src/eval/analyzer.js
|
|
@@ -527,26 +561,6 @@ function formatCache(content) → string
|
|
|
527
561
|
function formatCachePayload(content, model) → string
|
|
528
562
|
```
|
|
529
563
|
|
|
530
|
-
### src/format/dashboard.js
|
|
531
|
-
```
|
|
532
|
-
module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak }
|
|
533
|
-
function toNumber(v)
|
|
534
|
-
function percentile(values, p)
|
|
535
|
-
function overBudgetStreak(entries)
|
|
536
|
-
function loadConfig(cwd)
|
|
537
|
-
function shouldExclude(rel, excludeSet)
|
|
538
|
-
function detectLanguage(filePath)
|
|
539
|
-
function walkFiles(dir, maxDepth, depth, out, excludeSet)
|
|
540
|
-
function computeExtractorCoverage(cwd)
|
|
541
|
-
function readBenchmarkTrend(cwd)
|
|
542
|
-
function lineChartSvg(values, title, ySuffix)
|
|
543
|
-
function barChartSvg(perLanguage)
|
|
544
|
-
function sparkline(values)
|
|
545
|
-
function buildDashboardData(cwd, health)
|
|
546
|
-
function generateDashboardHtml(cwd, health)
|
|
547
|
-
function renderHistoryCharts(cwd, health)
|
|
548
|
-
```
|
|
549
|
-
|
|
550
564
|
### src/format/llm-txt.js
|
|
551
565
|
```
|
|
552
566
|
module.exports = { format, outputPath }
|
|
@@ -590,6 +604,21 @@ module.exports = { score }
|
|
|
590
604
|
function score(cwd) → { * score: number, * grad
|
|
591
605
|
```
|
|
592
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
|
+
|
|
593
622
|
### src/map/class-hierarchy.js
|
|
594
623
|
```
|
|
595
624
|
module.exports = { analyze }
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,30 @@ 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
|
+
|
|
27
|
+
## [5.1.0] — 2026-04-16
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Benchmark history tracking** — all three benchmark scripts (`run-retrieval-benchmark.mjs`, `run-benchmark.mjs`, `run-task-benchmark.mjs`) now append a structured NDJSON entry to `.context/benchmark-history.ndjson` after each run (`type: "retrieval" | "token-reduction" | "task"`).
|
|
32
|
+
- **`sigmap history` benchmark trend rows** — when `.context/benchmark-history.ndjson` exists, `sigmap history` prints a retrieval `hit@5` sparkline row and a token-reduction sparkline row below the usage table. The command no longer exits early when the usage log is empty.
|
|
33
|
+
- **Dashboard `readBenchmarkTrend` uses local history** — `src/format/dashboard.js` now prefers `.context/benchmark-history.ndjson` over the CI-only `benchmarks/results/` directory, so the dashboard hit@5 trend chart populates for all users after running any benchmark locally.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
13
37
|
## [5.0.0] — 2026-04-16
|
|
14
38
|
|
|
15
39
|
### Added
|
package/gen-context.js
CHANGED
|
@@ -3152,6 +3152,25 @@ __factories["./src/format/cache"] = function(module, exports) {
|
|
|
3152
3152
|
}
|
|
3153
3153
|
|
|
3154
3154
|
function readBenchmarkTrend(cwd) {
|
|
3155
|
+
// Prefer per-user history file written by benchmark scripts
|
|
3156
|
+
const histPath = path.join(cwd, '.context', 'benchmark-history.ndjson');
|
|
3157
|
+
if (fs.existsSync(histPath)) {
|
|
3158
|
+
const histValues = [];
|
|
3159
|
+
try {
|
|
3160
|
+
for (const line of fs.readFileSync(histPath, 'utf8').trim().split('\n').filter(Boolean)) {
|
|
3161
|
+
try {
|
|
3162
|
+
const obj = JSON.parse(line);
|
|
3163
|
+
if (obj.type === 'retrieval') {
|
|
3164
|
+
const v = toNumber(obj.hitAt5Pct);
|
|
3165
|
+
if (v !== null) histValues.push(v);
|
|
3166
|
+
}
|
|
3167
|
+
} catch (_) {}
|
|
3168
|
+
}
|
|
3169
|
+
} catch (_) {}
|
|
3170
|
+
if (histValues.length > 0) return histValues.slice(-30);
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
// Fallback: legacy benchmarks/results directory (CI artifacts)
|
|
3155
3174
|
const resultDir = path.join(cwd, 'benchmarks', 'results');
|
|
3156
3175
|
if (!fs.existsSync(resultDir)) return [];
|
|
3157
3176
|
const files = [];
|
|
@@ -4665,7 +4684,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
4665
4684
|
const index = buildSigIndex(cwd);
|
|
4666
4685
|
if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
|
|
4667
4686
|
const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
|
|
4668
|
-
const results = rank(args.query, index, { topK });
|
|
4687
|
+
const results = rank(args.query, index, { topK, cwd });
|
|
4669
4688
|
return formatRankTable(results, args.query);
|
|
4670
4689
|
} catch (err) {
|
|
4671
4690
|
return `_query_context failed: ${err.message}_`;
|
|
@@ -4687,6 +4706,132 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
4687
4706
|
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact };
|
|
4688
4707
|
};
|
|
4689
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
|
+
|
|
4690
4835
|
// ── ./src/mcp/server ──
|
|
4691
4836
|
__factories["./src/mcp/server"] = function(module, exports) {
|
|
4692
4837
|
|
|
@@ -4708,7 +4853,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
4708
4853
|
|
|
4709
4854
|
const SERVER_INFO = {
|
|
4710
4855
|
name: 'sigmap',
|
|
4711
|
-
|
|
4856
|
+
version: '5.2.0',
|
|
4712
4857
|
description: 'SigMap MCP server — code signatures on demand',
|
|
4713
4858
|
};
|
|
4714
4859
|
|
|
@@ -5310,6 +5455,10 @@ __factories["./src/security/scanner"] = function(module, exports) {
|
|
|
5310
5455
|
__factories["./src/judge/judge-engine"] = function(module, exports) {
|
|
5311
5456
|
'use strict';
|
|
5312
5457
|
|
|
5458
|
+
const fs = require('fs');
|
|
5459
|
+
const path = require('path');
|
|
5460
|
+
const { boostFiles, normalizeFile, penalizeFiles } = __require('./src/learning/weights');
|
|
5461
|
+
|
|
5313
5462
|
const STOP = new Set([
|
|
5314
5463
|
'the','a','an','in','on','at','to','of','for','and','or','but',
|
|
5315
5464
|
'is','are','was','were','be','been','being','have','has','had',
|
|
@@ -5340,6 +5489,24 @@ __factories["./src/judge/judge-engine"] = function(module, exports) {
|
|
|
5340
5489
|
'as a general rule',
|
|
5341
5490
|
];
|
|
5342
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
|
+
|
|
5343
5510
|
function judge(response, context, opts) {
|
|
5344
5511
|
opts = opts || {};
|
|
5345
5512
|
const score = groundedness(response, context);
|
|
@@ -5355,7 +5522,37 @@ __factories["./src/judge/judge-engine"] = function(module, exports) {
|
|
|
5355
5522
|
}
|
|
5356
5523
|
}
|
|
5357
5524
|
const verdict = score >= threshold && reasons.length === 0 ? 'pass' : 'fail';
|
|
5358
|
-
|
|
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;
|
|
5359
5556
|
}
|
|
5360
5557
|
|
|
5361
5558
|
module.exports = { groundedness, judge };
|
|
@@ -5510,6 +5707,7 @@ __factories["./src/retrieval/tokenizer"] = function(module, exports) {
|
|
|
5510
5707
|
// ── ./src/retrieval/ranker ──
|
|
5511
5708
|
__factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
5512
5709
|
'use strict';
|
|
5710
|
+
const { loadWeights } = __require('./src/learning/weights');
|
|
5513
5711
|
const { tokenize, STOP_WORDS } = __require('./src/retrieval/tokenizer');
|
|
5514
5712
|
const DEFAULT_WEIGHTS = {
|
|
5515
5713
|
exactToken: 1.0, symbolMatch: 0.5, prefixMatch: 0.3, pathMatch: 0.8, recencyBoost: 1.5,
|
|
@@ -5542,6 +5740,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
5542
5740
|
const recencyMultiplier = (opts && opts.recencyBoost) || DEFAULT_WEIGHTS.recencyBoost;
|
|
5543
5741
|
const recencySet = (opts && opts.recencySet) || null;
|
|
5544
5742
|
const weights = (opts && opts.weights) ? Object.assign({}, DEFAULT_WEIGHTS, opts.weights) : DEFAULT_WEIGHTS;
|
|
5743
|
+
const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;
|
|
5545
5744
|
const queryTokens = tokenize(query);
|
|
5546
5745
|
if (queryTokens.length === 0) {
|
|
5547
5746
|
const all = [];
|
|
@@ -5553,6 +5752,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
5553
5752
|
for (const [file, sigs] of sigIndex.entries()) {
|
|
5554
5753
|
let score = scoreFile(file, sigs, queryTokens, weights);
|
|
5555
5754
|
if (recencySet && recencySet.has(file) && score > 0) score *= recencyMultiplier;
|
|
5755
|
+
if (learnedWeights && score > 0) score *= learnedWeights[file] || 1.0;
|
|
5556
5756
|
scored.push({ file, score, sigs, tokens: Math.ceil(sigs.join('\n').length / 4) });
|
|
5557
5757
|
}
|
|
5558
5758
|
scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
@@ -6371,7 +6571,7 @@ const path = require('path');
|
|
|
6371
6571
|
const os = require('os');
|
|
6372
6572
|
const { execSync } = require('child_process');
|
|
6373
6573
|
|
|
6374
|
-
const VERSION = '5.
|
|
6574
|
+
const VERSION = '5.2.0';
|
|
6375
6575
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
6376
6576
|
|
|
6377
6577
|
function requireSourceOrBundled(key) {
|
|
@@ -8002,6 +8202,11 @@ Usage:
|
|
|
8002
8202
|
${cmd} --query "<text>" Rank files by relevance to a query
|
|
8003
8203
|
${cmd} --query "<text>" --json Ranked results as JSON
|
|
8004
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
|
|
8005
8210
|
${cmd} --impact <file> Show every file impacted by changing <file>
|
|
8006
8211
|
${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
8007
8212
|
${cmd} --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
@@ -8117,6 +8322,38 @@ function extractQuerySymbols(query) {
|
|
|
8117
8322
|
return (query.match(/\b[A-Z][a-zA-Z]+|[a-z]+(?:[A-Z][a-z]+)+\b/g) || []);
|
|
8118
8323
|
}
|
|
8119
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
|
+
|
|
8120
8357
|
function main() {
|
|
8121
8358
|
const args = process.argv.slice(2);
|
|
8122
8359
|
|
|
@@ -8217,7 +8454,7 @@ function main() {
|
|
|
8217
8454
|
process.exit(1);
|
|
8218
8455
|
}
|
|
8219
8456
|
|
|
8220
|
-
const ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights });
|
|
8457
|
+
const ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });
|
|
8221
8458
|
const miniCtx = buildMiniContext(ranked, cwd);
|
|
8222
8459
|
const outPath = path.join(cwd, '.context', 'query-context.md');
|
|
8223
8460
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
@@ -8363,6 +8600,75 @@ function main() {
|
|
|
8363
8600
|
process.exit(0);
|
|
8364
8601
|
}
|
|
8365
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
|
+
|
|
8366
8672
|
// v4.3: `sigmap validate` — config + coverage + optional query symbol check
|
|
8367
8673
|
if (args[0] === 'validate') {
|
|
8368
8674
|
const issues = [];
|
|
@@ -8395,7 +8701,7 @@ function main() {
|
|
|
8395
8701
|
if (q && !q.startsWith('--')) {
|
|
8396
8702
|
try {
|
|
8397
8703
|
const { rank, buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
8398
|
-
const ranked = rank(q, buildSigIndex(cwd), { topK: 5 });
|
|
8704
|
+
const ranked = rank(q, buildSigIndex(cwd), { topK: 5, cwd });
|
|
8399
8705
|
const symbols = extractQuerySymbols(q);
|
|
8400
8706
|
const missing = symbols.filter((sym) =>
|
|
8401
8707
|
!ranked.some((r) => r.sigs && r.sigs.some((s) => s.toLowerCase().includes(sym.toLowerCase())))
|
|
@@ -8428,7 +8734,7 @@ function main() {
|
|
|
8428
8734
|
const ctxIdx = args.indexOf('--context');
|
|
8429
8735
|
|
|
8430
8736
|
if (respIdx < 0 || ctxIdx < 0) {
|
|
8431
|
-
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]');
|
|
8432
8738
|
process.exit(1);
|
|
8433
8739
|
}
|
|
8434
8740
|
|
|
@@ -8448,6 +8754,10 @@ function main() {
|
|
|
8448
8754
|
|
|
8449
8755
|
const thrIdx = args.indexOf('--threshold');
|
|
8450
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
|
+
}
|
|
8451
8761
|
|
|
8452
8762
|
const { judge: runJudge } = requireSourceOrBundled('./src/judge/judge-engine');
|
|
8453
8763
|
const result = runJudge(responseText, contextText, judgeOpts);
|
|
@@ -8462,8 +8772,11 @@ function main() {
|
|
|
8462
8772
|
` Score : ${result.score}`,
|
|
8463
8773
|
` Verdict : ${result.verdict}`,
|
|
8464
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,
|
|
8465
8778
|
bar,
|
|
8466
|
-
].join('\n'));
|
|
8779
|
+
].filter(Boolean).join('\n'));
|
|
8467
8780
|
}
|
|
8468
8781
|
process.exit(result.verdict === 'pass' ? 0 : 1);
|
|
8469
8782
|
}
|
|
@@ -8482,11 +8795,6 @@ function main() {
|
|
|
8482
8795
|
process.exit(0);
|
|
8483
8796
|
}
|
|
8484
8797
|
|
|
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
8798
|
const SPARK_CHARS = '▁▂▃▄▅▆▇█';
|
|
8491
8799
|
function sparkline(values) {
|
|
8492
8800
|
if (values.length === 0) return '';
|
|
@@ -8499,25 +8807,48 @@ function main() {
|
|
|
8499
8807
|
}).join('');
|
|
8500
8808
|
}
|
|
8501
8809
|
|
|
8502
|
-
const tokens = last.map((e) => e.finalTokens || 0);
|
|
8503
|
-
const spark = sparkline(tokens);
|
|
8504
|
-
|
|
8505
8810
|
const bar = '─'.repeat(62);
|
|
8506
8811
|
console.log(bar);
|
|
8507
|
-
console.log(` sigmap history (last ${last.length} runs)`);
|
|
8812
|
+
console.log(` sigmap history (last ${Math.max(last.length, 1)} runs)`);
|
|
8508
8813
|
console.log(bar);
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
const
|
|
8516
|
-
|
|
8517
|
-
|
|
8814
|
+
|
|
8815
|
+
if (last.length === 0) {
|
|
8816
|
+
console.log(' No usage log entries. Enable tracking: true in config to start recording runs.');
|
|
8817
|
+
} else {
|
|
8818
|
+
console.log(` ${'Date'.padEnd(24)} ${'Files'.padStart(5)} ${'Tokens'.padStart(7)} ${'Reduction'.padStart(9)} ${'Budget?'.padStart(7)}`);
|
|
8819
|
+
console.log(` ${'─'.repeat(24)} ${'─'.repeat(5)} ${'─'.repeat(7)} ${'─'.repeat(9)} ${'─'.repeat(7)}`);
|
|
8820
|
+
for (const e of last) {
|
|
8821
|
+
const date = (e.ts || '').slice(0, 19).replace('T', ' ');
|
|
8822
|
+
const files = String(e.fileCount || 0).padStart(5);
|
|
8823
|
+
const tok = String(e.finalTokens || 0).padStart(7);
|
|
8824
|
+
const red = `${e.reductionPct || 0}%`.padStart(9);
|
|
8825
|
+
const over = (e.overBudget ? ' ⚠ yes' : ' no').padStart(7);
|
|
8826
|
+
console.log(` ${date.padEnd(24)} ${files} ${tok} ${red} ${over}`);
|
|
8827
|
+
}
|
|
8828
|
+
console.log(bar);
|
|
8829
|
+
const tokens = last.map((e) => e.finalTokens || 0);
|
|
8830
|
+
console.log(` Token trend: ${sparkline(tokens)}`);
|
|
8831
|
+
}
|
|
8832
|
+
|
|
8833
|
+
// Show benchmark trend row if .context/benchmark-history.ndjson exists
|
|
8834
|
+
const benchHistPath = path.join(cwd, '.context', 'benchmark-history.ndjson');
|
|
8835
|
+
if (fs.existsSync(benchHistPath)) {
|
|
8836
|
+
try {
|
|
8837
|
+
const benchEntries = fs.readFileSync(benchHistPath, 'utf8').trim().split('\n')
|
|
8838
|
+
.map((l) => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
|
|
8839
|
+
const retrieval = benchEntries.filter((e) => e.type === 'retrieval').slice(-n);
|
|
8840
|
+
if (retrieval.length > 0) {
|
|
8841
|
+
const hits = retrieval.map((e) => e.hitAt5Pct || 0);
|
|
8842
|
+
console.log(` hit@5 trend: ${sparkline(hits)} ${hits.at(-1)}% (latest)`);
|
|
8843
|
+
}
|
|
8844
|
+
const tokenBench = benchEntries.filter((e) => e.type === 'token-reduction').slice(-n);
|
|
8845
|
+
if (tokenBench.length > 0) {
|
|
8846
|
+
const reds = tokenBench.map((e) => e.reduction || e.avgReductionPct || 0);
|
|
8847
|
+
console.log(` tok reduce : ${sparkline(reds)} ${reds.at(-1)}% (latest)`);
|
|
8848
|
+
}
|
|
8849
|
+
} catch (_) {}
|
|
8518
8850
|
}
|
|
8519
|
-
|
|
8520
|
-
console.log(` Token trend: ${spark}`);
|
|
8851
|
+
|
|
8521
8852
|
console.log(bar);
|
|
8522
8853
|
process.exit(0);
|
|
8523
8854
|
}
|
|
@@ -8972,7 +9303,7 @@ function main() {
|
|
|
8972
9303
|
const topK = topIdx >= 0 ? Math.min(Math.max(1, parseInt(args[topIdx + 1], 10) || 10), 25)
|
|
8973
9304
|
: ((config && config.retrieval && config.retrieval.topK) || 10);
|
|
8974
9305
|
const recencyBoost = (config && config.retrieval && config.retrieval.recencyBoost) || 1.5;
|
|
8975
|
-
const results = rank(query, index, { topK, recencyBoost });
|
|
9306
|
+
const results = rank(query, index, { topK, recencyBoost, cwd });
|
|
8976
9307
|
if (args.includes('--context')) {
|
|
8977
9308
|
const miniCtx = buildMiniContext(results, cwd);
|
|
8978
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
|
package/src/format/dashboard.js
CHANGED
|
@@ -140,6 +140,26 @@ function computeExtractorCoverage(cwd) {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
function readBenchmarkTrend(cwd) {
|
|
143
|
+
// Prefer per-user history file written by benchmark scripts
|
|
144
|
+
const histPath = path.join(cwd, '.context', 'benchmark-history.ndjson');
|
|
145
|
+
if (fs.existsSync(histPath)) {
|
|
146
|
+
const values = [];
|
|
147
|
+
try {
|
|
148
|
+
const lines = fs.readFileSync(histPath, 'utf8').trim().split('\n').filter(Boolean);
|
|
149
|
+
for (const line of lines) {
|
|
150
|
+
try {
|
|
151
|
+
const obj = JSON.parse(line);
|
|
152
|
+
if (obj.type === 'retrieval') {
|
|
153
|
+
const v = toNumber(obj.hitAt5Pct);
|
|
154
|
+
if (v !== null) values.push(v);
|
|
155
|
+
}
|
|
156
|
+
} catch (_) {}
|
|
157
|
+
}
|
|
158
|
+
} catch (_) {}
|
|
159
|
+
if (values.length > 0) return values.slice(-30);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Fallback: legacy benchmarks/results directory (CI artifacts)
|
|
143
163
|
const resultDir = path.join(cwd, 'benchmarks', 'results');
|
|
144
164
|
if (!fs.existsSync(resultDir)) return [];
|
|
145
165
|
|
|
@@ -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,
|