sigmap 6.10.9 → 6.10.11
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 +89 -129
- package/CHANGELOG.md +29 -0
- package/README.md +15 -14
- package/gen-context.js +409 -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 +133 -24
- package/src/graph/impact.js +6 -1
- package/src/mcp/handlers.js +60 -93
- package/src/mcp/server.js +1 -1
- package/src/retrieval/ranker.js +80 -6
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,10 @@ 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
|
+
}
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
if (!
|
|
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
|
|
306
|
+
const normFilePath = normalizePath(filePath);
|
|
307
|
+
const deps = extractFileDeps(filePath, content, fileSetNormalized, cwd, ctx);
|
|
216
308
|
if (deps.length > 0) {
|
|
217
|
-
forward.set(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
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/handlers.js
CHANGED
|
@@ -5,6 +5,16 @@ const path = require('path');
|
|
|
5
5
|
const { execSync } = require('child_process');
|
|
6
6
|
|
|
7
7
|
const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
|
|
8
|
+
const CONTEXT_COLD_FILE = path.join('.github', 'context-cold.md');
|
|
9
|
+
|
|
10
|
+
function _readContextFiles(cwd) {
|
|
11
|
+
const paths = [path.join(cwd, CONTEXT_FILE), path.join(cwd, CONTEXT_COLD_FILE)];
|
|
12
|
+
const chunks = [];
|
|
13
|
+
for (const p of paths) {
|
|
14
|
+
if (fs.existsSync(p)) chunks.push(fs.readFileSync(p, 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
return chunks.join('\n');
|
|
17
|
+
}
|
|
8
18
|
|
|
9
19
|
// Section header keywords in PROJECT_MAP.md
|
|
10
20
|
const MAP_SECTIONS = {
|
|
@@ -20,13 +30,11 @@ const MAP_SECTIONS = {
|
|
|
20
30
|
* contain the given module substring.
|
|
21
31
|
*/
|
|
22
32
|
function readContext(args, cwd) {
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
33
|
+
const content = _readContextFiles(cwd);
|
|
34
|
+
if (!content) {
|
|
25
35
|
return 'No context file found. Run: node gen-context.js';
|
|
26
36
|
}
|
|
27
37
|
|
|
28
|
-
const content = fs.readFileSync(contextPath, 'utf8');
|
|
29
|
-
|
|
30
38
|
if (!args || !args.module) return content;
|
|
31
39
|
|
|
32
40
|
const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
|
|
@@ -62,41 +70,28 @@ function readContext(args, cwd) {
|
|
|
62
70
|
function searchSignatures(args, cwd) {
|
|
63
71
|
if (!args || !args.query) return 'Missing required argument: query';
|
|
64
72
|
|
|
65
|
-
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
66
|
-
if (!fs.existsSync(contextPath)) {
|
|
67
|
-
return 'No context file found. Run: node gen-context.js';
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const content = fs.readFileSync(contextPath, 'utf8');
|
|
71
73
|
const query = args.query.toLowerCase();
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
for (const line of lines) {
|
|
79
|
-
if (line.startsWith('### ')) {
|
|
80
|
-
currentFile = line.slice(4).trim();
|
|
81
|
-
fileHeaderAdded = false;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
// Skip markdown fences and top-level headers
|
|
85
|
-
if (line.startsWith('```') || line.startsWith('## ') || line.startsWith('# ') || line.startsWith('<!--')) {
|
|
86
|
-
continue;
|
|
74
|
+
try {
|
|
75
|
+
const { buildSigIndex } = require('../retrieval/ranker');
|
|
76
|
+
const index = buildSigIndex(cwd);
|
|
77
|
+
if (index.size === 0) {
|
|
78
|
+
return 'No context file found. Run: node gen-context.js';
|
|
87
79
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
result.push(
|
|
80
|
+
|
|
81
|
+
const result = [];
|
|
82
|
+
for (const [file, sigs] of index.entries()) {
|
|
83
|
+
const hits = sigs.filter((s) => s.toLowerCase().includes(query));
|
|
84
|
+
if (hits.length === 0) continue;
|
|
85
|
+
if (result.length > 0) result.push('');
|
|
86
|
+
result.push(`### ${file}`);
|
|
87
|
+
result.push(...hits);
|
|
95
88
|
}
|
|
96
|
-
}
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
|
|
90
|
+
if (result.length === 0) return `No signatures found matching: ${args.query}`;
|
|
91
|
+
return result.join('\n');
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return `_search_signatures failed: ${err.message}_`;
|
|
94
|
+
}
|
|
100
95
|
}
|
|
101
96
|
|
|
102
97
|
/**
|
|
@@ -280,39 +275,29 @@ function explainFile(args, cwd) {
|
|
|
280
275
|
|
|
281
276
|
const lines = ['# explain_file: ' + targetRel, ''];
|
|
282
277
|
|
|
283
|
-
// ── Signatures (
|
|
278
|
+
// ── Signatures (hot + cold + cache via buildSigIndex) ───────────────────
|
|
284
279
|
lines.push('## Signatures');
|
|
285
280
|
let indexedFiles = [];
|
|
286
281
|
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
let
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
capturing = rel === targetRel || rel.endsWith('/' + targetRel) || targetRel.endsWith('/' + rel);
|
|
298
|
-
if (capturing) continue;
|
|
299
|
-
} else if (capturing) {
|
|
300
|
-
sigLines.push(line);
|
|
282
|
+
try {
|
|
283
|
+
const { buildSigIndex } = require('../retrieval/ranker');
|
|
284
|
+
const index = buildSigIndex(cwd);
|
|
285
|
+
let sigs = index.get(targetRel);
|
|
286
|
+
if (!sigs) {
|
|
287
|
+
for (const [file, fileSigs] of index.entries()) {
|
|
288
|
+
if (file === targetRel || file.endsWith('/' + targetRel) || targetRel.endsWith('/' + file)) {
|
|
289
|
+
sigs = fileSigs;
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
301
292
|
}
|
|
302
293
|
}
|
|
303
|
-
|
|
304
|
-
const sigs = sigLines.filter((l) => l !== '```' && l.trim() !== '');
|
|
305
|
-
if (sigs.length > 0) {
|
|
294
|
+
if (sigs && sigs.length > 0) {
|
|
306
295
|
lines.push(...sigs);
|
|
307
296
|
} else {
|
|
308
297
|
lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
|
|
309
298
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
.split('\n')
|
|
313
|
-
.filter((l) => l.startsWith('### '))
|
|
314
|
-
.map((l) => path.resolve(cwd, l.slice(4).trim()));
|
|
315
|
-
} else {
|
|
299
|
+
indexedFiles = [...index.keys()].map((rel) => path.resolve(cwd, rel));
|
|
300
|
+
} catch (_) {
|
|
316
301
|
lines.push('_No context file found. Run: node gen-context.js_');
|
|
317
302
|
}
|
|
318
303
|
|
|
@@ -377,37 +362,21 @@ function explainFile(args, cwd) {
|
|
|
377
362
|
* descending. Helps agents decide which module to query with read_context.
|
|
378
363
|
*/
|
|
379
364
|
function listModules(args, cwd) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const ctxLines = content.split('\n');
|
|
387
|
-
|
|
388
|
-
const groups = {}; // key: top-level dir, value: { fileCount, tokenCount }
|
|
389
|
-
let currentGroup = null;
|
|
390
|
-
let blockBuf = [];
|
|
391
|
-
|
|
392
|
-
function flushBlock() {
|
|
393
|
-
if (currentGroup === null || blockBuf.length === 0) return;
|
|
394
|
-
if (!groups[currentGroup]) groups[currentGroup] = { fileCount: 0, tokenCount: 0 };
|
|
395
|
-
groups[currentGroup].fileCount++;
|
|
396
|
-
groups[currentGroup].tokenCount += Math.ceil(blockBuf.join('\n').length / 4);
|
|
397
|
-
blockBuf = [];
|
|
398
|
-
}
|
|
365
|
+
try {
|
|
366
|
+
const { buildSigIndex } = require('../retrieval/ranker');
|
|
367
|
+
const index = buildSigIndex(cwd);
|
|
368
|
+
if (index.size === 0) {
|
|
369
|
+
return 'No context file found. Run: node gen-context.js';
|
|
370
|
+
}
|
|
399
371
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
blockBuf.push(line);
|
|
372
|
+
const groups = {};
|
|
373
|
+
for (const [rel, sigs] of index.entries()) {
|
|
374
|
+
const parts = rel.replace(/\\/g, '/').split('/');
|
|
375
|
+
const mod = parts.length > 1 ? parts[0] : '.';
|
|
376
|
+
if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
|
|
377
|
+
groups[mod].fileCount++;
|
|
378
|
+
groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
|
|
408
379
|
}
|
|
409
|
-
}
|
|
410
|
-
flushBlock();
|
|
411
380
|
|
|
412
381
|
const sorted = Object.entries(groups)
|
|
413
382
|
.map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
|
|
@@ -428,6 +397,9 @@ function listModules(args, cwd) {
|
|
|
428
397
|
'',
|
|
429
398
|
'_Use `read_context({ module: "name" })` to get signatures for a specific module._',
|
|
430
399
|
].join('\n');
|
|
400
|
+
} catch (err) {
|
|
401
|
+
return `_list_modules failed: ${err.message}_`;
|
|
402
|
+
}
|
|
431
403
|
}
|
|
432
404
|
|
|
433
405
|
/**
|
|
@@ -439,11 +411,6 @@ function listModules(args, cwd) {
|
|
|
439
411
|
function queryContext(args, cwd) {
|
|
440
412
|
if (!args || !args.query) return 'Missing required argument: query';
|
|
441
413
|
|
|
442
|
-
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
443
|
-
if (!fs.existsSync(contextPath)) {
|
|
444
|
-
return 'No context file found. Run: node gen-context.js';
|
|
445
|
-
}
|
|
446
|
-
|
|
447
414
|
try {
|
|
448
415
|
const { rank, buildSigIndex, formatRankTable } = require('../retrieval/ranker');
|
|
449
416
|
const { buildFromCwd } = require('../graph/builder');
|
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
|
}
|
|
@@ -342,6 +361,58 @@ function _parseContextFile(contextPath) {
|
|
|
342
361
|
return index;
|
|
343
362
|
}
|
|
344
363
|
|
|
364
|
+
/** Merge source index into target; prefer non-empty sig lists. */
|
|
365
|
+
function _mergeSigIndex(target, source) {
|
|
366
|
+
for (const [file, sigs] of source.entries()) {
|
|
367
|
+
if (!sigs || sigs.length === 0) continue;
|
|
368
|
+
if (!target.has(file) || target.get(file).length < sigs.length) {
|
|
369
|
+
target.set(file, sigs);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return target;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Load signatures from .sigmap-cache.json (absolute paths → repo-relative keys).
|
|
377
|
+
* @param {string} cwd
|
|
378
|
+
* @returns {Map<string, string[]>}
|
|
379
|
+
*/
|
|
380
|
+
function _buildSigIndexFromCache(cwd) {
|
|
381
|
+
const fs = require('fs');
|
|
382
|
+
const path = require('path');
|
|
383
|
+
const index = new Map();
|
|
384
|
+
try {
|
|
385
|
+
const { loadCache } = require('../cache/sig-cache');
|
|
386
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
387
|
+
let version = '0.0.0';
|
|
388
|
+
if (fs.existsSync(pkgPath)) {
|
|
389
|
+
version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || version;
|
|
390
|
+
}
|
|
391
|
+
const cache = loadCache(cwd, version);
|
|
392
|
+
for (const [absPath, entry] of cache.entries()) {
|
|
393
|
+
if (!entry || !entry.sigs || entry.sigs.length === 0) continue;
|
|
394
|
+
const rel = path.relative(cwd, absPath).replace(/\\/g, '/');
|
|
395
|
+
if (!rel || rel.startsWith('..')) continue;
|
|
396
|
+
index.set(rel, entry.sigs);
|
|
397
|
+
}
|
|
398
|
+
} catch (_) {}
|
|
399
|
+
return index;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Hot-cold and per-module strategies store most signatures outside the primary
|
|
404
|
+
* copilot-instructions.md file. MCP tools must merge all sources.
|
|
405
|
+
* @param {string} cwd
|
|
406
|
+
* @returns {Map<string, string[]>}
|
|
407
|
+
*/
|
|
408
|
+
function _enrichSigIndexFromStrategy(cwd, index) {
|
|
409
|
+
const path = require('path');
|
|
410
|
+
const coldPath = path.join(cwd, '.github', 'context-cold.md');
|
|
411
|
+
_mergeSigIndex(index, _parseContextFile(coldPath));
|
|
412
|
+
_mergeSigIndex(index, _buildSigIndexFromCache(cwd));
|
|
413
|
+
return index;
|
|
414
|
+
}
|
|
415
|
+
|
|
345
416
|
/**
|
|
346
417
|
* Build a signature index from the generated context file.
|
|
347
418
|
* Returns Map<filePath, string[]> where filePath is the relative path
|
|
@@ -363,7 +434,8 @@ function buildSigIndex(cwd, opts) {
|
|
|
363
434
|
|
|
364
435
|
// 1. Caller supplied an explicit path — use it directly.
|
|
365
436
|
if (opts && opts.contextPath) {
|
|
366
|
-
|
|
437
|
+
const index = _parseContextFile(opts.contextPath);
|
|
438
|
+
return _enrichSigIndexFromStrategy(cwd, index);
|
|
367
439
|
}
|
|
368
440
|
|
|
369
441
|
// 2. Check gen-context.config.json for a persisted customOutput path.
|
|
@@ -374,7 +446,7 @@ function buildSigIndex(cwd, opts) {
|
|
|
374
446
|
if (cfg.customOutput) {
|
|
375
447
|
const customPath = path.resolve(cwd, cfg.customOutput);
|
|
376
448
|
const index = _parseContextFile(customPath);
|
|
377
|
-
if (index.size > 0) return index;
|
|
449
|
+
if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
|
|
378
450
|
}
|
|
379
451
|
}
|
|
380
452
|
} catch (_) {}
|
|
@@ -383,10 +455,12 @@ function buildSigIndex(cwd, opts) {
|
|
|
383
455
|
for (const parts of ADAPTER_OUTPUT_PATHS) {
|
|
384
456
|
const contextPath = path.join(cwd, ...parts);
|
|
385
457
|
const index = _parseContextFile(contextPath);
|
|
386
|
-
if (index.size > 0) return index;
|
|
458
|
+
if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
|
|
387
459
|
}
|
|
388
460
|
|
|
389
|
-
|
|
461
|
+
// 4. Primary file empty/missing (hot-cold) — still serve cold + cache.
|
|
462
|
+
const fallback = new Map();
|
|
463
|
+
return _enrichSigIndexFromStrategy(cwd, fallback);
|
|
390
464
|
}
|
|
391
465
|
|
|
392
466
|
/**
|