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 +98 -135
- package/CHANGELOG.md +16 -0
- package/README.md +19 -2
- package/gen-context.js +115 -20
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/index.js +1 -0
- package/packages/core/package.json +1 -1
- package/src/discovery/language-detector.js +1 -0
- package/src/discovery/source-root-registry.js +9 -0
- package/src/discovery/source-root-resolver.js +5 -1
- package/src/eval/analyzer.js +1 -0
- package/src/extractors/python.js +33 -2
- package/src/extractors/python_ast.py +348 -0
- package/src/extractors/r.js +136 -0
- package/src/mcp/server.js +1 -1
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/
|
|
62
|
-
src/
|
|
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
|
-
|
|
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
|
|
252
|
-
function
|
|
253
|
-
function
|
|
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
|
-
|
|
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/
|
|
572
|
+
### src/discovery/source-root-registry.js
|
|
660
573
|
```
|
|
661
|
-
module.exports = {
|
|
662
|
-
|
|
663
|
-
|
|
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
|
[](package.json)
|
|
13
13
|
[](LICENSE)
|
|
14
14
|
[](https://github.com/manojmallick/sigmap/stargazers)
|
|
15
|
-
[](https://star-history.com/#manojmallick/sigmap&Date)
|
|
16
|
+
[](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.
|
|
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.
|
|
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
|
-
//
|
|
8009
|
-
const
|
|
8010
|
-
.
|
|
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
|
-
|
|
8015
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
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
package/packages/core/index.js
CHANGED
|
@@ -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
|
|
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;
|
package/src/eval/analyzer.js
CHANGED
package/src/extractors/python.js
CHANGED
|
@@ -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