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.
@@ -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 };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "6.10.0",
3
+ "version": "6.10.2",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -29,6 +29,8 @@ const EXT_MAP = {
29
29
  '.swift': 'swift',
30
30
  '.dart': 'dart',
31
31
  '.scala': 'scala', '.sc': 'scala',
32
+ '.gd': 'gdscript',
33
+ '.r': 'r', '.R': 'r',
32
34
  '.vue': 'vue',
33
35
  '.svelte': 'svelte',
34
36
  '.html': 'html', '.htm': 'html',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "6.10.0",
3
+ "version": "6.10.2",
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,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 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,8 @@ const EXT_MAP = {
29
29
  '.swift': 'swift',
30
30
  '.dart': 'dart',
31
31
  '.scala': 'scala', '.sc': 'scala',
32
+ '.gd': 'gdscript',
33
+ '.r': 'r', '.R': 'r',
32
34
  '.vue': 'vue',
33
35
  '.svelte': 'svelte',
34
36
  '.html': 'html', '.htm': 'html',
@@ -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 };
@@ -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 };