sigmap 1.5.1 → 2.0.0-beta.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "1.5.1",
3
+ "version": "2.0.0-beta.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
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "scripts": {
12
12
  "test": "node test/run.js",
13
13
  "test:integration": "node test/integration/strategy.test.js && node test/integration/secret-scan.test.js && node test/integration/token-budget.test.js && node test/integration/mcp-server.test.js",
14
+ "test:integration:all": "node test/integration/all.js",
14
15
  "test:all": "node test/run.js && node test/integration/strategy.test.js && node test/integration/secret-scan.test.js",
15
16
  "generate": "node gen-context.js",
16
17
  "watch": "node gen-context.js --watch",
@@ -12,7 +12,13 @@ const DEFAULTS = {
12
12
  outputs: ['copilot'],
13
13
 
14
14
  // Directories to scan (relative to project root)
15
- srcDirs: ['src', 'app', 'lib', 'packages', 'services', 'api'],
15
+ srcDirs: [
16
+ 'src', 'app', 'lib', 'packages', 'services', 'api',
17
+ // common monorepo / multi-project top-level names
18
+ 'server', 'client', 'web', 'frontend', 'backend',
19
+ 'desktop', 'mobile', 'shared', 'common', 'core',
20
+ 'workers', 'functions', 'lambda', 'cmd',
21
+ ],
16
22
 
17
23
  // Directory/file names to exclude entirely
18
24
  exclude: [
@@ -39,6 +45,15 @@ const DEFAULTS = {
39
45
  // Sort recently git-committed files higher in output
40
46
  diffPriority: true,
41
47
 
48
+ // Context strategy controls how the output is split and injected.
49
+ // 'full' -> single context file (default)
50
+ // 'per-module' -> one context-<module>.md per top-level srcDir + thin overview
51
+ // 'hot-cold' -> recent files in primary output, older files in context-cold.md
52
+ strategy: 'full',
53
+
54
+ // For hot-cold strategy: how many recent git commits count as "hot"
55
+ hotCommits: 10,
56
+
42
57
  // Debounce delay (ms) between file-system events and regeneration in watch mode
43
58
  watchDebounce: 300,
44
59
 
@@ -56,6 +71,33 @@ const DEFAULTS = {
56
71
  mcp: {
57
72
  autoRegister: true,
58
73
  },
74
+
75
+ // Enrich signatures with return types, type hints, and schema field collapse
76
+ enrichSignatures: true,
77
+
78
+ // Include a compact import dependency map at top of output
79
+ depMap: true,
80
+
81
+ // Collapse Pydantic BaseModel / @dataclass fields to a single line
82
+ schemaFields: true,
83
+
84
+ // Include TODO/FIXME/HACK/XXX comments as compact section
85
+ todos: true,
86
+
87
+ // Include compact recent git changes section
88
+ changes: true,
89
+
90
+ // Number of commits used for changes section
91
+ changesCommits: 5,
92
+
93
+ // Add test coverage markers to extracted function signatures (opt-in)
94
+ testCoverage: false,
95
+
96
+ // Directories scanned for tests when testCoverage is enabled
97
+ testDirs: ['tests', 'test', '__tests__', 'spec'],
98
+
99
+ // Add reverse dependency usage hints on file headings (opt-in)
100
+ impactRadius: false,
59
101
  };
60
102
 
61
103
  module.exports = { DEFAULTS };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function walkFiles(dir) {
7
+ let out = [];
8
+ let entries;
9
+ try {
10
+ entries = fs.readdirSync(dir, { withFileTypes: true });
11
+ } catch (_) {
12
+ return out;
13
+ }
14
+ for (const entry of entries) {
15
+ const full = path.join(dir, entry.name);
16
+ if (entry.isDirectory()) out = out.concat(walkFiles(full));
17
+ else if (entry.isFile()) out.push(full);
18
+ }
19
+ return out;
20
+ }
21
+
22
+ function buildTestIndex(cwd, testDirs) {
23
+ const dirs = Array.isArray(testDirs) && testDirs.length ? testDirs : ['tests', 'test', '__tests__', 'spec'];
24
+ const names = new Set();
25
+
26
+ for (const dir of dirs) {
27
+ const abs = path.join(cwd, dir);
28
+ if (!fs.existsSync(abs)) continue;
29
+ for (const file of walkFiles(abs)) {
30
+ let src = '';
31
+ try {
32
+ src = fs.readFileSync(file, 'utf8');
33
+ } catch (_) {
34
+ continue;
35
+ }
36
+
37
+ for (const m of src.matchAll(/\b(?:test_|it\(|test\(|describe\()\s*['"`]?([\w_]+)/g)) {
38
+ if (m[1]) names.add(m[1].toLowerCase());
39
+ }
40
+
41
+ for (const m of src.matchAll(/\b([a-zA-Z_][a-zA-Z0-9_]*)\b/g)) {
42
+ if (m[1]) names.add(m[1].toLowerCase());
43
+ }
44
+ }
45
+ }
46
+
47
+ return names;
48
+ }
49
+
50
+ function isTested(funcName, testIndex) {
51
+ if (!funcName || !testIndex || testIndex.size === 0) return false;
52
+ const lower = funcName.toLowerCase();
53
+ if (testIndex.has(lower) || testIndex.has(`test_${lower}`)) return true;
54
+ for (const token of testIndex) {
55
+ if (token.includes(lower)) return true;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ module.exports = { buildTestIndex, isTested };
@@ -23,9 +23,11 @@ function extract(src) {
23
23
  }
24
24
 
25
25
  // Top-level function declarations/definitions (not inside a class)
26
- for (const m of stripped.matchAll(/^(?!class|struct|if|for|while|switch)[\w:*&<> ]+\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?\{/gm)) {
27
- if (m[1].startsWith('_')) continue;
28
- sigs.push(`${m[1]}(${normalizeParams(m[2])})`);
26
+ for (const m of stripped.matchAll(/^(?!class|struct|if|for|while|switch)([\w:*&<> ]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?\{/gm)) {
27
+ if (m[2].startsWith('_')) continue;
28
+ const ret = normalizeType(m[1]);
29
+ const retStr = ret ? ` → ${ret}` : '';
30
+ sigs.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
29
31
  }
30
32
 
31
33
  return sigs.slice(0, 25);
@@ -44,10 +46,12 @@ function extractBlock(src, startIndex) {
44
46
 
45
47
  function extractMembers(block) {
46
48
  const members = [];
47
- const methodRe = /^\s+(?:virtual\s+|static\s+|inline\s+)?(?!private:|protected:|public:)[\w:*&<> ]+\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:=\s*0\s*)?;/gm;
49
+ const methodRe = /^\s+(?:virtual\s+|static\s+|inline\s+)?(?!private:|protected:|public:)([\w:*&<> ]+?)\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:=\s*0\s*)?;/gm;
48
50
  for (const m of block.matchAll(methodRe)) {
49
- if (m[1].startsWith('_')) continue;
50
- members.push(`${m[1]}(${normalizeParams(m[2])})`);
51
+ if (m[2].startsWith('_')) continue;
52
+ const ret = normalizeType(m[1]);
53
+ const retStr = ret ? ` → ${ret}` : '';
54
+ members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
51
55
  }
52
56
  return members.slice(0, 8);
53
57
  }
@@ -57,4 +61,9 @@ function normalizeParams(params) {
57
61
  return params.trim().replace(/\s+/g, ' ');
58
62
  }
59
63
 
64
+ function normalizeType(type) {
65
+ if (!type) return '';
66
+ return type.trim().replace(/\s+/g, ' ').slice(0, 30);
67
+ }
68
+
60
69
  module.exports = { extract };
@@ -37,12 +37,23 @@ function extractBlock(src, startIndex) {
37
37
 
38
38
  function extractMembers(block) {
39
39
  const members = [];
40
- const methodRe = /^\s+(?:public|internal|protected)\s+(?:static\s+|virtual\s+|override\s+|async\s+)*(?:[\w<>\[\]?]+\s+)+(\w+)\s*\(([^)]*)\)/gm;
40
+ const methodRe = /^\s+(?:public|internal|protected)\s+(?:static\s+|virtual\s+|override\s+|async\s+)*(?:where\s+\w+\s*:\s*[^\n]+\s+)?([\w<>\[\]?., ]+)\s+(\w+)\s*\(([^)]*)\)/gm;
41
41
  for (const m of block.matchAll(methodRe)) {
42
- const sig = m[0].trim().split('{')[0].trim();
43
- members.push(sig);
42
+ const ret = normalizeType(m[1]);
43
+ const retStr = ret ? ` → ${ret}` : '';
44
+ members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
44
45
  }
45
46
  return members.slice(0, 8);
46
47
  }
47
48
 
49
+ function normalizeParams(params) {
50
+ if (!params) return '';
51
+ return params.trim().replace(/\s+/g, ' ');
52
+ }
53
+
54
+ function normalizeType(type) {
55
+ if (!type) return '';
56
+ return type.trim().replace(/\s+/g, ' ').slice(0, 30);
57
+ }
58
+
48
59
  module.exports = { extract };
@@ -21,10 +21,11 @@ function extract(src) {
21
21
  for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
22
22
  }
23
23
 
24
- // Top-level functions
25
- for (const m of stripped.matchAll(/^(?:Future|void|[\w<>?]+)\s+(\w+)\s*\(([^)]*)\)/gm)) {
26
- if (m[1].startsWith('_')) continue;
27
- sigs.push(`${m[1]}(${normalizeParams(m[2])})`);
24
+ // Top-level functions — capture return type (prefix before name) and show as suffix
25
+ for (const m of stripped.matchAll(/^((?:Future<[\w<>?,\s]*>|[\w<>?]+))\s+(\w+)\s*\(([^)]*)\)/gm)) {
26
+ 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}`);
28
29
  }
29
30
 
30
31
  return sigs.slice(0, 25);
@@ -43,9 +44,10 @@ function extractBlock(src, startIndex) {
43
44
 
44
45
  function extractMembers(block) {
45
46
  const members = [];
46
- for (const m of block.matchAll(/^\s+(?:@override\s+)?(?:Future|void|[\w<>?]+)\s+(\w+)\s*\(([^)]*)\)/gm)) {
47
- if (m[1].startsWith('_')) continue;
48
- members.push(`${m[1]}(${normalizeParams(m[2])})`);
47
+ for (const m of block.matchAll(/^\s+(?:@override\s+)?(?:@\w+\s+)*((?:Future<[\w<>?,\s]*>|[\w<>?]+))\s+(\w+)\s*\(([^)]*)\)/gm)) {
48
+ 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}`);
49
51
  }
50
52
  return members.slice(0, 8);
51
53
  }
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract import dependencies from Python and TypeScript/JavaScript files.
5
+ * Returns compact dependency arrays for the dep-map section of the context output.
6
+ */
7
+
8
+ const PYTHON_STDLIB = new Set([
9
+ 'os', 'sys', 're', 'json', 'time', 'threading', 'logging', 'typing',
10
+ 'dataclasses', 'datetime', 'uuid', 'pathlib', 'collections', 'functools',
11
+ 'itertools', 'math', 'random', 'string', 'struct', 'io', 'copy', 'pprint',
12
+ 'traceback', 'inspect', 'abc', 'enum', 'contextlib', 'weakref', 'gc',
13
+ 'socket', 'ssl', 'http', 'urllib', 'email', 'html', 'xml', 'csv', 'sqlite3',
14
+ 'argparse', 'subprocess', 'shutil', 'tempfile', 'glob', 'fnmatch', 'stat',
15
+ 'hashlib', 'hmac', 'base64', 'binascii', 'codecs', 'unicodedata', 'locale',
16
+ 'decimal', 'fractions', 'numbers', 'cmath', 'heapq', 'bisect', 'array',
17
+ 'queue', 'asyncio', 'concurrent', 'multiprocessing', 'signal', 'mmap',
18
+ 'builtins', 'warnings', 'operator', 'textwrap', 'difflib', 'readline',
19
+ ]);
20
+
21
+ /**
22
+ * Extract project-level import dependencies from Python source.
23
+ * @param {string} src
24
+ * @returns {string[]}
25
+ */
26
+ function extractPythonDeps(src) {
27
+ const deps = new Set();
28
+ for (const m of src.matchAll(/^from\s+([\w.]+)\s+import/gm)) {
29
+ const mod = m[1];
30
+ const root = mod.replace(/^\.+/, '').split('.')[0];
31
+ // Include relative imports and non-stdlib modules
32
+ if (mod.startsWith('.') || (root && !PYTHON_STDLIB.has(root))) {
33
+ deps.add(root || mod);
34
+ }
35
+ }
36
+ for (const m of src.matchAll(/^import\s+([\w.]+)/gm)) {
37
+ const root = m[1].split('.')[0];
38
+ if (root && !PYTHON_STDLIB.has(root)) deps.add(root);
39
+ }
40
+ return [...deps].filter(Boolean).slice(0, 5);
41
+ }
42
+
43
+ /**
44
+ * Extract relative import dependencies from TypeScript/JavaScript source.
45
+ * @param {string} src
46
+ * @returns {string[]}
47
+ */
48
+ function extractTSDeps(src) {
49
+ // Strip single-line comments to avoid matching commented-out imports
50
+ const stripped = src.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
51
+ const deps = new Set();
52
+ for (const m of stripped.matchAll(/from\s+['"](\.[\/\w.-]+)['"]/g)) {
53
+ // Normalise: '../store/authStore' → store/authStore, './utils' → utils
54
+ const clean = m[1]
55
+ .replace(/^\.\.\//, '')
56
+ .replace(/^\.\//, '')
57
+ .replace(/\.\w+$/, '');
58
+ if (clean) deps.add(clean);
59
+ }
60
+ return [...deps].slice(0, 5);
61
+ }
62
+
63
+ /**
64
+ * Build reverse dependency map from forward map.
65
+ * @param {Map<string, string[]>} forwardMap
66
+ * @returns {Map<string, string[]>}
67
+ */
68
+ function buildReverseDepMap(forwardMap) {
69
+ const reverse = new Map();
70
+ if (!forwardMap || typeof forwardMap.entries !== 'function') return reverse;
71
+ for (const [file, deps] of forwardMap.entries()) {
72
+ if (!Array.isArray(deps)) continue;
73
+ for (const dep of deps) {
74
+ if (!reverse.has(dep)) reverse.set(dep, []);
75
+ reverse.get(dep).push(file);
76
+ }
77
+ }
78
+ return reverse;
79
+ }
80
+
81
+ module.exports = { extractPythonDeps, extractTSDeps, buildReverseDepMap };
@@ -25,10 +25,12 @@ function extract(src) {
25
25
  for (const method of extractInterfaceMethods(block)) sigs.push(` ${method}`);
26
26
  }
27
27
 
28
- // Functions and methods
29
- for (const m of stripped.matchAll(/^func\s+(?:\((\w+)\s+[\w*]+\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*[\w*()\[\],\s]+)?\s*\{/gm)) {
28
+ // Functions and methods — capture return type between ) and {
29
+ for (const m of stripped.matchAll(/^func\s+(?:\((\w+)\s+[\w*]+\)\s+)?(\w+)\s*\(([^)]*)\)([^{]*)\{/gm)) {
30
30
  const receiver = m[1] ? `(${m[1]}) ` : '';
31
- sigs.push(`func ${receiver}${m[2]}(${normalizeParams(m[3])})`);
31
+ 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}`);
32
34
  }
33
35
 
34
36
  return sigs.slice(0, 25);
@@ -47,8 +49,10 @@ function extractBlock(src, startIndex) {
47
49
 
48
50
  function extractInterfaceMethods(block) {
49
51
  const methods = [];
50
- for (const m of block.matchAll(/^\s+(\w+)\s*\(([^)]*)\)/gm)) {
51
- methods.push(`${m[1]}(${normalizeParams(m[2])})`);
52
+ for (const m of block.matchAll(/^\s+(\w+)\s*\(([^)]*)\)([^\n]*)/gm)) {
53
+ 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}`);
52
56
  }
53
57
  return methods.slice(0, 8);
54
58
  }
@@ -38,12 +38,23 @@ function extractBlock(src, startIndex) {
38
38
 
39
39
  function extractMembers(block) {
40
40
  const members = [];
41
- const methodRe = /^\s+(?:public|protected)\s+(?:static\s+)?(?:final\s+)?(?:[\w<>\[\]]+\s+)+(\w+)\s*\(([^)]*)\)/gm;
41
+ const methodRe = /^\s+(?:public|protected)\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:<[^>]+>\s+)?([\w<>\[\], ?.]+)\s+(\w+)\s*\(([^)]*)\)/gm;
42
42
  for (const m of block.matchAll(methodRe)) {
43
- const sig = m[0].trim().split('{')[0].trim();
44
- members.push(sig);
43
+ const ret = normalizeType(m[1]);
44
+ const retStr = ret ? ` → ${ret}` : '';
45
+ members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
45
46
  }
46
47
  return members.slice(0, 8);
47
48
  }
48
49
 
50
+ function normalizeParams(params) {
51
+ if (!params) return '';
52
+ return params.trim().replace(/\s+/g, ' ');
53
+ }
54
+
55
+ function normalizeType(type) {
56
+ if (!type) return '';
57
+ return type.trim().replace(/\s+/g, ' ').slice(0, 30);
58
+ }
59
+
49
60
  module.exports = { extract };
@@ -8,6 +8,7 @@
8
8
  function extract(src) {
9
9
  if (!src || typeof src !== 'string') return [];
10
10
  const sigs = [];
11
+ const returnHints = buildReturnHints(src);
11
12
 
12
13
  const stripped = src
13
14
  .replace(/\/\/.*$/gm, '')
@@ -19,19 +20,21 @@ function extract(src) {
19
20
  const prefix = m[1] ? m[1].trim() + ' ' : '';
20
21
  sigs.push(`${prefix}class ${m[2]}`);
21
22
  const block = extractBlock(stripped, m.index + m[0].length);
22
- for (const meth of extractClassMembers(block)) sigs.push(` ${meth}`);
23
+ for (const meth of extractClassMembers(block, returnHints)) sigs.push(` ${meth}`);
23
24
  }
24
25
 
25
26
  // Exported named functions
26
27
  for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
27
28
  const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
28
- sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})`);
29
+ const retStr = formatReturnHint(returnHints.get(m[1]));
30
+ sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
29
31
  }
30
32
 
31
33
  // Exported arrow functions
32
34
  for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/gm)) {
33
35
  const asyncKw = m[0].includes('async') ? 'async ' : '';
34
- sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>`);
36
+ const retStr = formatReturnHint(returnHints.get(m[1]));
37
+ sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
35
38
  }
36
39
 
37
40
  // module.exports = { ... }
@@ -44,7 +47,8 @@ function extract(src) {
44
47
  // Top-level named functions (non-exported)
45
48
  for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
46
49
  const asyncKw = m[0].startsWith('async') ? 'async ' : '';
47
- sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})`);
50
+ const retStr = formatReturnHint(returnHints.get(m[1]));
51
+ sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
48
52
  }
49
53
 
50
54
  return sigs.slice(0, 25);
@@ -62,18 +66,42 @@ function extractBlock(src, startIndex) {
62
66
  return src.slice(startIndex, i - 1);
63
67
  }
64
68
 
65
- function extractClassMembers(block) {
69
+ function extractClassMembers(block, returnHints) {
66
70
  const members = [];
67
71
  for (const m of block.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(([^)]*)\)\s*\{/gm)) {
68
72
  if (/^_/.test(m[1])) continue;
69
73
  if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
70
74
  const isAsync = m[0].includes('async ') ? 'async ' : '';
71
75
  const isStatic = m[0].includes('static ') ? 'static ' : '';
72
- members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})`);
76
+ const retStr = formatReturnHint(returnHints.get(m[1]));
77
+ members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`);
73
78
  }
74
79
  return members.slice(0, 8);
75
80
  }
76
81
 
82
+ function buildReturnHints(src) {
83
+ const hints = new Map();
84
+ for (const m of src.matchAll(/\/\*\*[\s\S]*?@returns?\s+\{([^}]+)\}[\s\S]*?\*\/\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g)) {
85
+ hints.set(m[2], normalizeType(m[1]));
86
+ }
87
+ for (const m of src.matchAll(/\/\*\*[\s\S]*?@returns?\s+\{([^}]+)\}[\s\S]*?\*\/\s*export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/g)) {
88
+ hints.set(m[2], normalizeType(m[1]));
89
+ }
90
+ for (const m of src.matchAll(/\/\*\*[\s\S]*?@returns?\s+\{([^}]+)\}[\s\S]*?\*\/\s*(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(/g)) {
91
+ hints.set(m[2], normalizeType(m[1]));
92
+ }
93
+ return hints;
94
+ }
95
+
96
+ function normalizeType(type) {
97
+ if (!type) return '';
98
+ return type.trim().replace(/\s+/g, ' ').slice(0, 25);
99
+ }
100
+
101
+ function formatReturnHint(type) {
102
+ return type ? ` → ${type}` : '';
103
+ }
104
+
77
105
  function normalizeParams(params) {
78
106
  if (!params) return '';
79
107
  return params.trim().replace(/\s+/g, ' ');
@@ -20,10 +20,12 @@ function extract(src) {
20
20
  for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
21
21
  }
22
22
 
23
- // Top-level functions
24
- for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)/gm)) {
23
+ // Top-level functions — capture `: RetType` after params
24
+ for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{=]+))?/gm)) {
25
25
  const suspend = m[0].includes('suspend') ? 'suspend ' : '';
26
- sigs.push(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})`);
26
+ 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}`);
27
29
  }
28
30
 
29
31
  return sigs.slice(0, 25);
@@ -42,10 +44,12 @@ function extractBlock(src, startIndex) {
42
44
 
43
45
  function extractMembers(block) {
44
46
  const members = [];
45
- for (const m of block.matchAll(/^\s+(?:public\s+|internal\s+|override\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)/gm)) {
47
+ for (const m of block.matchAll(/^\s+(?:public\s+|internal\s+|override\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\n{=]+))?/gm)) {
46
48
  if (m[1].startsWith('_')) continue;
47
49
  const suspend = m[0].includes('suspend') ? 'suspend ' : '';
48
- members.push(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})`);
50
+ 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}`);
49
53
  }
50
54
  return members.slice(0, 8);
51
55
  }
@@ -25,8 +25,10 @@ function extract(src) {
25
25
  }
26
26
 
27
27
  // Top-level functions
28
- for (const m of stripped.matchAll(/^function\s+(\w+)\s*\(([^)]*)\)/gm)) {
29
- sigs.push(`function ${m[1]}(${normalizeParams(m[2])})`);
28
+ for (const m of stripped.matchAll(/^function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*([^\n{]+))?/gm)) {
29
+ const ret = normalizeType(m[3]);
30
+ const retStr = ret ? ` → ${ret}` : '';
31
+ sigs.push(`function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
30
32
  }
31
33
 
32
34
  return sigs.slice(0, 25);
@@ -45,11 +47,13 @@ function extractBlock(src, startIndex) {
45
47
 
46
48
  function extractMembers(block) {
47
49
  const members = [];
48
- const methodRe = /^\s+(?:public|protected)\s+(?:static\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm;
50
+ const methodRe = /^\s+(?:public|protected)\s+(?:static\s+)?function\s+(\w+)\s*\(([^)]*)\)\s*(?::\s*([^\n{]+))?/gm;
49
51
  for (const m of block.matchAll(methodRe)) {
50
52
  if (m[1].startsWith('_')) continue;
51
53
  const isStatic = m[0].includes('static ') ? 'static ' : '';
52
- members.push(`${isStatic}function ${m[1]}(${normalizeParams(m[2])})`);
54
+ const ret = normalizeType(m[3]);
55
+ const retStr = ret ? ` → ${ret}` : '';
56
+ members.push(`${isStatic}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
53
57
  }
54
58
  return members.slice(0, 8);
55
59
  }
@@ -59,4 +63,9 @@ function normalizeParams(params) {
59
63
  return params.trim().replace(/\s+/g, ' ');
60
64
  }
61
65
 
66
+ function normalizeType(type) {
67
+ if (!type) return '';
68
+ return type.trim().replace(/[;\s]+$/g, '').replace(/\s+/g, ' ').slice(0, 25);
69
+ }
70
+
62
71
  module.exports = { extract };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Compare signature arrays and produce compact diff markers.
5
+ * @param {string[]} baseSigs
6
+ * @param {string[]} currentSigs
7
+ * @returns {{added:string[], removed:string[], modified:string[]}}
8
+ */
9
+ function diffSignatures(baseSigs, currentSigs) {
10
+ const base = new Set(baseSigs || []);
11
+ const curr = new Set(currentSigs || []);
12
+
13
+ const added = [...curr].filter((s) => !base.has(s));
14
+ const removed = [...base].filter((s) => !curr.has(s));
15
+
16
+ const byName = (arr) => {
17
+ const m = new Map();
18
+ for (const s of arr) {
19
+ const n = extractName(s);
20
+ if (!n) continue;
21
+ if (!m.has(n)) m.set(n, []);
22
+ m.get(n).push(s);
23
+ }
24
+ return m;
25
+ };
26
+
27
+ const aBy = byName(added);
28
+ const rBy = byName(removed);
29
+ const modified = [];
30
+
31
+ for (const [name] of aBy) {
32
+ if (rBy.has(name)) modified.push(name);
33
+ }
34
+
35
+ return { added, removed, modified };
36
+ }
37
+
38
+ function extractName(sig) {
39
+ if (!sig) return '';
40
+ const t = sig.trim();
41
+ const m = t.match(/(?:def|function|func|class|interface|trait|struct|enum|record)?\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(|$)/);
42
+ return m ? m[1] : '';
43
+ }
44
+
45
+ module.exports = { diffSignatures, extractName };