sigmap 6.10.12 → 6.11.1

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,27 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [6.11.1] — 2026-06-04
14
+
15
+ ### Fixed
16
+
17
+ - **MCP hot-cold cold signatures in bundled server** — the bundled MCP server now includes the hot-cold "cold" signatures, so context lookups return complete results under the hot-cold strategy (closes #201, PR #216). Thanks @rudi193-cmd.
18
+
19
+ ---
20
+
21
+ ## [6.11.0] — 2026-06-03
22
+
23
+ ### Added
24
+
25
+ - **Line anchors on signatures (Surgical Context Phase 1)** — top-level TypeScript and Python signatures now carry a `:start-end` line anchor (e.g. `export class UserRepository :18-36`), so agents can read the exact lines instead of re-opening whole files. Rendered automatically by `ask`, `CLAUDE.md`, and every adapter — no consumer changes (closes #212).
26
+ - New shared `src/extractors/line-anchor.js` helper (`lineAt`, `anchor`, `withAnchor`).
27
+
28
+ ### Fixed
29
+
30
+ - **Block-comment / docstring line-shift bug** — comment stripping that blanked `/* */` and `"""…"""` to `''` destroyed newlines and corrupted line numbers. Replaced with a newline-preserving strip so char-offset → line-number stays exact. The Python AST and regex fallback paths now produce identical anchors.
31
+
32
+ ---
33
+
13
34
  ## [6.10.12] — 2026-05-27
14
35
 
15
36
  ### Added
package/README.md CHANGED
@@ -47,10 +47,10 @@ SigMap extracts function and class signatures from your codebase and feeds the r
47
47
 
48
48
  ## Why SigMap?
49
49
 
50
- - **78.9% hit@5** — right file found in top 5 results (vs 13.6% baseline)
51
- - **97.9% token reduction** — 278K instead of 13.5M tokens across 21 repos
52
- - **52.2% task success rate** — up from 10% without context
53
- - **1.66 prompts per task** — down from 2.84 (40.6% fewer retries)
50
+ - **81.1% hit@5** — right file found in top 5 results (vs 13.6% baseline)
51
+ - **96.5% token reduction** — average across 21 real repos
52
+ - **53.3% task success rate** — up from 10% without context
53
+ - **1.66 prompts per task** — down from 2.84 (41.8% fewer retries)
54
54
  - **31 languages supported** — TypeScript, Python, Go, Rust, Java, R, and 25 others
55
55
  - **No vendor lock-in** — works with any AI assistant or local LLM
56
56
  - **No API costs** — use local models (Ollama, llama.cpp, vLLM) with zero token fees
@@ -63,7 +63,7 @@ SigMap extracts function and class signatures from your codebase and feeds the r
63
63
 
64
64
  | Without SigMap | With SigMap |
65
65
  |---|---|
66
- | ❌ Guessing which files are relevant | ✅ Right file in context — 80% of the time |
66
+ | ❌ Guessing which files are relevant | ✅ Right file in context — 81% of the time |
67
67
  | ❌ Sending the full repo to your AI | ✅ Minimal context — only what matters |
68
68
  | ❌ Embeddings / vector DB required | ✅ Grounded answers, no infra needed |
69
69
 
@@ -87,12 +87,12 @@ Ask → Rank → Context → Validate → Judge → Learn
87
87
  ## Benchmark
88
88
 
89
89
  ```
90
- Benchmark : sigmap-v6.10-main (21 repositories, including R language)
91
- Date : 2026-05-22
90
+ Benchmark : sigmap-v6.11-main (21 repositories, including R language)
91
+ Date : 2026-06-04
92
92
 
93
- Hit@5 : 80% (baseline 13.6% — 5.9× lift)
93
+ Hit@5 : 81.1% (baseline 13.6% — 6.0× lift)
94
94
  Token reduction: 96.5% (across 21 repos)
95
- Prompt reduction : 41.4% (2.84 → 1.67 prompts per task)
95
+ Prompt reduction : 41.8% (2.84 → 1.66 prompts per task)
96
96
  Task success : 53.3% (baseline 10%)
97
97
  Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift, Ruby, PHP, Scala, Kotlin, and more)
98
98
  ```
package/gen-context.js CHANGED
@@ -1174,6 +1174,30 @@ __factories["./src/extractors/php"] = function(module, exports) {
1174
1174
 
1175
1175
  };
1176
1176
 
1177
+ // ── ./src/extractors/line-anchor ──
1178
+ __factories["./src/extractors/line-anchor"] = function(module, exports) {
1179
+
1180
+ function lineAt(src, idx) {
1181
+ let line = 1;
1182
+ const end = Math.min(idx, src.length);
1183
+ for (let i = 0; i < end; i++) {
1184
+ if (src.charCodeAt(i) === 10) line++;
1185
+ }
1186
+ return line;
1187
+ }
1188
+
1189
+ function anchor(start, end) {
1190
+ return ` :${start}-${end}`;
1191
+ }
1192
+
1193
+ function withAnchor(sig, start, end) {
1194
+ return `${sig}${anchor(start, end)}`;
1195
+ }
1196
+
1197
+ module.exports = { lineAt, anchor, withAnchor };
1198
+
1199
+ };
1200
+
1177
1201
  // ── ./src/extractors/python ──
1178
1202
  __factories["./src/extractors/python"] = function(module, exports) {
1179
1203
 
@@ -5339,42 +5363,29 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
5339
5363
  */
5340
5364
  function searchSignatures(args, cwd) {
5341
5365
  if (!args || !args.query) return 'Missing required argument: query';
5342
-
5343
- const contextPath = path.join(cwd, CONTEXT_FILE);
5344
- if (!fs.existsSync(contextPath)) {
5345
- return 'No context file found. Run: node gen-context.js';
5346
- }
5347
-
5348
- const content = fs.readFileSync(contextPath, 'utf8');
5349
5366
  const query = args.query.toLowerCase();
5350
- const lines = content.split('\n');
5351
-
5352
- const result = [];
5353
- let currentFile = '';
5354
- let fileHeaderAdded = false;
5355
-
5356
- for (const line of lines) {
5357
- if (line.startsWith('### ')) {
5358
- currentFile = line.slice(4).trim();
5359
- fileHeaderAdded = false;
5360
- continue;
5361
- }
5362
- // Skip markdown fences and top-level headers
5363
- if (line.startsWith('```') || line.startsWith('## ') || line.startsWith('# ') || line.startsWith('<!--')) {
5364
- continue;
5367
+
5368
+ try {
5369
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
5370
+ const index = buildSigIndex(cwd);
5371
+ if (index.size === 0) {
5372
+ return 'No context file found. Run: node gen-context.js';
5365
5373
  }
5366
- if (line.toLowerCase().includes(query)) {
5367
- if (currentFile && !fileHeaderAdded) {
5368
- if (result.length > 0) result.push('');
5369
- result.push(`### ${currentFile}`);
5370
- fileHeaderAdded = true;
5371
- }
5372
- result.push(line);
5374
+
5375
+ const result = [];
5376
+ for (const [file, sigs] of index.entries()) {
5377
+ const hits = sigs.filter((s) => s.toLowerCase().includes(query));
5378
+ if (hits.length === 0) continue;
5379
+ if (result.length > 0) result.push('');
5380
+ result.push(`### ${file}`);
5381
+ result.push(...hits);
5373
5382
  }
5383
+
5384
+ if (result.length === 0) return `No signatures found matching: ${args.query}`;
5385
+ return result.join('\n');
5386
+ } catch (err) {
5387
+ return `_search_signatures failed: ${err.message}_`;
5374
5388
  }
5375
-
5376
- if (result.length === 0) return `No signatures found matching: ${args.query}`;
5377
- return result.join('\n');
5378
5389
  }
5379
5390
 
5380
5391
  /**
@@ -5640,56 +5651,44 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
5640
5651
  }
5641
5652
 
5642
5653
  function listModules(args, cwd) {
5643
- const contextPath = path.join(cwd, CONTEXT_FILE);
5644
- if (!fs.existsSync(contextPath)) {
5645
- return 'No context file found. Run: node gen-context.js';
5646
- }
5647
-
5648
- const content = fs.readFileSync(contextPath, 'utf8');
5649
- const ctxLines = content.split('\n');
5650
- const groups = {};
5651
- let currentGroup = null;
5652
- let blockBuf = [];
5653
-
5654
- function flushBlock() {
5655
- if (currentGroup === null || blockBuf.length === 0) return;
5656
- if (!groups[currentGroup]) groups[currentGroup] = { fileCount: 0, tokenCount: 0 };
5657
- groups[currentGroup].fileCount++;
5658
- groups[currentGroup].tokenCount += Math.ceil(blockBuf.join('\n').length / 4);
5659
- blockBuf = [];
5660
- }
5661
-
5662
- for (const line of ctxLines) {
5663
- if (line.startsWith('### ')) {
5664
- flushBlock();
5665
- const rel = line.slice(4).trim().replace(/\\/g, '/');
5654
+ try {
5655
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
5656
+ const index = buildSigIndex(cwd);
5657
+ if (index.size === 0) {
5658
+ return 'No context file found. Run: node gen-context.js';
5659
+ }
5660
+
5661
+ const groups = {};
5662
+ for (const [rel, sigs] of index.entries()) {
5666
5663
  const parts = rel.split('/');
5667
- currentGroup = parts.length > 1 ? parts[0] : '.';
5668
- } else if (currentGroup !== null) {
5669
- blockBuf.push(line);
5664
+ const mod = parts.length > 1 ? parts[0] : '.';
5665
+ if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
5666
+ groups[mod].fileCount++;
5667
+ groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
5670
5668
  }
5669
+
5670
+ const sorted = Object.entries(groups)
5671
+ .map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
5672
+ .sort((a, b) => b.tokenCount - a.tokenCount);
5673
+
5674
+ if (sorted.length === 0) return 'No modules found in context file.';
5675
+
5676
+ const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
5677
+
5678
+ return [
5679
+ '# Modules',
5680
+ '',
5681
+ '| Module | Files | Tokens |',
5682
+ '|--------|-------|--------|',
5683
+ ...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
5684
+ '',
5685
+ `**Total context tokens: ~${total}**`,
5686
+ '',
5687
+ '_Use `read_context({ module: "name" })` to get signatures for a specific module._',
5688
+ ].join('\n');
5689
+ } catch (err) {
5690
+ return `_list_modules failed: ${err.message}_`;
5671
5691
  }
5672
- flushBlock();
5673
-
5674
- const sorted = Object.entries(groups)
5675
- .map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
5676
- .sort((a, b) => b.tokenCount - a.tokenCount);
5677
-
5678
- if (sorted.length === 0) return 'No modules found in context file.';
5679
-
5680
- const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
5681
-
5682
- return [
5683
- '# Modules',
5684
- '',
5685
- '| Module | Files | Tokens |',
5686
- '|--------|-------|--------|',
5687
- ...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
5688
- '',
5689
- `**Total context tokens: ~${total}**`,
5690
- '',
5691
- '_Use `read_context({ module: "name" })` to get signatures for a specific module._',
5692
- ].join('\n');
5693
5692
  }
5694
5693
 
5695
5694
  function queryContext(args, cwd) {
@@ -5901,7 +5900,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
5901
5900
 
5902
5901
  const SERVER_INFO = {
5903
5902
  name: 'sigmap',
5904
- version: '6.10.12',
5903
+ version: '6.11.1',
5905
5904
  description: 'SigMap MCP server — code signatures on demand',
5906
5905
  };
5907
5906
 
@@ -6968,9 +6967,50 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
6968
6967
  if (currentFile !== null) index.set(currentFile, sigs);
6969
6968
  return index;
6970
6969
  }
6970
+
6971
+ function _mergeSigIndex(target, source) {
6972
+ for (const [file, sigs] of source.entries()) {
6973
+ if (!sigs || sigs.length === 0) continue;
6974
+ if (!target.has(file) || target.get(file).length < sigs.length) {
6975
+ target.set(file, sigs);
6976
+ }
6977
+ }
6978
+ return target;
6979
+ }
6980
+
6981
+ function _buildSigIndexFromCache(cwd) {
6982
+ const fs = require('fs');
6983
+ const path = require('path');
6984
+ const index = new Map();
6985
+ try {
6986
+ const { loadCache } = require('../cache/sig-cache');
6987
+ const pkgPath = path.join(cwd, 'package.json');
6988
+ let version = '0.0.0';
6989
+ if (fs.existsSync(pkgPath)) {
6990
+ version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || version;
6991
+ }
6992
+ const cache = loadCache(cwd, version);
6993
+ for (const [absPath, entry] of cache.entries()) {
6994
+ if (!entry || !entry.sigs || entry.sigs.length === 0) continue;
6995
+ const rel = path.relative(cwd, absPath).replace(/\\/g, '/');
6996
+ if (!rel || rel.startsWith('..')) continue;
6997
+ index.set(rel, entry.sigs);
6998
+ }
6999
+ } catch (_) {}
7000
+ return index;
7001
+ }
7002
+
7003
+ function _enrichSigIndexFromStrategy(cwd, index) {
7004
+ const path = require('path');
7005
+ const coldPath = path.join(cwd, '.github', 'context-cold.md');
7006
+ _mergeSigIndex(index, _parseContextFile(coldPath));
7007
+ _mergeSigIndex(index, _buildSigIndexFromCache(cwd));
7008
+ return index;
7009
+ }
7010
+
6971
7011
  function buildSigIndex(cwd, opts) {
6972
7012
  const fs = require('fs'); const path = require('path');
6973
- if (opts && opts.contextPath) return _parseContextFile(opts.contextPath);
7013
+ if (opts && opts.contextPath) return _enrichSigIndexFromStrategy(cwd, _parseContextFile(opts.contextPath));
6974
7014
  // Check gen-context.config.json for a persisted customOutput path.
6975
7015
  try {
6976
7016
  const cfgPath = path.join(cwd, 'gen-context.config.json');
@@ -6978,16 +7018,16 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
6978
7018
  const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
6979
7019
  if (cfg.customOutput) {
6980
7020
  const idx = _parseContextFile(path.resolve(cwd, cfg.customOutput));
6981
- if (idx.size > 0) return idx;
7021
+ if (idx.size > 0) return _enrichSigIndexFromStrategy(cwd, idx);
6982
7022
  }
6983
7023
  }
6984
7024
  } catch (_) {}
6985
7025
  for (const parts of ADAPTER_OUTPUT_PATHS) {
6986
7026
  const contextPath = path.join(cwd, ...parts);
6987
7027
  const index = _parseContextFile(contextPath);
6988
- if (index.size > 0) return index;
7028
+ if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
6989
7029
  }
6990
- return new Map();
7030
+ return _enrichSigIndexFromStrategy(cwd, new Map());
6991
7031
  }
6992
7032
  function formatRankTable(results, query) {
6993
7033
  if (!results || results.length === 0) return `No matching files found for query: "${query}"\n`;
@@ -8655,7 +8695,7 @@ const path = require('path');
8655
8695
  const os = require('os');
8656
8696
  const { execSync } = require('child_process');
8657
8697
 
8658
- const VERSION = '6.10.12';
8698
+ const VERSION = '6.11.1';
8659
8699
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8660
8700
 
8661
8701
  function requireSourceOrBundled(key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.10.12",
3
+ "version": "6.11.1",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "6.10.12",
3
+ "version": "6.11.1",
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": "6.10.12",
3
+ "version": "6.11.1",
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,52 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Line-anchor helpers for Surgical Context (v6.11.0).
5
+ *
6
+ * Signatures carry their source location as a `:start-end` suffix so an agent
7
+ * can read the exact lines instead of re-opening the whole file. The anchor is
8
+ * a plain string suffix, which keeps the existing `string[]` signature contract
9
+ * intact — ranker, adapters, and CLAUDE.md render it for free.
10
+ */
11
+
12
+ /**
13
+ * 1-based line number of character index `idx` within `src`.
14
+ * Counts newlines in the prefix, so it stays correct as long as the source
15
+ * being indexed preserves every newline (see the newline-preserving comment
16
+ * strips in the extractors).
17
+ *
18
+ * @param {string} src
19
+ * @param {number} idx
20
+ * @returns {number}
21
+ */
22
+ function lineAt(src, idx) {
23
+ let line = 1;
24
+ const end = Math.min(idx, src.length);
25
+ for (let i = 0; i < end; i++) {
26
+ if (src.charCodeAt(i) === 10) line++;
27
+ }
28
+ return line;
29
+ }
30
+
31
+ /**
32
+ * Render an anchor suffix: ` :start-end`.
33
+ * @param {number} start
34
+ * @param {number} end
35
+ * @returns {string}
36
+ */
37
+ function anchor(start, end) {
38
+ return ` :${start}-${end}`;
39
+ }
40
+
41
+ /**
42
+ * Append a line anchor to a signature string.
43
+ * @param {string} sig
44
+ * @param {number} start
45
+ * @param {number} end
46
+ * @returns {string}
47
+ */
48
+ function withAnchor(sig, start, end) {
49
+ return `${sig}${anchor(start, end)}`;
50
+ }
51
+
52
+ module.exports = { lineAt, anchor, withAnchor };
@@ -1,6 +1,27 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
+ const { lineAt } = require('./line-anchor');
5
+
6
+ /**
7
+ * 1-based line of the last source line belonging to a top-level (indent 0)
8
+ * def/class body that starts at `startLine` (1-based). Trailing blank lines
9
+ * are excluded.
10
+ * @param {string[]} srcLines
11
+ * @param {number} startLine
12
+ * @returns {number}
13
+ */
14
+ function pyBlockEnd(srcLines, startLine) {
15
+ let end = startLine;
16
+ for (let i = startLine; i < srcLines.length; i++) {
17
+ const line = srcLines[i];
18
+ if (line.trim() === '') continue;
19
+ const indent = line.match(/^(\s*)/)[1].length;
20
+ if (indent === 0) break;
21
+ end = i + 1;
22
+ }
23
+ return end;
24
+ }
4
25
 
5
26
  /**
6
27
  * Try to extract signatures using the native Python AST extractor.
@@ -42,21 +63,26 @@ function extract(src, filePath) {
42
63
 
43
64
  // noComments: strip only # comments, keep docstrings (needed for @decorator detection)
44
65
  const noComments = src.replace(/#.*$/gm, '');
45
- // stripped: also strip docstrings (safe for regex matching)
66
+ // stripped: also strip docstrings (safe for regex matching). Docstrings are
67
+ // blanked newline-by-newline (non-newline chars → spaces) so character
68
+ // offsets and line numbers stay exact for line anchors.
46
69
  const stripped = noComments
47
- .replace(/"""[\s\S]*?"""/g, '')
48
- .replace(/'''[\s\S]*?'''/g, '');
70
+ .replace(/"""[\s\S]*?"""/g, (m) => m.replace(/[^\n]/g, ' '))
71
+ .replace(/'''[\s\S]*?'''/g, (m) => m.replace(/[^\n]/g, ' '));
72
+ const srcLines = src.split('\n');
49
73
 
50
74
  // Classes
51
75
  for (const m of stripped.matchAll(/^class\s+(\w+)(?:\s*\(([^)]*)\))?\s*:/gm)) {
52
76
  const className = m[1];
53
77
  const baseName = m[2] ? m[2].trim() : '';
54
78
  const bodyStart = m.index + m[0].length;
79
+ const clsStart = lineAt(stripped, m.index);
80
+ const clsAnchor = ` :${clsStart}-${pyBlockEnd(srcLines, clsStart)}`;
55
81
 
56
82
  // Try @dataclass collapse
57
83
  const dcFields = tryExtractDataclassFields(stripped, m.index);
58
84
  if (dcFields !== null) {
59
- sigs.push(`@dataclass ${className}(${dcFields})`);
85
+ sigs.push(`@dataclass ${className}(${dcFields})${clsAnchor}`);
60
86
  continue;
61
87
  }
62
88
 
@@ -64,13 +90,13 @@ function extract(src, filePath) {
64
90
  if (/(BaseModel|BaseSettings)/.test(baseName)) {
65
91
  const bmFields = tryExtractBaseModelFields(stripped, bodyStart);
66
92
  if (bmFields) {
67
- sigs.push(`class ${className}(${baseName}) ${bmFields}`);
93
+ sigs.push(`class ${className}(${baseName}) ${bmFields}${clsAnchor}`);
68
94
  continue;
69
95
  }
70
96
  }
71
97
 
72
98
  const baseStr = baseName ? `(${baseName})` : '';
73
- sigs.push(`class ${className}${baseStr}`);
99
+ sigs.push(`class ${className}${baseStr}${clsAnchor}`);
74
100
 
75
101
  // Class-level ALL_CAPS constants
76
102
  for (const c of extractClassConstants(stripped, bodyStart)) {
@@ -91,7 +117,9 @@ function extract(src, filePath) {
91
117
  const retStr = retType ? ` → ${retType}` : '';
92
118
  const hint = extractDocHint(src, m[2], m[0]);
93
119
  const hintStr = hint ? ` # ${hint}` : '';
94
- sigs.push(`${asyncKw}def ${m[2]}(${params})${retStr}${hintStr}`);
120
+ const fnStart = lineAt(stripped, m.index);
121
+ const fnAnchor = ` :${fnStart}-${pyBlockEnd(srcLines, fnStart)}`;
122
+ sigs.push(`${asyncKw}def ${m[2]}(${params})${retStr}${fnAnchor}${hintStr}`);
95
123
  }
96
124
 
97
125
  // FastAPI router endpoints: @router.METHOD("path") + async def name(...)
@@ -103,7 +131,7 @@ function extract(src, filePath) {
103
131
  for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
104
132
  const fl = lines[j].trim();
105
133
  const fm = fl.match(/^(?:async\s+)?def\s+(\w+)/);
106
- if (fm) { sigs.push(`${rm[1].toUpperCase()} ${rm[2]} → ${fm[1]}()`); break; }
134
+ if (fm) { const rs = j + 1; sigs.push(`${rm[1].toUpperCase()} ${rm[2]} → ${fm[1]}() :${rs}-${pyBlockEnd(srcLines, rs)}`); break; }
107
135
  if (fl && !fl.startsWith('@') && !fl.startsWith('#')) break;
108
136
  }
109
137
  }
@@ -213,6 +213,12 @@ def extract_class_constants(class_node):
213
213
  yield f"{name}={val}"
214
214
 
215
215
 
216
+ def _anchor(node):
217
+ """Return a ` :start-end` line anchor for a top-level node (1-based)."""
218
+ end = getattr(node, "end_lineno", None) or node.lineno
219
+ return f" :{node.lineno}-{end}"
220
+
221
+
216
222
  def extract_method_sig(func_node):
217
223
  """Format a method signature string (already indented by caller)."""
218
224
  is_async = isinstance(func_node, ast.AsyncFunctionDef)
@@ -232,7 +238,7 @@ def extract_function_sig(func_node, src_lines=None):
232
238
  ret_str = f" → {ret}" if ret else ""
233
239
  hint = get_docstring_hint(func_node)
234
240
  hint_str = f" # {hint}" if hint else ""
235
- return f"{prefix}def {func_node.name}({params}){ret_str}{hint_str}"
241
+ return f"{prefix}def {func_node.name}({params}){ret_str}{_anchor(func_node)}{hint_str}"
236
242
 
237
243
 
238
244
  def extract_fastapi_routes(tree, src_lines):
@@ -255,7 +261,7 @@ def extract_fastapi_routes(tree, src_lines):
255
261
  path_node = dec.args[0]
256
262
  if isinstance(path_node, ast.Constant):
257
263
  path = path_node.value
258
- routes.append(f"{method.upper()} {path} → {node.name}()")
264
+ routes.append(f"{method.upper()} {path} → {node.name}(){_anchor(node)}")
259
265
  return routes
260
266
 
261
267
 
@@ -276,10 +282,11 @@ def extract(filepath):
276
282
  if isinstance(node, ast.ClassDef):
277
283
  bases_str = ", ".join(annotation_to_str(b) for b in node.bases if b)
278
284
  dec_names = get_decorator_names(node)
285
+ cls_anchor = _anchor(node)
279
286
 
280
287
  if is_dataclass(node):
281
288
  fields = extract_dataclass_fields(node)
282
- sigs.append(f"@dataclass {node.name}({fields})")
289
+ sigs.append(f"@dataclass {node.name}({fields}){cls_anchor}")
283
290
  elif is_basemodel(node.bases):
284
291
  bm_fields = extract_basemodel_fields(node)
285
292
  base_label = next(
@@ -288,12 +295,12 @@ def extract(filepath):
288
295
  "BaseModel"
289
296
  )
290
297
  if bm_fields:
291
- sigs.append(f"class {node.name}({base_label}) {bm_fields}")
298
+ sigs.append(f"class {node.name}({base_label}) {bm_fields}{cls_anchor}")
292
299
  else:
293
- sigs.append(f"class {node.name}({base_label})")
300
+ sigs.append(f"class {node.name}({base_label}){cls_anchor}")
294
301
  else:
295
302
  base_part = f"({bases_str})" if bases_str else ""
296
- sigs.append(f"class {node.name}{base_part}")
303
+ sigs.append(f"class {node.name}{base_part}{cls_anchor}")
297
304
 
298
305
  # Class constants
299
306
  for const in extract_class_constants(node):
@@ -1,37 +1,54 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from TypeScript source code.
7
+ * Top-level declarations carry a `:start-end` line anchor (see line-anchor.js);
8
+ * indented members do not.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
8
12
  function extract(src) {
9
13
  if (!src || typeof src !== 'string') return [];
10
14
  const sigs = [];
11
-
12
- // Strip single-line comments
15
+ // anchors[i] is [start, end] for a top-level sig, or null for an indented member.
16
+ // Kept parallel to `sigs` so existing push/mutation logic stays untouched;
17
+ // anchors are applied once at return.
18
+ const anchors = [];
19
+
20
+ // Strip comments to simplify matching. Block comments are blanked
21
+ // newline-by-newline (non-newline chars → spaces) so character offsets AND
22
+ // line numbers stay exact. Line comments preserve their trailing newline.
13
23
  const stripped = src
14
24
  .replace(/\/\/.*$/gm, '')
15
- .replace(/\/\*[\s\S]*?\*\//g, '');
25
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
26
+
27
+ // Index of the closing brace for a block whose body starts at bodyStart.
28
+ const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
16
29
 
17
30
  // Exported interfaces
18
31
  for (const m of stripped.matchAll(/^export\s+interface\s+(\w+)(?:<[^{]*>)?\s*(?:extends\s+[^{]+)?\{/gm)) {
32
+ const bodyStart = m.index + m[0].length;
19
33
  sigs.push(`export interface ${m[1]}`);
34
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
20
35
  // Collect members
21
- const start = m.index + m[0].length;
22
- const block = extractBlock(stripped, start);
36
+ const block = extractBlock(stripped, bodyStart);
23
37
  const members = extractInterfaceMembers(block);
24
- for (const mem of members) sigs.push(` ${mem}`);
38
+ for (const mem of members) { sigs.push(` ${mem}`); anchors.push(null); }
25
39
  }
26
40
 
27
41
  // Exported type aliases
28
42
  for (const m of stripped.matchAll(/^export\s+type\s+(\w+)(?:<[^=]*>)?\s*=/gm)) {
29
43
  sigs.push(`export type ${m[1]}`);
44
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, m.index + m[0].length)]);
30
45
  }
31
46
 
32
47
  // Exported enums
33
48
  for (const m of stripped.matchAll(/^export\s+(?:const\s+)?enum\s+(\w+)\s*\{/gm)) {
49
+ const bodyStart = m.index + m[0].length;
34
50
  sigs.push(`export enum ${m[1]}`);
51
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
35
52
  }
36
53
 
37
54
  // Classes (exported and internal)
@@ -39,11 +56,12 @@ function extract(src) {
39
56
  for (const m of stripped.matchAll(classRegex)) {
40
57
  const prefix = m[1] ? 'export ' : '';
41
58
  const abs = m[2] ? 'abstract ' : '';
59
+ const bodyStart = m.index + m[0].length;
42
60
  sigs.push(`${prefix}${abs}class ${m[3]}`);
43
- const start = m.index + m[0].length;
44
- const block = extractBlock(stripped, start);
61
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
62
+ const block = extractBlock(stripped, bodyStart);
45
63
  const methods = extractClassMembers(block);
46
- for (const meth of methods) sigs.push(` ${meth}`);
64
+ for (const meth of methods) { sigs.push(` ${meth}`); anchors.push(null); }
47
65
  }
48
66
 
49
67
  // Exported top-level functions (not methods)
@@ -53,11 +71,12 @@ function extract(src) {
53
71
  const retMatch = m[0].match(/\)\s*:\s*([^{]+)\s*\{/);
54
72
  const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 30) : '';
55
73
  const retStr = retType ? ` → ${retType}` : '';
74
+ const bodyStart = m.index + m[0].length;
56
75
  sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
76
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
57
77
 
58
78
  // Hooks: capture compact return object shape for use* functions.
59
79
  if (m[1].startsWith('use')) {
60
- const bodyStart = m.index + m[0].length;
61
80
  const body = stripped.slice(bodyStart, bodyStart + 800);
62
81
  const ret = body.match(/return\s*\{([^}]{1,260})\}/);
63
82
  if (ret) {
@@ -78,10 +97,14 @@ function extract(src) {
78
97
  const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
79
98
  const params = normalizeParams(m[2]);
80
99
  sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
100
+ const bodyStart = stripped.indexOf('{', m.index + m[0].length);
101
+ const endLn = bodyStart !== -1
102
+ ? lineAt(stripped, blockEndIdx(bodyStart + 1))
103
+ : lineAt(stripped, m.index + m[0].length);
104
+ anchors.push([lineAt(stripped, m.index), endLn]);
81
105
 
82
106
  // Hooks: capture compact return object shape for use* functions.
83
107
  if (m[1].startsWith('use')) {
84
- const bodyStart = stripped.indexOf('{', m.index + m[0].length);
85
108
  if (bodyStart !== -1) {
86
109
  const body = stripped.slice(bodyStart, bodyStart + 800);
87
110
  const ret = body.match(/return\s*\{([^}]{1,260})\}/);
@@ -102,22 +125,30 @@ function extract(src) {
102
125
  // Zustand stores: export const useXxxStore = create<State>()(...)
103
126
  for (const m of stripped.matchAll(/^export\s+const\s+(use\w+Store)\s*=\s*create(?:<[^>]*>)?\s*\(/gm)) {
104
127
  const stateType = m[0].match(/create<([\w]+)>/)?.[1] || '';
128
+ const startLn = lineAt(stripped, m.index);
105
129
  sigs.push(`export const ${m[1]} = create<${stateType}>(...)`);
130
+ anchors.push([startLn, startLn]);
106
131
  const ifaceRe = new RegExp(`interface\\s+${stateType}\\s*\\{([\\s\\S]*?)\\}`);
107
132
  const ifm = stripped.match(ifaceRe);
108
133
  if (ifm) {
109
- for (const fm of ifm[1].matchAll(/^\s+(\w+)\s*(?:\([^)]*\))?\s*:/gm)) sigs.push(` ${fm[1]}`);
134
+ for (const fm of ifm[1].matchAll(/^\s+(\w+)\s*(?:\([^)]*\))?\s*:/gm)) { sigs.push(` ${fm[1]}`); anchors.push(null); }
110
135
  }
111
136
  }
112
137
 
113
138
  // API client objects: const xxxApi = { method: async () => {} }
114
139
  for (const m of stripped.matchAll(/^(?:export\s+default\s+|const\s+)(\w*[Aa]pi\w*)\s*=\s*\{/gm)) {
115
- const block = extractBlock(stripped, m.index + m[0].length);
140
+ const bodyStart = m.index + m[0].length;
141
+ const block = extractBlock(stripped, bodyStart);
116
142
  const methods = [...block.matchAll(/^\s+(\w+)\s*:\s*(?:async\s+)?(?:\([^)]*\)|\w+)\s*=>/gm)].map(mm => mm[1]);
117
- if (methods.length) sigs.push(`${m[1]}: { ${methods.join(', ')} }`);
143
+ if (methods.length) {
144
+ sigs.push(`${m[1]}: { ${methods.join(', ')} }`);
145
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)]);
146
+ }
118
147
  }
119
148
 
120
- return sigs.slice(0, 35);
149
+ return sigs
150
+ .map((s, i) => (anchors[i] ? withAnchor(s, anchors[i][0], anchors[i][1]) : s))
151
+ .slice(0, 35);
121
152
  }
122
153
 
123
154
  function extractBlock(src, startIndex) {
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '6.10.12',
21
+ version: '6.11.1',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24