sigmap 6.10.0 → 6.10.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
@@ -56,10 +56,17 @@ 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
+ ## deps
60
+ ```
61
+ src/extractors/python_ast.py ← ast
62
+ ```
63
+
59
64
  ## changes (last 5 commits — 0 seconds ago)
60
65
  ```
61
- src/eval/usefulness-scorer.js +scoreUsefulness +computeUsefulnessStats
62
- src/workspace/detector.js +detectWorkspaces +inferPackage +_getMatchLength +scopeToPackage
66
+ src/discovery/language-detector.js ~detectLanguages
67
+ src/extractors/python.js +tryNativeExtract +extract ~extract ~extractDocHint
68
+ src/extractors/python_ast.py +annotation_to_str +format_args +arguments +get_decorator_names
69
+ src/extractors/r.js +extract +definitions +readBalancedParens +normalizeParams
63
70
  ```
64
71
 
65
72
  ## packages
@@ -107,18 +114,6 @@ code-fence js
107
114
  code-fence ---
108
115
  ```
109
116
 
110
- ### packages/core/index.js
111
- ```
112
- module.exports = { extract, rank, buildSigIndex, scan, score, adapt }
113
- function _resolveExtractor(language)
114
- function extract(src, language) → string[]
115
- function rank(query, sigIndex, opts) → { file: string, score: nu
116
- function buildSigIndex(cwd) → Map<string, string[]>
117
- function scan(sigs, filePath) → { safe: string[], redacte
118
- function score(cwd) → { * score: number, * grad
119
- function adapt(context, adapterName, opts = {}) → string
120
- ```
121
-
122
117
  ### packages/adapters/copilot.js
123
118
  ```
124
119
  module.exports = { name, format, outputPath, write }
@@ -178,92 +173,19 @@ function outputPath(cwd) → string
178
173
  function write(context, cwd, opts = {})
179
174
  ```
180
175
 
181
- ## src
182
-
183
- ### src/extractors/python.js
184
- ```
185
- module.exports = { extract }
186
- function extract(src) → string[]
187
- function extractClassMethods(stripped, startIndex)
188
- function tryExtractDataclassFields(stripped, classIndex)
189
- function tryExtractBaseModelFields(stripped, bodyStart)
190
- function extractClassConstants(stripped, startIndex)
191
- function extractReturnType(sigLine)
192
- function normalizeParams(params)
193
- function extractDocHint(src, fnName, fnSigLine)
194
- ```
195
-
196
- ### src/extractors/ruby.js
197
- ```
198
- module.exports = { extract }
199
- function extract(src) → string[]
200
- function normalizeParams(params)
201
- function extractReturnHint(stripped, index)
202
- ```
203
-
204
- ### src/extractors/rust.js
205
- ```
206
- module.exports = { extract }
207
- function extract(src) → string[]
208
- function extractBlock(src, startIndex)
209
- function extractMethods(block)
210
- function normalizeParams(params)
211
- function extractReturnType(afterParen)
212
- ```
213
-
214
- ### src/extractors/scala.js
215
- ```
216
- module.exports = { extract }
217
- function extract(src) → string[]
218
- function extractBlock(src, startIndex)
219
- function extractMembers(block)
220
- function normalizeParams(params)
221
- function normalizeType(type)
222
- ```
223
-
224
- ### src/extractors/svelte.js
225
- ```
226
- module.exports = { extract }
227
- function extract(src) → string[]
228
- function normalizeParams(params)
229
- function normalizeType(type)
230
- ```
231
-
232
- ### src/extractors/swift.js
233
- ```
234
- module.exports = { extract }
235
- function extract(src) → string[]
236
- function extractBlock(src, startIndex)
237
- function extractMembers(block)
238
- function normalizeParams(params)
239
- function extractArrowType(str)
240
- ```
241
-
242
- ### src/extractors/todos.js
243
- ```
244
- module.exports = { extractTodos }
245
- function extractTodos(src) → {line:number, tag:string,
246
- ```
247
-
248
- ### src/extractors/vue.js
176
+ ### packages/core/index.js
249
177
  ```
250
- module.exports = { extract }
251
- function extract(src) → string[]
252
- function normalizeParams(params)
253
- function normalizeType(type)
178
+ module.exports = { extract, rank, buildSigIndex, scan, score, adapt }
179
+ function _resolveExtractor(language)
180
+ function extract(src, language) → string[]
181
+ function rank(query, sigIndex, opts) → { file: string, score: nu
182
+ function buildSigIndex(cwd) → Map<string, string[]>
183
+ function scan(sigs, filePath) → { safe: string[], redacte
184
+ function score(cwd) → { * score: number, * grad
185
+ function adapt(context, adapterName, opts = {}) → string
254
186
  ```
255
187
 
256
- ### src/eval/scorer.js
257
- ```
258
- module.exports = { hitAtK, reciprocalRank, precisionAtK, aggregate, firstRank }
259
- function firstRank(ranked, expected) → number
260
- function normalizePath(p) → string
261
- function hitAtK(ranked, expected, k = 5) → 0|1
262
- function reciprocalRank(ranked, expected) → number
263
- function precisionAtK(ranked, expected, k = 5) → number
264
- function aggregate(results, k = 5) → { * hitAt5: number, // fr
265
- function round(x)
266
- ```
188
+ ## src
267
189
 
268
190
  ### src/eval/runner.js
269
191
  ```
@@ -427,19 +349,6 @@ function detectVersion(cwd)
427
349
  function format(context, cwd, writtenFiles, sigmapVersion)
428
350
  ```
429
351
 
430
- ### src/eval/analyzer.js
431
- ```
432
- module.exports = { analyzeFiles, formatAnalysisTable, formatAnalysisJSON }
433
- function isDockerfile(name)
434
- function getExtractorName(filePath)
435
- function tokenCount(sigs)
436
- function hasCoverage(filePath, cwd)
437
- function loadExtractor(name, cwd)
438
- function analyzeFiles(files, cwd, opts) → object[]
439
- function formatAnalysisTable(stats, showSlow) → string
440
- function formatAnalysisJSON(stats) → object
441
- ```
442
-
443
352
  ### src/format/dashboard.js
444
353
  ```
445
354
  module.exports = { generateDashboardHtml, renderHistoryCharts, computeExtractorCoverage, percentile, overBudgetStreak }
@@ -582,13 +491,6 @@ function _existsAnywhere(cwd, filename, maxDepth)
582
491
  function _walkFind(dir, name, depth)
583
492
  ```
584
493
 
585
- ### src/discovery/language-detector.js
586
- ```
587
- module.exports = { detectLanguages }
588
- function detectLanguages(cwd)
589
- function _walkDepth(dir, depth, extCount)
590
- ```
591
-
592
494
  ### src/discovery/sigmapignore.js
593
495
  ```
594
496
  module.exports = { loadIgnorePatterns, matchesIgnorePattern }
@@ -596,11 +498,6 @@ function loadIgnorePatterns(cwd)
596
498
  function matchesIgnorePattern(dirName, patterns)
597
499
  ```
598
500
 
599
- ### src/discovery/source-root-registry.js
600
- ```
601
- module.exports = { REGISTRY }
602
- ```
603
-
604
501
  ### src/retrieval/ranker.js
605
502
  ```
606
503
  module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent }
@@ -645,6 +542,22 @@ function scoreCandidate(dirName, fullPath, context)
645
542
  function _countSourceFiles(dir, depth)
646
543
  ```
647
544
 
545
+ ### src/eval/usefulness-scorer.js
546
+ ```
547
+ module.exports = { scoreUsefulness, computeUsefulnessStats }
548
+ function scoreUsefulness(taskResult, rankingScore)
549
+ function computeUsefulnessStats(taskResults)
550
+ ```
551
+
552
+ ### src/workspace/detector.js
553
+ ```
554
+ module.exports = { detectWorkspaces, inferPackage, scopeToPackage }
555
+ function detectWorkspaces(cwd)
556
+ function inferPackage(query, workspaceDirs, cwd)
557
+ function _getMatchLength(name, token)
558
+ function scopeToPackage(filePath, packageDir)
559
+ ```
560
+
648
561
  ### src/discovery/source-root-resolver.js
649
562
  ```
650
563
  module.exports = { resolveSourceRoots }
@@ -656,11 +569,70 @@ function _dedupeNested(scored)
656
569
  function _computeConfidence(frameworks, languages, scoredCount)
657
570
  ```
658
571
 
659
- ### src/eval/usefulness-scorer.js
572
+ ### src/discovery/source-root-registry.js
660
573
  ```
661
- module.exports = { scoreUsefulness, computeUsefulnessStats }
662
- function scoreUsefulness(taskResult, rankingScore)
663
- function computeUsefulnessStats(taskResults)
574
+ module.exports = { REGISTRY }
575
+ ```
576
+
577
+ ### src/discovery/language-detector.js
578
+ ```
579
+ module.exports = { detectLanguages }
580
+ function detectLanguages(cwd)
581
+ function _walkDepth(dir, depth, extCount)
582
+ ```
583
+
584
+ ### src/eval/analyzer.js
585
+ ```
586
+ module.exports = { analyzeFiles, formatAnalysisTable, formatAnalysisJSON }
587
+ function isDockerfile(name)
588
+ function getExtractorName(filePath)
589
+ function tokenCount(sigs)
590
+ function hasCoverage(filePath, cwd)
591
+ function loadExtractor(name, cwd)
592
+ function analyzeFiles(files, cwd, opts) → object[]
593
+ function formatAnalysisTable(stats, showSlow) → string
594
+ function formatAnalysisJSON(stats) → object
595
+ ```
596
+
597
+ ### src/extractors/python.js
598
+ ```
599
+ module.exports = { extract, tryNativeExtract }
600
+ function tryNativeExtract(filePath) → string[]|null
601
+ function extract(src, filePath) → string[]
602
+ function extractClassMethods(stripped, startIndex)
603
+ function tryExtractDataclassFields(stripped, classIndex)
604
+ function tryExtractBaseModelFields(stripped, bodyStart)
605
+ function extractClassConstants(stripped, startIndex)
606
+ function extractReturnType(sigLine)
607
+ function normalizeParams(params)
608
+ function extractDocHint(src, fnName, fnSigLine)
609
+ ```
610
+
611
+ ### src/extractors/python_ast.py
612
+ ```
613
+ def annotation_to_str(node) # Convert an AST annotation node to a string representation
614
+ def format_args(args_node) # Format a function arguments node into a compact signature st
615
+ def get_decorator_names(node) # Return a list of decorator name strings for a function/class
616
+ def is_dataclass(node)
617
+ def is_basemodel(bases) # Check if class bases include BaseModel or BaseSettings
618
+ def is_optional_annotation(annotation) # Check if an annotation represents an Optional type
619
+ def get_docstring_hint(node) # Extract first sentence of docstring, if present
620
+ def extract_dataclass_fields(class_node) # Return a collapsed fields string for a @dataclass class
621
+ def extract_basemodel_fields(class_node) # Return a compact {required*, optional
622
+ def extract_class_constants(class_node) # Yield ALL_CAPS constant assignments from class body
623
+ def extract_method_sig(func_node) # Format a method signature string (already indented by caller
624
+ def extract_function_sig(func_node, src_lines) # Format a top-level function signature string
625
+ def extract_fastapi_routes(tree, src_lines) # Extract FastAPI route signatures from top-level decorated fu
626
+ def extract(filepath)
627
+ def main()
628
+ ```
629
+
630
+ ### src/extractors/r.js
631
+ ```
632
+ module.exports = { extract }
633
+ function extract(src) → string[]
634
+ function readBalancedParens(src, openIdx, cap = 4096)
635
+ function normalizeParams(raw)
664
636
  ```
665
637
 
666
638
  ### src/mcp/server.js
@@ -671,12 +643,3 @@ function respondError(id, code, message)
671
643
  function dispatch(msg, cwd)
672
644
  function start(cwd)
673
645
  ```
674
-
675
- ### src/workspace/detector.js
676
- ```
677
- module.exports = { detectWorkspaces, inferPackage, scopeToPackage }
678
- function detectWorkspaces(cwd)
679
- function inferPackage(query, workspaceDirs, cwd)
680
- function _getMatchLength(name, token)
681
- function scopeToPackage(filePath, packageDir)
682
- ```
package/CHANGELOG.md CHANGED
@@ -10,6 +10,22 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [6.10.1] — 2026-05-10
14
+
15
+ ### Added
16
+
17
+ - **R language support (Phase 1)** — Extract function signatures from `.r` and `.R` files with support for function definitions (`<-`, `=`, `<<-` forms), multi-line arguments with string-literal protection, S4 patterns (setGeneric, setMethod, setClass), and private function filtering. Shiny framework detection via `app.R`/`ui.R`/`server.R` triplet.
18
+ - **Native Python AST extractor** — Fallback to `python_ast.py` using `ast.parse()` for accurate extraction of complex signatures (multiline args, stacked decorators, complex generics). Preserves regex fallback for Python 2 / no-Python3 environments. Zero breaking changes to output format.
19
+
20
+ ### Fixed
21
+
22
+ - **ReferenceError in `--query`** — Fixed variable scope issue where `adpIdx` was undefined when no context file present. Moved variable declaration to proper scope before conditional block.
23
+ - **Windows path handling** — Normalized path separators in nested path deduplication. Windows backslashes no longer cause false negatives when matching nested source roots.
24
+ - **.contextignore patterns** — Fixed bracket character classes (`[Bb]in/`) being treated as literals. Fixed trailing slashes on directory patterns not matching nested paths. Added error handling for malformed bracket syntax.
25
+ - **Claude adapter in per-module and hot-cold strategies** — Fixed adapter not being written to output in per-module and hot-cold context strategies.
26
+
27
+ ---
28
+
13
29
  ## [6.10.0] — 2026-05-05
14
30
 
15
31
  ### Added
package/README.md CHANGED
@@ -12,7 +12,8 @@
12
12
  [![Zero deps](https://img.shields.io/badge/dependencies-zero-22c55e)](package.json)
13
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-7c6af7.svg)](LICENSE)
14
14
  [![GitHub Stars](https://img.shields.io/github/stars/manojmallick/sigmap?style=flat&color=f59e0b&logo=github)](https://github.com/manojmallick/sigmap/stargazers)
15
- [![Hacker News](https://img.shields.io/badge/Hacker%20News-Discussion-orange?logo=ycombinator)](https://news.ycombinator.com/item?id=47956790)
15
+ [![Star History Chart](https://api.star-history.com/svg?repos=manojmallick/sigmap&type=Date)](https://star-history.com/#manojmallick/sigmap&Date)
16
+ [![Discover on ShyPD](https://img.shields.io/badge/ShyPD-Discover-7c6af7?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgdmlld0JveD0iMCAwIDE2IDE2Ij48Y2lyY2xlIGN4PSI4IiBjeT0iOCIgcj0iOCIgZmlsbD0id2hpdGUiLz48L3N2Zz4=&logoColor=7c6af7)](https://shypd.ai/tools/sigmap)
16
17
 
17
18
  </div>
18
19
 
@@ -20,9 +21,11 @@
20
21
 
21
22
  ## Try it now
22
23
 
24
+ **No install required.** Run instantly on any machine:
25
+
23
26
  ```bash
24
27
  npx sigmap
25
- sigmap ask "Where is auth handled?"
28
+ npx sigmap ask "Where is auth handled?"
26
29
  ```
27
30
 
28
31
  Zero config. Zero dependencies. Under 10 seconds.
@@ -228,6 +231,20 @@ If SigMap saves you context or API spend, a ⭐ on [GitHub](https://github.com/m
228
231
 
229
232
  ---
230
233
 
234
+ ## Contributing
235
+
236
+ SigMap welcomes contributions!
237
+
238
+ **Before submitting a PR:**
239
+ 1. Read [CONTRIBUTING.md](CONTRIBUTING.md)
240
+ 2. Check [Discussions → Announcements](../../discussions) for workflow setup
241
+ 3. Target the `develop` branch (not main)
242
+ 4. Follow the [contributor checklist](.github/CONTRIBUTOR_CHECKLIST.txt)
243
+
244
+ See [.github/PULL_REQUEST_TEMPLATE.md](.github/PULL_REQUEST_TEMPLATE.md) for the PR checklist. All contributors are credited in the CHANGELOG and release notes.
245
+
246
+ ---
247
+
231
248
  ## Why not embeddings?
232
249
 
233
250
  | | Embeddings | SigMap |
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.10.0',
5390
+ version: '6.10.1',
5391
5391
  description: 'SigMap MCP server — code signatures on demand',
5392
5392
  };
5393
5393
 
@@ -7913,6 +7913,95 @@ __factories["./src/discovery/source-root-resolver"] = function(module, exports)
7913
7913
  module.exports = { resolveSourceRoots };
7914
7914
  };
7915
7915
 
7916
+ // ── ./src/workspace/detector ──
7917
+ __factories["./src/workspace/detector"] = function(module, exports) {
7918
+ 'use strict';
7919
+ const fs = require('fs');
7920
+ const path = require('path');
7921
+ module.exports = { detectWorkspaces, inferPackage, scopeToPackage };
7922
+
7923
+ function detectWorkspaces(cwd) {
7924
+ const pkgPath = path.join(cwd, 'package.json');
7925
+ if (!fs.existsSync(pkgPath)) return [];
7926
+
7927
+ let pkg;
7928
+ try {
7929
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
7930
+ } catch {
7931
+ return [];
7932
+ }
7933
+
7934
+ const patterns = pkg.workspaces || [];
7935
+ const dirs = [];
7936
+
7937
+ // Handle both flat array and object with packages field (Yarn v2 format)
7938
+ const patternArray = Array.isArray(patterns) ? patterns : (patterns.packages || []);
7939
+
7940
+ for (const p of patternArray) {
7941
+ const base = p.replace(/\/\*\*?$/, '');
7942
+ const resolved = path.join(cwd, base);
7943
+ if (fs.existsSync(resolved)) {
7944
+ try {
7945
+ for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
7946
+ if (entry.isDirectory()) dirs.push(path.join(resolved, entry.name));
7947
+ }
7948
+ } catch (_) {}
7949
+ }
7950
+ }
7951
+
7952
+ return dirs;
7953
+ }
7954
+
7955
+ // Infer package from query tokens: "add rate limiting to payments" → "packages/payments"
7956
+ function inferPackage(query, workspaceDirs, cwd) {
7957
+ const tokens = query.toLowerCase().split(/\W+/).filter(t => t.length > 2);
7958
+
7959
+ // Find longest matching package name
7960
+ let bestMatch = null;
7961
+ let bestLen = 0;
7962
+ let bestMatchLen = 0;
7963
+
7964
+ for (const dir of workspaceDirs) {
7965
+ const name = path.basename(dir).toLowerCase();
7966
+ for (const token of tokens) {
7967
+ const matchLen = _getMatchLength(name, token);
7968
+ // Only consider matches; use longest match, and break ties by longest package name
7969
+ if (matchLen > 0 && (matchLen > bestLen || (matchLen === bestLen && name.length > bestMatchLen))) {
7970
+ bestMatch = dir;
7971
+ bestLen = matchLen;
7972
+ bestMatchLen = name.length;
7973
+ }
7974
+ }
7975
+ }
7976
+
7977
+ return bestMatch;
7978
+ }
7979
+
7980
+ function _getMatchLength(name, token) {
7981
+ if (name === token) return 1000 + name.length; // Exact match is best
7982
+ if (name.startsWith(token) && token.length >= 3) return 100 + token.length;
7983
+ if (token.startsWith(name) && name.length >= 3) return name.length;
7984
+ return 0;
7985
+ }
7986
+
7987
+ // Return boost multiplier for files inside the inferred package
7988
+ function scopeToPackage(filePath, packageDir) {
7989
+ const normalized = filePath.replace(/\\/g, '/');
7990
+ const normalizedPkg = packageDir.replace(/\\/g, '/');
7991
+
7992
+ // Ensure we match the directory boundary, not just a prefix
7993
+ // e.g., packages/payment should not match packages/payment-old
7994
+ if (normalized.startsWith(normalizedPkg)) {
7995
+ const afterPrefix = normalized.slice(normalizedPkg.length);
7996
+ // Check if next char is / or if it's the exact match
7997
+ if (afterPrefix === '' || afterPrefix[0] === '/') {
7998
+ return 0.30;
7999
+ }
8000
+ }
8001
+ return 0;
8002
+ }
8003
+ };
8004
+
7916
8005
  /**
7917
8006
  * SigMap — gen-context.js v1.2.0
7918
8007
  * Zero-dependency AI context engine.
@@ -7925,7 +8014,7 @@ const path = require('path');
7925
8014
  const os = require('os');
7926
8015
  const { execSync } = require('child_process');
7927
8016
 
7928
- const VERSION = '6.10.0';
8017
+ const VERSION = '6.10.1';
7929
8018
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
7930
8019
 
7931
8020
  function requireSourceOrBundled(key) {
@@ -8005,14 +8094,22 @@ function loadIgnorePatterns(cwd) {
8005
8094
  function matchesIgnore(relPath, patterns) {
8006
8095
  for (const pat of patterns) {
8007
8096
  const normalized = pat.replace(/\\/g, '/');
8008
- // Simple glob: support * and ** and trailing /
8009
- const regexStr = normalized
8010
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
8097
+ // Strip trailing slash (gitignore style directory patterns)
8098
+ const patternToUse = normalized.endsWith('/')
8099
+ ? normalized.slice(0, -1)
8100
+ : normalized;
8101
+ // Escape regex special chars but NOT brackets (keep them for character classes)
8102
+ const regexStr = patternToUse
8103
+ .replace(/[.+^${}()|\\]/g, '\\$&')
8011
8104
  .replace(/\*\*/g, '___DOUBLE___')
8012
8105
  .replace(/\*/g, '[^/]*')
8013
8106
  .replace(/___DOUBLE___/g, '.*');
8014
- const regex = new RegExp(`(^|/)${regexStr}($|/)`);
8015
- if (regex.test(relPath)) return true;
8107
+ try {
8108
+ const regex = new RegExp(`(^|/)${regexStr}($|/)`);
8109
+ if (regex.test(relPath)) return true;
8110
+ } catch (_) {
8111
+ // Malformed bracket syntax or invalid regex — skip this pattern
8112
+ }
8016
8113
  }
8017
8114
  return false;
8018
8115
  }
@@ -8940,7 +9037,7 @@ function runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal) {
8940
9037
  overviewLines.push('> Inject the relevant module file into your IDE context window.');
8941
9038
  overviewLines.push('> For cross-module questions load both files.');
8942
9039
  const overviewContent = overviewLines.join('\n') + '\n';
8943
- const primaryTargets = (config.outputs || ['copilot']).filter((t) => t !== 'claude');
9040
+ const primaryTargets = config.outputs || ['copilot'];
8944
9041
  writeOutputs(overviewContent, primaryTargets, cwd, config);
8945
9042
 
8946
9043
  const overviewTokens = estimateTokens(overviewContent);
@@ -8959,7 +9056,7 @@ function runHotColdStrategy(cwd, config, fileEntries, recentFiles, inputTokenTot
8959
9056
  const hotContent = hotEntries.length > 0
8960
9057
  ? formatOutput(hotEntries, cwd, false, config, null)
8961
9058
  : '<!-- Generated by SigMap — no recently changed files -->\n';
8962
- const primaryTargets = (config.outputs || ['copilot']).filter((t) => t !== 'claude');
9059
+ const primaryTargets = config.outputs || ['copilot'];
8963
9060
  writeOutputs(hotContent, primaryTargets, cwd, config);
8964
9061
  const hotTokens = estimateTokens(hotContent);
8965
9062
 
@@ -11093,6 +11190,7 @@ function main() {
11093
11190
  // Priority: --output flag > --adapter flag > buildSigIndex probe order
11094
11191
  // (customOutput from config is handled inside buildSigIndex itself)
11095
11192
  let queryOpts;
11193
+ const adpIdx = args.indexOf('--adapter');
11096
11194
 
11097
11195
  // 1. --output <file> pins to an explicit path
11098
11196
  if (config.customOutput) {
@@ -11100,17 +11198,14 @@ function main() {
11100
11198
  }
11101
11199
 
11102
11200
  // 2. --adapter <name> pins to that adapter's output path (if --output not given)
11103
- if (!queryOpts) {
11104
- const adpIdx = args.indexOf('--adapter');
11105
- if (adpIdx >= 0) {
11106
- const adapterName = (args[adpIdx + 1] || '').trim().toLowerCase();
11107
- const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
11108
- if (VALID_ADAPTERS.includes(adapterName)) {
11109
- try {
11110
- const adapterMod = __require('./packages/adapters/' + adapterName);
11111
- queryOpts = { contextPath: adapterMod.outputPath(cwd) };
11112
- } catch (_) {}
11113
- }
11201
+ if (!queryOpts && adpIdx >= 0) {
11202
+ const adapterName = (args[adpIdx + 1] || '').trim().toLowerCase();
11203
+ const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
11204
+ if (VALID_ADAPTERS.includes(adapterName)) {
11205
+ try {
11206
+ const adapterMod = __require('./packages/adapters/' + adapterName);
11207
+ queryOpts = { contextPath: adapterMod.outputPath(cwd) };
11208
+ } catch (_) {}
11114
11209
  }
11115
11210
  }
11116
11211
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.10.0",
3
+ "version": "6.10.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.10.0",
3
+ "version": "6.10.1",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -29,6 +29,7 @@ const EXT_MAP = {
29
29
  '.swift': 'swift',
30
30
  '.dart': 'dart',
31
31
  '.scala': 'scala', '.sc': 'scala',
32
+ '.r': 'r', '.R': 'r',
32
33
  '.vue': 'vue',
33
34
  '.svelte': 'svelte',
34
35
  '.html': 'html', '.htm': 'html',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "6.10.0",
3
+ "version": "6.10.1",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -19,6 +19,7 @@ const EXT_TO_LANG = {
19
19
  '.java': 'java', '.kt': 'kotlin', '.cs': 'csharp', '.cpp': 'cpp',
20
20
  '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.swift': 'swift',
21
21
  '.dart': 'dart', '.scala': 'scala', '.php': 'php',
22
+ '.r': 'r', '.R': 'r',
22
23
  };
23
24
 
24
25
  function detectLanguages(cwd) {
@@ -161,6 +161,15 @@ const REGISTRY = {
161
161
  srcDirs: ['src/main/scala','src'],
162
162
  penalties: ['target'],
163
163
  },
164
+
165
+ r: {
166
+ manifestFiles: ['DESCRIPTION','renv.lock'],
167
+ frameworks: {
168
+ shiny: { detectionFiles: ['app.R','ui.R','server.R'], srcDirs: ['R','inst','tests'], entrypoints: ['app.R','server.R'] },
169
+ },
170
+ srcDirs: ['R','src','inst'],
171
+ penalties: ['renv','packrat','.Rcheck'],
172
+ },
164
173
  };
165
174
 
166
175
  module.exports = { REGISTRY };
@@ -181,7 +181,11 @@ function _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks) {
181
181
  function _dedupeNested(scored) {
182
182
  const result = [];
183
183
  for (const c of scored) {
184
- const isNested = result.some(r => c.dir.startsWith(r.dir + '/'));
184
+ const cNorm = c.dir.replace(/\\/g, '/');
185
+ const isNested = result.some(r => {
186
+ const rNorm = r.dir.replace(/\\/g, '/');
187
+ return cNorm.startsWith(rNorm + '/');
188
+ });
185
189
  if (!isNested) result.push(c);
186
190
  }
187
191
  return result;
@@ -29,6 +29,7 @@ const EXT_MAP = {
29
29
  '.swift': 'swift',
30
30
  '.dart': 'dart',
31
31
  '.scala': 'scala', '.sc': 'scala',
32
+ '.r': 'r', '.R': 'r',
32
33
  '.vue': 'vue',
33
34
  '.svelte': 'svelte',
34
35
  '.html': 'html', '.htm': 'html',
@@ -1,11 +1,42 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Try to extract signatures using the native Python AST extractor.
7
+ * Returns null if Python3 is unavailable or the script returns empty results.
8
+ * @param {string} filePath - Absolute path to the Python file
9
+ * @returns {string[]|null}
10
+ */
11
+ function tryNativeExtract(filePath) {
12
+ try {
13
+ const { execFileSync } = require('child_process');
14
+ const scriptPath = path.join(__dirname, 'python_ast.py');
15
+ const result = execFileSync('python3', [scriptPath, filePath], {
16
+ timeout: 5000,
17
+ encoding: 'utf8',
18
+ });
19
+ const sigs = JSON.parse(result.trim());
20
+ if (Array.isArray(sigs) && sigs.length > 0) return sigs;
21
+ } catch (_) {}
22
+ return null;
23
+ }
24
+
3
25
  /**
4
26
  * Extract signatures from Python source code.
27
+ * When a real file path is provided, tries the native Python AST extractor first
28
+ * (more accurate for multiline signatures, stacked decorators, and type annotations).
29
+ * Falls back to the regex approach if Python3 is unavailable or returns no results.
5
30
  * @param {string} src - Raw file content
31
+ * @param {string} [filePath] - Optional absolute path to the source file
6
32
  * @returns {string[]} Array of signature strings
7
33
  */
8
- function extract(src) {
34
+ function extract(src, filePath) {
35
+ // Prefer native AST extractor when a real file path is available
36
+ if (filePath && typeof filePath === 'string') {
37
+ const native = tryNativeExtract(filePath);
38
+ if (native) return native;
39
+ }
9
40
  if (!src || typeof src !== 'string') return [];
10
41
  const sigs = [];
11
42
 
@@ -200,4 +231,4 @@ function extractDocHint(src, fnName, fnSigLine) {
200
231
  return sentence.slice(0, 60);
201
232
  }
202
233
 
203
- module.exports = { extract };
234
+ module.exports = { extract, tryNativeExtract };
@@ -0,0 +1,348 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ python_ast.py — Native Python AST-based signature extractor for SigMap.
4
+
5
+ More accurate than the JS regex approach:
6
+ - Handles multiline signatures correctly
7
+ - Decorator stacking resolved properly
8
+ - Type annotations extracted from AST nodes
9
+ - No false positives from regex on string contents
10
+
11
+ Usage (called by SigMap's python.js extractor as fallback):
12
+ python3 python_ast.py <filepath>
13
+
14
+ Output: JSON array of signature strings (one per line → stdout)
15
+ """
16
+
17
+ import ast
18
+ import json
19
+ import sys
20
+
21
+ MAX_SIGS = 30
22
+ MAX_DOC_HINT_LEN = 60
23
+
24
+
25
+ def annotation_to_str(node):
26
+ """Convert an AST annotation node to a string representation."""
27
+ if node is None:
28
+ return None
29
+ try:
30
+ return ast.unparse(node)
31
+ except Exception:
32
+ # Fallback for older Python versions without ast.unparse
33
+ if isinstance(node, ast.Name):
34
+ return node.id
35
+ if isinstance(node, ast.Attribute):
36
+ return f"{annotation_to_str(node.value)}.{node.attr}"
37
+ if isinstance(node, ast.Subscript):
38
+ val = annotation_to_str(node.value)
39
+ slc = annotation_to_str(node.slice)
40
+ return f"{val}[{slc}]"
41
+ if isinstance(node, ast.Index):
42
+ return annotation_to_str(node.value)
43
+ if isinstance(node, ast.Tuple):
44
+ parts = ", ".join(annotation_to_str(e) for e in node.elts)
45
+ return parts
46
+ if isinstance(node, ast.Constant):
47
+ return repr(node.value)
48
+ return "..."
49
+
50
+
51
+ def format_args(args_node):
52
+ """Format a function arguments node into a compact signature string."""
53
+ parts = []
54
+ all_args = args_node.args or []
55
+ defaults = args_node.defaults or []
56
+ # Align defaults to the right of args
57
+ default_offset = len(all_args) - len(defaults)
58
+
59
+ for i, arg in enumerate(all_args):
60
+ name = arg.arg
61
+ ann = annotation_to_str(arg.annotation) if arg.annotation else None
62
+ default_idx = i - default_offset
63
+ has_default = default_idx >= 0
64
+ token = name
65
+ if ann:
66
+ token = f"{name}: {ann}"
67
+ if has_default:
68
+ token = f"{token}=..."
69
+ parts.append(token)
70
+
71
+ # *args
72
+ vararg = args_node.vararg
73
+ if vararg:
74
+ ann = annotation_to_str(vararg.annotation) if vararg.annotation else None
75
+ token = f"*{vararg.arg}"
76
+ if ann:
77
+ token = f"*{vararg.arg}: {ann}"
78
+ parts.append(token)
79
+
80
+ # keyword-only args
81
+ kwonly = args_node.kwonlyargs or []
82
+ kw_defaults = args_node.kw_defaults or []
83
+ for i, arg in enumerate(kwonly):
84
+ name = arg.arg
85
+ ann = annotation_to_str(arg.annotation) if arg.annotation else None
86
+ has_default = i < len(kw_defaults) and kw_defaults[i] is not None
87
+ token = name
88
+ if ann:
89
+ token = f"{name}: {ann}"
90
+ if has_default:
91
+ token = f"{token}=..."
92
+ parts.append(token)
93
+
94
+ # **kwargs
95
+ kwarg = args_node.kwarg
96
+ if kwarg:
97
+ ann = annotation_to_str(kwarg.annotation) if kwarg.annotation else None
98
+ token = f"**{kwarg.arg}"
99
+ if ann:
100
+ token = f"**{kwarg.arg}: {ann}"
101
+ parts.append(token)
102
+
103
+ return ", ".join(parts)
104
+
105
+
106
+ def get_decorator_names(node):
107
+ """Return a list of decorator name strings for a function/class node."""
108
+ names = []
109
+ for dec in node.decorator_list:
110
+ if isinstance(dec, ast.Name):
111
+ names.append(dec.id)
112
+ elif isinstance(dec, ast.Attribute):
113
+ names.append(dec.attr)
114
+ elif isinstance(dec, ast.Call):
115
+ func = dec.func
116
+ if isinstance(func, ast.Name):
117
+ names.append(func.id)
118
+ elif isinstance(func, ast.Attribute):
119
+ names.append(func.attr)
120
+ return names
121
+
122
+
123
+ def is_dataclass(node):
124
+ return "dataclass" in get_decorator_names(node)
125
+
126
+
127
+ def is_basemodel(bases):
128
+ """Check if class bases include BaseModel or BaseSettings."""
129
+ for base in bases:
130
+ name = annotation_to_str(base) or ""
131
+ if "BaseModel" in name or "BaseSettings" in name:
132
+ return True
133
+ return False
134
+
135
+
136
+ def is_optional_annotation(annotation):
137
+ """Check if an annotation represents an Optional type."""
138
+ if annotation is None:
139
+ return False
140
+ ann_str = annotation_to_str(annotation) or ""
141
+ return (
142
+ "Optional[" in ann_str
143
+ or ("Union[" in ann_str and "None" in ann_str)
144
+ or "| None" in ann_str
145
+ or "None |" in ann_str
146
+ )
147
+
148
+
149
+ def get_docstring_hint(node):
150
+ """Extract first sentence of docstring, if present."""
151
+ try:
152
+ doc = ast.get_docstring(node)
153
+ if doc:
154
+ first_line = doc.strip().splitlines()[0]
155
+ return first_line[:MAX_DOC_HINT_LEN] if len(first_line) > MAX_DOC_HINT_LEN else first_line
156
+ except Exception:
157
+ pass
158
+ return None
159
+
160
+
161
+ def extract_dataclass_fields(class_node):
162
+ """Return a collapsed fields string for a @dataclass class."""
163
+ fields = []
164
+ for stmt in class_node.body:
165
+ if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
166
+ name = stmt.target.id
167
+ has_default = stmt.value is not None
168
+ is_optional = is_optional_annotation(stmt.annotation) or has_default
169
+ suffix = "?" if is_optional else ""
170
+ fields.append(f"{name}{suffix}")
171
+ return ", ".join(fields)
172
+
173
+
174
+ def extract_basemodel_fields(class_node):
175
+ """Return a compact {required*, optional?} string for a BaseModel subclass."""
176
+ req = []
177
+ opt = []
178
+ for stmt in class_node.body:
179
+ if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
180
+ name = stmt.target.id
181
+ has_default = stmt.value is not None
182
+ is_optional = is_optional_annotation(stmt.annotation) or has_default
183
+ if is_optional:
184
+ opt.append(f"{name}?")
185
+ else:
186
+ req.append(f"{name}*")
187
+ all_fields = req + opt
188
+ if not all_fields:
189
+ return None
190
+ return "{" + ", ".join(all_fields) + "}"
191
+
192
+
193
+ def extract_class_constants(class_node):
194
+ """Yield ALL_CAPS constant assignments from class body."""
195
+ for stmt in class_node.body:
196
+ if isinstance(stmt, ast.Assign):
197
+ for target in stmt.targets:
198
+ if isinstance(target, ast.Name) and target.id.isupper():
199
+ try:
200
+ val = ast.unparse(stmt.value)
201
+ except Exception:
202
+ val = "..."
203
+ yield f"{target.id}={val}"
204
+ elif isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
205
+ name = stmt.target.id
206
+ if name.isupper():
207
+ val = "..."
208
+ if stmt.value:
209
+ try:
210
+ val = ast.unparse(stmt.value)
211
+ except Exception:
212
+ pass
213
+ yield f"{name}={val}"
214
+
215
+
216
+ def extract_method_sig(func_node):
217
+ """Format a method signature string (already indented by caller)."""
218
+ is_async = isinstance(func_node, ast.AsyncFunctionDef)
219
+ prefix = "async " if is_async else ""
220
+ params = format_args(func_node.args)
221
+ ret = annotation_to_str(func_node.returns) if func_node.returns else None
222
+ ret_str = f" → {ret}" if ret else ""
223
+ return f"{prefix}def {func_node.name}({params}){ret_str}"
224
+
225
+
226
+ def extract_function_sig(func_node, src_lines=None):
227
+ """Format a top-level function signature string."""
228
+ is_async = isinstance(func_node, ast.AsyncFunctionDef)
229
+ prefix = "async " if is_async else ""
230
+ params = format_args(func_node.args)
231
+ ret = annotation_to_str(func_node.returns) if func_node.returns else None
232
+ ret_str = f" → {ret}" if ret else ""
233
+ hint = get_docstring_hint(func_node)
234
+ hint_str = f" # {hint}" if hint else ""
235
+ return f"{prefix}def {func_node.name}({params}){ret_str}{hint_str}"
236
+
237
+
238
+ def extract_fastapi_routes(tree, src_lines):
239
+ """Extract FastAPI route signatures from top-level decorated functions only."""
240
+ routes = []
241
+ http_methods = {"get", "post", "put", "patch", "delete", "head"}
242
+ for node in tree.body:
243
+ if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
244
+ continue
245
+ for dec in node.decorator_list:
246
+ if not isinstance(dec, ast.Call):
247
+ continue
248
+ func = dec.func
249
+ if not isinstance(func, ast.Attribute):
250
+ continue
251
+ method = func.attr.lower()
252
+ if method not in http_methods:
253
+ continue
254
+ if dec.args:
255
+ path_node = dec.args[0]
256
+ if isinstance(path_node, ast.Constant):
257
+ path = path_node.value
258
+ routes.append(f"{method.upper()} {path} → {node.name}()")
259
+ return routes
260
+
261
+
262
+ def extract(filepath):
263
+ with open(filepath, "r", encoding="utf-8", errors="replace") as f:
264
+ src = f.read()
265
+
266
+ tree = ast.parse(src, filename=filepath)
267
+ src_lines = src.splitlines()
268
+ sigs = []
269
+
270
+ # Walk top-level statements only
271
+ for node in tree.body:
272
+ if len(sigs) >= MAX_SIGS:
273
+ break
274
+
275
+ # Classes
276
+ if isinstance(node, ast.ClassDef):
277
+ bases_str = ", ".join(annotation_to_str(b) for b in node.bases if b)
278
+ dec_names = get_decorator_names(node)
279
+
280
+ if is_dataclass(node):
281
+ fields = extract_dataclass_fields(node)
282
+ sigs.append(f"@dataclass {node.name}({fields})")
283
+ elif is_basemodel(node.bases):
284
+ bm_fields = extract_basemodel_fields(node)
285
+ base_label = next(
286
+ (annotation_to_str(b) for b in node.bases
287
+ if "BaseModel" in (annotation_to_str(b) or "") or "BaseSettings" in (annotation_to_str(b) or "")),
288
+ "BaseModel"
289
+ )
290
+ if bm_fields:
291
+ sigs.append(f"class {node.name}({base_label}) {bm_fields}")
292
+ else:
293
+ sigs.append(f"class {node.name}({base_label})")
294
+ else:
295
+ base_part = f"({bases_str})" if bases_str else ""
296
+ sigs.append(f"class {node.name}{base_part}")
297
+
298
+ # Class constants
299
+ for const in extract_class_constants(node):
300
+ if len(sigs) >= MAX_SIGS:
301
+ break
302
+ sigs.append(f" {const}")
303
+
304
+ # Methods (skip private except __init__, skip all other dunder)
305
+ for stmt in node.body:
306
+ if len(sigs) >= MAX_SIGS:
307
+ break
308
+ if not isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)):
309
+ continue
310
+ name = stmt.name
311
+ if name.startswith("_") and name != "__init__":
312
+ continue
313
+ sigs.append(f" {extract_method_sig(stmt)}")
314
+
315
+ # Top-level functions
316
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
317
+ if node.name.startswith("_"):
318
+ continue
319
+ sigs.append(extract_function_sig(node, src_lines))
320
+
321
+ # FastAPI routes (extract top-level decorated functions)
322
+ routes = extract_fastapi_routes(tree, src_lines)
323
+ seen_sigs = set(sigs)
324
+ for route in routes:
325
+ if len(sigs) >= MAX_SIGS:
326
+ break
327
+ if route not in seen_sigs:
328
+ sigs.append(route)
329
+ seen_sigs.add(route)
330
+
331
+ return sigs[:MAX_SIGS]
332
+
333
+
334
+ def main():
335
+ if len(sys.argv) < 2:
336
+ print("[]")
337
+ return
338
+
339
+ filepath = sys.argv[1]
340
+ try:
341
+ sigs = extract(filepath)
342
+ print(json.dumps(sigs))
343
+ except Exception:
344
+ print("[]")
345
+
346
+
347
+ if __name__ == "__main__":
348
+ main()
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract signatures from R source code.
5
+ * @param {string} src - Raw file content
6
+ * @returns {string[]} Array of signature strings
7
+ */
8
+ function extract(src) {
9
+ if (!src || typeof src !== 'string') return [];
10
+ const sigs = [];
11
+
12
+ // Strip line comments. R uses # comments. Roxygen2 (#') comments are
13
+ // stripped along with regular ones; Phase 2 may parse them.
14
+ const stripped = src.replace(/#.*$/gm, '');
15
+
16
+ // Function definitions:
17
+ // name <- function(args) { ... }
18
+ // name = function(args) { ... }
19
+ // name <<- function(args) { ... }
20
+ // Args may span multiple lines and contain default values, so we need to
21
+ // match a balanced parenthesis group rather than a single line.
22
+ const funcRe = /^(?:[ \t]*)([\w.]+)\s*(?:<<-|<-|=)\s*function\s*\(/gm;
23
+ let m;
24
+ while ((m = funcRe.exec(stripped)) !== null) {
25
+ const name = m[1];
26
+ if (name.startsWith('.')) continue; // private convention
27
+ const argsStart = funcRe.lastIndex;
28
+ const args = readBalancedParens(stripped, argsStart - 1);
29
+ if (args === null) continue;
30
+ sigs.push(`${name} <- function(${normalizeParams(args)})`);
31
+ }
32
+
33
+ // S4 setMethod / setGeneric:
34
+ // setGeneric("name", function(args) standardGeneric("name"))
35
+ // setMethod("name", "ClassName", function(args) { ... })
36
+ for (const sm of stripped.matchAll(/^[ \t]*setGeneric\s*\(\s*["']([\w.]+)["']/gm)) {
37
+ sigs.push(`setGeneric("${sm[1]}")`);
38
+ }
39
+ for (const sm of stripped.matchAll(/^[ \t]*setMethod\s*\(\s*["']([\w.]+)["']\s*,\s*["']([\w.]+)["']/gm)) {
40
+ sigs.push(`setMethod("${sm[1]}", "${sm[2]}")`);
41
+ }
42
+
43
+ // S4 class definitions:
44
+ // setClass("Name", representation(...), ...)
45
+ for (const sm of stripped.matchAll(/^[ \t]*setClass\s*\(\s*["']([\w.]+)["']/gm)) {
46
+ sigs.push(`setClass("${sm[1]}")`);
47
+ }
48
+
49
+ return sigs.slice(0, 30);
50
+ }
51
+
52
+ /**
53
+ * Read a parenthesis-balanced substring starting at the position of the
54
+ * opening '(' character, returning the inner content (without the outer
55
+ * parens). Returns null if no matching close paren is found within `cap`
56
+ * characters, which guards against runaway scans on malformed input.
57
+ */
58
+ function readBalancedParens(src, openIdx, cap = 4096) {
59
+ if (src[openIdx] !== '(') return null;
60
+ let depth = 1;
61
+ let i = openIdx + 1;
62
+ const end = Math.min(src.length, openIdx + cap);
63
+ let inString = null; // null | '"' | "'"
64
+ while (i < end) {
65
+ const ch = src[i];
66
+ if (inString) {
67
+ if (ch === '\\') { i += 2; continue; }
68
+ if (ch === inString) inString = null;
69
+ i++;
70
+ continue;
71
+ }
72
+ if (ch === '"' || ch === "'") { inString = ch; i++; continue; }
73
+ if (ch === '(') depth++;
74
+ else if (ch === ')') {
75
+ depth--;
76
+ if (depth === 0) return src.slice(openIdx + 1, i);
77
+ }
78
+ i++;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ /**
84
+ * Compress whitespace inside a parameter list, collapse multi-line default
85
+ * expressions onto a single line, and trim. The goal is one-line readable
86
+ * signatures, not a faithful AST.
87
+ *
88
+ * String literals are protected so that commas/equals inside default values
89
+ * like sep = "," don't get respaced.
90
+ */
91
+ function normalizeParams(raw) {
92
+ const tokens = [];
93
+ let buf = '';
94
+ let inString = null;
95
+ for (let i = 0; i < raw.length; i++) {
96
+ const ch = raw[i];
97
+ if (inString) {
98
+ buf += ch;
99
+ if (ch === '\\' && i + 1 < raw.length) { buf += raw[i + 1]; i++; continue; }
100
+ if (ch === inString) inString = null;
101
+ continue;
102
+ }
103
+ if (ch === '"' || ch === "'") { inString = ch; buf += ch; continue; }
104
+ buf += ch;
105
+ }
106
+ // Now buf === raw with strings preserved character-for-character.
107
+ // Walk again: collapse non-string runs of whitespace, normalize ', ' and ' = '.
108
+ let out = '';
109
+ inString = null;
110
+ for (let i = 0; i < buf.length; i++) {
111
+ const ch = buf[i];
112
+ if (inString) {
113
+ out += ch;
114
+ if (ch === '\\' && i + 1 < buf.length) { out += buf[i + 1]; i++; continue; }
115
+ if (ch === inString) inString = null;
116
+ continue;
117
+ }
118
+ if (ch === '"' || ch === "'") { inString = ch; out += ch; continue; }
119
+ if (/\s/.test(ch)) {
120
+ if (out.length && !/\s$/.test(out)) out += ' ';
121
+ continue;
122
+ }
123
+ if (ch === ',') {
124
+ out = out.replace(/\s+$/, '') + ', ';
125
+ continue;
126
+ }
127
+ if (ch === '=') {
128
+ out = out.replace(/\s+$/, '') + ' = ';
129
+ continue;
130
+ }
131
+ out += ch;
132
+ }
133
+ return out.trim();
134
+ }
135
+
136
+ module.exports = { extract };
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.10.0',
21
+ version: '6.10.1',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24