sigmap 6.10.9 → 6.10.10

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.
@@ -12,6 +12,12 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
 
15
+ // Normalize paths for cross-platform consistency (Windows uses backslashes, Unix uses forward slashes)
16
+ // Use lowercase to enable case-insensitive lookups on case-sensitive Windows filesystems
17
+ function normalizePath(p) {
18
+ return path.normalize(p).toLowerCase();
19
+ }
20
+
15
21
  // ---------------------------------------------------------------------------
16
22
  // Language-specific import extractors
17
23
  // ---------------------------------------------------------------------------
@@ -22,6 +28,7 @@ const GO_EXTS = new Set(['.go']);
22
28
  const RS_EXTS = new Set(['.rs']);
23
29
  const JVM_EXTS = new Set(['.java', '.kt', '.kts', '.scala', '.sc']);
24
30
  const RB_EXTS = new Set(['.rb', '.rake']);
31
+ const R_EXTS = new Set(['.r', '.R']);
25
32
 
26
33
  /**
27
34
  * Resolve a JS/TS relative import string to an absolute path in fileSet.
@@ -40,7 +47,34 @@ function resolveJsPath(dir, importStr, fileSet) {
40
47
  path.join(base, 'index.js'),
41
48
  ];
42
49
  for (const c of candidates) {
43
- if (fileSet.has(c)) return c;
50
+ const normC = normalizePath(c);
51
+ if (fileSet.has(normC)) return normC;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Resolve an R `source(...)` argument to an absolute path in fileSet.
58
+ * Tries the dir-relative path first, then a cwd-relative path so that
59
+ * `source("R/helpers.R")` resolves from the project root.
60
+ */
61
+ function escapeRegex(s) {
62
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
63
+ }
64
+
65
+ function resolveRPath(dir, importStr, fileSet, cwd) {
66
+ const tried = new Set();
67
+ const bases = [path.resolve(dir, importStr)];
68
+ if (cwd) bases.push(path.resolve(cwd, importStr));
69
+ for (const base of bases) {
70
+ for (const c of [base, base + '.R', base + '.r']) {
71
+ const normC = normalizePath(c);
72
+ if (tried.has(normC)) continue;
73
+ tried.add(normC);
74
+ // Check both original and normalized paths (tests may pass non-normalized fileSet)
75
+ if (fileSet.has(c)) return c;
76
+ if (fileSet.has(normC)) return normC;
77
+ }
44
78
  }
45
79
  return null;
46
80
  }
@@ -50,9 +84,14 @@ function resolveJsPath(dir, importStr, fileSet) {
50
84
  * @param {string} filePath - absolute path to the file
51
85
  * @param {string} content - file source content
52
86
  * @param {Set<string>} fileSet - set of all known absolute file paths
87
+ * @param {string} [cwd] - project root, used to resolve R `source("R/...")` calls
88
+ * @param {{ rPackage?: string, rLocalDefs?: Map<string,string> }} [ctx]
89
+ * Optional cross-file context. When present and the file is R, a
90
+ * `localPkg::fn` reference (where `localPkg` matches `rPackage`) is
91
+ * resolved to the file in `rLocalDefs` that defines `fn`.
53
92
  * @returns {string[]} resolved absolute paths this file imports
54
93
  */
55
- function extractFileDeps(filePath, content, fileSet) {
94
+ function extractFileDeps(filePath, content, fileSet, cwd, ctx) {
56
95
  const ext = path.extname(filePath).toLowerCase();
57
96
  const dir = path.dirname(filePath);
58
97
  const found = [];
@@ -91,7 +130,10 @@ function extractFileDeps(filePath, content, fileSet) {
91
130
  const candidate = modPart
92
131
  ? path.join(base, modPart + '.py')
93
132
  : null;
94
- if (candidate && fileSet.has(candidate)) found.push(candidate);
133
+ if (candidate) {
134
+ const normC = normalizePath(candidate);
135
+ if (fileSet.has(normC)) found.push(normC);
136
+ }
95
137
  }
96
138
 
97
139
  // Absolute imports: from package.module import ... (infer from project structure)
@@ -105,8 +147,9 @@ function extractFileDeps(filePath, content, fileSet) {
105
147
  path.resolve(dir, '..', modulePath, '__init__.py'),
106
148
  ];
107
149
  for (const c of candidates) {
108
- if (fileSet.has(c)) {
109
- found.push(c);
150
+ const normC = normalizePath(c);
151
+ if (fileSet.has(normC)) {
152
+ found.push(normC);
110
153
  break;
111
154
  }
112
155
  }
@@ -129,9 +172,10 @@ function extractFileDeps(filePath, content, fileSet) {
129
172
  for (const imp of imports) {
130
173
  const suffix = imp.split('/').pop();
131
174
  for (const f of fileSet) {
132
- if (f.endsWith(path.sep + suffix + '.go') ||
133
- f.includes(path.sep + suffix + path.sep)) {
134
- found.push(f);
175
+ const normF = normalizePath(f);
176
+ if (normF.endsWith(path.sep + suffix + '.go') ||
177
+ normF.includes(path.sep + suffix + path.sep)) {
178
+ found.push(normF);
135
179
  break;
136
180
  }
137
181
  }
@@ -145,10 +189,12 @@ function extractFileDeps(filePath, content, fileSet) {
145
189
  let m;
146
190
  while ((m = reMod.exec(content)) !== null) {
147
191
  const candidate = path.join(dir, m[1] + '.rs');
148
- if (fileSet.has(candidate)) found.push(candidate);
192
+ const normC = normalizePath(candidate);
193
+ if (fileSet.has(normC)) found.push(normC);
149
194
  // Also try mod/mod.rs
150
195
  const candidate2 = path.join(dir, m[1], 'mod.rs');
151
- if (fileSet.has(candidate2)) found.push(candidate2);
196
+ const normC2 = normalizePath(candidate2);
197
+ if (fileSet.has(normC2)) found.push(normC2);
152
198
  }
153
199
  }
154
200
 
@@ -162,7 +208,8 @@ function extractFileDeps(filePath, content, fileSet) {
162
208
  const asPath = m[1].replace(/\./g, path.sep);
163
209
  for (const jvmExt of ['.java', '.kt', '.kts', '.scala', '.sc']) {
164
210
  for (const f of fileSet) {
165
- if (f.endsWith(asPath + jvmExt)) { found.push(f); break; }
211
+ const normF = normalizePath(f);
212
+ if (normF.endsWith(normalizePath(asPath + jvmExt))) { found.push(normF); break; }
166
213
  }
167
214
  }
168
215
  }
@@ -175,7 +222,44 @@ function extractFileDeps(filePath, content, fileSet) {
175
222
  while ((m = re.exec(content)) !== null) {
176
223
  const base = path.resolve(dir, m[1]);
177
224
  const candidate = base.endsWith('.rb') ? base : base + '.rb';
178
- if (fileSet.has(candidate)) found.push(candidate);
225
+ const normC = normalizePath(candidate);
226
+ if (fileSet.has(normC)) found.push(normC);
227
+ }
228
+ }
229
+
230
+ // ── R ─────────────────────────────────────────────────────────────────────
231
+ // R doesn't have JS-style relative imports inside packages — files in R/ are
232
+ // auto-sourced in alphabetical order. We emit edges for:
233
+ // 1. Explicit `source("path/file.R")` calls (common in Shiny / scripts).
234
+ // 2. `localPkg::fn` references where `localPkg` matches the project's
235
+ // own DESCRIPTION#Package — resolved via the symbol→file map in ctx.
236
+ // `library(pkg)` / external `pkg::fn` calls are not graph edges.
237
+ if (R_EXTS.has(ext)) {
238
+ const stripped = content.replace(/#.*$/gm, '');
239
+ const reSrc = /(?:^|[^\w.])source\s*\(\s*["']([^"']+)["']/g;
240
+ let m;
241
+ while ((m = reSrc.exec(stripped)) !== null) {
242
+ const r = resolveRPath(dir, m[1], fileSet, cwd);
243
+ if (r) found.push(r);
244
+ }
245
+ if (ctx && ctx.rPackage && ctx.rLocalDefs && ctx.rLocalDefs.size > 0) {
246
+ const pkg = ctx.rPackage;
247
+ // Match `pkg::fn` or `pkg:::fn`. The `::` form needs to be the local
248
+ // package — references to other packages are external.
249
+ const reNs = new RegExp(`\\b${escapeRegex(pkg)}:::?([A-Za-z][\\w.]*)`, 'g');
250
+ while ((m = reNs.exec(stripped)) !== null) {
251
+ const target = ctx.rLocalDefs.get(m[1]);
252
+ if (!target) continue;
253
+ const normTarget = normalizePath(target);
254
+ const normFilePath = normalizePath(filePath);
255
+ if (normTarget === normFilePath) continue;
256
+ // Check both original and normalized paths (tests may pass non-normalized fileSet)
257
+ if (fileSet.has(target)) {
258
+ found.push(target);
259
+ } else if (fileSet.has(normTarget)) {
260
+ found.push(normTarget);
261
+ }
262
+ }
179
263
  }
180
264
  }
181
265
 
@@ -191,17 +275,24 @@ function extractFileDeps(filePath, content, fileSet) {
191
275
  *
192
276
  * @param {string[]} files - absolute file paths to analyze
193
277
  * @param {string} cwd - project root (used only for error reporting)
278
+ * @param {{ rPackage?: string, rLocalDefs?: Map<string,string> }} [ctx]
279
+ * Optional cross-file context for namespace-aware resolution. Built
280
+ * automatically by `buildFromCwd` when DESCRIPTION + NAMESPACE exist.
194
281
  * @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
195
282
  */
196
- function build(files, cwd) {
283
+ function build(files, cwd, ctx) {
197
284
  const fileSet = new Set(files.map((f) => path.resolve(f)));
285
+ // Create a normalized version for cross-platform case-insensitive lookups
286
+ const fileSetNormalized = new Set([...fileSet].map(normalizePath));
198
287
  const forward = new Map();
199
288
  const reverse = new Map();
200
289
 
201
290
  // Initialise every known file in both maps (ensures isolated files appear)
291
+ // Store using normalized paths for Windows compatibility
202
292
  for (const f of fileSet) {
203
- if (!forward.has(f)) forward.set(f, []);
204
- if (!reverse.has(f)) reverse.set(f, []);
293
+ const normF = normalizePath(f);
294
+ if (!forward.has(normF)) forward.set(normF, []);
295
+ if (!reverse.has(normF)) reverse.set(normF, []);
205
296
  }
206
297
 
207
298
  for (const filePath of fileSet) {
@@ -212,12 +303,13 @@ function build(files, cwd) {
212
303
  continue;
213
304
  }
214
305
 
215
- const deps = extractFileDeps(filePath, content, fileSet);
306
+ const normFilePath = normalizePath(filePath);
307
+ const deps = extractFileDeps(filePath, content, fileSetNormalized, cwd, ctx);
216
308
  if (deps.length > 0) {
217
- forward.set(filePath, deps);
309
+ forward.set(normFilePath, deps);
218
310
  for (const dep of deps) {
219
311
  if (!reverse.has(dep)) reverse.set(dep, []);
220
- reverse.get(dep).push(filePath);
312
+ reverse.get(dep).push(normFilePath);
221
313
  }
222
314
  }
223
315
  }
@@ -236,7 +328,9 @@ function build(files, cwd) {
236
328
  * @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
237
329
  */
238
330
  function buildFromCwd(cwd, opts) {
239
- const { srcDirs = ['src', 'app', 'lib'], exclude = ['node_modules', '.git', 'dist', 'build'] } = opts || {};
331
+ // R-package layouts use `R/` and `inst/`; Shiny apps put helpers in `R/`.
332
+ // The existence check below makes these no-ops in non-R projects.
333
+ const { srcDirs = ['src', 'app', 'lib', 'R', 'inst'], exclude = ['node_modules', '.git', 'dist', 'build'] } = opts || {};
240
334
  const excludeSet = new Set(exclude);
241
335
 
242
336
  function walkDir(dir, depth) {
@@ -252,7 +346,8 @@ function buildFromCwd(cwd, opts) {
252
346
  } else if (e.isFile()) {
253
347
  const ext = path.extname(e.name).toLowerCase();
254
348
  if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || GO_EXTS.has(ext) ||
255
- RS_EXTS.has(ext) || JVM_EXTS.has(ext) || RB_EXTS.has(ext)) {
349
+ RS_EXTS.has(ext) || JVM_EXTS.has(ext) || RB_EXTS.has(ext) ||
350
+ R_EXTS.has(ext)) {
256
351
  out.push(full);
257
352
  }
258
353
  }
@@ -265,13 +360,27 @@ function buildFromCwd(cwd, opts) {
265
360
  const absDir = path.resolve(cwd, sd);
266
361
  if (fs.existsSync(absDir)) files.push(...walkDir(absDir, 0));
267
362
  }
268
- // Also include root-level entry files
269
- for (const rootFile of ['gen-context.js', 'index.js', 'main.js', 'app.js']) {
363
+ // Also include root-level entry files (R: app.R/server.R/ui.R/global.R for Shiny)
364
+ for (const rootFile of ['gen-context.js', 'index.js', 'main.js', 'app.js',
365
+ 'app.R', 'server.R', 'ui.R', 'global.R']) {
270
366
  const abs = path.resolve(cwd, rootFile);
271
367
  if (fs.existsSync(abs)) files.push(abs);
272
368
  }
273
369
 
274
- return build(files, cwd);
370
+ // Build R namespace context if this looks like an R package.
371
+ let ctx;
372
+ try {
373
+ const { readDescription, collectLocalDefs } = require('../discovery/r-manifest');
374
+ const desc = readDescription(cwd);
375
+ if (desc && desc.package) {
376
+ const rFiles = files.filter((f) => R_EXTS.has(path.extname(f).toLowerCase()));
377
+ if (rFiles.length > 0) {
378
+ ctx = { rPackage: desc.package, rLocalDefs: collectLocalDefs(rFiles) };
379
+ }
380
+ }
381
+ } catch (_) { /* manifest module missing or read failed — proceed without ctx */ }
382
+
383
+ return build(files, cwd, ctx);
275
384
  }
276
385
 
277
- module.exports = { build, buildFromCwd, extractFileDeps };
386
+ module.exports = { build, buildFromCwd, extractFileDeps, normalizePath };
@@ -12,6 +12,11 @@
12
12
  const path = require('path');
13
13
  const { buildFromCwd } = require('./builder');
14
14
 
15
+ // Normalize paths for cross-platform consistency (same as in builder.js)
16
+ function normalizePath(p) {
17
+ return path.normalize(p).toLowerCase();
18
+ }
19
+
15
20
  // ---------------------------------------------------------------------------
16
21
  // Core BFS traversal
17
22
  // ---------------------------------------------------------------------------
@@ -111,7 +116,7 @@ function isRouteFile(f) { return ROUTE_PATTERNS.some((re) => re.test(f.replace(/
111
116
  function getImpact(changedFile, graph, opts) {
112
117
  const { depth = 0, cwd = process.cwd() } = opts || {};
113
118
 
114
- const absChanged = path.resolve(cwd, changedFile);
119
+ const absChanged = normalizePath(path.resolve(cwd, changedFile));
115
120
 
116
121
  // Bail gracefully if file not in graph
117
122
  if (!graph || !graph.reverse) {
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.9',
21
+ version: '6.10.10',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -83,8 +83,9 @@ function _computeHubs(graph) {
83
83
 
84
84
  // Common utility paths that should be treated as hubs regardless of fanout
85
85
  function _isHub(filePath) {
86
- return /\/(utils|helpers|shared|common|constants|types|interfaces|index)\.(ts|tsx|js|jsx)$/.test(filePath)
87
- || filePath.endsWith('/index.ts') || filePath.endsWith('/index.js');
86
+ return /\/(utils|helpers|shared|common|constants|types|interfaces|index|zzz|globals)\.(ts|tsx|js|jsx|r|R)$/.test(filePath)
87
+ || filePath.endsWith('/index.ts') || filePath.endsWith('/index.js')
88
+ || filePath.endsWith('/R/utils.R') || filePath.endsWith('/R/zzz.R') || filePath.endsWith('/R/globals.R');
88
89
  }
89
90
 
90
91
  /**
@@ -277,6 +278,24 @@ function rank(query, sigIndex, opts) {
277
278
  }
278
279
  }
279
280
 
281
+ // Compute confidence levels based on score distribution
282
+ if (scored.length > 0) {
283
+ const scores = scored.map(s => s.score);
284
+ const maxScore = Math.max(...scores);
285
+ const minScore = Math.min(...scores);
286
+ const scoreRange = maxScore - minScore || 1;
287
+
288
+ // Confidence tiers: top 33% = high, next 33% = medium, rest = low
289
+ for (const entry of scored) {
290
+ if (entry.score <= 0) {
291
+ entry.confidence = 'low';
292
+ } else {
293
+ const normalized = (entry.score - minScore) / scoreRange;
294
+ entry.confidence = normalized > 0.66 ? 'high' : normalized > 0.33 ? 'medium' : 'low';
295
+ }
296
+ }
297
+ }
298
+
280
299
  scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
281
300
  return scored.slice(0, topK);
282
301
  }