sigmap 6.5.1 → 6.5.2

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
@@ -56,13 +56,13 @@ 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 — 6 minutes ago)
59
+ ## changes (last 5 commits — 2 days ago)
60
60
  ```
61
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
62
  src/discovery/source-root-resolver.js +resolveSourceRoots +_detectMonorepo +_enumerateCandidates +_applySpecialRules
63
+ src/discovery/language-detector.js +detectLanguages +_walkDepth
65
64
  src/discovery/source-root-scorer.js +getRecentlyChangedDirs +scoreCandidate +_countSourceFiles
65
+ src/discovery/framework-detector.js +detectFrameworks +_readDeps +_readFile +_existsAnywhere
66
66
  src/discovery/sigmapignore.js +loadIgnorePatterns +matchesIgnorePattern
67
67
  src/retrieval/ranker.js +_computePenalty ~scoreFile ~rank ~buildSigIndex
68
68
  ```
@@ -166,19 +166,19 @@ function _confidenceMeta(opts)
166
166
  function outputPath(cwd) → string
167
167
  ```
168
168
 
169
- ### packages/adapters/codex.js
169
+ ### packages/adapters/claude.js
170
170
  ```
171
171
  module.exports = { name, format, outputPath, write }
172
172
  function format(context, opts = {}) → string
173
+ function _confidenceMeta(opts)
173
174
  function outputPath(cwd) → string
174
175
  function write(context, cwd, opts = {})
175
176
  ```
176
177
 
177
- ### packages/adapters/claude.js
178
+ ### packages/adapters/codex.js
178
179
  ```
179
180
  module.exports = { name, format, outputPath, write }
180
181
  function format(context, opts = {}) → string
181
- function _confidenceMeta(opts)
182
182
  function outputPath(cwd) → string
183
183
  function write(context, cwd, opts = {})
184
184
  ```
@@ -483,11 +483,6 @@ function formatAnalysisTable(stats, showSlow) → string
483
483
  function formatAnalysisJSON(stats) → object
484
484
  ```
485
485
 
486
- ### src/config/defaults.js
487
- ```
488
- module.exports = { DEFAULTS }
489
- ```
490
-
491
486
  ### src/format/dashboard.js
492
487
  ```
493
488
  module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak }
@@ -610,6 +605,11 @@ function exportWeights(cwd, outputPath)
610
605
  function importWeights(cwd, importPath, replace)
611
606
  ```
612
607
 
608
+ ### src/config/defaults.js
609
+ ```
610
+ module.exports = { DEFAULTS }
611
+ ```
612
+
613
613
  ### src/config/loader.js
614
614
  ```
615
615
  module.exports = { loadConfig, loadBaseConfig }
@@ -620,23 +620,6 @@ function loadConfig(cwd) → object
620
620
  function deepClone(obj)
621
621
  ```
622
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
623
  ### src/discovery/source-root-registry.js
641
624
  ```
642
625
  module.exports = { REGISTRY }
@@ -653,6 +636,13 @@ function _dedupeNested(scored)
653
636
  function _computeConfidence(frameworks, languages, scoredCount)
654
637
  ```
655
638
 
639
+ ### src/discovery/language-detector.js
640
+ ```
641
+ module.exports = { detectLanguages }
642
+ function detectLanguages(cwd)
643
+ function _walkDepth(dir, depth, extCount)
644
+ ```
645
+
656
646
  ### src/discovery/source-root-scorer.js
657
647
  ```
658
648
  module.exports = { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS }
@@ -661,6 +651,16 @@ function scoreCandidate(dirName, fullPath, context)
661
651
  function _countSourceFiles(dir, depth)
662
652
  ```
663
653
 
654
+ ### src/discovery/framework-detector.js
655
+ ```
656
+ module.exports = { detectFrameworks }
657
+ function detectFrameworks(cwd)
658
+ function _readDeps(cwd)
659
+ function _readFile(p)
660
+ function _existsAnywhere(cwd, filename, maxDepth)
661
+ function _walkFind(dir, name, depth)
662
+ ```
663
+
664
664
  ### src/discovery/sigmapignore.js
665
665
  ```
666
666
  module.exports = { loadIgnorePatterns, matchesIgnorePattern }
@@ -670,8 +670,10 @@ function matchesIgnorePattern(dirName, patterns)
670
670
 
671
671
  ### src/retrieval/ranker.js
672
672
  ```
673
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent }
673
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent }
674
674
  function _computePenalty(filePath)
675
+ function _computeHubs(graph)
676
+ function _isHub(filePath)
675
677
  function scoreFile(filePath, sigs, queryTokens, weights) → { score: number, signals:
676
678
  function rank(query, sigIndex, opts) → { file: string, score: nu
677
679
  function _parseContextFile(contextPath) → Map<string, string[]>
package/CHANGELOG.md CHANGED
@@ -10,6 +10,17 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [6.5.2] — 2026-04-27
14
+
15
+ ### Added
16
+
17
+ - **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.
18
+ - **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.
19
+ - **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.
20
+ - **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.
21
+
22
+ ---
23
+
13
24
  ## [6.5.1] — 2026-04-25
14
25
 
15
26
  ### 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.5.1',
5390
+ version: '6.5.2',
5391
5391
  description: 'SigMap MCP server — code signatures on demand',
5392
5392
  };
5393
5393
 
@@ -7210,6 +7210,521 @@ __factories["./packages/adapters/llm-full"] = function(module, exports) {
7210
7210
  module.exports = { name: 'llm-full', format, outputPath, write };
7211
7211
  };
7212
7212
 
7213
+ // ── ./src/discovery/source-root-registry ──
7214
+ __factories["./src/discovery/source-root-registry"] = function(module, exports) {
7215
+ 'use strict';
7216
+ const REGISTRY = {
7217
+ javascript: {
7218
+ manifestFiles: ['package.json'],
7219
+ frameworks: {
7220
+ 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'] },
7221
+ nestjs: { detectionFiles: ['nest-cli.json'], detectionDeps: ['@nestjs/core'], srcDirs: ['src'], entrypoints: ['src/main.ts','src/app.module.ts'] },
7222
+ express: { detectionFiles: [], detectionDeps: ['express'], srcDirs: ['src','routes','middleware','controllers','services'], entrypoints: ['src/index.js','server.js','app.js'] },
7223
+ fastify: { detectionFiles: [], detectionDeps: ['fastify'], srcDirs: ['src','routes','plugins'], entrypoints: ['src/index.js'] },
7224
+ react: { detectionFiles: [], detectionDeps: ['react'], srcDirs: ['src','components','hooks','context','pages','app','lib','utils'] },
7225
+ vue: { detectionFiles: ['vue.config.js','vue.config.ts'], detectionDeps: ['vue'], srcDirs: ['src','components','composables','pages','views'] },
7226
+ nuxt: { detectionFiles: ['nuxt.config.js','nuxt.config.ts'], detectionDeps: ['nuxt'], srcDirs: ['pages','components','composables','server','middleware','plugins'] },
7227
+ svelte: { detectionFiles: ['svelte.config.js'], detectionDeps: ['svelte','@sveltejs/kit'], srcDirs: ['src','src/routes','src/lib'] },
7228
+ angular: { detectionFiles: ['angular.json'], detectionDeps: ['@angular/core'], srcDirs: ['src','src/app','projects','apps','libs'] },
7229
+ gatsby: { detectionFiles: ['gatsby-config.js','gatsby-config.ts'], detectionDeps: ['gatsby'], srcDirs: ['src','gatsby'] },
7230
+ vite: { detectionFiles: ['vite.config.js','vite.config.ts'], detectionDeps: ['vite'], srcDirs: ['src'] },
7231
+ remix: { detectionFiles: ['remix.config.js'], detectionDeps: ['@remix-run/react'], srcDirs: ['app'] },
7232
+ trpc: { detectionFiles: [], detectionDeps: ['@trpc/server'], srcDirs: ['src','server','routers'] },
7233
+ },
7234
+ srcDirs: ['src','lib','index.js','server.js','app.js'],
7235
+ penalties: ['dist','build','.next','.nuxt','coverage','storybook-static'],
7236
+ },
7237
+ typescript: {
7238
+ manifestFiles: ['package.json','tsconfig.json'],
7239
+ frameworks: {
7240
+ nextjs: { detectionFiles: ['next.config.ts','next.config.mjs'], detectionDeps: ['next'], srcDirs: ['app','src/app','pages','src','components','lib','hooks','utils'] },
7241
+ nestjs: { detectionFiles: ['nest-cli.json'], detectionDeps: ['@nestjs/core'], srcDirs: ['src'], entrypoints: ['src/main.ts'] },
7242
+ angular: { detectionFiles: ['angular.json'], detectionDeps: ['@angular/core'], srcDirs: ['src','src/app','projects','apps','libs'] },
7243
+ },
7244
+ srcDirs: ['src','lib','packages'],
7245
+ penalties: ['dist','build','.next'],
7246
+ },
7247
+ python: {
7248
+ manifestFiles: ['requirements.txt','pyproject.toml','setup.py','Pipfile'],
7249
+ frameworks: {
7250
+ django: { detectionFiles: ['manage.py'], detectionDeps: ['Django'], srcDirs: [], specialRule: 'django-app-dirs', entrypoints: ['manage.py'] },
7251
+ fastapi: { detectionFiles: [], detectionDeps: ['fastapi'], srcDirs: ['app','src','routers','api'], entrypoints: ['main.py','app/main.py'] },
7252
+ flask: { detectionFiles: ['wsgi.py','app.py'], detectionDeps: ['Flask'], srcDirs: ['app','src'], entrypoints: ['app.py','wsgi.py'] },
7253
+ celery: { detectionFiles: [], detectionDeps: ['celery'], srcDirs: ['tasks','workers','app'] },
7254
+ },
7255
+ srcDirs: ['.'],
7256
+ penalties: ['venv','.venv','__pycache__','.pytest_cache','htmlcov'],
7257
+ },
7258
+ go: {
7259
+ manifestFiles: ['go.mod'],
7260
+ frameworks: {
7261
+ gin: { detectionFiles: [], detectionDeps: ['github.com/gin-gonic/gin'], srcDirs: ['internal','cmd','pkg','api','handler','middleware'] },
7262
+ echo: { detectionFiles: [], detectionDeps: ['github.com/labstack/echo'], srcDirs: ['internal','cmd','handler','middleware'] },
7263
+ fiber: { detectionFiles: [], detectionDeps: ['github.com/gofiber/fiber'], srcDirs: ['internal','cmd','handler','routes'] },
7264
+ grpc: { detectionFiles: [], detectionDeps: ['google.golang.org/grpc'], srcDirs: ['internal','proto','server','client'] },
7265
+ chi: { detectionFiles: [], detectionDeps: ['github.com/go-chi/chi'], srcDirs: ['internal','cmd','handler'] },
7266
+ },
7267
+ srcDirs: ['internal','cmd','pkg','api'],
7268
+ penalties: ['vendor'],
7269
+ },
7270
+ rust: {
7271
+ manifestFiles: ['Cargo.toml'],
7272
+ frameworks: {
7273
+ actix: { detectionFiles: [], detectionDeps: ['actix-web'], srcDirs: ['src'] },
7274
+ axum: { detectionFiles: [], detectionDeps: ['axum'], srcDirs: ['src'] },
7275
+ tauri: { detectionFiles: ['src-tauri/tauri.conf.json'], detectionDeps: ['tauri'], srcDirs: ['src','src-tauri/src'] },
7276
+ },
7277
+ srcDirs: ['src'],
7278
+ penalties: ['target'],
7279
+ },
7280
+ java: {
7281
+ manifestFiles: ['pom.xml','build.gradle'],
7282
+ frameworks: {
7283
+ spring: { detectionFiles: [], detectionDeps: ['spring-boot'], srcDirs: ['src/main/java','src/main/kotlin','src/main/resources'] },
7284
+ quarkus: { detectionFiles: [], detectionDeps: ['io.quarkus'], srcDirs: ['src/main/java'] },
7285
+ android: { detectionFiles: ['AndroidManifest.xml'], srcDirs: ['app/src/main/java','app/src/main','src'] },
7286
+ micronaut:{ detectionFiles: [], detectionDeps: ['io.micronaut'],srcDirs: ['src/main/java'] },
7287
+ },
7288
+ srcDirs: ['src/main/java','src'],
7289
+ penalties: ['target','build'],
7290
+ },
7291
+ kotlin: {
7292
+ manifestFiles: ['build.gradle.kts'],
7293
+ frameworks: {
7294
+ spring: { detectionFiles: [], detectionDeps: ['spring-boot'], srcDirs: ['src/main/kotlin'] },
7295
+ android: { detectionFiles: ['AndroidManifest.xml'], srcDirs: ['app/src/main/kotlin','app/src/main/java'] },
7296
+ ktor: { detectionFiles: [], detectionDeps: ['io.ktor'], srcDirs: ['src'] },
7297
+ compose: { detectionFiles: [], detectionDeps: ['compose-runtime'], srcDirs: ['app/src/main/kotlin','src'] },
7298
+ },
7299
+ srcDirs: ['src/main/kotlin','src'],
7300
+ penalties: ['build','.gradle'],
7301
+ },
7302
+ csharp: {
7303
+ manifestFiles: ['.csproj','.sln'],
7304
+ frameworks: {
7305
+ aspnet: { detectionFiles: ['appsettings.json'], detectionDeps: ['Microsoft.AspNetCore'], srcDirs: ['Controllers','Services','Models','Middleware','Pages'] },
7306
+ blazor: { detectionFiles: [], detectionDeps: ['Microsoft.AspNetCore.Components'], srcDirs: ['Components','Pages','Services'] },
7307
+ unity: { detectionFiles: ['ProjectSettings/ProjectSettings.asset'], srcDirs: ['Assets/Scripts','Assets'] },
7308
+ maui: { detectionFiles: [], detectionDeps: ['Microsoft.Maui'], srcDirs: ['src','Pages','ViewModels'] },
7309
+ },
7310
+ srcDirs: ['src','Controllers','Services','Models'],
7311
+ penalties: ['bin','obj','.vs'],
7312
+ },
7313
+ php: {
7314
+ manifestFiles: ['composer.json'],
7315
+ frameworks: {
7316
+ laravel: { detectionFiles: ['artisan'], srcDirs: ['app','routes','config','database','resources','tests'], entrypoints: ['artisan'] },
7317
+ symfony: { detectionFiles: ['symfony.lock'], srcDirs: ['src','config','templates'], specialRule: 'symfony-bundle-dirs' },
7318
+ wordpress: { detectionFiles: ['wp-config.php'], srcDirs: ['wp-content/themes','wp-content/plugins','wp-content/mu-plugins'] },
7319
+ slim: { detectionFiles: [], detectionDeps: ['slim/slim'], srcDirs: ['src','app','routes'] },
7320
+ },
7321
+ srcDirs: ['src','app'],
7322
+ penalties: ['vendor'],
7323
+ },
7324
+ ruby: {
7325
+ manifestFiles: ['Gemfile'],
7326
+ frameworks: {
7327
+ rails: { detectionFiles: ['config/routes.rb'], srcDirs: ['app','lib','config','db','spec','test'], entrypoints: ['config/routes.rb'] },
7328
+ sinatra: { detectionFiles: ['config.ru','app.rb'], srcDirs: ['.','lib'], entrypoints: ['app.rb','config.ru'] },
7329
+ hanami: { detectionFiles: [], detectionDeps: ['hanami'], srcDirs: ['apps','lib','slices'] },
7330
+ },
7331
+ srcDirs: ['app','lib'],
7332
+ penalties: ['vendor','coverage','.bundle'],
7333
+ },
7334
+ swift: {
7335
+ manifestFiles: ['Package.swift'],
7336
+ frameworks: {
7337
+ vapor: { detectionFiles: [], detectionDeps: ['vapor/vapor'], srcDirs: ['Sources','App'] },
7338
+ swiftui: { detectionFiles: ['.xcodeproj'], srcDirs: [], specialRule: 'swift-project-dir' },
7339
+ swiftpm: { detectionFiles: ['Package.swift'],srcDirs: ['Sources'] },
7340
+ },
7341
+ srcDirs: ['Sources','Source'],
7342
+ penalties: ['.build','DerivedData','Pods','Carthage'],
7343
+ },
7344
+ dart: {
7345
+ manifestFiles: ['pubspec.yaml'],
7346
+ frameworks: {
7347
+ flutter: { detectionFiles: [], detectionDeps: ['flutter'], srcDirs: ['lib','lib/src'], entrypoints: ['lib/main.dart'] },
7348
+ serverpod: { detectionFiles: [], detectionDeps: ['serverpod'], srcDirs: ['lib','endpoints','models'] },
7349
+ 'dart-frog':{ detectionFiles: ['dart_frog.yaml'], srcDirs: ['routes','lib'] },
7350
+ },
7351
+ srcDirs: ['lib','lib/src'],
7352
+ penalties: ['.dart_tool','build'],
7353
+ },
7354
+ scala: {
7355
+ manifestFiles: ['build.sbt'],
7356
+ frameworks: {
7357
+ akka: { detectionFiles: [], detectionDeps: ['akka'], srcDirs: ['src/main/scala','src'] },
7358
+ play: { detectionFiles: [], detectionDeps: ['play'], srcDirs: ['app','conf'] },
7359
+ spark: { detectionFiles: [], detectionDeps: ['spark'],srcDirs: ['src/main/scala'] },
7360
+ zio: { detectionFiles: [], detectionDeps: ['zio'], srcDirs: ['src/main/scala'] },
7361
+ },
7362
+ srcDirs: ['src/main/scala','src'],
7363
+ penalties: ['target'],
7364
+ },
7365
+ };
7366
+ module.exports = { REGISTRY };
7367
+ };
7368
+
7369
+ // ── ./src/discovery/sigmapignore ──
7370
+ __factories["./src/discovery/sigmapignore"] = function(module, exports) {
7371
+ 'use strict';
7372
+ const fs = require('fs');
7373
+ const path = require('path');
7374
+ function loadIgnorePatterns(cwd) {
7375
+ for (const fname of ['.sigmapignore', '.contextignore']) {
7376
+ const p = path.join(cwd, fname);
7377
+ if (fs.existsSync(p)) {
7378
+ return fs.readFileSync(p, 'utf8')
7379
+ .split('\n')
7380
+ .map(l => l.trim())
7381
+ .filter(l => l && !l.startsWith('#'));
7382
+ }
7383
+ }
7384
+ return [];
7385
+ }
7386
+ function matchesIgnorePattern(dirName, patterns) {
7387
+ for (const pat of patterns) {
7388
+ const clean = pat.replace(/\/$/, '');
7389
+ if (clean === dirName) return true;
7390
+ if (clean.endsWith('/**') && dirName.startsWith(clean.slice(0, -3))) return true;
7391
+ if (clean.endsWith('/*') && dirName.startsWith(clean.slice(0, -2))) return true;
7392
+ }
7393
+ return false;
7394
+ }
7395
+ module.exports = { loadIgnorePatterns, matchesIgnorePattern };
7396
+ };
7397
+
7398
+ // ── ./src/discovery/language-detector ──
7399
+ __factories["./src/discovery/language-detector"] = function(module, exports) {
7400
+ 'use strict';
7401
+ const fs = require('fs');
7402
+ const path = require('path');
7403
+ const { REGISTRY } = __require('./src/discovery/source-root-registry');
7404
+ function detectLanguages(cwd) {
7405
+ const weights = {};
7406
+ for (const [lang, reg] of Object.entries(REGISTRY)) {
7407
+ for (const mf of (reg.manifestFiles || [])) {
7408
+ if (fs.existsSync(path.join(cwd, mf))) {
7409
+ weights[lang] = (weights[lang] || 0) + 3;
7410
+ }
7411
+ }
7412
+ }
7413
+ try {
7414
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
7415
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
7416
+ if (allDeps.typescript) { weights.typescript = (weights.typescript || 0) + 2; }
7417
+ } catch (_) {}
7418
+ const extCount = {};
7419
+ (function _walkDepth(dir, depth, extCount) {
7420
+ if (depth <= 0) return;
7421
+ let entries;
7422
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
7423
+ for (const e of entries) {
7424
+ const SKIP_DIRS = new Set(['node_modules','dist','build','.git','.next','.nuxt','vendor','DerivedData','Pods','target','coverage','__pycache__','.venv','venv','.build','Carthage','storybook-static']);
7425
+ if (SKIP_DIRS.has(e.name)) continue;
7426
+ if (e.isDirectory()) {
7427
+ _walkDepth(path.join(dir, e.name), depth - 1, extCount);
7428
+ } else if (e.isFile()) {
7429
+ 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'};
7430
+ const ext = path.extname(e.name).toLowerCase();
7431
+ if (EXT_TO_LANG[ext]) extCount[ext] = (extCount[ext] || 0) + 1;
7432
+ }
7433
+ }
7434
+ })(cwd, 3, extCount);
7435
+ const maxCount = Math.max(1, ...Object.values(extCount));
7436
+ 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'};
7437
+ for (const [ext, count] of Object.entries(extCount)) {
7438
+ const lang = EXT_TO_LANG[ext];
7439
+ if (lang) {
7440
+ weights[lang] = (weights[lang] || 0) + Math.min(5, (count / maxCount) * 5);
7441
+ }
7442
+ }
7443
+ const maxW = Math.max(1, ...Object.values(weights));
7444
+ return Object.entries(weights)
7445
+ .map(([name, w]) => ({ name, weight: Math.round(w / maxW * 100) / 100 }))
7446
+ .sort((a, b) => b.weight - a.weight);
7447
+ }
7448
+ module.exports = { detectLanguages };
7449
+ };
7450
+
7451
+ // ── ./src/discovery/source-root-scorer ──
7452
+ __factories["./src/discovery/source-root-scorer"] = function(module, exports) {
7453
+ 'use strict';
7454
+ const fs = require('fs');
7455
+ const path = require('path');
7456
+ const { execSync } = require('child_process');
7457
+ 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']);
7458
+ 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']);
7459
+ const PENALTY_DIRS = new Set(['test','tests','spec','__tests__','e2e','docs','doc','docs-vp','examples','example','fixtures','mocks','__mocks__','demo','samples','migrations','benchmarks','scripts']);
7460
+ 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'] };
7461
+ function getRecentlyChangedDirs(cwd) {
7462
+ try {
7463
+ const out = execSync('git log --name-only --format="" HEAD~10 2>/dev/null', { cwd, timeout: 3000 }).toString();
7464
+ return new Set(out.split('\n').filter(Boolean).map(f => f.split('/')[0]));
7465
+ } catch { return new Set(); }
7466
+ }
7467
+ function scoreCandidate(dirName, fullPath, context) {
7468
+ const { frameworks, languages, recentDirs, frameworkSrcDirs, entrypoints, frameworkPenalties } = context;
7469
+ if (AUTO_SKIP.has(dirName)) return -99;
7470
+ if (!fs.existsSync(fullPath)) return -99;
7471
+ let score = 0;
7472
+ if (frameworkSrcDirs.has(dirName)) score += 3.0;
7473
+ const sourceFileCount = (function _countSourceFiles(dir, depth) {
7474
+ if (depth <= 0) return 0;
7475
+ let count = 0;
7476
+ try {
7477
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
7478
+ if (e.isFile() && CODE_EXTS.has(path.extname(e.name).toLowerCase())) count++;
7479
+ else if (e.isDirectory() && depth > 1) count += _countSourceFiles(path.join(dir, e.name), depth - 1);
7480
+ }
7481
+ } catch (_) {}
7482
+ return count;
7483
+ })(fullPath, 2);
7484
+ const density = Math.min(1.0, sourceFileCount / 10);
7485
+ score += density * 2.5;
7486
+ if (sourceFileCount >= 3) score += 2.0;
7487
+ if ((entrypoints || []).some(ep => ep.startsWith(dirName + '/'))) score += 1.5;
7488
+ if (fs.existsSync(path.join(fullPath, 'package.json')) ||
7489
+ fs.existsSync(path.join(fullPath, 'go.mod')) ||
7490
+ fs.existsSync(path.join(fullPath, 'Cargo.toml')) ||
7491
+ fs.existsSync(path.join(fullPath, 'pom.xml'))) {
7492
+ score += 1.0;
7493
+ }
7494
+ if (recentDirs.has(dirName)) score += 2.0;
7495
+ if (PENALTY_DIRS.has(dirName.toLowerCase()) && !frameworkSrcDirs.has(dirName)) score -= 3.0;
7496
+ if ((frameworkPenalties || []).includes(dirName)) score -= 3.0;
7497
+ return Math.round(score * 100) / 100;
7498
+ }
7499
+ module.exports = { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS };
7500
+ };
7501
+
7502
+ // ── ./src/discovery/framework-detector ──
7503
+ __factories["./src/discovery/framework-detector"] = function(module, exports) {
7504
+ 'use strict';
7505
+ const fs = require('fs');
7506
+ const path = require('path');
7507
+ const { REGISTRY } = __require('./src/discovery/source-root-registry');
7508
+ function detectFrameworks(cwd) {
7509
+ const detected = [];
7510
+ for (const [lang, reg] of Object.entries(REGISTRY)) {
7511
+ if (!reg.frameworks) continue;
7512
+ for (const [name, fw] of Object.entries(reg.frameworks)) {
7513
+ let confidence = 0;
7514
+ for (const f of (fw.detectionFiles || [])) {
7515
+ if ((function _existsAnywhere(cwd, filename, maxDepth) {
7516
+ const parts = filename.split('/');
7517
+ if (parts.length > 1) return fs.existsSync(path.join(cwd, filename));
7518
+ return (function _walkFind(dir, name, depth) {
7519
+ if (depth <= 0) return false;
7520
+ try {
7521
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
7522
+ for (const e of entries) {
7523
+ if (e.name === name) return true;
7524
+ if (e.isDirectory() && depth > 1) {
7525
+ if (_walkFind(path.join(dir, e.name), name, depth - 1)) return true;
7526
+ }
7527
+ }
7528
+ } catch (_) {}
7529
+ return false;
7530
+ })(cwd, parts[0], maxDepth);
7531
+ })(cwd, f, 3)) { confidence = Math.max(confidence, 0.93); }
7532
+ }
7533
+ if (fw.detectionDeps?.length) {
7534
+ const deps = (function _readDeps(cwd) {
7535
+ try {
7536
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
7537
+ return new Set([...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.devDependencies || {})]);
7538
+ } catch { return new Set(); }
7539
+ })(cwd);
7540
+ for (const dep of fw.detectionDeps) {
7541
+ if (deps.has(dep)) { confidence = Math.max(confidence, 0.90); }
7542
+ }
7543
+ }
7544
+ if (lang === 'go' && fw.detectionDeps?.length) {
7545
+ const goMod = (function _readFile(p) {
7546
+ try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
7547
+ })(path.join(cwd, 'go.mod'));
7548
+ for (const dep of fw.detectionDeps) {
7549
+ if (goMod.includes(dep)) { confidence = Math.max(confidence, 0.90); }
7550
+ }
7551
+ }
7552
+ if (lang === 'rust' && fw.detectionDeps?.length) {
7553
+ const cargoToml = (function _readFile(p) {
7554
+ try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
7555
+ })(path.join(cwd, 'Cargo.toml'));
7556
+ for (const dep of fw.detectionDeps) {
7557
+ if (cargoToml.includes(dep)) { confidence = Math.max(confidence, 0.88); }
7558
+ }
7559
+ }
7560
+ if (fw.specialRule === 'django-app-dirs' && fs.existsSync(path.join(cwd, 'manage.py'))) {
7561
+ confidence = Math.max(confidence, 0.95);
7562
+ }
7563
+ if (fw.specialRule === 'swift-project-dir' && (function _existsAnywhere(cwd, filename, maxDepth) {
7564
+ const parts = filename.split('/');
7565
+ if (parts.length > 1) return fs.existsSync(path.join(cwd, filename));
7566
+ return (function _walkFind(dir, name, depth) {
7567
+ if (depth <= 0) return false;
7568
+ try {
7569
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
7570
+ for (const e of entries) {
7571
+ if (e.name === name) return true;
7572
+ if (e.isDirectory() && depth > 1) {
7573
+ if (_walkFind(path.join(dir, e.name), name, depth - 1)) return true;
7574
+ }
7575
+ }
7576
+ } catch (_) {}
7577
+ return false;
7578
+ })(cwd, parts[0], maxDepth);
7579
+ })(cwd, '.xcodeproj', 2)) {
7580
+ confidence = Math.max(confidence, 0.90);
7581
+ }
7582
+ if (confidence > 0) detected.push({ name, language: lang, confidence });
7583
+ }
7584
+ }
7585
+ return detected.sort((a, b) => b.confidence - a.confidence);
7586
+ }
7587
+ module.exports = { detectFrameworks };
7588
+ };
7589
+
7590
+ // ── ./src/discovery/source-root-resolver ──
7591
+ __factories["./src/discovery/source-root-resolver"] = function(module, exports) {
7592
+ 'use strict';
7593
+ const fs = require('fs');
7594
+ const path = require('path');
7595
+ const { REGISTRY } = __require('./src/discovery/source-root-registry');
7596
+ const { detectLanguages } = __require('./src/discovery/language-detector');
7597
+ const { detectFrameworks } = __require('./src/discovery/framework-detector');
7598
+ const { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS } = __require('./src/discovery/source-root-scorer');
7599
+ const { loadIgnorePatterns, matchesIgnorePattern } = __require('./src/discovery/sigmapignore');
7600
+ function resolveSourceRoots(cwd, opts = {}) {
7601
+ const ignorePatterns = loadIgnorePatterns(cwd);
7602
+ const languages = detectLanguages(cwd);
7603
+ const frameworks = detectFrameworks(cwd);
7604
+ const recentDirs = getRecentlyChangedDirs(cwd);
7605
+ const isMonorepo = (function _detectMonorepo(cwd) {
7606
+ const MONOREPO_MARKERS = ['pnpm-workspace.yaml','turbo.json','nx.json','lerna.json'];
7607
+ for (const m of MONOREPO_MARKERS) {
7608
+ if (fs.existsSync(path.join(cwd, m))) return true;
7609
+ }
7610
+ try {
7611
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
7612
+ if (pkg.workspaces) return true;
7613
+ } catch (_) {}
7614
+ return false;
7615
+ })(cwd);
7616
+ const primaryLang = languages[0]?.name;
7617
+ const primaryFw = frameworks[0];
7618
+ const registry = primaryLang ? REGISTRY[primaryLang] : null;
7619
+ const fwEntry = primaryFw && registry?.frameworks?.[primaryFw.name];
7620
+ const frameworkSrcDirs = new Set(fwEntry?.srcDirs || registry?.srcDirs || []);
7621
+ const entrypoints = fwEntry?.entrypoints || [];
7622
+ const frameworkPenalties = registry?.penalties || [];
7623
+ const context = { frameworks, languages, recentDirs, frameworkSrcDirs, entrypoints, frameworkPenalties };
7624
+ const candidates = (function _enumerateCandidates(cwd, isMonorepo, ignorePatterns, excludeList) {
7625
+ const candidates = [];
7626
+ const excSet = new Set(excludeList);
7627
+ try {
7628
+ for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
7629
+ if (!e.isDirectory()) continue;
7630
+ if (excSet.has(e.name)) continue;
7631
+ if (matchesIgnorePattern(e.name, ignorePatterns)) continue;
7632
+ candidates.push({ name: e.name, full: path.join(cwd, e.name) });
7633
+ }
7634
+ } catch (_) {}
7635
+ if (isMonorepo) {
7636
+ for (const top of ['packages','apps','services','modules']) {
7637
+ const topFull = path.join(cwd, top);
7638
+ if (!fs.existsSync(topFull)) continue;
7639
+ try {
7640
+ for (const pkg of fs.readdirSync(topFull, { withFileTypes: true })) {
7641
+ if (!pkg.isDirectory()) continue;
7642
+ const srcFull = path.join(topFull, pkg.name, 'src');
7643
+ if (fs.existsSync(srcFull)) {
7644
+ candidates.push({ name: `${top}/${pkg.name}/src`, full: srcFull });
7645
+ }
7646
+ candidates.push({ name: `${top}/${pkg.name}`, full: path.join(topFull, pkg.name) });
7647
+ }
7648
+ } catch (_) {}
7649
+ }
7650
+ }
7651
+ 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'];
7652
+ for (const dp of DEEP_PATHS) {
7653
+ const full = path.join(cwd, dp);
7654
+ if (fs.existsSync(full)) candidates.push({ name: dp, full });
7655
+ }
7656
+ return candidates;
7657
+ })(cwd, isMonorepo, ignorePatterns, opts.exclude || []);
7658
+ const scored = candidates
7659
+ .map(({ name, full }) => ({
7660
+ dir: name,
7661
+ full,
7662
+ score: scoreCandidate(name, full, context),
7663
+ }))
7664
+ .filter(c => c.score > 0)
7665
+ .sort((a, b) => b.score - a.score);
7666
+ let roots = (function _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks) {
7667
+ let roots = [...scored];
7668
+ if (primaryFw?.name === 'django' || frameworks.some(f => f.name === 'django')) {
7669
+ try {
7670
+ for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
7671
+ if (!e.isDirectory()) continue;
7672
+ const d = path.join(cwd, e.name);
7673
+ if (fs.existsSync(path.join(d, 'models.py')) || fs.existsSync(path.join(d, 'views.py'))) {
7674
+ if (!roots.find(r => r.dir === e.name)) {
7675
+ roots.push({ dir: e.name, full: d, score: 5.0 });
7676
+ }
7677
+ }
7678
+ }
7679
+ } catch (_) {}
7680
+ roots.sort((a, b) => b.score - a.score);
7681
+ }
7682
+ if (frameworks.some(f => f.name === 'swiftui')) {
7683
+ try {
7684
+ for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
7685
+ if (!e.isDirectory()) continue;
7686
+ const d = path.join(cwd, e.name);
7687
+ const swiftCount = (fs.readdirSync(d).filter(f => f.endsWith('.swift'))).length;
7688
+ if (swiftCount >= 3 && !roots.find(r => r.dir === e.name)) {
7689
+ roots.push({ dir: e.name, full: d, score: 4.0 });
7690
+ }
7691
+ }
7692
+ } catch (_) {}
7693
+ roots.sort((a, b) => b.score - a.score);
7694
+ }
7695
+ return roots;
7696
+ })(scored, cwd, primaryFw, fwEntry, frameworks);
7697
+ roots = (function _dedupeNested(scored) {
7698
+ const result = [];
7699
+ for (const c of scored) {
7700
+ const isNested = result.some(r => c.dir.startsWith(r.dir + '/'));
7701
+ if (!isNested) result.push(c);
7702
+ }
7703
+ return result;
7704
+ })(roots);
7705
+ const MAX_ROOTS = 6;
7706
+ roots = roots.slice(0, MAX_ROOTS).map(r => r.dir);
7707
+ const confidence = (function _computeConfidence(frameworks, languages, scoredCount) {
7708
+ if (frameworks.length > 0 && frameworks[0].confidence >= 0.90) return 'high';
7709
+ if (languages.length > 0 && scoredCount > 0) return 'medium';
7710
+ return 'low';
7711
+ })(frameworks, languages, scored.length);
7712
+ return {
7713
+ roots,
7714
+ languages,
7715
+ frameworks,
7716
+ confidence,
7717
+ explanation: scored.slice(0, 8).map(c => ({
7718
+ dir: c.dir,
7719
+ score: c.score,
7720
+ reason: `score: ${c.score}`,
7721
+ })),
7722
+ isMonorepo,
7723
+ };
7724
+ }
7725
+ module.exports = { resolveSourceRoots };
7726
+ };
7727
+
7213
7728
  /**
7214
7729
  * SigMap — gen-context.js v1.2.0
7215
7730
  * Zero-dependency AI context engine.
@@ -7222,7 +7737,7 @@ const path = require('path');
7222
7737
  const os = require('os');
7223
7738
  const { execSync } = require('child_process');
7224
7739
 
7225
- const VERSION = '6.5.1';
7740
+ const VERSION = '6.5.2';
7226
7741
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
7227
7742
 
7228
7743
  function requireSourceOrBundled(key) {
@@ -8429,6 +8944,13 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
8429
8944
  const hotCommits = config.hotCommits || 10;
8430
8945
  const recentFiles = config.diffPriority ? getRecentlyCommittedFiles(cwd, hotCommits) : new Set();
8431
8946
 
8947
+ // v6.7: Load signature cache if enabled
8948
+ let cache = null;
8949
+ const { loadCache, saveCache, getChangedFiles, updateCacheEntries } = requireSourceOrBundled('./src/cache/sig-cache');
8950
+ if (config.sigCache) {
8951
+ cache = loadCache(cwd, VERSION);
8952
+ }
8953
+
8432
8954
  let inputTokenTotal = 0;
8433
8955
  let fileEntries = [];
8434
8956
  let testIndex = null;
@@ -8449,7 +8971,23 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
8449
8971
  continue;
8450
8972
  }
8451
8973
 
8452
- let sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
8974
+ // v6.7: Check cache before extracting
8975
+ let sigs = [];
8976
+ if (cache) {
8977
+ const cached = cache.get(filePath);
8978
+ let mtime = 0;
8979
+ try { mtime = fs.statSync(filePath).mtimeMs; } catch (_) {}
8980
+ if (cached && cached.mtime === mtime) {
8981
+ sigs = cached.sigs;
8982
+ } else {
8983
+ sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
8984
+ if (sigs.length > 0) {
8985
+ cache.set(filePath, { mtime, sigs });
8986
+ }
8987
+ }
8988
+ } else {
8989
+ sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
8990
+ }
8453
8991
  if (sigs.length === 0) continue;
8454
8992
 
8455
8993
  // Baseline = estimated tokens of original source content for intuitive reduction stats.
@@ -8651,6 +9189,15 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
8651
9189
  process.stderr.write(lines.join('\n'));
8652
9190
  }
8653
9191
 
9192
+ // v6.7: Save cache if enabled
9193
+ if (config.sigCache && cache) {
9194
+ try {
9195
+ saveCache(cwd, VERSION, cache);
9196
+ } catch (err) {
9197
+ console.warn(`[sigmap] cache save failed: ${err.message}`);
9198
+ }
9199
+ }
9200
+
8654
9201
  return result;
8655
9202
  }
8656
9203
 
@@ -9954,6 +10501,19 @@ function main() {
9954
10501
  const fakeEntries = allFiles.map(f => ({ filePath: f }));
9955
10502
  coverageResult = coverageScore(cwd, fakeEntries, cfg);
9956
10503
  } catch (_) {}
10504
+ // v6.7: Collect cache stats if enabled
10505
+ let cacheStats = null;
10506
+ try {
10507
+ const cachePath = path.join(cwd, '.sigmap-cache.json');
10508
+ if (fs.existsSync(cachePath)) {
10509
+ const raw = fs.readFileSync(cachePath, 'utf8');
10510
+ const data = JSON.parse(raw);
10511
+ const sizeKb = Math.round(Buffer.byteLength(raw) / 1024);
10512
+ const entries = Object.keys(data.entries || {}).length;
10513
+ const mtime = fs.statSync(cachePath).mtimeMs;
10514
+ cacheStats = { sizeKb, entries, mtimeMs: mtime };
10515
+ }
10516
+ } catch (_) {}
9957
10517
  if (args.includes('--json')) {
9958
10518
  // Feature 3 (VS Code) + Feature 5 (JetBrains): emit tokens + reduction for plugins
9959
10519
  const ctxPath = path.join(cwd, '.github', 'copilot-instructions.md');
@@ -9970,6 +10530,9 @@ function main() {
9970
10530
  payload.coverageTotalFiles = coverageResult.total;
9971
10531
  payload.coverageIncludedFiles = coverageResult.included;
9972
10532
  }
10533
+ if (cacheStats) {
10534
+ payload.cacheStats = cacheStats;
10535
+ }
9973
10536
  process.stdout.write(JSON.stringify(payload) + '\n');
9974
10537
  } else {
9975
10538
  console.log('[sigmap] health:');
@@ -9983,6 +10546,9 @@ function main() {
9983
10546
  if (result.strategyFreshnessDays !== null) {
9984
10547
  console.log(` cold freshness : ${result.strategyFreshnessDays} day(s)`);
9985
10548
  }
10549
+ if (cacheStats) {
10550
+ console.log(` sig-cache : ${cacheStats.entries} files cached ${cacheStats.sizeKb}KB on disk`);
10551
+ }
9986
10552
  console.log(` total runs : ${result.totalRuns}`);
9987
10553
  console.log(` over-budget runs: ${result.overBudgetRuns}`);
9988
10554
  console.log(` p50 token count : ${result.p50TokenCount}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.5.1",
3
+ "version": "6.5.2",
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.5.1",
3
+ "version": "6.5.2",
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.5.1",
3
+ "version": "6.5.2",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '6.5.1',
21
+ version: '6.5.2',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -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: for each file with score > 0, add graphBoost to 1-hop forward
202
- // neighbors that are also in the index. sigIndex uses relative paths; graph uses absolute.
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 a map: relative path index position in scored array for O(1) lookup
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 += weights.graphBoost;
220
- scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + weights.graphBoost;
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 };