sigmap 8.16.1 → 8.18.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/llms-full.txt CHANGED
@@ -11,19 +11,19 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.16.1 | Benchmark: sigmap-v8.16-main (2026-07-11)
14
+ # Version: 8.18.0 | Benchmark: sigmap-v8.18-main (2026-07-12)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
18
18
  ---
19
19
 
20
- ## Core metrics (benchmark: sigmap-v8.16-main, 2026-07-11)
20
+ ## Core metrics (benchmark: sigmap-v8.18-main, 2026-07-12)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
24
- | Retrieval hit@5 | 13.6% (random) | 87.8% (6.5× lift) |
25
- | Token reduction | — | 97.0% average |
26
- | Task success proxy | 10% | 67.8% |
24
+ | Retrieval hit@5 | 13.6% (random) | 86.7% (6.4× lift) |
25
+ | Token reduction | — | 96.9% average |
26
+ | Task success proxy | 10% | 68.9% |
27
27
  | Prompts per task | 2.84 | 1.44 (49.2% fewer) |
28
28
  | Supported languages | — | 33 |
29
29
  | MCP tools | — | 20 |
@@ -343,7 +343,7 @@ testCoverage = false
343
343
  testDirs = ["tests","test","__tests__","spec"]
344
344
  sigCache = false
345
345
  impactRadius = false
346
- retrieval = {"topK":10,"recencyBoost":1.5,"callGraphBoost":false}
346
+ retrieval = {"topK":10,"recencyBoost":1.5,"callGraphBoost":false,"surfaceEnrichment":false}
347
347
  impact = {"depth":3,"includeSigs":true}
348
348
  ```
349
349
 
package/llms.txt CHANGED
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.16.1 | Benchmark: sigmap-v8.16-main (2026-07-11)
14
+ # Version: 8.18.0 | Benchmark: sigmap-v8.18-main (2026-07-12)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
@@ -23,11 +23,11 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
23
23
  - No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
24
24
  - Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
25
25
 
26
- ## Core metrics (benchmark: sigmap-v8.16-main, 2026-07-11)
26
+ ## Core metrics (benchmark: sigmap-v8.18-main, 2026-07-12)
27
27
 
28
- - hit@5 retrieval: 87.8% vs 13.6% random baseline (6.5× lift)
29
- - Token reduction: 97.0% average across benchmark repos
30
- - Task success: 67.8% vs 10% without SigMap
28
+ - hit@5 retrieval: 86.7% vs 13.6% random baseline (6.4× lift)
29
+ - Token reduction: 96.9% average across benchmark repos
30
+ - Task success: 68.9% vs 10% without SigMap
31
31
  - Prompts per task: 1.44 vs 2.84 baseline (49.2% fewer)
32
32
  - Languages: 33 supported · MCP tools: 20
33
33
  - Dependencies: zero npm runtime dependencies · fully offline
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.16.1",
3
+ "version": "8.18.0",
4
4
  "description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
5
5
  "main": "packages/core/index.js",
6
6
  "exports": {
@@ -31,6 +31,7 @@
31
31
  "benchmark:test-discovery": "node scripts/run-test-discovery-benchmark.mjs --save",
32
32
  "benchmark:terse": "node scripts/run-terse-benchmark.mjs --save",
33
33
  "benchmark:callgraph-boost": "node scripts/run-callgraph-boost-benchmark.mjs --save",
34
+ "benchmark:surface-enrichment": "node scripts/run-surface-enrichment-benchmark.mjs --save",
34
35
  "validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
35
36
  "health": "node gen-context.js --health",
36
37
  "map": "node gen-project-map.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.16.1",
3
+ "version": "8.18.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": "8.16.1",
3
+ "version": "8.18.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -149,6 +149,8 @@ const DEFAULTS = {
149
149
  recencyBoost: 1.5,
150
150
  // Boost files call-graph-connected to query matches (opt-in, measure-gated)
151
151
  callGraphBoost: false,
152
+ // Append route pseudo-signatures to the rankable index (opt-in, measure-gated)
153
+ surfaceEnrichment: false,
152
154
  },
153
155
 
154
156
  // Impact layer settings (v2.5)
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from C# source code.
7
+ * Signatures carry `:start-end` line anchors (Surgical Context); the comment
8
+ * strip below is newline-preserving so anchor lines match the original file.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
@@ -11,14 +15,18 @@ function extract(src) {
11
15
 
12
16
  const stripped = src
13
17
  .replace(/\/\/.*$/gm, '')
14
- .replace(/\/\*[\s\S]*?\*\//g, '');
18
+ .replace(/\/\*[\s\S]*?\*\//g, (s) => s.replace(/[^\n]/g, ' '));
15
19
 
16
20
  // Classes and interfaces
17
21
  const typeRe = /^\s*(?:public\s+|internal\s+|protected\s+)?(?:abstract\s+|sealed\s+|static\s+)?(class|interface|enum|record|struct)\s+(\w+)(?:<[^{]*>)?(?:\s*:\s*[\w<>, .]+)?\s*\{/gm;
18
22
  for (const m of stripped.matchAll(typeRe)) {
19
- sigs.push(`${m[1]} ${m[2]}`);
20
- const block = extractBlock(stripped, m.index + m[0].length);
21
- for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
23
+ const declIdx = m.index + (m[0].length - m[0].trimStart().length);
24
+ const bodyStart = m.index + m[0].length;
25
+ const block = extractBlock(stripped, bodyStart);
26
+ sigs.push(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, declIdx), lineAt(stripped, bodyStart + block.length)));
27
+ for (const meth of extractMembers(block)) {
28
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
29
+ }
22
30
  }
23
31
 
24
32
  return sigs.slice(0, 25);
@@ -41,7 +49,11 @@ function extractMembers(block) {
41
49
  for (const m of block.matchAll(methodRe)) {
42
50
  const ret = normalizeType(m[1]);
43
51
  const retStr = ret ? ` → ${ret}` : '';
44
- members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
52
+ members.push({
53
+ text: `${m[2]}(${normalizeParams(m[3])})${retStr}`,
54
+ declIdx: m.index + (m[0].length - m[0].trimStart().length),
55
+ endIdx: m.index + m[0].length,
56
+ });
45
57
  }
46
58
  return members.slice(0, 8);
47
59
  }
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from Dart source code.
7
+ * Signatures carry `:start-end` line anchors (Surgical Context); the comment
8
+ * strip below is newline-preserving so anchor lines match the original file.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
@@ -11,21 +15,37 @@ function extract(src) {
11
15
 
12
16
  const stripped = src
13
17
  .replace(/\/\/.*$/gm, '')
14
- .replace(/\/\*[\s\S]*?\*\//g, '');
18
+ .replace(/\/\*[\s\S]*?\*\//g, (s) => s.replace(/[^\n]/g, ' '));
19
+
20
+ // Anchor range: scan past same-line trivia (`async`, `=>` stops) to a body `{`.
21
+ const rangeFor = (declIdx, afterIdx) => {
22
+ let k = afterIdx;
23
+ while (k < stripped.length && /[ \tA-Za-z0-9_]/.test(stripped[k])) k++;
24
+ if (stripped[k] === '{') {
25
+ const end = k + 1 + extractBlock(stripped, k + 1).length;
26
+ return [lineAt(stripped, declIdx), lineAt(stripped, end)];
27
+ }
28
+ const line = lineAt(stripped, declIdx);
29
+ return [line, line];
30
+ };
15
31
 
16
32
  // Classes and abstract classes
17
33
  for (const m of stripped.matchAll(/^(?:abstract\s+)?class\s+(\w+)(?:<[^{]*>)?(?:\s+extends\s+[\w<>, ]+)?(?:\s+(?:implements|with|on)\s+[\w<>, ]+)?\s*\{/gm)) {
18
34
  const abs = m[0].trimStart().startsWith('abstract') ? 'abstract ' : '';
19
- sigs.push(`${abs}class ${m[1]}`);
20
- const block = extractBlock(stripped, m.index + m[0].length);
21
- for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
35
+ const bodyStart = m.index + m[0].length;
36
+ const block = extractBlock(stripped, bodyStart);
37
+ sigs.push(withAnchor(`${abs}class ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
38
+ for (const meth of extractMembers(block)) {
39
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
40
+ }
22
41
  }
23
42
 
24
43
  // Top-level functions — capture return type (prefix before name) and show as suffix
25
44
  for (const m of stripped.matchAll(/^((?:Future<[\w<>?,\s]*>|[\w<>?]+))\s+(\w+)\s*\(([^)]*)\)/gm)) {
26
45
  if (m[2].startsWith('_')) continue;
27
- const retStr = (m[1] && m[1] !== 'void') ? ` \u2192 ${m[1].replace(/\s+/g, '').slice(0, 25)}` : '';
28
- sigs.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
46
+ const retStr = (m[1] && m[1] !== 'void') ? ` ${m[1].replace(/\s+/g, '').slice(0, 25)}` : '';
47
+ const [s, e] = rangeFor(m.index, m.index + m[0].length);
48
+ sigs.push(withAnchor(`${m[2]}(${normalizeParams(m[3])})${retStr}`, s, e));
29
49
  }
30
50
 
31
51
  return sigs.slice(0, 25);
@@ -46,8 +66,12 @@ function extractMembers(block) {
46
66
  const members = [];
47
67
  for (const m of block.matchAll(/^\s+(?:@override\s+)?(?:@\w+\s+)*((?:Future<[\w<>?,\s]*>|[\w<>?]+))\s+(\w+)\s*\(([^)]*)\)/gm)) {
48
68
  if (m[2].startsWith('_')) continue;
49
- const retStr = (m[1] && m[1] !== 'void') ? ` \u2192 ${m[1].replace(/\s+/g, '').slice(0, 25)}` : '';
50
- members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
69
+ const retStr = (m[1] && m[1] !== 'void') ? ` ${m[1].replace(/\s+/g, '').slice(0, 25)}` : '';
70
+ members.push({
71
+ text: `${m[2]}(${normalizeParams(m[3])})${retStr}`,
72
+ declIdx: m.index + (m[0].length - m[0].trimStart().length),
73
+ endIdx: m.index + m[0].length,
74
+ });
51
75
  }
52
76
  return members.slice(0, 8);
53
77
  }
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from Go source code.
7
+ * Signatures carry `:start-end` line anchors (Surgical Context); the comment
8
+ * strip below is newline-preserving so anchor lines match the original file.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
@@ -11,26 +15,34 @@ function extract(src) {
11
15
 
12
16
  const stripped = src
13
17
  .replace(/\/\/.*$/gm, '')
14
- .replace(/\/\*[\s\S]*?\*\//g, '');
18
+ .replace(/\/\*[\s\S]*?\*\//g, (s) => s.replace(/[^\n]/g, ' '));
19
+
20
+ // Index of the closing brace for a block opened just before startIndex.
21
+ const blockEndIdx = (startIndex) => startIndex + extractBlock(stripped, startIndex).length;
15
22
 
16
23
  // Structs
17
24
  for (const m of stripped.matchAll(/^type\s+(\w+)\s+struct\s*\{/gm)) {
18
- sigs.push(`type ${m[1]} struct`);
25
+ const end = blockEndIdx(m.index + m[0].length);
26
+ sigs.push(withAnchor(`type ${m[1]} struct`, lineAt(stripped, m.index), lineAt(stripped, end)));
19
27
  }
20
28
 
21
29
  // Interfaces
22
30
  for (const m of stripped.matchAll(/^type\s+(\w+)\s+interface\s*\{/gm)) {
23
- sigs.push(`type ${m[1]} interface`);
24
- const block = extractBlock(stripped, m.index + m[0].length);
25
- for (const method of extractInterfaceMethods(block)) sigs.push(` ${method}`);
31
+ const bodyStart = m.index + m[0].length;
32
+ const block = extractBlock(stripped, bodyStart);
33
+ sigs.push(withAnchor(`type ${m[1]} interface`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
34
+ for (const meth of extractInterfaceMethods(block)) {
35
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
36
+ }
26
37
  }
27
38
 
28
39
  // Functions and methods — capture return type between ) and {
29
40
  for (const m of stripped.matchAll(/^func\s+(?:\((\w+)\s+[\w*]+\)\s+)?(\w+)\s*\(([^)]*)\)([^{]*)\{/gm)) {
30
41
  const receiver = m[1] ? `(${m[1]}) ` : '';
31
42
  const retType = m[4] ? m[4].trim().replace(/\s+/g, ' ') : '';
32
- const retStr = retType ? ` \u2192 ${retType.slice(0, 30)}` : '';
33
- sigs.push(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`);
43
+ const retStr = retType ? ` ${retType.slice(0, 30)}` : '';
44
+ const end = blockEndIdx(m.index + m[0].length);
45
+ sigs.push(withAnchor(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`, lineAt(stripped, m.index), lineAt(stripped, end)));
34
46
  }
35
47
 
36
48
  return sigs.slice(0, 25);
@@ -51,8 +63,12 @@ function extractInterfaceMethods(block) {
51
63
  const methods = [];
52
64
  for (const m of block.matchAll(/^\s+(\w+)\s*\(([^)]*)\)([^\n]*)/gm)) {
53
65
  const retType = m[3] ? m[3].trim().replace(/\s+/g, ' ') : '';
54
- const retStr = retType ? ` \u2192 ${retType.slice(0, 30)}` : '';
55
- methods.push(`${m[1]}(${normalizeParams(m[2])})${retStr}`);
66
+ const retStr = retType ? ` ${retType.slice(0, 30)}` : '';
67
+ methods.push({
68
+ text: `${m[1]}(${normalizeParams(m[2])})${retStr}`,
69
+ declIdx: m.index + (m[0].length - m[0].trimStart().length),
70
+ endIdx: m.index + m[0].length,
71
+ });
56
72
  }
57
73
  return methods.slice(0, 8);
58
74
  }
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from Java source code.
7
+ * Signatures carry `:start-end` line anchors (Surgical Context); the comment
8
+ * strip below is newline-preserving so anchor lines match the original file.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
@@ -11,14 +15,17 @@ function extract(src) {
11
15
 
12
16
  const stripped = src
13
17
  .replace(/\/\/.*$/gm, '')
14
- .replace(/\/\*[\s\S]*?\*\//g, '');
18
+ .replace(/\/\*[\s\S]*?\*\//g, (s) => s.replace(/[^\n]/g, ' '));
15
19
 
16
20
  // Classes and interfaces
17
21
  const typeRegex = /^(?:public\s+|protected\s+)?(?:abstract\s+|final\s+)?(class|interface|enum)\s+(\w+)(?:\s+extends\s+[\w<>, .]+)?(?:\s+implements\s+[\w<>, .]+)?\s*\{/gm;
18
22
  for (const m of stripped.matchAll(typeRegex)) {
19
- sigs.push(`${m[1]} ${m[2]}`);
20
- const block = extractBlock(stripped, m.index + m[0].length);
21
- for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
23
+ const bodyStart = m.index + m[0].length;
24
+ const block = extractBlock(stripped, bodyStart);
25
+ sigs.push(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
26
+ for (const meth of extractMembers(block)) {
27
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
28
+ }
22
29
  }
23
30
 
24
31
  return sigs.slice(0, 25);
@@ -42,7 +49,11 @@ function extractMembers(block) {
42
49
  for (const m of block.matchAll(methodRe)) {
43
50
  const ret = normalizeType(m[1]);
44
51
  const retStr = ret ? ` → ${ret}` : '';
45
- members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
52
+ members.push({
53
+ text: `${m[2]}(${normalizeParams(m[3])})${retStr}`,
54
+ declIdx: m.index + (m[0].length - m[0].trimStart().length),
55
+ endIdx: m.index + m[0].length,
56
+ });
46
57
  }
47
58
  return members.slice(0, 8);
48
59
  }
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from Kotlin source code.
7
+ * Signatures carry `:start-end` line anchors (Surgical Context); the comment
8
+ * strip below is newline-preserving so anchor lines match the original file.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
@@ -11,21 +15,37 @@ function extract(src) {
11
15
 
12
16
  const stripped = src
13
17
  .replace(/\/\/.*$/gm, '')
14
- .replace(/\/\*[\s\S]*?\*\//g, '');
18
+ .replace(/\/\*[\s\S]*?\*\//g, (s) => s.replace(/[^\n]/g, ' '));
19
+
20
+ // Anchor range: scan past same-line modifiers to a body `{` (range) else single line.
21
+ const rangeFor = (declIdx, afterIdx) => {
22
+ let k = afterIdx;
23
+ while (k < stripped.length && /[ \tA-Za-z0-9_]/.test(stripped[k])) k++;
24
+ if (stripped[k] === '{') {
25
+ const end = k + 1 + extractBlock(stripped, k + 1).length;
26
+ return [lineAt(stripped, declIdx), lineAt(stripped, end)];
27
+ }
28
+ const line = lineAt(stripped, declIdx);
29
+ return [line, line];
30
+ };
15
31
 
16
32
  // Classes, objects, interfaces
17
33
  for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:data\s+|sealed\s+|abstract\s+|open\s+)?(class|object|interface)\s+(\w+)(?:[^{]*)\{/gm)) {
18
- sigs.push(`${m[1]} ${m[2]}`);
19
- const block = extractBlock(stripped, m.index + m[0].length);
20
- for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
34
+ const bodyStart = m.index + m[0].length;
35
+ const block = extractBlock(stripped, bodyStart);
36
+ sigs.push(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
37
+ for (const meth of extractMembers(block)) {
38
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
39
+ }
21
40
  }
22
41
 
23
42
  // Top-level functions — capture `: RetType` after params
24
43
  for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{=]+))?/gm)) {
25
44
  const suspend = m[0].includes('suspend') ? 'suspend ' : '';
26
45
  const retType = m[3] ? m[3].trim().replace(/\s+/g, ' ') : '';
27
- const retStr = retType ? ` \u2192 ${retType.slice(0, 25)}` : '';
28
- sigs.push(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})${retStr}`);
46
+ const retStr = retType ? ` ${retType.slice(0, 25)}` : '';
47
+ const [s, e] = rangeFor(m.index, m.index + m[0].length);
48
+ sigs.push(withAnchor(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e));
29
49
  }
30
50
 
31
51
  return sigs.slice(0, 25);
@@ -48,8 +68,12 @@ function extractMembers(block) {
48
68
  if (m[1].startsWith('_')) continue;
49
69
  const suspend = m[0].includes('suspend') ? 'suspend ' : '';
50
70
  const retType = m[3] ? m[3].trim().replace(/\s+/g, ' ') : '';
51
- const retStr = retType ? ` \u2192 ${retType.slice(0, 25)}` : '';
52
- members.push(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})${retStr}`);
71
+ const retStr = retType ? ` ${retType.slice(0, 25)}` : '';
72
+ members.push({
73
+ text: `${suspend}fun ${m[1]}(${normalizeParams(m[2])})${retStr}`,
74
+ declIdx: m.index + (m[0].length - m[0].trimStart().length),
75
+ endIdx: m.index + m[0].length,
76
+ });
53
77
  }
54
78
  return members.slice(0, 8);
55
79
  }
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from PHP source code.
7
+ * Signatures carry `:start-end` line anchors (Surgical Context); the comment
8
+ * strips below are newline-preserving so anchor lines match the original file.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
@@ -12,23 +16,40 @@ function extract(src) {
12
16
  const stripped = src
13
17
  .replace(/\/\/.*$/gm, '')
14
18
  .replace(/#.*$/gm, '')
15
- .replace(/\/\*[\s\S]*?\*\//g, '');
19
+ .replace(/\/\*[\s\S]*?\*\//g, (s) => s.replace(/[^\n]/g, ' '));
20
+
21
+ // Anchor range: scan past same-line trivia to a body `{` (range) else single line.
22
+ const rangeFor = (declIdx, afterIdx) => {
23
+ let k = afterIdx;
24
+ while (k < stripped.length && /[ \tA-Za-z0-9_:?\\]/.test(stripped[k])) k++;
25
+ if (stripped[k] === '{' || (stripped[k] === '\n' && stripped[k + 1] === '{')) {
26
+ const open = stripped[k] === '{' ? k : k + 1;
27
+ const end = open + 1 + extractBlock(stripped, open + 1).length;
28
+ return [lineAt(stripped, declIdx), lineAt(stripped, end)];
29
+ }
30
+ const line = lineAt(stripped, declIdx);
31
+ return [line, line];
32
+ };
16
33
 
17
34
  // Classes and interfaces
18
35
  const typeRe = /^(?:abstract\s+)?(?:class|interface|trait)\s+(\w+)(?:\s+extends\s+\w+)?(?:\s+implements\s+[\w, ]+)?\s*\{/gm;
19
36
  for (const m of stripped.matchAll(typeRe)) {
20
37
  const kind = m[0].trimStart().startsWith('interface') ? 'interface' :
21
38
  m[0].trimStart().startsWith('trait') ? 'trait' : 'class';
22
- sigs.push(`${kind} ${m[1]}`);
23
- const block = extractBlock(stripped, m.index + m[0].length);
24
- for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
39
+ const bodyStart = m.index + m[0].length;
40
+ const block = extractBlock(stripped, bodyStart);
41
+ sigs.push(withAnchor(`${kind} ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
42
+ for (const meth of extractMembers(block)) {
43
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
44
+ }
25
45
  }
26
46
 
27
47
  // Top-level functions
28
48
  for (const m of stripped.matchAll(/^function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*([^\n{]+))?/gm)) {
29
49
  const ret = normalizeType(m[3]);
30
50
  const retStr = ret ? ` → ${ret}` : '';
31
- sigs.push(`function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
51
+ const [s, e] = rangeFor(m.index, m.index + m[0].length);
52
+ sigs.push(withAnchor(`function ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e));
32
53
  }
33
54
 
34
55
  return sigs.slice(0, 25);
@@ -53,7 +74,11 @@ function extractMembers(block) {
53
74
  const isStatic = m[0].includes('static ') ? 'static ' : '';
54
75
  const ret = normalizeType(m[3]);
55
76
  const retStr = ret ? ` → ${ret}` : '';
56
- members.push(`${isStatic}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
77
+ members.push({
78
+ text: `${isStatic}function ${m[1]}(${normalizeParams(m[2])})${retStr}`,
79
+ declIdx: m.index + (m[0].length - m[0].trimStart().length),
80
+ endIdx: m.index + m[0].length,
81
+ });
57
82
  }
58
83
  return members.slice(0, 8);
59
84
  }
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from Rust source code.
7
+ * Signatures carry `:start-end` line anchors (Surgical Context); the comment
8
+ * strip below is newline-preserving so anchor lines match the original file.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
@@ -11,35 +15,55 @@ function extract(src) {
11
15
 
12
16
  const stripped = src
13
17
  .replace(/\/\/.*$/gm, '')
14
- .replace(/\/\*[\s\S]*?\*\//g, '');
18
+ .replace(/\/\*[\s\S]*?\*\//g, (s) => s.replace(/[^\n]/g, ' '));
19
+
20
+ // Anchor range for a declaration at declIdx whose header ends at afterIdx:
21
+ // if a `{` body follows, range to its closing brace; else single-line.
22
+ const rangeFor = (declIdx, afterIdx) => {
23
+ let k = afterIdx;
24
+ while (k < stripped.length && /[ \t]/.test(stripped[k])) k++;
25
+ if (stripped[k] === '{') {
26
+ const end = k + 1 + extractBlock(stripped, k + 1).length;
27
+ return [lineAt(stripped, declIdx), lineAt(stripped, end)];
28
+ }
29
+ const line = lineAt(stripped, declIdx);
30
+ return [line, line];
31
+ };
15
32
 
16
33
  // Structs
17
34
  for (const m of stripped.matchAll(/^pub\s+struct\s+(\w+)(?:<[^{]*>)?/gm)) {
18
- sigs.push(`pub struct ${m[1]}`);
35
+ const [s, e] = rangeFor(m.index, m.index + m[0].length);
36
+ sigs.push(withAnchor(`pub struct ${m[1]}`, s, e));
19
37
  }
20
38
 
21
39
  // Enums
22
40
  for (const m of stripped.matchAll(/^pub\s+enum\s+(\w+)(?:<[^{]*>)?/gm)) {
23
- sigs.push(`pub enum ${m[1]}`);
41
+ const [s, e] = rangeFor(m.index, m.index + m[0].length);
42
+ sigs.push(withAnchor(`pub enum ${m[1]}`, s, e));
24
43
  }
25
44
 
26
45
  // Traits
27
46
  for (const m of stripped.matchAll(/^pub\s+trait\s+(\w+)(?:<[^{]*>)?/gm)) {
28
- sigs.push(`pub trait ${m[1]}`);
47
+ const [s, e] = rangeFor(m.index, m.index + m[0].length);
48
+ sigs.push(withAnchor(`pub trait ${m[1]}`, s, e));
29
49
  }
30
50
 
31
51
  // impl blocks
32
52
  for (const m of stripped.matchAll(/^impl(?:<[^>]*>)?\s+(?:[\w:]+\s+for\s+)?(\w+)(?:<[^{]*>)?\s*\{/gm)) {
33
- sigs.push(`impl ${m[1]}`);
34
- const block = extractBlock(stripped, m.index + m[0].length);
35
- for (const fn of extractMethods(block)) sigs.push(` ${fn}`);
53
+ const bodyStart = m.index + m[0].length;
54
+ const block = extractBlock(stripped, bodyStart);
55
+ sigs.push(withAnchor(`impl ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
56
+ for (const fn of extractMethods(block)) {
57
+ sigs.push(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)));
58
+ }
36
59
  }
37
60
 
38
61
  // Top-level pub fns — capture everything after ) up to { or ; for return type
39
62
  for (const m of stripped.matchAll(/^pub(?:\s+async)?\s+fn\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)([^{;]*)/gm)) {
40
63
  const asyncKw = m[0].includes('async') ? 'async ' : '';
41
64
  const retStr = extractReturnType(m[3]);
42
- sigs.push(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`);
65
+ const [s, e] = rangeFor(m.index, m.index + m[0].length);
66
+ sigs.push(withAnchor(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e));
43
67
  }
44
68
 
45
69
  return sigs.slice(0, 25);
@@ -61,7 +85,11 @@ function extractMethods(block) {
61
85
  for (const m of block.matchAll(/^\s+pub(?:\s+async)?\s+fn\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)([^{;]*)/gm)) {
62
86
  const asyncKw = m[0].includes('async') ? 'async ' : '';
63
87
  const retStr = extractReturnType(m[3]);
64
- methods.push(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`);
88
+ methods.push({
89
+ text: `pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`,
90
+ declIdx: m.index + (m[0].length - m[0].trimStart().length),
91
+ endIdx: m.index + m[0].length,
92
+ });
65
93
  }
66
94
  return methods.slice(0, 8);
67
95
  }
@@ -76,7 +104,7 @@ function extractReturnType(afterParen) {
76
104
  const m = afterParen.match(/->\s*([^{;]+)/);
77
105
  if (!m) return '';
78
106
  const rt = m[1].trim().replace(/\s+/g, ' ');
79
- return ` \u2192 ${rt.length > 30 ? rt.slice(0, 27) + '...' : rt}`;
107
+ return ` ${rt.length > 30 ? rt.slice(0, 27) + '...' : rt}`;
80
108
  }
81
109
 
82
110
  module.exports = { extract };
@@ -1,7 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const { lineAt, withAnchor } = require('./line-anchor');
4
+
3
5
  /**
4
6
  * Extract signatures from Scala source code.
7
+ * Signatures carry `:start-end` line anchors (Surgical Context); the comment
8
+ * strip below is newline-preserving so anchor lines match the original file.
5
9
  * @param {string} src - Raw file content
6
10
  * @returns {string[]} Array of signature strings
7
11
  */
@@ -11,7 +15,7 @@ function extract(src) {
11
15
 
12
16
  const stripped = src
13
17
  .replace(/\/\/.*$/gm, '')
14
- .replace(/\/\*[\s\S]*?\*\//g, '');
18
+ .replace(/\/\*[\s\S]*?\*\//g, (s) => s.replace(/[^\n]/g, ' '));
15
19
 
16
20
  // Classes, traits, objects
17
21
  const typeRe = /^(?:case\s+)?(?:class|trait|object)\s+(\w+)(?:\[[\w, ]+\])?(?:[^{]*)\{/gm;
@@ -19,9 +23,12 @@ function extract(src) {
19
23
  const kind = m[0].trimStart().startsWith('case class') ? 'case class' :
20
24
  m[0].trimStart().startsWith('trait') ? 'trait' :
21
25
  m[0].trimStart().startsWith('object') ? 'object' : 'class';
22
- sigs.push(`${kind} ${m[1]}`);
23
- const block = extractBlock(stripped, m.index + m[0].length);
24
- for (const fn of extractMembers(block)) sigs.push(` ${fn}`);
26
+ const bodyStart = m.index + m[0].length;
27
+ const block = extractBlock(stripped, bodyStart);
28
+ sigs.push(withAnchor(`${kind} ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
29
+ for (const fn of extractMembers(block)) {
30
+ sigs.push(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)));
31
+ }
25
32
  }
26
33
 
27
34
  // Top-level defs
@@ -30,7 +37,8 @@ function extract(src) {
30
37
  const params = m[2] ? `(${normalizeParams(m[2])})` : '';
31
38
  const ret = normalizeType(m[3]);
32
39
  const retStr = ret ? ` → ${ret}` : '';
33
- sigs.push(`def ${m[1]}${params}${retStr}`);
40
+ const line = lineAt(stripped, m.index);
41
+ sigs.push(withAnchor(`def ${m[1]}${params}${retStr}`, line, line));
34
42
  }
35
43
 
36
44
  return sigs.slice(0, 25);
@@ -54,7 +62,11 @@ function extractMembers(block) {
54
62
  const params = m[2] ? `(${normalizeParams(m[2])})` : '';
55
63
  const ret = normalizeType(m[3]);
56
64
  const retStr = ret ? ` → ${ret}` : '';
57
- members.push(`def ${m[1]}${params}${retStr}`);
65
+ members.push({
66
+ text: `def ${m[1]}${params}${retStr}`,
67
+ declIdx: m.index + (m[0].length - m[0].trimStart().length),
68
+ endIdx: m.index + m[0].length,
69
+ });
58
70
  }
59
71
  return members.slice(0, 8);
60
72
  }