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 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
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures };
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.3.0',
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 (12 tools).
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
- 'use strict';
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, symbolMatch: 0.5, prefixMatch: 0.3, pathMatch: 0.8, recencyBoost: 1.5,
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 sigTokenSet = new Set(tokenize(sigs.join(' ')));
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
- score += w.exactToken;
7998
- if (sigs.some((sig) => tokenize(sig.replace(/[^a-zA-Z0-9_\s]/g, ' ')).includes(qt))) score += w.symbolMatch;
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)) { score += w.prefixMatch; break; }
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
- if (pathTokenSet.has(qt)) score += w.pathMatch;
8363
+
8364
+ // Path token match
8365
+ if (pathTokenSet.has(qt)) {
8366
+ score += w.pathMatch;
8367
+ signals.pathMatch += w.pathMatch;
8368
+ }
8006
8369
  }
8007
- return score;
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 weights = (opts && opts.weights) ? Object.assign({}, DEFAULT_WEIGHTS, opts.weights) : DEFAULT_WEIGHTS;
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()) all.push({ file, score: sigs.length, sigs, tokens: Math.ceil(sigs.join('\n').length / 4) });
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
- let score = scoreFile(file, sigs, queryTokens, weights);
8027
- if (recencySet && recencySet.has(file) && score > 0) score *= recencyMultiplier;
8028
- if (learnedWeights && score > 0) score *= learnedWeights[file] || 1.0;
8029
- scored.push({ file, score, sigs, tokens: Math.ceil(sigs.join('\n').length / 4) });
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; let inBlock = false; let sigs = [];
8561
+ let currentFile = null;
8562
+ let inBlock = false;
8563
+ let sigs = [];
8564
+
8069
8565
  for (const line of lines) {
8070
- const hm = line.match(/^###\s+(\S+)\s*$/);
8071
- if (hm) { if (currentFile !== null) index.set(currentFile, sigs); currentFile = hm[1]; sigs = []; inBlock = false; continue; }
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 } = require('../cache/sig-cache');
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 = require('fs'); const path = require('path');
8121
- if (opts && opts.contextPath) return _enrichSigIndexFromStrategy(cwd, _parseContextFile(opts.contextPath));
8122
- // Check gen-context.config.json for a persisted customOutput path.
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 idx = _parseContextFile(path.resolve(cwd, cfg.customOutput));
8129
- if (idx.size > 0) return _enrichSigIndexFromStrategy(cwd, idx);
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
- return _enrichSigIndexFromStrategy(cwd, new Map());
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) return `No matching files found for query: "${query}"\n`;
8142
- const lines = [`## Query: ${query}`, '', '| Rank | File | Score | Sigs | Tokens |', '|------|------|-------|------|--------|',
8143
- ...results.map((r, i) => `| ${i + 1} | ${r.file} | ${r.score.toFixed(2)} | ${r.sigs.length} | ${r.tokens} |`), ''];
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}`, '```', ...r.sigs.slice(0, 10));
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
- return { query, results: (results || []).map((r, i) => ({ rank: i + 1, file: r.file, score: r.score, sigs: r.sigs, tokens: r.tokens })), totalResults: (results || []).length };
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.3.0';
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.3.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
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 | — | 12 |
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 — 12 tools
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 (34 extractors)
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.3.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
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: 12
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.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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 };
@@ -596,4 +596,80 @@ function getCalleeSignatures(args, cwd) {
596
596
  }
597
597
  }
598
598
 
599
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures };
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.3.0',
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 (12 tools).
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 };