sigmap 8.32.1 → 8.34.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/AGENTS.md +317 -293
- package/CHANGELOG.md +25 -0
- package/README.md +5 -5
- package/gen-context.js +414 -69
- package/llms-full.txt +5 -5
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/index.js +1 -0
- package/packages/core/package.json +1 -1
- package/src/config/loader.js +1 -1
- package/src/config/tune.js +1 -1
- package/src/discovery/language-detector.js +1 -0
- package/src/discovery/source-root-registry.js +10 -1
- package/src/discovery/source-root-scorer.js +1 -1
- package/src/eval/analyzer.js +5 -39
- package/src/extractors/deps.js +23 -1
- package/src/extractors/dispatch.js +18 -1
- package/src/extractors/lua.js +152 -0
- package/src/graph/call-graph.js +81 -1
- package/src/map/route-table.js +46 -9
- package/src/mcp/server.js +1 -1
package/llms-full.txt
CHANGED
|
@@ -11,13 +11,13 @@ 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.
|
|
14
|
+
# Version: 8.34.0 | Benchmark: sigmap-v8.34-main (2026-09-13)
|
|
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.
|
|
20
|
+
## Core metrics (benchmark: sigmap-v8.34-main, 2026-09-13)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
@@ -25,7 +25,7 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
25
25
|
| Token reduction | — | 96.8% average |
|
|
26
26
|
| Task-success proxy (modeled) | — | 62.9% |
|
|
27
27
|
| Prompts per task | 2.84 | 1.58 (44.4% fewer) |
|
|
28
|
-
| Supported languages | — |
|
|
28
|
+
| Supported languages | — | 33 |
|
|
29
29
|
| MCP tools | — | 21 |
|
|
30
30
|
| npm runtime dependencies | — | 0 |
|
|
31
31
|
|
|
@@ -370,9 +370,9 @@ impact = {"depth":3,"includeSigs":true}
|
|
|
370
370
|
|
|
371
371
|
---
|
|
372
372
|
|
|
373
|
-
## Supported languages (
|
|
373
|
+
## Supported languages (33 extractors)
|
|
374
374
|
|
|
375
|
-
cpp, csharp, css, dart, dockerfile, gdscript, go, graphql, html, java, javascript, kotlin, markdown, php, properties, protobuf, python, r, ruby, rust, scala, shell, sql, svelte, swift, terraform, toml, typescript, typescript_react, vue_sfc, xml, yaml
|
|
375
|
+
cpp, csharp, css, dart, dockerfile, gdscript, go, graphql, html, java, javascript, kotlin, lua, markdown, php, properties, protobuf, python, r, ruby, rust, scala, shell, sql, svelte, swift, terraform, toml, typescript, typescript_react, vue_sfc, xml, yaml
|
|
376
376
|
|
|
377
377
|
---
|
|
378
378
|
|
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.
|
|
14
|
+
# Version: 8.34.0 | Benchmark: sigmap-v8.34-main (2026-09-13)
|
|
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,13 +23,13 @@ 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.
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.34-main, 2026-09-13)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 78.9% vs 44.0% single-shot grep baseline (1.73× lift)
|
|
29
29
|
- Token reduction: 96.8% average across benchmark repos
|
|
30
30
|
- Task-success proxy: 62.9% (modeled from retrieval tiers, not measured LLM sessions)
|
|
31
31
|
- Prompts per task: 1.58 vs 2.84 baseline (44.4% fewer, modeled)
|
|
32
|
-
- Languages:
|
|
32
|
+
- Languages: 33 supported · MCP tools: 21
|
|
33
33
|
- Dependencies: zero npm runtime dependencies · fully offline
|
|
34
34
|
|
|
35
35
|
## Quick start
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.34.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": {
|
package/packages/core/index.js
CHANGED
package/src/config/loader.js
CHANGED
|
@@ -81,7 +81,7 @@ const SUPPORTED_CODE_EXTS = new Set([
|
|
|
81
81
|
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
82
82
|
'.py', '.pyw', '.java', '.kt', '.kts', '.go', '.rs', '.cs',
|
|
83
83
|
'.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.rake', '.php',
|
|
84
|
-
'.swift', '.dart', '.scala', '.sc', '.vue', '.svelte',
|
|
84
|
+
'.swift', '.dart', '.scala', '.sc', '.lua', '.vue', '.svelte',
|
|
85
85
|
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
|
86
86
|
'.yml', '.yaml', '.sh', '.bash', '.zsh', '.fish',
|
|
87
87
|
'.sql', '.graphql', '.gql', '.tf', '.tfvars', '.proto',
|
package/src/config/tune.js
CHANGED
|
@@ -38,7 +38,7 @@ const TOKENS_PER_FILE = 25;
|
|
|
38
38
|
const SOURCE_EXTS = new Set([
|
|
39
39
|
'.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.py', '.rb', '.go', '.rs',
|
|
40
40
|
'.java', '.kt', '.cs', '.cpp', '.c', '.h', '.hpp', '.swift', '.dart',
|
|
41
|
-
'.scala', '.php', '.gd', '.r', '.R',
|
|
41
|
+
'.scala', '.php', '.lua', '.gd', '.r', '.R',
|
|
42
42
|
]);
|
|
43
43
|
|
|
44
44
|
/** Raw user config file content, or null when absent/unparsable. */
|
|
@@ -19,6 +19,7 @@ const EXT_TO_LANG = {
|
|
|
19
19
|
'.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.cpp': 'cpp',
|
|
20
20
|
'.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.swift': 'swift',
|
|
21
21
|
'.dart': 'dart', '.scala': 'scala', '.php': 'php',
|
|
22
|
+
'.lua': 'lua',
|
|
22
23
|
'.gd': 'gdscript',
|
|
23
24
|
'.r': 'r', '.R': 'r',
|
|
24
25
|
};
|
|
@@ -156,12 +156,21 @@ const REGISTRY = {
|
|
|
156
156
|
akka: { detectionFiles: [], detectionDeps: ['akka'], srcDirs: ['src/main/scala','src'] },
|
|
157
157
|
play: { detectionFiles: [], detectionDeps: ['play'], srcDirs: ['app','conf'] },
|
|
158
158
|
spark: { detectionFiles: [], detectionDeps: ['spark'],srcDirs: ['src/main/scala'] },
|
|
159
|
-
zio: { detectionFiles: [], detectionDeps: ['zio'],
|
|
159
|
+
zio: { detectionFiles: [], detectionDeps: ['zio'], srcDirs: ['src/main/scala'] },
|
|
160
160
|
},
|
|
161
161
|
srcDirs: ['src/main/scala','src'],
|
|
162
162
|
penalties: ['target'],
|
|
163
163
|
},
|
|
164
164
|
|
|
165
|
+
lua: {
|
|
166
|
+
manifestFiles: ['.luarc.json', 'selene.toml', 'stylua.toml'],
|
|
167
|
+
frameworks: {
|
|
168
|
+
luarocks: { detectionFiles: ['*.rockspec'], srcDirs: ['src','lua','lib'] },
|
|
169
|
+
},
|
|
170
|
+
srcDirs: ['src','lua','lib'],
|
|
171
|
+
penalties: ['.luarocks','luarocks_modules'],
|
|
172
|
+
},
|
|
173
|
+
|
|
165
174
|
r: {
|
|
166
175
|
manifestFiles: ['DESCRIPTION','renv.lock'],
|
|
167
176
|
frameworks: {
|
|
@@ -7,7 +7,7 @@ const { git } = require('../util/git');
|
|
|
7
7
|
const CODE_EXTS = new Set([
|
|
8
8
|
'.js','.mjs','.cjs','.ts','.tsx','.jsx',
|
|
9
9
|
'.py','.rb','.go','.rs','.java','.kt',
|
|
10
|
-
'.cs','.cpp','.c','.h','.swift','.dart','.scala','.php',
|
|
10
|
+
'.cs','.cpp','.c','.h','.swift','.dart','.scala','.php','.lua',
|
|
11
11
|
]);
|
|
12
12
|
|
|
13
13
|
const AUTO_SKIP = new Set([
|
package/src/eval/analyzer.js
CHANGED
|
@@ -14,47 +14,13 @@ const fs = require('fs');
|
|
|
14
14
|
const path = require('path');
|
|
15
15
|
|
|
16
16
|
// Extension → extractor name (mirrors EXT_MAP in gen-context.js)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
'.java': 'java',
|
|
22
|
-
'.kt': 'kotlin', '.kts': 'kotlin',
|
|
23
|
-
'.go': 'go',
|
|
24
|
-
'.rs': 'rust',
|
|
25
|
-
'.cs': 'csharp',
|
|
26
|
-
'.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
|
|
27
|
-
'.rb': 'ruby', '.rake': 'ruby',
|
|
28
|
-
'.php': 'php',
|
|
29
|
-
'.swift': 'swift',
|
|
30
|
-
'.dart': 'dart',
|
|
31
|
-
'.scala': 'scala', '.sc': 'scala',
|
|
32
|
-
'.gd': 'gdscript',
|
|
33
|
-
'.r': 'r', '.R': 'r',
|
|
34
|
-
'.svelte': 'svelte',
|
|
35
|
-
'.html': 'html', '.htm': 'html',
|
|
36
|
-
'.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
|
|
37
|
-
'.yml': 'yaml', '.yaml': 'yaml',
|
|
38
|
-
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
|
|
39
|
-
'.toml': 'toml',
|
|
40
|
-
'.properties': 'properties',
|
|
41
|
-
'.xml': 'xml',
|
|
42
|
-
'.md': 'markdown',
|
|
43
|
-
// Phase C specialized extractors
|
|
44
|
-
'.tsx': 'typescript_react',
|
|
45
|
-
'.vue': 'vue_sfc',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
function isDockerfile(name) {
|
|
49
|
-
return name === 'Dockerfile' || name.startsWith('Dockerfile.');
|
|
50
|
-
}
|
|
17
|
+
// Extractor resolution goes through the dispatcher — the single source of
|
|
18
|
+
// truth (#591). This file previously kept its own copy, which had drifted to
|
|
19
|
+
// a dead duplicate `.vue` key.
|
|
20
|
+
const { langFor } = require('../extractors/dispatch');
|
|
51
21
|
|
|
52
22
|
function getExtractorName(filePath) {
|
|
53
|
-
|
|
54
|
-
const ext = path.extname(base).toLowerCase();
|
|
55
|
-
if (EXT_MAP[ext]) return EXT_MAP[ext];
|
|
56
|
-
if (isDockerfile(base)) return 'dockerfile';
|
|
57
|
-
return null;
|
|
23
|
+
return langFor(filePath);
|
|
58
24
|
}
|
|
59
25
|
|
|
60
26
|
/** Rough token estimate: chars / 4 */
|
package/src/extractors/deps.js
CHANGED
|
@@ -89,6 +89,28 @@ function extractRDeps(src) {
|
|
|
89
89
|
return [...deps].slice(0, 5);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Extract Lua require() module dependencies.
|
|
94
|
+
* Captures `require "mod"` and `require("mod")`, returning compact module
|
|
95
|
+
* names as they appear in source.
|
|
96
|
+
* @param {string} src
|
|
97
|
+
* @returns {string[]}
|
|
98
|
+
*/
|
|
99
|
+
function extractLuaDeps(src) {
|
|
100
|
+
const deps = new Set();
|
|
101
|
+
const stripped = stripLuaComments(src || '');
|
|
102
|
+
for (const m of stripped.matchAll(/\brequire\s*(?:\(\s*)?["']([A-Za-z0-9_.\/-]+)["']\s*\)?/g)) {
|
|
103
|
+
if (m[1]) deps.add(m[1]);
|
|
104
|
+
}
|
|
105
|
+
return [...deps].slice(0, 5);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stripLuaComments(src) {
|
|
109
|
+
return String(src || '')
|
|
110
|
+
.replace(/--\[\[[\s\S]*?\]\]/g, '')
|
|
111
|
+
.replace(/--.*$/gm, '');
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
/**
|
|
93
115
|
* Build reverse dependency map from forward map.
|
|
94
116
|
* @param {Map<string, string[]>} forwardMap
|
|
@@ -107,4 +129,4 @@ function buildReverseDepMap(forwardMap) {
|
|
|
107
129
|
return reverse;
|
|
108
130
|
}
|
|
109
131
|
|
|
110
|
-
module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, buildReverseDepMap };
|
|
132
|
+
module.exports = { extractPythonDeps, extractTSDeps, extractRDeps, extractLuaDeps, buildReverseDepMap };
|
|
@@ -28,6 +28,7 @@ const EXTRACTORS = {
|
|
|
28
28
|
swift: require('./swift'),
|
|
29
29
|
dart: require('./dart'),
|
|
30
30
|
scala: require('./scala'),
|
|
31
|
+
lua: require('./lua'),
|
|
31
32
|
gdscript: require('./gdscript'),
|
|
32
33
|
r: require('./r'),
|
|
33
34
|
vue_sfc: require('./vue_sfc'),
|
|
@@ -48,6 +49,21 @@ const EXTRACTORS = {
|
|
|
48
49
|
generic: require('./generic'),
|
|
49
50
|
};
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Extension → extractor module name. **The single source of truth for
|
|
54
|
+
* extractor resolution** (#591).
|
|
55
|
+
*
|
|
56
|
+
* Anything that decides *which extractor module to load* must go through
|
|
57
|
+
* `langFor` rather than declaring its own copy. Three copies existed and two
|
|
58
|
+
* had drifted: `src/eval/analyzer.js` carried a dead duplicate `.vue` key, and
|
|
59
|
+
* the `--diagnose-extractors` map pointed at `vue.js` after that module was
|
|
60
|
+
* deleted — which is how an unreachable extractor survived unnoticed (#582).
|
|
61
|
+
*
|
|
62
|
+
* Not every extension map in the codebase belongs here. `language-detector.js`
|
|
63
|
+
* maps `.tsx → typescript` for language *statistics*, and `dashboard.js` keeps
|
|
64
|
+
* short display *labels*. Both are correct for their purpose and deliberately
|
|
65
|
+
* differ from resolution — folding them in would miscount languages.
|
|
66
|
+
*/
|
|
51
67
|
const EXT_MAP = {
|
|
52
68
|
'.ts': 'typescript', '.tsx': 'typescript_react',
|
|
53
69
|
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
@@ -63,6 +79,7 @@ const EXT_MAP = {
|
|
|
63
79
|
'.swift': 'swift',
|
|
64
80
|
'.dart': 'dart',
|
|
65
81
|
'.scala': 'scala', '.sc': 'scala',
|
|
82
|
+
'.lua': 'lua',
|
|
66
83
|
'.gd': 'gdscript',
|
|
67
84
|
'.r': 'r', '.R': 'r',
|
|
68
85
|
'.vue': 'vue_sfc',
|
|
@@ -108,4 +125,4 @@ function extractFile(filePathOrName, src) {
|
|
|
108
125
|
}
|
|
109
126
|
}
|
|
110
127
|
|
|
111
|
-
module.exports = { extractFile, langFor };
|
|
128
|
+
module.exports = { extractFile, langFor, EXT_MAP };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { capWithNotice } = require('../util/truncate');
|
|
4
|
+
|
|
5
|
+
// Ceiling discloses what it drops rather than truncating silently (#583).
|
|
6
|
+
// Collection runs to completion so the marker reports the true overflow —
|
|
7
|
+
// stopping early made r.js report "+1 more" where 50 were hidden (#584).
|
|
8
|
+
const PER_FILE_LIMIT = 30;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Extract signatures from Lua source code.
|
|
12
|
+
*
|
|
13
|
+
* Recognised constructs:
|
|
14
|
+
* - Global functions: `function name(args)`
|
|
15
|
+
* - Module-table functions: `function M.name(args)` / `function M:name(args)`
|
|
16
|
+
* - Local functions: `local function name(args)`
|
|
17
|
+
* - Assigned functions: `name = function(args)` / `M.name = function(args)`
|
|
18
|
+
* - Module imports: `local mod = require("mod")` as compact hints
|
|
19
|
+
* - LDoc-style doc comments (`---`) as first-sentence hints
|
|
20
|
+
*
|
|
21
|
+
* The extractor is regex-only and zero-dependency, matching SigMap's Tier-3
|
|
22
|
+
* language extractor style.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} src - Raw file content
|
|
25
|
+
* @returns {string[]} Array of signature strings
|
|
26
|
+
*/
|
|
27
|
+
function extract(src) {
|
|
28
|
+
if (!src || typeof src !== 'string') return [];
|
|
29
|
+
const sigs = [];
|
|
30
|
+
const hints = collectDocHints(src);
|
|
31
|
+
const stripped = stripLuaComments(src);
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
|
|
34
|
+
// local foo = require('bar.baz') — useful module-surface hint, capped low.
|
|
35
|
+
for (const m of stripped.matchAll(/^\s*(?:local\s+)?([A-Za-z_]\w*)\s*=\s*require\s*\(\s*['"]([A-Za-z0-9_.\/-]+)['"]\s*\)/gm)) {
|
|
36
|
+
pushUnique(sigs, seen, `require ${m[2]} as ${m[1]}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// local function name(args)
|
|
40
|
+
for (const m of stripped.matchAll(/^\s*local\s+function\s+([A-Za-z_]\w*)\s*\(([^)]*)\)/gm)) {
|
|
41
|
+
if (m[1].startsWith('_')) continue;
|
|
42
|
+
pushUnique(sigs, seen, `local function ${m[1]}(${normalizeParams(m[2])})${applyHint(hints, m[1])}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// function name(args), function M.name(args), function M:name(args)
|
|
46
|
+
for (const m of stripped.matchAll(/^\s*function\s+([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*\(([^)]*)\)/gm)) {
|
|
47
|
+
const name = m[1];
|
|
48
|
+
if (name.startsWith('_')) continue;
|
|
49
|
+
pushUnique(sigs, seen, `function ${name}(${normalizeParams(m[2])})${applyHint(hints, name)}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// name = function(args), M.name = function(args), M:name = function(args)
|
|
53
|
+
for (const m of stripped.matchAll(/^\s*(?:local\s+)?([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*=\s*function\s*\(([^)]*)\)/gm)) {
|
|
54
|
+
const name = m[1];
|
|
55
|
+
if (name.startsWith('_')) continue;
|
|
56
|
+
pushUnique(sigs, seen, `${name} = function(${normalizeParams(m[2])})${applyHint(hints, name)}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function pushUnique(out, seen, sig) {
|
|
63
|
+
if (!sig || seen.has(sig)) return;
|
|
64
|
+
seen.add(sig);
|
|
65
|
+
out.push(sig);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeParams(params) {
|
|
69
|
+
return String(params || '')
|
|
70
|
+
.replace(/--.*$/gm, '')
|
|
71
|
+
.split(',')
|
|
72
|
+
.map((p) => p.trim().replace(/\s+/g, ' '))
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
.join(', ');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function applyHint(hints, name) {
|
|
78
|
+
const h = hints.get(name);
|
|
79
|
+
return h ? ` # ${h}` : '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Attach each contiguous `---` doc block to the next function-like declaration
|
|
84
|
+
* by its extracted symbol name.
|
|
85
|
+
*/
|
|
86
|
+
function collectDocHints(src) {
|
|
87
|
+
const hints = new Map();
|
|
88
|
+
const lines = src.split('\n');
|
|
89
|
+
let block = [];
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const doc = line.match(/^\s*---\s?(.*)$/);
|
|
92
|
+
if (doc) {
|
|
93
|
+
block.push(doc[1]);
|
|
94
|
+
} else if (block.length > 0) {
|
|
95
|
+
const decl = line.match(/^\s*local\s+function\s+([A-Za-z_]\w*)\s*\(/)
|
|
96
|
+
|| line.match(/^\s*function\s+([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*\(/)
|
|
97
|
+
|| line.match(/^\s*(?:local\s+)?([A-Za-z_]\w*(?:(?:\.|:)[A-Za-z_]\w*)*)\s*=\s*function\s*\(/);
|
|
98
|
+
if (decl) {
|
|
99
|
+
const hint = firstDocSentence(block);
|
|
100
|
+
if (hint) hints.set(decl[1], hint);
|
|
101
|
+
}
|
|
102
|
+
block = [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return hints;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function firstDocSentence(block) {
|
|
109
|
+
for (const raw of block) {
|
|
110
|
+
const line = String(raw || '').trim();
|
|
111
|
+
if (!line || line.startsWith('@')) continue;
|
|
112
|
+
return line.replace(/\s+/g, ' ').slice(0, 60).replace(/[.,;:!?]+$/, '').trim();
|
|
113
|
+
}
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Strip Lua line and long comments while preserving strings enough for regex scans. */
|
|
118
|
+
function stripLuaComments(src) {
|
|
119
|
+
const out = src.split('');
|
|
120
|
+
const blank = (a, b) => { for (let i = a; i < b; i++) if (out[i] !== '\n') out[i] = ' '; };
|
|
121
|
+
let i = 0;
|
|
122
|
+
while (i < src.length) {
|
|
123
|
+
const ch = src[i];
|
|
124
|
+
if (ch === '"' || ch === "'") {
|
|
125
|
+
const quote = ch;
|
|
126
|
+
i++;
|
|
127
|
+
while (i < src.length) {
|
|
128
|
+
if (src[i] === '\\') { i += 2; continue; }
|
|
129
|
+
if (src[i] === quote) { i++; break; }
|
|
130
|
+
if (src[i] === '\n') break;
|
|
131
|
+
i++;
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (src.startsWith('--[[', i)) {
|
|
136
|
+
const end = src.indexOf(']]', i + 4);
|
|
137
|
+
blank(i, end === -1 ? src.length : end + 2);
|
|
138
|
+
i = end === -1 ? src.length : end + 2;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (src.startsWith('--', i)) {
|
|
142
|
+
const end = src.indexOf('\n', i + 2);
|
|
143
|
+
blank(i, end === -1 ? src.length : end);
|
|
144
|
+
i = end === -1 ? src.length : end;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
return out.join('');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { extract };
|
package/src/graph/call-graph.js
CHANGED
|
@@ -26,6 +26,8 @@ const PY_EXTS = new Set(['.py', '.pyw']);
|
|
|
26
26
|
const JAVA_EXTS = new Set(['.java']);
|
|
27
27
|
const GO_EXTS = new Set(['.go']);
|
|
28
28
|
const RS_EXTS = new Set(['.rs']);
|
|
29
|
+
const KT_EXTS = new Set(['.kt', '.kts']);
|
|
30
|
+
const SCALA_EXTS = new Set(['.scala', '.sc']);
|
|
29
31
|
|
|
30
32
|
// Tokens that look like `name(` calls or definition headers but are language
|
|
31
33
|
// keywords, not user symbols — never treated as a call or a definition.
|
|
@@ -349,6 +351,81 @@ function rustDefs(masked) {
|
|
|
349
351
|
|
|
350
352
|
// Pick the masker whose comment/string syntax matches the language.
|
|
351
353
|
// Java and Go share JS syntax (Go raw strings mask like template literals).
|
|
354
|
+
/**
|
|
355
|
+
* Body range for a JVM-family member that may use either a brace body or an
|
|
356
|
+
* expression body (`fun f() = expr` / `def f = expr`), which Java has not.
|
|
357
|
+
* An expression body runs to the end of its line — enough to capture the calls
|
|
358
|
+
* it makes, which is all the graph needs.
|
|
359
|
+
* @returns {{bodyStart:number, bodyEnd:number}|null} null when neither form follows
|
|
360
|
+
*/
|
|
361
|
+
function jvmBodyRange(masked, from) {
|
|
362
|
+
let k = from;
|
|
363
|
+
while (k < masked.length && masked[k] !== '{' && masked[k] !== '=' && masked[k] !== '\n' && masked[k] !== ';') k++;
|
|
364
|
+
if (masked[k] === '{') return { bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') };
|
|
365
|
+
if (masked[k] === '=') {
|
|
366
|
+
// `= {` is still a brace body, just written with an assignment.
|
|
367
|
+
let j = k + 1;
|
|
368
|
+
while (j < masked.length && /\s/.test(masked[j])) j++;
|
|
369
|
+
if (masked[j] === '{') return { bodyStart: j, bodyEnd: matchDelim(masked, j, '{', '}') };
|
|
370
|
+
const eol = masked.indexOf('\n', k);
|
|
371
|
+
return { bodyStart: k, bodyEnd: eol === -1 ? masked.length : eol };
|
|
372
|
+
}
|
|
373
|
+
return null; // abstract member or interface declaration
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Kotlin `fun` definitions, including extension functions (`fun Foo.bar()`,
|
|
378
|
+
* recorded as `bar`) and expression bodies.
|
|
379
|
+
*/
|
|
380
|
+
function ktDefs(masked) {
|
|
381
|
+
const defs = [];
|
|
382
|
+
const seen = new Set();
|
|
383
|
+
const re = /(?:^|\n)[ \t]*((?:(?:public|private|protected|internal|open|override|abstract|final|suspend|inline|operator|infix|tailrec|external|expect|actual|inner|companion)\s+)*)fun\s+(?:<[^>\n]{0,80}>\s*)?(?:[A-Za-z_][\w.<>]*\.)?([A-Za-z_][\w]*)\s*\(/g;
|
|
384
|
+
let m;
|
|
385
|
+
while ((m = re.exec(masked)) !== null) {
|
|
386
|
+
const name = m[2];
|
|
387
|
+
if (NON_CALL.has(name)) continue;
|
|
388
|
+
const paren = masked.indexOf('(', m.index + m[0].length - 1);
|
|
389
|
+
const close = matchDelim(masked, paren, '(', ')');
|
|
390
|
+
if (close < 0) continue;
|
|
391
|
+
const range = jvmBodyRange(masked, close + 1);
|
|
392
|
+
const line = lineAt(masked, m.index + 1);
|
|
393
|
+
const key = name + ':' + (range ? range.bodyStart : line);
|
|
394
|
+
if (seen.has(key)) continue;
|
|
395
|
+
seen.add(key);
|
|
396
|
+
// An abstract/interface member owns no body: it can receive edges but
|
|
397
|
+
// never produces them, the same treatment javaDefs gives declarations.
|
|
398
|
+
defs.push(range ? { name, line, ...range } : { name, line, bodyStart: close, bodyEnd: close });
|
|
399
|
+
}
|
|
400
|
+
return defs;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Scala `def` definitions. Handles parameterless members (`def foo: Int = 1`),
|
|
405
|
+
* type parameters, and expression bodies.
|
|
406
|
+
*/
|
|
407
|
+
function scalaDefs(masked) {
|
|
408
|
+
const defs = [];
|
|
409
|
+
const seen = new Set();
|
|
410
|
+
const re = /(?:^|\n)[ \t]*((?:(?:private|protected|override|implicit|final|lazy|sealed|abstract|inline)(?:\s*\[[^\]\n]{0,60}\])?\s+)*)def\s+([A-Za-z_][\w]*)\s*(?=[\[\(:=])/g;
|
|
411
|
+
let m;
|
|
412
|
+
while ((m = re.exec(masked)) !== null) {
|
|
413
|
+
const name = m[2];
|
|
414
|
+
if (NON_CALL.has(name)) continue;
|
|
415
|
+
// Step past an optional type-parameter list, then an optional value list.
|
|
416
|
+
let k = m.index + m[0].length;
|
|
417
|
+
if (masked[k] === '[') { const e = matchDelim(masked, k, '[', ']'); if (e < 0) continue; k = e + 1; }
|
|
418
|
+
while (masked[k] === '(') { const e = matchDelim(masked, k, '(', ')'); if (e < 0) break; k = e + 1; }
|
|
419
|
+
const range = jvmBodyRange(masked, k);
|
|
420
|
+
const line = lineAt(masked, m.index + 1);
|
|
421
|
+
const key = name + ':' + (range ? range.bodyStart : line);
|
|
422
|
+
if (seen.has(key)) continue;
|
|
423
|
+
seen.add(key);
|
|
424
|
+
defs.push(range ? { name, line, ...range } : { name, line, bodyStart: k, bodyEnd: k });
|
|
425
|
+
}
|
|
426
|
+
return defs;
|
|
427
|
+
}
|
|
428
|
+
|
|
352
429
|
function maskFor(filePath, src) {
|
|
353
430
|
const ext = path.extname(filePath).toLowerCase();
|
|
354
431
|
if (PY_EXTS.has(ext)) return maskPy(src);
|
|
@@ -363,6 +440,8 @@ function extractDefs(filePath, src) {
|
|
|
363
440
|
if (JAVA_EXTS.has(ext)) return javaDefs(maskJs(src));
|
|
364
441
|
if (GO_EXTS.has(ext)) return goDefs(maskJs(src));
|
|
365
442
|
if (RS_EXTS.has(ext)) return rustDefs(maskRust(src));
|
|
443
|
+
if (KT_EXTS.has(ext)) return ktDefs(maskJs(src));
|
|
444
|
+
if (SCALA_EXTS.has(ext)) return scalaDefs(maskJs(src));
|
|
366
445
|
return null; // unsupported language
|
|
367
446
|
}
|
|
368
447
|
|
|
@@ -452,7 +531,8 @@ function _walk(dir, excludeSet, out, depth, maxDepth) {
|
|
|
452
531
|
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
|
|
453
532
|
else if (e.isFile()) {
|
|
454
533
|
const ext = path.extname(e.name).toLowerCase();
|
|
455
|
-
if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext)
|
|
534
|
+
if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext)
|
|
535
|
+
|| RS_EXTS.has(ext) || KT_EXTS.has(ext) || SCALA_EXTS.has(ext)) out.push(full);
|
|
456
536
|
}
|
|
457
537
|
}
|
|
458
538
|
}
|
package/src/map/route-table.js
CHANGED
|
@@ -20,6 +20,45 @@ function shouldSkipFile(rel) {
|
|
|
20
20
|
return /(^|\/)(gen-context|gen-project-map)\.js$/.test(normalized);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Byte offsets and prefixes of every `@Controller(...)` in a file.
|
|
25
|
+
* A file may declare several controllers, so each route is attributed to the
|
|
26
|
+
* nearest one above it rather than to a single file-wide prefix.
|
|
27
|
+
* @param {string} content
|
|
28
|
+
* @returns {Array<{index:number, prefix:string}>} ascending by index
|
|
29
|
+
*/
|
|
30
|
+
function nestControllerPrefixes(content) {
|
|
31
|
+
const out = [];
|
|
32
|
+
const re = /@Controller\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
|
|
33
|
+
let m;
|
|
34
|
+
while ((m = re.exec(content)) !== null) {
|
|
35
|
+
out.push({ index: m.index, prefix: m[1] || '' });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Prefix of the nearest `@Controller` above `index`, or '' when there is none. */
|
|
41
|
+
function prefixBefore(controllers, index) {
|
|
42
|
+
let prefix = '';
|
|
43
|
+
for (const c of controllers) {
|
|
44
|
+
if (c.index > index) break;
|
|
45
|
+
prefix = c.prefix;
|
|
46
|
+
}
|
|
47
|
+
return prefix;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Join a controller prefix and a method path into one route path.
|
|
52
|
+
* Either side may be empty, absent, or carry its own slashes.
|
|
53
|
+
* @returns {string} always slash-prefixed; never a trailing slash except '/'
|
|
54
|
+
*/
|
|
55
|
+
function joinRoute(prefix, methodPath) {
|
|
56
|
+
const parts = [prefix, methodPath]
|
|
57
|
+
.map((p) => String(p || '').trim().replace(/^\/+|\/+$/g, ''))
|
|
58
|
+
.filter(Boolean);
|
|
59
|
+
return parts.length ? '/' + parts.join('/') : '/';
|
|
60
|
+
}
|
|
61
|
+
|
|
23
62
|
/**
|
|
24
63
|
* Structured route rows across the supported frameworks — the data behind
|
|
25
64
|
* `analyze`, exposed for retrieval surface-enrichment (#488).
|
|
@@ -48,16 +87,14 @@ function collectRoutes(files, cwd) {
|
|
|
48
87
|
routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
|
|
49
88
|
}
|
|
50
89
|
|
|
51
|
-
// NestJS
|
|
52
|
-
|
|
90
|
+
// NestJS: @Get(':id') / @Post() — composed with the enclosing
|
|
91
|
+
// @Controller('prefix'). Without the prefix the emitted path matches
|
|
92
|
+
// nothing real, which defeats the point of route pseudo-signatures (#585).
|
|
93
|
+
const controllers = nestControllerPrefixes(content);
|
|
94
|
+
const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/g;
|
|
53
95
|
while ((m = re2.exec(content)) !== null) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// NestJS: @Get() with no path
|
|
58
|
-
const re3 = /@(Get|Post|Put|Patch|Delete)\s*\(\s*\)/g;
|
|
59
|
-
while ((m = re3.exec(content)) !== null) {
|
|
60
|
-
routes.push({ method: m[1].toUpperCase(), path: '/', file: rel });
|
|
96
|
+
const prefix = prefixBefore(controllers, m.index);
|
|
97
|
+
routes.push({ method: m[1].toUpperCase(), path: joinRoute(prefix, m[2]), file: rel });
|
|
61
98
|
}
|
|
62
99
|
}
|
|
63
100
|
|
package/src/mcp/server.js
CHANGED