sigmap 6.4.0 → 6.5.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/AGENTS.md +102 -111
- package/CHANGELOG.md +32 -0
- package/README.md +9 -8
- package/gen-context.js +59 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/loader.js +26 -2
- package/src/discovery/framework-detector.js +88 -0
- package/src/discovery/language-detector.js +74 -0
- package/src/discovery/sigmapignore.js +29 -0
- package/src/discovery/source-root-registry.js +166 -0
- package/src/discovery/source-root-resolver.js +181 -0
- package/src/discovery/source-root-scorer.js +98 -0
- package/src/mcp/server.js +1 -1
- package/src/retrieval/ranker.js +88 -23
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { REGISTRY } = require('./source-root-registry');
|
|
6
|
+
|
|
7
|
+
module.exports = { detectLanguages };
|
|
8
|
+
|
|
9
|
+
const SKIP_DIRS = new Set([
|
|
10
|
+
'node_modules','dist','build','.git','venv','.venv','target',
|
|
11
|
+
'DerivedData','Pods','.build','Carthage','coverage','.next','.nuxt',
|
|
12
|
+
'__pycache__','.pytest_cache','vendor','.bundle','Carthage',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const EXT_TO_LANG = {
|
|
16
|
+
'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
17
|
+
'.ts': 'typescript', '.tsx': 'typescript', '.jsx': 'javascript',
|
|
18
|
+
'.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust',
|
|
19
|
+
'.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.cpp': 'cpp',
|
|
20
|
+
'.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.swift': 'swift',
|
|
21
|
+
'.dart': 'dart', '.scala': 'scala', '.php': 'php',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function detectLanguages(cwd) {
|
|
25
|
+
const weights = {};
|
|
26
|
+
|
|
27
|
+
// Signal 1: manifest files (+3 each)
|
|
28
|
+
for (const [lang, reg] of Object.entries(REGISTRY)) {
|
|
29
|
+
for (const mf of (reg.manifestFiles || [])) {
|
|
30
|
+
if (fs.existsSync(path.join(cwd, mf))) {
|
|
31
|
+
weights[lang] = (weights[lang] || 0) + 3;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Signal 2: TypeScript dep in package.json (+2)
|
|
37
|
+
try {
|
|
38
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
39
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
40
|
+
if (allDeps.typescript) { weights.typescript = (weights.typescript || 0) + 2; }
|
|
41
|
+
} catch (_) {}
|
|
42
|
+
|
|
43
|
+
// Signal 3: file extension count (walk depth 3, capped at +5 per language)
|
|
44
|
+
const extCount = {};
|
|
45
|
+
_walkDepth(cwd, 3, extCount);
|
|
46
|
+
const maxCount = Math.max(1, ...Object.values(extCount));
|
|
47
|
+
for (const [ext, count] of Object.entries(extCount)) {
|
|
48
|
+
const lang = EXT_TO_LANG[ext];
|
|
49
|
+
if (lang) {
|
|
50
|
+
weights[lang] = (weights[lang] || 0) + Math.min(5, (count / maxCount) * 5);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Normalize to [0,1] and sort
|
|
55
|
+
const maxW = Math.max(1, ...Object.values(weights));
|
|
56
|
+
return Object.entries(weights)
|
|
57
|
+
.map(([name, w]) => ({ name, weight: Math.round(w / maxW * 100) / 100 }))
|
|
58
|
+
.sort((a, b) => b.weight - a.weight);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _walkDepth(dir, depth, extCount) {
|
|
62
|
+
if (depth <= 0) return;
|
|
63
|
+
let entries;
|
|
64
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
67
|
+
if (e.isDirectory()) {
|
|
68
|
+
_walkDepth(path.join(dir, e.name), depth - 1, extCount);
|
|
69
|
+
} else if (e.isFile()) {
|
|
70
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
71
|
+
if (EXT_TO_LANG[ext]) extCount[ext] = (extCount[ext] || 0) + 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
module.exports = { loadIgnorePatterns, matchesIgnorePattern };
|
|
7
|
+
|
|
8
|
+
function loadIgnorePatterns(cwd) {
|
|
9
|
+
for (const fname of ['.sigmapignore', '.contextignore']) {
|
|
10
|
+
const p = path.join(cwd, fname);
|
|
11
|
+
if (fs.existsSync(p)) {
|
|
12
|
+
return fs.readFileSync(p, 'utf8')
|
|
13
|
+
.split('\n')
|
|
14
|
+
.map(l => l.trim())
|
|
15
|
+
.filter(l => l && !l.startsWith('#'));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function matchesIgnorePattern(dirName, patterns) {
|
|
22
|
+
for (const pat of patterns) {
|
|
23
|
+
const clean = pat.replace(/\/$/, '');
|
|
24
|
+
if (clean === dirName) return true;
|
|
25
|
+
if (clean.endsWith('/**') && dirName.startsWith(clean.slice(0, -3))) return true;
|
|
26
|
+
if (clean.endsWith('/*') && dirName.startsWith(clean.slice(0, -2))) return true;
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const REGISTRY = {
|
|
4
|
+
javascript: {
|
|
5
|
+
manifestFiles: ['package.json'],
|
|
6
|
+
frameworks: {
|
|
7
|
+
nextjs: { detectionFiles: ['next.config.js','next.config.ts','next.config.mjs'], detectionDeps: ['next'], srcDirs: ['app','src/app','pages','src/pages','src','components','lib','hooks','utils'], entrypoints: ['app/page.tsx','pages/index.tsx'] },
|
|
8
|
+
nestjs: { detectionFiles: ['nest-cli.json'], detectionDeps: ['@nestjs/core'], srcDirs: ['src'], entrypoints: ['src/main.ts','src/app.module.ts'] },
|
|
9
|
+
express: { detectionFiles: [], detectionDeps: ['express'], srcDirs: ['src','routes','middleware','controllers','services'], entrypoints: ['src/index.js','server.js','app.js'] },
|
|
10
|
+
fastify: { detectionFiles: [], detectionDeps: ['fastify'], srcDirs: ['src','routes','plugins'], entrypoints: ['src/index.js'] },
|
|
11
|
+
react: { detectionFiles: [], detectionDeps: ['react'], srcDirs: ['src','components','hooks','context','pages','app','lib','utils'] },
|
|
12
|
+
vue: { detectionFiles: ['vue.config.js','vue.config.ts'], detectionDeps: ['vue'], srcDirs: ['src','components','composables','pages','views'] },
|
|
13
|
+
nuxt: { detectionFiles: ['nuxt.config.js','nuxt.config.ts'], detectionDeps: ['nuxt'], srcDirs: ['pages','components','composables','server','middleware','plugins'] },
|
|
14
|
+
svelte: { detectionFiles: ['svelte.config.js'], detectionDeps: ['svelte','@sveltejs/kit'], srcDirs: ['src','src/routes','src/lib'] },
|
|
15
|
+
angular: { detectionFiles: ['angular.json'], detectionDeps: ['@angular/core'], srcDirs: ['src','src/app','projects','apps','libs'] },
|
|
16
|
+
gatsby: { detectionFiles: ['gatsby-config.js','gatsby-config.ts'], detectionDeps: ['gatsby'], srcDirs: ['src','gatsby'] },
|
|
17
|
+
vite: { detectionFiles: ['vite.config.js','vite.config.ts'], detectionDeps: ['vite'], srcDirs: ['src'] },
|
|
18
|
+
remix: { detectionFiles: ['remix.config.js'], detectionDeps: ['@remix-run/react'], srcDirs: ['app'] },
|
|
19
|
+
trpc: { detectionFiles: [], detectionDeps: ['@trpc/server'], srcDirs: ['src','server','routers'] },
|
|
20
|
+
},
|
|
21
|
+
srcDirs: ['src','lib','index.js','server.js','app.js'],
|
|
22
|
+
penalties: ['dist','build','.next','.nuxt','coverage','storybook-static'],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
typescript: {
|
|
26
|
+
manifestFiles: ['package.json','tsconfig.json'],
|
|
27
|
+
frameworks: {
|
|
28
|
+
nextjs: { detectionFiles: ['next.config.ts','next.config.mjs'], detectionDeps: ['next'], srcDirs: ['app','src/app','pages','src','components','lib','hooks','utils'] },
|
|
29
|
+
nestjs: { detectionFiles: ['nest-cli.json'], detectionDeps: ['@nestjs/core'], srcDirs: ['src'], entrypoints: ['src/main.ts'] },
|
|
30
|
+
angular: { detectionFiles: ['angular.json'], detectionDeps: ['@angular/core'], srcDirs: ['src','src/app','projects','apps','libs'] },
|
|
31
|
+
},
|
|
32
|
+
srcDirs: ['src','lib','packages'],
|
|
33
|
+
penalties: ['dist','build','.next'],
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
python: {
|
|
37
|
+
manifestFiles: ['requirements.txt','pyproject.toml','setup.py','Pipfile'],
|
|
38
|
+
frameworks: {
|
|
39
|
+
django: { detectionFiles: ['manage.py'], detectionDeps: ['Django'], srcDirs: [], specialRule: 'django-app-dirs', entrypoints: ['manage.py'] },
|
|
40
|
+
fastapi: { detectionFiles: [], detectionDeps: ['fastapi'], srcDirs: ['app','src','routers','api'], entrypoints: ['main.py','app/main.py'] },
|
|
41
|
+
flask: { detectionFiles: ['wsgi.py','app.py'], detectionDeps: ['Flask'], srcDirs: ['app','src'], entrypoints: ['app.py','wsgi.py'] },
|
|
42
|
+
celery: { detectionFiles: [], detectionDeps: ['celery'], srcDirs: ['tasks','workers','app'] },
|
|
43
|
+
},
|
|
44
|
+
srcDirs: ['.'],
|
|
45
|
+
penalties: ['venv','.venv','__pycache__','.pytest_cache','htmlcov'],
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
go: {
|
|
49
|
+
manifestFiles: ['go.mod'],
|
|
50
|
+
frameworks: {
|
|
51
|
+
gin: { detectionFiles: [], detectionDeps: ['github.com/gin-gonic/gin'], srcDirs: ['internal','cmd','pkg','api','handler','middleware'] },
|
|
52
|
+
echo: { detectionFiles: [], detectionDeps: ['github.com/labstack/echo'], srcDirs: ['internal','cmd','handler','middleware'] },
|
|
53
|
+
fiber: { detectionFiles: [], detectionDeps: ['github.com/gofiber/fiber'], srcDirs: ['internal','cmd','handler','routes'] },
|
|
54
|
+
grpc: { detectionFiles: [], detectionDeps: ['google.golang.org/grpc'], srcDirs: ['internal','proto','server','client'] },
|
|
55
|
+
chi: { detectionFiles: [], detectionDeps: ['github.com/go-chi/chi'], srcDirs: ['internal','cmd','handler'] },
|
|
56
|
+
},
|
|
57
|
+
srcDirs: ['internal','cmd','pkg','api'],
|
|
58
|
+
penalties: ['vendor'],
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
rust: {
|
|
62
|
+
manifestFiles: ['Cargo.toml'],
|
|
63
|
+
frameworks: {
|
|
64
|
+
actix: { detectionFiles: [], detectionDeps: ['actix-web'], srcDirs: ['src'] },
|
|
65
|
+
axum: { detectionFiles: [], detectionDeps: ['axum'], srcDirs: ['src'] },
|
|
66
|
+
tauri: { detectionFiles: ['src-tauri/tauri.conf.json'], detectionDeps: ['tauri'], srcDirs: ['src','src-tauri/src'] },
|
|
67
|
+
},
|
|
68
|
+
srcDirs: ['src'],
|
|
69
|
+
penalties: ['target'],
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
java: {
|
|
73
|
+
manifestFiles: ['pom.xml','build.gradle'],
|
|
74
|
+
frameworks: {
|
|
75
|
+
spring: { detectionFiles: [], detectionDeps: ['spring-boot'], srcDirs: ['src/main/java','src/main/kotlin','src/main/resources'] },
|
|
76
|
+
quarkus: { detectionFiles: [], detectionDeps: ['io.quarkus'], srcDirs: ['src/main/java'] },
|
|
77
|
+
android: { detectionFiles: ['AndroidManifest.xml'], srcDirs: ['app/src/main/java','app/src/main','src'] },
|
|
78
|
+
micronaut:{ detectionFiles: [], detectionDeps: ['io.micronaut'],srcDirs: ['src/main/java'] },
|
|
79
|
+
},
|
|
80
|
+
srcDirs: ['src/main/java','src'],
|
|
81
|
+
penalties: ['target','build'],
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
kotlin: {
|
|
85
|
+
manifestFiles: ['build.gradle.kts'],
|
|
86
|
+
frameworks: {
|
|
87
|
+
spring: { detectionFiles: [], detectionDeps: ['spring-boot'], srcDirs: ['src/main/kotlin'] },
|
|
88
|
+
android: { detectionFiles: ['AndroidManifest.xml'], srcDirs: ['app/src/main/kotlin','app/src/main/java'] },
|
|
89
|
+
ktor: { detectionFiles: [], detectionDeps: ['io.ktor'], srcDirs: ['src'] },
|
|
90
|
+
compose: { detectionFiles: [], detectionDeps: ['compose-runtime'], srcDirs: ['app/src/main/kotlin','src'] },
|
|
91
|
+
},
|
|
92
|
+
srcDirs: ['src/main/kotlin','src'],
|
|
93
|
+
penalties: ['build','.gradle'],
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
csharp: {
|
|
97
|
+
manifestFiles: ['.csproj','.sln'],
|
|
98
|
+
frameworks: {
|
|
99
|
+
aspnet: { detectionFiles: ['appsettings.json'], detectionDeps: ['Microsoft.AspNetCore'], srcDirs: ['Controllers','Services','Models','Middleware','Pages'] },
|
|
100
|
+
blazor: { detectionFiles: [], detectionDeps: ['Microsoft.AspNetCore.Components'], srcDirs: ['Components','Pages','Services'] },
|
|
101
|
+
unity: { detectionFiles: ['ProjectSettings/ProjectSettings.asset'], srcDirs: ['Assets/Scripts','Assets'] },
|
|
102
|
+
maui: { detectionFiles: [], detectionDeps: ['Microsoft.Maui'], srcDirs: ['src','Pages','ViewModels'] },
|
|
103
|
+
},
|
|
104
|
+
srcDirs: ['src','Controllers','Services','Models'],
|
|
105
|
+
penalties: ['bin','obj','.vs'],
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
php: {
|
|
109
|
+
manifestFiles: ['composer.json'],
|
|
110
|
+
frameworks: {
|
|
111
|
+
laravel: { detectionFiles: ['artisan'], srcDirs: ['app','routes','config','database','resources','tests'], entrypoints: ['artisan'] },
|
|
112
|
+
symfony: { detectionFiles: ['symfony.lock'], srcDirs: ['src','config','templates'], specialRule: 'symfony-bundle-dirs' },
|
|
113
|
+
wordpress: { detectionFiles: ['wp-config.php'], srcDirs: ['wp-content/themes','wp-content/plugins','wp-content/mu-plugins'] },
|
|
114
|
+
slim: { detectionFiles: [], detectionDeps: ['slim/slim'], srcDirs: ['src','app','routes'] },
|
|
115
|
+
},
|
|
116
|
+
srcDirs: ['src','app'],
|
|
117
|
+
penalties: ['vendor'],
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
ruby: {
|
|
121
|
+
manifestFiles: ['Gemfile'],
|
|
122
|
+
frameworks: {
|
|
123
|
+
rails: { detectionFiles: ['config/routes.rb'], srcDirs: ['app','lib','config','db','spec','test'], entrypoints: ['config/routes.rb'] },
|
|
124
|
+
sinatra: { detectionFiles: ['config.ru','app.rb'], srcDirs: ['.','lib'], entrypoints: ['app.rb','config.ru'] },
|
|
125
|
+
hanami: { detectionFiles: [], detectionDeps: ['hanami'], srcDirs: ['apps','lib','slices'] },
|
|
126
|
+
},
|
|
127
|
+
srcDirs: ['app','lib'],
|
|
128
|
+
penalties: ['vendor','coverage','.bundle'],
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
swift: {
|
|
132
|
+
manifestFiles: ['Package.swift'],
|
|
133
|
+
frameworks: {
|
|
134
|
+
vapor: { detectionFiles: [], detectionDeps: ['vapor/vapor'], srcDirs: ['Sources','App'] },
|
|
135
|
+
swiftui: { detectionFiles: ['.xcodeproj'], srcDirs: [], specialRule: 'swift-project-dir' },
|
|
136
|
+
swiftpm: { detectionFiles: ['Package.swift'],srcDirs: ['Sources'] },
|
|
137
|
+
},
|
|
138
|
+
srcDirs: ['Sources','Source'],
|
|
139
|
+
penalties: ['.build','DerivedData','Pods','Carthage'],
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
dart: {
|
|
143
|
+
manifestFiles: ['pubspec.yaml'],
|
|
144
|
+
frameworks: {
|
|
145
|
+
flutter: { detectionFiles: [], detectionDeps: ['flutter'], srcDirs: ['lib','lib/src'], entrypoints: ['lib/main.dart'] },
|
|
146
|
+
serverpod: { detectionFiles: [], detectionDeps: ['serverpod'], srcDirs: ['lib','endpoints','models'] },
|
|
147
|
+
'dart-frog':{ detectionFiles: ['dart_frog.yaml'], srcDirs: ['routes','lib'] },
|
|
148
|
+
},
|
|
149
|
+
srcDirs: ['lib','lib/src'],
|
|
150
|
+
penalties: ['.dart_tool','build'],
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
scala: {
|
|
154
|
+
manifestFiles: ['build.sbt'],
|
|
155
|
+
frameworks: {
|
|
156
|
+
akka: { detectionFiles: [], detectionDeps: ['akka'], srcDirs: ['src/main/scala','src'] },
|
|
157
|
+
play: { detectionFiles: [], detectionDeps: ['play'], srcDirs: ['app','conf'] },
|
|
158
|
+
spark: { detectionFiles: [], detectionDeps: ['spark'],srcDirs: ['src/main/scala'] },
|
|
159
|
+
zio: { detectionFiles: [], detectionDeps: ['zio'], srcDirs: ['src/main/scala'] },
|
|
160
|
+
},
|
|
161
|
+
srcDirs: ['src/main/scala','src'],
|
|
162
|
+
penalties: ['target'],
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
module.exports = { REGISTRY };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { REGISTRY } = require('./source-root-registry');
|
|
6
|
+
const { detectLanguages } = require('./language-detector');
|
|
7
|
+
const { detectFrameworks } = require('./framework-detector');
|
|
8
|
+
const { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS } = require('./source-root-scorer');
|
|
9
|
+
const { loadIgnorePatterns, matchesIgnorePattern } = require('./sigmapignore');
|
|
10
|
+
|
|
11
|
+
module.exports = { resolveSourceRoots };
|
|
12
|
+
|
|
13
|
+
const MONOREPO_MARKERS = ['pnpm-workspace.yaml','turbo.json','nx.json','lerna.json'];
|
|
14
|
+
const MAX_ROOTS = 6;
|
|
15
|
+
|
|
16
|
+
function resolveSourceRoots(cwd, opts = {}) {
|
|
17
|
+
const ignorePatterns = loadIgnorePatterns(cwd);
|
|
18
|
+
const languages = detectLanguages(cwd);
|
|
19
|
+
const frameworks = detectFrameworks(cwd);
|
|
20
|
+
const recentDirs = getRecentlyChangedDirs(cwd);
|
|
21
|
+
const isMonorepo = _detectMonorepo(cwd);
|
|
22
|
+
|
|
23
|
+
const primaryLang = languages[0]?.name;
|
|
24
|
+
const primaryFw = frameworks[0];
|
|
25
|
+
const registry = primaryLang ? REGISTRY[primaryLang] : null;
|
|
26
|
+
|
|
27
|
+
// Build framework-derived context
|
|
28
|
+
const fwEntry = primaryFw && registry?.frameworks?.[primaryFw.name];
|
|
29
|
+
const frameworkSrcDirs = new Set(fwEntry?.srcDirs || registry?.srcDirs || []);
|
|
30
|
+
const entrypoints = fwEntry?.entrypoints || [];
|
|
31
|
+
const frameworkPenalties = registry?.penalties || [];
|
|
32
|
+
|
|
33
|
+
const context = { frameworks, languages, recentDirs, frameworkSrcDirs, entrypoints, frameworkPenalties };
|
|
34
|
+
|
|
35
|
+
// Enumerate candidates
|
|
36
|
+
const candidates = _enumerateCandidates(cwd, isMonorepo, ignorePatterns, opts.exclude || []);
|
|
37
|
+
|
|
38
|
+
// Score each candidate
|
|
39
|
+
const scored = candidates
|
|
40
|
+
.map(({ name, full }) => ({
|
|
41
|
+
dir: name,
|
|
42
|
+
full,
|
|
43
|
+
score: scoreCandidate(name, full, context),
|
|
44
|
+
}))
|
|
45
|
+
.filter(c => c.score > 0)
|
|
46
|
+
.sort((a, b) => b.score - a.score);
|
|
47
|
+
|
|
48
|
+
// Handle special rules
|
|
49
|
+
let roots = _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks);
|
|
50
|
+
|
|
51
|
+
// Dedupe nested paths (prefer parent)
|
|
52
|
+
roots = _dedupeNested(roots);
|
|
53
|
+
|
|
54
|
+
// Cap at MAX_ROOTS
|
|
55
|
+
roots = roots.slice(0, MAX_ROOTS).map(r => r.dir);
|
|
56
|
+
|
|
57
|
+
// Fallback: if nothing scored, return empty (caller falls back to legacy)
|
|
58
|
+
const confidence = _computeConfidence(frameworks, languages, scored.length);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
roots,
|
|
62
|
+
languages,
|
|
63
|
+
frameworks,
|
|
64
|
+
confidence,
|
|
65
|
+
explanation: scored.slice(0, 8).map(c => ({
|
|
66
|
+
dir: c.dir,
|
|
67
|
+
score: c.score,
|
|
68
|
+
reason: `score: ${c.score}`,
|
|
69
|
+
})),
|
|
70
|
+
isMonorepo,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _detectMonorepo(cwd) {
|
|
75
|
+
for (const m of MONOREPO_MARKERS) {
|
|
76
|
+
if (fs.existsSync(path.join(cwd, m))) return true;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
80
|
+
if (pkg.workspaces) return true;
|
|
81
|
+
} catch (_) {}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _enumerateCandidates(cwd, isMonorepo, ignorePatterns, excludeList) {
|
|
86
|
+
const candidates = [];
|
|
87
|
+
const excSet = new Set(excludeList);
|
|
88
|
+
|
|
89
|
+
// Root-level dirs
|
|
90
|
+
try {
|
|
91
|
+
for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
92
|
+
if (!e.isDirectory()) continue;
|
|
93
|
+
if (excSet.has(e.name)) continue;
|
|
94
|
+
if (matchesIgnorePattern(e.name, ignorePatterns)) continue;
|
|
95
|
+
candidates.push({ name: e.name, full: path.join(cwd, e.name) });
|
|
96
|
+
}
|
|
97
|
+
} catch (_) {}
|
|
98
|
+
|
|
99
|
+
// Monorepo sub-packages: packages/*/src, apps/*/src, services/*/src
|
|
100
|
+
if (isMonorepo) {
|
|
101
|
+
for (const top of ['packages','apps','services','modules']) {
|
|
102
|
+
const topFull = path.join(cwd, top);
|
|
103
|
+
if (!fs.existsSync(topFull)) continue;
|
|
104
|
+
try {
|
|
105
|
+
for (const pkg of fs.readdirSync(topFull, { withFileTypes: true })) {
|
|
106
|
+
if (!pkg.isDirectory()) continue;
|
|
107
|
+
const srcFull = path.join(topFull, pkg.name, 'src');
|
|
108
|
+
if (fs.existsSync(srcFull)) {
|
|
109
|
+
candidates.push({ name: `${top}/${pkg.name}/src`, full: srcFull });
|
|
110
|
+
}
|
|
111
|
+
// Also consider the package root itself
|
|
112
|
+
candidates.push({ name: `${top}/${pkg.name}`, full: path.join(topFull, pkg.name) });
|
|
113
|
+
}
|
|
114
|
+
} catch (_) {}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Deep paths known by language/framework (e.g. src/main/java, src-tauri/src)
|
|
119
|
+
const DEEP_PATHS = [
|
|
120
|
+
'src/main/java','src/main/kotlin','src/main/scala',
|
|
121
|
+
'src-tauri/src','Sources/App','app/src/main/java','app/src/main/kotlin',
|
|
122
|
+
];
|
|
123
|
+
for (const dp of DEEP_PATHS) {
|
|
124
|
+
const full = path.join(cwd, dp);
|
|
125
|
+
if (fs.existsSync(full)) candidates.push({ name: dp, full });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return candidates;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks) {
|
|
132
|
+
let roots = [...scored];
|
|
133
|
+
|
|
134
|
+
// Django: walk root dirs for any containing models.py or views.py
|
|
135
|
+
if (primaryFw?.name === 'django' || frameworks.some(f => f.name === 'django')) {
|
|
136
|
+
try {
|
|
137
|
+
for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
138
|
+
if (!e.isDirectory()) continue;
|
|
139
|
+
const d = path.join(cwd, e.name);
|
|
140
|
+
if (fs.existsSync(path.join(d, 'models.py')) || fs.existsSync(path.join(d, 'views.py'))) {
|
|
141
|
+
if (!roots.find(r => r.dir === e.name)) {
|
|
142
|
+
roots.push({ dir: e.name, full: d, score: 5.0 });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (_) {}
|
|
147
|
+
roots.sort((a, b) => b.score - a.score);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Swift project dir: dirs with ≥3 .swift files
|
|
151
|
+
if (frameworks.some(f => f.name === 'swiftui')) {
|
|
152
|
+
try {
|
|
153
|
+
for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
154
|
+
if (!e.isDirectory()) continue;
|
|
155
|
+
const d = path.join(cwd, e.name);
|
|
156
|
+
const swiftCount = (fs.readdirSync(d).filter(f => f.endsWith('.swift'))).length;
|
|
157
|
+
if (swiftCount >= 3 && !roots.find(r => r.dir === e.name)) {
|
|
158
|
+
roots.push({ dir: e.name, full: d, score: 4.0 });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} catch (_) {}
|
|
162
|
+
roots.sort((a, b) => b.score - a.score);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return roots;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function _dedupeNested(scored) {
|
|
169
|
+
const result = [];
|
|
170
|
+
for (const c of scored) {
|
|
171
|
+
const isNested = result.some(r => c.dir.startsWith(r.dir + '/'));
|
|
172
|
+
if (!isNested) result.push(c);
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function _computeConfidence(frameworks, languages, scoredCount) {
|
|
178
|
+
if (frameworks.length > 0 && frameworks[0].confidence >= 0.90) return 'high';
|
|
179
|
+
if (languages.length > 0 && scoredCount > 0) return 'medium';
|
|
180
|
+
return 'low';
|
|
181
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const CODE_EXTS = new Set([
|
|
8
|
+
'.js','.mjs','.cjs','.ts','.tsx','.jsx',
|
|
9
|
+
'.py','.rb','.go','.rs','.java','.kt',
|
|
10
|
+
'.cs','.cpp','.c','.h','.swift','.dart','.scala','.php',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const AUTO_SKIP = new Set([
|
|
14
|
+
'node_modules','dist','build','.git','.next','.nuxt','vendor',
|
|
15
|
+
'DerivedData','Pods','target','coverage','__pycache__','.venv','venv',
|
|
16
|
+
'.build','Carthage','storybook-static','.gradle','bin','obj','.vs',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const PENALTY_DIRS = new Set([
|
|
20
|
+
'test','tests','spec','__tests__','e2e','docs','doc','docs-vp',
|
|
21
|
+
'examples','example','fixtures','mocks','__mocks__','demo','samples','migrations',
|
|
22
|
+
'benchmarks','scripts',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const ROOT_ENTRYPOINTS = {
|
|
26
|
+
go: ['main.go'],
|
|
27
|
+
python: ['app.py','main.py','wsgi.py','asgi.py'],
|
|
28
|
+
javascript: ['index.js','server.js','app.js'],
|
|
29
|
+
typescript: ['index.ts','main.ts'],
|
|
30
|
+
rust: [],
|
|
31
|
+
php: ['index.php'],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function getRecentlyChangedDirs(cwd) {
|
|
35
|
+
try {
|
|
36
|
+
const out = execSync('git log --name-only --format="" HEAD~10 2>/dev/null', { cwd, timeout: 3000 }).toString();
|
|
37
|
+
return new Set(out.split('\n').filter(Boolean).map(f => f.split('/')[0]));
|
|
38
|
+
} catch { return new Set(); }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function scoreCandidate(dirName, fullPath, context) {
|
|
42
|
+
const { frameworks, languages, recentDirs, frameworkSrcDirs, entrypoints, frameworkPenalties } = context;
|
|
43
|
+
|
|
44
|
+
// Auto-skip noise
|
|
45
|
+
if (AUTO_SKIP.has(dirName)) return -99;
|
|
46
|
+
if (!fs.existsSync(fullPath)) return -99;
|
|
47
|
+
|
|
48
|
+
let score = 0;
|
|
49
|
+
|
|
50
|
+
// Framework match: +3.0 if this dir is in the framework's srcDirs
|
|
51
|
+
if (frameworkSrcDirs.has(dirName)) score += 3.0;
|
|
52
|
+
|
|
53
|
+
// Count source files in dir (depth 2)
|
|
54
|
+
const sourceFileCount = _countSourceFiles(fullPath, 2);
|
|
55
|
+
const density = Math.min(1.0, sourceFileCount / 10);
|
|
56
|
+
|
|
57
|
+
// Language density: +2.5
|
|
58
|
+
score += density * 2.5;
|
|
59
|
+
|
|
60
|
+
// Symbol density: +2.0 if ≥3 source files
|
|
61
|
+
if (sourceFileCount >= 3) score += 2.0;
|
|
62
|
+
|
|
63
|
+
// Entrypoint: +1.5 if a known entrypoint lives in this dir
|
|
64
|
+
if ((entrypoints || []).some(ep => ep.startsWith(dirName + '/'))) score += 1.5;
|
|
65
|
+
|
|
66
|
+
// Manifest proximity: +1.0 if a manifest file is in this dir
|
|
67
|
+
if (fs.existsSync(path.join(fullPath, 'package.json')) ||
|
|
68
|
+
fs.existsSync(path.join(fullPath, 'go.mod')) ||
|
|
69
|
+
fs.existsSync(path.join(fullPath, 'Cargo.toml')) ||
|
|
70
|
+
fs.existsSync(path.join(fullPath, 'pom.xml'))) {
|
|
71
|
+
score += 1.0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Git activity bonus: +2.0 if recently committed files exist here
|
|
75
|
+
if (recentDirs.has(dirName)) score += 2.0;
|
|
76
|
+
|
|
77
|
+
// Noise penalty: -3.0 (unless directory is in framework's srcDirs)
|
|
78
|
+
if (PENALTY_DIRS.has(dirName.toLowerCase()) && !frameworkSrcDirs.has(dirName)) score -= 3.0;
|
|
79
|
+
|
|
80
|
+
// Framework penalty dirs
|
|
81
|
+
if ((frameworkPenalties || []).includes(dirName)) score -= 3.0;
|
|
82
|
+
|
|
83
|
+
return Math.round(score * 100) / 100;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function _countSourceFiles(dir, depth) {
|
|
87
|
+
if (depth <= 0) return 0;
|
|
88
|
+
let count = 0;
|
|
89
|
+
try {
|
|
90
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
91
|
+
if (e.isFile() && CODE_EXTS.has(path.extname(e.name).toLowerCase())) count++;
|
|
92
|
+
else if (e.isDirectory() && depth > 1) count += _countSourceFiles(path.join(dir, e.name), depth - 1);
|
|
93
|
+
}
|
|
94
|
+
} catch (_) {}
|
|
95
|
+
return count;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS };
|
package/src/mcp/server.js
CHANGED