sigmap 6.5.1 → 6.6.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 +54 -61
- package/CHANGELOG.md +21 -0
- package/gen-context.js +712 -4
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/config/defaults.js +3 -0
- package/src/mcp/server.js +1 -1
- package/src/plan/planner.js +65 -0
- package/src/retrieval/ranker.js +61 -7
- package/src/session/memory.js +56 -0
package/AGENTS.md
CHANGED
|
@@ -56,15 +56,11 @@ Use this marker block for all appendable context files:
|
|
|
56
56
|
| To query by topic | `sigmap --query "<topic>"` |
|
|
57
57
|
|
|
58
58
|
Always run `sigmap ask` or `sigmap --query` before searching for files relevant to a task.
|
|
59
|
-
## changes (last 5 commits —
|
|
59
|
+
## changes (last 5 commits — 8 minutes ago)
|
|
60
60
|
```
|
|
61
|
-
src/
|
|
62
|
-
src/
|
|
63
|
-
src/
|
|
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
|
|
61
|
+
src/plan/planner.js +createPlan
|
|
62
|
+
src/retrieval/ranker.js +_computePenalty +_computeHubs +_isHub ~scoreFile
|
|
63
|
+
src/session/memory.js +sessionPath +loadSession +saveSession +mergeSessionContext
|
|
68
64
|
```
|
|
69
65
|
|
|
70
66
|
## packages
|
|
@@ -166,46 +162,25 @@ function _confidenceMeta(opts)
|
|
|
166
162
|
function outputPath(cwd) → string
|
|
167
163
|
```
|
|
168
164
|
|
|
169
|
-
### packages/adapters/
|
|
165
|
+
### packages/adapters/claude.js
|
|
170
166
|
```
|
|
171
167
|
module.exports = { name, format, outputPath, write }
|
|
172
168
|
function format(context, opts = {}) → string
|
|
169
|
+
function _confidenceMeta(opts)
|
|
173
170
|
function outputPath(cwd) → string
|
|
174
171
|
function write(context, cwd, opts = {})
|
|
175
172
|
```
|
|
176
173
|
|
|
177
|
-
### packages/adapters/
|
|
174
|
+
### packages/adapters/codex.js
|
|
178
175
|
```
|
|
179
176
|
module.exports = { name, format, outputPath, write }
|
|
180
177
|
function format(context, opts = {}) → string
|
|
181
|
-
function _confidenceMeta(opts)
|
|
182
178
|
function outputPath(cwd) → string
|
|
183
179
|
function write(context, cwd, opts = {})
|
|
184
180
|
```
|
|
185
181
|
|
|
186
182
|
## src
|
|
187
183
|
|
|
188
|
-
### src/extractors/javascript.js
|
|
189
|
-
```
|
|
190
|
-
module.exports = { extract }
|
|
191
|
-
function extract(src) → string[]
|
|
192
|
-
function extractBlock(src, startIndex)
|
|
193
|
-
function extractClassMembers(block, returnHints)
|
|
194
|
-
function buildReturnHints(src)
|
|
195
|
-
function normalizeType(type)
|
|
196
|
-
function formatReturnHint(type)
|
|
197
|
-
function normalizeParams(params)
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### src/extractors/kotlin.js
|
|
201
|
-
```
|
|
202
|
-
module.exports = { extract }
|
|
203
|
-
function extract(src) → string[]
|
|
204
|
-
function extractBlock(src, startIndex)
|
|
205
|
-
function extractMembers(block)
|
|
206
|
-
function normalizeParams(params)
|
|
207
|
-
```
|
|
208
|
-
|
|
209
184
|
### src/extractors/php.js
|
|
210
185
|
```
|
|
211
186
|
module.exports = { extract }
|
|
@@ -483,11 +458,6 @@ function formatAnalysisTable(stats, showSlow) → string
|
|
|
483
458
|
function formatAnalysisJSON(stats) → object
|
|
484
459
|
```
|
|
485
460
|
|
|
486
|
-
### src/config/defaults.js
|
|
487
|
-
```
|
|
488
|
-
module.exports = { DEFAULTS }
|
|
489
|
-
```
|
|
490
|
-
|
|
491
461
|
### src/format/dashboard.js
|
|
492
462
|
```
|
|
493
463
|
module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak }
|
|
@@ -610,6 +580,11 @@ function exportWeights(cwd, outputPath)
|
|
|
610
580
|
function importWeights(cwd, importPath, replace)
|
|
611
581
|
```
|
|
612
582
|
|
|
583
|
+
### src/config/defaults.js
|
|
584
|
+
```
|
|
585
|
+
module.exports = { DEFAULTS }
|
|
586
|
+
```
|
|
587
|
+
|
|
613
588
|
### src/config/loader.js
|
|
614
589
|
```
|
|
615
590
|
module.exports = { loadConfig, loadBaseConfig }
|
|
@@ -620,23 +595,6 @@ function loadConfig(cwd) → object
|
|
|
620
595
|
function deepClone(obj)
|
|
621
596
|
```
|
|
622
597
|
|
|
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
598
|
### src/discovery/source-root-registry.js
|
|
641
599
|
```
|
|
642
600
|
module.exports = { REGISTRY }
|
|
@@ -653,6 +611,13 @@ function _dedupeNested(scored)
|
|
|
653
611
|
function _computeConfidence(frameworks, languages, scoredCount)
|
|
654
612
|
```
|
|
655
613
|
|
|
614
|
+
### src/discovery/language-detector.js
|
|
615
|
+
```
|
|
616
|
+
module.exports = { detectLanguages }
|
|
617
|
+
function detectLanguages(cwd)
|
|
618
|
+
function _walkDepth(dir, depth, extCount)
|
|
619
|
+
```
|
|
620
|
+
|
|
656
621
|
### src/discovery/source-root-scorer.js
|
|
657
622
|
```
|
|
658
623
|
module.exports = { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS }
|
|
@@ -661,6 +626,16 @@ function scoreCandidate(dirName, fullPath, context)
|
|
|
661
626
|
function _countSourceFiles(dir, depth)
|
|
662
627
|
```
|
|
663
628
|
|
|
629
|
+
### src/discovery/framework-detector.js
|
|
630
|
+
```
|
|
631
|
+
module.exports = { detectFrameworks }
|
|
632
|
+
function detectFrameworks(cwd)
|
|
633
|
+
function _readDeps(cwd)
|
|
634
|
+
function _readFile(p)
|
|
635
|
+
function _existsAnywhere(cwd, filename, maxDepth)
|
|
636
|
+
function _walkFind(dir, name, depth)
|
|
637
|
+
```
|
|
638
|
+
|
|
664
639
|
### src/discovery/sigmapignore.js
|
|
665
640
|
```
|
|
666
641
|
module.exports = { loadIgnorePatterns, matchesIgnorePattern }
|
|
@@ -668,10 +643,27 @@ function loadIgnorePatterns(cwd)
|
|
|
668
643
|
function matchesIgnorePattern(dirName, patterns)
|
|
669
644
|
```
|
|
670
645
|
|
|
646
|
+
### src/plan/planner.js
|
|
647
|
+
```
|
|
648
|
+
module.exports = { createPlan }
|
|
649
|
+
function createPlan(goal, cwd, config)
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### src/mcp/server.js
|
|
653
|
+
```
|
|
654
|
+
module.exports = { start }
|
|
655
|
+
function respond(id, result)
|
|
656
|
+
function respondError(id, code, message)
|
|
657
|
+
function dispatch(msg, cwd)
|
|
658
|
+
function start(cwd)
|
|
659
|
+
```
|
|
660
|
+
|
|
671
661
|
### src/retrieval/ranker.js
|
|
672
662
|
```
|
|
673
|
-
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent }
|
|
663
|
+
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent }
|
|
674
664
|
function _computePenalty(filePath)
|
|
665
|
+
function _computeHubs(graph)
|
|
666
|
+
function _isHub(filePath)
|
|
675
667
|
function scoreFile(filePath, sigs, queryTokens, weights) → { score: number, signals:
|
|
676
668
|
function rank(query, sigIndex, opts) → { file: string, score: nu
|
|
677
669
|
function _parseContextFile(contextPath) → Map<string, string[]>
|
|
@@ -681,11 +673,12 @@ function formatRankJSON(results, query) → object
|
|
|
681
673
|
function detectIntent(query)
|
|
682
674
|
```
|
|
683
675
|
|
|
684
|
-
### src/
|
|
676
|
+
### src/session/memory.js
|
|
685
677
|
```
|
|
686
|
-
module.exports = {
|
|
687
|
-
function
|
|
688
|
-
function
|
|
689
|
-
function
|
|
690
|
-
function
|
|
678
|
+
module.exports = { loadSession, saveSession, mergeSessionContext, clearSession }
|
|
679
|
+
function sessionPath(cwd)
|
|
680
|
+
function loadSession(cwd)
|
|
681
|
+
function saveSession(cwd, { intent, topFiles, query })
|
|
682
|
+
function mergeSessionContext(scores, session, currentIntent)
|
|
683
|
+
function clearSession(cwd)
|
|
691
684
|
```
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,27 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [6.6.0] — 2026-04-27
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **Session memory** — Carry context across follow-up queries within a coding session. New `src/session/memory.js` module manages session state with 4-hour TTL. Previous session's top-5 files get +0.2 score boost in next query; boost reduced to +0.1 if intent differs (topic-switch guard).
|
|
18
|
+
- **`sigmap ask --followup`** — Reuse previous session's context when making follow-up queries. Session automatically saved after each `ask` command for seamless context carry-forward.
|
|
19
|
+
- **`sigmap plan "<goal>"`** — Analyze change impact and plan modifications. Returns files grouped by confidence (inspect-first vs likely-to-change), impact radius, and affected tests. Supports `--json` output for agent integration.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [6.5.2] — 2026-04-27
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **2-hop graph boost with decay** — `rank()` now traverses 2 hops in the dependency graph instead of 1. Direct imports (+0.40) and second-order imports (+0.15 with decay) receive score boosts for better context relevance in multi-layer dependency scenarios.
|
|
28
|
+
- **Hub suppression** — shared utility files (detected by >20% fanout threshold or static patterns like `util/`, `helper/`, `common/`) are now excluded from graph boosts to prevent over-boosting generic utilities.
|
|
29
|
+
- **Incremental signature cache (`sigCache`)** — new opt-in `sigCache: true` config key enables mtime-based caching of extracted signatures. Cache is automatically busted on version changes, and unchanged files skip re-extraction for faster subsequent runs.
|
|
30
|
+
- **Cache health statistics** — `--health` output now includes cache stats: entry count and disk size in KB. `--health --json` includes `cacheStats` field with `entries` and `sizeKb` when cache exists.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
13
34
|
## [6.5.1] — 2026-04-25
|
|
14
35
|
|
|
15
36
|
### Added
|
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.6.0',
|
|
5391
5391
|
description: 'SigMap MCP server — code signatures on demand',
|
|
5392
5392
|
};
|
|
5393
5393
|
|
|
@@ -6211,6 +6211,62 @@ __factories["./src/tracking/logger"] = function(module, exports) {
|
|
|
6211
6211
|
|
|
6212
6212
|
};
|
|
6213
6213
|
|
|
6214
|
+
// ── ./src/session/memory ──
|
|
6215
|
+
__factories["./src/session/memory"] = function(module, exports) {
|
|
6216
|
+
'use strict';
|
|
6217
|
+
|
|
6218
|
+
const fs = require('fs');
|
|
6219
|
+
const path = require('path');
|
|
6220
|
+
|
|
6221
|
+
const SESSION_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours — one coding session
|
|
6222
|
+
|
|
6223
|
+
function sessionPath(cwd) {
|
|
6224
|
+
return path.join(cwd, '.context', 'session.json');
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
function loadSession(cwd) {
|
|
6228
|
+
const p = sessionPath(cwd);
|
|
6229
|
+
if (!fs.existsSync(p)) return null;
|
|
6230
|
+
try {
|
|
6231
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
6232
|
+
if (Date.now() - raw.ts > SESSION_TTL_MS) return null; // expired
|
|
6233
|
+
return raw;
|
|
6234
|
+
} catch {
|
|
6235
|
+
return null;
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
|
|
6239
|
+
function saveSession(cwd, { intent, topFiles, query }) {
|
|
6240
|
+
const p = sessionPath(cwd);
|
|
6241
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
6242
|
+
fs.writeFileSync(p, JSON.stringify({
|
|
6243
|
+
ts: Date.now(),
|
|
6244
|
+
intent,
|
|
6245
|
+
topFiles, // [{ file, score }] — top 5 from last ask
|
|
6246
|
+
lastQuery: query,
|
|
6247
|
+
}));
|
|
6248
|
+
}
|
|
6249
|
+
|
|
6250
|
+
function mergeSessionContext(scores, session, currentIntent) {
|
|
6251
|
+
if (!session) return scores;
|
|
6252
|
+
|
|
6253
|
+
const boostAmount = session.intent === currentIntent ? 0.20 : 0.10;
|
|
6254
|
+
const sessionBoost = new Map(session.topFiles.map(f => [f.file, boostAmount]));
|
|
6255
|
+
|
|
6256
|
+
return scores.map(r => ({
|
|
6257
|
+
...r,
|
|
6258
|
+
score: r.score + (sessionBoost.get(r.file) || 0),
|
|
6259
|
+
}));
|
|
6260
|
+
}
|
|
6261
|
+
|
|
6262
|
+
function clearSession(cwd) {
|
|
6263
|
+
const p = sessionPath(cwd);
|
|
6264
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
6265
|
+
}
|
|
6266
|
+
|
|
6267
|
+
module.exports = { loadSession, saveSession, mergeSessionContext, clearSession };
|
|
6268
|
+
};
|
|
6269
|
+
|
|
6214
6270
|
// ── ./src/retrieval/tokenizer ──
|
|
6215
6271
|
__factories["./src/retrieval/tokenizer"] = function(module, exports) {
|
|
6216
6272
|
'use strict';
|
|
@@ -7210,6 +7266,521 @@ __factories["./packages/adapters/llm-full"] = function(module, exports) {
|
|
|
7210
7266
|
module.exports = { name: 'llm-full', format, outputPath, write };
|
|
7211
7267
|
};
|
|
7212
7268
|
|
|
7269
|
+
// ── ./src/discovery/source-root-registry ──
|
|
7270
|
+
__factories["./src/discovery/source-root-registry"] = function(module, exports) {
|
|
7271
|
+
'use strict';
|
|
7272
|
+
const REGISTRY = {
|
|
7273
|
+
javascript: {
|
|
7274
|
+
manifestFiles: ['package.json'],
|
|
7275
|
+
frameworks: {
|
|
7276
|
+
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'] },
|
|
7277
|
+
nestjs: { detectionFiles: ['nest-cli.json'], detectionDeps: ['@nestjs/core'], srcDirs: ['src'], entrypoints: ['src/main.ts','src/app.module.ts'] },
|
|
7278
|
+
express: { detectionFiles: [], detectionDeps: ['express'], srcDirs: ['src','routes','middleware','controllers','services'], entrypoints: ['src/index.js','server.js','app.js'] },
|
|
7279
|
+
fastify: { detectionFiles: [], detectionDeps: ['fastify'], srcDirs: ['src','routes','plugins'], entrypoints: ['src/index.js'] },
|
|
7280
|
+
react: { detectionFiles: [], detectionDeps: ['react'], srcDirs: ['src','components','hooks','context','pages','app','lib','utils'] },
|
|
7281
|
+
vue: { detectionFiles: ['vue.config.js','vue.config.ts'], detectionDeps: ['vue'], srcDirs: ['src','components','composables','pages','views'] },
|
|
7282
|
+
nuxt: { detectionFiles: ['nuxt.config.js','nuxt.config.ts'], detectionDeps: ['nuxt'], srcDirs: ['pages','components','composables','server','middleware','plugins'] },
|
|
7283
|
+
svelte: { detectionFiles: ['svelte.config.js'], detectionDeps: ['svelte','@sveltejs/kit'], srcDirs: ['src','src/routes','src/lib'] },
|
|
7284
|
+
angular: { detectionFiles: ['angular.json'], detectionDeps: ['@angular/core'], srcDirs: ['src','src/app','projects','apps','libs'] },
|
|
7285
|
+
gatsby: { detectionFiles: ['gatsby-config.js','gatsby-config.ts'], detectionDeps: ['gatsby'], srcDirs: ['src','gatsby'] },
|
|
7286
|
+
vite: { detectionFiles: ['vite.config.js','vite.config.ts'], detectionDeps: ['vite'], srcDirs: ['src'] },
|
|
7287
|
+
remix: { detectionFiles: ['remix.config.js'], detectionDeps: ['@remix-run/react'], srcDirs: ['app'] },
|
|
7288
|
+
trpc: { detectionFiles: [], detectionDeps: ['@trpc/server'], srcDirs: ['src','server','routers'] },
|
|
7289
|
+
},
|
|
7290
|
+
srcDirs: ['src','lib','index.js','server.js','app.js'],
|
|
7291
|
+
penalties: ['dist','build','.next','.nuxt','coverage','storybook-static'],
|
|
7292
|
+
},
|
|
7293
|
+
typescript: {
|
|
7294
|
+
manifestFiles: ['package.json','tsconfig.json'],
|
|
7295
|
+
frameworks: {
|
|
7296
|
+
nextjs: { detectionFiles: ['next.config.ts','next.config.mjs'], detectionDeps: ['next'], srcDirs: ['app','src/app','pages','src','components','lib','hooks','utils'] },
|
|
7297
|
+
nestjs: { detectionFiles: ['nest-cli.json'], detectionDeps: ['@nestjs/core'], srcDirs: ['src'], entrypoints: ['src/main.ts'] },
|
|
7298
|
+
angular: { detectionFiles: ['angular.json'], detectionDeps: ['@angular/core'], srcDirs: ['src','src/app','projects','apps','libs'] },
|
|
7299
|
+
},
|
|
7300
|
+
srcDirs: ['src','lib','packages'],
|
|
7301
|
+
penalties: ['dist','build','.next'],
|
|
7302
|
+
},
|
|
7303
|
+
python: {
|
|
7304
|
+
manifestFiles: ['requirements.txt','pyproject.toml','setup.py','Pipfile'],
|
|
7305
|
+
frameworks: {
|
|
7306
|
+
django: { detectionFiles: ['manage.py'], detectionDeps: ['Django'], srcDirs: [], specialRule: 'django-app-dirs', entrypoints: ['manage.py'] },
|
|
7307
|
+
fastapi: { detectionFiles: [], detectionDeps: ['fastapi'], srcDirs: ['app','src','routers','api'], entrypoints: ['main.py','app/main.py'] },
|
|
7308
|
+
flask: { detectionFiles: ['wsgi.py','app.py'], detectionDeps: ['Flask'], srcDirs: ['app','src'], entrypoints: ['app.py','wsgi.py'] },
|
|
7309
|
+
celery: { detectionFiles: [], detectionDeps: ['celery'], srcDirs: ['tasks','workers','app'] },
|
|
7310
|
+
},
|
|
7311
|
+
srcDirs: ['.'],
|
|
7312
|
+
penalties: ['venv','.venv','__pycache__','.pytest_cache','htmlcov'],
|
|
7313
|
+
},
|
|
7314
|
+
go: {
|
|
7315
|
+
manifestFiles: ['go.mod'],
|
|
7316
|
+
frameworks: {
|
|
7317
|
+
gin: { detectionFiles: [], detectionDeps: ['github.com/gin-gonic/gin'], srcDirs: ['internal','cmd','pkg','api','handler','middleware'] },
|
|
7318
|
+
echo: { detectionFiles: [], detectionDeps: ['github.com/labstack/echo'], srcDirs: ['internal','cmd','handler','middleware'] },
|
|
7319
|
+
fiber: { detectionFiles: [], detectionDeps: ['github.com/gofiber/fiber'], srcDirs: ['internal','cmd','handler','routes'] },
|
|
7320
|
+
grpc: { detectionFiles: [], detectionDeps: ['google.golang.org/grpc'], srcDirs: ['internal','proto','server','client'] },
|
|
7321
|
+
chi: { detectionFiles: [], detectionDeps: ['github.com/go-chi/chi'], srcDirs: ['internal','cmd','handler'] },
|
|
7322
|
+
},
|
|
7323
|
+
srcDirs: ['internal','cmd','pkg','api'],
|
|
7324
|
+
penalties: ['vendor'],
|
|
7325
|
+
},
|
|
7326
|
+
rust: {
|
|
7327
|
+
manifestFiles: ['Cargo.toml'],
|
|
7328
|
+
frameworks: {
|
|
7329
|
+
actix: { detectionFiles: [], detectionDeps: ['actix-web'], srcDirs: ['src'] },
|
|
7330
|
+
axum: { detectionFiles: [], detectionDeps: ['axum'], srcDirs: ['src'] },
|
|
7331
|
+
tauri: { detectionFiles: ['src-tauri/tauri.conf.json'], detectionDeps: ['tauri'], srcDirs: ['src','src-tauri/src'] },
|
|
7332
|
+
},
|
|
7333
|
+
srcDirs: ['src'],
|
|
7334
|
+
penalties: ['target'],
|
|
7335
|
+
},
|
|
7336
|
+
java: {
|
|
7337
|
+
manifestFiles: ['pom.xml','build.gradle'],
|
|
7338
|
+
frameworks: {
|
|
7339
|
+
spring: { detectionFiles: [], detectionDeps: ['spring-boot'], srcDirs: ['src/main/java','src/main/kotlin','src/main/resources'] },
|
|
7340
|
+
quarkus: { detectionFiles: [], detectionDeps: ['io.quarkus'], srcDirs: ['src/main/java'] },
|
|
7341
|
+
android: { detectionFiles: ['AndroidManifest.xml'], srcDirs: ['app/src/main/java','app/src/main','src'] },
|
|
7342
|
+
micronaut:{ detectionFiles: [], detectionDeps: ['io.micronaut'],srcDirs: ['src/main/java'] },
|
|
7343
|
+
},
|
|
7344
|
+
srcDirs: ['src/main/java','src'],
|
|
7345
|
+
penalties: ['target','build'],
|
|
7346
|
+
},
|
|
7347
|
+
kotlin: {
|
|
7348
|
+
manifestFiles: ['build.gradle.kts'],
|
|
7349
|
+
frameworks: {
|
|
7350
|
+
spring: { detectionFiles: [], detectionDeps: ['spring-boot'], srcDirs: ['src/main/kotlin'] },
|
|
7351
|
+
android: { detectionFiles: ['AndroidManifest.xml'], srcDirs: ['app/src/main/kotlin','app/src/main/java'] },
|
|
7352
|
+
ktor: { detectionFiles: [], detectionDeps: ['io.ktor'], srcDirs: ['src'] },
|
|
7353
|
+
compose: { detectionFiles: [], detectionDeps: ['compose-runtime'], srcDirs: ['app/src/main/kotlin','src'] },
|
|
7354
|
+
},
|
|
7355
|
+
srcDirs: ['src/main/kotlin','src'],
|
|
7356
|
+
penalties: ['build','.gradle'],
|
|
7357
|
+
},
|
|
7358
|
+
csharp: {
|
|
7359
|
+
manifestFiles: ['.csproj','.sln'],
|
|
7360
|
+
frameworks: {
|
|
7361
|
+
aspnet: { detectionFiles: ['appsettings.json'], detectionDeps: ['Microsoft.AspNetCore'], srcDirs: ['Controllers','Services','Models','Middleware','Pages'] },
|
|
7362
|
+
blazor: { detectionFiles: [], detectionDeps: ['Microsoft.AspNetCore.Components'], srcDirs: ['Components','Pages','Services'] },
|
|
7363
|
+
unity: { detectionFiles: ['ProjectSettings/ProjectSettings.asset'], srcDirs: ['Assets/Scripts','Assets'] },
|
|
7364
|
+
maui: { detectionFiles: [], detectionDeps: ['Microsoft.Maui'], srcDirs: ['src','Pages','ViewModels'] },
|
|
7365
|
+
},
|
|
7366
|
+
srcDirs: ['src','Controllers','Services','Models'],
|
|
7367
|
+
penalties: ['bin','obj','.vs'],
|
|
7368
|
+
},
|
|
7369
|
+
php: {
|
|
7370
|
+
manifestFiles: ['composer.json'],
|
|
7371
|
+
frameworks: {
|
|
7372
|
+
laravel: { detectionFiles: ['artisan'], srcDirs: ['app','routes','config','database','resources','tests'], entrypoints: ['artisan'] },
|
|
7373
|
+
symfony: { detectionFiles: ['symfony.lock'], srcDirs: ['src','config','templates'], specialRule: 'symfony-bundle-dirs' },
|
|
7374
|
+
wordpress: { detectionFiles: ['wp-config.php'], srcDirs: ['wp-content/themes','wp-content/plugins','wp-content/mu-plugins'] },
|
|
7375
|
+
slim: { detectionFiles: [], detectionDeps: ['slim/slim'], srcDirs: ['src','app','routes'] },
|
|
7376
|
+
},
|
|
7377
|
+
srcDirs: ['src','app'],
|
|
7378
|
+
penalties: ['vendor'],
|
|
7379
|
+
},
|
|
7380
|
+
ruby: {
|
|
7381
|
+
manifestFiles: ['Gemfile'],
|
|
7382
|
+
frameworks: {
|
|
7383
|
+
rails: { detectionFiles: ['config/routes.rb'], srcDirs: ['app','lib','config','db','spec','test'], entrypoints: ['config/routes.rb'] },
|
|
7384
|
+
sinatra: { detectionFiles: ['config.ru','app.rb'], srcDirs: ['.','lib'], entrypoints: ['app.rb','config.ru'] },
|
|
7385
|
+
hanami: { detectionFiles: [], detectionDeps: ['hanami'], srcDirs: ['apps','lib','slices'] },
|
|
7386
|
+
},
|
|
7387
|
+
srcDirs: ['app','lib'],
|
|
7388
|
+
penalties: ['vendor','coverage','.bundle'],
|
|
7389
|
+
},
|
|
7390
|
+
swift: {
|
|
7391
|
+
manifestFiles: ['Package.swift'],
|
|
7392
|
+
frameworks: {
|
|
7393
|
+
vapor: { detectionFiles: [], detectionDeps: ['vapor/vapor'], srcDirs: ['Sources','App'] },
|
|
7394
|
+
swiftui: { detectionFiles: ['.xcodeproj'], srcDirs: [], specialRule: 'swift-project-dir' },
|
|
7395
|
+
swiftpm: { detectionFiles: ['Package.swift'],srcDirs: ['Sources'] },
|
|
7396
|
+
},
|
|
7397
|
+
srcDirs: ['Sources','Source'],
|
|
7398
|
+
penalties: ['.build','DerivedData','Pods','Carthage'],
|
|
7399
|
+
},
|
|
7400
|
+
dart: {
|
|
7401
|
+
manifestFiles: ['pubspec.yaml'],
|
|
7402
|
+
frameworks: {
|
|
7403
|
+
flutter: { detectionFiles: [], detectionDeps: ['flutter'], srcDirs: ['lib','lib/src'], entrypoints: ['lib/main.dart'] },
|
|
7404
|
+
serverpod: { detectionFiles: [], detectionDeps: ['serverpod'], srcDirs: ['lib','endpoints','models'] },
|
|
7405
|
+
'dart-frog':{ detectionFiles: ['dart_frog.yaml'], srcDirs: ['routes','lib'] },
|
|
7406
|
+
},
|
|
7407
|
+
srcDirs: ['lib','lib/src'],
|
|
7408
|
+
penalties: ['.dart_tool','build'],
|
|
7409
|
+
},
|
|
7410
|
+
scala: {
|
|
7411
|
+
manifestFiles: ['build.sbt'],
|
|
7412
|
+
frameworks: {
|
|
7413
|
+
akka: { detectionFiles: [], detectionDeps: ['akka'], srcDirs: ['src/main/scala','src'] },
|
|
7414
|
+
play: { detectionFiles: [], detectionDeps: ['play'], srcDirs: ['app','conf'] },
|
|
7415
|
+
spark: { detectionFiles: [], detectionDeps: ['spark'],srcDirs: ['src/main/scala'] },
|
|
7416
|
+
zio: { detectionFiles: [], detectionDeps: ['zio'], srcDirs: ['src/main/scala'] },
|
|
7417
|
+
},
|
|
7418
|
+
srcDirs: ['src/main/scala','src'],
|
|
7419
|
+
penalties: ['target'],
|
|
7420
|
+
},
|
|
7421
|
+
};
|
|
7422
|
+
module.exports = { REGISTRY };
|
|
7423
|
+
};
|
|
7424
|
+
|
|
7425
|
+
// ── ./src/discovery/sigmapignore ──
|
|
7426
|
+
__factories["./src/discovery/sigmapignore"] = function(module, exports) {
|
|
7427
|
+
'use strict';
|
|
7428
|
+
const fs = require('fs');
|
|
7429
|
+
const path = require('path');
|
|
7430
|
+
function loadIgnorePatterns(cwd) {
|
|
7431
|
+
for (const fname of ['.sigmapignore', '.contextignore']) {
|
|
7432
|
+
const p = path.join(cwd, fname);
|
|
7433
|
+
if (fs.existsSync(p)) {
|
|
7434
|
+
return fs.readFileSync(p, 'utf8')
|
|
7435
|
+
.split('\n')
|
|
7436
|
+
.map(l => l.trim())
|
|
7437
|
+
.filter(l => l && !l.startsWith('#'));
|
|
7438
|
+
}
|
|
7439
|
+
}
|
|
7440
|
+
return [];
|
|
7441
|
+
}
|
|
7442
|
+
function matchesIgnorePattern(dirName, patterns) {
|
|
7443
|
+
for (const pat of patterns) {
|
|
7444
|
+
const clean = pat.replace(/\/$/, '');
|
|
7445
|
+
if (clean === dirName) return true;
|
|
7446
|
+
if (clean.endsWith('/**') && dirName.startsWith(clean.slice(0, -3))) return true;
|
|
7447
|
+
if (clean.endsWith('/*') && dirName.startsWith(clean.slice(0, -2))) return true;
|
|
7448
|
+
}
|
|
7449
|
+
return false;
|
|
7450
|
+
}
|
|
7451
|
+
module.exports = { loadIgnorePatterns, matchesIgnorePattern };
|
|
7452
|
+
};
|
|
7453
|
+
|
|
7454
|
+
// ── ./src/discovery/language-detector ──
|
|
7455
|
+
__factories["./src/discovery/language-detector"] = function(module, exports) {
|
|
7456
|
+
'use strict';
|
|
7457
|
+
const fs = require('fs');
|
|
7458
|
+
const path = require('path');
|
|
7459
|
+
const { REGISTRY } = __require('./src/discovery/source-root-registry');
|
|
7460
|
+
function detectLanguages(cwd) {
|
|
7461
|
+
const weights = {};
|
|
7462
|
+
for (const [lang, reg] of Object.entries(REGISTRY)) {
|
|
7463
|
+
for (const mf of (reg.manifestFiles || [])) {
|
|
7464
|
+
if (fs.existsSync(path.join(cwd, mf))) {
|
|
7465
|
+
weights[lang] = (weights[lang] || 0) + 3;
|
|
7466
|
+
}
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
try {
|
|
7470
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
7471
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
7472
|
+
if (allDeps.typescript) { weights.typescript = (weights.typescript || 0) + 2; }
|
|
7473
|
+
} catch (_) {}
|
|
7474
|
+
const extCount = {};
|
|
7475
|
+
(function _walkDepth(dir, depth, extCount) {
|
|
7476
|
+
if (depth <= 0) return;
|
|
7477
|
+
let entries;
|
|
7478
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
7479
|
+
for (const e of entries) {
|
|
7480
|
+
const SKIP_DIRS = new Set(['node_modules','dist','build','.git','.next','.nuxt','vendor','DerivedData','Pods','target','coverage','__pycache__','.venv','venv','.build','Carthage','storybook-static']);
|
|
7481
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
7482
|
+
if (e.isDirectory()) {
|
|
7483
|
+
_walkDepth(path.join(dir, e.name), depth - 1, extCount);
|
|
7484
|
+
} else if (e.isFile()) {
|
|
7485
|
+
const EXT_TO_LANG = {'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.jsx': 'javascript', '.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.swift': 'swift', '.dart': 'dart', '.scala': 'scala', '.php': 'php'};
|
|
7486
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
7487
|
+
if (EXT_TO_LANG[ext]) extCount[ext] = (extCount[ext] || 0) + 1;
|
|
7488
|
+
}
|
|
7489
|
+
}
|
|
7490
|
+
})(cwd, 3, extCount);
|
|
7491
|
+
const maxCount = Math.max(1, ...Object.values(extCount));
|
|
7492
|
+
const EXT_TO_LANG = {'.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.jsx': 'javascript', '.py': 'python', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.swift': 'swift', '.dart': 'dart', '.scala': 'scala', '.php': 'php'};
|
|
7493
|
+
for (const [ext, count] of Object.entries(extCount)) {
|
|
7494
|
+
const lang = EXT_TO_LANG[ext];
|
|
7495
|
+
if (lang) {
|
|
7496
|
+
weights[lang] = (weights[lang] || 0) + Math.min(5, (count / maxCount) * 5);
|
|
7497
|
+
}
|
|
7498
|
+
}
|
|
7499
|
+
const maxW = Math.max(1, ...Object.values(weights));
|
|
7500
|
+
return Object.entries(weights)
|
|
7501
|
+
.map(([name, w]) => ({ name, weight: Math.round(w / maxW * 100) / 100 }))
|
|
7502
|
+
.sort((a, b) => b.weight - a.weight);
|
|
7503
|
+
}
|
|
7504
|
+
module.exports = { detectLanguages };
|
|
7505
|
+
};
|
|
7506
|
+
|
|
7507
|
+
// ── ./src/discovery/source-root-scorer ──
|
|
7508
|
+
__factories["./src/discovery/source-root-scorer"] = function(module, exports) {
|
|
7509
|
+
'use strict';
|
|
7510
|
+
const fs = require('fs');
|
|
7511
|
+
const path = require('path');
|
|
7512
|
+
const { execSync } = require('child_process');
|
|
7513
|
+
const CODE_EXTS = new Set(['.js','.mjs','.cjs','.ts','.tsx','.jsx','.py','.rb','.go','.rs','.java','.kt','.cs','.cpp','.c','.h','.swift','.dart','.scala','.php']);
|
|
7514
|
+
const AUTO_SKIP = new Set(['node_modules','dist','build','.git','.next','.nuxt','vendor','DerivedData','Pods','target','coverage','__pycache__','.venv','venv','.build','Carthage','storybook-static','.gradle','bin','obj','.vs']);
|
|
7515
|
+
const PENALTY_DIRS = new Set(['test','tests','spec','__tests__','e2e','docs','doc','docs-vp','examples','example','fixtures','mocks','__mocks__','demo','samples','migrations','benchmarks','scripts']);
|
|
7516
|
+
const ROOT_ENTRYPOINTS = { go: ['main.go'], python: ['app.py','main.py','wsgi.py','asgi.py'], javascript: ['index.js','server.js','app.js'], typescript: ['index.ts','main.ts'], rust: [], php: ['index.php'] };
|
|
7517
|
+
function getRecentlyChangedDirs(cwd) {
|
|
7518
|
+
try {
|
|
7519
|
+
const out = execSync('git log --name-only --format="" HEAD~10 2>/dev/null', { cwd, timeout: 3000 }).toString();
|
|
7520
|
+
return new Set(out.split('\n').filter(Boolean).map(f => f.split('/')[0]));
|
|
7521
|
+
} catch { return new Set(); }
|
|
7522
|
+
}
|
|
7523
|
+
function scoreCandidate(dirName, fullPath, context) {
|
|
7524
|
+
const { frameworks, languages, recentDirs, frameworkSrcDirs, entrypoints, frameworkPenalties } = context;
|
|
7525
|
+
if (AUTO_SKIP.has(dirName)) return -99;
|
|
7526
|
+
if (!fs.existsSync(fullPath)) return -99;
|
|
7527
|
+
let score = 0;
|
|
7528
|
+
if (frameworkSrcDirs.has(dirName)) score += 3.0;
|
|
7529
|
+
const sourceFileCount = (function _countSourceFiles(dir, depth) {
|
|
7530
|
+
if (depth <= 0) return 0;
|
|
7531
|
+
let count = 0;
|
|
7532
|
+
try {
|
|
7533
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
7534
|
+
if (e.isFile() && CODE_EXTS.has(path.extname(e.name).toLowerCase())) count++;
|
|
7535
|
+
else if (e.isDirectory() && depth > 1) count += _countSourceFiles(path.join(dir, e.name), depth - 1);
|
|
7536
|
+
}
|
|
7537
|
+
} catch (_) {}
|
|
7538
|
+
return count;
|
|
7539
|
+
})(fullPath, 2);
|
|
7540
|
+
const density = Math.min(1.0, sourceFileCount / 10);
|
|
7541
|
+
score += density * 2.5;
|
|
7542
|
+
if (sourceFileCount >= 3) score += 2.0;
|
|
7543
|
+
if ((entrypoints || []).some(ep => ep.startsWith(dirName + '/'))) score += 1.5;
|
|
7544
|
+
if (fs.existsSync(path.join(fullPath, 'package.json')) ||
|
|
7545
|
+
fs.existsSync(path.join(fullPath, 'go.mod')) ||
|
|
7546
|
+
fs.existsSync(path.join(fullPath, 'Cargo.toml')) ||
|
|
7547
|
+
fs.existsSync(path.join(fullPath, 'pom.xml'))) {
|
|
7548
|
+
score += 1.0;
|
|
7549
|
+
}
|
|
7550
|
+
if (recentDirs.has(dirName)) score += 2.0;
|
|
7551
|
+
if (PENALTY_DIRS.has(dirName.toLowerCase()) && !frameworkSrcDirs.has(dirName)) score -= 3.0;
|
|
7552
|
+
if ((frameworkPenalties || []).includes(dirName)) score -= 3.0;
|
|
7553
|
+
return Math.round(score * 100) / 100;
|
|
7554
|
+
}
|
|
7555
|
+
module.exports = { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS };
|
|
7556
|
+
};
|
|
7557
|
+
|
|
7558
|
+
// ── ./src/discovery/framework-detector ──
|
|
7559
|
+
__factories["./src/discovery/framework-detector"] = function(module, exports) {
|
|
7560
|
+
'use strict';
|
|
7561
|
+
const fs = require('fs');
|
|
7562
|
+
const path = require('path');
|
|
7563
|
+
const { REGISTRY } = __require('./src/discovery/source-root-registry');
|
|
7564
|
+
function detectFrameworks(cwd) {
|
|
7565
|
+
const detected = [];
|
|
7566
|
+
for (const [lang, reg] of Object.entries(REGISTRY)) {
|
|
7567
|
+
if (!reg.frameworks) continue;
|
|
7568
|
+
for (const [name, fw] of Object.entries(reg.frameworks)) {
|
|
7569
|
+
let confidence = 0;
|
|
7570
|
+
for (const f of (fw.detectionFiles || [])) {
|
|
7571
|
+
if ((function _existsAnywhere(cwd, filename, maxDepth) {
|
|
7572
|
+
const parts = filename.split('/');
|
|
7573
|
+
if (parts.length > 1) return fs.existsSync(path.join(cwd, filename));
|
|
7574
|
+
return (function _walkFind(dir, name, depth) {
|
|
7575
|
+
if (depth <= 0) return false;
|
|
7576
|
+
try {
|
|
7577
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
7578
|
+
for (const e of entries) {
|
|
7579
|
+
if (e.name === name) return true;
|
|
7580
|
+
if (e.isDirectory() && depth > 1) {
|
|
7581
|
+
if (_walkFind(path.join(dir, e.name), name, depth - 1)) return true;
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
} catch (_) {}
|
|
7585
|
+
return false;
|
|
7586
|
+
})(cwd, parts[0], maxDepth);
|
|
7587
|
+
})(cwd, f, 3)) { confidence = Math.max(confidence, 0.93); }
|
|
7588
|
+
}
|
|
7589
|
+
if (fw.detectionDeps?.length) {
|
|
7590
|
+
const deps = (function _readDeps(cwd) {
|
|
7591
|
+
try {
|
|
7592
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
7593
|
+
return new Set([...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})]);
|
|
7594
|
+
} catch { return new Set(); }
|
|
7595
|
+
})(cwd);
|
|
7596
|
+
for (const dep of fw.detectionDeps) {
|
|
7597
|
+
if (deps.has(dep)) { confidence = Math.max(confidence, 0.90); }
|
|
7598
|
+
}
|
|
7599
|
+
}
|
|
7600
|
+
if (lang === 'go' && fw.detectionDeps?.length) {
|
|
7601
|
+
const goMod = (function _readFile(p) {
|
|
7602
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
7603
|
+
})(path.join(cwd, 'go.mod'));
|
|
7604
|
+
for (const dep of fw.detectionDeps) {
|
|
7605
|
+
if (goMod.includes(dep)) { confidence = Math.max(confidence, 0.90); }
|
|
7606
|
+
}
|
|
7607
|
+
}
|
|
7608
|
+
if (lang === 'rust' && fw.detectionDeps?.length) {
|
|
7609
|
+
const cargoToml = (function _readFile(p) {
|
|
7610
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
7611
|
+
})(path.join(cwd, 'Cargo.toml'));
|
|
7612
|
+
for (const dep of fw.detectionDeps) {
|
|
7613
|
+
if (cargoToml.includes(dep)) { confidence = Math.max(confidence, 0.88); }
|
|
7614
|
+
}
|
|
7615
|
+
}
|
|
7616
|
+
if (fw.specialRule === 'django-app-dirs' && fs.existsSync(path.join(cwd, 'manage.py'))) {
|
|
7617
|
+
confidence = Math.max(confidence, 0.95);
|
|
7618
|
+
}
|
|
7619
|
+
if (fw.specialRule === 'swift-project-dir' && (function _existsAnywhere(cwd, filename, maxDepth) {
|
|
7620
|
+
const parts = filename.split('/');
|
|
7621
|
+
if (parts.length > 1) return fs.existsSync(path.join(cwd, filename));
|
|
7622
|
+
return (function _walkFind(dir, name, depth) {
|
|
7623
|
+
if (depth <= 0) return false;
|
|
7624
|
+
try {
|
|
7625
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
7626
|
+
for (const e of entries) {
|
|
7627
|
+
if (e.name === name) return true;
|
|
7628
|
+
if (e.isDirectory() && depth > 1) {
|
|
7629
|
+
if (_walkFind(path.join(dir, e.name), name, depth - 1)) return true;
|
|
7630
|
+
}
|
|
7631
|
+
}
|
|
7632
|
+
} catch (_) {}
|
|
7633
|
+
return false;
|
|
7634
|
+
})(cwd, parts[0], maxDepth);
|
|
7635
|
+
})(cwd, '.xcodeproj', 2)) {
|
|
7636
|
+
confidence = Math.max(confidence, 0.90);
|
|
7637
|
+
}
|
|
7638
|
+
if (confidence > 0) detected.push({ name, language: lang, confidence });
|
|
7639
|
+
}
|
|
7640
|
+
}
|
|
7641
|
+
return detected.sort((a, b) => b.confidence - a.confidence);
|
|
7642
|
+
}
|
|
7643
|
+
module.exports = { detectFrameworks };
|
|
7644
|
+
};
|
|
7645
|
+
|
|
7646
|
+
// ── ./src/discovery/source-root-resolver ──
|
|
7647
|
+
__factories["./src/discovery/source-root-resolver"] = function(module, exports) {
|
|
7648
|
+
'use strict';
|
|
7649
|
+
const fs = require('fs');
|
|
7650
|
+
const path = require('path');
|
|
7651
|
+
const { REGISTRY } = __require('./src/discovery/source-root-registry');
|
|
7652
|
+
const { detectLanguages } = __require('./src/discovery/language-detector');
|
|
7653
|
+
const { detectFrameworks } = __require('./src/discovery/framework-detector');
|
|
7654
|
+
const { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS } = __require('./src/discovery/source-root-scorer');
|
|
7655
|
+
const { loadIgnorePatterns, matchesIgnorePattern } = __require('./src/discovery/sigmapignore');
|
|
7656
|
+
function resolveSourceRoots(cwd, opts = {}) {
|
|
7657
|
+
const ignorePatterns = loadIgnorePatterns(cwd);
|
|
7658
|
+
const languages = detectLanguages(cwd);
|
|
7659
|
+
const frameworks = detectFrameworks(cwd);
|
|
7660
|
+
const recentDirs = getRecentlyChangedDirs(cwd);
|
|
7661
|
+
const isMonorepo = (function _detectMonorepo(cwd) {
|
|
7662
|
+
const MONOREPO_MARKERS = ['pnpm-workspace.yaml','turbo.json','nx.json','lerna.json'];
|
|
7663
|
+
for (const m of MONOREPO_MARKERS) {
|
|
7664
|
+
if (fs.existsSync(path.join(cwd, m))) return true;
|
|
7665
|
+
}
|
|
7666
|
+
try {
|
|
7667
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
7668
|
+
if (pkg.workspaces) return true;
|
|
7669
|
+
} catch (_) {}
|
|
7670
|
+
return false;
|
|
7671
|
+
})(cwd);
|
|
7672
|
+
const primaryLang = languages[0]?.name;
|
|
7673
|
+
const primaryFw = frameworks[0];
|
|
7674
|
+
const registry = primaryLang ? REGISTRY[primaryLang] : null;
|
|
7675
|
+
const fwEntry = primaryFw && registry?.frameworks?.[primaryFw.name];
|
|
7676
|
+
const frameworkSrcDirs = new Set(fwEntry?.srcDirs || registry?.srcDirs || []);
|
|
7677
|
+
const entrypoints = fwEntry?.entrypoints || [];
|
|
7678
|
+
const frameworkPenalties = registry?.penalties || [];
|
|
7679
|
+
const context = { frameworks, languages, recentDirs, frameworkSrcDirs, entrypoints, frameworkPenalties };
|
|
7680
|
+
const candidates = (function _enumerateCandidates(cwd, isMonorepo, ignorePatterns, excludeList) {
|
|
7681
|
+
const candidates = [];
|
|
7682
|
+
const excSet = new Set(excludeList);
|
|
7683
|
+
try {
|
|
7684
|
+
for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
7685
|
+
if (!e.isDirectory()) continue;
|
|
7686
|
+
if (excSet.has(e.name)) continue;
|
|
7687
|
+
if (matchesIgnorePattern(e.name, ignorePatterns)) continue;
|
|
7688
|
+
candidates.push({ name: e.name, full: path.join(cwd, e.name) });
|
|
7689
|
+
}
|
|
7690
|
+
} catch (_) {}
|
|
7691
|
+
if (isMonorepo) {
|
|
7692
|
+
for (const top of ['packages','apps','services','modules']) {
|
|
7693
|
+
const topFull = path.join(cwd, top);
|
|
7694
|
+
if (!fs.existsSync(topFull)) continue;
|
|
7695
|
+
try {
|
|
7696
|
+
for (const pkg of fs.readdirSync(topFull, { withFileTypes: true })) {
|
|
7697
|
+
if (!pkg.isDirectory()) continue;
|
|
7698
|
+
const srcFull = path.join(topFull, pkg.name, 'src');
|
|
7699
|
+
if (fs.existsSync(srcFull)) {
|
|
7700
|
+
candidates.push({ name: `${top}/${pkg.name}/src`, full: srcFull });
|
|
7701
|
+
}
|
|
7702
|
+
candidates.push({ name: `${top}/${pkg.name}`, full: path.join(topFull, pkg.name) });
|
|
7703
|
+
}
|
|
7704
|
+
} catch (_) {}
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
const DEEP_PATHS = ['src/main/java','src/main/kotlin','src/main/scala','src-tauri/src','Sources/App','app/src/main/java','app/src/main/kotlin'];
|
|
7708
|
+
for (const dp of DEEP_PATHS) {
|
|
7709
|
+
const full = path.join(cwd, dp);
|
|
7710
|
+
if (fs.existsSync(full)) candidates.push({ name: dp, full });
|
|
7711
|
+
}
|
|
7712
|
+
return candidates;
|
|
7713
|
+
})(cwd, isMonorepo, ignorePatterns, opts.exclude || []);
|
|
7714
|
+
const scored = candidates
|
|
7715
|
+
.map(({ name, full }) => ({
|
|
7716
|
+
dir: name,
|
|
7717
|
+
full,
|
|
7718
|
+
score: scoreCandidate(name, full, context),
|
|
7719
|
+
}))
|
|
7720
|
+
.filter(c => c.score > 0)
|
|
7721
|
+
.sort((a, b) => b.score - a.score);
|
|
7722
|
+
let roots = (function _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks) {
|
|
7723
|
+
let roots = [...scored];
|
|
7724
|
+
if (primaryFw?.name === 'django' || frameworks.some(f => f.name === 'django')) {
|
|
7725
|
+
try {
|
|
7726
|
+
for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
7727
|
+
if (!e.isDirectory()) continue;
|
|
7728
|
+
const d = path.join(cwd, e.name);
|
|
7729
|
+
if (fs.existsSync(path.join(d, 'models.py')) || fs.existsSync(path.join(d, 'views.py'))) {
|
|
7730
|
+
if (!roots.find(r => r.dir === e.name)) {
|
|
7731
|
+
roots.push({ dir: e.name, full: d, score: 5.0 });
|
|
7732
|
+
}
|
|
7733
|
+
}
|
|
7734
|
+
}
|
|
7735
|
+
} catch (_) {}
|
|
7736
|
+
roots.sort((a, b) => b.score - a.score);
|
|
7737
|
+
}
|
|
7738
|
+
if (frameworks.some(f => f.name === 'swiftui')) {
|
|
7739
|
+
try {
|
|
7740
|
+
for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
7741
|
+
if (!e.isDirectory()) continue;
|
|
7742
|
+
const d = path.join(cwd, e.name);
|
|
7743
|
+
const swiftCount = (fs.readdirSync(d).filter(f => f.endsWith('.swift'))).length;
|
|
7744
|
+
if (swiftCount >= 3 && !roots.find(r => r.dir === e.name)) {
|
|
7745
|
+
roots.push({ dir: e.name, full: d, score: 4.0 });
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
} catch (_) {}
|
|
7749
|
+
roots.sort((a, b) => b.score - a.score);
|
|
7750
|
+
}
|
|
7751
|
+
return roots;
|
|
7752
|
+
})(scored, cwd, primaryFw, fwEntry, frameworks);
|
|
7753
|
+
roots = (function _dedupeNested(scored) {
|
|
7754
|
+
const result = [];
|
|
7755
|
+
for (const c of scored) {
|
|
7756
|
+
const isNested = result.some(r => c.dir.startsWith(r.dir + '/'));
|
|
7757
|
+
if (!isNested) result.push(c);
|
|
7758
|
+
}
|
|
7759
|
+
return result;
|
|
7760
|
+
})(roots);
|
|
7761
|
+
const MAX_ROOTS = 6;
|
|
7762
|
+
roots = roots.slice(0, MAX_ROOTS).map(r => r.dir);
|
|
7763
|
+
const confidence = (function _computeConfidence(frameworks, languages, scoredCount) {
|
|
7764
|
+
if (frameworks.length > 0 && frameworks[0].confidence >= 0.90) return 'high';
|
|
7765
|
+
if (languages.length > 0 && scoredCount > 0) return 'medium';
|
|
7766
|
+
return 'low';
|
|
7767
|
+
})(frameworks, languages, scored.length);
|
|
7768
|
+
return {
|
|
7769
|
+
roots,
|
|
7770
|
+
languages,
|
|
7771
|
+
frameworks,
|
|
7772
|
+
confidence,
|
|
7773
|
+
explanation: scored.slice(0, 8).map(c => ({
|
|
7774
|
+
dir: c.dir,
|
|
7775
|
+
score: c.score,
|
|
7776
|
+
reason: `score: ${c.score}`,
|
|
7777
|
+
})),
|
|
7778
|
+
isMonorepo,
|
|
7779
|
+
};
|
|
7780
|
+
}
|
|
7781
|
+
module.exports = { resolveSourceRoots };
|
|
7782
|
+
};
|
|
7783
|
+
|
|
7213
7784
|
/**
|
|
7214
7785
|
* SigMap — gen-context.js v1.2.0
|
|
7215
7786
|
* Zero-dependency AI context engine.
|
|
@@ -7222,7 +7793,7 @@ const path = require('path');
|
|
|
7222
7793
|
const os = require('os');
|
|
7223
7794
|
const { execSync } = require('child_process');
|
|
7224
7795
|
|
|
7225
|
-
const VERSION = '6.
|
|
7796
|
+
const VERSION = '6.6.0';
|
|
7226
7797
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
7227
7798
|
|
|
7228
7799
|
function requireSourceOrBundled(key) {
|
|
@@ -8429,6 +9000,13 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
|
8429
9000
|
const hotCommits = config.hotCommits || 10;
|
|
8430
9001
|
const recentFiles = config.diffPriority ? getRecentlyCommittedFiles(cwd, hotCommits) : new Set();
|
|
8431
9002
|
|
|
9003
|
+
// v6.7: Load signature cache if enabled
|
|
9004
|
+
let cache = null;
|
|
9005
|
+
const { loadCache, saveCache, getChangedFiles, updateCacheEntries } = requireSourceOrBundled('./src/cache/sig-cache');
|
|
9006
|
+
if (config.sigCache) {
|
|
9007
|
+
cache = loadCache(cwd, VERSION);
|
|
9008
|
+
}
|
|
9009
|
+
|
|
8432
9010
|
let inputTokenTotal = 0;
|
|
8433
9011
|
let fileEntries = [];
|
|
8434
9012
|
let testIndex = null;
|
|
@@ -8449,7 +9027,23 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
|
8449
9027
|
continue;
|
|
8450
9028
|
}
|
|
8451
9029
|
|
|
8452
|
-
|
|
9030
|
+
// v6.7: Check cache before extracting
|
|
9031
|
+
let sigs = [];
|
|
9032
|
+
if (cache) {
|
|
9033
|
+
const cached = cache.get(filePath);
|
|
9034
|
+
let mtime = 0;
|
|
9035
|
+
try { mtime = fs.statSync(filePath).mtimeMs; } catch (_) {}
|
|
9036
|
+
if (cached && cached.mtime === mtime) {
|
|
9037
|
+
sigs = cached.sigs;
|
|
9038
|
+
} else {
|
|
9039
|
+
sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
|
|
9040
|
+
if (sigs.length > 0) {
|
|
9041
|
+
cache.set(filePath, { mtime, sigs });
|
|
9042
|
+
}
|
|
9043
|
+
}
|
|
9044
|
+
} else {
|
|
9045
|
+
sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
|
|
9046
|
+
}
|
|
8453
9047
|
if (sigs.length === 0) continue;
|
|
8454
9048
|
|
|
8455
9049
|
// Baseline = estimated tokens of original source content for intuitive reduction stats.
|
|
@@ -8651,6 +9245,15 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
|
8651
9245
|
process.stderr.write(lines.join('\n'));
|
|
8652
9246
|
}
|
|
8653
9247
|
|
|
9248
|
+
// v6.7: Save cache if enabled
|
|
9249
|
+
if (config.sigCache && cache) {
|
|
9250
|
+
try {
|
|
9251
|
+
saveCache(cwd, VERSION, cache);
|
|
9252
|
+
} catch (err) {
|
|
9253
|
+
console.warn(`[sigmap] cache save failed: ${err.message}`);
|
|
9254
|
+
}
|
|
9255
|
+
}
|
|
9256
|
+
|
|
8654
9257
|
return result;
|
|
8655
9258
|
}
|
|
8656
9259
|
|
|
@@ -9244,6 +9847,7 @@ function main() {
|
|
|
9244
9847
|
|
|
9245
9848
|
const { detectIntent, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
9246
9849
|
const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
|
|
9850
|
+
const { loadSession, saveSession, mergeSessionContext } = requireSourceOrBundled('./src/session/memory');
|
|
9247
9851
|
|
|
9248
9852
|
const intent = detectIntent(query);
|
|
9249
9853
|
const intentWeights = getIntentWeights(intent);
|
|
@@ -9254,7 +9858,21 @@ function main() {
|
|
|
9254
9858
|
process.exit(1);
|
|
9255
9859
|
}
|
|
9256
9860
|
|
|
9257
|
-
|
|
9861
|
+
let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });
|
|
9862
|
+
|
|
9863
|
+
// v6.8: --followup support — carry session context forward
|
|
9864
|
+
const isFollowup = args.includes('--followup');
|
|
9865
|
+
const session = isFollowup ? loadSession(cwd) : null;
|
|
9866
|
+
if (session) {
|
|
9867
|
+
ranked = mergeSessionContext(ranked, session, intent);
|
|
9868
|
+
ranked.sort((a, b) => b.score - a.score);
|
|
9869
|
+
if (!args.includes('--json')) {
|
|
9870
|
+
process.stderr.write(`[sigmap] ♻ followup — carrying context from: "${session.lastQuery.slice(0, 60)}"\n`);
|
|
9871
|
+
}
|
|
9872
|
+
}
|
|
9873
|
+
|
|
9874
|
+
// v6.8: Save session for future --followup calls
|
|
9875
|
+
saveSession(cwd, { intent, topFiles: ranked.slice(0, 5).map(r => ({ file: r.file, score: r.score })), query });
|
|
9258
9876
|
const miniCtx = buildMiniContext(ranked, cwd);
|
|
9259
9877
|
const outPath = path.join(cwd, '.context', 'query-context.md');
|
|
9260
9878
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
@@ -9400,6 +10018,77 @@ function main() {
|
|
|
9400
10018
|
process.exit(0);
|
|
9401
10019
|
}
|
|
9402
10020
|
|
|
10021
|
+
// v6.8: `sigmap plan "<goal>"` — analyze what files need changes and impact radius
|
|
10022
|
+
if (args[0] === 'plan') {
|
|
10023
|
+
const goal = args[1];
|
|
10024
|
+
if (!goal || goal.startsWith('--')) {
|
|
10025
|
+
console.error('[sigmap] Usage: sigmap plan "<goal>"');
|
|
10026
|
+
console.error(' Example: sigmap plan "add rate limiting to the API"');
|
|
10027
|
+
process.exit(1);
|
|
10028
|
+
}
|
|
10029
|
+
|
|
10030
|
+
const { detectIntent, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');
|
|
10031
|
+
|
|
10032
|
+
const intent = detectIntent(goal);
|
|
10033
|
+
const intentWeights = getIntentWeights(intent);
|
|
10034
|
+
|
|
10035
|
+
const sigIndex = buildSigIndex(cwd);
|
|
10036
|
+
if (sigIndex.size === 0) {
|
|
10037
|
+
console.error('[sigmap] no context file found. Run: sigmap (to generate first)');
|
|
10038
|
+
process.exit(1);
|
|
10039
|
+
}
|
|
10040
|
+
|
|
10041
|
+
const ranked = rank(goal, sigIndex, { topK: 15, weights: intentWeights, cwd });
|
|
10042
|
+
|
|
10043
|
+
// Separate into confidence levels
|
|
10044
|
+
const highConf = ranked.filter(r => r.confidence === 'high').slice(0, 5);
|
|
10045
|
+
const medConf = ranked.filter(r => r.confidence === 'medium').slice(0, 5);
|
|
10046
|
+
|
|
10047
|
+
// Compute impact radius (simplified — no graph for now)
|
|
10048
|
+
let impact = null;
|
|
10049
|
+
|
|
10050
|
+
// Identify likely-affected tests (simplified — checks for .test. or .spec. in filename)
|
|
10051
|
+
const testedFiles = highConf.filter(r => /\.(test|spec)\.(js|ts|py)$|_test\.(js|ts|py)$/.test(r.file));
|
|
10052
|
+
|
|
10053
|
+
if (args.includes('--json')) {
|
|
10054
|
+
process.stdout.write(JSON.stringify({
|
|
10055
|
+
goal, intent,
|
|
10056
|
+
inspectFirst: highConf.map(r => r.file),
|
|
10057
|
+
likelyToChange: medConf.map(r => r.file),
|
|
10058
|
+
impactRadius: impact,
|
|
10059
|
+
testsAffected: testedFiles.map(r => r.file),
|
|
10060
|
+
}, null, 2) + '\n');
|
|
10061
|
+
} else {
|
|
10062
|
+
const bar = '─'.repeat(50);
|
|
10063
|
+
console.log(bar);
|
|
10064
|
+
console.log(` sigmap plan "${goal}"`);
|
|
10065
|
+
console.log(` Intent : ${intent}`);
|
|
10066
|
+
console.log(bar);
|
|
10067
|
+
console.log('');
|
|
10068
|
+
console.log(' Inspect first (highest relevance):');
|
|
10069
|
+
if (highConf.length === 0) {
|
|
10070
|
+
console.log(' (no files found)');
|
|
10071
|
+
} else {
|
|
10072
|
+
highConf.forEach((r, i) => console.log(` ${i + 1}. ${path.relative(cwd, r.file)}`));
|
|
10073
|
+
}
|
|
10074
|
+
console.log('');
|
|
10075
|
+
console.log(' Likely to change:');
|
|
10076
|
+
if (medConf.length === 0) {
|
|
10077
|
+
console.log(' (no files found)');
|
|
10078
|
+
} else {
|
|
10079
|
+
medConf.forEach((r, i) => console.log(` ${i + 1}. ${path.relative(cwd, r.file)}`));
|
|
10080
|
+
}
|
|
10081
|
+
if (testedFiles.length > 0) {
|
|
10082
|
+
console.log('');
|
|
10083
|
+
console.log(' Tests to run after change:');
|
|
10084
|
+
testedFiles.forEach(r => console.log(` • ${path.relative(cwd, r.file)}`));
|
|
10085
|
+
}
|
|
10086
|
+
console.log('');
|
|
10087
|
+
console.log(bar);
|
|
10088
|
+
}
|
|
10089
|
+
process.exit(0);
|
|
10090
|
+
}
|
|
10091
|
+
|
|
9403
10092
|
// v5.9: `sigmap bench --submit` — format local benchmark history as a shareable community block
|
|
9404
10093
|
if (args[0] === 'bench' && args.includes('--submit')) {
|
|
9405
10094
|
const versionMeta = (() => {
|
|
@@ -9954,6 +10643,19 @@ function main() {
|
|
|
9954
10643
|
const fakeEntries = allFiles.map(f => ({ filePath: f }));
|
|
9955
10644
|
coverageResult = coverageScore(cwd, fakeEntries, cfg);
|
|
9956
10645
|
} catch (_) {}
|
|
10646
|
+
// v6.7: Collect cache stats if enabled
|
|
10647
|
+
let cacheStats = null;
|
|
10648
|
+
try {
|
|
10649
|
+
const cachePath = path.join(cwd, '.sigmap-cache.json');
|
|
10650
|
+
if (fs.existsSync(cachePath)) {
|
|
10651
|
+
const raw = fs.readFileSync(cachePath, 'utf8');
|
|
10652
|
+
const data = JSON.parse(raw);
|
|
10653
|
+
const sizeKb = Math.round(Buffer.byteLength(raw) / 1024);
|
|
10654
|
+
const entries = Object.keys(data.entries || {}).length;
|
|
10655
|
+
const mtime = fs.statSync(cachePath).mtimeMs;
|
|
10656
|
+
cacheStats = { sizeKb, entries, mtimeMs: mtime };
|
|
10657
|
+
}
|
|
10658
|
+
} catch (_) {}
|
|
9957
10659
|
if (args.includes('--json')) {
|
|
9958
10660
|
// Feature 3 (VS Code) + Feature 5 (JetBrains): emit tokens + reduction for plugins
|
|
9959
10661
|
const ctxPath = path.join(cwd, '.github', 'copilot-instructions.md');
|
|
@@ -9970,6 +10672,9 @@ function main() {
|
|
|
9970
10672
|
payload.coverageTotalFiles = coverageResult.total;
|
|
9971
10673
|
payload.coverageIncludedFiles = coverageResult.included;
|
|
9972
10674
|
}
|
|
10675
|
+
if (cacheStats) {
|
|
10676
|
+
payload.cacheStats = cacheStats;
|
|
10677
|
+
}
|
|
9973
10678
|
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
9974
10679
|
} else {
|
|
9975
10680
|
console.log('[sigmap] health:');
|
|
@@ -9983,6 +10688,9 @@ function main() {
|
|
|
9983
10688
|
if (result.strategyFreshnessDays !== null) {
|
|
9984
10689
|
console.log(` cold freshness : ${result.strategyFreshnessDays} day(s)`);
|
|
9985
10690
|
}
|
|
10691
|
+
if (cacheStats) {
|
|
10692
|
+
console.log(` sig-cache : ${cacheStats.entries} files cached ${cacheStats.sizeKb}KB on disk`);
|
|
10693
|
+
}
|
|
9986
10694
|
console.log(` total runs : ${result.totalRuns}`);
|
|
9987
10695
|
console.log(` over-budget runs: ${result.overBudgetRuns}`);
|
|
9988
10696
|
console.log(` p50 token count : ${result.p50TokenCount}`);
|
package/package.json
CHANGED
package/src/config/defaults.js
CHANGED
|
@@ -125,6 +125,9 @@ const DEFAULTS = {
|
|
|
125
125
|
// Directories scanned for tests when testCoverage is enabled
|
|
126
126
|
testDirs: ['tests', 'test', '__tests__', 'spec'],
|
|
127
127
|
|
|
128
|
+
// Enable incremental signature cache (v6.7) - only re-extract changed files
|
|
129
|
+
sigCache: false,
|
|
130
|
+
|
|
128
131
|
// Add reverse dependency usage hints on file headings (opt-in)
|
|
129
132
|
impactRadius: false,
|
|
130
133
|
|
package/src/mcp/server.js
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { buildFromCwd } = require('../graph/builder');
|
|
6
|
+
const { getImpact } = require('../graph/impact');
|
|
7
|
+
const { buildSigIndex, rank, detectIntent } = require('../retrieval/ranker');
|
|
8
|
+
const { buildTestIndex, isTested } = require('../extractors/coverage');
|
|
9
|
+
|
|
10
|
+
module.exports = { createPlan };
|
|
11
|
+
|
|
12
|
+
function createPlan(goal, cwd, config) {
|
|
13
|
+
// Step 1: Detect intent and rank files for the goal
|
|
14
|
+
const intent = detectIntent(goal);
|
|
15
|
+
const sigIndex = buildSigIndex(cwd);
|
|
16
|
+
if (sigIndex.size === 0) {
|
|
17
|
+
return { error: 'no context found' };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ranked = rank(goal, sigIndex, { topK: 15, cwd });
|
|
21
|
+
|
|
22
|
+
// Step 2: Separate into confidence levels
|
|
23
|
+
const highConf = ranked.filter(r => r.confidence === 'high').slice(0, 5);
|
|
24
|
+
const medConf = ranked.filter(r => r.confidence === 'medium').slice(0, 5);
|
|
25
|
+
|
|
26
|
+
// Step 3: Compute impact radius for highest-confidence file
|
|
27
|
+
let impact = null;
|
|
28
|
+
if (highConf.length > 0) {
|
|
29
|
+
const entryFile = highConf[0].file;
|
|
30
|
+
try {
|
|
31
|
+
const graph = buildFromCwd(cwd);
|
|
32
|
+
impact = getImpact(entryFile, graph, { maxDepth: 3, cwd });
|
|
33
|
+
} catch (_) {
|
|
34
|
+
// Graph build failed, continue without impact
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Step 4: Identify likely-affected tests
|
|
39
|
+
let testedFiles = [];
|
|
40
|
+
try {
|
|
41
|
+
const testIndex = buildTestIndex(cwd, config.testDirs || ['test', 'tests', '__tests__', 'spec']);
|
|
42
|
+
testedFiles = highConf.filter(r => {
|
|
43
|
+
const sigs = r.sigs || [];
|
|
44
|
+
const fnNames = sigs.map(s => {
|
|
45
|
+
const m = s.match(/(?:function|def|fn)\s+(\w+)/);
|
|
46
|
+
return m ? m[1] : null;
|
|
47
|
+
}).filter(Boolean);
|
|
48
|
+
return fnNames.some(fn => isTested(fn, testIndex));
|
|
49
|
+
});
|
|
50
|
+
} catch (_) {
|
|
51
|
+
// Coverage index failed, continue without test info
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
goal,
|
|
56
|
+
intent,
|
|
57
|
+
inspectFirst: highConf.map(r => r.file),
|
|
58
|
+
likelyToChange: medConf.map(r => r.file),
|
|
59
|
+
impactRadius: impact ? {
|
|
60
|
+
direct: [...(impact.direct || [])],
|
|
61
|
+
transitive: [...(impact.transitive || [])],
|
|
62
|
+
} : null,
|
|
63
|
+
testsAffected: testedFiles.map(r => r.file),
|
|
64
|
+
};
|
|
65
|
+
}
|
package/src/retrieval/ranker.js
CHANGED
|
@@ -32,6 +32,12 @@ const DEFAULT_WEIGHTS = {
|
|
|
32
32
|
graphBoost: 0.4, // additive bonus for 1-hop import neighbors of matching files
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
// Graph boost amounts for 2-hop traversal with decay (v6.7)
|
|
36
|
+
const GRAPH_BOOST_AMOUNTS = {
|
|
37
|
+
hop1: 0.40, // direct import neighbor of a file with score > 0
|
|
38
|
+
hop2: 0.15, // 2 hops away (transitive), with decay
|
|
39
|
+
};
|
|
40
|
+
|
|
35
41
|
// Intent-specific weight adjustments
|
|
36
42
|
const INTENT_WEIGHTS = {
|
|
37
43
|
search: DEFAULT_WEIGHTS,
|
|
@@ -61,6 +67,26 @@ function _computePenalty(filePath) {
|
|
|
61
67
|
return 1.0;
|
|
62
68
|
}
|
|
63
69
|
|
|
70
|
+
// Detect hub files: those with fanout > 20% of all files in the graph
|
|
71
|
+
function _computeHubs(graph) {
|
|
72
|
+
if (!graph || !graph.reverse) return new Set();
|
|
73
|
+
const fileCount = Math.max(1, graph.reverse.size);
|
|
74
|
+
const threshold = Math.ceil(fileCount * 0.2);
|
|
75
|
+
const hubs = new Set();
|
|
76
|
+
for (const [file, deps] of graph.reverse) {
|
|
77
|
+
if ((deps && deps.size >= threshold) || (Array.isArray(deps) && deps.length >= threshold)) {
|
|
78
|
+
hubs.add(file);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return hubs;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Common utility paths that should be treated as hubs regardless of fanout
|
|
85
|
+
function _isHub(filePath) {
|
|
86
|
+
return /\/(utils|helpers|shared|common|constants|types|interfaces|index)\.(ts|tsx|js|jsx)$/.test(filePath)
|
|
87
|
+
|| filePath.endsWith('/index.ts') || filePath.endsWith('/index.js');
|
|
88
|
+
}
|
|
89
|
+
|
|
64
90
|
/**
|
|
65
91
|
* Score a single file against a query, returning detailed signal breakdown.
|
|
66
92
|
*
|
|
@@ -198,26 +224,54 @@ function rank(query, sigIndex, opts) {
|
|
|
198
224
|
});
|
|
199
225
|
}
|
|
200
226
|
|
|
201
|
-
// Graph neighbor boost:
|
|
202
|
-
//
|
|
227
|
+
// Graph neighbor boost: 2-hop traversal with decay (v6.7)
|
|
228
|
+
// Hop 1: add hop1 amount to direct import neighbors (score > 0)
|
|
229
|
+
// Hop 2: add hop2 amount to neighbors of hop1 files (with decay)
|
|
230
|
+
// Hub suppression: files with high fanout (>20%) are not boosted
|
|
203
231
|
if (graph && cwd) {
|
|
204
232
|
const path = require('path');
|
|
205
|
-
// Build
|
|
233
|
+
// Build maps for relative ↔ absolute path conversion and index lookup
|
|
206
234
|
const relToIdx = new Map();
|
|
235
|
+
const absToRel = new Map();
|
|
207
236
|
for (let i = 0; i < scored.length; i++) {
|
|
208
237
|
relToIdx.set(scored[i].file, i);
|
|
238
|
+
const abs = path.resolve(cwd, scored[i].file);
|
|
239
|
+
absToRel.set(abs, scored[i].file);
|
|
209
240
|
}
|
|
241
|
+
|
|
242
|
+
const hubs = _computeHubs(graph);
|
|
243
|
+
const hop1Files = new Set(); // track which files received hop1 boost
|
|
244
|
+
|
|
245
|
+
// Hop 1: direct neighbors of scored files
|
|
210
246
|
for (const entry of scored) {
|
|
211
247
|
if (entry.score <= 0) continue;
|
|
212
|
-
// Resolve relative path to absolute for graph lookup
|
|
213
248
|
const abs = path.resolve(cwd, entry.file);
|
|
214
249
|
const neighbors = graph.forward.get(abs) || [];
|
|
215
250
|
for (const neighborAbs of neighbors) {
|
|
251
|
+
if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
|
|
216
252
|
const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
|
|
217
253
|
const idx = relToIdx.get(neighborRel);
|
|
218
254
|
if (idx !== undefined) {
|
|
219
|
-
scored[idx].score +=
|
|
220
|
-
scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) +
|
|
255
|
+
scored[idx].score += GRAPH_BOOST_AMOUNTS.hop1;
|
|
256
|
+
scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop1;
|
|
257
|
+
hop1Files.add(neighborAbs);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Hop 2: neighbors of hop1 files (only if they didn't get a direct score)
|
|
263
|
+
for (const hop1File of hop1Files) {
|
|
264
|
+
if (!absToRel.has(hop1File)) continue; // skip files not in index
|
|
265
|
+
const neighbors = graph.forward.get(hop1File) || [];
|
|
266
|
+
for (const neighborAbs of neighbors) {
|
|
267
|
+
if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
|
|
268
|
+
if (hop1Files.has(neighborAbs)) continue; // skip already hop1-boosted
|
|
269
|
+
const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
|
|
270
|
+
const idx = relToIdx.get(neighborRel);
|
|
271
|
+
if (idx !== undefined && scored[idx].score > 0) {
|
|
272
|
+
// Only boost files that have some baseline score (not noise)
|
|
273
|
+
scored[idx].score += GRAPH_BOOST_AMOUNTS.hop2;
|
|
274
|
+
scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop2;
|
|
221
275
|
}
|
|
222
276
|
}
|
|
223
277
|
}
|
|
@@ -425,4 +479,4 @@ function detectIntent(query) {
|
|
|
425
479
|
return 'search';
|
|
426
480
|
}
|
|
427
481
|
|
|
428
|
-
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent };
|
|
482
|
+
module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
module.exports = { loadSession, saveSession, mergeSessionContext, clearSession };
|
|
7
|
+
|
|
8
|
+
const SESSION_TTL_MS = 4 * 60 * 60 * 1000; // 4 hours — one coding session
|
|
9
|
+
|
|
10
|
+
function sessionPath(cwd) {
|
|
11
|
+
return path.join(cwd, '.context', 'session.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function loadSession(cwd) {
|
|
15
|
+
const p = sessionPath(cwd);
|
|
16
|
+
if (!fs.existsSync(p)) return null;
|
|
17
|
+
try {
|
|
18
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
19
|
+
if (Date.now() - raw.ts > SESSION_TTL_MS) return null; // expired
|
|
20
|
+
return raw;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveSession(cwd, { intent, topFiles, query }) {
|
|
27
|
+
const p = sessionPath(cwd);
|
|
28
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
29
|
+
fs.writeFileSync(p, JSON.stringify({
|
|
30
|
+
ts: Date.now(),
|
|
31
|
+
intent,
|
|
32
|
+
topFiles, // [{ file, score }] — top 5 from last ask
|
|
33
|
+
lastQuery: query,
|
|
34
|
+
}));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Merge session context as additional ranking signal:
|
|
38
|
+
// any file that appeared in the previous session's top-5 gets a +0.2 boost
|
|
39
|
+
// Reduce to +0.1 if the intent has changed (topic switch guard)
|
|
40
|
+
function mergeSessionContext(scores, session, currentIntent) {
|
|
41
|
+
if (!session) return scores;
|
|
42
|
+
|
|
43
|
+
const boostAmount = session.intent === currentIntent ? 0.20 : 0.10;
|
|
44
|
+
const sessionBoost = new Map(session.topFiles.map(f => [f.file, boostAmount]));
|
|
45
|
+
|
|
46
|
+
return scores.map(r => ({
|
|
47
|
+
...r,
|
|
48
|
+
// Additive boost — cannot reduce a score
|
|
49
|
+
score: r.score + (sessionBoost.get(r.file) || 0),
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function clearSession(cwd) {
|
|
54
|
+
const p = sessionPath(cwd);
|
|
55
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
56
|
+
}
|