sigmap 6.5.0 → 6.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -47,6 +47,26 @@ Use this marker block for all appendable context files:
47
47
  <!-- Updated by gen-context.js -->
48
48
  # Code signatures
49
49
 
50
+ ## SigMap commands
51
+
52
+ | When | Command |
53
+ |------|---------|
54
+ | Before answering a question | `sigmap ask "<your question>"` |
55
+ | After code changes | `sigmap validate` |
56
+ | To query by topic | `sigmap --query "<topic>"` |
57
+
58
+ Always run `sigmap ask` or `sigmap --query` before searching for files relevant to a task.
59
+ ## changes (last 5 commits — 6 minutes ago)
60
+ ```
61
+ src/config/loader.js +_legacyDetectAutoSrcDirs ~detectAutoSrcDirs
62
+ src/discovery/language-detector.js +detectLanguages +_walkDepth
63
+ src/discovery/framework-detector.js +detectFrameworks +_readDeps +_readFile +_existsAnywhere
64
+ src/discovery/source-root-resolver.js +resolveSourceRoots +_detectMonorepo +_enumerateCandidates +_applySpecialRules
65
+ src/discovery/source-root-scorer.js +getRecentlyChangedDirs +scoreCandidate +_countSourceFiles
66
+ src/discovery/sigmapignore.js +loadIgnorePatterns +matchesIgnorePattern
67
+ src/retrieval/ranker.js +_computePenalty ~scoreFile ~rank ~buildSigIndex
68
+ ```
69
+
50
70
  ## packages
51
71
 
52
72
  ### packages/cli/index.js
@@ -104,15 +124,7 @@ function score(cwd) → { * score: number, * grad
104
124
  function adapt(context, adapterName, opts = {}) → string
105
125
  ```
106
126
 
107
- ### packages/adapters/codex.js
108
- ```
109
- module.exports = { name, format, outputPath, write }
110
- function format(context, opts = {}) → string
111
- function outputPath(cwd) → string
112
- function write(context, cwd, opts = {})
113
- ```
114
-
115
- ### packages/adapters/claude.js
127
+ ### packages/adapters/copilot.js
116
128
  ```
117
129
  module.exports = { name, format, outputPath, write }
118
130
  function format(context, opts = {}) → string
@@ -121,13 +133,12 @@ function outputPath(cwd) → string
121
133
  function write(context, cwd, opts = {})
122
134
  ```
123
135
 
124
- ### packages/adapters/copilot.js
136
+ ### packages/adapters/cursor.js
125
137
  ```
126
- module.exports = { name, format, outputPath, write }
138
+ module.exports = { name, format, outputPath }
127
139
  function format(context, opts = {}) → string
128
140
  function _confidenceMeta(opts)
129
141
  function outputPath(cwd) → string
130
- function write(context, cwd, opts = {})
131
142
  ```
132
143
 
133
144
  ### packages/adapters/gemini.js
@@ -139,14 +150,6 @@ function write(context, cwd, opts = {})
139
150
  function _confidenceMeta(opts)
140
151
  ```
141
152
 
142
- ### packages/adapters/cursor.js
143
- ```
144
- module.exports = { name, format, outputPath }
145
- function format(context, opts = {}) → string
146
- function _confidenceMeta(opts)
147
- function outputPath(cwd) → string
148
- ```
149
-
150
153
  ### packages/adapters/openai.js
151
154
  ```
152
155
  module.exports = { name, format, outputPath }
@@ -163,74 +166,24 @@ function _confidenceMeta(opts)
163
166
  function outputPath(cwd) → string
164
167
  ```
165
168
 
166
- ## src
167
-
168
- ### src/security/patterns.js
169
- ```
170
- module.exports = { PATTERNS }
171
- ```
172
-
173
- ### src/security/scanner.js
174
- ```
175
- module.exports = { scan }
176
- function scan(signatures, filePath) → { safe: string[], redacte
177
- ```
178
-
179
- ### src/extractors/cpp.js
180
- ```
181
- module.exports = { extract }
182
- function extract(src) → string[]
183
- function extractBlock(src, startIndex)
184
- function extractMembers(block)
185
- function normalizeParams(params)
186
- function normalizeType(type)
187
- ```
188
-
189
- ### src/extractors/csharp.js
190
- ```
191
- module.exports = { extract }
192
- function extract(src) → string[]
193
- function extractBlock(src, startIndex)
194
- function extractMembers(block)
195
- function normalizeParams(params)
196
- function normalizeType(type)
197
- ```
198
-
199
- ### src/extractors/dart.js
200
- ```
201
- module.exports = { extract }
202
- function extract(src) → string[]
203
- function extractBlock(src, startIndex)
204
- function extractMembers(block)
205
- function normalizeParams(params)
206
- ```
207
-
208
- ### src/extractors/deps.js
169
+ ### packages/adapters/codex.js
209
170
  ```
210
- module.exports = { extractPythonDeps, extractTSDeps, buildReverseDepMap }
211
- function extractPythonDeps(src) → string[]
212
- function extractTSDeps(src) → string[]
213
- function buildReverseDepMap(forwardMap) → Map<string, string[]>
171
+ module.exports = { name, format, outputPath, write }
172
+ function format(context, opts = {}) → string
173
+ function outputPath(cwd) → string
174
+ function write(context, cwd, opts = {})
214
175
  ```
215
176
 
216
- ### src/extractors/go.js
177
+ ### packages/adapters/claude.js
217
178
  ```
218
- module.exports = { extract }
219
- function extract(src) → string[]
220
- function extractBlock(src, startIndex)
221
- function extractInterfaceMethods(block)
222
- function normalizeParams(params)
179
+ module.exports = { name, format, outputPath, write }
180
+ function format(context, opts = {}) → string
181
+ function _confidenceMeta(opts)
182
+ function outputPath(cwd) → string
183
+ function write(context, cwd, opts = {})
223
184
  ```
224
185
 
225
- ### src/extractors/java.js
226
- ```
227
- module.exports = { extract }
228
- function extract(src) → string[]
229
- function extractBlock(src, startIndex)
230
- function extractMembers(block)
231
- function normalizeParams(params)
232
- function normalizeType(type)
233
- ```
186
+ ## src
234
187
 
235
188
  ### src/extractors/javascript.js
236
189
  ```
@@ -622,18 +575,6 @@ function queryContext(args, cwd)
622
575
  function getImpact(args, cwd)
623
576
  ```
624
577
 
625
- ### src/retrieval/ranker.js
626
- ```
627
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent }
628
- function scoreFile(filePath, sigs, queryTokens, weights) → number
629
- function rank(query, sigIndex, opts) → { file: string, score: nu
630
- function _parseContextFile(contextPath) → Map<string, string[]>
631
- function buildSigIndex(cwd, opts) → Map<string, string[]>
632
- function formatRankTable(results, query) → string
633
- function formatRankJSON(results, query) → object
634
- function detectIntent(query)
635
- ```
636
-
637
578
  ### src/tracking/logger.js
638
579
  ```
639
580
  module.exports = { logRun, readLog, summarize }
@@ -652,15 +593,6 @@ function extractClassMembers(block)
652
593
  function normalizeParams(params)
653
594
  ```
654
595
 
655
- ### src/config/loader.js
656
- ```
657
- module.exports = { loadConfig, loadBaseConfig }
658
- function loadBaseConfig(extendsVal, cwd)
659
- function detectAutoSrcDirs(cwd, excludeList) → string[]
660
- function loadConfig(cwd) → object
661
- function deepClone(obj)
662
- ```
663
-
664
596
  ### src/learning/weights.js
665
597
  ```
666
598
  module.exports = { BASELINE, DECAY, MAX_MULT, MIN_MULT, weightsPath, clampMultiplier, normalizeFile, loadWeights, saveWeights, updateWeights, boostFiles, penalizeFiles, resetWeights, exportWeights, importWeights }
@@ -678,6 +610,77 @@ function exportWeights(cwd, outputPath)
678
610
  function importWeights(cwd, importPath, replace)
679
611
  ```
680
612
 
613
+ ### src/config/loader.js
614
+ ```
615
+ module.exports = { loadConfig, loadBaseConfig }
616
+ function loadBaseConfig(extendsVal, cwd)
617
+ function detectAutoSrcDirs(cwd, excludeList) → string[]
618
+ function _legacyDetectAutoSrcDirs(cwd, excludeList) → string[]
619
+ function loadConfig(cwd) → object
620
+ function deepClone(obj)
621
+ ```
622
+
623
+ ### src/discovery/language-detector.js
624
+ ```
625
+ module.exports = { detectLanguages }
626
+ function detectLanguages(cwd)
627
+ function _walkDepth(dir, depth, extCount)
628
+ ```
629
+
630
+ ### src/discovery/framework-detector.js
631
+ ```
632
+ module.exports = { detectFrameworks }
633
+ function detectFrameworks(cwd)
634
+ function _readDeps(cwd)
635
+ function _readFile(p)
636
+ function _existsAnywhere(cwd, filename, maxDepth)
637
+ function _walkFind(dir, name, depth)
638
+ ```
639
+
640
+ ### src/discovery/source-root-registry.js
641
+ ```
642
+ module.exports = { REGISTRY }
643
+ ```
644
+
645
+ ### src/discovery/source-root-resolver.js
646
+ ```
647
+ module.exports = { resolveSourceRoots }
648
+ function resolveSourceRoots(cwd, opts = {})
649
+ function _detectMonorepo(cwd)
650
+ function _enumerateCandidates(cwd, isMonorepo, ignorePatterns, excludeList)
651
+ function _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks)
652
+ function _dedupeNested(scored)
653
+ function _computeConfidence(frameworks, languages, scoredCount)
654
+ ```
655
+
656
+ ### src/discovery/source-root-scorer.js
657
+ ```
658
+ module.exports = { scoreCandidate, getRecentlyChangedDirs, ROOT_ENTRYPOINTS }
659
+ function getRecentlyChangedDirs(cwd)
660
+ function scoreCandidate(dirName, fullPath, context)
661
+ function _countSourceFiles(dir, depth)
662
+ ```
663
+
664
+ ### src/discovery/sigmapignore.js
665
+ ```
666
+ module.exports = { loadIgnorePatterns, matchesIgnorePattern }
667
+ function loadIgnorePatterns(cwd)
668
+ function matchesIgnorePattern(dirName, patterns)
669
+ ```
670
+
671
+ ### src/retrieval/ranker.js
672
+ ```
673
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent }
674
+ function _computePenalty(filePath)
675
+ function scoreFile(filePath, sigs, queryTokens, weights) → { score: number, signals:
676
+ function rank(query, sigIndex, opts) → { file: string, score: nu
677
+ function _parseContextFile(contextPath) → Map<string, string[]>
678
+ function buildSigIndex(cwd, opts) → Map<string, string[]>
679
+ function formatRankTable(results, query) → string
680
+ function formatRankJSON(results, query) → object
681
+ function detectIntent(query)
682
+ ```
683
+
681
684
  ### src/mcp/server.js
682
685
  ```
683
686
  module.exports = { start }
package/CHANGELOG.md CHANGED
@@ -10,6 +10,21 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [6.5.1] — 2026-04-25
14
+
15
+ ### Added
16
+
17
+ - **Retrieval explain** — `rank()` and `scoreFile()` now return detailed signal breakdown (exactToken, symbolMatch, prefixMatch, pathMatch, penalty) for transparency in ranking decisions
18
+ - **7-intent ranking** — expanded intent detection from 4 to 7 patterns (debug, explain, refactor, review, test, integrate, navigate). Each intent applies tuned weights to prioritize relevant signals.
19
+ - **Negative-signal penalty layer** — formalized penalties for test files (0.4x), generated code (0.3x), documentation (0.2x), and node_modules (0.0x) to deprioritize non-source content
20
+
21
+ ### Changed
22
+
23
+ - `formatRankTable` now shows penalty column and signals breakdown for top 3 results
24
+ - `formatRankJSON` now includes `intent` and `signals` fields in output for API consumers
25
+
26
+ ---
27
+
13
28
  ## [6.5.0] — 2026-04-25
14
29
 
15
30
  ### Added
package/README.md CHANGED
@@ -38,7 +38,7 @@ Works with Copilot, Claude, Cursor, Windsurf, and any LLM.
38
38
 
39
39
  ## Why SigMap?
40
40
 
41
- - **78.9% hit@5** — right file found in top 5 results (vs 13.6% baseline)
41
+ - **81.1% hit@5** — right file found in top 5 results (vs 13.6% baseline)
42
42
  - **40–98% token reduction** — 2K–4K tokens instead of 80K+
43
43
  - **52.2% task success rate** — up from 10% without context
44
44
  - **1.69 prompts per task** — down from 2.84
@@ -51,7 +51,7 @@ Works with Copilot, Claude, Cursor, Windsurf, and any LLM.
51
51
 
52
52
  | Without SigMap | With SigMap |
53
53
  |---|---|
54
- | ❌ Guessing which files are relevant | ✅ Right file in context — 79% of the time |
54
+ | ❌ Guessing which files are relevant | ✅ Right file in context — 81% of the time |
55
55
  | ❌ Sending the full repo to your AI | ✅ Minimal context — only what matters |
56
56
  | ❌ Embeddings / vector DB required | ✅ Grounded answers, no infra needed |
57
57
 
@@ -75,11 +75,11 @@ Ask → Rank → Context → Validate → Judge → Learn
75
75
  ## Benchmark
76
76
 
77
77
  ```
78
- Benchmark : sigmap-v6.4-main
79
- Date : 2026-04-23
78
+ Benchmark : sigmap-v6.5-main
79
+ Date : 2026-04-25
80
80
 
81
- Hit@5 : 78.9% (baseline 13.6% — 5.8× lift)
82
- Prompt reduction : 40.6%
81
+ Hit@5 : 81.1% (baseline 13.6% — 6.0× lift)
82
+ Prompt reduction : 41.4%
83
83
  Task success : 52.2% (baseline 10%)
84
84
  Prompts / task : 1.69 (baseline 2.84)
85
85
  Token reduction: 40–98% (avg 96.9% across 18 real repos)
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.0',
5390
+ version: '6.5.1',
5391
5391
  description: 'SigMap MCP server — code signatures on demand',
5392
5392
  };
5393
5393
 
@@ -7222,7 +7222,7 @@ const path = require('path');
7222
7222
  const os = require('os');
7223
7223
  const { execSync } = require('child_process');
7224
7224
 
7225
- const VERSION = '6.5.0';
7225
+ const VERSION = '6.5.1';
7226
7226
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
7227
7227
 
7228
7228
  function requireSourceOrBundled(key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.5.0",
3
+ "version": "6.5.1",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "6.5.0",
3
+ "version": "6.5.1",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "6.5.0",
3
+ "version": "6.5.1",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
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.0',
21
+ version: '6.5.1',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -32,19 +32,49 @@ const DEFAULT_WEIGHTS = {
32
32
  graphBoost: 0.4, // additive bonus for 1-hop import neighbors of matching files
33
33
  };
34
34
 
35
+ // Intent-specific weight adjustments
36
+ const INTENT_WEIGHTS = {
37
+ search: DEFAULT_WEIGHTS,
38
+ debug: { ...DEFAULT_WEIGHTS, exactToken: 1.2, pathMatch: 0.6 },
39
+ explain: { ...DEFAULT_WEIGHTS, symbolMatch: 0.8, pathMatch: 0.9 },
40
+ refactor: { ...DEFAULT_WEIGHTS, symbolMatch: 0.9, exactToken: 0.8 },
41
+ review: { ...DEFAULT_WEIGHTS, pathMatch: 1.0, exactToken: 0.9 },
42
+ test: { ...DEFAULT_WEIGHTS, exactToken: 0.7, symbolMatch: 0.4 },
43
+ integrate: { ...DEFAULT_WEIGHTS, graphBoost: 0.7, pathMatch: 1.1 },
44
+ navigate: { ...DEFAULT_WEIGHTS, pathMatch: 1.2, exactToken: 0.9 },
45
+ };
46
+
47
+ // Penalty multipliers for negative signals
48
+ const PENALTY_SIGNALS = {
49
+ testFile: 0.4, // test/spec/__tests__ in path
50
+ generatedCode: 0.3, // dist/build/.next in path
51
+ docsFile: 0.2, // docs/doc/README in path
52
+ nodeModules: 0.0, // node_modules (zero score)
53
+ };
54
+
55
+ function _computePenalty(filePath) {
56
+ const pathLower = filePath.toLowerCase();
57
+ if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
58
+ if (/(^|\/)(test|tests|spec|__tests__|e2e)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.testFile;
59
+ if (/(^|\/)(dist|build|\.next|\.nuxt|out|\.venv|venv)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.generatedCode;
60
+ if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.docsFile;
61
+ return 1.0;
62
+ }
63
+
35
64
  /**
36
- * Score a single file against a query.
65
+ * Score a single file against a query, returning detailed signal breakdown.
37
66
  *
38
67
  * @param {string} filePath - relative file path (e.g. 'src/extractors/python.js')
39
68
  * @param {string[]} sigs - signature strings for this file
40
69
  * @param {string[]} queryTokens - pre-tokenized query
41
70
  * @param {object} weights
42
- * @returns {number}
71
+ * @returns {{ score: number, signals: { exactToken: number, symbolMatch: number, prefixMatch: number, pathMatch: number, penalty: number } }}
43
72
  */
44
73
  function scoreFile(filePath, sigs, queryTokens, weights) {
45
- if (!sigs || sigs.length === 0) return 0;
74
+ if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
46
75
 
47
76
  const w = weights || DEFAULT_WEIGHTS;
77
+ const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath) };
48
78
 
49
79
  // Build token set from all signatures
50
80
  const sigText = sigs.join(' ');
@@ -60,14 +90,19 @@ function scoreFile(filePath, sigs, queryTokens, weights) {
60
90
 
61
91
  // Exact token match in sigs
62
92
  if (sigTokenSet.has(qt)) {
63
- score += w.exactToken;
93
+ const bonus = w.exactToken;
94
+ score += bonus;
95
+ signals.exactToken += bonus;
64
96
 
65
97
  // Bonus: appears directly in a function/class/method name line
66
98
  const nameLineMatch = sigs.some((sig) => {
67
99
  const nt = tokenize(sig.replace(/[^a-zA-Z0-9_\s]/g, ' '));
68
100
  return nt.includes(qt);
69
101
  });
70
- if (nameLineMatch) score += w.symbolMatch;
102
+ if (nameLineMatch) {
103
+ score += w.symbolMatch;
104
+ signals.symbolMatch += w.symbolMatch;
105
+ }
71
106
  }
72
107
 
73
108
  // Prefix match (e.g. query "python" matches "pythonDeps")
@@ -75,6 +110,7 @@ function scoreFile(filePath, sigs, queryTokens, weights) {
75
110
  for (const st of sigTokenSet) {
76
111
  if (st !== qt && st.startsWith(qt)) {
77
112
  score += w.prefixMatch;
113
+ signals.prefixMatch += w.prefixMatch;
78
114
  break; // one bonus per query token
79
115
  }
80
116
  }
@@ -83,10 +119,14 @@ function scoreFile(filePath, sigs, queryTokens, weights) {
83
119
  // Path token match
84
120
  if (pathTokenSet.has(qt)) {
85
121
  score += w.pathMatch;
122
+ signals.pathMatch += w.pathMatch;
86
123
  }
87
124
  }
88
125
 
89
- return score;
126
+ // Apply penalty multiplier
127
+ score *= signals.penalty;
128
+
129
+ return { score, signals };
90
130
  }
91
131
 
92
132
  /**
@@ -101,7 +141,7 @@ function scoreFile(filePath, sigs, queryTokens, weights) {
101
141
  * @param {object} [opts.weights] - override scoring weights
102
142
  * @param {string} [opts.cwd] - project root for learned ranking weights
103
143
  * @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
104
- * @returns {{ file: string, score: number, sigs: string[], tokens: number }[]}
144
+ * @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
105
145
  */
106
146
  function rank(query, sigIndex, opts) {
107
147
  if (!query || typeof query !== 'string') return [];
@@ -110,17 +150,21 @@ function rank(query, sigIndex, opts) {
110
150
  const topK = (opts && opts.topK) || 10;
111
151
  const recencyMultiplier = (opts && opts.recencyBoost) || DEFAULT_WEIGHTS.recencyBoost;
112
152
  const recencySet = (opts && opts.recencySet) || null;
113
- const weights = (opts && opts.weights) ? Object.assign({}, DEFAULT_WEIGHTS, opts.weights) : DEFAULT_WEIGHTS;
114
- const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;
115
153
  const graph = (opts && opts.graph && opts.graph.forward instanceof Map) ? opts.graph : null;
116
154
  const cwd = (opts && opts.cwd) || null;
117
155
 
156
+ // Detect query intent and get appropriate weights
157
+ const intent = detectIntent(query);
158
+ const intentWeights = INTENT_WEIGHTS[intent] || DEFAULT_WEIGHTS;
159
+ const weights = (opts && opts.weights) ? Object.assign({}, intentWeights, opts.weights) : intentWeights;
160
+ const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;
161
+
118
162
  const queryTokens = tokenize(query);
119
163
  if (queryTokens.length === 0) {
120
164
  // Empty query: return top-K by file count (most signatures = most useful)
121
165
  const all = [];
122
166
  for (const [file, sigs] of sigIndex.entries()) {
123
- all.push({ file, score: sigs.length, sigs, tokens: Math.ceil(sigs.join('\n').length / 4) });
167
+ all.push({ file, score: sigs.length, sigs, tokens: Math.ceil(sigs.join('\n').length / 4), intent, signals: {} });
124
168
  }
125
169
  all.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
126
170
  return all.slice(0, topK);
@@ -128,15 +172,20 @@ function rank(query, sigIndex, opts) {
128
172
 
129
173
  const scored = [];
130
174
  for (const [file, sigs] of sigIndex.entries()) {
131
- let score = scoreFile(file, sigs, queryTokens, weights);
175
+ const result = scoreFile(file, sigs, queryTokens, weights);
176
+ let score = result.score;
177
+ const signals = result.signals;
132
178
 
133
179
  // Recency boost
134
180
  if (recencySet && recencySet.has(file) && score > 0) {
135
181
  score *= recencyMultiplier;
182
+ signals.recencyBoost = recencyMultiplier;
136
183
  }
137
184
 
138
185
  if (learnedWeights && score > 0) {
139
- score *= learnedWeights[file] || 1.0;
186
+ const multiplier = learnedWeights[file] || 1.0;
187
+ score *= multiplier;
188
+ signals.learnedWeights = multiplier;
140
189
  }
141
190
 
142
191
  scored.push({
@@ -144,6 +193,8 @@ function rank(query, sigIndex, opts) {
144
193
  score,
145
194
  sigs,
146
195
  tokens: Math.ceil(sigs.join('\n').length / 4),
196
+ intent,
197
+ signals,
147
198
  });
148
199
  }
149
200
 
@@ -166,6 +217,7 @@ function rank(query, sigIndex, opts) {
166
217
  const idx = relToIdx.get(neighborRel);
167
218
  if (idx !== undefined) {
168
219
  scored[idx].score += weights.graphBoost;
220
+ scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + weights.graphBoost;
169
221
  }
170
222
  }
171
223
  }
@@ -286,7 +338,7 @@ function buildSigIndex(cwd, opts) {
286
338
  /**
287
339
  * Format ranked results as a markdown table string.
288
340
  *
289
- * @param {{ file: string, score: number, sigs: string[], tokens: number }[]} results
341
+ * @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
290
342
  * @param {string} query
291
343
  * @returns {string}
292
344
  */
@@ -295,14 +347,17 @@ function formatRankTable(results, query) {
295
347
  return `No matching files found for query: "${query}"\n`;
296
348
  }
297
349
 
350
+ const intent = (results[0] && results[0].intent) || 'search';
298
351
  const lines = [
299
352
  `## Query: ${query}`,
353
+ `Intent: ${intent}`,
300
354
  '',
301
- '| Rank | File | Score | Sigs | Tokens |',
302
- '|------|------|-------|------|--------|',
303
- ...results.map((r, i) =>
304
- `| ${i + 1} | ${r.file} | ${r.score.toFixed(2)} | ${r.sigs.length} | ${r.tokens} |`
305
- ),
355
+ '| Rank | File | Score | Sigs | Penalty |',
356
+ '|------|------|-------|------|---------|',
357
+ ...results.map((r, i) => {
358
+ const penalty = r.signals && r.signals.penalty ? r.signals.penalty.toFixed(2) : '1.00';
359
+ return `| ${i + 1} | ${r.file} | ${r.score.toFixed(2)} | ${r.sigs.length} | ${penalty} |`;
360
+ }),
306
361
  '',
307
362
  ];
308
363
 
@@ -310,6 +365,10 @@ function formatRankTable(results, query) {
310
365
  for (const r of results.slice(0, 3)) {
311
366
  if (r.sigs.length > 0) {
312
367
  lines.push(`### ${r.file}`);
368
+ if (r.signals) {
369
+ const sig = r.signals;
370
+ lines.push(`Signals: exactToken=${(sig.exactToken || 0).toFixed(2)} symbolMatch=${(sig.symbolMatch || 0).toFixed(2)} prefixMatch=${(sig.prefixMatch || 0).toFixed(2)} pathMatch=${(sig.pathMatch || 0).toFixed(2)} penalty=${(sig.penalty || 1).toFixed(2)}`);
371
+ }
313
372
  lines.push('```');
314
373
  lines.push(...r.sigs.slice(0, 10));
315
374
  if (r.sigs.length > 10) lines.push(`... (${r.sigs.length - 10} more)`);
@@ -324,32 +383,38 @@ function formatRankTable(results, query) {
324
383
  /**
325
384
  * Format ranked results as a structured JSON-serialisable object.
326
385
  *
327
- * @param {{ file: string, score: number, sigs: string[], tokens: number }[]} results
386
+ * @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
328
387
  * @param {string} query
329
388
  * @returns {object}
330
389
  */
331
390
  function formatRankJSON(results, query) {
391
+ const intent = (results && results[0] && results[0].intent) || 'search';
332
392
  return {
333
393
  query,
394
+ intent,
334
395
  results: (results || []).map((r, i) => ({
335
396
  rank: i + 1,
336
397
  file: r.file,
337
398
  score: r.score,
338
399
  sigs: r.sigs,
339
400
  tokens: r.tokens,
401
+ signals: r.signals || {},
340
402
  })),
341
403
  totalResults: (results || []).length,
342
404
  };
343
405
  }
344
406
 
345
407
  // ---------------------------------------------------------------------------
346
- // Intent detection
408
+ // Intent detection — 7 intents
347
409
  // ---------------------------------------------------------------------------
348
410
  const INTENT_PATTERNS = {
349
411
  debug: /\b(bug|fix|error|crash|exception|broken|failing|issue|problem|regression)\b/i,
350
- explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me)\b/i,
351
- refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify)\b/i,
352
- review: /\b(review|check|audit|security|pr|pull request|assess)\b/i,
412
+ explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me|teach)\b/i,
413
+ refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify|optimize)\b/i,
414
+ review: /\b(review|check|audit|security|pr|pull request|assess|validate)\b/i,
415
+ test: /\b(test|unit test|integration test|testing|spec|assert|mock)\b/i,
416
+ integrate:/\b(import|integrate|connect|wire|bind|require|export|depend|graph)\b|require[ds]\b/i,
417
+ navigate: /\b(find|locate|where|search|look for|show me|navigate|browse|list)\b/i,
353
418
  };
354
419
 
355
420
  function detectIntent(query) {