sigmap 6.10.0 → 6.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +504 -283
- package/CHANGELOG.md +31 -0
- package/README.md +41 -11
- package/gen-context.js +257 -20
- package/package.json +1 -1
- package/packages/adapters/index.js +4 -2
- package/packages/adapters/willow.js +200 -0
- package/packages/cli/package.json +1 -1
- package/packages/core/index.js +2 -0
- package/packages/core/package.json +1 -1
- package/src/discovery/language-detector.js +2 -0
- package/src/discovery/source-root-registry.js +9 -0
- package/src/discovery/source-root-resolver.js +5 -1
- package/src/eval/analyzer.js +2 -0
- package/src/extractors/gdscript.js +131 -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/map/import-graph.js +1 -1
- package/src/mcp/server.js +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Willow adapter — writes SigMap context to Willow MCP knowledge store.
|
|
5
|
+
*
|
|
6
|
+
* Instead of writing a flat .willow-context.md file, this adapter sends
|
|
7
|
+
* signature atoms to a Willow MCP server (https://github.com/rudi193-cmd/willow-1.9)
|
|
8
|
+
* via HTTP POST. Each indexed file becomes a searchable knowledge atom.
|
|
9
|
+
*
|
|
10
|
+
* Contract:
|
|
11
|
+
* format(context, opts?) → string (markdown for display/debug)
|
|
12
|
+
* outputPath(cwd) → string (placeholder — no file written)
|
|
13
|
+
* write(context, cwd, opts?) → Promise<void> (POSTs to Willow MCP, must await)
|
|
14
|
+
*
|
|
15
|
+
* Configuration (env vars or opts):
|
|
16
|
+
* WILLOW_MCP_URL — MCP server base URL (default: http://localhost:8000)
|
|
17
|
+
* WILLOW_AGENT — agent namespace (default: sigmap)
|
|
18
|
+
* WILLOW_TIMEOUT — fetch timeout in ms (default: 30000)
|
|
19
|
+
* WILLOW_MAX_ATOM_SIZE — max atom size in bytes (default: 100000)
|
|
20
|
+
* WILLOW_RETRIES — max retry attempts for transient failures (default: 3)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
const name = 'willow';
|
|
25
|
+
const DEFAULT_MCP_URL = 'http://localhost:8000';
|
|
26
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
27
|
+
const DEFAULT_MAX_ATOM_SIZE = 100000;
|
|
28
|
+
const DEFAULT_RETRIES = 3;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format SigMap context as markdown for display or debug.
|
|
32
|
+
* @param {string} context - Raw SigMap context string
|
|
33
|
+
* @param {object} [opts] - Unused; reserved for future options
|
|
34
|
+
* @returns {string}
|
|
35
|
+
*/
|
|
36
|
+
function format(context, opts = {}) {
|
|
37
|
+
if (!context || typeof context !== 'string') return '';
|
|
38
|
+
const ts = new Date().toISOString();
|
|
39
|
+
return `<!-- SigMap Willow context — ${ts} -->\n\n${context}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Return the placeholder output path (no file is written by this adapter).
|
|
44
|
+
* @param {string} cwd - Working directory
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function outputPath(cwd) {
|
|
48
|
+
return '.willow-context.md';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate a cryptographically strong ID for an atom.
|
|
53
|
+
* Uses SHA256 hash of the filepath to ensure uniqueness and prevent collisions.
|
|
54
|
+
* @param {string} filepath - File path to hash
|
|
55
|
+
* @returns {string} - sigmap-{32-char-hex}
|
|
56
|
+
*/
|
|
57
|
+
function generateAtomId(filepath) {
|
|
58
|
+
const hash = crypto
|
|
59
|
+
.createHash('sha256')
|
|
60
|
+
.update(filepath)
|
|
61
|
+
.digest('hex');
|
|
62
|
+
return `sigmap-${hash}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetch with timeout support.
|
|
67
|
+
* @param {string} url - URL to fetch
|
|
68
|
+
* @param {object} opts - Fetch options
|
|
69
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
70
|
+
* @returns {Promise<Response>}
|
|
71
|
+
*/
|
|
72
|
+
async function fetchWithTimeout(url, opts, timeoutMs) {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
75
|
+
try {
|
|
76
|
+
return await fetch(url, { ...opts, signal: controller.signal });
|
|
77
|
+
} finally {
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* POST an atom to Willow with exponential backoff retry.
|
|
84
|
+
* @param {object} atom - Atom to ingest
|
|
85
|
+
* @param {string} mcpUrl - MCP server URL
|
|
86
|
+
* @param {number} timeoutMs - Fetch timeout
|
|
87
|
+
* @param {number} maxRetries - Max retry attempts
|
|
88
|
+
* @returns {Promise<boolean>} - True if succeeded, false if all retries exhausted
|
|
89
|
+
*/
|
|
90
|
+
async function postAtomWithRetry(atom, mcpUrl, timeoutMs, maxRetries) {
|
|
91
|
+
let lastErr;
|
|
92
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
93
|
+
try {
|
|
94
|
+
const resp = await fetchWithTimeout(
|
|
95
|
+
`${mcpUrl}/tools/call`,
|
|
96
|
+
{
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
name: 'willow_knowledge_ingest',
|
|
101
|
+
arguments: {
|
|
102
|
+
app_id: atom.agent,
|
|
103
|
+
title: atom.title,
|
|
104
|
+
summary: atom.summary,
|
|
105
|
+
domain: atom.domain,
|
|
106
|
+
source_type: atom.source_type,
|
|
107
|
+
category: 'code',
|
|
108
|
+
record_id: atom.id,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
timeoutMs,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (resp.ok) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (resp.status >= 500) {
|
|
120
|
+
lastErr = new Error(`HTTP ${resp.status}`);
|
|
121
|
+
if (attempt < maxRetries - 1) {
|
|
122
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt)));
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
process.stderr.write(`[willow-adapter] ${atom.id}: HTTP ${resp.status} (not retryable)\n`);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
lastErr = err;
|
|
131
|
+
if (attempt < maxRetries - 1) {
|
|
132
|
+
await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt)));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
process.stderr.write(
|
|
139
|
+
`[willow-adapter] ${atom.id}: failed after ${maxRetries} attempts: ${lastErr?.message || 'unknown'}\n`,
|
|
140
|
+
);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* POST each file section from SigMap context to the Willow MCP knowledge store.
|
|
146
|
+
* Each `## filepath` section becomes one searchable knowledge atom.
|
|
147
|
+
* Failures are per-atom and logged to stderr; function never throws.
|
|
148
|
+
* IMPORTANT: This is async — caller MUST await write() before process exit.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} context - Raw SigMap context string
|
|
151
|
+
* @param {string} cwd - Working directory (used as project label)
|
|
152
|
+
* @param {object} [opts] - Optional overrides: { mcpUrl, agent, timeoutMs, maxAtomSize, maxRetries }
|
|
153
|
+
* @returns {Promise<void>}
|
|
154
|
+
*/
|
|
155
|
+
async function write(context, cwd, opts = {}) {
|
|
156
|
+
if (!context) return;
|
|
157
|
+
|
|
158
|
+
const mcpUrl = opts.mcpUrl || process.env.WILLOW_MCP_URL || DEFAULT_MCP_URL;
|
|
159
|
+
const agent = opts.agent || process.env.WILLOW_AGENT || 'sigmap';
|
|
160
|
+
const timeoutMs = opts.timeoutMs || parseInt(process.env.WILLOW_TIMEOUT, 10) || DEFAULT_TIMEOUT_MS;
|
|
161
|
+
const maxAtomSize = opts.maxAtomSize || parseInt(process.env.WILLOW_MAX_ATOM_SIZE, 10) || DEFAULT_MAX_ATOM_SIZE;
|
|
162
|
+
const maxRetries = opts.maxRetries || parseInt(process.env.WILLOW_RETRIES, 10) || DEFAULT_RETRIES;
|
|
163
|
+
|
|
164
|
+
const sections = context.split(/\n(?=##\s)/);
|
|
165
|
+
const atoms = sections
|
|
166
|
+
.map((section) => {
|
|
167
|
+
const titleMatch = section.match(/^##\s+(.+)/);
|
|
168
|
+
if (!titleMatch) return null;
|
|
169
|
+
|
|
170
|
+
const title = titleMatch[1].trim();
|
|
171
|
+
const contentSize = section.length;
|
|
172
|
+
|
|
173
|
+
if (contentSize > maxAtomSize) {
|
|
174
|
+
process.stderr.write(`[willow-adapter] ${title}: oversized (${contentSize} > ${maxAtomSize} bytes)\n`);
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
id: generateAtomId(title),
|
|
180
|
+
title,
|
|
181
|
+
summary: `${title} (${contentSize} bytes)`,
|
|
182
|
+
content: section.trim(),
|
|
183
|
+
domain: 'code',
|
|
184
|
+
source_type: 'sigmap',
|
|
185
|
+
agent,
|
|
186
|
+
project: cwd ? require('path').basename(cwd) : 'unknown',
|
|
187
|
+
};
|
|
188
|
+
})
|
|
189
|
+
.filter(Boolean);
|
|
190
|
+
|
|
191
|
+
if (!atoms.length) return;
|
|
192
|
+
|
|
193
|
+
await Promise.all(
|
|
194
|
+
atoms.map((atom) => postAtomWithRetry(atom, mcpUrl, timeoutMs, maxRetries).catch((err) => {
|
|
195
|
+
process.stderr.write(`[willow-adapter] ${atom.id}: unexpected error: ${err.message}\n`);
|
|
196
|
+
})),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
module.exports = { name, format, outputPath, write };
|
package/packages/core/index.js
CHANGED
|
@@ -19,6 +19,8 @@ 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
|
+
'.gd': 'gdscript',
|
|
23
|
+
'.r': 'r', '.R': 'r',
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
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
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract signatures from Godot GDScript source code.
|
|
5
|
+
* @param {string} src - Raw file content
|
|
6
|
+
* @returns {string[]} Array of signature strings
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function extract(src) {
|
|
10
|
+
if (!src || typeof src !== 'string') return [];
|
|
11
|
+
const sigs = [];
|
|
12
|
+
|
|
13
|
+
const stripped = src.replace(/#.*$/gm, '');
|
|
14
|
+
|
|
15
|
+
let className = null;
|
|
16
|
+
let baseName = null;
|
|
17
|
+
const addedClasses = new Set();
|
|
18
|
+
|
|
19
|
+
const cm = stripped.match(/^class_name\s+(\w+)(?:\s+extends\s+([\w.]+))?/m);
|
|
20
|
+
if (cm) {
|
|
21
|
+
className = cm[1];
|
|
22
|
+
if (cm[2]) baseName = cm[2];
|
|
23
|
+
}
|
|
24
|
+
if (!baseName) {
|
|
25
|
+
const em = stripped.match(/^extends\s+([\w."/]+)/m);
|
|
26
|
+
if (em) baseName = em[1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (className) {
|
|
30
|
+
sigs.push(baseName ? `class ${className}(${baseName})` : `class ${className}`);
|
|
31
|
+
addedClasses.add(className);
|
|
32
|
+
} else if (baseName) {
|
|
33
|
+
sigs.push(`extends ${baseName}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const indent = (className || baseName) ? ' ' : '';
|
|
37
|
+
|
|
38
|
+
for (const m of stripped.matchAll(/^signal\s+(\w+)(?:\s*\(([^)]*)\))?/gm)) {
|
|
39
|
+
sigs.push(`${indent}signal ${m[1]}(${normalizeParams(m[2] || '')})`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const m of stripped.matchAll(/^enum\s+(\w+)\s*\{([^}]*)\}/gm)) {
|
|
43
|
+
const members = m[2]
|
|
44
|
+
.split(',')
|
|
45
|
+
.map((s) => s.trim().split(/\s*=/)[0].trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
sigs.push(`${indent}enum ${m[1]} { ${members.slice(0, 6).join(', ')} }`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let constCount = 0;
|
|
51
|
+
for (const m of stripped.matchAll(/^const\s+(\w+)(?:\s*:\s*[^=\n]+)?\s*:?=\s*([^\n]+)$/gm)) {
|
|
52
|
+
let val = m[2].trim();
|
|
53
|
+
const preloadMatch = val.match(/^preload\s*\(([^)]+)\)/);
|
|
54
|
+
if (preloadMatch) {
|
|
55
|
+
val = `preload(${preloadMatch[1]})`;
|
|
56
|
+
} else if (val.length > 40) {
|
|
57
|
+
val = val.slice(0, 37) + '...';
|
|
58
|
+
}
|
|
59
|
+
sigs.push(`${indent}const ${m[1]} = ${val}`);
|
|
60
|
+
if (++constCount >= 5) break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const m of stripped.matchAll(/^((?:@\w+(?:\([^)]*\))?\s+)*)var\s+(\w+)(?:\s*:\s*([^=\n]+?))?(?:\s*:?=\s*[^\n]+)?$/gm)) {
|
|
64
|
+
const decorators = m[1] || '';
|
|
65
|
+
const name = m[2];
|
|
66
|
+
const hasDecorator = /@\w+/.test(decorators);
|
|
67
|
+
if (!hasDecorator && name.startsWith('_')) continue;
|
|
68
|
+
|
|
69
|
+
let prefix = decorators.replace(/\([^)]*\)/g, '').trim().split(/\s+/).join(' ');
|
|
70
|
+
if (prefix) prefix += ' ';
|
|
71
|
+
|
|
72
|
+
const type = (m[3] || '').trim();
|
|
73
|
+
const typeStr = type ? `: ${type}` : '';
|
|
74
|
+
sigs.push(`${indent}${prefix}var ${name}${typeStr}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const m of stripped.matchAll(/^(static\s+)?func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:\n]+))?\s*:/gm)) {
|
|
78
|
+
const params = normalizeParams(m[3]);
|
|
79
|
+
const ret = (m[4] || '').trim();
|
|
80
|
+
const retStr = ret ? ` → ${ret.slice(0, 30)}` : '';
|
|
81
|
+
const staticKw = m[1] ? 'static ' : '';
|
|
82
|
+
sigs.push(`${indent}${staticKw}func ${m[2]}(${params})${retStr}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const m of stripped.matchAll(/^class\s+(\w+)(?:\s+extends\s+(\w+))?\s*:/gm)) {
|
|
86
|
+
if (addedClasses.has(m[1])) continue;
|
|
87
|
+
addedClasses.add(m[1]);
|
|
88
|
+
sigs.push(m[2] ? `class ${m[1]}(${m[2]})` : `class ${m[1]}`);
|
|
89
|
+
const startIdx = m.index + m[0].length;
|
|
90
|
+
for (const meth of extractInnerMembers(stripped, startIdx)) {
|
|
91
|
+
sigs.push(` ${meth}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return sigs.slice(0, 25);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractInnerMembers(stripped, startIndex) {
|
|
99
|
+
const members = [];
|
|
100
|
+
const lines = stripped.slice(startIndex).split('\n');
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
if (line.trim() === '') continue;
|
|
103
|
+
if (!/^\s+/.test(line)) break;
|
|
104
|
+
const fm = line.match(/^\s+(static\s+)?func\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*([^:\n]+))?\s*:/);
|
|
105
|
+
if (fm) {
|
|
106
|
+
const params = normalizeParams(fm[3]);
|
|
107
|
+
const ret = (fm[4] || '').trim();
|
|
108
|
+
const retStr = ret ? ` → ${ret.slice(0, 30)}` : '';
|
|
109
|
+
const staticKw = fm[1] ? 'static ' : '';
|
|
110
|
+
members.push(`${staticKw}func ${fm[2]}(${params})${retStr}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return members.slice(0, 6);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeParams(params) {
|
|
117
|
+
if (!params) return '';
|
|
118
|
+
return params.trim()
|
|
119
|
+
.split(',')
|
|
120
|
+
.map((p) => {
|
|
121
|
+
const part = p.trim();
|
|
122
|
+
if (!part) return '';
|
|
123
|
+
const eqIdx = part.indexOf('=');
|
|
124
|
+
const noDefault = eqIdx !== -1 ? part.slice(0, eqIdx).trim() : part;
|
|
125
|
+
return noDefault.split(':')[0].trim();
|
|
126
|
+
})
|
|
127
|
+
.filter(Boolean)
|
|
128
|
+
.join(', ');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { extract };
|
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 };
|