sigmap 7.3.0 → 7.4.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 +12 -0
- package/gen-context.js +656 -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/extractors/dispatch.js +112 -0
- package/src/mcp/handlers.js +77 -1
- package/src/mcp/server.js +5 -2
- package/src/mcp/tools.js +45 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [7.4.0] — 2026-06-17
|
|
14
|
+
|
|
15
|
+
Minor release — live-index write hooks (grounded codegen, Layer 1).
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **MCP write hooks — live index for agent-created code (#286):** three new tools — `sigmap_notify_file_created`, `sigmap_notify_symbol_added`, `sigmap_notify_file_deleted` — keep the index fresh while an agent creates/modifies/deletes files mid-session, so newly-written symbols are immediately resolvable by `search_signatures` / `get_callee_signatures` instead of being re-hallucinated. They update the persisted sig-cache, which `buildSigIndex` already merges, so changes are live on the next read. New bundle-safe `src/extractors/dispatch.js` (static extractor dispatch for the standalone bundle). Brings the MCP server to **15 tools**.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Standalone-bundle cache merge (#286):** the bundled `ranker` factory carried a raw `require('../cache/sig-cache')` that was never rewritten to `__require`, so the cache merge — and `get_callee_signatures`' cache path — silently failed in the SEA binary. Regenerated the factory; the full create→resolve→delete cycle now works from the bundle with no `src/` present.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
13
25
|
## [7.3.0] — 2026-06-17
|
|
14
26
|
|
|
15
27
|
Minor release — a 12th MCP tool that gives agents exact callee signatures before they write.
|
package/gen-context.js
CHANGED
|
@@ -20,6 +20,122 @@ function __require(key) {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
// ── ./src/extractors/dispatch ──
|
|
24
|
+
__factories["./src/extractors/dispatch"] = function(module, exports) {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Bundle-safe extractor dispatch.
|
|
28
|
+
*
|
|
29
|
+
* `packages/core`'s extract() resolves extractors via dynamic `require(path.join(…))`,
|
|
30
|
+
* which cannot run inside the standalone bundle (no filesystem `src/`). This module
|
|
31
|
+
* uses STATIC requires so the bundler rewrites them to `__require` and the extractors
|
|
32
|
+
* resolve from the bundled factories. Used by the live-index MCP write hooks.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const path = require('path');
|
|
36
|
+
|
|
37
|
+
// Static language → extractor map (every entry is a bundled factory).
|
|
38
|
+
const EXTRACTORS = {
|
|
39
|
+
typescript: __require('./src/extractors/typescript'),
|
|
40
|
+
typescript_react: __require('./src/extractors/typescript_react'),
|
|
41
|
+
javascript: __require('./src/extractors/javascript'),
|
|
42
|
+
python: __require('./src/extractors/python'),
|
|
43
|
+
java: __require('./src/extractors/java'),
|
|
44
|
+
kotlin: __require('./src/extractors/kotlin'),
|
|
45
|
+
go: __require('./src/extractors/go'),
|
|
46
|
+
rust: __require('./src/extractors/rust'),
|
|
47
|
+
csharp: __require('./src/extractors/csharp'),
|
|
48
|
+
cpp: __require('./src/extractors/cpp'),
|
|
49
|
+
ruby: __require('./src/extractors/ruby'),
|
|
50
|
+
php: __require('./src/extractors/php'),
|
|
51
|
+
swift: __require('./src/extractors/swift'),
|
|
52
|
+
dart: __require('./src/extractors/dart'),
|
|
53
|
+
scala: __require('./src/extractors/scala'),
|
|
54
|
+
gdscript: __require('./src/extractors/gdscript'),
|
|
55
|
+
r: __require('./src/extractors/r'),
|
|
56
|
+
vue: __require('./src/extractors/vue'),
|
|
57
|
+
vue_sfc: __require('./src/extractors/vue_sfc'),
|
|
58
|
+
svelte: __require('./src/extractors/svelte'),
|
|
59
|
+
html: __require('./src/extractors/html'),
|
|
60
|
+
css: __require('./src/extractors/css'),
|
|
61
|
+
yaml: __require('./src/extractors/yaml'),
|
|
62
|
+
shell: __require('./src/extractors/shell'),
|
|
63
|
+
sql: __require('./src/extractors/sql'),
|
|
64
|
+
graphql: __require('./src/extractors/graphql'),
|
|
65
|
+
terraform: __require('./src/extractors/terraform'),
|
|
66
|
+
protobuf: __require('./src/extractors/protobuf'),
|
|
67
|
+
toml: __require('./src/extractors/toml'),
|
|
68
|
+
properties: __require('./src/extractors/properties'),
|
|
69
|
+
xml: __require('./src/extractors/xml'),
|
|
70
|
+
markdown: __require('./src/extractors/markdown'),
|
|
71
|
+
dockerfile: __require('./src/extractors/dockerfile'),
|
|
72
|
+
generic: __require('./src/extractors/generic'),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const EXT_MAP = {
|
|
76
|
+
'.ts': 'typescript', '.tsx': 'typescript_react',
|
|
77
|
+
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
78
|
+
'.py': 'python', '.pyw': 'python',
|
|
79
|
+
'.java': 'java',
|
|
80
|
+
'.kt': 'kotlin', '.kts': 'kotlin',
|
|
81
|
+
'.go': 'go',
|
|
82
|
+
'.rs': 'rust',
|
|
83
|
+
'.cs': 'csharp',
|
|
84
|
+
'.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
|
|
85
|
+
'.rb': 'ruby', '.rake': 'ruby',
|
|
86
|
+
'.php': 'php',
|
|
87
|
+
'.swift': 'swift',
|
|
88
|
+
'.dart': 'dart',
|
|
89
|
+
'.scala': 'scala', '.sc': 'scala',
|
|
90
|
+
'.gd': 'gdscript',
|
|
91
|
+
'.r': 'r', '.R': 'r',
|
|
92
|
+
'.vue': 'vue_sfc',
|
|
93
|
+
'.svelte': 'svelte',
|
|
94
|
+
'.html': 'html', '.htm': 'html',
|
|
95
|
+
'.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
|
|
96
|
+
'.yml': 'yaml', '.yaml': 'yaml',
|
|
97
|
+
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
|
|
98
|
+
'.sql': 'sql',
|
|
99
|
+
'.graphql': 'graphql', '.gql': 'graphql',
|
|
100
|
+
'.tf': 'terraform', '.tfvars': 'terraform',
|
|
101
|
+
'.proto': 'protobuf',
|
|
102
|
+
'.toml': 'toml',
|
|
103
|
+
'.properties': 'properties',
|
|
104
|
+
'.xml': 'xml',
|
|
105
|
+
'.md': 'markdown',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Resolve a language key from a file path/name. */
|
|
109
|
+
function langFor(filePathOrName) {
|
|
110
|
+
const base = path.basename(String(filePathOrName || ''));
|
|
111
|
+
if (base === 'Dockerfile' || base.startsWith('Dockerfile.')) return 'dockerfile';
|
|
112
|
+
const ext = path.extname(base).toLowerCase();
|
|
113
|
+
return EXT_MAP[ext] || null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Extract signatures from a file's content using the right extractor.
|
|
118
|
+
* @param {string} filePathOrName - path or name (extension drives the extractor)
|
|
119
|
+
* @param {string} src - file content
|
|
120
|
+
* @returns {string[]}
|
|
121
|
+
*/
|
|
122
|
+
function extractFile(filePathOrName, src) {
|
|
123
|
+
if (!src || typeof src !== 'string') return [];
|
|
124
|
+
const lang = langFor(filePathOrName);
|
|
125
|
+
const mod = lang ? EXTRACTORS[lang] : null;
|
|
126
|
+
if (!mod || typeof mod.extract !== 'function') return [];
|
|
127
|
+
try {
|
|
128
|
+
const out = mod.extract(src);
|
|
129
|
+
return Array.isArray(out) ? out : [];
|
|
130
|
+
} catch (_) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = { extractFile, langFor };
|
|
136
|
+
|
|
137
|
+
};
|
|
138
|
+
|
|
23
139
|
// ── ./src/config/defaults ──
|
|
24
140
|
__factories["./src/config/defaults"] = function(module, exports) {
|
|
25
141
|
|
|
@@ -6125,7 +6241,83 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
6125
6241
|
}
|
|
6126
6242
|
}
|
|
6127
6243
|
|
|
6128
|
-
|
|
6244
|
+
// ── Layer 1: live-index write hooks ────────────────────────────────────────
|
|
6245
|
+
// Keep the sig-cache fresh while an agent creates/modifies/deletes files, so
|
|
6246
|
+
// new code is discoverable in the same session. buildSigIndex already merges
|
|
6247
|
+
// the cache (_buildSigIndexFromCache), so updates are live on the next read.
|
|
6248
|
+
|
|
6249
|
+
function _pkgVersion(cwd) {
|
|
6250
|
+
try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
|
|
6251
|
+
catch (_) { return '0.0.0'; }
|
|
6252
|
+
}
|
|
6253
|
+
|
|
6254
|
+
|
|
6255
|
+
/** notify_file_created — extract a file's signatures and index it live. */
|
|
6256
|
+
function notifyFileCreated(args, cwd) {
|
|
6257
|
+
const rel = args && args.path;
|
|
6258
|
+
if (!rel) return 'Missing required argument: path';
|
|
6259
|
+
try {
|
|
6260
|
+
const { extractFile } = __require('./src/extractors/dispatch');
|
|
6261
|
+
const { loadCache, saveCache } = __require('./src/cache/sig-cache');
|
|
6262
|
+
const abs = path.resolve(cwd, rel);
|
|
6263
|
+
let content = args.content;
|
|
6264
|
+
if (typeof content !== 'string') {
|
|
6265
|
+
try { content = fs.readFileSync(abs, 'utf8'); } catch (_) { content = ''; }
|
|
6266
|
+
}
|
|
6267
|
+
const sigs = extractFile(abs, content);
|
|
6268
|
+
const version = _pkgVersion(cwd);
|
|
6269
|
+
const cache = loadCache(cwd, version);
|
|
6270
|
+
if (sigs.length > 0) {
|
|
6271
|
+
cache.set(abs, { mtime: Date.now(), sigs });
|
|
6272
|
+
}
|
|
6273
|
+
saveCache(cwd, version, cache);
|
|
6274
|
+
return `Indexed ${rel}: ${sigs.length} signature(s) now live.`;
|
|
6275
|
+
} catch (err) {
|
|
6276
|
+
return `_notify_file_created failed: ${err.message}_`;
|
|
6277
|
+
}
|
|
6278
|
+
}
|
|
6279
|
+
|
|
6280
|
+
/** notify_symbol_added — append one signature to a file's live cache entry. */
|
|
6281
|
+
function notifySymbolAdded(args, cwd) {
|
|
6282
|
+
if (!args || !args.signature || !args.file) {
|
|
6283
|
+
return 'Missing required arguments: signature, file';
|
|
6284
|
+
}
|
|
6285
|
+
try {
|
|
6286
|
+
const { loadCache, saveCache } = __require('./src/cache/sig-cache');
|
|
6287
|
+
const abs = path.resolve(cwd, args.file);
|
|
6288
|
+
const version = _pkgVersion(cwd);
|
|
6289
|
+
const cache = loadCache(cwd, version);
|
|
6290
|
+
const entry = cache.get(abs) || { mtime: Date.now(), sigs: [] };
|
|
6291
|
+
const line = Number.isFinite(Number(args.line)) ? ` :${args.line}` : '';
|
|
6292
|
+
const sig = String(args.signature) + line;
|
|
6293
|
+
if (!entry.sigs.includes(sig)) entry.sigs.push(sig);
|
|
6294
|
+
entry.mtime = Date.now();
|
|
6295
|
+
cache.set(abs, entry);
|
|
6296
|
+
saveCache(cwd, version, cache);
|
|
6297
|
+
return `Added signature to ${args.file} (${entry.sigs.length} total).`;
|
|
6298
|
+
} catch (err) {
|
|
6299
|
+
return `_notify_symbol_added failed: ${err.message}_`;
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
|
|
6303
|
+
/** notify_file_deleted — drop a file's cache-overlay entry. */
|
|
6304
|
+
function notifyFileDeleted(args, cwd) {
|
|
6305
|
+
const rel = args && args.path;
|
|
6306
|
+
if (!rel) return 'Missing required argument: path';
|
|
6307
|
+
try {
|
|
6308
|
+
const { loadCache, saveCache } = __require('./src/cache/sig-cache');
|
|
6309
|
+
const abs = path.resolve(cwd, rel);
|
|
6310
|
+
const version = _pkgVersion(cwd);
|
|
6311
|
+
const cache = loadCache(cwd, version);
|
|
6312
|
+
const had = cache.delete(abs);
|
|
6313
|
+
saveCache(cwd, version, cache);
|
|
6314
|
+
return had ? `Removed ${rel} from the live index.` : `${rel} was not in the live cache.`;
|
|
6315
|
+
} catch (err) {
|
|
6316
|
+
return `_notify_file_deleted failed: ${err.message}_`;
|
|
6317
|
+
}
|
|
6318
|
+
}
|
|
6319
|
+
|
|
6320
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted };
|
|
6129
6321
|
|
|
6130
6322
|
};
|
|
6131
6323
|
// ── ./src/learning/weights ──
|
|
@@ -6302,11 +6494,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
6302
6494
|
|
|
6303
6495
|
const readline = require('readline');
|
|
6304
6496
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
6305
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures } = __require('./src/mcp/handlers');
|
|
6497
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted } = __require('./src/mcp/handlers');
|
|
6306
6498
|
|
|
6307
6499
|
const SERVER_INFO = {
|
|
6308
6500
|
name: 'sigmap',
|
|
6309
|
-
version: '7.
|
|
6501
|
+
version: '7.4.0',
|
|
6310
6502
|
description: 'SigMap MCP server — code signatures on demand',
|
|
6311
6503
|
};
|
|
6312
6504
|
|
|
@@ -6366,6 +6558,9 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
6366
6558
|
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
6367
6559
|
else if (name === 'read_memory') text = readMemory(args, cwd);
|
|
6368
6560
|
else if (name === 'get_callee_signatures') text = getCalleeSignatures(args, cwd);
|
|
6561
|
+
else if (name === 'sigmap_notify_file_created') text = notifyFileCreated(args, cwd);
|
|
6562
|
+
else if (name === 'sigmap_notify_symbol_added') text = notifySymbolAdded(args, cwd);
|
|
6563
|
+
else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
|
|
6369
6564
|
else {
|
|
6370
6565
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
6371
6566
|
return;
|
|
@@ -6425,10 +6620,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
6425
6620
|
__factories["./src/mcp/tools"] = function(module, exports) {
|
|
6426
6621
|
|
|
6427
6622
|
/**
|
|
6428
|
-
* MCP tool definitions for SigMap (
|
|
6623
|
+
* MCP tool definitions for SigMap (15 tools).
|
|
6429
6624
|
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
6430
6625
|
* explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
|
|
6431
|
-
* get_callee_signatures
|
|
6626
|
+
* get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
|
|
6627
|
+
* sigmap_notify_file_deleted.
|
|
6432
6628
|
*/
|
|
6433
6629
|
|
|
6434
6630
|
const TOOLS = [
|
|
@@ -6659,6 +6855,48 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
6659
6855
|
required: ['symbols'],
|
|
6660
6856
|
},
|
|
6661
6857
|
},
|
|
6858
|
+
{
|
|
6859
|
+
name: 'sigmap_notify_file_created',
|
|
6860
|
+
description:
|
|
6861
|
+
'Tell SigMap a file was created or modified so its signatures are indexed ' +
|
|
6862
|
+
'live for the rest of the session. Call this after writing a file — the new ' +
|
|
6863
|
+
'symbols become resolvable by search_signatures / get_callee_signatures.',
|
|
6864
|
+
inputSchema: {
|
|
6865
|
+
type: 'object',
|
|
6866
|
+
properties: {
|
|
6867
|
+
path: { type: 'string', description: 'File path relative to the project root.' },
|
|
6868
|
+
content: { type: 'string', description: 'Optional file content; read from disk if omitted.' },
|
|
6869
|
+
},
|
|
6870
|
+
required: ['path'],
|
|
6871
|
+
},
|
|
6872
|
+
},
|
|
6873
|
+
{
|
|
6874
|
+
name: 'sigmap_notify_symbol_added',
|
|
6875
|
+
description:
|
|
6876
|
+
'Fast path: register a single new symbol signature directly in the live ' +
|
|
6877
|
+
'index without re-reading the whole file.',
|
|
6878
|
+
inputSchema: {
|
|
6879
|
+
type: 'object',
|
|
6880
|
+
properties: {
|
|
6881
|
+
signature: { type: 'string', description: 'The signature line (e.g. "function check(key)").' },
|
|
6882
|
+
file: { type: 'string', description: 'File path the symbol belongs to (relative to root).' },
|
|
6883
|
+
line: { type: 'number', description: 'Optional 1-based line number.' },
|
|
6884
|
+
},
|
|
6885
|
+
required: ['signature', 'file'],
|
|
6886
|
+
},
|
|
6887
|
+
},
|
|
6888
|
+
{
|
|
6889
|
+
name: 'sigmap_notify_file_deleted',
|
|
6890
|
+
description:
|
|
6891
|
+
'Tell SigMap a file was deleted so its symbols are dropped from the live index.',
|
|
6892
|
+
inputSchema: {
|
|
6893
|
+
type: 'object',
|
|
6894
|
+
properties: {
|
|
6895
|
+
path: { type: 'string', description: 'Deleted file path relative to the project root.' },
|
|
6896
|
+
},
|
|
6897
|
+
required: ['path'],
|
|
6898
|
+
},
|
|
6899
|
+
},
|
|
6662
6900
|
];
|
|
6663
6901
|
|
|
6664
6902
|
module.exports = { TOOLS };
|
|
@@ -7979,61 +8217,293 @@ __factories["./src/retrieval/tokenizer"] = function(module, exports) {
|
|
|
7979
8217
|
|
|
7980
8218
|
// ── ./src/retrieval/ranker ──
|
|
7981
8219
|
__factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
7982
|
-
|
|
8220
|
+
|
|
8221
|
+
/**
|
|
8222
|
+
* SigMap zero-dependency relevance ranker.
|
|
8223
|
+
*
|
|
8224
|
+
* Ranks all files in a signature index against a natural-language query.
|
|
8225
|
+
* Scoring weights:
|
|
8226
|
+
* - keyword overlap (exact token match against sigs)
|
|
8227
|
+
* - symbol match (token appears in a top-level identifier / function name)
|
|
8228
|
+
* - partial prefix match (token is prefix of a sig token, length ≥ 4)
|
|
8229
|
+
* - path relevance (query token appears in the file path)
|
|
8230
|
+
* - recency boost (applied externally via recency map)
|
|
8231
|
+
*
|
|
8232
|
+
* Usage:
|
|
8233
|
+
* const { rank } = __require('./src/retrieval/src/retrieval/ranker');
|
|
8234
|
+
* const results = rank(query, sigIndex, { topK: 10 });
|
|
8235
|
+
* // results: [{ file, score, sigs, tokens }]
|
|
8236
|
+
*/
|
|
8237
|
+
|
|
7983
8238
|
const { loadWeights } = __require('./src/learning/weights');
|
|
7984
8239
|
const { tokenize, STOP_WORDS } = __require('./src/retrieval/tokenizer');
|
|
8240
|
+
|
|
8241
|
+
// ---------------------------------------------------------------------------
|
|
8242
|
+
// Default weights
|
|
8243
|
+
// ---------------------------------------------------------------------------
|
|
7985
8244
|
const DEFAULT_WEIGHTS = {
|
|
7986
|
-
exactToken: 1.0,
|
|
8245
|
+
exactToken: 1.0, // query token exactly in sig tokens
|
|
8246
|
+
symbolMatch: 0.5, // bonus if token appears in a function/class name line
|
|
8247
|
+
prefixMatch: 0.3, // partial prefix hit (query token ≥ 4 chars)
|
|
8248
|
+
pathMatch: 0.8, // query token appears in the file path
|
|
8249
|
+
recencyBoost: 1.5, // multiplier applied when file is in recencySet
|
|
8250
|
+
graphBoost: 0.4, // additive bonus for 1-hop import neighbors of matching files
|
|
7987
8251
|
};
|
|
8252
|
+
|
|
8253
|
+
// Graph boost amounts for 2-hop traversal with decay (v6.7)
|
|
8254
|
+
const GRAPH_BOOST_AMOUNTS = {
|
|
8255
|
+
hop1: 0.40, // direct import neighbor of a file with score > 0
|
|
8256
|
+
hop2: 0.15, // 2 hops away (transitive), with decay
|
|
8257
|
+
};
|
|
8258
|
+
|
|
8259
|
+
// Intent-specific weight adjustments
|
|
8260
|
+
const INTENT_WEIGHTS = {
|
|
8261
|
+
search: DEFAULT_WEIGHTS,
|
|
8262
|
+
debug: { ...DEFAULT_WEIGHTS, exactToken: 1.2, pathMatch: 0.6 },
|
|
8263
|
+
explain: { ...DEFAULT_WEIGHTS, symbolMatch: 0.8, pathMatch: 0.9 },
|
|
8264
|
+
refactor: { ...DEFAULT_WEIGHTS, symbolMatch: 0.9, exactToken: 0.8 },
|
|
8265
|
+
review: { ...DEFAULT_WEIGHTS, pathMatch: 1.0, exactToken: 0.9 },
|
|
8266
|
+
test: { ...DEFAULT_WEIGHTS, exactToken: 0.7, symbolMatch: 0.4 },
|
|
8267
|
+
integrate: { ...DEFAULT_WEIGHTS, graphBoost: 0.7, pathMatch: 1.1 },
|
|
8268
|
+
navigate: { ...DEFAULT_WEIGHTS, pathMatch: 1.2, exactToken: 0.9 },
|
|
8269
|
+
};
|
|
8270
|
+
|
|
8271
|
+
// Penalty multipliers for negative signals
|
|
8272
|
+
const PENALTY_SIGNALS = {
|
|
8273
|
+
testFile: 0.4, // test/spec/__tests__ in path
|
|
8274
|
+
generatedCode: 0.3, // dist/build/.next in path
|
|
8275
|
+
docsFile: 0.2, // docs/doc/README in path
|
|
8276
|
+
nodeModules: 0.0, // node_modules (zero score)
|
|
8277
|
+
};
|
|
8278
|
+
|
|
8279
|
+
function _computePenalty(filePath) {
|
|
8280
|
+
const pathLower = filePath.toLowerCase();
|
|
8281
|
+
if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
|
|
8282
|
+
if (/(^|\/)(test|tests|spec|__tests__|e2e)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.testFile;
|
|
8283
|
+
if (/(^|\/)(dist|build|\.next|\.nuxt|out|\.venv|venv)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.generatedCode;
|
|
8284
|
+
if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.docsFile;
|
|
8285
|
+
return 1.0;
|
|
8286
|
+
}
|
|
8287
|
+
|
|
8288
|
+
// Detect hub files: those with fanout > 20% of all files in the graph
|
|
8289
|
+
function _computeHubs(graph) {
|
|
8290
|
+
if (!graph || !graph.reverse) return new Set();
|
|
8291
|
+
const fileCount = Math.max(1, graph.reverse.size);
|
|
8292
|
+
const threshold = Math.ceil(fileCount * 0.2);
|
|
8293
|
+
const hubs = new Set();
|
|
8294
|
+
for (const [file, deps] of graph.reverse) {
|
|
8295
|
+
if ((deps && deps.size >= threshold) || (Array.isArray(deps) && deps.length >= threshold)) {
|
|
8296
|
+
hubs.add(file);
|
|
8297
|
+
}
|
|
8298
|
+
}
|
|
8299
|
+
return hubs;
|
|
8300
|
+
}
|
|
8301
|
+
|
|
8302
|
+
// Common utility paths that should be treated as hubs regardless of fanout
|
|
8303
|
+
function _isHub(filePath) {
|
|
8304
|
+
return /\/(utils|helpers|shared|common|constants|types|interfaces|index|zzz|globals)\.(ts|tsx|js|jsx|r|R)$/.test(filePath)
|
|
8305
|
+
|| filePath.endsWith('/index.ts') || filePath.endsWith('/index.js')
|
|
8306
|
+
|| filePath.endsWith('/R/utils.R') || filePath.endsWith('/R/zzz.R') || filePath.endsWith('/R/globals.R');
|
|
8307
|
+
}
|
|
8308
|
+
|
|
8309
|
+
/**
|
|
8310
|
+
* Score a single file against a query, returning detailed signal breakdown.
|
|
8311
|
+
*
|
|
8312
|
+
* @param {string} filePath - relative file path (e.g. 'src/extractors/python.js')
|
|
8313
|
+
* @param {string[]} sigs - signature strings for this file
|
|
8314
|
+
* @param {string[]} queryTokens - pre-tokenized query
|
|
8315
|
+
* @param {object} weights
|
|
8316
|
+
* @returns {{ score: number, signals: { exactToken: number, symbolMatch: number, prefixMatch: number, pathMatch: number, penalty: number } }}
|
|
8317
|
+
*/
|
|
7988
8318
|
function scoreFile(filePath, sigs, queryTokens, weights) {
|
|
7989
|
-
if (!sigs || sigs.length === 0) return 0;
|
|
8319
|
+
if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
|
|
8320
|
+
|
|
7990
8321
|
const w = weights || DEFAULT_WEIGHTS;
|
|
7991
|
-
const
|
|
8322
|
+
const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath) };
|
|
8323
|
+
|
|
8324
|
+
// Build token set from all signatures
|
|
8325
|
+
const sigText = sigs.join(' ');
|
|
8326
|
+
const sigTokenSet = new Set(tokenize(sigText));
|
|
8327
|
+
|
|
8328
|
+
// Build token set from the file path
|
|
7992
8329
|
const pathTokenSet = new Set(tokenize(filePath));
|
|
8330
|
+
|
|
7993
8331
|
let score = 0;
|
|
8332
|
+
|
|
7994
8333
|
for (const qt of queryTokens) {
|
|
7995
8334
|
if (STOP_WORDS.has(qt)) continue;
|
|
8335
|
+
|
|
8336
|
+
// Exact token match in sigs
|
|
7996
8337
|
if (sigTokenSet.has(qt)) {
|
|
7997
|
-
|
|
7998
|
-
|
|
8338
|
+
const bonus = w.exactToken;
|
|
8339
|
+
score += bonus;
|
|
8340
|
+
signals.exactToken += bonus;
|
|
8341
|
+
|
|
8342
|
+
// Bonus: appears directly in a function/class/method name line
|
|
8343
|
+
const nameLineMatch = sigs.some((sig) => {
|
|
8344
|
+
const nt = tokenize(sig.replace(/[^a-zA-Z0-9_\s]/g, ' '));
|
|
8345
|
+
return nt.includes(qt);
|
|
8346
|
+
});
|
|
8347
|
+
if (nameLineMatch) {
|
|
8348
|
+
score += w.symbolMatch;
|
|
8349
|
+
signals.symbolMatch += w.symbolMatch;
|
|
8350
|
+
}
|
|
7999
8351
|
}
|
|
8352
|
+
|
|
8353
|
+
// Prefix match (e.g. query "python" matches "pythonDeps")
|
|
8000
8354
|
if (qt.length >= 4) {
|
|
8001
8355
|
for (const st of sigTokenSet) {
|
|
8002
|
-
if (st !== qt && st.startsWith(qt)) {
|
|
8356
|
+
if (st !== qt && st.startsWith(qt)) {
|
|
8357
|
+
score += w.prefixMatch;
|
|
8358
|
+
signals.prefixMatch += w.prefixMatch;
|
|
8359
|
+
break; // one bonus per query token
|
|
8360
|
+
}
|
|
8003
8361
|
}
|
|
8004
8362
|
}
|
|
8005
|
-
|
|
8363
|
+
|
|
8364
|
+
// Path token match
|
|
8365
|
+
if (pathTokenSet.has(qt)) {
|
|
8366
|
+
score += w.pathMatch;
|
|
8367
|
+
signals.pathMatch += w.pathMatch;
|
|
8368
|
+
}
|
|
8006
8369
|
}
|
|
8007
|
-
|
|
8370
|
+
|
|
8371
|
+
// Apply penalty multiplier
|
|
8372
|
+
score *= signals.penalty;
|
|
8373
|
+
|
|
8374
|
+
return { score, signals };
|
|
8008
8375
|
}
|
|
8376
|
+
|
|
8377
|
+
/**
|
|
8378
|
+
* Rank all files in a signature index against a query.
|
|
8379
|
+
*
|
|
8380
|
+
* @param {string} query - natural language query
|
|
8381
|
+
* @param {Map<string,string[]>} sigIndex - Map<file, sigs[]>
|
|
8382
|
+
* @param {object} [opts]
|
|
8383
|
+
* @param {number} [opts.topK=10] - max results to return
|
|
8384
|
+
* @param {number} [opts.recencyBoost=1.5] - multiplier for recent files
|
|
8385
|
+
* @param {Set<string>} [opts.recencySet] - set of recently-changed file paths
|
|
8386
|
+
* @param {object} [opts.weights] - override scoring weights
|
|
8387
|
+
* @param {string} [opts.cwd] - project root for learned ranking weights
|
|
8388
|
+
* @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
|
|
8389
|
+
* @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
|
|
8390
|
+
*/
|
|
8009
8391
|
function rank(query, sigIndex, opts) {
|
|
8010
8392
|
if (!query || typeof query !== 'string') return [];
|
|
8011
8393
|
if (!sigIndex || !(sigIndex instanceof Map) || sigIndex.size === 0) return [];
|
|
8394
|
+
|
|
8012
8395
|
const topK = (opts && opts.topK) || 10;
|
|
8013
8396
|
const recencyMultiplier = (opts && opts.recencyBoost) || DEFAULT_WEIGHTS.recencyBoost;
|
|
8014
8397
|
const recencySet = (opts && opts.recencySet) || null;
|
|
8015
|
-
const
|
|
8398
|
+
const graph = (opts && opts.graph && opts.graph.forward instanceof Map) ? opts.graph : null;
|
|
8399
|
+
const cwd = (opts && opts.cwd) || null;
|
|
8400
|
+
|
|
8401
|
+
// Detect query intent and get appropriate weights
|
|
8402
|
+
const intent = detectIntent(query);
|
|
8403
|
+
const intentWeights = INTENT_WEIGHTS[intent] || DEFAULT_WEIGHTS;
|
|
8404
|
+
const weights = (opts && opts.weights) ? Object.assign({}, intentWeights, opts.weights) : intentWeights;
|
|
8016
8405
|
const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;
|
|
8406
|
+
|
|
8017
8407
|
const queryTokens = tokenize(query);
|
|
8018
8408
|
if (queryTokens.length === 0) {
|
|
8409
|
+
// Empty query: return top-K by file count (most signatures = most useful)
|
|
8019
8410
|
const all = [];
|
|
8020
|
-
for (const [file, sigs] of sigIndex.entries())
|
|
8411
|
+
for (const [file, sigs] of sigIndex.entries()) {
|
|
8412
|
+
all.push({ file, score: sigs.length, sigs, tokens: Math.ceil(sigs.join('\n').length / 4), intent, signals: {} });
|
|
8413
|
+
}
|
|
8021
8414
|
all.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
8022
8415
|
return all.slice(0, topK);
|
|
8023
8416
|
}
|
|
8417
|
+
|
|
8024
8418
|
const scored = [];
|
|
8025
8419
|
for (const [file, sigs] of sigIndex.entries()) {
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8420
|
+
const result = scoreFile(file, sigs, queryTokens, weights);
|
|
8421
|
+
let score = result.score;
|
|
8422
|
+
const signals = result.signals;
|
|
8423
|
+
|
|
8424
|
+
// Recency boost
|
|
8425
|
+
if (recencySet && recencySet.has(file) && score > 0) {
|
|
8426
|
+
score *= recencyMultiplier;
|
|
8427
|
+
signals.recencyBoost = recencyMultiplier;
|
|
8428
|
+
}
|
|
8429
|
+
|
|
8430
|
+
if (learnedWeights && score > 0) {
|
|
8431
|
+
const multiplier = learnedWeights[file] || 1.0;
|
|
8432
|
+
score *= multiplier;
|
|
8433
|
+
signals.learnedWeights = multiplier;
|
|
8434
|
+
}
|
|
8435
|
+
|
|
8436
|
+
scored.push({
|
|
8437
|
+
file,
|
|
8438
|
+
score,
|
|
8439
|
+
sigs,
|
|
8440
|
+
tokens: Math.ceil(sigs.join('\n').length / 4),
|
|
8441
|
+
intent,
|
|
8442
|
+
signals,
|
|
8443
|
+
});
|
|
8444
|
+
}
|
|
8445
|
+
|
|
8446
|
+
// Graph neighbor boost: 2-hop traversal with decay (v6.7)
|
|
8447
|
+
// Hop 1: add hop1 amount to direct import neighbors (score > 0)
|
|
8448
|
+
// Hop 2: add hop2 amount to neighbors of hop1 files (with decay)
|
|
8449
|
+
// Hub suppression: files with high fanout (>20%) are not boosted
|
|
8450
|
+
if (graph && cwd) {
|
|
8451
|
+
const path = require('path');
|
|
8452
|
+
// Build maps for relative ↔ absolute path conversion and index lookup
|
|
8453
|
+
const relToIdx = new Map();
|
|
8454
|
+
const absToRel = new Map();
|
|
8455
|
+
for (let i = 0; i < scored.length; i++) {
|
|
8456
|
+
relToIdx.set(scored[i].file, i);
|
|
8457
|
+
const abs = path.resolve(cwd, scored[i].file);
|
|
8458
|
+
absToRel.set(abs, scored[i].file);
|
|
8459
|
+
}
|
|
8460
|
+
|
|
8461
|
+
const hubs = _computeHubs(graph);
|
|
8462
|
+
const hop1Files = new Set(); // track which files received hop1 boost
|
|
8463
|
+
|
|
8464
|
+
// Hop 1: direct neighbors of scored files
|
|
8465
|
+
for (const entry of scored) {
|
|
8466
|
+
if (entry.score <= 0) continue;
|
|
8467
|
+
const abs = path.resolve(cwd, entry.file);
|
|
8468
|
+
const neighbors = graph.forward.get(abs) || [];
|
|
8469
|
+
for (const neighborAbs of neighbors) {
|
|
8470
|
+
if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
|
|
8471
|
+
const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
|
|
8472
|
+
const idx = relToIdx.get(neighborRel);
|
|
8473
|
+
if (idx !== undefined) {
|
|
8474
|
+
scored[idx].score += GRAPH_BOOST_AMOUNTS.hop1;
|
|
8475
|
+
scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop1;
|
|
8476
|
+
hop1Files.add(neighborAbs);
|
|
8477
|
+
}
|
|
8478
|
+
}
|
|
8479
|
+
}
|
|
8480
|
+
|
|
8481
|
+
// Hop 2: neighbors of hop1 files (only if they didn't get a direct score)
|
|
8482
|
+
for (const hop1File of hop1Files) {
|
|
8483
|
+
if (!absToRel.has(hop1File)) continue; // skip files not in index
|
|
8484
|
+
const neighbors = graph.forward.get(hop1File) || [];
|
|
8485
|
+
for (const neighborAbs of neighbors) {
|
|
8486
|
+
if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
|
|
8487
|
+
if (hop1Files.has(neighborAbs)) continue; // skip already hop1-boosted
|
|
8488
|
+
const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
|
|
8489
|
+
const idx = relToIdx.get(neighborRel);
|
|
8490
|
+
if (idx !== undefined && scored[idx].score > 0) {
|
|
8491
|
+
// Only boost files that have some baseline score (not noise)
|
|
8492
|
+
scored[idx].score += GRAPH_BOOST_AMOUNTS.hop2;
|
|
8493
|
+
scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop2;
|
|
8494
|
+
}
|
|
8495
|
+
}
|
|
8496
|
+
}
|
|
8030
8497
|
}
|
|
8498
|
+
|
|
8031
8499
|
// Compute confidence levels based on score distribution
|
|
8032
8500
|
if (scored.length > 0) {
|
|
8033
8501
|
const scores = scored.map(s => s.score);
|
|
8034
8502
|
const maxScore = Math.max(...scores);
|
|
8035
8503
|
const minScore = Math.min(...scores);
|
|
8036
8504
|
const scoreRange = maxScore - minScore || 1;
|
|
8505
|
+
|
|
8506
|
+
// Confidence tiers: top 33% = high, next 33% = medium, rest = low
|
|
8037
8507
|
for (const entry of scored) {
|
|
8038
8508
|
if (entry.score <= 0) {
|
|
8039
8509
|
entry.confidence = 'low';
|
|
@@ -8043,39 +8513,73 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8043
8513
|
}
|
|
8044
8514
|
}
|
|
8045
8515
|
}
|
|
8516
|
+
|
|
8046
8517
|
scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
8047
8518
|
return scored.slice(0, topK);
|
|
8048
8519
|
}
|
|
8520
|
+
|
|
8521
|
+
/**
|
|
8522
|
+
* All paths where sigmap adapters write their context files, in probe order.
|
|
8523
|
+
* The first existing file with a non-empty index wins when no explicit path
|
|
8524
|
+
* is supplied.
|
|
8525
|
+
*/
|
|
8049
8526
|
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'],
|
|
8527
|
+
['.github', 'copilot-instructions.md'], // copilot (default)
|
|
8528
|
+
['CLAUDE.md'], // claude
|
|
8529
|
+
['AGENTS.md'], // codex
|
|
8530
|
+
['.cursorrules'], // cursor
|
|
8531
|
+
['.windsurfrules'], // windsurf
|
|
8532
|
+
['.github', 'openai-context.md'], // openai
|
|
8533
|
+
['.github', 'gemini-context.md'], // gemini
|
|
8534
|
+
['llm-full.txt'], // llm-full
|
|
8535
|
+
['llm.txt'], // llm
|
|
8059
8536
|
];
|
|
8537
|
+
|
|
8538
|
+
/**
|
|
8539
|
+
* Parse a single context file into a Map<filePath, string[]>.
|
|
8540
|
+
*
|
|
8541
|
+
* Files that contain human-written content before an
|
|
8542
|
+
* "## Auto-generated signatures" marker (e.g. CLAUDE.md) are handled
|
|
8543
|
+
* by skipping everything above the marker before scanning for ### headers.
|
|
8544
|
+
*
|
|
8545
|
+
* @param {string} contextPath - absolute path to the context file
|
|
8546
|
+
* @returns {Map<string, string[]>}
|
|
8547
|
+
*/
|
|
8060
8548
|
function _parseContextFile(contextPath) {
|
|
8061
8549
|
const fs = require('fs');
|
|
8062
8550
|
const index = new Map();
|
|
8551
|
+
|
|
8063
8552
|
if (!fs.existsSync(contextPath)) return index;
|
|
8553
|
+
|
|
8064
8554
|
let content = fs.readFileSync(contextPath, 'utf8');
|
|
8555
|
+
|
|
8556
|
+
// Skip any human-written preamble that sits above the auto-generated block.
|
|
8065
8557
|
const markerIdx = content.indexOf('## Auto-generated signatures');
|
|
8066
8558
|
if (markerIdx !== -1) content = content.slice(markerIdx);
|
|
8559
|
+
|
|
8067
8560
|
const lines = content.split('\n');
|
|
8068
|
-
let currentFile = null;
|
|
8561
|
+
let currentFile = null;
|
|
8562
|
+
let inBlock = false;
|
|
8563
|
+
let sigs = [];
|
|
8564
|
+
|
|
8069
8565
|
for (const line of lines) {
|
|
8070
|
-
const
|
|
8071
|
-
if (
|
|
8566
|
+
const headerMatch = line.match(/^###\s+(\S+)\s*$/);
|
|
8567
|
+
if (headerMatch) {
|
|
8568
|
+
if (currentFile !== null) index.set(currentFile, sigs);
|
|
8569
|
+
currentFile = headerMatch[1];
|
|
8570
|
+
sigs = [];
|
|
8571
|
+
inBlock = false;
|
|
8572
|
+
continue;
|
|
8573
|
+
}
|
|
8072
8574
|
if (line.startsWith('```')) { inBlock = !inBlock; continue; }
|
|
8073
8575
|
if (inBlock && currentFile && line.trim()) sigs.push(line.trim());
|
|
8074
8576
|
}
|
|
8075
8577
|
if (currentFile !== null) index.set(currentFile, sigs);
|
|
8578
|
+
|
|
8076
8579
|
return index;
|
|
8077
8580
|
}
|
|
8078
8581
|
|
|
8582
|
+
/** Merge source index into target; prefer non-empty sig lists. */
|
|
8079
8583
|
function _mergeSigIndex(target, source) {
|
|
8080
8584
|
for (const [file, sigs] of source.entries()) {
|
|
8081
8585
|
if (!sigs || sigs.length === 0) continue;
|
|
@@ -8086,12 +8590,17 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8086
8590
|
return target;
|
|
8087
8591
|
}
|
|
8088
8592
|
|
|
8593
|
+
/**
|
|
8594
|
+
* Load signatures from .sigmap-cache.json (absolute paths → repo-relative keys).
|
|
8595
|
+
* @param {string} cwd
|
|
8596
|
+
* @returns {Map<string, string[]>}
|
|
8597
|
+
*/
|
|
8089
8598
|
function _buildSigIndexFromCache(cwd) {
|
|
8090
8599
|
const fs = require('fs');
|
|
8091
8600
|
const path = require('path');
|
|
8092
8601
|
const index = new Map();
|
|
8093
8602
|
try {
|
|
8094
|
-
const { loadCache } =
|
|
8603
|
+
const { loadCache } = __require('./src/cache/sig-cache');
|
|
8095
8604
|
const pkgPath = path.join(cwd, 'package.json');
|
|
8096
8605
|
let version = '0.0.0';
|
|
8097
8606
|
if (fs.existsSync(pkgPath)) {
|
|
@@ -8108,6 +8617,12 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8108
8617
|
return index;
|
|
8109
8618
|
}
|
|
8110
8619
|
|
|
8620
|
+
/**
|
|
8621
|
+
* Hot-cold and per-module strategies store most signatures outside the primary
|
|
8622
|
+
* copilot-instructions.md file. MCP tools must merge all sources.
|
|
8623
|
+
* @param {string} cwd
|
|
8624
|
+
* @returns {Map<string, string[]>}
|
|
8625
|
+
*/
|
|
8111
8626
|
function _enrichSigIndexFromStrategy(cwd, index) {
|
|
8112
8627
|
const path = require('path');
|
|
8113
8628
|
const coldPath = path.join(cwd, '.github', 'context-cold.md');
|
|
@@ -8116,49 +8631,138 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8116
8631
|
return index;
|
|
8117
8632
|
}
|
|
8118
8633
|
|
|
8634
|
+
/**
|
|
8635
|
+
* Build a signature index from the generated context file.
|
|
8636
|
+
* Returns Map<filePath, string[]> where filePath is the relative path
|
|
8637
|
+
* as it appears in the ### headers of the context file.
|
|
8638
|
+
*
|
|
8639
|
+
* Resolution priority:
|
|
8640
|
+
* 1. `opts.contextPath` — explicit path from --output or --adapter flag
|
|
8641
|
+
* 2. `customOutput` key in gen-context.config.json — persisted from a
|
|
8642
|
+
* previous `--output <file>` generation run
|
|
8643
|
+
* 3. All known adapter output paths probed in order (first non-empty wins)
|
|
8644
|
+
*
|
|
8645
|
+
* @param {string} cwd
|
|
8646
|
+
* @param {{ contextPath?: string }} [opts]
|
|
8647
|
+
* @returns {Map<string, string[]>}
|
|
8648
|
+
*/
|
|
8119
8649
|
function buildSigIndex(cwd, opts) {
|
|
8120
|
-
const fs
|
|
8121
|
-
|
|
8122
|
-
|
|
8650
|
+
const fs = require('fs');
|
|
8651
|
+
const path = require('path');
|
|
8652
|
+
|
|
8653
|
+
// 1. Caller supplied an explicit path — use it directly.
|
|
8654
|
+
if (opts && opts.contextPath) {
|
|
8655
|
+
const index = _parseContextFile(opts.contextPath);
|
|
8656
|
+
return _enrichSigIndexFromStrategy(cwd, index);
|
|
8657
|
+
}
|
|
8658
|
+
|
|
8659
|
+
// 2. Check gen-context.config.json for a persisted customOutput path.
|
|
8123
8660
|
try {
|
|
8124
8661
|
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
8125
8662
|
if (fs.existsSync(cfgPath)) {
|
|
8126
8663
|
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
8127
8664
|
if (cfg.customOutput) {
|
|
8128
|
-
const
|
|
8129
|
-
|
|
8665
|
+
const customPath = path.resolve(cwd, cfg.customOutput);
|
|
8666
|
+
const index = _parseContextFile(customPath);
|
|
8667
|
+
if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
|
|
8130
8668
|
}
|
|
8131
8669
|
}
|
|
8132
8670
|
} catch (_) {}
|
|
8671
|
+
|
|
8672
|
+
// 3. Probe all known adapter output paths; return first non-empty index.
|
|
8133
8673
|
for (const parts of ADAPTER_OUTPUT_PATHS) {
|
|
8134
8674
|
const contextPath = path.join(cwd, ...parts);
|
|
8135
8675
|
const index = _parseContextFile(contextPath);
|
|
8136
8676
|
if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
|
|
8137
8677
|
}
|
|
8138
|
-
|
|
8678
|
+
|
|
8679
|
+
// 4. Primary file empty/missing (hot-cold) — still serve cold + cache.
|
|
8680
|
+
const fallback = new Map();
|
|
8681
|
+
return _enrichSigIndexFromStrategy(cwd, fallback);
|
|
8139
8682
|
}
|
|
8683
|
+
|
|
8684
|
+
/**
|
|
8685
|
+
* Format ranked results as a markdown table string.
|
|
8686
|
+
*
|
|
8687
|
+
* @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
|
|
8688
|
+
* @param {string} query
|
|
8689
|
+
* @returns {string}
|
|
8690
|
+
*/
|
|
8140
8691
|
function formatRankTable(results, query) {
|
|
8141
|
-
if (!results || results.length === 0)
|
|
8142
|
-
|
|
8143
|
-
|
|
8692
|
+
if (!results || results.length === 0) {
|
|
8693
|
+
return `No matching files found for query: "${query}"\n`;
|
|
8694
|
+
}
|
|
8695
|
+
|
|
8696
|
+
const intent = (results[0] && results[0].intent) || 'search';
|
|
8697
|
+
const lines = [
|
|
8698
|
+
`## Query: ${query}`,
|
|
8699
|
+
`Intent: ${intent}`,
|
|
8700
|
+
'',
|
|
8701
|
+
'| Rank | File | Score | Sigs | Penalty |',
|
|
8702
|
+
'|------|------|-------|------|---------|',
|
|
8703
|
+
...results.map((r, i) => {
|
|
8704
|
+
const penalty = r.signals && r.signals.penalty ? r.signals.penalty.toFixed(2) : '1.00';
|
|
8705
|
+
return `| ${i + 1} | ${r.file} | ${r.score.toFixed(2)} | ${r.sigs.length} | ${penalty} |`;
|
|
8706
|
+
}),
|
|
8707
|
+
'',
|
|
8708
|
+
];
|
|
8709
|
+
|
|
8710
|
+
// Add signature details for top results
|
|
8144
8711
|
for (const r of results.slice(0, 3)) {
|
|
8145
8712
|
if (r.sigs.length > 0) {
|
|
8146
|
-
lines.push(`### ${r.file}
|
|
8713
|
+
lines.push(`### ${r.file}`);
|
|
8714
|
+
if (r.signals) {
|
|
8715
|
+
const sig = r.signals;
|
|
8716
|
+
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)}`);
|
|
8717
|
+
}
|
|
8718
|
+
lines.push('```');
|
|
8719
|
+
lines.push(...r.sigs.slice(0, 10));
|
|
8147
8720
|
if (r.sigs.length > 10) lines.push(`... (${r.sigs.length - 10} more)`);
|
|
8148
|
-
lines.push('```'
|
|
8721
|
+
lines.push('```');
|
|
8722
|
+
lines.push('');
|
|
8149
8723
|
}
|
|
8150
8724
|
}
|
|
8725
|
+
|
|
8151
8726
|
return lines.join('\n');
|
|
8152
8727
|
}
|
|
8728
|
+
|
|
8729
|
+
/**
|
|
8730
|
+
* Format ranked results as a structured JSON-serialisable object.
|
|
8731
|
+
*
|
|
8732
|
+
* @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
|
|
8733
|
+
* @param {string} query
|
|
8734
|
+
* @returns {object}
|
|
8735
|
+
*/
|
|
8153
8736
|
function formatRankJSON(results, query) {
|
|
8154
|
-
|
|
8737
|
+
const intent = (results && results[0] && results[0].intent) || 'search';
|
|
8738
|
+
return {
|
|
8739
|
+
query,
|
|
8740
|
+
intent,
|
|
8741
|
+
results: (results || []).map((r, i) => ({
|
|
8742
|
+
rank: i + 1,
|
|
8743
|
+
file: r.file,
|
|
8744
|
+
score: r.score,
|
|
8745
|
+
sigs: r.sigs,
|
|
8746
|
+
tokens: r.tokens,
|
|
8747
|
+
signals: r.signals || {},
|
|
8748
|
+
})),
|
|
8749
|
+
totalResults: (results || []).length,
|
|
8750
|
+
};
|
|
8155
8751
|
}
|
|
8752
|
+
|
|
8753
|
+
// ---------------------------------------------------------------------------
|
|
8754
|
+
// Intent detection — 7 intents
|
|
8755
|
+
// ---------------------------------------------------------------------------
|
|
8156
8756
|
const INTENT_PATTERNS = {
|
|
8157
8757
|
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,
|
|
8758
|
+
explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me|teach)\b/i,
|
|
8759
|
+
refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify|optimize)\b/i,
|
|
8760
|
+
review: /\b(review|check|audit|security|pr|pull request|assess|validate)\b/i,
|
|
8761
|
+
test: /\b(test|unit test|integration test|testing|spec|assert|mock)\b/i,
|
|
8762
|
+
integrate:/\b(import|integrate|connect|wire|bind|require|export|depend|graph)\b|require[ds]\b/i,
|
|
8763
|
+
navigate: /\b(find|locate|where|search|look for|show me|navigate|browse|list)\b/i,
|
|
8161
8764
|
};
|
|
8765
|
+
|
|
8162
8766
|
function detectIntent(query) {
|
|
8163
8767
|
if (!query || typeof query !== 'string') return 'search';
|
|
8164
8768
|
for (const [intent, re] of Object.entries(INTENT_PATTERNS)) {
|
|
@@ -8166,9 +8770,10 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
8166
8770
|
}
|
|
8167
8771
|
return 'search';
|
|
8168
8772
|
}
|
|
8169
|
-
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent };
|
|
8170
|
-
};
|
|
8171
8773
|
|
|
8774
|
+
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent };
|
|
8775
|
+
|
|
8776
|
+
};
|
|
8172
8777
|
// ── ./src/eval/scorer ──
|
|
8173
8778
|
__factories["./src/eval/scorer"] = function(module, exports) {
|
|
8174
8779
|
'use strict';
|
|
@@ -11571,7 +12176,7 @@ function __tryGit(args, opts = {}) {
|
|
|
11571
12176
|
catch (_) { return ''; }
|
|
11572
12177
|
}
|
|
11573
12178
|
|
|
11574
|
-
const VERSION = '7.
|
|
12179
|
+
const VERSION = '7.4.0';
|
|
11575
12180
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
11576
12181
|
|
|
11577
12182
|
function requireSourceOrBundled(key) {
|
package/llms-full.txt
CHANGED
|
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.
|
|
12
|
+
# Version: 7.4.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
|
@@ -24,7 +24,7 @@ Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
24
24
|
| Task success proxy | 10% | 52.2% |
|
|
25
25
|
| Prompts per task | 2.84 | 1.72 (39.4% fewer) |
|
|
26
26
|
| Supported languages | — | 31 |
|
|
27
|
-
| MCP tools | — |
|
|
27
|
+
| MCP tools | — | 15 |
|
|
28
28
|
| npm runtime dependencies | — | 0 |
|
|
29
29
|
|
|
30
30
|
---
|
|
@@ -114,7 +114,7 @@ sigmap --version Show version
|
|
|
114
114
|
|
|
115
115
|
---
|
|
116
116
|
|
|
117
|
-
## MCP server —
|
|
117
|
+
## MCP server — 15 tools
|
|
118
118
|
|
|
119
119
|
Start with `sigmap --mcp` (stdio JSON-RPC). Configure once:
|
|
120
120
|
|
|
@@ -218,6 +218,30 @@ Return the EXACT current signature(s) of named symbols (functions, classes, meth
|
|
|
218
218
|
Input: { symbols: array }
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
+
### sigmap_notify_file_created
|
|
222
|
+
|
|
223
|
+
Tell SigMap a file was created or modified so its signatures are indexed live for the rest of the session. Call this after writing a file — the new symbols become resolvable by search_signatures / get_callee_signatures.
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
Input: { path: string, content?: string }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### sigmap_notify_symbol_added
|
|
230
|
+
|
|
231
|
+
Fast path: register a single new symbol signature directly in the live index without re-reading the whole file.
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
Input: { signature: string, file: string, line?: number }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### sigmap_notify_file_deleted
|
|
238
|
+
|
|
239
|
+
Tell SigMap a file was deleted so its symbols are dropped from the live index.
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
Input: { path: string }
|
|
243
|
+
```
|
|
244
|
+
|
|
221
245
|
---
|
|
222
246
|
|
|
223
247
|
## Configuration (gen-context.config.json)
|
|
@@ -261,9 +285,9 @@ impact = {"depth":3,"includeSigs":true}
|
|
|
261
285
|
|
|
262
286
|
---
|
|
263
287
|
|
|
264
|
-
## Supported languages (
|
|
288
|
+
## Supported languages (35 extractors)
|
|
265
289
|
|
|
266
|
-
cpp, csharp, css, dart, dockerfile, gdscript, generic, go, graphql, html, java, javascript, kotlin, markdown, php, properties, protobuf, python, r, ruby, rust, scala, shell, sql, svelte, swift, terraform, toml, typescript, typescript_react, vue, vue_sfc, xml, yaml
|
|
290
|
+
cpp, csharp, css, dart, dispatch, dockerfile, gdscript, generic, go, graphql, html, java, javascript, kotlin, markdown, php, properties, protobuf, python, r, ruby, rust, scala, shell, sql, svelte, swift, terraform, toml, typescript, typescript_react, vue, vue_sfc, xml, yaml
|
|
267
291
|
|
|
268
292
|
---
|
|
269
293
|
|
package/llms.txt
CHANGED
|
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.
|
|
12
|
+
# Version: 7.4.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
|
@@ -27,7 +27,7 @@ Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
27
27
|
- Token reduction: 97.0% average across benchmark repos
|
|
28
28
|
- Task success: 52.2% vs 10% without SigMap
|
|
29
29
|
- Prompts per task: 1.72 vs 2.84 baseline (39.4% fewer)
|
|
30
|
-
- Languages: 31 supported · MCP tools:
|
|
30
|
+
- Languages: 31 supported · MCP tools: 15
|
|
31
31
|
- Dependencies: zero npm runtime dependencies · fully offline
|
|
32
32
|
|
|
33
33
|
## Quick start
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bundle-safe extractor dispatch.
|
|
5
|
+
*
|
|
6
|
+
* `packages/core`'s extract() resolves extractors via dynamic `require(path.join(…))`,
|
|
7
|
+
* which cannot run inside the standalone bundle (no filesystem `src/`). This module
|
|
8
|
+
* uses STATIC requires so the bundler rewrites them to `__require` and the extractors
|
|
9
|
+
* resolve from the bundled factories. Used by the live-index MCP write hooks.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Static language → extractor map (every entry is a bundled factory).
|
|
15
|
+
const EXTRACTORS = {
|
|
16
|
+
typescript: require('./typescript'),
|
|
17
|
+
typescript_react: require('./typescript_react'),
|
|
18
|
+
javascript: require('./javascript'),
|
|
19
|
+
python: require('./python'),
|
|
20
|
+
java: require('./java'),
|
|
21
|
+
kotlin: require('./kotlin'),
|
|
22
|
+
go: require('./go'),
|
|
23
|
+
rust: require('./rust'),
|
|
24
|
+
csharp: require('./csharp'),
|
|
25
|
+
cpp: require('./cpp'),
|
|
26
|
+
ruby: require('./ruby'),
|
|
27
|
+
php: require('./php'),
|
|
28
|
+
swift: require('./swift'),
|
|
29
|
+
dart: require('./dart'),
|
|
30
|
+
scala: require('./scala'),
|
|
31
|
+
gdscript: require('./gdscript'),
|
|
32
|
+
r: require('./r'),
|
|
33
|
+
vue: require('./vue'),
|
|
34
|
+
vue_sfc: require('./vue_sfc'),
|
|
35
|
+
svelte: require('./svelte'),
|
|
36
|
+
html: require('./html'),
|
|
37
|
+
css: require('./css'),
|
|
38
|
+
yaml: require('./yaml'),
|
|
39
|
+
shell: require('./shell'),
|
|
40
|
+
sql: require('./sql'),
|
|
41
|
+
graphql: require('./graphql'),
|
|
42
|
+
terraform: require('./terraform'),
|
|
43
|
+
protobuf: require('./protobuf'),
|
|
44
|
+
toml: require('./toml'),
|
|
45
|
+
properties: require('./properties'),
|
|
46
|
+
xml: require('./xml'),
|
|
47
|
+
markdown: require('./markdown'),
|
|
48
|
+
dockerfile: require('./dockerfile'),
|
|
49
|
+
generic: require('./generic'),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const EXT_MAP = {
|
|
53
|
+
'.ts': 'typescript', '.tsx': 'typescript_react',
|
|
54
|
+
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
55
|
+
'.py': 'python', '.pyw': 'python',
|
|
56
|
+
'.java': 'java',
|
|
57
|
+
'.kt': 'kotlin', '.kts': 'kotlin',
|
|
58
|
+
'.go': 'go',
|
|
59
|
+
'.rs': 'rust',
|
|
60
|
+
'.cs': 'csharp',
|
|
61
|
+
'.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
|
|
62
|
+
'.rb': 'ruby', '.rake': 'ruby',
|
|
63
|
+
'.php': 'php',
|
|
64
|
+
'.swift': 'swift',
|
|
65
|
+
'.dart': 'dart',
|
|
66
|
+
'.scala': 'scala', '.sc': 'scala',
|
|
67
|
+
'.gd': 'gdscript',
|
|
68
|
+
'.r': 'r', '.R': 'r',
|
|
69
|
+
'.vue': 'vue_sfc',
|
|
70
|
+
'.svelte': 'svelte',
|
|
71
|
+
'.html': 'html', '.htm': 'html',
|
|
72
|
+
'.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
|
|
73
|
+
'.yml': 'yaml', '.yaml': 'yaml',
|
|
74
|
+
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
|
|
75
|
+
'.sql': 'sql',
|
|
76
|
+
'.graphql': 'graphql', '.gql': 'graphql',
|
|
77
|
+
'.tf': 'terraform', '.tfvars': 'terraform',
|
|
78
|
+
'.proto': 'protobuf',
|
|
79
|
+
'.toml': 'toml',
|
|
80
|
+
'.properties': 'properties',
|
|
81
|
+
'.xml': 'xml',
|
|
82
|
+
'.md': 'markdown',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Resolve a language key from a file path/name. */
|
|
86
|
+
function langFor(filePathOrName) {
|
|
87
|
+
const base = path.basename(String(filePathOrName || ''));
|
|
88
|
+
if (base === 'Dockerfile' || base.startsWith('Dockerfile.')) return 'dockerfile';
|
|
89
|
+
const ext = path.extname(base).toLowerCase();
|
|
90
|
+
return EXT_MAP[ext] || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract signatures from a file's content using the right extractor.
|
|
95
|
+
* @param {string} filePathOrName - path or name (extension drives the extractor)
|
|
96
|
+
* @param {string} src - file content
|
|
97
|
+
* @returns {string[]}
|
|
98
|
+
*/
|
|
99
|
+
function extractFile(filePathOrName, src) {
|
|
100
|
+
if (!src || typeof src !== 'string') return [];
|
|
101
|
+
const lang = langFor(filePathOrName);
|
|
102
|
+
const mod = lang ? EXTRACTORS[lang] : null;
|
|
103
|
+
if (!mod || typeof mod.extract !== 'function') return [];
|
|
104
|
+
try {
|
|
105
|
+
const out = mod.extract(src);
|
|
106
|
+
return Array.isArray(out) ? out : [];
|
|
107
|
+
} catch (_) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { extractFile, langFor };
|
package/src/mcp/handlers.js
CHANGED
|
@@ -596,4 +596,80 @@ function getCalleeSignatures(args, cwd) {
|
|
|
596
596
|
}
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
-
|
|
599
|
+
// ── Layer 1: live-index write hooks ────────────────────────────────────────
|
|
600
|
+
// Keep the sig-cache fresh while an agent creates/modifies/deletes files, so
|
|
601
|
+
// new code is discoverable in the same session. buildSigIndex already merges
|
|
602
|
+
// the cache (_buildSigIndexFromCache), so updates are live on the next read.
|
|
603
|
+
|
|
604
|
+
function _pkgVersion(cwd) {
|
|
605
|
+
try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
|
|
606
|
+
catch (_) { return '0.0.0'; }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
/** notify_file_created — extract a file's signatures and index it live. */
|
|
611
|
+
function notifyFileCreated(args, cwd) {
|
|
612
|
+
const rel = args && args.path;
|
|
613
|
+
if (!rel) return 'Missing required argument: path';
|
|
614
|
+
try {
|
|
615
|
+
const { extractFile } = require('../extractors/dispatch');
|
|
616
|
+
const { loadCache, saveCache } = require('../cache/sig-cache');
|
|
617
|
+
const abs = path.resolve(cwd, rel);
|
|
618
|
+
let content = args.content;
|
|
619
|
+
if (typeof content !== 'string') {
|
|
620
|
+
try { content = fs.readFileSync(abs, 'utf8'); } catch (_) { content = ''; }
|
|
621
|
+
}
|
|
622
|
+
const sigs = extractFile(abs, content);
|
|
623
|
+
const version = _pkgVersion(cwd);
|
|
624
|
+
const cache = loadCache(cwd, version);
|
|
625
|
+
if (sigs.length > 0) {
|
|
626
|
+
cache.set(abs, { mtime: Date.now(), sigs });
|
|
627
|
+
}
|
|
628
|
+
saveCache(cwd, version, cache);
|
|
629
|
+
return `Indexed ${rel}: ${sigs.length} signature(s) now live.`;
|
|
630
|
+
} catch (err) {
|
|
631
|
+
return `_notify_file_created failed: ${err.message}_`;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** notify_symbol_added — append one signature to a file's live cache entry. */
|
|
636
|
+
function notifySymbolAdded(args, cwd) {
|
|
637
|
+
if (!args || !args.signature || !args.file) {
|
|
638
|
+
return 'Missing required arguments: signature, file';
|
|
639
|
+
}
|
|
640
|
+
try {
|
|
641
|
+
const { loadCache, saveCache } = require('../cache/sig-cache');
|
|
642
|
+
const abs = path.resolve(cwd, args.file);
|
|
643
|
+
const version = _pkgVersion(cwd);
|
|
644
|
+
const cache = loadCache(cwd, version);
|
|
645
|
+
const entry = cache.get(abs) || { mtime: Date.now(), sigs: [] };
|
|
646
|
+
const line = Number.isFinite(Number(args.line)) ? ` :${args.line}` : '';
|
|
647
|
+
const sig = String(args.signature) + line;
|
|
648
|
+
if (!entry.sigs.includes(sig)) entry.sigs.push(sig);
|
|
649
|
+
entry.mtime = Date.now();
|
|
650
|
+
cache.set(abs, entry);
|
|
651
|
+
saveCache(cwd, version, cache);
|
|
652
|
+
return `Added signature to ${args.file} (${entry.sigs.length} total).`;
|
|
653
|
+
} catch (err) {
|
|
654
|
+
return `_notify_symbol_added failed: ${err.message}_`;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/** notify_file_deleted — drop a file's cache-overlay entry. */
|
|
659
|
+
function notifyFileDeleted(args, cwd) {
|
|
660
|
+
const rel = args && args.path;
|
|
661
|
+
if (!rel) return 'Missing required argument: path';
|
|
662
|
+
try {
|
|
663
|
+
const { loadCache, saveCache } = require('../cache/sig-cache');
|
|
664
|
+
const abs = path.resolve(cwd, rel);
|
|
665
|
+
const version = _pkgVersion(cwd);
|
|
666
|
+
const cache = loadCache(cwd, version);
|
|
667
|
+
const had = cache.delete(abs);
|
|
668
|
+
saveCache(cwd, version, cache);
|
|
669
|
+
return had ? `Removed ${rel} from the live index.` : `${rel} was not in the live cache.`;
|
|
670
|
+
} catch (err) {
|
|
671
|
+
return `_notify_file_deleted failed: ${err.message}_`;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted };
|
package/src/mcp/server.js
CHANGED
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
const readline = require('readline');
|
|
16
16
|
const { TOOLS } = require('./tools');
|
|
17
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures } = require('./handlers');
|
|
17
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted } = require('./handlers');
|
|
18
18
|
|
|
19
19
|
const SERVER_INFO = {
|
|
20
20
|
name: 'sigmap',
|
|
21
|
-
version: '7.
|
|
21
|
+
version: '7.4.0',
|
|
22
22
|
description: 'SigMap MCP server — code signatures on demand',
|
|
23
23
|
};
|
|
24
24
|
|
|
@@ -78,6 +78,9 @@ function dispatch(msg, cwd) {
|
|
|
78
78
|
else if (name === 'get_lines') text = getLines(args, cwd);
|
|
79
79
|
else if (name === 'read_memory') text = readMemory(args, cwd);
|
|
80
80
|
else if (name === 'get_callee_signatures') text = getCalleeSignatures(args, cwd);
|
|
81
|
+
else if (name === 'sigmap_notify_file_created') text = notifyFileCreated(args, cwd);
|
|
82
|
+
else if (name === 'sigmap_notify_symbol_added') text = notifySymbolAdded(args, cwd);
|
|
83
|
+
else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
|
|
81
84
|
else {
|
|
82
85
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
83
86
|
return;
|
package/src/mcp/tools.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* MCP tool definitions for SigMap (
|
|
4
|
+
* MCP tool definitions for SigMap (15 tools).
|
|
5
5
|
* read_context, search_signatures, get_map, create_checkpoint, get_routing,
|
|
6
6
|
* explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
|
|
7
|
-
* get_callee_signatures
|
|
7
|
+
* get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
|
|
8
|
+
* sigmap_notify_file_deleted.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const TOOLS = [
|
|
@@ -235,6 +236,48 @@ const TOOLS = [
|
|
|
235
236
|
required: ['symbols'],
|
|
236
237
|
},
|
|
237
238
|
},
|
|
239
|
+
{
|
|
240
|
+
name: 'sigmap_notify_file_created',
|
|
241
|
+
description:
|
|
242
|
+
'Tell SigMap a file was created or modified so its signatures are indexed ' +
|
|
243
|
+
'live for the rest of the session. Call this after writing a file — the new ' +
|
|
244
|
+
'symbols become resolvable by search_signatures / get_callee_signatures.',
|
|
245
|
+
inputSchema: {
|
|
246
|
+
type: 'object',
|
|
247
|
+
properties: {
|
|
248
|
+
path: { type: 'string', description: 'File path relative to the project root.' },
|
|
249
|
+
content: { type: 'string', description: 'Optional file content; read from disk if omitted.' },
|
|
250
|
+
},
|
|
251
|
+
required: ['path'],
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: 'sigmap_notify_symbol_added',
|
|
256
|
+
description:
|
|
257
|
+
'Fast path: register a single new symbol signature directly in the live ' +
|
|
258
|
+
'index without re-reading the whole file.',
|
|
259
|
+
inputSchema: {
|
|
260
|
+
type: 'object',
|
|
261
|
+
properties: {
|
|
262
|
+
signature: { type: 'string', description: 'The signature line (e.g. "function check(key)").' },
|
|
263
|
+
file: { type: 'string', description: 'File path the symbol belongs to (relative to root).' },
|
|
264
|
+
line: { type: 'number', description: 'Optional 1-based line number.' },
|
|
265
|
+
},
|
|
266
|
+
required: ['signature', 'file'],
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: 'sigmap_notify_file_deleted',
|
|
271
|
+
description:
|
|
272
|
+
'Tell SigMap a file was deleted so its symbols are dropped from the live index.',
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: {
|
|
276
|
+
path: { type: 'string', description: 'Deleted file path relative to the project root.' },
|
|
277
|
+
},
|
|
278
|
+
required: ['path'],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
238
281
|
];
|
|
239
282
|
|
|
240
283
|
module.exports = { TOOLS };
|