sigmap 7.3.0 → 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/gen-context.js +784 -51
- package/llms-full.txt +29 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/cache/freshen.js +123 -0
- package/src/extractors/dispatch.js +112 -0
- package/src/mcp/handlers.js +79 -1
- package/src/mcp/server.js +5 -2
- package/src/mcp/tools.js +45 -2
package/gen-context.js
CHANGED
|
@@ -20,6 +20,248 @@ function __require(key) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
// ── ./src/cache/freshen ──
|
|
24
|
+
__factories["./src/cache/freshen"] = function(module, exports) {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read-time self-heal (IMPL.md Layer 1, "safety net" tier).
|
|
28
|
+
*
|
|
29
|
+
* Keeps the sig-cache in line with the current source tree so the index reflects
|
|
30
|
+
* on-disk reality even when no write hook was called. Re-extracts files modified
|
|
31
|
+
* since the context file was generated (bounded to actual session edits, not the
|
|
32
|
+
* whole tree), drops cache entries for deleted files, and persists. buildSigIndex
|
|
33
|
+
* already merges the cache, so the next read is fresh.
|
|
34
|
+
*
|
|
35
|
+
* Throttled per cwd. Skips entirely when there is no generated index to heal
|
|
36
|
+
* (a cold repo should run `generate` or use the notify hooks).
|
|
37
|
+
*
|
|
38
|
+
* Zero-dependency, bundle-safe (fs + dispatch + sig-cache).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const fs = require('fs');
|
|
42
|
+
const path = require('path');
|
|
43
|
+
const { loadCache, saveCache, getChangedFiles } = __require('./src/cache/sig-cache');
|
|
44
|
+
const { extractFile, langFor } = __require('./src/extractors/dispatch');
|
|
45
|
+
|
|
46
|
+
const DEFAULT_SRC_DIRS = ['src', 'app', 'lib', 'packages', 'services', 'api'];
|
|
47
|
+
const DEFAULT_EXCLUDE = [
|
|
48
|
+
'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
|
|
49
|
+
'.next', 'coverage', 'target', 'vendor', '.context',
|
|
50
|
+
];
|
|
51
|
+
const CONTEXT_PATHS = [
|
|
52
|
+
['.github', 'copilot-instructions.md'],
|
|
53
|
+
['CLAUDE.md'], ['AGENTS.md'], ['.github', 'context-cold.md'],
|
|
54
|
+
];
|
|
55
|
+
const THROTTLE_MS = 1500;
|
|
56
|
+
const _lastRun = new Map();
|
|
57
|
+
|
|
58
|
+
function _readConfig(cwd) {
|
|
59
|
+
try {
|
|
60
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
|
|
61
|
+
return cfg && typeof cfg === 'object' ? cfg : {};
|
|
62
|
+
} catch (_) { return {}; }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _pkgVersion(cwd) {
|
|
66
|
+
try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
|
|
67
|
+
catch (_) { return '0.0.0'; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Newest mtime among existing generated context files, or 0 if none. */
|
|
71
|
+
function _contextMtime(cwd) {
|
|
72
|
+
let newest = 0;
|
|
73
|
+
for (const parts of CONTEXT_PATHS) {
|
|
74
|
+
try { newest = Math.max(newest, fs.statSync(path.join(cwd, ...parts)).mtimeMs); } catch (_) {}
|
|
75
|
+
}
|
|
76
|
+
return newest;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _walk(dir, exclude, out, depth, maxDepth) {
|
|
80
|
+
if (depth > maxDepth) return;
|
|
81
|
+
let entries;
|
|
82
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
83
|
+
for (const e of entries) {
|
|
84
|
+
if (exclude.has(e.name)) continue;
|
|
85
|
+
const full = path.join(dir, e.name);
|
|
86
|
+
if (e.isDirectory()) _walk(full, exclude, out, depth + 1, maxDepth);
|
|
87
|
+
else if (e.isFile() && langFor(e.name)) out.push(full);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Re-extract source files changed since the last generate; drop deleted files.
|
|
93
|
+
* @param {string} cwd
|
|
94
|
+
* @param {{force?:boolean, now?:number}} [opts]
|
|
95
|
+
* @returns {number} cache entries touched
|
|
96
|
+
*/
|
|
97
|
+
function freshen(cwd, opts = {}) {
|
|
98
|
+
const now = opts.now != null ? opts.now : Date.now();
|
|
99
|
+
if (!opts.force) {
|
|
100
|
+
if (now - (_lastRun.get(cwd) || 0) < THROTTLE_MS) return 0;
|
|
101
|
+
}
|
|
102
|
+
_lastRun.set(cwd, now);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const version = _pkgVersion(cwd);
|
|
106
|
+
const cache = loadCache(cwd, version);
|
|
107
|
+
const ctxMtime = _contextMtime(cwd);
|
|
108
|
+
// Nothing to heal: no generated context AND no live cache overlay.
|
|
109
|
+
if (ctxMtime === 0 && cache.size === 0) return 0;
|
|
110
|
+
|
|
111
|
+
const cfg = _readConfig(cwd);
|
|
112
|
+
const srcDirs = Array.isArray(cfg.srcDirs) && cfg.srcDirs.length ? cfg.srcDirs : DEFAULT_SRC_DIRS;
|
|
113
|
+
const exclude = new Set([...DEFAULT_EXCLUDE, ...(Array.isArray(cfg.exclude) ? cfg.exclude : [])]);
|
|
114
|
+
const maxDepth = Number.isFinite(cfg.maxDepth) ? cfg.maxDepth : 8;
|
|
115
|
+
|
|
116
|
+
const files = [];
|
|
117
|
+
for (const d of srcDirs) {
|
|
118
|
+
const abs = path.isAbsolute(d) ? d : path.join(cwd, d);
|
|
119
|
+
if (fs.existsSync(abs)) _walk(abs, exclude, files, 0, maxDepth);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Candidates = files modified since the context was generated, or not yet cached.
|
|
123
|
+
const candidates = files.filter((f) => {
|
|
124
|
+
try { return fs.statSync(f).mtimeMs > ctxMtime || !cache.has(f); } catch (_) { return false; }
|
|
125
|
+
});
|
|
126
|
+
const { changed } = getChangedFiles(candidates, cache);
|
|
127
|
+
|
|
128
|
+
let touched = 0;
|
|
129
|
+
for (const f of changed) {
|
|
130
|
+
try {
|
|
131
|
+
const sigs = extractFile(f, fs.readFileSync(f, 'utf8'));
|
|
132
|
+
cache.set(f, { mtime: fs.statSync(f).mtimeMs, sigs });
|
|
133
|
+
touched++;
|
|
134
|
+
} catch (_) {}
|
|
135
|
+
}
|
|
136
|
+
// Note: deletions are NOT swept here — a cache entry may be a `notify`
|
|
137
|
+
// overlay for a file not yet on disk. Explicit removal is `notify_file_deleted`.
|
|
138
|
+
|
|
139
|
+
if (touched > 0) saveCache(cwd, version, cache);
|
|
140
|
+
return touched;
|
|
141
|
+
} catch (_) {
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { freshen };
|
|
147
|
+
|
|
148
|
+
};
|
|
149
|
+
// ── ./src/extractors/dispatch ──
|
|
150
|
+
__factories["./src/extractors/dispatch"] = function(module, exports) {
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Bundle-safe extractor dispatch.
|
|
154
|
+
*
|
|
155
|
+
* `packages/core`'s extract() resolves extractors via dynamic `require(path.join(…))`,
|
|
156
|
+
* which cannot run inside the standalone bundle (no filesystem `src/`). This module
|
|
157
|
+
* uses STATIC requires so the bundler rewrites them to `__require` and the extractors
|
|
158
|
+
* resolve from the bundled factories. Used by the live-index MCP write hooks.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
const path = require('path');
|
|
162
|
+
|
|
163
|
+
// Static language → extractor map (every entry is a bundled factory).
|
|
164
|
+
const EXTRACTORS = {
|
|
165
|
+
typescript: __require('./src/extractors/typescript'),
|
|
166
|
+
typescript_react: __require('./src/extractors/typescript_react'),
|
|
167
|
+
javascript: __require('./src/extractors/javascript'),
|
|
168
|
+
python: __require('./src/extractors/python'),
|
|
169
|
+
java: __require('./src/extractors/java'),
|
|
170
|
+
kotlin: __require('./src/extractors/kotlin'),
|
|
171
|
+
go: __require('./src/extractors/go'),
|
|
172
|
+
rust: __require('./src/extractors/rust'),
|
|
173
|
+
csharp: __require('./src/extractors/csharp'),
|
|
174
|
+
cpp: __require('./src/extractors/cpp'),
|
|
175
|
+
ruby: __require('./src/extractors/ruby'),
|
|
176
|
+
php: __require('./src/extractors/php'),
|
|
177
|
+
swift: __require('./src/extractors/swift'),
|
|
178
|
+
dart: __require('./src/extractors/dart'),
|
|
179
|
+
scala: __require('./src/extractors/scala'),
|
|
180
|
+
gdscript: __require('./src/extractors/gdscript'),
|
|
181
|
+
r: __require('./src/extractors/r'),
|
|
182
|
+
vue: __require('./src/extractors/vue'),
|
|
183
|
+
vue_sfc: __require('./src/extractors/vue_sfc'),
|
|
184
|
+
svelte: __require('./src/extractors/svelte'),
|
|
185
|
+
html: __require('./src/extractors/html'),
|
|
186
|
+
css: __require('./src/extractors/css'),
|
|
187
|
+
yaml: __require('./src/extractors/yaml'),
|
|
188
|
+
shell: __require('./src/extractors/shell'),
|
|
189
|
+
sql: __require('./src/extractors/sql'),
|
|
190
|
+
graphql: __require('./src/extractors/graphql'),
|
|
191
|
+
terraform: __require('./src/extractors/terraform'),
|
|
192
|
+
protobuf: __require('./src/extractors/protobuf'),
|
|
193
|
+
toml: __require('./src/extractors/toml'),
|
|
194
|
+
properties: __require('./src/extractors/properties'),
|
|
195
|
+
xml: __require('./src/extractors/xml'),
|
|
196
|
+
markdown: __require('./src/extractors/markdown'),
|
|
197
|
+
dockerfile: __require('./src/extractors/dockerfile'),
|
|
198
|
+
generic: __require('./src/extractors/generic'),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const EXT_MAP = {
|
|
202
|
+
'.ts': 'typescript', '.tsx': 'typescript_react',
|
|
203
|
+
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
204
|
+
'.py': 'python', '.pyw': 'python',
|
|
205
|
+
'.java': 'java',
|
|
206
|
+
'.kt': 'kotlin', '.kts': 'kotlin',
|
|
207
|
+
'.go': 'go',
|
|
208
|
+
'.rs': 'rust',
|
|
209
|
+
'.cs': 'csharp',
|
|
210
|
+
'.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
|
|
211
|
+
'.rb': 'ruby', '.rake': 'ruby',
|
|
212
|
+
'.php': 'php',
|
|
213
|
+
'.swift': 'swift',
|
|
214
|
+
'.dart': 'dart',
|
|
215
|
+
'.scala': 'scala', '.sc': 'scala',
|
|
216
|
+
'.gd': 'gdscript',
|
|
217
|
+
'.r': 'r', '.R': 'r',
|
|
218
|
+
'.vue': 'vue_sfc',
|
|
219
|
+
'.svelte': 'svelte',
|
|
220
|
+
'.html': 'html', '.htm': 'html',
|
|
221
|
+
'.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
|
|
222
|
+
'.yml': 'yaml', '.yaml': 'yaml',
|
|
223
|
+
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
|
|
224
|
+
'.sql': 'sql',
|
|
225
|
+
'.graphql': 'graphql', '.gql': 'graphql',
|
|
226
|
+
'.tf': 'terraform', '.tfvars': 'terraform',
|
|
227
|
+
'.proto': 'protobuf',
|
|
228
|
+
'.toml': 'toml',
|
|
229
|
+
'.properties': 'properties',
|
|
230
|
+
'.xml': 'xml',
|
|
231
|
+
'.md': 'markdown',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** Resolve a language key from a file path/name. */
|
|
235
|
+
function langFor(filePathOrName) {
|
|
236
|
+
const base = path.basename(String(filePathOrName || ''));
|
|
237
|
+
if (base === 'Dockerfile' || base.startsWith('Dockerfile.')) return 'dockerfile';
|
|
238
|
+
const ext = path.extname(base).toLowerCase();
|
|
239
|
+
return EXT_MAP[ext] || null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Extract signatures from a file's content using the right extractor.
|
|
244
|
+
* @param {string} filePathOrName - path or name (extension drives the extractor)
|
|
245
|
+
* @param {string} src - file content
|
|
246
|
+
* @returns {string[]}
|
|
247
|
+
*/
|
|
248
|
+
function extractFile(filePathOrName, src) {
|
|
249
|
+
if (!src || typeof src !== 'string') return [];
|
|
250
|
+
const lang = langFor(filePathOrName);
|
|
251
|
+
const mod = lang ? EXTRACTORS[lang] : null;
|
|
252
|
+
if (!mod || typeof mod.extract !== 'function') return [];
|
|
253
|
+
try {
|
|
254
|
+
const out = mod.extract(src);
|
|
255
|
+
return Array.isArray(out) ? out : [];
|
|
256
|
+
} catch (_) {
|
|
257
|
+
return [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = { extractFile, langFor };
|
|
262
|
+
|
|
263
|
+
};
|
|
264
|
+
|
|
23
265
|
// ── ./src/config/defaults ──
|
|
24
266
|
__factories["./src/config/defaults"] = function(module, exports) {
|
|
25
267
|
|
|
@@ -5601,6 +5843,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
5601
5843
|
|
|
5602
5844
|
const query = args.query.toLowerCase();
|
|
5603
5845
|
try {
|
|
5846
|
+
try { __require('./src/cache/freshen').freshen(cwd); } catch (_) {}
|
|
5604
5847
|
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
5605
5848
|
const index = buildSigIndex(cwd);
|
|
5606
5849
|
if (index.size === 0) {
|
|
@@ -6088,6 +6331,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
6088
6331
|
}
|
|
6089
6332
|
|
|
6090
6333
|
try {
|
|
6334
|
+
try { __require('./src/cache/freshen').freshen(cwd); } catch (_) {}
|
|
6091
6335
|
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
6092
6336
|
const { buildSymbolCandidates, closestMatch, formatSuggestion } = __require('./src/verify/closest-match');
|
|
6093
6337
|
const index = buildSigIndex(cwd);
|
|
@@ -6125,7 +6369,83 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
6125
6369
|
}
|
|
6126
6370
|
}
|
|
6127
6371
|
|
|
6128
|
-
|
|
6372
|
+
// ── Layer 1: live-index write hooks ────────────────────────────────────────
|
|
6373
|
+
// Keep the sig-cache fresh while an agent creates/modifies/deletes files, so
|
|
6374
|
+
// new code is discoverable in the same session. buildSigIndex already merges
|
|
6375
|
+
// the cache (_buildSigIndexFromCache), so updates are live on the next read.
|
|
6376
|
+
|
|
6377
|
+
function _pkgVersion(cwd) {
|
|
6378
|
+
try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
|
|
6379
|
+
catch (_) { return '0.0.0'; }
|
|
6380
|
+
}
|
|
6381
|
+
|
|
6382
|
+
|
|
6383
|
+
/** notify_file_created — extract a file's signatures and index it live. */
|
|
6384
|
+
function notifyFileCreated(args, cwd) {
|
|
6385
|
+
const rel = args && args.path;
|
|
6386
|
+
if (!rel) return 'Missing required argument: path';
|
|
6387
|
+
try {
|
|
6388
|
+
const { extractFile } = __require('./src/extractors/dispatch');
|
|
6389
|
+
const { loadCache, saveCache } = __require('./src/cache/sig-cache');
|
|
6390
|
+
const abs = path.resolve(cwd, rel);
|
|
6391
|
+
let content = args.content;
|
|
6392
|
+
if (typeof content !== 'string') {
|
|
6393
|
+
try { content = fs.readFileSync(abs, 'utf8'); } catch (_) { content = ''; }
|
|
6394
|
+
}
|
|
6395
|
+
const sigs = extractFile(abs, content);
|
|
6396
|
+
const version = _pkgVersion(cwd);
|
|
6397
|
+
const cache = loadCache(cwd, version);
|
|
6398
|
+
if (sigs.length > 0) {
|
|
6399
|
+
cache.set(abs, { mtime: Date.now(), sigs });
|
|
6400
|
+
}
|
|
6401
|
+
saveCache(cwd, version, cache);
|
|
6402
|
+
return `Indexed ${rel}: ${sigs.length} signature(s) now live.`;
|
|
6403
|
+
} catch (err) {
|
|
6404
|
+
return `_notify_file_created failed: ${err.message}_`;
|
|
6405
|
+
}
|
|
6406
|
+
}
|
|
6407
|
+
|
|
6408
|
+
/** notify_symbol_added — append one signature to a file's live cache entry. */
|
|
6409
|
+
function notifySymbolAdded(args, cwd) {
|
|
6410
|
+
if (!args || !args.signature || !args.file) {
|
|
6411
|
+
return 'Missing required arguments: signature, file';
|
|
6412
|
+
}
|
|
6413
|
+
try {
|
|
6414
|
+
const { loadCache, saveCache } = __require('./src/cache/sig-cache');
|
|
6415
|
+
const abs = path.resolve(cwd, args.file);
|
|
6416
|
+
const version = _pkgVersion(cwd);
|
|
6417
|
+
const cache = loadCache(cwd, version);
|
|
6418
|
+
const entry = cache.get(abs) || { mtime: Date.now(), sigs: [] };
|
|
6419
|
+
const line = Number.isFinite(Number(args.line)) ? ` :${args.line}` : '';
|
|
6420
|
+
const sig = String(args.signature) + line;
|
|
6421
|
+
if (!entry.sigs.includes(sig)) entry.sigs.push(sig);
|
|
6422
|
+
entry.mtime = Date.now();
|
|
6423
|
+
cache.set(abs, entry);
|
|
6424
|
+
saveCache(cwd, version, cache);
|
|
6425
|
+
return `Added signature to ${args.file} (${entry.sigs.length} total).`;
|
|
6426
|
+
} catch (err) {
|
|
6427
|
+
return `_notify_symbol_added failed: ${err.message}_`;
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
|
|
6431
|
+
/** notify_file_deleted — drop a file's cache-overlay entry. */
|
|
6432
|
+
function notifyFileDeleted(args, cwd) {
|
|
6433
|
+
const rel = args && args.path;
|
|
6434
|
+
if (!rel) return 'Missing required argument: path';
|
|
6435
|
+
try {
|
|
6436
|
+
const { loadCache, saveCache } = __require('./src/cache/sig-cache');
|
|
6437
|
+
const abs = path.resolve(cwd, rel);
|
|
6438
|
+
const version = _pkgVersion(cwd);
|
|
6439
|
+
const cache = loadCache(cwd, version);
|
|
6440
|
+
const had = cache.delete(abs);
|
|
6441
|
+
saveCache(cwd, version, cache);
|
|
6442
|
+
return had ? `Removed ${rel} from the live index.` : `${rel} was not in the live cache.`;
|
|
6443
|
+
} catch (err) {
|
|
6444
|
+
return `_notify_file_deleted failed: ${err.message}_`;
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
6447
|
+
|
|
6448
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted };
|
|
6129
6449
|
|
|
6130
6450
|
};
|
|
6131
6451
|
// ── ./src/learning/weights ──
|
|
@@ -6302,11 +6622,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
6302
6622
|
|
|
6303
6623
|
const readline = require('readline');
|
|
6304
6624
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
6305
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures } = __require('./src/mcp/handlers');
|
|
6625
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted } = __require('./src/mcp/handlers');
|
|
6306
6626
|
|
|
6307
6627
|
const SERVER_INFO = {
|
|
6308
6628
|
name: 'sigmap',
|
|
6309
|
-
version: '7.
|
|
6629
|
+
version: '7.5.0',
|
|
6310
6630
|
description: 'SigMap MCP server — code signatures on demand',
|
|
6311
6631
|
};
|
|
6312
6632
|
|
|
@@ -6366,6 +6686,9 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
6366
6686
|
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
6367
6687
|
else if (name === 'read_memory') text = readMemory(args, cwd);
|
|
6368
6688
|
else if (name === 'get_callee_signatures') text = getCalleeSignatures(args, cwd);
|
|
6689
|
+
else if (name === 'sigmap_notify_file_created') text = notifyFileCreated(args, cwd);
|
|
6690
|
+
else if (name === 'sigmap_notify_symbol_added') text = notifySymbolAdded(args, cwd);
|
|
6691
|
+
else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
|
|
6369
6692
|
else {
|
|
6370
6693
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
6371
6694
|
return;
|
|
@@ -6425,10 +6748,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
6425
6748
|
__factories["./src/mcp/tools"] = function(module, exports) {
|
|
6426
6749
|
|
|
6427
6750
|
/**
|
|
6428
|
-
* MCP tool definitions for SigMap (
|
|
6751
|
+
* MCP tool definitions for SigMap (15 tools).
|
|
6429
6752
|
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
6430
6753
|
* explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
|
|
6431
|
-
* get_callee_signatures
|
|
6754
|
+
* get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
|
|
6755
|
+
* sigmap_notify_file_deleted.
|
|
6432
6756
|
*/
|
|
6433
6757
|
|
|
6434
6758
|
const TOOLS = [
|
|
@@ -6659,6 +6983,48 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
6659
6983
|
required: ['symbols'],
|
|
6660
6984
|
},
|
|
6661
6985
|
},
|
|
6986
|
+
{
|
|
6987
|
+
name: 'sigmap_notify_file_created',
|
|
6988
|
+
description:
|
|
6989
|
+
'Tell SigMap a file was created or modified so its signatures are indexed ' +
|
|
6990
|
+
'live for the rest of the session. Call this after writing a file — the new ' +
|
|
6991
|
+
'symbols become resolvable by search_signatures / get_callee_signatures.',
|
|
6992
|
+
inputSchema: {
|
|
6993
|
+
type: 'object',
|
|
6994
|
+
properties: {
|
|
6995
|
+
path: { type: 'string', description: 'File path relative to the project root.' },
|
|
6996
|
+
content: { type: 'string', description: 'Optional file content; read from disk if omitted.' },
|
|
6997
|
+
},
|
|
6998
|
+
required: ['path'],
|
|
6999
|
+
},
|
|
7000
|
+
},
|
|
7001
|
+
{
|
|
7002
|
+
name: 'sigmap_notify_symbol_added',
|
|
7003
|
+
description:
|
|
7004
|
+
'Fast path: register a single new symbol signature directly in the live ' +
|
|
7005
|
+
'index without re-reading the whole file.',
|
|
7006
|
+
inputSchema: {
|
|
7007
|
+
type: 'object',
|
|
7008
|
+
properties: {
|
|
7009
|
+
signature: { type: 'string', description: 'The signature line (e.g. "function check(key)").' },
|
|
7010
|
+
file: { type: 'string', description: 'File path the symbol belongs to (relative to root).' },
|
|
7011
|
+
line: { type: 'number', description: 'Optional 1-based line number.' },
|
|
7012
|
+
},
|
|
7013
|
+
required: ['signature', 'file'],
|
|
7014
|
+
},
|
|
7015
|
+
},
|
|
7016
|
+
{
|
|
7017
|
+
name: 'sigmap_notify_file_deleted',
|
|
7018
|
+
description:
|
|
7019
|
+
'Tell SigMap a file was deleted so its symbols are dropped from the live index.',
|
|
7020
|
+
inputSchema: {
|
|
7021
|
+
type: 'object',
|
|
7022
|
+
properties: {
|
|
7023
|
+
path: { type: 'string', description: 'Deleted file path relative to the project root.' },
|
|
7024
|
+
},
|
|
7025
|
+
required: ['path'],
|
|
7026
|
+
},
|
|
7027
|
+
},
|
|
6662
7028
|
];
|
|
6663
7029
|
|
|
6664
7030
|
module.exports = { TOOLS };
|
|
@@ -7979,61 +8345,293 @@ __factories["./src/retrieval/tokenizer"] = function(module, exports) {
|
|
|
7979
8345
|
|
|
7980
8346
|
// ── ./src/retrieval/ranker ──
|
|
7981
8347
|
__factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
7982
|
-
|
|
8348
|
+
|
|
8349
|
+
/**
|
|
8350
|
+
* SigMap zero-dependency relevance ranker.
|
|
8351
|
+
*
|
|
8352
|
+
* Ranks all files in a signature index against a natural-language query.
|
|
8353
|
+
* Scoring weights:
|
|
8354
|
+
* - keyword overlap (exact token match against sigs)
|
|
8355
|
+
* - symbol match (token appears in a top-level identifier / function name)
|
|
8356
|
+
* - partial prefix match (token is prefix of a sig token, length ≥ 4)
|
|
8357
|
+
* - path relevance (query token appears in the file path)
|
|
8358
|
+
* - recency boost (applied externally via recency map)
|
|
8359
|
+
*
|
|
8360
|
+
* Usage:
|
|
8361
|
+
* const { rank } = __require('./src/retrieval/src/retrieval/ranker');
|
|
8362
|
+
* const results = rank(query, sigIndex, { topK: 10 });
|
|
8363
|
+
* // results: [{ file, score, sigs, tokens }]
|
|
8364
|
+
*/
|
|
8365
|
+
|
|
7983
8366
|
const { loadWeights } = __require('./src/learning/weights');
|
|
7984
8367
|
const { tokenize, STOP_WORDS } = __require('./src/retrieval/tokenizer');
|
|
8368
|
+
|
|
8369
|
+
// ---------------------------------------------------------------------------
|
|
8370
|
+
// Default weights
|
|
8371
|
+
// ---------------------------------------------------------------------------
|
|
7985
8372
|
const DEFAULT_WEIGHTS = {
|
|
7986
|
-
exactToken: 1.0,
|
|
8373
|
+
exactToken: 1.0, // query token exactly in sig tokens
|
|
8374
|
+
symbolMatch: 0.5, // bonus if token appears in a function/class name line
|
|
8375
|
+
prefixMatch: 0.3, // partial prefix hit (query token ≥ 4 chars)
|
|
8376
|
+
pathMatch: 0.8, // query token appears in the file path
|
|
8377
|
+
recencyBoost: 1.5, // multiplier applied when file is in recencySet
|
|
8378
|
+
graphBoost: 0.4, // additive bonus for 1-hop import neighbors of matching files
|
|
7987
8379
|
};
|
|
8380
|
+
|
|
8381
|
+
// Graph boost amounts for 2-hop traversal with decay (v6.7)
|
|
8382
|
+
const GRAPH_BOOST_AMOUNTS = {
|
|
8383
|
+
hop1: 0.40, // direct import neighbor of a file with score > 0
|
|
8384
|
+
hop2: 0.15, // 2 hops away (transitive), with decay
|
|
8385
|
+
};
|
|
8386
|
+
|
|
8387
|
+
// Intent-specific weight adjustments
|
|
8388
|
+
const INTENT_WEIGHTS = {
|
|
8389
|
+
search: DEFAULT_WEIGHTS,
|
|
8390
|
+
debug: { ...DEFAULT_WEIGHTS, exactToken: 1.2, pathMatch: 0.6 },
|
|
8391
|
+
explain: { ...DEFAULT_WEIGHTS, symbolMatch: 0.8, pathMatch: 0.9 },
|
|
8392
|
+
refactor: { ...DEFAULT_WEIGHTS, symbolMatch: 0.9, exactToken: 0.8 },
|
|
8393
|
+
review: { ...DEFAULT_WEIGHTS, pathMatch: 1.0, exactToken: 0.9 },
|
|
8394
|
+
test: { ...DEFAULT_WEIGHTS, exactToken: 0.7, symbolMatch: 0.4 },
|
|
8395
|
+
integrate: { ...DEFAULT_WEIGHTS, graphBoost: 0.7, pathMatch: 1.1 },
|
|
8396
|
+
navigate: { ...DEFAULT_WEIGHTS, pathMatch: 1.2, exactToken: 0.9 },
|
|
8397
|
+
};
|
|
8398
|
+
|
|
8399
|
+
// Penalty multipliers for negative signals
|
|
8400
|
+
const PENALTY_SIGNALS = {
|
|
8401
|
+
testFile: 0.4, // test/spec/__tests__ in path
|
|
8402
|
+
generatedCode: 0.3, // dist/build/.next in path
|
|
8403
|
+
docsFile: 0.2, // docs/doc/README in path
|
|
8404
|
+
nodeModules: 0.0, // node_modules (zero score)
|
|
8405
|
+
};
|
|
8406
|
+
|
|
8407
|
+
function _computePenalty(filePath) {
|
|
8408
|
+
const pathLower = filePath.toLowerCase();
|
|
8409
|
+
if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
|
|
8410
|
+
if (/(^|\/)(test|tests|spec|__tests__|e2e)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.testFile;
|
|
8411
|
+
if (/(^|\/)(dist|build|\.next|\.nuxt|out|\.venv|venv)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.generatedCode;
|
|
8412
|
+
if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.docsFile;
|
|
8413
|
+
return 1.0;
|
|
8414
|
+
}
|
|
8415
|
+
|
|
8416
|
+
// Detect hub files: those with fanout > 20% of all files in the graph
|
|
8417
|
+
function _computeHubs(graph) {
|
|
8418
|
+
if (!graph || !graph.reverse) return new Set();
|
|
8419
|
+
const fileCount = Math.max(1, graph.reverse.size);
|
|
8420
|
+
const threshold = Math.ceil(fileCount * 0.2);
|
|
8421
|
+
const hubs = new Set();
|
|
8422
|
+
for (const [file, deps] of graph.reverse) {
|
|
8423
|
+
if ((deps && deps.size >= threshold) || (Array.isArray(deps) && deps.length >= threshold)) {
|
|
8424
|
+
hubs.add(file);
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
return hubs;
|
|
8428
|
+
}
|
|
8429
|
+
|
|
8430
|
+
// Common utility paths that should be treated as hubs regardless of fanout
|
|
8431
|
+
function _isHub(filePath) {
|
|
8432
|
+
return /\/(utils|helpers|shared|common|constants|types|interfaces|index|zzz|globals)\.(ts|tsx|js|jsx|r|R)$/.test(filePath)
|
|
8433
|
+
|| filePath.endsWith('/index.ts') || filePath.endsWith('/index.js')
|
|
8434
|
+
|| filePath.endsWith('/R/utils.R') || filePath.endsWith('/R/zzz.R') || filePath.endsWith('/R/globals.R');
|
|
8435
|
+
}
|
|
8436
|
+
|
|
8437
|
+
/**
|
|
8438
|
+
* Score a single file against a query, returning detailed signal breakdown.
|
|
8439
|
+
*
|
|
8440
|
+
* @param {string} filePath - relative file path (e.g. 'src/extractors/python.js')
|
|
8441
|
+
* @param {string[]} sigs - signature strings for this file
|
|
8442
|
+
* @param {string[]} queryTokens - pre-tokenized query
|
|
8443
|
+
* @param {object} weights
|
|
8444
|
+
* @returns {{ score: number, signals: { exactToken: number, symbolMatch: number, prefixMatch: number, pathMatch: number, penalty: number } }}
|
|
8445
|
+
*/
|
|
7988
8446
|
function scoreFile(filePath, sigs, queryTokens, weights) {
|
|
7989
|
-
if (!sigs || sigs.length === 0) return 0;
|
|
8447
|
+
if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
|
|
8448
|
+
|
|
7990
8449
|
const w = weights || DEFAULT_WEIGHTS;
|
|
7991
|
-
const
|
|
8450
|
+
const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath) };
|
|
8451
|
+
|
|
8452
|
+
// Build token set from all signatures
|
|
8453
|
+
const sigText = sigs.join(' ');
|
|
8454
|
+
const sigTokenSet = new Set(tokenize(sigText));
|
|
8455
|
+
|
|
8456
|
+
// Build token set from the file path
|
|
7992
8457
|
const pathTokenSet = new Set(tokenize(filePath));
|
|
8458
|
+
|
|
7993
8459
|
let score = 0;
|
|
8460
|
+
|
|
7994
8461
|
for (const qt of queryTokens) {
|
|
7995
8462
|
if (STOP_WORDS.has(qt)) continue;
|
|
8463
|
+
|
|
8464
|
+
// Exact token match in sigs
|
|
7996
8465
|
if (sigTokenSet.has(qt)) {
|
|
7997
|
-
|
|
7998
|
-
|
|
8466
|
+
const bonus = w.exactToken;
|
|
8467
|
+
score += bonus;
|
|
8468
|
+
signals.exactToken += bonus;
|
|
8469
|
+
|
|
8470
|
+
// Bonus: appears directly in a function/class/method name line
|
|
8471
|
+
const nameLineMatch = sigs.some((sig) => {
|
|
8472
|
+
const nt = tokenize(sig.replace(/[^a-zA-Z0-9_\s]/g, ' '));
|
|
8473
|
+
return nt.includes(qt);
|
|
8474
|
+
});
|
|
8475
|
+
if (nameLineMatch) {
|
|
8476
|
+
score += w.symbolMatch;
|
|
8477
|
+
signals.symbolMatch += w.symbolMatch;
|
|
8478
|
+
}
|
|
7999
8479
|
}
|
|
8480
|
+
|
|
8481
|
+
// Prefix match (e.g. query "python" matches "pythonDeps")
|
|
8000
8482
|
if (qt.length >= 4) {
|
|
8001
8483
|
for (const st of sigTokenSet) {
|
|
8002
|
-
if (st !== qt && st.startsWith(qt)) {
|
|
8484
|
+
if (st !== qt && st.startsWith(qt)) {
|
|
8485
|
+
score += w.prefixMatch;
|
|
8486
|
+
signals.prefixMatch += w.prefixMatch;
|
|
8487
|
+
break; // one bonus per query token
|
|
8488
|
+
}
|
|
8003
8489
|
}
|
|
8004
8490
|
}
|
|
8005
|
-
|
|
8491
|
+
|
|
8492
|
+
// Path token match
|
|
8493
|
+
if (pathTokenSet.has(qt)) {
|
|
8494
|
+
score += w.pathMatch;
|
|
8495
|
+
signals.pathMatch += w.pathMatch;
|
|
8496
|
+
}
|
|
8006
8497
|
}
|
|
8007
|
-
|
|
8498
|
+
|
|
8499
|
+
// Apply penalty multiplier
|
|
8500
|
+
score *= signals.penalty;
|
|
8501
|
+
|
|
8502
|
+
return { score, signals };
|
|
8008
8503
|
}
|
|
8504
|
+
|
|
8505
|
+
/**
|
|
8506
|
+
* Rank all files in a signature index against a query.
|
|
8507
|
+
*
|
|
8508
|
+
* @param {string} query - natural language query
|
|
8509
|
+
* @param {Map<string,string[]>} sigIndex - Map<file, sigs[]>
|
|
8510
|
+
* @param {object} [opts]
|
|
8511
|
+
* @param {number} [opts.topK=10] - max results to return
|
|
8512
|
+
* @param {number} [opts.recencyBoost=1.5] - multiplier for recent files
|
|
8513
|
+
* @param {Set<string>} [opts.recencySet] - set of recently-changed file paths
|
|
8514
|
+
* @param {object} [opts.weights] - override scoring weights
|
|
8515
|
+
* @param {string} [opts.cwd] - project root for learned ranking weights
|
|
8516
|
+
* @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
|
|
8517
|
+
* @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
|
|
8518
|
+
*/
|
|
8009
8519
|
function rank(query, sigIndex, opts) {
|
|
8010
8520
|
if (!query || typeof query !== 'string') return [];
|
|
8011
8521
|
if (!sigIndex || !(sigIndex instanceof Map) || sigIndex.size === 0) return [];
|
|
8522
|
+
|
|
8012
8523
|
const topK = (opts && opts.topK) || 10;
|
|
8013
8524
|
const recencyMultiplier = (opts && opts.recencyBoost) || DEFAULT_WEIGHTS.recencyBoost;
|
|
8014
8525
|
const recencySet = (opts && opts.recencySet) || null;
|
|
8015
|
-
const
|
|
8526
|
+
const graph = (opts && opts.graph && opts.graph.forward instanceof Map) ? opts.graph : null;
|
|
8527
|
+
const cwd = (opts && opts.cwd) || null;
|
|
8528
|
+
|
|
8529
|
+
// Detect query intent and get appropriate weights
|
|
8530
|
+
const intent = detectIntent(query);
|
|
8531
|
+
const intentWeights = INTENT_WEIGHTS[intent] || DEFAULT_WEIGHTS;
|
|
8532
|
+
const weights = (opts && opts.weights) ? Object.assign({}, intentWeights, opts.weights) : intentWeights;
|
|
8016
8533
|
const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;
|
|
8534
|
+
|
|
8017
8535
|
const queryTokens = tokenize(query);
|
|
8018
8536
|
if (queryTokens.length === 0) {
|
|
8537
|
+
// Empty query: return top-K by file count (most signatures = most useful)
|
|
8019
8538
|
const all = [];
|
|
8020
|
-
for (const [file, sigs] of sigIndex.entries())
|
|
8539
|
+
for (const [file, sigs] of sigIndex.entries()) {
|
|
8540
|
+
all.push({ file, score: sigs.length, sigs, tokens: Math.ceil(sigs.join('\n').length / 4), intent, signals: {} });
|
|
8541
|
+
}
|
|
8021
8542
|
all.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
8022
8543
|
return all.slice(0, topK);
|
|
8023
8544
|
}
|
|
8545
|
+
|
|
8024
8546
|
const scored = [];
|
|
8025
8547
|
for (const [file, sigs] of sigIndex.entries()) {
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8548
|
+
const result = scoreFile(file, sigs, queryTokens, weights);
|
|
8549
|
+
let score = result.score;
|
|
8550
|
+
const signals = result.signals;
|
|
8551
|
+
|
|
8552
|
+
// Recency boost
|
|
8553
|
+
if (recencySet && recencySet.has(file) && score > 0) {
|
|
8554
|
+
score *= recencyMultiplier;
|
|
8555
|
+
signals.recencyBoost = recencyMultiplier;
|
|
8556
|
+
}
|
|
8557
|
+
|
|
8558
|
+
if (learnedWeights && score > 0) {
|
|
8559
|
+
const multiplier = learnedWeights[file] || 1.0;
|
|
8560
|
+
score *= multiplier;
|
|
8561
|
+
signals.learnedWeights = multiplier;
|
|
8562
|
+
}
|
|
8563
|
+
|
|
8564
|
+
scored.push({
|
|
8565
|
+
file,
|
|
8566
|
+
score,
|
|
8567
|
+
sigs,
|
|
8568
|
+
tokens: Math.ceil(sigs.join('\n').length / 4),
|
|
8569
|
+
intent,
|
|
8570
|
+
signals,
|
|
8571
|
+
});
|
|
8572
|
+
}
|
|
8573
|
+
|
|
8574
|
+
// Graph neighbor boost: 2-hop traversal with decay (v6.7)
|
|
8575
|
+
// Hop 1: add hop1 amount to direct import neighbors (score > 0)
|
|
8576
|
+
// Hop 2: add hop2 amount to neighbors of hop1 files (with decay)
|
|
8577
|
+
// Hub suppression: files with high fanout (>20%) are not boosted
|
|
8578
|
+
if (graph && cwd) {
|
|
8579
|
+
const path = require('path');
|
|
8580
|
+
// Build maps for relative ↔ absolute path conversion and index lookup
|
|
8581
|
+
const relToIdx = new Map();
|
|
8582
|
+
const absToRel = new Map();
|
|
8583
|
+
for (let i = 0; i < scored.length; i++) {
|
|
8584
|
+
relToIdx.set(scored[i].file, i);
|
|
8585
|
+
const abs = path.resolve(cwd, scored[i].file);
|
|
8586
|
+
absToRel.set(abs, scored[i].file);
|
|
8587
|
+
}
|
|
8588
|
+
|
|
8589
|
+
const hubs = _computeHubs(graph);
|
|
8590
|
+
const hop1Files = new Set(); // track which files received hop1 boost
|
|
8591
|
+
|
|
8592
|
+
// Hop 1: direct neighbors of scored files
|
|
8593
|
+
for (const entry of scored) {
|
|
8594
|
+
if (entry.score <= 0) continue;
|
|
8595
|
+
const abs = path.resolve(cwd, entry.file);
|
|
8596
|
+
const neighbors = graph.forward.get(abs) || [];
|
|
8597
|
+
for (const neighborAbs of neighbors) {
|
|
8598
|
+
if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
|
|
8599
|
+
const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
|
|
8600
|
+
const idx = relToIdx.get(neighborRel);
|
|
8601
|
+
if (idx !== undefined) {
|
|
8602
|
+
scored[idx].score += GRAPH_BOOST_AMOUNTS.hop1;
|
|
8603
|
+
scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop1;
|
|
8604
|
+
hop1Files.add(neighborAbs);
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8607
|
+
}
|
|
8608
|
+
|
|
8609
|
+
// Hop 2: neighbors of hop1 files (only if they didn't get a direct score)
|
|
8610
|
+
for (const hop1File of hop1Files) {
|
|
8611
|
+
if (!absToRel.has(hop1File)) continue; // skip files not in index
|
|
8612
|
+
const neighbors = graph.forward.get(hop1File) || [];
|
|
8613
|
+
for (const neighborAbs of neighbors) {
|
|
8614
|
+
if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
|
|
8615
|
+
if (hop1Files.has(neighborAbs)) continue; // skip already hop1-boosted
|
|
8616
|
+
const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
|
|
8617
|
+
const idx = relToIdx.get(neighborRel);
|
|
8618
|
+
if (idx !== undefined && scored[idx].score > 0) {
|
|
8619
|
+
// Only boost files that have some baseline score (not noise)
|
|
8620
|
+
scored[idx].score += GRAPH_BOOST_AMOUNTS.hop2;
|
|
8621
|
+
scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop2;
|
|
8622
|
+
}
|
|
8623
|
+
}
|
|
8624
|
+
}
|
|
8030
8625
|
}
|
|
8626
|
+
|
|
8031
8627
|
// Compute confidence levels based on score distribution
|
|
8032
8628
|
if (scored.length > 0) {
|
|
8033
8629
|
const scores = scored.map(s => s.score);
|
|
8034
8630
|
const maxScore = Math.max(...scores);
|
|
8035
8631
|
const minScore = Math.min(...scores);
|
|
8036
8632
|
const scoreRange = maxScore - minScore || 1;
|
|
8633
|
+
|
|
8634
|
+
// Confidence tiers: top 33% = high, next 33% = medium, rest = low
|
|
8037
8635
|
for (const entry of scored) {
|
|
8038
8636
|
if (entry.score <= 0) {
|
|
8039
8637
|
entry.confidence = 'low';
|
|
@@ -8043,39 +8641,73 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8043
8641
|
}
|
|
8044
8642
|
}
|
|
8045
8643
|
}
|
|
8644
|
+
|
|
8046
8645
|
scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
8047
8646
|
return scored.slice(0, topK);
|
|
8048
8647
|
}
|
|
8648
|
+
|
|
8649
|
+
/**
|
|
8650
|
+
* All paths where sigmap adapters write their context files, in probe order.
|
|
8651
|
+
* The first existing file with a non-empty index wins when no explicit path
|
|
8652
|
+
* is supplied.
|
|
8653
|
+
*/
|
|
8049
8654
|
const ADAPTER_OUTPUT_PATHS = [
|
|
8050
|
-
['.github', 'copilot-instructions.md'],
|
|
8051
|
-
['CLAUDE.md'],
|
|
8052
|
-
['AGENTS.md'],
|
|
8053
|
-
['.cursorrules'],
|
|
8054
|
-
['.windsurfrules'],
|
|
8055
|
-
['.github', 'openai-context.md'],
|
|
8056
|
-
['.github', 'gemini-context.md'],
|
|
8057
|
-
['llm-full.txt'],
|
|
8058
|
-
['llm.txt'],
|
|
8655
|
+
['.github', 'copilot-instructions.md'], // copilot (default)
|
|
8656
|
+
['CLAUDE.md'], // claude
|
|
8657
|
+
['AGENTS.md'], // codex
|
|
8658
|
+
['.cursorrules'], // cursor
|
|
8659
|
+
['.windsurfrules'], // windsurf
|
|
8660
|
+
['.github', 'openai-context.md'], // openai
|
|
8661
|
+
['.github', 'gemini-context.md'], // gemini
|
|
8662
|
+
['llm-full.txt'], // llm-full
|
|
8663
|
+
['llm.txt'], // llm
|
|
8059
8664
|
];
|
|
8665
|
+
|
|
8666
|
+
/**
|
|
8667
|
+
* Parse a single context file into a Map<filePath, string[]>.
|
|
8668
|
+
*
|
|
8669
|
+
* Files that contain human-written content before an
|
|
8670
|
+
* "## Auto-generated signatures" marker (e.g. CLAUDE.md) are handled
|
|
8671
|
+
* by skipping everything above the marker before scanning for ### headers.
|
|
8672
|
+
*
|
|
8673
|
+
* @param {string} contextPath - absolute path to the context file
|
|
8674
|
+
* @returns {Map<string, string[]>}
|
|
8675
|
+
*/
|
|
8060
8676
|
function _parseContextFile(contextPath) {
|
|
8061
8677
|
const fs = require('fs');
|
|
8062
8678
|
const index = new Map();
|
|
8679
|
+
|
|
8063
8680
|
if (!fs.existsSync(contextPath)) return index;
|
|
8681
|
+
|
|
8064
8682
|
let content = fs.readFileSync(contextPath, 'utf8');
|
|
8683
|
+
|
|
8684
|
+
// Skip any human-written preamble that sits above the auto-generated block.
|
|
8065
8685
|
const markerIdx = content.indexOf('## Auto-generated signatures');
|
|
8066
8686
|
if (markerIdx !== -1) content = content.slice(markerIdx);
|
|
8687
|
+
|
|
8067
8688
|
const lines = content.split('\n');
|
|
8068
|
-
let currentFile = null;
|
|
8689
|
+
let currentFile = null;
|
|
8690
|
+
let inBlock = false;
|
|
8691
|
+
let sigs = [];
|
|
8692
|
+
|
|
8069
8693
|
for (const line of lines) {
|
|
8070
|
-
const
|
|
8071
|
-
if (
|
|
8694
|
+
const headerMatch = line.match(/^###\s+(\S+)\s*$/);
|
|
8695
|
+
if (headerMatch) {
|
|
8696
|
+
if (currentFile !== null) index.set(currentFile, sigs);
|
|
8697
|
+
currentFile = headerMatch[1];
|
|
8698
|
+
sigs = [];
|
|
8699
|
+
inBlock = false;
|
|
8700
|
+
continue;
|
|
8701
|
+
}
|
|
8072
8702
|
if (line.startsWith('```')) { inBlock = !inBlock; continue; }
|
|
8073
8703
|
if (inBlock && currentFile && line.trim()) sigs.push(line.trim());
|
|
8074
8704
|
}
|
|
8075
8705
|
if (currentFile !== null) index.set(currentFile, sigs);
|
|
8706
|
+
|
|
8076
8707
|
return index;
|
|
8077
8708
|
}
|
|
8078
8709
|
|
|
8710
|
+
/** Merge source index into target; prefer non-empty sig lists. */
|
|
8079
8711
|
function _mergeSigIndex(target, source) {
|
|
8080
8712
|
for (const [file, sigs] of source.entries()) {
|
|
8081
8713
|
if (!sigs || sigs.length === 0) continue;
|
|
@@ -8086,12 +8718,17 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8086
8718
|
return target;
|
|
8087
8719
|
}
|
|
8088
8720
|
|
|
8721
|
+
/**
|
|
8722
|
+
* Load signatures from .sigmap-cache.json (absolute paths → repo-relative keys).
|
|
8723
|
+
* @param {string} cwd
|
|
8724
|
+
* @returns {Map<string, string[]>}
|
|
8725
|
+
*/
|
|
8089
8726
|
function _buildSigIndexFromCache(cwd) {
|
|
8090
8727
|
const fs = require('fs');
|
|
8091
8728
|
const path = require('path');
|
|
8092
8729
|
const index = new Map();
|
|
8093
8730
|
try {
|
|
8094
|
-
const { loadCache } =
|
|
8731
|
+
const { loadCache } = __require('./src/cache/sig-cache');
|
|
8095
8732
|
const pkgPath = path.join(cwd, 'package.json');
|
|
8096
8733
|
let version = '0.0.0';
|
|
8097
8734
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -8108,6 +8745,12 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8108
8745
|
return index;
|
|
8109
8746
|
}
|
|
8110
8747
|
|
|
8748
|
+
/**
|
|
8749
|
+
* Hot-cold and per-module strategies store most signatures outside the primary
|
|
8750
|
+
* copilot-instructions.md file. MCP tools must merge all sources.
|
|
8751
|
+
* @param {string} cwd
|
|
8752
|
+
* @returns {Map<string, string[]>}
|
|
8753
|
+
*/
|
|
8111
8754
|
function _enrichSigIndexFromStrategy(cwd, index) {
|
|
8112
8755
|
const path = require('path');
|
|
8113
8756
|
const coldPath = path.join(cwd, '.github', 'context-cold.md');
|
|
@@ -8116,49 +8759,138 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8116
8759
|
return index;
|
|
8117
8760
|
}
|
|
8118
8761
|
|
|
8762
|
+
/**
|
|
8763
|
+
* Build a signature index from the generated context file.
|
|
8764
|
+
* Returns Map<filePath, string[]> where filePath is the relative path
|
|
8765
|
+
* as it appears in the ### headers of the context file.
|
|
8766
|
+
*
|
|
8767
|
+
* Resolution priority:
|
|
8768
|
+
* 1. `opts.contextPath` — explicit path from --output or --adapter flag
|
|
8769
|
+
* 2. `customOutput` key in gen-context.config.json — persisted from a
|
|
8770
|
+
* previous `--output <file>` generation run
|
|
8771
|
+
* 3. All known adapter output paths probed in order (first non-empty wins)
|
|
8772
|
+
*
|
|
8773
|
+
* @param {string} cwd
|
|
8774
|
+
* @param {{ contextPath?: string }} [opts]
|
|
8775
|
+
* @returns {Map<string, string[]>}
|
|
8776
|
+
*/
|
|
8119
8777
|
function buildSigIndex(cwd, opts) {
|
|
8120
|
-
const fs
|
|
8121
|
-
|
|
8122
|
-
|
|
8778
|
+
const fs = require('fs');
|
|
8779
|
+
const path = require('path');
|
|
8780
|
+
|
|
8781
|
+
// 1. Caller supplied an explicit path — use it directly.
|
|
8782
|
+
if (opts && opts.contextPath) {
|
|
8783
|
+
const index = _parseContextFile(opts.contextPath);
|
|
8784
|
+
return _enrichSigIndexFromStrategy(cwd, index);
|
|
8785
|
+
}
|
|
8786
|
+
|
|
8787
|
+
// 2. Check gen-context.config.json for a persisted customOutput path.
|
|
8123
8788
|
try {
|
|
8124
8789
|
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
8125
8790
|
if (fs.existsSync(cfgPath)) {
|
|
8126
8791
|
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
8127
8792
|
if (cfg.customOutput) {
|
|
8128
|
-
const
|
|
8129
|
-
|
|
8793
|
+
const customPath = path.resolve(cwd, cfg.customOutput);
|
|
8794
|
+
const index = _parseContextFile(customPath);
|
|
8795
|
+
if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
|
|
8130
8796
|
}
|
|
8131
8797
|
}
|
|
8132
8798
|
} catch (_) {}
|
|
8799
|
+
|
|
8800
|
+
// 3. Probe all known adapter output paths; return first non-empty index.
|
|
8133
8801
|
for (const parts of ADAPTER_OUTPUT_PATHS) {
|
|
8134
8802
|
const contextPath = path.join(cwd, ...parts);
|
|
8135
8803
|
const index = _parseContextFile(contextPath);
|
|
8136
8804
|
if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
|
|
8137
8805
|
}
|
|
8138
|
-
|
|
8806
|
+
|
|
8807
|
+
// 4. Primary file empty/missing (hot-cold) — still serve cold + cache.
|
|
8808
|
+
const fallback = new Map();
|
|
8809
|
+
return _enrichSigIndexFromStrategy(cwd, fallback);
|
|
8139
8810
|
}
|
|
8811
|
+
|
|
8812
|
+
/**
|
|
8813
|
+
* Format ranked results as a markdown table string.
|
|
8814
|
+
*
|
|
8815
|
+
* @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
|
|
8816
|
+
* @param {string} query
|
|
8817
|
+
* @returns {string}
|
|
8818
|
+
*/
|
|
8140
8819
|
function formatRankTable(results, query) {
|
|
8141
|
-
if (!results || results.length === 0)
|
|
8142
|
-
|
|
8143
|
-
|
|
8820
|
+
if (!results || results.length === 0) {
|
|
8821
|
+
return `No matching files found for query: "${query}"\n`;
|
|
8822
|
+
}
|
|
8823
|
+
|
|
8824
|
+
const intent = (results[0] && results[0].intent) || 'search';
|
|
8825
|
+
const lines = [
|
|
8826
|
+
`## Query: ${query}`,
|
|
8827
|
+
`Intent: ${intent}`,
|
|
8828
|
+
'',
|
|
8829
|
+
'| Rank | File | Score | Sigs | Penalty |',
|
|
8830
|
+
'|------|------|-------|------|---------|',
|
|
8831
|
+
...results.map((r, i) => {
|
|
8832
|
+
const penalty = r.signals && r.signals.penalty ? r.signals.penalty.toFixed(2) : '1.00';
|
|
8833
|
+
return `| ${i + 1} | ${r.file} | ${r.score.toFixed(2)} | ${r.sigs.length} | ${penalty} |`;
|
|
8834
|
+
}),
|
|
8835
|
+
'',
|
|
8836
|
+
];
|
|
8837
|
+
|
|
8838
|
+
// Add signature details for top results
|
|
8144
8839
|
for (const r of results.slice(0, 3)) {
|
|
8145
8840
|
if (r.sigs.length > 0) {
|
|
8146
|
-
lines.push(`### ${r.file}
|
|
8841
|
+
lines.push(`### ${r.file}`);
|
|
8842
|
+
if (r.signals) {
|
|
8843
|
+
const sig = r.signals;
|
|
8844
|
+
lines.push(`Signals: exactToken=${(sig.exactToken || 0).toFixed(2)} symbolMatch=${(sig.symbolMatch || 0).toFixed(2)} prefixMatch=${(sig.prefixMatch || 0).toFixed(2)} pathMatch=${(sig.pathMatch || 0).toFixed(2)} penalty=${(sig.penalty || 1).toFixed(2)}`);
|
|
8845
|
+
}
|
|
8846
|
+
lines.push('```');
|
|
8847
|
+
lines.push(...r.sigs.slice(0, 10));
|
|
8147
8848
|
if (r.sigs.length > 10) lines.push(`... (${r.sigs.length - 10} more)`);
|
|
8148
|
-
lines.push('```'
|
|
8849
|
+
lines.push('```');
|
|
8850
|
+
lines.push('');
|
|
8149
8851
|
}
|
|
8150
8852
|
}
|
|
8853
|
+
|
|
8151
8854
|
return lines.join('\n');
|
|
8152
8855
|
}
|
|
8856
|
+
|
|
8857
|
+
/**
|
|
8858
|
+
* Format ranked results as a structured JSON-serialisable object.
|
|
8859
|
+
*
|
|
8860
|
+
* @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
|
|
8861
|
+
* @param {string} query
|
|
8862
|
+
* @returns {object}
|
|
8863
|
+
*/
|
|
8153
8864
|
function formatRankJSON(results, query) {
|
|
8154
|
-
|
|
8865
|
+
const intent = (results && results[0] && results[0].intent) || 'search';
|
|
8866
|
+
return {
|
|
8867
|
+
query,
|
|
8868
|
+
intent,
|
|
8869
|
+
results: (results || []).map((r, i) => ({
|
|
8870
|
+
rank: i + 1,
|
|
8871
|
+
file: r.file,
|
|
8872
|
+
score: r.score,
|
|
8873
|
+
sigs: r.sigs,
|
|
8874
|
+
tokens: r.tokens,
|
|
8875
|
+
signals: r.signals || {},
|
|
8876
|
+
})),
|
|
8877
|
+
totalResults: (results || []).length,
|
|
8878
|
+
};
|
|
8155
8879
|
}
|
|
8880
|
+
|
|
8881
|
+
// ---------------------------------------------------------------------------
|
|
8882
|
+
// Intent detection — 7 intents
|
|
8883
|
+
// ---------------------------------------------------------------------------
|
|
8156
8884
|
const INTENT_PATTERNS = {
|
|
8157
8885
|
debug: /\b(bug|fix|error|crash|exception|broken|failing|issue|problem|regression)\b/i,
|
|
8158
|
-
explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me)\b/i,
|
|
8159
|
-
refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify)\b/i,
|
|
8160
|
-
review: /\b(review|check|audit|security|pr|pull request|assess)\b/i,
|
|
8886
|
+
explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me|teach)\b/i,
|
|
8887
|
+
refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify|optimize)\b/i,
|
|
8888
|
+
review: /\b(review|check|audit|security|pr|pull request|assess|validate)\b/i,
|
|
8889
|
+
test: /\b(test|unit test|integration test|testing|spec|assert|mock)\b/i,
|
|
8890
|
+
integrate:/\b(import|integrate|connect|wire|bind|require|export|depend|graph)\b|require[ds]\b/i,
|
|
8891
|
+
navigate: /\b(find|locate|where|search|look for|show me|navigate|browse|list)\b/i,
|
|
8161
8892
|
};
|
|
8893
|
+
|
|
8162
8894
|
function detectIntent(query) {
|
|
8163
8895
|
if (!query || typeof query !== 'string') return 'search';
|
|
8164
8896
|
for (const [intent, re] of Object.entries(INTENT_PATTERNS)) {
|
|
@@ -8166,9 +8898,10 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8166
8898
|
}
|
|
8167
8899
|
return 'search';
|
|
8168
8900
|
}
|
|
8169
|
-
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent };
|
|
8170
|
-
};
|
|
8171
8901
|
|
|
8902
|
+
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent };
|
|
8903
|
+
|
|
8904
|
+
};
|
|
8172
8905
|
// ── ./src/eval/scorer ──
|
|
8173
8906
|
__factories["./src/eval/scorer"] = function(module, exports) {
|
|
8174
8907
|
'use strict';
|
|
@@ -11571,7 +12304,7 @@ function __tryGit(args, opts = {}) {
|
|
|
11571
12304
|
catch (_) { return ''; }
|
|
11572
12305
|
}
|
|
11573
12306
|
|
|
11574
|
-
const VERSION = '7.
|
|
12307
|
+
const VERSION = '7.5.0';
|
|
11575
12308
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
11576
12309
|
|
|
11577
12310
|
function requireSourceOrBundled(key) {
|