sigmap 6.10.11 → 6.11.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,27 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [6.11.0] — 2026-06-03
14
+
15
+ ### Added
16
+
17
+ - **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).
18
+ - New shared `src/extractors/line-anchor.js` helper (`lineAt`, `anchor`, `withAnchor`).
19
+
20
+ ### Fixed
21
+
22
+ - **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.
23
+
24
+ ---
25
+
26
+ ## [6.10.12] — 2026-05-27
27
+
28
+ ### Added
29
+
30
+ - **Portable `.mcp.json` support** — MCP server registration now detects and prioritizes `.mcp.json` at the project root, making MCP configuration portable across multiple agentic harnesses (Claude, Cursor, Windsurf, etc.). Falls back to `.claude/settings.json` if `.mcp.json` doesn't exist (closes #209).
31
+
32
+ ---
33
+
13
34
  ## [6.10.11] — 2026-05-22
14
35
 
15
36
  ### Fixed
package/gen-context.js CHANGED
@@ -5901,7 +5901,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
5901
5901
 
5902
5902
  const SERVER_INFO = {
5903
5903
  name: 'sigmap',
5904
- version: '6.10.11',
5904
+ version: '6.11.0',
5905
5905
  description: 'SigMap MCP server — code signatures on demand',
5906
5906
  };
5907
5907
 
@@ -8655,7 +8655,7 @@ const path = require('path');
8655
8655
  const os = require('os');
8656
8656
  const { execSync } = require('child_process');
8657
8657
 
8658
- const VERSION = '6.10.11';
8658
+ const VERSION = '6.11.0';
8659
8659
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8660
8660
 
8661
8661
  function requireSourceOrBundled(key) {
@@ -10455,9 +10455,11 @@ function registerMcp(cwd, scriptPath) {
10455
10455
  args: [path.resolve(scriptPath), '--mcp'],
10456
10456
  };
10457
10457
 
10458
- // JSON mcpServers targets: Claude, Cursor, Windsurf project, Windsurf global,
10459
- // VS Code (GitHub Copilot 1.99+), OpenCode project, OpenCode global, Gemini CLI
10458
+ // JSON mcpServers targets: portable .mcp.json (priority), Claude, Cursor,
10459
+ // Windsurf project, Windsurf global, VS Code (GitHub Copilot 1.99+),
10460
+ // OpenCode project, OpenCode global, Gemini CLI
10460
10461
  const jsonTargets = [
10462
+ path.join(cwd, '.mcp.json'),
10461
10463
  path.join(cwd, '.claude', 'settings.json'),
10462
10464
  path.join(cwd, '.cursor', 'mcp.json'),
10463
10465
  path.join(cwd, '.windsurf', 'mcp.json'),
@@ -10496,6 +10498,8 @@ function registerMcp(cwd, scriptPath) {
10496
10498
 
10497
10499
  // Print manual snippets for all targets
10498
10500
  console.warn('[sigmap] MCP / context server config snippets:');
10501
+ console.warn(' .mcp.json (portable, recommended):');
10502
+ console.warn(JSON.stringify({ mcpServers: { sigmap: serverEntry } }, null, 2));
10499
10503
  console.warn(' Claude / Cursor / Windsurf / VS Code / OpenCode / Gemini CLI:');
10500
10504
  console.warn(JSON.stringify({ mcpServers: { sigmap: serverEntry } }, null, 2));
10501
10505
  console.warn(' Zed (~/.config/zed/settings.json):');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.10.11",
3
+ "version": "6.11.0",
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.11",
3
+ "version": "6.11.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": "6.10.11",
3
+ "version": "6.11.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,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.11',
21
+ version: '6.11.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24