sigmap 6.4.0 → 6.5.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 +13 -25
- package/CHANGELOG.md +17 -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/AGENTS.md
CHANGED
|
@@ -47,18 +47,6 @@ Use this marker block for all appendable context files:
|
|
|
47
47
|
<!-- Updated by gen-context.js -->
|
|
48
48
|
# Code signatures
|
|
49
49
|
|
|
50
|
-
## changes (last 5 commits — 0 seconds ago)
|
|
51
|
-
```
|
|
52
|
-
src/learning/weights.js +exportWeights +importWeights ~resetWeights
|
|
53
|
-
packages/adapters/codex.js ~write ~format
|
|
54
|
-
packages/adapters/claude.js ~format ~write
|
|
55
|
-
packages/adapters/gemini.js ~format
|
|
56
|
-
packages/adapters/copilot.js ~format
|
|
57
|
-
packages/adapters/cursor.js ~format
|
|
58
|
-
packages/adapters/openai.js ~format
|
|
59
|
-
packages/adapters/windsurf.js ~format
|
|
60
|
-
```
|
|
61
|
-
|
|
62
50
|
## packages
|
|
63
51
|
|
|
64
52
|
### packages/cli/index.js
|
|
@@ -133,22 +121,22 @@ function outputPath(cwd) → string
|
|
|
133
121
|
function write(context, cwd, opts = {})
|
|
134
122
|
```
|
|
135
123
|
|
|
136
|
-
### packages/adapters/
|
|
124
|
+
### packages/adapters/copilot.js
|
|
137
125
|
```
|
|
138
126
|
module.exports = { name, format, outputPath, write }
|
|
139
127
|
function format(context, opts = {}) → string
|
|
128
|
+
function _confidenceMeta(opts)
|
|
140
129
|
function outputPath(cwd) → string
|
|
141
130
|
function write(context, cwd, opts = {})
|
|
142
|
-
function _confidenceMeta(opts)
|
|
143
131
|
```
|
|
144
132
|
|
|
145
|
-
### packages/adapters/
|
|
133
|
+
### packages/adapters/gemini.js
|
|
146
134
|
```
|
|
147
135
|
module.exports = { name, format, outputPath, write }
|
|
148
136
|
function format(context, opts = {}) → string
|
|
149
|
-
function _confidenceMeta(opts)
|
|
150
137
|
function outputPath(cwd) → string
|
|
151
138
|
function write(context, cwd, opts = {})
|
|
139
|
+
function _confidenceMeta(opts)
|
|
152
140
|
```
|
|
153
141
|
|
|
154
142
|
### packages/adapters/cursor.js
|
|
@@ -547,15 +535,6 @@ function formatAnalysisJSON(stats) → object
|
|
|
547
535
|
module.exports = { DEFAULTS }
|
|
548
536
|
```
|
|
549
537
|
|
|
550
|
-
### src/config/loader.js
|
|
551
|
-
```
|
|
552
|
-
module.exports = { loadConfig, loadBaseConfig }
|
|
553
|
-
function loadBaseConfig(extendsVal, cwd)
|
|
554
|
-
function detectAutoSrcDirs(cwd, excludeList) → string[]
|
|
555
|
-
function loadConfig(cwd) → object
|
|
556
|
-
function deepClone(obj)
|
|
557
|
-
```
|
|
558
|
-
|
|
559
538
|
### src/format/dashboard.js
|
|
560
539
|
```
|
|
561
540
|
module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak }
|
|
@@ -673,6 +652,15 @@ function extractClassMembers(block)
|
|
|
673
652
|
function normalizeParams(params)
|
|
674
653
|
```
|
|
675
654
|
|
|
655
|
+
### src/config/loader.js
|
|
656
|
+
```
|
|
657
|
+
module.exports = { loadConfig, loadBaseConfig }
|
|
658
|
+
function loadBaseConfig(extendsVal, cwd)
|
|
659
|
+
function detectAutoSrcDirs(cwd, excludeList) → string[]
|
|
660
|
+
function loadConfig(cwd) → object
|
|
661
|
+
function deepClone(obj)
|
|
662
|
+
```
|
|
663
|
+
|
|
676
664
|
### src/learning/weights.js
|
|
677
665
|
```
|
|
678
666
|
module.exports = { BASELINE, DECAY, MAX_MULT, MIN_MULT, weightsPath, clampMultiplier, normalizeFile, loadWeights, saveWeights, updateWeights, boostFiles, penalizeFiles, resetWeights, exportWeights, importWeights }
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,23 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [6.5.0] — 2026-04-25
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Source Root Resolver (v6.5)** — intelligent auto-detection of source directories for 17 languages and 50+ frameworks (Next.js, Django, Rails, Spring Boot, Flutter, Go, Rust, etc.). Uses multi-signal scoring: manifest files, language/framework detection, file density, git activity, and framework-specific srcDirs. Returns confidence level (high/medium/low) and detailed explanation. Integrated into `loadConfig()` with graceful fallback to legacy heuristics.
|
|
18
|
+
- **`.sigmapignore` pattern matching** — new `.sigmapignore` file support (fallback to `.contextignore`) for excluding directories. Supports simple patterns like `legacy/` and globs like `src/**`.
|
|
19
|
+
- **`sigmap roots` CLI command** — three modes: `--explain` (default, shows detected languages/frameworks and scores), `--json` (structured output), `--fix` (interactive prompt to correct srcDirs and write to config).
|
|
20
|
+
- **Monorepo detection and enumeration** — auto-detects monorepos via pnpm-workspace.yaml, turbo.json, nx.json, lerna.json, and package.json workspaces. Enumerates all sub-packages and common deep paths.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **Framework-discovery tests** — updated registry entries to include all framework-specific srcDirs expected by legacy detector (Rails: db/spec/test, Laravel: resources/tests, Angular: projects/apps/libs, Next: hooks/utils).
|
|
25
|
+
- **Scoring penalty for framework srcDirs** — test directories (spec, test, tests) no longer penalized when explicitly in framework's srcDirs list.
|
|
26
|
+
- **CLI command ordering** — `roots` command handler now executes before `explain` to prevent flag conflict.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
13
30
|
## [6.4.0] — 2026-04-23
|
|
14
31
|
|
|
15
32
|
### Changed
|
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
**SigMap finds the right files before your AI answers.**
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/sigmap)
|
|
10
|
+
[](https://www.npmjs.com/package/sigmap)
|
|
10
11
|
[](https://github.com/manojmallick/sigmap/actions/workflows/ci.yml)
|
|
11
12
|
[](package.json)
|
|
12
13
|
[](LICENSE)
|
|
@@ -37,10 +38,10 @@ Works with Copilot, Claude, Cursor, Windsurf, and any LLM.
|
|
|
37
38
|
|
|
38
39
|
## Why SigMap?
|
|
39
40
|
|
|
40
|
-
- **
|
|
41
|
+
- **78.9% hit@5** — right file found in top 5 results (vs 13.6% baseline)
|
|
41
42
|
- **40–98% token reduction** — 2K–4K tokens instead of 80K+
|
|
42
43
|
- **52.2% task success rate** — up from 10% without context
|
|
43
|
-
- **1.
|
|
44
|
+
- **1.69 prompts per task** — down from 2.84
|
|
44
45
|
- **Works with any LLM** — no API key, no cloud, no accounts
|
|
45
46
|
- **Zero npm dependencies** — `npx sigmap` on any machine
|
|
46
47
|
|
|
@@ -50,7 +51,7 @@ Works with Copilot, Claude, Cursor, Windsurf, and any LLM.
|
|
|
50
51
|
|
|
51
52
|
| Without SigMap | With SigMap |
|
|
52
53
|
|---|---|
|
|
53
|
-
| ❌ Guessing which files are relevant | ✅ Right file in context —
|
|
54
|
+
| ❌ Guessing which files are relevant | ✅ Right file in context — 79% of the time |
|
|
54
55
|
| ❌ Sending the full repo to your AI | ✅ Minimal context — only what matters |
|
|
55
56
|
| ❌ Embeddings / vector DB required | ✅ Grounded answers, no infra needed |
|
|
56
57
|
|
|
@@ -74,13 +75,13 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
74
75
|
## Benchmark
|
|
75
76
|
|
|
76
77
|
```
|
|
77
|
-
Benchmark : sigmap-v6.
|
|
78
|
-
Date : 2026-04-
|
|
78
|
+
Benchmark : sigmap-v6.4-main
|
|
79
|
+
Date : 2026-04-23
|
|
79
80
|
|
|
80
|
-
Hit@5 :
|
|
81
|
-
Prompt reduction : 40.
|
|
81
|
+
Hit@5 : 78.9% (baseline 13.6% — 5.8× lift)
|
|
82
|
+
Prompt reduction : 40.6%
|
|
82
83
|
Task success : 52.2% (baseline 10%)
|
|
83
|
-
Prompts / task : 1.
|
|
84
|
+
Prompts / task : 1.69 (baseline 2.84)
|
|
84
85
|
Token reduction: 40–98% (avg 96.9% across 18 real repos)
|
|
85
86
|
```
|
|
86
87
|
|
package/gen-context.js
CHANGED
|
@@ -5387,7 +5387,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
5387
5387
|
|
|
5388
5388
|
const SERVER_INFO = {
|
|
5389
5389
|
name: 'sigmap',
|
|
5390
|
-
version: '6.
|
|
5390
|
+
version: '6.5.0',
|
|
5391
5391
|
description: 'SigMap MCP server — code signatures on demand',
|
|
5392
5392
|
};
|
|
5393
5393
|
|
|
@@ -7222,7 +7222,7 @@ const path = require('path');
|
|
|
7222
7222
|
const os = require('os');
|
|
7223
7223
|
const { execSync } = require('child_process');
|
|
7224
7224
|
|
|
7225
|
-
const VERSION = '6.
|
|
7225
|
+
const VERSION = '6.5.0';
|
|
7226
7226
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
7227
7227
|
|
|
7228
7228
|
function requireSourceOrBundled(key) {
|
|
@@ -8477,6 +8477,15 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
|
8477
8477
|
fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime });
|
|
8478
8478
|
}
|
|
8479
8479
|
|
|
8480
|
+
// v6.5: Coverage feedback loop — warn if symbol density is suspiciously low
|
|
8481
|
+
const symbolsFound = fileEntries.reduce((n, f) => n + (f.sigs?.length || 0), 0);
|
|
8482
|
+
const filesFound = fileEntries.length;
|
|
8483
|
+
const coverageRatio = filesFound > 0 ? symbolsFound / filesFound : 1;
|
|
8484
|
+
if (coverageRatio < 0.25 && !config.srcDirs?.length) {
|
|
8485
|
+
process.stderr.write('[sigmap] ⚠ low symbol coverage — source roots may be wrong\n');
|
|
8486
|
+
process.stderr.write('[sigmap] run: sigmap roots --explain\n');
|
|
8487
|
+
}
|
|
8488
|
+
|
|
8480
8489
|
const strategy = config.strategy || 'full';
|
|
8481
8490
|
const beforeCount = fileEntries.length;
|
|
8482
8491
|
|
|
@@ -9797,6 +9806,54 @@ function main() {
|
|
|
9797
9806
|
process.exit(0);
|
|
9798
9807
|
}
|
|
9799
9808
|
|
|
9809
|
+
// v6.5: `sigmap roots` — detect source roots
|
|
9810
|
+
if (args[0] === 'roots') {
|
|
9811
|
+
const { resolveSourceRoots } = requireSourceOrBundled('./src/discovery/source-root-resolver');
|
|
9812
|
+
const result = resolveSourceRoots(cwd);
|
|
9813
|
+
|
|
9814
|
+
if (args.includes('--json')) {
|
|
9815
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9816
|
+
process.exit(0);
|
|
9817
|
+
}
|
|
9818
|
+
|
|
9819
|
+
if (args.includes('--fix')) {
|
|
9820
|
+
console.log('[sigmap] Current detected roots:', result.roots.join(', '));
|
|
9821
|
+
const readline = require('readline');
|
|
9822
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
9823
|
+
rl.question('Enter correct srcDirs (comma-separated): ', answer => {
|
|
9824
|
+
const dirs = answer.split(',').map(d => d.trim()).filter(Boolean);
|
|
9825
|
+
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
9826
|
+
const cfg = fs.existsSync(cfgPath) ? JSON.parse(fs.readFileSync(cfgPath, 'utf8')) : {};
|
|
9827
|
+
cfg.srcDirs = dirs;
|
|
9828
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
9829
|
+
console.log(`[sigmap] Saved srcDirs: ${dirs.join(', ')} → gen-context.config.json`);
|
|
9830
|
+
rl.close();
|
|
9831
|
+
process.exit(0);
|
|
9832
|
+
});
|
|
9833
|
+
return;
|
|
9834
|
+
}
|
|
9835
|
+
|
|
9836
|
+
// --explain (default)
|
|
9837
|
+
console.log('\nDetected languages:');
|
|
9838
|
+
for (const l of result.languages.slice(0, 4)) {
|
|
9839
|
+
console.log(` ${l.name.padEnd(16)} ${l.weight.toFixed(2)}`);
|
|
9840
|
+
}
|
|
9841
|
+
console.log('\nDetected frameworks:');
|
|
9842
|
+
if (result.frameworks.length === 0) console.log(' (none)');
|
|
9843
|
+
for (const f of result.frameworks.slice(0, 3)) {
|
|
9844
|
+
console.log(` ${f.name.padEnd(16)} ${f.confidence.toFixed(2)}`);
|
|
9845
|
+
}
|
|
9846
|
+
console.log('\nChosen source roots:');
|
|
9847
|
+
if (result.roots.length === 0) console.log(' (none — legacy fallback used)');
|
|
9848
|
+
result.roots.forEach((r, i) => {
|
|
9849
|
+
const exp = result.explanation?.find(e => e.dir === r);
|
|
9850
|
+
console.log(` ${i + 1}. ${r.padEnd(20)} ${exp ? 'score ' + exp.score : ''}`);
|
|
9851
|
+
});
|
|
9852
|
+
console.log('\nMonorepo:', result.isMonorepo ? 'yes' : 'no');
|
|
9853
|
+
console.log('Confidence:', result.confidence);
|
|
9854
|
+
process.exit(0);
|
|
9855
|
+
}
|
|
9856
|
+
|
|
9800
9857
|
// Feature 1: `sigmap explain <file>` — why a file is included or excluded
|
|
9801
9858
|
if (args[0] === 'explain' || args.includes('--explain')) {
|
|
9802
9859
|
const target = args[0] === 'explain'
|
package/package.json
CHANGED
package/src/config/loader.js
CHANGED
|
@@ -89,14 +89,38 @@ const SUPPORTED_CODE_EXTS = new Set([
|
|
|
89
89
|
]);
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
* Detect source directories for the given project root
|
|
93
|
-
*
|
|
92
|
+
* Detect source directories for the given project root.
|
|
93
|
+
* Uses smart resolver (v6.5+) with fallback to legacy heuristics.
|
|
94
94
|
*
|
|
95
95
|
* @param {string} cwd - Project root
|
|
96
96
|
* @param {string[]} excludeList - Folders to skip
|
|
97
97
|
* @returns {string[]}
|
|
98
98
|
*/
|
|
99
99
|
function detectAutoSrcDirs(cwd, excludeList) {
|
|
100
|
+
try {
|
|
101
|
+
const { resolveSourceRoots } = require('../discovery/source-root-resolver');
|
|
102
|
+
const result = resolveSourceRoots(cwd, { exclude: excludeList || [] });
|
|
103
|
+
if (result.roots.length > 0) {
|
|
104
|
+
if (result.confidence === 'low') {
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
'[sigmap] low confidence root detection — run "sigmap roots --explain" to verify\n'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return result.roots;
|
|
110
|
+
}
|
|
111
|
+
} catch (_) {}
|
|
112
|
+
|
|
113
|
+
return _legacyDetectAutoSrcDirs(cwd, excludeList);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Legacy source directory detection (fallback).
|
|
118
|
+
*
|
|
119
|
+
* @param {string} cwd - Project root
|
|
120
|
+
* @param {string[]} excludeList - Folders to skip
|
|
121
|
+
* @returns {string[]}
|
|
122
|
+
*/
|
|
123
|
+
function _legacyDetectAutoSrcDirs(cwd, excludeList) {
|
|
100
124
|
const excludeSet = new Set(excludeList || []);
|
|
101
125
|
const candidates = new Set(DEFAULTS.srcDirs);
|
|
102
126
|
|
|
@@ -0,0 +1,88 @@
|
|
|
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 = { detectFrameworks };
|
|
8
|
+
|
|
9
|
+
function detectFrameworks(cwd) {
|
|
10
|
+
const detected = [];
|
|
11
|
+
|
|
12
|
+
for (const [lang, reg] of Object.entries(REGISTRY)) {
|
|
13
|
+
if (!reg.frameworks) continue;
|
|
14
|
+
for (const [name, fw] of Object.entries(reg.frameworks)) {
|
|
15
|
+
let confidence = 0;
|
|
16
|
+
|
|
17
|
+
// Detection files: +0.95 / 0.93 / 0.90 depending on specificity
|
|
18
|
+
for (const f of (fw.detectionFiles || [])) {
|
|
19
|
+
if (_existsAnywhere(cwd, f, 3)) { confidence = Math.max(confidence, 0.93); }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Detection deps in package.json
|
|
23
|
+
if (fw.detectionDeps?.length) {
|
|
24
|
+
const deps = _readDeps(cwd);
|
|
25
|
+
for (const dep of fw.detectionDeps) {
|
|
26
|
+
if (deps.has(dep)) { confidence = Math.max(confidence, 0.90); }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// go.mod and Cargo.toml deps
|
|
31
|
+
if (lang === 'go' && fw.detectionDeps?.length) {
|
|
32
|
+
const goMod = _readFile(path.join(cwd, 'go.mod'));
|
|
33
|
+
for (const dep of fw.detectionDeps) {
|
|
34
|
+
if (goMod.includes(dep)) { confidence = Math.max(confidence, 0.90); }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (lang === 'rust' && fw.detectionDeps?.length) {
|
|
38
|
+
const cargoToml = _readFile(path.join(cwd, 'Cargo.toml'));
|
|
39
|
+
for (const dep of fw.detectionDeps) {
|
|
40
|
+
if (cargoToml.includes(dep)) { confidence = Math.max(confidence, 0.88); }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Special rules
|
|
45
|
+
if (fw.specialRule === 'django-app-dirs' && fs.existsSync(path.join(cwd, 'manage.py'))) {
|
|
46
|
+
confidence = Math.max(confidence, 0.95);
|
|
47
|
+
}
|
|
48
|
+
if (fw.specialRule === 'swift-project-dir' && _existsAnywhere(cwd, '.xcodeproj', 2)) {
|
|
49
|
+
confidence = Math.max(confidence, 0.90);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (confidence > 0) detected.push({ name, language: lang, confidence });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return detected.sort((a, b) => b.confidence - a.confidence);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _readDeps(cwd) {
|
|
60
|
+
try {
|
|
61
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
62
|
+
return new Set([...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})]);
|
|
63
|
+
} catch { return new Set(); }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _readFile(p) {
|
|
67
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _existsAnywhere(cwd, filename, maxDepth) {
|
|
71
|
+
const parts = filename.split('/');
|
|
72
|
+
if (parts.length > 1) return fs.existsSync(path.join(cwd, filename));
|
|
73
|
+
return _walkFind(cwd, filename, maxDepth);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _walkFind(dir, name, depth) {
|
|
77
|
+
if (depth <= 0) return false;
|
|
78
|
+
try {
|
|
79
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
80
|
+
for (const e of entries) {
|
|
81
|
+
if (e.name === name) return true;
|
|
82
|
+
if (e.isDirectory() && depth > 1) {
|
|
83
|
+
if (_walkFind(path.join(dir, e.name), name, depth - 1)) return true;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (_) {}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
@@ -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