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 CHANGED
@@ -47,16 +47,24 @@ 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)
50
+ ## SigMap commands
51
+
52
+ | When | Command |
53
+ |------|---------|
54
+ | Before answering a question | `sigmap ask "<your question>"` |
55
+ | After code changes | `sigmap validate` |
56
+ | To query by topic | `sigmap --query "<topic>"` |
57
+
58
+ Always run `sigmap ask` or `sigmap --query` before searching for files relevant to a task.
59
+ ## changes (last 5 commits — 6 minutes ago)
51
60
  ```
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
61
+ src/config/loader.js +_legacyDetectAutoSrcDirs ~detectAutoSrcDirs
62
+ src/discovery/language-detector.js +detectLanguages +_walkDepth
63
+ src/discovery/framework-detector.js +detectFrameworks +_readDeps +_readFile +_existsAnywhere
64
+ src/discovery/source-root-resolver.js +resolveSourceRoots +_detectMonorepo +_enumerateCandidates +_applySpecialRules
65
+ src/discovery/source-root-scorer.js +getRecentlyChangedDirs +scoreCandidate +_countSourceFiles
66
+ src/discovery/sigmapignore.js +loadIgnorePatterns +matchesIgnorePattern
67
+ src/retrieval/ranker.js +_computePenalty ~scoreFile ~rank ~buildSigIndex
60
68
  ```
61
69
 
62
70
  ## packages
@@ -116,21 +124,21 @@ function score(cwd) → { * score: number, * grad
116
124
  function adapt(context, adapterName, opts = {}) → string
117
125
  ```
118
126
 
119
- ### packages/adapters/codex.js
127
+ ### packages/adapters/copilot.js
120
128
  ```
121
129
  module.exports = { name, format, outputPath, write }
122
130
  function format(context, opts = {}) → string
131
+ function _confidenceMeta(opts)
123
132
  function outputPath(cwd) → string
124
133
  function write(context, cwd, opts = {})
125
134
  ```
126
135
 
127
- ### packages/adapters/claude.js
136
+ ### packages/adapters/cursor.js
128
137
  ```
129
- module.exports = { name, format, outputPath, write }
138
+ module.exports = { name, format, outputPath }
130
139
  function format(context, opts = {}) → string
131
140
  function _confidenceMeta(opts)
132
141
  function outputPath(cwd) → string
133
- function write(context, cwd, opts = {})
134
142
  ```
135
143
 
136
144
  ### packages/adapters/gemini.js
@@ -142,16 +150,15 @@ function write(context, cwd, opts = {})
142
150
  function _confidenceMeta(opts)
143
151
  ```
144
152
 
145
- ### packages/adapters/copilot.js
153
+ ### packages/adapters/openai.js
146
154
  ```
147
- module.exports = { name, format, outputPath, write }
155
+ module.exports = { name, format, outputPath }
148
156
  function format(context, opts = {}) → string
149
- function _confidenceMeta(opts)
150
157
  function outputPath(cwd) → string
151
- function write(context, cwd, opts = {})
158
+ function _confidenceMeta(opts)
152
159
  ```
153
160
 
154
- ### packages/adapters/cursor.js
161
+ ### packages/adapters/windsurf.js
155
162
  ```
156
163
  module.exports = { name, format, outputPath }
157
164
  function format(context, opts = {}) → string
@@ -159,91 +166,25 @@ function _confidenceMeta(opts)
159
166
  function outputPath(cwd) → string
160
167
  ```
161
168
 
162
- ### packages/adapters/openai.js
169
+ ### packages/adapters/codex.js
163
170
  ```
164
- module.exports = { name, format, outputPath }
171
+ module.exports = { name, format, outputPath, write }
165
172
  function format(context, opts = {}) → string
166
173
  function outputPath(cwd) → string
167
- function _confidenceMeta(opts)
174
+ function write(context, cwd, opts = {})
168
175
  ```
169
176
 
170
- ### packages/adapters/windsurf.js
177
+ ### packages/adapters/claude.js
171
178
  ```
172
- module.exports = { name, format, outputPath }
179
+ module.exports = { name, format, outputPath, write }
173
180
  function format(context, opts = {}) → string
174
181
  function _confidenceMeta(opts)
175
182
  function outputPath(cwd) → string
183
+ function write(context, cwd, opts = {})
176
184
  ```
177
185
 
178
186
  ## src
179
187
 
180
- ### src/security/patterns.js
181
- ```
182
- module.exports = { PATTERNS }
183
- ```
184
-
185
- ### src/security/scanner.js
186
- ```
187
- module.exports = { scan }
188
- function scan(signatures, filePath) → { safe: string[], redacte
189
- ```
190
-
191
- ### src/extractors/cpp.js
192
- ```
193
- module.exports = { extract }
194
- function extract(src) → string[]
195
- function extractBlock(src, startIndex)
196
- function extractMembers(block)
197
- function normalizeParams(params)
198
- function normalizeType(type)
199
- ```
200
-
201
- ### src/extractors/csharp.js
202
- ```
203
- module.exports = { extract }
204
- function extract(src) → string[]
205
- function extractBlock(src, startIndex)
206
- function extractMembers(block)
207
- function normalizeParams(params)
208
- function normalizeType(type)
209
- ```
210
-
211
- ### src/extractors/dart.js
212
- ```
213
- module.exports = { extract }
214
- function extract(src) → string[]
215
- function extractBlock(src, startIndex)
216
- function extractMembers(block)
217
- function normalizeParams(params)
218
- ```
219
-
220
- ### src/extractors/deps.js
221
- ```
222
- module.exports = { extractPythonDeps, extractTSDeps, buildReverseDepMap }
223
- function extractPythonDeps(src) → string[]
224
- function extractTSDeps(src) → string[]
225
- function buildReverseDepMap(forwardMap) → Map<string, string[]>
226
- ```
227
-
228
- ### src/extractors/go.js
229
- ```
230
- module.exports = { extract }
231
- function extract(src) → string[]
232
- function extractBlock(src, startIndex)
233
- function extractInterfaceMethods(block)
234
- function normalizeParams(params)
235
- ```
236
-
237
- ### src/extractors/java.js
238
- ```
239
- module.exports = { extract }
240
- function extract(src) → string[]
241
- function extractBlock(src, startIndex)
242
- function extractMembers(block)
243
- function normalizeParams(params)
244
- function normalizeType(type)
245
- ```
246
-
247
188
  ### src/extractors/javascript.js
248
189
  ```
249
190
  module.exports = { extract }
@@ -547,15 +488,6 @@ function formatAnalysisJSON(stats) → object
547
488
  module.exports = { DEFAULTS }
548
489
  ```
549
490
 
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
491
  ### src/format/dashboard.js
560
492
  ```
561
493
  module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak }
@@ -643,18 +575,6 @@ function queryContext(args, cwd)
643
575
  function getImpact(args, cwd)
644
576
  ```
645
577
 
646
- ### src/retrieval/ranker.js
647
- ```
648
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent }
649
- function scoreFile(filePath, sigs, queryTokens, weights) → number
650
- function rank(query, sigIndex, opts) → { file: string, score: nu
651
- function _parseContextFile(contextPath) → Map<string, string[]>
652
- function buildSigIndex(cwd, opts) → Map<string, string[]>
653
- function formatRankTable(results, query) → string
654
- function formatRankJSON(results, query) → object
655
- function detectIntent(query)
656
- ```
657
-
658
578
  ### src/tracking/logger.js
659
579
  ```
660
580
  module.exports = { logRun, readLog, summarize }
@@ -690,6 +610,77 @@ function exportWeights(cwd, outputPath)
690
610
  function importWeights(cwd, importPath, replace)
691
611
  ```
692
612
 
613
+ ### src/config/loader.js
614
+ ```
615
+ module.exports = { loadConfig, loadBaseConfig }
616
+ function loadBaseConfig(extendsVal, cwd)
617
+ function detectAutoSrcDirs(cwd, excludeList) → string[]
618
+ function _legacyDetectAutoSrcDirs(cwd, excludeList) → string[]
619
+ function loadConfig(cwd) → object
620
+ function deepClone(obj)
621
+ ```
622
+
623
+ ### src/discovery/language-detector.js
624
+ ```
625
+ module.exports = { detectLanguages }
626
+ function detectLanguages(cwd)
627
+ function _walkDepth(dir, depth, extCount)
628
+ ```
629
+
630
+ ### src/discovery/framework-detector.js
631
+ ```
632
+ module.exports = { detectFrameworks }
633
+ function detectFrameworks(cwd)
634
+ function _readDeps(cwd)
635
+ function _readFile(p)
636
+ function _existsAnywhere(cwd, filename, maxDepth)
637
+ function _walkFind(dir, name, depth)
638
+ ```
639
+
640
+ ### src/discovery/source-root-registry.js
641
+ ```
642
+ module.exports = { REGISTRY }
643
+ ```
644
+
645
+ ### src/discovery/source-root-resolver.js
646
+ ```
647
+ module.exports = { resolveSourceRoots }
648
+ function resolveSourceRoots(cwd, opts = {})
649
+ function _detectMonorepo(cwd)
650
+ function _enumerateCandidates(cwd, isMonorepo, ignorePatterns, excludeList)
651
+ function _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks)
652
+ function _dedupeNested(scored)
653
+ function _computeConfidence(frameworks, languages, scoredCount)
654
+ ```
655
+
656
+ ### src/discovery/source-root-scorer.js
657
+ ```
658
+ module.exports = { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS }
659
+ function getRecentlyChangedDirs(cwd)
660
+ function scoreCandidate(dirName, fullPath, context)
661
+ function _countSourceFiles(dir, depth)
662
+ ```
663
+
664
+ ### src/discovery/sigmapignore.js
665
+ ```
666
+ module.exports = { loadIgnorePatterns, matchesIgnorePattern }
667
+ function loadIgnorePatterns(cwd)
668
+ function matchesIgnorePattern(dirName, patterns)
669
+ ```
670
+
671
+ ### src/retrieval/ranker.js
672
+ ```
673
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent }
674
+ function _computePenalty(filePath)
675
+ function scoreFile(filePath, sigs, queryTokens, weights) → { score: number, signals:
676
+ function rank(query, sigIndex, opts) → { file: string, score: nu
677
+ function _parseContextFile(contextPath) → Map<string, string[]>
678
+ function buildSigIndex(cwd, opts) → Map<string, string[]>
679
+ function formatRankTable(results, query) → string
680
+ function formatRankJSON(results, query) → object
681
+ function detectIntent(query)
682
+ ```
683
+
693
684
  ### src/mcp/server.js
694
685
  ```
695
686
  module.exports = { start }
package/CHANGELOG.md CHANGED
@@ -10,6 +10,38 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [6.5.1] — 2026-04-25
14
+
15
+ ### Added
16
+
17
+ - **Retrieval explain** — `rank()` and `scoreFile()` now return detailed signal breakdown (exactToken, symbolMatch, prefixMatch, pathMatch, penalty) for transparency in ranking decisions
18
+ - **7-intent ranking** — expanded intent detection from 4 to 7 patterns (debug, explain, refactor, review, test, integrate, navigate). Each intent applies tuned weights to prioritize relevant signals.
19
+ - **Negative-signal penalty layer** — formalized penalties for test files (0.4x), generated code (0.3x), documentation (0.2x), and node_modules (0.0x) to deprioritize non-source content
20
+
21
+ ### Changed
22
+
23
+ - `formatRankTable` now shows penalty column and signals breakdown for top 3 results
24
+ - `formatRankJSON` now includes `intent` and `signals` fields in output for API consumers
25
+
26
+ ---
27
+
28
+ ## [6.5.0] — 2026-04-25
29
+
30
+ ### Added
31
+
32
+ - **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.
33
+ - **`.sigmapignore` pattern matching** — new `.sigmapignore` file support (fallback to `.contextignore`) for excluding directories. Supports simple patterns like `legacy/` and globs like `src/**`.
34
+ - **`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).
35
+ - **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.
36
+
37
+ ### Fixed
38
+
39
+ - **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).
40
+ - **Scoring penalty for framework srcDirs** — test directories (spec, test, tests) no longer penalized when explicitly in framework's srcDirs list.
41
+ - **CLI command ordering** — `roots` command handler now executes before `explain` to prevent flag conflict.
42
+
43
+ ---
44
+
13
45
  ## [6.4.0] — 2026-04-23
14
46
 
15
47
  ### Changed
package/README.md CHANGED
@@ -7,6 +7,7 @@
7
7
  **SigMap finds the right files before your AI answers.**
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/sigmap?color=7c6af7&label=latest&logo=npm)](https://www.npmjs.com/package/sigmap)
10
+ [![npm downloads](https://img.shields.io/npm/dt/sigmap?color=22c55e&label=downloads&logo=npm)](https://www.npmjs.com/package/sigmap)
10
11
  [![CI](https://github.com/manojmallick/sigmap/actions/workflows/ci.yml/badge.svg)](https://github.com/manojmallick/sigmap/actions/workflows/ci.yml)
11
12
  [![Zero deps](https://img.shields.io/badge/dependencies-zero-22c55e)](package.json)
12
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-7c6af7.svg)](LICENSE)
@@ -37,10 +38,10 @@ Works with Copilot, Claude, Cursor, Windsurf, and any LLM.
37
38
 
38
39
  ## Why SigMap?
39
40
 
40
- - **80.0% hit@5** — right file found in top 5 results (vs 13.6% baseline)
41
+ - **81.1% 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.68 prompts per task** — down from 2.84
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 — 80% of the time |
54
+ | ❌ Guessing which files are relevant | ✅ Right file in context — 81% 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.0-main
78
- Date : 2026-04-19
78
+ Benchmark : sigmap-v6.5-main
79
+ Date : 2026-04-25
79
80
 
80
- Hit@5 : 80.0% (baseline 13.6% — 5.8× lift)
81
- Prompt reduction : 40.8%
81
+ Hit@5 : 81.1% (baseline 13.6% — 6.0× lift)
82
+ Prompt reduction : 41.4%
82
83
  Task success : 52.2% (baseline 10%)
83
- Prompts / task : 1.68 (baseline 2.84)
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.4.0',
5390
+ version: '6.5.1',
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.4.0';
7225
+ const VERSION = '6.5.1';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.4.0",
3
+ "version": "6.5.1",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "6.4.0",
3
+ "version": "6.5.1",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "6.4.0",
3
+ "version": "6.5.1",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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 by reading manifest
93
- * files and scanning top-level directories for code files.
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
+ }