sigmap 8.30.0 → 8.31.0
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/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/gen-context.js +310 -20
- package/llms-full.txt +4 -2
- package/llms.txt +2 -2
- package/package.json +3 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/graph/builder.js +39 -3
- package/src/graph/call-graph.js +216 -13
- package/src/mcp/server.js +1 -1
- package/src/skills/skills.js +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,20 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.31.0] — 2026-09-08
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- `sigmap lines <file> <start>-<end>` — the CLI twin of the `get_lines` MCP tool, for environments where MCP is unavailable. Accepts a `:94` anchor pasted straight off a signature with `--context N`. Delegates to the same handler as MCP, so it shares the project-root sandbox, EOF clamping and secret redaction (#568)
|
|
17
|
+
- Spring interface calls now link to their implementation, so blast radius on the class that owns the code is no longer empty. Exactly one implementation resolves outright; several resolve only via a single `@Primary`; anything still ambiguous produces no edge rather than a guess (#565)
|
|
18
|
+
- A JVM call-graph gate (`npm run validate:callgraph-jvm`) asserting named caller→callee pairs and edge volume against a committed baseline — offline and deterministic. Verified to fail on a simulated revert (#567)
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- The dependency graph was **empty** on Maven/Gradle repos: `buildFromCwd` hard-coded `srcDirs` to `src`/`app`/`lib`/`R`/`inst` and never read the project config, then capped the walk at 8 directories. Java package-import resolution already worked but was never reached. On a 524-file Spring repo: 0 → 524 nodes, 341 with importers (#561)
|
|
22
|
+
- The Java call graph discarded every `receiver.method(` call — 58% of call sites in a real Spring module — so controller→service edges did not exist. Receiver types are now resolved from field and local declarations; unresolvable receivers still produce no edge. Interface method declarations are indexed so calls to them have a target. On the same repo: 0 → 10,213 edges (#563)
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Retrieval baseline re-recorded. The hard split moved 76.7% → 74.4% purely because the index is regenerated under a token budget and this release added ~1,000 lines; holding the index fixed, pre- and post-merge code produce identical results and the same 23 misses. MRR rose 0.639 → 0.644 (#569)
|
|
26
|
+
|
|
13
27
|
## [8.30.0] — 2026-09-07
|
|
14
28
|
|
|
15
29
|
### Added
|
package/README.md
CHANGED
|
@@ -122,8 +122,8 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
122
122
|
|
|
123
123
|
<!--SM:benchmarkBlock-->
|
|
124
124
|
```
|
|
125
|
-
Benchmark : sigmap-v8.
|
|
126
|
-
Date : 2026-09-
|
|
125
|
+
Benchmark : sigmap-v8.31-main (21 repositories, including R language)
|
|
126
|
+
Date : 2026-09-08
|
|
127
127
|
|
|
128
128
|
Hit@5 : 81.1% (grep-agent baseline 44.0% — 1.73× lift)
|
|
129
129
|
Token reduction: 96.8% (across 21 repos)
|
package/gen-context.js
CHANGED
|
@@ -11752,24 +11752,60 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
11752
11752
|
return { forward, reverse };
|
|
11753
11753
|
}
|
|
11754
11754
|
|
|
11755
|
+
// Directory names assumed when neither the caller nor the project config says
|
|
11756
|
+
// otherwise. A Maven/Gradle module (`mall-portal/`, `service-api/`) matches none
|
|
11757
|
+
// of them, which is why the config is consulted first.
|
|
11758
|
+
const DEFAULT_SRC_DIRS = ['src', 'app', 'lib', 'R', 'inst'];
|
|
11759
|
+
|
|
11760
|
+
// Walk depth measured from EACH srcDir root, not from cwd — so this is not the
|
|
11761
|
+
// same quantity as the extractor's cwd-relative `maxDepth` and must not be read
|
|
11762
|
+
// from it. A standard Maven tree reaches `src/main/java/<group>/<artifact>/…`
|
|
11763
|
+
// nine directories below its module root, so the previous ceiling of 8 silently
|
|
11764
|
+
// dropped the deepest packages (on macrozheng/mall: every `service/impl/` class).
|
|
11765
|
+
const DEFAULT_WALK_DEPTH = 12;
|
|
11766
|
+
|
|
11767
|
+
/**
|
|
11768
|
+
* Source directories declared in the project's own config, or null when there
|
|
11769
|
+
* is no readable config. Read directly rather than through `loadConfig`, which
|
|
11770
|
+
* can fetch `extends` over the network and spawn a child process — neither is
|
|
11771
|
+
* acceptable inside a graph build.
|
|
11772
|
+
*/
|
|
11773
|
+
function _configuredSrcDirs(cwd) {
|
|
11774
|
+
try {
|
|
11775
|
+
const raw = fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8');
|
|
11776
|
+
const cfg = JSON.parse(raw);
|
|
11777
|
+
if (Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0) return cfg.srcDirs;
|
|
11778
|
+
} catch (_) { /* absent or unparsable — fall back to the defaults */ }
|
|
11779
|
+
return null;
|
|
11780
|
+
}
|
|
11781
|
+
|
|
11755
11782
|
/**
|
|
11756
11783
|
* Build a dependency graph scoped to a single cwd by walking all JS/TS/Py/Go
|
|
11757
11784
|
* files under srcDirs. Useful for the MCP tool handler.
|
|
11758
11785
|
*
|
|
11786
|
+
* srcDirs resolution order: explicit `opts.srcDirs` → `gen-context.config.json`
|
|
11787
|
+
* → DEFAULT_SRC_DIRS. Without the config step the graph is empty on any repo
|
|
11788
|
+
* whose sources do not sit under a conventionally-named directory.
|
|
11789
|
+
*
|
|
11759
11790
|
* @param {string} cwd
|
|
11760
11791
|
* @param {object} [opts]
|
|
11761
11792
|
* @param {string[]} [opts.srcDirs]
|
|
11762
11793
|
* @param {string[]} [opts.exclude]
|
|
11794
|
+
* @param {number} [opts.maxDepth] - walk depth from each srcDir root
|
|
11763
11795
|
* @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
|
|
11764
11796
|
*/
|
|
11765
11797
|
function buildFromCwd(cwd, opts) {
|
|
11766
11798
|
// R-package layouts use `R/` and `inst/`; Shiny apps put helpers in `R/`.
|
|
11767
11799
|
// The existence check below makes these no-ops in non-R projects.
|
|
11768
|
-
const {
|
|
11800
|
+
const {
|
|
11801
|
+
srcDirs = _configuredSrcDirs(cwd) || DEFAULT_SRC_DIRS,
|
|
11802
|
+
exclude = ['node_modules', '.git', 'dist', 'build'],
|
|
11803
|
+
maxDepth = DEFAULT_WALK_DEPTH,
|
|
11804
|
+
} = opts || {};
|
|
11769
11805
|
const excludeSet = new Set(exclude);
|
|
11770
11806
|
|
|
11771
11807
|
function walkDir(dir, depth) {
|
|
11772
|
-
if (depth >
|
|
11808
|
+
if (depth > maxDepth) return [];
|
|
11773
11809
|
let entries;
|
|
11774
11810
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return []; }
|
|
11775
11811
|
const out = [];
|
|
@@ -11818,7 +11854,7 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
11818
11854
|
return build(files, cwd, ctx);
|
|
11819
11855
|
}
|
|
11820
11856
|
|
|
11821
|
-
module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias };
|
|
11857
|
+
module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias, _configuredSrcDirs, DEFAULT_SRC_DIRS, DEFAULT_WALK_DEPTH };
|
|
11822
11858
|
|
|
11823
11859
|
};
|
|
11824
11860
|
|
|
@@ -12073,6 +12109,44 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12073
12109
|
// Java: methods + constructors with braced bodies. Statement-shaped matches
|
|
12074
12110
|
// (calls, control flow) are rejected because their `)` is followed by `;`,
|
|
12075
12111
|
// and keyword headers (`if`, `while`, …) fall to the NON_CALL guard.
|
|
12112
|
+
// Words that can precede `name(` in a STATEMENT, so their presence means the
|
|
12113
|
+
// line is not a method declaration.
|
|
12114
|
+
const STMT_KEYWORDS = new Set([
|
|
12115
|
+
'return', 'throw', 'else', 'do', 'try', 'case', 'yield', 'assert', 'new',
|
|
12116
|
+
'if', 'while', 'for', 'switch', 'catch', 'synchronized', 'instanceof', 'await',
|
|
12117
|
+
]);
|
|
12118
|
+
|
|
12119
|
+
// Types a Java class declares it implements or extends, with generic arguments
|
|
12120
|
+
// stripped and any package qualifier dropped: `implements Foo<Bar, Baz>` yields
|
|
12121
|
+
// ['Foo'], never 'Baz>'. Also records whether the class is a Spring bean and
|
|
12122
|
+
// whether it is @Primary, which is what disambiguates several implementations.
|
|
12123
|
+
function javaTypeDecl(masked) {
|
|
12124
|
+
const m = /(?:^|\n)[^\n]*?\bclass\s+([A-Za-z_$][\w$]*)([^{]*)\{/.exec(masked);
|
|
12125
|
+
if (!m) return null;
|
|
12126
|
+
const [, className, tail] = m;
|
|
12127
|
+
const supers = [];
|
|
12128
|
+
for (const kw of ['implements', 'extends']) {
|
|
12129
|
+
const k = new RegExp('\\b' + kw + '\\s+([^{]*?)(?=\\b(?:implements|extends)\\b|$)').exec(tail);
|
|
12130
|
+
if (!k) continue;
|
|
12131
|
+
let depth = 0;
|
|
12132
|
+
let cur = '';
|
|
12133
|
+
for (const ch of k[1]) {
|
|
12134
|
+
if (ch === '<') { depth++; continue; }
|
|
12135
|
+
if (ch === '>') { depth--; continue; }
|
|
12136
|
+
if (ch === ',' && depth === 0) { if (cur.trim()) supers.push(cur.trim()); cur = ''; continue; }
|
|
12137
|
+
if (depth === 0) cur += ch;
|
|
12138
|
+
}
|
|
12139
|
+
if (cur.trim()) supers.push(cur.trim());
|
|
12140
|
+
}
|
|
12141
|
+
const head = masked.slice(0, m.index + m[0].length);
|
|
12142
|
+
return {
|
|
12143
|
+
className,
|
|
12144
|
+
supers: supers.map((t) => t.split('.').pop().trim()).filter(Boolean),
|
|
12145
|
+
isBean: /@(Service|Component|Repository|Controller|RestController)\b/.test(head),
|
|
12146
|
+
isPrimary: /@Primary\b/.test(head),
|
|
12147
|
+
};
|
|
12148
|
+
}
|
|
12149
|
+
|
|
12076
12150
|
function javaDefs(masked) {
|
|
12077
12151
|
const defs = [];
|
|
12078
12152
|
const seen = new Set();
|
|
@@ -12089,11 +12163,24 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12089
12163
|
// skip `throws A, B` up to the body `{` (same line — multi-line headers are skipped)
|
|
12090
12164
|
let k = close + 1;
|
|
12091
12165
|
while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n' && masked[k] !== '=') k++;
|
|
12092
|
-
|
|
12166
|
+
// A `;` here is an interface or abstract method DECLARATION. It owns no body,
|
|
12167
|
+
// so it emits no calls — but in Spring the declared interface is what callers
|
|
12168
|
+
// name, so without it as a node every controller→service edge has no target.
|
|
12169
|
+
// Recorded with an empty body range: can receive edges, never produces them.
|
|
12170
|
+
// `before` is everything between line start and the method name. A real
|
|
12171
|
+
// declaration has modifiers or a return type there (`void chargeCard(`);
|
|
12172
|
+
// a call statement has only whitespace (`identity(1);`) or a statement
|
|
12173
|
+
// keyword (`return helper(a);`) — neither may be read as a declaration,
|
|
12174
|
+
// or the call resolves to a phantom local def instead of the real target.
|
|
12175
|
+
const headWord = (before.match(/([A-Za-z_$][\w$]*)\s*$/) || [])[1];
|
|
12176
|
+
const isDecl = masked[k] === ';' && /\S/.test(before) && !STMT_KEYWORDS.has(headWord);
|
|
12177
|
+
if (masked[k] !== '{' && !isDecl) continue;
|
|
12093
12178
|
const key = name + ':' + k;
|
|
12094
12179
|
if (seen.has(key)) continue;
|
|
12095
12180
|
seen.add(key);
|
|
12096
|
-
defs.push(
|
|
12181
|
+
defs.push(isDecl
|
|
12182
|
+
? { name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: k }
|
|
12183
|
+
: { name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
|
|
12097
12184
|
}
|
|
12098
12185
|
return defs;
|
|
12099
12186
|
}
|
|
@@ -12147,7 +12234,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12147
12234
|
const re = /([A-Za-z_$][\w$]*)\s*\(/g;
|
|
12148
12235
|
let m;
|
|
12149
12236
|
while ((m = re.exec(slice)) !== null) {
|
|
12150
|
-
// skip a `.name(` method access
|
|
12237
|
+
// skip a `.name(` method access — resolved separately via receiverCallsInRange
|
|
12151
12238
|
const before = slice[m.index - 1];
|
|
12152
12239
|
if (before === '.') continue;
|
|
12153
12240
|
if (!NON_CALL.has(m[1])) names.add(m[1]);
|
|
@@ -12155,16 +12242,75 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12155
12242
|
return names;
|
|
12156
12243
|
}
|
|
12157
12244
|
|
|
12245
|
+
// Collect `receiver.method(` pairs within [start,end). A chained or computed
|
|
12246
|
+
// receiver (`a.b().c(`, `arr[0].c(`) is skipped: only a plain identifier can be
|
|
12247
|
+
// looked up in the declaration map, and guessing is worse than no edge.
|
|
12248
|
+
function receiverCallsInRange(masked, start, end) {
|
|
12249
|
+
const slice = masked.slice(start, end);
|
|
12250
|
+
const out = [];
|
|
12251
|
+
const re = /([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/g;
|
|
12252
|
+
let m;
|
|
12253
|
+
while ((m = re.exec(slice)) !== null) {
|
|
12254
|
+
const before = slice[m.index - 1];
|
|
12255
|
+
if (before === '.' || before === ')' || before === ']') continue;
|
|
12256
|
+
if (NON_CALL.has(m[2])) continue;
|
|
12257
|
+
out.push({ receiver: m[1], method: m[2] });
|
|
12258
|
+
}
|
|
12259
|
+
return out;
|
|
12260
|
+
}
|
|
12261
|
+
|
|
12262
|
+
// `private UserService userService;` / `UserService svc = new UserService();`
|
|
12263
|
+
// / `for (OmsOrderItem item : list)` → { userService: 'UserService', … }.
|
|
12264
|
+
// Declarations only: a bare assignment carries no type and is not inferred.
|
|
12265
|
+
const DECL_RE = /(?:^|[;{}(,\n])\s*(?:(?:public|private|protected|static|final|volatile|transient)\s+)*([A-Z][\w$]*)(?:\s*<[^>;=(){}]*>)?(?:\s*\[\s*\])?\s+([a-z_$][\w$]*)\s*(?=[;=:)])/g;
|
|
12266
|
+
|
|
12267
|
+
function buildTypeMap(masked) {
|
|
12268
|
+
const map = new Map();
|
|
12269
|
+
let m;
|
|
12270
|
+
DECL_RE.lastIndex = 0;
|
|
12271
|
+
while ((m = DECL_RE.exec(masked)) !== null) {
|
|
12272
|
+
const [, type, name] = m;
|
|
12273
|
+
if (JVM_KEYWORDS.has(type) || JVM_KEYWORDS.has(name)) continue;
|
|
12274
|
+
if (!map.has(name)) map.set(name, type); // first declaration wins — deterministic
|
|
12275
|
+
}
|
|
12276
|
+
return map;
|
|
12277
|
+
}
|
|
12278
|
+
|
|
12279
|
+
// Type names that are never a user class, so never a resolvable receiver type.
|
|
12280
|
+
const JVM_KEYWORDS = new Set([
|
|
12281
|
+
'return', 'new', 'if', 'else', 'for', 'while', 'switch', 'case', 'throw', 'catch',
|
|
12282
|
+
'String', 'Integer', 'Long', 'Boolean', 'Double', 'Float', 'Object', 'List', 'Map',
|
|
12283
|
+
'Set', 'Collection', 'Optional', 'Override', 'Autowired', 'Resource', 'Deprecated',
|
|
12284
|
+
]);
|
|
12285
|
+
|
|
12158
12286
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
12159
12287
|
|
|
12160
|
-
|
|
12161
|
-
|
|
12288
|
+
// Walk depth from each srcDir root (not from cwd). A Maven module reaches
|
|
12289
|
+
// `src/main/java/<group>/<artifact>/service/impl` nine directories down, so the
|
|
12290
|
+
// previous ceiling of 8 never saw the classes that own the method bodies.
|
|
12291
|
+
const DEFAULT_WALK_DEPTH = 12;
|
|
12292
|
+
|
|
12293
|
+
/**
|
|
12294
|
+
* Source directories declared in the project's own config, or null. Read
|
|
12295
|
+
* directly rather than through `loadConfig`, which can fetch `extends` over the
|
|
12296
|
+
* network and spawn a child process — neither belongs inside a graph build.
|
|
12297
|
+
*/
|
|
12298
|
+
function _configuredSrcDirs(cwd) {
|
|
12299
|
+
try {
|
|
12300
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
|
|
12301
|
+
if (Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0) return cfg.srcDirs;
|
|
12302
|
+
} catch (_) { /* absent or unparsable — fall back to the defaults */ }
|
|
12303
|
+
return null;
|
|
12304
|
+
}
|
|
12305
|
+
|
|
12306
|
+
function _walk(dir, excludeSet, out, depth, maxDepth) {
|
|
12307
|
+
if (depth > (maxDepth === undefined ? DEFAULT_WALK_DEPTH : maxDepth)) return;
|
|
12162
12308
|
let entries;
|
|
12163
12309
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
12164
12310
|
for (const e of entries) {
|
|
12165
12311
|
if (excludeSet.has(e.name) || e.name.startsWith('.')) continue;
|
|
12166
12312
|
const full = path.join(dir, e.name);
|
|
12167
|
-
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
|
|
12313
|
+
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
|
|
12168
12314
|
else if (e.isFile()) {
|
|
12169
12315
|
const ext = path.extname(e.name).toLowerCase();
|
|
12170
12316
|
if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext) || RS_EXTS.has(ext)) out.push(full);
|
|
@@ -12190,9 +12336,13 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12190
12336
|
const excludeSet = new Set(opts.exclude || ['node_modules', '.git', 'dist', 'build', 'coverage', 'vendor']);
|
|
12191
12337
|
let files = opts.files ? opts.files.map((f) => path.resolve(f)) : [];
|
|
12192
12338
|
if (!opts.files) {
|
|
12193
|
-
|
|
12339
|
+
// Same resolution order as the dependency graph (#560): explicit opts →
|
|
12340
|
+
// the project's own config → the historical defaults. Without the config
|
|
12341
|
+
// step this is empty on any repo whose sources are not under src/app/lib.
|
|
12342
|
+
const srcDirs = opts.srcDirs || _configuredSrcDirs(cwd) || ['src', 'app', 'lib'];
|
|
12343
|
+
for (const sd of srcDirs) {
|
|
12194
12344
|
const abs = path.resolve(cwd, sd);
|
|
12195
|
-
if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0);
|
|
12345
|
+
if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0, opts.maxDepth);
|
|
12196
12346
|
}
|
|
12197
12347
|
}
|
|
12198
12348
|
|
|
@@ -12206,6 +12356,44 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12206
12356
|
const normToAbs = new Map(); // normalized abs → abs
|
|
12207
12357
|
const defs = new Map(); // symbolId → {file,name,line}
|
|
12208
12358
|
|
|
12359
|
+
// JVM convention: a public type lives in a file of the same name. This is the
|
|
12360
|
+
// deterministic type→file mapping receiver resolution needs, with no AST.
|
|
12361
|
+
const fileByTypeName = new Map(); // 'UserService' → [absFile]
|
|
12362
|
+
for (const f of files) {
|
|
12363
|
+
const ext = path.extname(f).toLowerCase();
|
|
12364
|
+
if (JAVA_EXTS.has(ext)) {
|
|
12365
|
+
const base = path.basename(f, path.extname(f));
|
|
12366
|
+
if (!fileByTypeName.has(base)) fileByTypeName.set(base, []);
|
|
12367
|
+
fileByTypeName.get(base).push(f);
|
|
12368
|
+
}
|
|
12369
|
+
}
|
|
12370
|
+
|
|
12371
|
+
// interface/superclass name → implementing files, for the Spring hop below.
|
|
12372
|
+
const implsByType = new Map(); // 'PaymentService' → [{ file, isBean, isPrimary }]
|
|
12373
|
+
for (const f of files) {
|
|
12374
|
+
if (!JAVA_EXTS.has(path.extname(f).toLowerCase())) continue;
|
|
12375
|
+
let decl;
|
|
12376
|
+
try { decl = javaTypeDecl(maskJs(fs.readFileSync(f, 'utf8'))); } catch (_) { continue; }
|
|
12377
|
+
if (!decl) continue;
|
|
12378
|
+
for (const sup of decl.supers) {
|
|
12379
|
+
if (!implsByType.has(sup)) implsByType.set(sup, []);
|
|
12380
|
+
implsByType.get(sup).push({ file: f, isBean: decl.isBean, isPrimary: decl.isPrimary });
|
|
12381
|
+
}
|
|
12382
|
+
}
|
|
12383
|
+
|
|
12384
|
+
/**
|
|
12385
|
+
* The single implementing file for a type, or null when it is ambiguous.
|
|
12386
|
+
* One implementation resolves outright; several resolve only via @Primary.
|
|
12387
|
+
* Anything still ambiguous yields no edge — polymorphism is not guessed.
|
|
12388
|
+
*/
|
|
12389
|
+
const soleImpl = (typeName) => {
|
|
12390
|
+
const cands = implsByType.get(typeName) || [];
|
|
12391
|
+
if (cands.length === 1) return cands[0].file;
|
|
12392
|
+
const primary = cands.filter((c) => c.isPrimary);
|
|
12393
|
+
if (primary.length === 1) return primary[0].file;
|
|
12394
|
+
return null;
|
|
12395
|
+
};
|
|
12396
|
+
|
|
12209
12397
|
for (const f of files) {
|
|
12210
12398
|
normToAbs.set(normalizePath(path.resolve(f)), path.resolve(f));
|
|
12211
12399
|
let src;
|
|
@@ -12225,12 +12413,20 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12225
12413
|
|
|
12226
12414
|
const forward = new Map();
|
|
12227
12415
|
const reverse = new Map();
|
|
12228
|
-
|
|
12416
|
+
// Additive: `forward`/`reverse` keep their existing shape, so every current
|
|
12417
|
+
// consumer is unaffected. Confidence is looked up by "from\u0000to".
|
|
12418
|
+
const edgeConfidence = new Map();
|
|
12419
|
+
const addEdge = (from, to, confidence) => {
|
|
12229
12420
|
if (from === to) return;
|
|
12230
12421
|
if (!forward.has(from)) forward.set(from, new Set());
|
|
12231
12422
|
forward.get(from).add(to);
|
|
12232
12423
|
if (!reverse.has(to)) reverse.set(to, new Set());
|
|
12233
12424
|
reverse.get(to).add(from);
|
|
12425
|
+
if (confidence) {
|
|
12426
|
+
const k = from + '\u0000' + to;
|
|
12427
|
+
// A 'high' resolution never loses to a later 'medium' one.
|
|
12428
|
+
if (edgeConfidence.get(k) !== 'high') edgeConfidence.set(k, confidence);
|
|
12429
|
+
}
|
|
12234
12430
|
};
|
|
12235
12431
|
|
|
12236
12432
|
for (const [f, fileDefs] of perFileDefs.entries()) {
|
|
@@ -12248,16 +12444,59 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12248
12444
|
.sort();
|
|
12249
12445
|
importedAbs.push(...siblings);
|
|
12250
12446
|
}
|
|
12447
|
+
// Receiver types come from declarations anywhere in the file: fields are
|
|
12448
|
+
// declared outside any method body, locals inside one.
|
|
12449
|
+
const typeMap = JAVA_EXTS.has(ext) ? buildTypeMap(masked) : null;
|
|
12450
|
+
// Types reachable from this file, by name — imports first, then same-package
|
|
12451
|
+
// siblings, so an import always wins over a coincidental sibling name.
|
|
12452
|
+
const scopeByTypeName = new Map();
|
|
12453
|
+
if (typeMap) {
|
|
12454
|
+
for (const imp of importedAbs) {
|
|
12455
|
+
const base = path.basename(imp, path.extname(imp));
|
|
12456
|
+
if (!scopeByTypeName.has(base)) scopeByTypeName.set(base, imp);
|
|
12457
|
+
}
|
|
12458
|
+
}
|
|
12459
|
+
|
|
12251
12460
|
for (const d of fileDefs) {
|
|
12252
12461
|
const callerId = symId(cwd, f, d.name);
|
|
12253
12462
|
if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
|
|
12254
12463
|
const callees = callsInRange(masked, d.bodyStart, d.bodyEnd);
|
|
12255
12464
|
for (const nm of callees) {
|
|
12256
12465
|
const local = (defsByName.get(f) || new Map()).get(nm);
|
|
12257
|
-
if (local && local.length) { for (const id of local) addEdge(callerId, id); continue; }
|
|
12466
|
+
if (local && local.length) { for (const id of local) addEdge(callerId, id, 'high'); continue; }
|
|
12258
12467
|
for (const imp of importedAbs) {
|
|
12259
12468
|
const ids = (defsByName.get(imp) || new Map()).get(nm);
|
|
12260
|
-
if (ids && ids.length) { for (const id of ids) addEdge(callerId, id); break; }
|
|
12469
|
+
if (ids && ids.length) { for (const id of ids) addEdge(callerId, id, 'high'); break; }
|
|
12470
|
+
}
|
|
12471
|
+
}
|
|
12472
|
+
|
|
12473
|
+
// `receiver.method(` — resolve the receiver's declared type to a file.
|
|
12474
|
+
if (!typeMap) continue;
|
|
12475
|
+
for (const { receiver, method } of receiverCallsInRange(masked, d.bodyStart, d.bodyEnd)) {
|
|
12476
|
+
// A receiver that is itself a type name is a static call: `Foo.bar()`.
|
|
12477
|
+
const typeName = typeMap.get(receiver)
|
|
12478
|
+
|| (fileByTypeName.has(receiver) ? receiver : null);
|
|
12479
|
+
if (!typeName) continue; // unknown receiver → no edge, never a guess
|
|
12480
|
+
|
|
12481
|
+
let target = scopeByTypeName.get(typeName);
|
|
12482
|
+
let confidence = 'high'; // typed receiver, resolved in scope
|
|
12483
|
+
if (!target) {
|
|
12484
|
+
const candidates = fileByTypeName.get(typeName) || [];
|
|
12485
|
+
if (candidates.length !== 1) continue; // ambiguous or absent → no edge
|
|
12486
|
+
target = candidates[0];
|
|
12487
|
+
confidence = 'medium'; // type known, but not in this file's scope
|
|
12488
|
+
}
|
|
12489
|
+
const ids = (defsByName.get(target) || new Map()).get(method);
|
|
12490
|
+
if (ids && ids.length) for (const id of ids) addEdge(callerId, id, confidence);
|
|
12491
|
+
|
|
12492
|
+
// Spring: the call names the interface, but the code that runs — and
|
|
12493
|
+
// that a reviewer changes — lives in the implementation. Both edges are
|
|
12494
|
+
// true, so both are recorded; without the second, blast radius on an
|
|
12495
|
+
// implementation is empty.
|
|
12496
|
+
const implFile = soleImpl(typeName);
|
|
12497
|
+
if (implFile && implFile !== target) {
|
|
12498
|
+
const implIds = (defsByName.get(implFile) || new Map()).get(method);
|
|
12499
|
+
if (implIds && implIds.length) for (const id of implIds) addEdge(callerId, id, confidence);
|
|
12261
12500
|
}
|
|
12262
12501
|
}
|
|
12263
12502
|
}
|
|
@@ -12268,7 +12507,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12268
12507
|
for (const [k, set] of mapOfSets.entries()) out.set(k, [...set]);
|
|
12269
12508
|
return out;
|
|
12270
12509
|
};
|
|
12271
|
-
return { forward: toArr(forward), reverse: toArr(reverse), defs };
|
|
12510
|
+
return { forward: toArr(forward), reverse: toArr(reverse), defs, edgeConfidence };
|
|
12272
12511
|
}
|
|
12273
12512
|
|
|
12274
12513
|
/**
|
|
@@ -12390,7 +12629,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
|
|
|
12390
12629
|
}
|
|
12391
12630
|
|
|
12392
12631
|
module.exports = {
|
|
12393
|
-
buildCallGraph, buildCallFileGraph, methodImpact, methodCallees,
|
|
12632
|
+
buildCallGraph, buildTypeMap, receiverCallsInRange, javaTypeDecl, DEFAULT_WALK_DEPTH, buildCallFileGraph, methodImpact, methodCallees,
|
|
12394
12633
|
formatCallGraph, formatCallGraphJSON,
|
|
12395
12634
|
extractDefs, maskJs, maskPy, maskRust,
|
|
12396
12635
|
};
|
|
@@ -15440,7 +15679,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
15440
15679
|
|
|
15441
15680
|
const SERVER_INFO = {
|
|
15442
15681
|
name: 'sigmap',
|
|
15443
|
-
version: '8.
|
|
15682
|
+
version: '8.31.0',
|
|
15444
15683
|
description: 'SigMap MCP server — code signatures on demand',
|
|
15445
15684
|
};
|
|
15446
15685
|
|
|
@@ -18805,7 +19044,7 @@ __factories["./src/skills/skills"] = function(module, exports) {
|
|
|
18805
19044
|
'Follow this loop before any file exploration in a repo with SigMap installed.',
|
|
18806
19045
|
'',
|
|
18807
19046
|
'1. **Ask before reading.** `sigmap ask "<task>"` (or the `query_context` MCP tool) ranks the relevant files as ~hundreds of tokens of signatures instead of thousands of raw-file tokens. Never open files to "look around".',
|
|
18808
|
-
'2. **Read ranges, not files.** Use the `get_lines` MCP tool with the `:start-end` line anchors carried on every signature to pull only the lines you need.',
|
|
19047
|
+
'2. **Read ranges, not files.** Use the `get_lines` MCP tool — or `sigmap lines <file> :<line> --context <n>` where MCP is unavailable — with the `:start-end` line anchors carried on every signature to pull only the lines you need.',
|
|
18809
19048
|
'3. **Ground before trusting.** Run the `verify_suggestion` MCP tool (or `sigmap verify-ai-output`) on generated code before applying it — it flags fabricated files, imports, symbols, and npm scripts against the live index.',
|
|
18810
19049
|
'4. **Squeeze big pastes.** Any stack trace, CI/build log, or JSON blob goes through `sigmap squeeze` (or the `squeeze_output` MCP tool) before it enters context — the signal survives, the noise does not.',
|
|
18811
19050
|
'5. **Checkpoint progress.** Use the `create_checkpoint` MCP tool or `sigmap note "<decision>"` so a follow-up session resumes without re-deriving state.',
|
|
@@ -18823,7 +19062,7 @@ __factories["./src/skills/skills"] = function(module, exports) {
|
|
|
18823
19062
|
'',
|
|
18824
19063
|
'1. **Look up, do not search.** `npx sigmap ask "<the task>"` — this writes `.context/query-context.md`.',
|
|
18825
19064
|
'2. **Read the map.** `cat .context/query-context.md`. It ranks the relevant files and lists their signatures with `:start-end` line anchors — a few hundred tokens where the same files read whole are tens of thousands. Say which files it surfaced before continuing. If nothing relevant appears, re-run step 1 with different wording; fall back to search only after two attempts, and say so.',
|
|
18826
|
-
'3. **
|
|
19065
|
+
'3. **Read the anchored range, by command.** A signature ending `:425-425` means line 425, not the 547-line file. Run `npx sigmap lines <file> :425 --context 10` — paste the anchor straight off the signature. Never `cat` a whole file when you hold an anchor for it: on a real repo a 220-line span costs ~2,700 tokens where the anchored window costs ~220.',
|
|
18827
19066
|
'4. **Make the change.** Follow the conventions visible in the signatures — same layering, same response wrapper, same annotation style. Add no dependencies.',
|
|
18828
19067
|
'5. **Verify before reporting.** Write what you changed to `.sigmap-notes.md`, naming every file by its **full repository-relative path** (a bare filename is reported as fake), then run `npx sigmap verify-ai-output .sigmap-notes.md`. It checks every name against the real index, offline, with no model call. Fix anything it flags and re-run before you reply.',
|
|
18829
19068
|
'6. **Refresh the map.** `npx sigmap` — your edits made it stale.',
|
|
@@ -21734,7 +21973,7 @@ function __tryGit(args, opts = {}) {
|
|
|
21734
21973
|
catch (_) { return ''; }
|
|
21735
21974
|
}
|
|
21736
21975
|
|
|
21737
|
-
const VERSION = '8.
|
|
21976
|
+
const VERSION = '8.31.0';
|
|
21738
21977
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
21739
21978
|
|
|
21740
21979
|
function requireSourceOrBundled(key) {
|
|
@@ -23740,6 +23979,8 @@ Usage:
|
|
|
23740
23979
|
${cmd} tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
|
|
23741
23980
|
${cmd} skills list List skill clients (Claude/Cursor/Windsurf/Copilot/AGENTS.md) and install state (--json)
|
|
23742
23981
|
${cmd} skills install Install the SigMap agent playbooks for detected clients (--client <name> | --all)
|
|
23982
|
+
${cmd} lines <file> <start>-<end> Print an exact line range — CLI twin of get_lines (secrets redacted)
|
|
23983
|
+
${cmd} lines <file> :<line> --context <n> Window around one signature anchor (default ±10)
|
|
23743
23984
|
${cmd} note "<text>" Append a note to the cross-session decision log
|
|
23744
23985
|
${cmd} note List recent notes (also: note --list <N>)
|
|
23745
23986
|
${cmd} status Show repo state — branch, dirty files, index freshness, notes
|
|
@@ -25343,6 +25584,55 @@ function main() {
|
|
|
25343
25584
|
process.exit(0);
|
|
25344
25585
|
}
|
|
25345
25586
|
|
|
25587
|
+
// `sigmap lines <file> <start>-<end>` — the CLI twin of the get_lines MCP
|
|
25588
|
+
// tool. Without it, an agent in an MCP-less environment receives precise
|
|
25589
|
+
// `:start-end` anchors from `ask` and has no sanctioned way to spend them,
|
|
25590
|
+
// so it falls back to reading whole files and throws the saving away.
|
|
25591
|
+
// Delegates to the same handler as MCP so both paths share the sandbox,
|
|
25592
|
+
// the bounds clamping and the secret redaction.
|
|
25593
|
+
if (args[0] === 'lines') {
|
|
25594
|
+
const valOf = (f) => { const i = args.indexOf(f); return i >= 0 && args[i + 1] ? args[i + 1] : null; };
|
|
25595
|
+
const positional = [];
|
|
25596
|
+
const VALUE_FLAGS = new Set(['--cwd', '--context']);
|
|
25597
|
+
for (let i = 1; i < args.length; i++) {
|
|
25598
|
+
const a = args[i];
|
|
25599
|
+
if (a.startsWith('--')) { if (VALUE_FLAGS.has(a)) i++; continue; }
|
|
25600
|
+
positional.push(a);
|
|
25601
|
+
}
|
|
25602
|
+
const file = positional[0];
|
|
25603
|
+
const range = positional[1];
|
|
25604
|
+
if (!file || !range) {
|
|
25605
|
+
console.error('[sigmap] usage: sigmap lines <file> <start>-<end> (or :<line> --context <n>)');
|
|
25606
|
+
process.exit(2);
|
|
25607
|
+
}
|
|
25608
|
+
|
|
25609
|
+
// Accept `84-104`, a bare `94`, or the `:94` form copied straight off a
|
|
25610
|
+
// signature anchor — the whole point is to paste what `ask` printed.
|
|
25611
|
+
const ctx = Math.max(0, parseInt(valOf('--context') || '10', 10));
|
|
25612
|
+
let start;
|
|
25613
|
+
let end;
|
|
25614
|
+
const span = /^:?(\d+)\s*-\s*(\d+)$/.exec(range);
|
|
25615
|
+
const single = /^:?(\d+)$/.exec(range);
|
|
25616
|
+
if (span) { start = parseInt(span[1], 10); end = parseInt(span[2], 10); }
|
|
25617
|
+
else if (single) { const n = parseInt(single[1], 10); start = Math.max(1, n - ctx); end = n + ctx; }
|
|
25618
|
+
else {
|
|
25619
|
+
console.error(`[sigmap] lines: could not read range "${range}" — expected <start>-<end> or :<line>`);
|
|
25620
|
+
process.exit(2);
|
|
25621
|
+
}
|
|
25622
|
+
|
|
25623
|
+
const { getLines } = requireSourceOrBundled('./src/mcp/handlers');
|
|
25624
|
+
const out = getLines({ file, start, end }, cwd);
|
|
25625
|
+
// The handler reports its own failures as prose; surface them on stderr
|
|
25626
|
+
// with a non-zero exit so a script can tell a hit from a miss.
|
|
25627
|
+
if (/^(Missing required argument|Refused:|File not found:|Could not read |Arguments )/.test(out)
|
|
25628
|
+
|| /has only \d+ lines; requested/.test(out)) {
|
|
25629
|
+
console.error('[sigmap] ' + out);
|
|
25630
|
+
process.exit(1);
|
|
25631
|
+
}
|
|
25632
|
+
process.stdout.write(out + '\n');
|
|
25633
|
+
process.exit(0);
|
|
25634
|
+
}
|
|
25635
|
+
|
|
25346
25636
|
if (args[0] === 'note') {
|
|
25347
25637
|
const jsonOut = args.includes('--json');
|
|
25348
25638
|
const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');
|
package/llms-full.txt
CHANGED
|
@@ -11,13 +11,13 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.31.0 | Benchmark: sigmap-v8.31-main (2026-09-08)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
20
|
+
## Core metrics (benchmark: sigmap-v8.31-main, 2026-09-08)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
@@ -132,6 +132,8 @@ sigmap tune Recommend config from repo detection
|
|
|
132
132
|
sigmap tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
|
|
133
133
|
sigmap skills list List skill clients (Claude/Cursor/Windsurf/Copilot/AGENTS.md) and install state (--json)
|
|
134
134
|
sigmap skills install Install the SigMap agent playbooks for detected clients (--client <name> | --all)
|
|
135
|
+
sigmap lines <file> <start>-<end> Print an exact line range — CLI twin of get_lines (secrets redacted)
|
|
136
|
+
sigmap lines <file> :<line> --context <n> Window around one signature anchor (default ±10)
|
|
135
137
|
sigmap note "<text>" Append a note to the cross-session decision log
|
|
136
138
|
sigmap note List recent notes (also: note --list <N>)
|
|
137
139
|
sigmap status Show repo state — branch, dirty files, index freshness, notes
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.31.0 | Benchmark: sigmap-v8.31-main (2026-09-08)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
23
23
|
- No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
|
|
24
24
|
- Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
|
|
25
25
|
|
|
26
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.31-main, 2026-09-08)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 81.1% vs 44.0% single-shot grep baseline (1.73× lift)
|
|
29
29
|
- Token reduction: 96.8% average across benchmark repos
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.31.0",
|
|
4
4
|
"description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
"benchmark:test-discovery": "node scripts/run-test-discovery-benchmark.mjs --save",
|
|
32
32
|
"benchmark:terse": "node scripts/run-terse-benchmark.mjs --save",
|
|
33
33
|
"benchmark:callgraph-boost": "node scripts/run-callgraph-boost-benchmark.mjs --save",
|
|
34
|
+
"benchmark:callgraph-jvm": "node scripts/run-callgraph-jvm-benchmark.mjs",
|
|
35
|
+
"validate:callgraph-jvm": "node scripts/run-callgraph-jvm-benchmark.mjs --gate",
|
|
34
36
|
"benchmark:centrality-blend": "node scripts/run-centrality-blend-benchmark.mjs --save",
|
|
35
37
|
"benchmark:surface-enrichment": "node scripts/run-surface-enrichment-benchmark.mjs --save",
|
|
36
38
|
"validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
|
package/src/graph/builder.js
CHANGED
|
@@ -434,24 +434,60 @@ function build(files, cwd, ctx) {
|
|
|
434
434
|
return { forward, reverse };
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
+
// Directory names assumed when neither the caller nor the project config says
|
|
438
|
+
// otherwise. A Maven/Gradle module (`mall-portal/`, `service-api/`) matches none
|
|
439
|
+
// of them, which is why the config is consulted first.
|
|
440
|
+
const DEFAULT_SRC_DIRS = ['src', 'app', 'lib', 'R', 'inst'];
|
|
441
|
+
|
|
442
|
+
// Walk depth measured from EACH srcDir root, not from cwd — so this is not the
|
|
443
|
+
// same quantity as the extractor's cwd-relative `maxDepth` and must not be read
|
|
444
|
+
// from it. A standard Maven tree reaches `src/main/java/<group>/<artifact>/…`
|
|
445
|
+
// nine directories below its module root, so the previous ceiling of 8 silently
|
|
446
|
+
// dropped the deepest packages (on macrozheng/mall: every `service/impl/` class).
|
|
447
|
+
const DEFAULT_WALK_DEPTH = 12;
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Source directories declared in the project's own config, or null when there
|
|
451
|
+
* is no readable config. Read directly rather than through `loadConfig`, which
|
|
452
|
+
* can fetch `extends` over the network and spawn a child process — neither is
|
|
453
|
+
* acceptable inside a graph build.
|
|
454
|
+
*/
|
|
455
|
+
function _configuredSrcDirs(cwd) {
|
|
456
|
+
try {
|
|
457
|
+
const raw = fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8');
|
|
458
|
+
const cfg = JSON.parse(raw);
|
|
459
|
+
if (Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0) return cfg.srcDirs;
|
|
460
|
+
} catch (_) { /* absent or unparsable — fall back to the defaults */ }
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
437
464
|
/**
|
|
438
465
|
* Build a dependency graph scoped to a single cwd by walking all JS/TS/Py/Go
|
|
439
466
|
* files under srcDirs. Useful for the MCP tool handler.
|
|
440
467
|
*
|
|
468
|
+
* srcDirs resolution order: explicit `opts.srcDirs` → `gen-context.config.json`
|
|
469
|
+
* → DEFAULT_SRC_DIRS. Without the config step the graph is empty on any repo
|
|
470
|
+
* whose sources do not sit under a conventionally-named directory.
|
|
471
|
+
*
|
|
441
472
|
* @param {string} cwd
|
|
442
473
|
* @param {object} [opts]
|
|
443
474
|
* @param {string[]} [opts.srcDirs]
|
|
444
475
|
* @param {string[]} [opts.exclude]
|
|
476
|
+
* @param {number} [opts.maxDepth] - walk depth from each srcDir root
|
|
445
477
|
* @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
|
|
446
478
|
*/
|
|
447
479
|
function buildFromCwd(cwd, opts) {
|
|
448
480
|
// R-package layouts use `R/` and `inst/`; Shiny apps put helpers in `R/`.
|
|
449
481
|
// The existence check below makes these no-ops in non-R projects.
|
|
450
|
-
const {
|
|
482
|
+
const {
|
|
483
|
+
srcDirs = _configuredSrcDirs(cwd) || DEFAULT_SRC_DIRS,
|
|
484
|
+
exclude = ['node_modules', '.git', 'dist', 'build'],
|
|
485
|
+
maxDepth = DEFAULT_WALK_DEPTH,
|
|
486
|
+
} = opts || {};
|
|
451
487
|
const excludeSet = new Set(exclude);
|
|
452
488
|
|
|
453
489
|
function walkDir(dir, depth) {
|
|
454
|
-
if (depth >
|
|
490
|
+
if (depth > maxDepth) return [];
|
|
455
491
|
let entries;
|
|
456
492
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return []; }
|
|
457
493
|
const out = [];
|
|
@@ -500,4 +536,4 @@ function buildFromCwd(cwd, opts) {
|
|
|
500
536
|
return build(files, cwd, ctx);
|
|
501
537
|
}
|
|
502
538
|
|
|
503
|
-
module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias };
|
|
539
|
+
module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias, _configuredSrcDirs, DEFAULT_SRC_DIRS, DEFAULT_WALK_DEPTH };
|
package/src/graph/call-graph.js
CHANGED
|
@@ -248,6 +248,44 @@ function goDefs(masked) {
|
|
|
248
248
|
// Java: methods + constructors with braced bodies. Statement-shaped matches
|
|
249
249
|
// (calls, control flow) are rejected because their `)` is followed by `;`,
|
|
250
250
|
// and keyword headers (`if`, `while`, …) fall to the NON_CALL guard.
|
|
251
|
+
// Words that can precede `name(` in a STATEMENT, so their presence means the
|
|
252
|
+
// line is not a method declaration.
|
|
253
|
+
const STMT_KEYWORDS = new Set([
|
|
254
|
+
'return', 'throw', 'else', 'do', 'try', 'case', 'yield', 'assert', 'new',
|
|
255
|
+
'if', 'while', 'for', 'switch', 'catch', 'synchronized', 'instanceof', 'await',
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
// Types a Java class declares it implements or extends, with generic arguments
|
|
259
|
+
// stripped and any package qualifier dropped: `implements Foo<Bar, Baz>` yields
|
|
260
|
+
// ['Foo'], never 'Baz>'. Also records whether the class is a Spring bean and
|
|
261
|
+
// whether it is @Primary, which is what disambiguates several implementations.
|
|
262
|
+
function javaTypeDecl(masked) {
|
|
263
|
+
const m = /(?:^|\n)[^\n]*?\bclass\s+([A-Za-z_$][\w$]*)([^{]*)\{/.exec(masked);
|
|
264
|
+
if (!m) return null;
|
|
265
|
+
const [, className, tail] = m;
|
|
266
|
+
const supers = [];
|
|
267
|
+
for (const kw of ['implements', 'extends']) {
|
|
268
|
+
const k = new RegExp('\\b' + kw + '\\s+([^{]*?)(?=\\b(?:implements|extends)\\b|$)').exec(tail);
|
|
269
|
+
if (!k) continue;
|
|
270
|
+
let depth = 0;
|
|
271
|
+
let cur = '';
|
|
272
|
+
for (const ch of k[1]) {
|
|
273
|
+
if (ch === '<') { depth++; continue; }
|
|
274
|
+
if (ch === '>') { depth--; continue; }
|
|
275
|
+
if (ch === ',' && depth === 0) { if (cur.trim()) supers.push(cur.trim()); cur = ''; continue; }
|
|
276
|
+
if (depth === 0) cur += ch;
|
|
277
|
+
}
|
|
278
|
+
if (cur.trim()) supers.push(cur.trim());
|
|
279
|
+
}
|
|
280
|
+
const head = masked.slice(0, m.index + m[0].length);
|
|
281
|
+
return {
|
|
282
|
+
className,
|
|
283
|
+
supers: supers.map((t) => t.split('.').pop().trim()).filter(Boolean),
|
|
284
|
+
isBean: /@(Service|Component|Repository|Controller|RestController)\b/.test(head),
|
|
285
|
+
isPrimary: /@Primary\b/.test(head),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
251
289
|
function javaDefs(masked) {
|
|
252
290
|
const defs = [];
|
|
253
291
|
const seen = new Set();
|
|
@@ -264,11 +302,24 @@ function javaDefs(masked) {
|
|
|
264
302
|
// skip `throws A, B` up to the body `{` (same line — multi-line headers are skipped)
|
|
265
303
|
let k = close + 1;
|
|
266
304
|
while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n' && masked[k] !== '=') k++;
|
|
267
|
-
|
|
305
|
+
// A `;` here is an interface or abstract method DECLARATION. It owns no body,
|
|
306
|
+
// so it emits no calls — but in Spring the declared interface is what callers
|
|
307
|
+
// name, so without it as a node every controller→service edge has no target.
|
|
308
|
+
// Recorded with an empty body range: can receive edges, never produces them.
|
|
309
|
+
// `before` is everything between line start and the method name. A real
|
|
310
|
+
// declaration has modifiers or a return type there (`void chargeCard(`);
|
|
311
|
+
// a call statement has only whitespace (`identity(1);`) or a statement
|
|
312
|
+
// keyword (`return helper(a);`) — neither may be read as a declaration,
|
|
313
|
+
// or the call resolves to a phantom local def instead of the real target.
|
|
314
|
+
const headWord = (before.match(/([A-Za-z_$][\w$]*)\s*$/) || [])[1];
|
|
315
|
+
const isDecl = masked[k] === ';' && /\S/.test(before) && !STMT_KEYWORDS.has(headWord);
|
|
316
|
+
if (masked[k] !== '{' && !isDecl) continue;
|
|
268
317
|
const key = name + ':' + k;
|
|
269
318
|
if (seen.has(key)) continue;
|
|
270
319
|
seen.add(key);
|
|
271
|
-
defs.push(
|
|
320
|
+
defs.push(isDecl
|
|
321
|
+
? { name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: k }
|
|
322
|
+
: { name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
|
|
272
323
|
}
|
|
273
324
|
return defs;
|
|
274
325
|
}
|
|
@@ -322,7 +373,7 @@ function callsInRange(masked, start, end) {
|
|
|
322
373
|
const re = /([A-Za-z_$][\w$]*)\s*\(/g;
|
|
323
374
|
let m;
|
|
324
375
|
while ((m = re.exec(slice)) !== null) {
|
|
325
|
-
// skip a `.name(` method access
|
|
376
|
+
// skip a `.name(` method access — resolved separately via receiverCallsInRange
|
|
326
377
|
const before = slice[m.index - 1];
|
|
327
378
|
if (before === '.') continue;
|
|
328
379
|
if (!NON_CALL.has(m[1])) names.add(m[1]);
|
|
@@ -330,16 +381,75 @@ function callsInRange(masked, start, end) {
|
|
|
330
381
|
return names;
|
|
331
382
|
}
|
|
332
383
|
|
|
384
|
+
// Collect `receiver.method(` pairs within [start,end). A chained or computed
|
|
385
|
+
// receiver (`a.b().c(`, `arr[0].c(`) is skipped: only a plain identifier can be
|
|
386
|
+
// looked up in the declaration map, and guessing is worse than no edge.
|
|
387
|
+
function receiverCallsInRange(masked, start, end) {
|
|
388
|
+
const slice = masked.slice(start, end);
|
|
389
|
+
const out = [];
|
|
390
|
+
const re = /([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/g;
|
|
391
|
+
let m;
|
|
392
|
+
while ((m = re.exec(slice)) !== null) {
|
|
393
|
+
const before = slice[m.index - 1];
|
|
394
|
+
if (before === '.' || before === ')' || before === ']') continue;
|
|
395
|
+
if (NON_CALL.has(m[2])) continue;
|
|
396
|
+
out.push({ receiver: m[1], method: m[2] });
|
|
397
|
+
}
|
|
398
|
+
return out;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// `private UserService userService;` / `UserService svc = new UserService();`
|
|
402
|
+
// / `for (OmsOrderItem item : list)` → { userService: 'UserService', … }.
|
|
403
|
+
// Declarations only: a bare assignment carries no type and is not inferred.
|
|
404
|
+
const DECL_RE = /(?:^|[;{}(,\n])\s*(?:(?:public|private|protected|static|final|volatile|transient)\s+)*([A-Z][\w$]*)(?:\s*<[^>;=(){}]*>)?(?:\s*\[\s*\])?\s+([a-z_$][\w$]*)\s*(?=[;=:)])/g;
|
|
405
|
+
|
|
406
|
+
function buildTypeMap(masked) {
|
|
407
|
+
const map = new Map();
|
|
408
|
+
let m;
|
|
409
|
+
DECL_RE.lastIndex = 0;
|
|
410
|
+
while ((m = DECL_RE.exec(masked)) !== null) {
|
|
411
|
+
const [, type, name] = m;
|
|
412
|
+
if (JVM_KEYWORDS.has(type) || JVM_KEYWORDS.has(name)) continue;
|
|
413
|
+
if (!map.has(name)) map.set(name, type); // first declaration wins — deterministic
|
|
414
|
+
}
|
|
415
|
+
return map;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Type names that are never a user class, so never a resolvable receiver type.
|
|
419
|
+
const JVM_KEYWORDS = new Set([
|
|
420
|
+
'return', 'new', 'if', 'else', 'for', 'while', 'switch', 'case', 'throw', 'catch',
|
|
421
|
+
'String', 'Integer', 'Long', 'Boolean', 'Double', 'Float', 'Object', 'List', 'Map',
|
|
422
|
+
'Set', 'Collection', 'Optional', 'Override', 'Autowired', 'Resource', 'Deprecated',
|
|
423
|
+
]);
|
|
424
|
+
|
|
333
425
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
334
426
|
|
|
335
|
-
|
|
336
|
-
|
|
427
|
+
// Walk depth from each srcDir root (not from cwd). A Maven module reaches
|
|
428
|
+
// `src/main/java/<group>/<artifact>/service/impl` nine directories down, so the
|
|
429
|
+
// previous ceiling of 8 never saw the classes that own the method bodies.
|
|
430
|
+
const DEFAULT_WALK_DEPTH = 12;
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Source directories declared in the project's own config, or null. Read
|
|
434
|
+
* directly rather than through `loadConfig`, which can fetch `extends` over the
|
|
435
|
+
* network and spawn a child process — neither belongs inside a graph build.
|
|
436
|
+
*/
|
|
437
|
+
function _configuredSrcDirs(cwd) {
|
|
438
|
+
try {
|
|
439
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
|
|
440
|
+
if (Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0) return cfg.srcDirs;
|
|
441
|
+
} catch (_) { /* absent or unparsable — fall back to the defaults */ }
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function _walk(dir, excludeSet, out, depth, maxDepth) {
|
|
446
|
+
if (depth > (maxDepth === undefined ? DEFAULT_WALK_DEPTH : maxDepth)) return;
|
|
337
447
|
let entries;
|
|
338
448
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
|
339
449
|
for (const e of entries) {
|
|
340
450
|
if (excludeSet.has(e.name) || e.name.startsWith('.')) continue;
|
|
341
451
|
const full = path.join(dir, e.name);
|
|
342
|
-
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
|
|
452
|
+
if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
|
|
343
453
|
else if (e.isFile()) {
|
|
344
454
|
const ext = path.extname(e.name).toLowerCase();
|
|
345
455
|
if (JS_EXTS.has(ext) || PY_EXTS.has(ext) || JAVA_EXTS.has(ext) || GO_EXTS.has(ext) || RS_EXTS.has(ext)) out.push(full);
|
|
@@ -365,9 +475,13 @@ function buildCallGraph(cwd, opts = {}) {
|
|
|
365
475
|
const excludeSet = new Set(opts.exclude || ['node_modules', '.git', 'dist', 'build', 'coverage', 'vendor']);
|
|
366
476
|
let files = opts.files ? opts.files.map((f) => path.resolve(f)) : [];
|
|
367
477
|
if (!opts.files) {
|
|
368
|
-
|
|
478
|
+
// Same resolution order as the dependency graph (#560): explicit opts →
|
|
479
|
+
// the project's own config → the historical defaults. Without the config
|
|
480
|
+
// step this is empty on any repo whose sources are not under src/app/lib.
|
|
481
|
+
const srcDirs = opts.srcDirs || _configuredSrcDirs(cwd) || ['src', 'app', 'lib'];
|
|
482
|
+
for (const sd of srcDirs) {
|
|
369
483
|
const abs = path.resolve(cwd, sd);
|
|
370
|
-
if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0);
|
|
484
|
+
if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0, opts.maxDepth);
|
|
371
485
|
}
|
|
372
486
|
}
|
|
373
487
|
|
|
@@ -381,6 +495,44 @@ function buildCallGraph(cwd, opts = {}) {
|
|
|
381
495
|
const normToAbs = new Map(); // normalized abs → abs
|
|
382
496
|
const defs = new Map(); // symbolId → {file,name,line}
|
|
383
497
|
|
|
498
|
+
// JVM convention: a public type lives in a file of the same name. This is the
|
|
499
|
+
// deterministic type→file mapping receiver resolution needs, with no AST.
|
|
500
|
+
const fileByTypeName = new Map(); // 'UserService' → [absFile]
|
|
501
|
+
for (const f of files) {
|
|
502
|
+
const ext = path.extname(f).toLowerCase();
|
|
503
|
+
if (JAVA_EXTS.has(ext)) {
|
|
504
|
+
const base = path.basename(f, path.extname(f));
|
|
505
|
+
if (!fileByTypeName.has(base)) fileByTypeName.set(base, []);
|
|
506
|
+
fileByTypeName.get(base).push(f);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// interface/superclass name → implementing files, for the Spring hop below.
|
|
511
|
+
const implsByType = new Map(); // 'PaymentService' → [{ file, isBean, isPrimary }]
|
|
512
|
+
for (const f of files) {
|
|
513
|
+
if (!JAVA_EXTS.has(path.extname(f).toLowerCase())) continue;
|
|
514
|
+
let decl;
|
|
515
|
+
try { decl = javaTypeDecl(maskJs(fs.readFileSync(f, 'utf8'))); } catch (_) { continue; }
|
|
516
|
+
if (!decl) continue;
|
|
517
|
+
for (const sup of decl.supers) {
|
|
518
|
+
if (!implsByType.has(sup)) implsByType.set(sup, []);
|
|
519
|
+
implsByType.get(sup).push({ file: f, isBean: decl.isBean, isPrimary: decl.isPrimary });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The single implementing file for a type, or null when it is ambiguous.
|
|
525
|
+
* One implementation resolves outright; several resolve only via @Primary.
|
|
526
|
+
* Anything still ambiguous yields no edge — polymorphism is not guessed.
|
|
527
|
+
*/
|
|
528
|
+
const soleImpl = (typeName) => {
|
|
529
|
+
const cands = implsByType.get(typeName) || [];
|
|
530
|
+
if (cands.length === 1) return cands[0].file;
|
|
531
|
+
const primary = cands.filter((c) => c.isPrimary);
|
|
532
|
+
if (primary.length === 1) return primary[0].file;
|
|
533
|
+
return null;
|
|
534
|
+
};
|
|
535
|
+
|
|
384
536
|
for (const f of files) {
|
|
385
537
|
normToAbs.set(normalizePath(path.resolve(f)), path.resolve(f));
|
|
386
538
|
let src;
|
|
@@ -400,12 +552,20 @@ function buildCallGraph(cwd, opts = {}) {
|
|
|
400
552
|
|
|
401
553
|
const forward = new Map();
|
|
402
554
|
const reverse = new Map();
|
|
403
|
-
|
|
555
|
+
// Additive: `forward`/`reverse` keep their existing shape, so every current
|
|
556
|
+
// consumer is unaffected. Confidence is looked up by "from\u0000to".
|
|
557
|
+
const edgeConfidence = new Map();
|
|
558
|
+
const addEdge = (from, to, confidence) => {
|
|
404
559
|
if (from === to) return;
|
|
405
560
|
if (!forward.has(from)) forward.set(from, new Set());
|
|
406
561
|
forward.get(from).add(to);
|
|
407
562
|
if (!reverse.has(to)) reverse.set(to, new Set());
|
|
408
563
|
reverse.get(to).add(from);
|
|
564
|
+
if (confidence) {
|
|
565
|
+
const k = from + '\u0000' + to;
|
|
566
|
+
// A 'high' resolution never loses to a later 'medium' one.
|
|
567
|
+
if (edgeConfidence.get(k) !== 'high') edgeConfidence.set(k, confidence);
|
|
568
|
+
}
|
|
409
569
|
};
|
|
410
570
|
|
|
411
571
|
for (const [f, fileDefs] of perFileDefs.entries()) {
|
|
@@ -423,16 +583,59 @@ function buildCallGraph(cwd, opts = {}) {
|
|
|
423
583
|
.sort();
|
|
424
584
|
importedAbs.push(...siblings);
|
|
425
585
|
}
|
|
586
|
+
// Receiver types come from declarations anywhere in the file: fields are
|
|
587
|
+
// declared outside any method body, locals inside one.
|
|
588
|
+
const typeMap = JAVA_EXTS.has(ext) ? buildTypeMap(masked) : null;
|
|
589
|
+
// Types reachable from this file, by name — imports first, then same-package
|
|
590
|
+
// siblings, so an import always wins over a coincidental sibling name.
|
|
591
|
+
const scopeByTypeName = new Map();
|
|
592
|
+
if (typeMap) {
|
|
593
|
+
for (const imp of importedAbs) {
|
|
594
|
+
const base = path.basename(imp, path.extname(imp));
|
|
595
|
+
if (!scopeByTypeName.has(base)) scopeByTypeName.set(base, imp);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
426
599
|
for (const d of fileDefs) {
|
|
427
600
|
const callerId = symId(cwd, f, d.name);
|
|
428
601
|
if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
|
|
429
602
|
const callees = callsInRange(masked, d.bodyStart, d.bodyEnd);
|
|
430
603
|
for (const nm of callees) {
|
|
431
604
|
const local = (defsByName.get(f) || new Map()).get(nm);
|
|
432
|
-
if (local && local.length) { for (const id of local) addEdge(callerId, id); continue; }
|
|
605
|
+
if (local && local.length) { for (const id of local) addEdge(callerId, id, 'high'); continue; }
|
|
433
606
|
for (const imp of importedAbs) {
|
|
434
607
|
const ids = (defsByName.get(imp) || new Map()).get(nm);
|
|
435
|
-
if (ids && ids.length) { for (const id of ids) addEdge(callerId, id); break; }
|
|
608
|
+
if (ids && ids.length) { for (const id of ids) addEdge(callerId, id, 'high'); break; }
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// `receiver.method(` — resolve the receiver's declared type to a file.
|
|
613
|
+
if (!typeMap) continue;
|
|
614
|
+
for (const { receiver, method } of receiverCallsInRange(masked, d.bodyStart, d.bodyEnd)) {
|
|
615
|
+
// A receiver that is itself a type name is a static call: `Foo.bar()`.
|
|
616
|
+
const typeName = typeMap.get(receiver)
|
|
617
|
+
|| (fileByTypeName.has(receiver) ? receiver : null);
|
|
618
|
+
if (!typeName) continue; // unknown receiver → no edge, never a guess
|
|
619
|
+
|
|
620
|
+
let target = scopeByTypeName.get(typeName);
|
|
621
|
+
let confidence = 'high'; // typed receiver, resolved in scope
|
|
622
|
+
if (!target) {
|
|
623
|
+
const candidates = fileByTypeName.get(typeName) || [];
|
|
624
|
+
if (candidates.length !== 1) continue; // ambiguous or absent → no edge
|
|
625
|
+
target = candidates[0];
|
|
626
|
+
confidence = 'medium'; // type known, but not in this file's scope
|
|
627
|
+
}
|
|
628
|
+
const ids = (defsByName.get(target) || new Map()).get(method);
|
|
629
|
+
if (ids && ids.length) for (const id of ids) addEdge(callerId, id, confidence);
|
|
630
|
+
|
|
631
|
+
// Spring: the call names the interface, but the code that runs — and
|
|
632
|
+
// that a reviewer changes — lives in the implementation. Both edges are
|
|
633
|
+
// true, so both are recorded; without the second, blast radius on an
|
|
634
|
+
// implementation is empty.
|
|
635
|
+
const implFile = soleImpl(typeName);
|
|
636
|
+
if (implFile && implFile !== target) {
|
|
637
|
+
const implIds = (defsByName.get(implFile) || new Map()).get(method);
|
|
638
|
+
if (implIds && implIds.length) for (const id of implIds) addEdge(callerId, id, confidence);
|
|
436
639
|
}
|
|
437
640
|
}
|
|
438
641
|
}
|
|
@@ -443,7 +646,7 @@ function buildCallGraph(cwd, opts = {}) {
|
|
|
443
646
|
for (const [k, set] of mapOfSets.entries()) out.set(k, [...set]);
|
|
444
647
|
return out;
|
|
445
648
|
};
|
|
446
|
-
return { forward: toArr(forward), reverse: toArr(reverse), defs };
|
|
649
|
+
return { forward: toArr(forward), reverse: toArr(reverse), defs, edgeConfidence };
|
|
447
650
|
}
|
|
448
651
|
|
|
449
652
|
/**
|
|
@@ -565,7 +768,7 @@ function formatCallGraphJSON(result, kind) {
|
|
|
565
768
|
}
|
|
566
769
|
|
|
567
770
|
module.exports = {
|
|
568
|
-
buildCallGraph, buildCallFileGraph, methodImpact, methodCallees,
|
|
771
|
+
buildCallGraph, buildTypeMap, receiverCallsInRange, javaTypeDecl, DEFAULT_WALK_DEPTH, buildCallFileGraph, methodImpact, methodCallees,
|
|
569
772
|
formatCallGraph, formatCallGraphJSON,
|
|
570
773
|
extractDefs, maskJs, maskPy, maskRust,
|
|
571
774
|
};
|
package/src/mcp/server.js
CHANGED
package/src/skills/skills.js
CHANGED
|
@@ -27,7 +27,7 @@ const SKILLS = {
|
|
|
27
27
|
'Follow this loop before any file exploration in a repo with SigMap installed.',
|
|
28
28
|
'',
|
|
29
29
|
'1. **Ask before reading.** `sigmap ask "<task>"` (or the `query_context` MCP tool) ranks the relevant files as ~hundreds of tokens of signatures instead of thousands of raw-file tokens. Never open files to "look around".',
|
|
30
|
-
'2. **Read ranges, not files.** Use the `get_lines` MCP tool with the `:start-end` line anchors carried on every signature to pull only the lines you need.',
|
|
30
|
+
'2. **Read ranges, not files.** Use the `get_lines` MCP tool — or `sigmap lines <file> :<line> --context <n>` where MCP is unavailable — with the `:start-end` line anchors carried on every signature to pull only the lines you need.',
|
|
31
31
|
'3. **Ground before trusting.** Run the `verify_suggestion` MCP tool (or `sigmap verify-ai-output`) on generated code before applying it — it flags fabricated files, imports, symbols, and npm scripts against the live index.',
|
|
32
32
|
'4. **Squeeze big pastes.** Any stack trace, CI/build log, or JSON blob goes through `sigmap squeeze` (or the `squeeze_output` MCP tool) before it enters context — the signal survives, the noise does not.',
|
|
33
33
|
'5. **Checkpoint progress.** Use the `create_checkpoint` MCP tool or `sigmap note "<decision>"` so a follow-up session resumes without re-deriving state.',
|
|
@@ -45,7 +45,7 @@ const SKILLS = {
|
|
|
45
45
|
'',
|
|
46
46
|
'1. **Look up, do not search.** `npx sigmap ask "<the task>"` — this writes `.context/query-context.md`.',
|
|
47
47
|
'2. **Read the map.** `cat .context/query-context.md`. It ranks the relevant files and lists their signatures with `:start-end` line anchors — a few hundred tokens where the same files read whole are tens of thousands. Say which files it surfaced before continuing. If nothing relevant appears, re-run step 1 with different wording; fall back to search only after two attempts, and say so.',
|
|
48
|
-
'3. **
|
|
48
|
+
'3. **Read the anchored range, by command.** A signature ending `:425-425` means line 425, not the 547-line file. Run `npx sigmap lines <file> :425 --context 10` — paste the anchor straight off the signature. Never `cat` a whole file when you hold an anchor for it: on a real repo a 220-line span costs ~2,700 tokens where the anchored window costs ~220.',
|
|
49
49
|
'4. **Make the change.** Follow the conventions visible in the signatures — same layering, same response wrapper, same annotation style. Add no dependencies.',
|
|
50
50
|
'5. **Verify before reporting.** Write what you changed to `.sigmap-notes.md`, naming every file by its **full repository-relative path** (a bare filename is reported as fake), then run `npx sigmap verify-ai-output .sigmap-notes.md`. It checks every name against the real index, offline, with no model call. Fix anything it flags and re-run before you reply.',
|
|
51
51
|
'6. **Refresh the map.** `npx sigmap` — your edits made it stale.',
|