sigmap 6.10.7 → 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.
- package/AGENTS.md +81 -107
- package/CHANGELOG.md +35 -0
- package/gen-context.js +298 -85
- package/package.json +3 -3
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/discovery/r-manifest.js +176 -0
- package/src/extractors/deps.js +30 -1
- package/src/extractors/r.js +182 -45
- package/src/graph/builder.js +149 -22
- package/src/graph/impact.js +6 -1
- package/src/mcp/server.js +1 -1
- package/src/retrieval/ranker.js +21 -2
package/src/graph/builder.js
CHANGED
|
@@ -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
|
-
|
|
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,29 @@ function extractFileDeps(filePath, content, fileSet) {
|
|
|
91
130
|
const candidate = modPart
|
|
92
131
|
? path.join(base, modPart + '.py')
|
|
93
132
|
: null;
|
|
94
|
-
if (candidate
|
|
133
|
+
if (candidate) {
|
|
134
|
+
const normC = normalizePath(candidate);
|
|
135
|
+
if (fileSet.has(normC)) found.push(normC);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Absolute imports: from package.module import ... (infer from project structure)
|
|
140
|
+
const reAbs = /^[ \t]*from\s+([\w.]+)\s+import/gm;
|
|
141
|
+
while ((m = reAbs.exec(content)) !== null) {
|
|
142
|
+
const modulePath = m[1].replace(/\./g, '/');
|
|
143
|
+
const candidates = [
|
|
144
|
+
path.join(dir, modulePath + '.py'),
|
|
145
|
+
path.join(dir, modulePath, '__init__.py'),
|
|
146
|
+
path.resolve(dir, '..', modulePath + '.py'),
|
|
147
|
+
path.resolve(dir, '..', modulePath, '__init__.py'),
|
|
148
|
+
];
|
|
149
|
+
for (const c of candidates) {
|
|
150
|
+
const normC = normalizePath(c);
|
|
151
|
+
if (fileSet.has(normC)) {
|
|
152
|
+
found.push(normC);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
95
156
|
}
|
|
96
157
|
}
|
|
97
158
|
|
|
@@ -111,9 +172,10 @@ function extractFileDeps(filePath, content, fileSet) {
|
|
|
111
172
|
for (const imp of imports) {
|
|
112
173
|
const suffix = imp.split('/').pop();
|
|
113
174
|
for (const f of fileSet) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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);
|
|
117
179
|
break;
|
|
118
180
|
}
|
|
119
181
|
}
|
|
@@ -127,10 +189,12 @@ function extractFileDeps(filePath, content, fileSet) {
|
|
|
127
189
|
let m;
|
|
128
190
|
while ((m = reMod.exec(content)) !== null) {
|
|
129
191
|
const candidate = path.join(dir, m[1] + '.rs');
|
|
130
|
-
|
|
192
|
+
const normC = normalizePath(candidate);
|
|
193
|
+
if (fileSet.has(normC)) found.push(normC);
|
|
131
194
|
// Also try mod/mod.rs
|
|
132
195
|
const candidate2 = path.join(dir, m[1], 'mod.rs');
|
|
133
|
-
|
|
196
|
+
const normC2 = normalizePath(candidate2);
|
|
197
|
+
if (fileSet.has(normC2)) found.push(normC2);
|
|
134
198
|
}
|
|
135
199
|
}
|
|
136
200
|
|
|
@@ -144,7 +208,8 @@ function extractFileDeps(filePath, content, fileSet) {
|
|
|
144
208
|
const asPath = m[1].replace(/\./g, path.sep);
|
|
145
209
|
for (const jvmExt of ['.java', '.kt', '.kts', '.scala', '.sc']) {
|
|
146
210
|
for (const f of fileSet) {
|
|
147
|
-
|
|
211
|
+
const normF = normalizePath(f);
|
|
212
|
+
if (normF.endsWith(normalizePath(asPath + jvmExt))) { found.push(normF); break; }
|
|
148
213
|
}
|
|
149
214
|
}
|
|
150
215
|
}
|
|
@@ -157,7 +222,44 @@ function extractFileDeps(filePath, content, fileSet) {
|
|
|
157
222
|
while ((m = re.exec(content)) !== null) {
|
|
158
223
|
const base = path.resolve(dir, m[1]);
|
|
159
224
|
const candidate = base.endsWith('.rb') ? base : base + '.rb';
|
|
160
|
-
|
|
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
|
+
}
|
|
161
263
|
}
|
|
162
264
|
}
|
|
163
265
|
|
|
@@ -173,17 +275,24 @@ function extractFileDeps(filePath, content, fileSet) {
|
|
|
173
275
|
*
|
|
174
276
|
* @param {string[]} files - absolute file paths to analyze
|
|
175
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.
|
|
176
281
|
* @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
|
|
177
282
|
*/
|
|
178
|
-
function build(files, cwd) {
|
|
283
|
+
function build(files, cwd, ctx) {
|
|
179
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));
|
|
180
287
|
const forward = new Map();
|
|
181
288
|
const reverse = new Map();
|
|
182
289
|
|
|
183
290
|
// Initialise every known file in both maps (ensures isolated files appear)
|
|
291
|
+
// Store using normalized paths for Windows compatibility
|
|
184
292
|
for (const f of fileSet) {
|
|
185
|
-
|
|
186
|
-
if (!
|
|
293
|
+
const normF = normalizePath(f);
|
|
294
|
+
if (!forward.has(normF)) forward.set(normF, []);
|
|
295
|
+
if (!reverse.has(normF)) reverse.set(normF, []);
|
|
187
296
|
}
|
|
188
297
|
|
|
189
298
|
for (const filePath of fileSet) {
|
|
@@ -194,12 +303,13 @@ function build(files, cwd) {
|
|
|
194
303
|
continue;
|
|
195
304
|
}
|
|
196
305
|
|
|
197
|
-
const
|
|
306
|
+
const normFilePath = normalizePath(filePath);
|
|
307
|
+
const deps = extractFileDeps(filePath, content, fileSetNormalized, cwd, ctx);
|
|
198
308
|
if (deps.length > 0) {
|
|
199
|
-
forward.set(
|
|
309
|
+
forward.set(normFilePath, deps);
|
|
200
310
|
for (const dep of deps) {
|
|
201
311
|
if (!reverse.has(dep)) reverse.set(dep, []);
|
|
202
|
-
reverse.get(dep).push(
|
|
312
|
+
reverse.get(dep).push(normFilePath);
|
|
203
313
|
}
|
|
204
314
|
}
|
|
205
315
|
}
|
|
@@ -218,7 +328,9 @@ function build(files, cwd) {
|
|
|
218
328
|
* @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
|
|
219
329
|
*/
|
|
220
330
|
function buildFromCwd(cwd, opts) {
|
|
221
|
-
|
|
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 || {};
|
|
222
334
|
const excludeSet = new Set(exclude);
|
|
223
335
|
|
|
224
336
|
function walkDir(dir, depth) {
|
|
@@ -234,7 +346,8 @@ function buildFromCwd(cwd, opts) {
|
|
|
234
346
|
} else if (e.isFile()) {
|
|
235
347
|
const ext = path.extname(e.name).toLowerCase();
|
|
236
348
|
if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || GO_EXTS.has(ext) ||
|
|
237
|
-
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)) {
|
|
238
351
|
out.push(full);
|
|
239
352
|
}
|
|
240
353
|
}
|
|
@@ -247,13 +360,27 @@ function buildFromCwd(cwd, opts) {
|
|
|
247
360
|
const absDir = path.resolve(cwd, sd);
|
|
248
361
|
if (fs.existsSync(absDir)) files.push(...walkDir(absDir, 0));
|
|
249
362
|
}
|
|
250
|
-
// Also include root-level entry files
|
|
251
|
-
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']) {
|
|
252
366
|
const abs = path.resolve(cwd, rootFile);
|
|
253
367
|
if (fs.existsSync(abs)) files.push(abs);
|
|
254
368
|
}
|
|
255
369
|
|
|
256
|
-
|
|
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);
|
|
257
384
|
}
|
|
258
385
|
|
|
259
|
-
module.exports = { build, buildFromCwd, extractFileDeps };
|
|
386
|
+
module.exports = { build, buildFromCwd, extractFileDeps, normalizePath };
|
package/src/graph/impact.js
CHANGED
|
@@ -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
package/src/retrieval/ranker.js
CHANGED
|
@@ -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
|
}
|