sigmap 8.29.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/gen-context.js CHANGED
@@ -6424,6 +6424,19 @@ __factories["./src/extractors/html"] = function(module, exports) {
6424
6424
  __factories["./src/extractors/java"] = function(module, exports) {
6425
6425
 
6426
6426
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
6427
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
6428
+
6429
+ // Class bodies are scanned to this many characters. Generated JVM sources
6430
+ // (MyBatis/JPA entities) routinely run past 10KB, so the ceiling only guards
6431
+ // against pathological input rather than trimming ordinary classes.
6432
+ const MAX_CLASS_BODY_CHARS = 200000;
6433
+
6434
+ // Per-class member ceiling. Sits above the default `maxSigsPerFile` so the
6435
+ // caller's configured budget governs the output rather than this file.
6436
+ const MAX_MEMBERS_PER_CLASS = 120;
6437
+
6438
+ // Per-file signature ceiling, likewise above the configured default.
6439
+ const MAX_SIGS_PER_FILE = 200;
6427
6440
 
6428
6441
  /**
6429
6442
  * Extract signatures from Java source code.
@@ -6451,17 +6464,20 @@ __factories["./src/extractors/java"] = function(module, exports) {
6451
6464
  const block = extractBlock(stripped, bodyStart);
6452
6465
  sigs.push(hinted(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[2]));
6453
6466
  for (const meth of extractMembers(block)) {
6454
- sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)), meth.name));
6467
+ // The disclosure marker carries no offsets; anchor it at the class body.
6468
+ const declIdx = meth.declIdx || 0;
6469
+ const endIdx = meth.endIdx || 0;
6470
+ sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + declIdx), lineAt(stripped, bodyStart + endIdx)), meth.name));
6455
6471
  }
6456
6472
  }
6457
6473
 
6458
- return sigs.slice(0, 25);
6474
+ return capWithNotice(sigs, MAX_SIGS_PER_FILE, 'signatures');
6459
6475
  }
6460
6476
 
6461
6477
  function extractBlock(src, startIndex) {
6462
6478
  let depth = 1;
6463
6479
  let i = startIndex;
6464
- const end = Math.min(src.length, startIndex + 5000);
6480
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
6465
6481
  while (i < end && depth > 0) {
6466
6482
  if (src[i] === '{') depth++;
6467
6483
  else if (src[i] === '}') depth--;
@@ -6483,7 +6499,7 @@ __factories["./src/extractors/java"] = function(module, exports) {
6483
6499
  endIdx: m.index + m[0].length,
6484
6500
  });
6485
6501
  }
6486
- return members.slice(0, 8);
6502
+ return capMembersWithNotice(members, MAX_MEMBERS_PER_CLASS);
6487
6503
  }
6488
6504
 
6489
6505
  function normalizeParams(params) {
@@ -11736,24 +11752,60 @@ __factories["./src/graph/builder"] = function(module, exports) {
11736
11752
  return { forward, reverse };
11737
11753
  }
11738
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
+
11739
11782
  /**
11740
11783
  * Build a dependency graph scoped to a single cwd by walking all JS/TS/Py/Go
11741
11784
  * files under srcDirs. Useful for the MCP tool handler.
11742
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
+ *
11743
11790
  * @param {string} cwd
11744
11791
  * @param {object} [opts]
11745
11792
  * @param {string[]} [opts.srcDirs]
11746
11793
  * @param {string[]} [opts.exclude]
11794
+ * @param {number} [opts.maxDepth] - walk depth from each srcDir root
11747
11795
  * @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
11748
11796
  */
11749
11797
  function buildFromCwd(cwd, opts) {
11750
11798
  // R-package layouts use `R/` and `inst/`; Shiny apps put helpers in `R/`.
11751
11799
  // The existence check below makes these no-ops in non-R projects.
11752
- const { srcDirs = ['src', 'app', 'lib', 'R', 'inst'], exclude = ['node_modules', '.git', 'dist', 'build'] } = opts || {};
11800
+ const {
11801
+ srcDirs = _configuredSrcDirs(cwd) || DEFAULT_SRC_DIRS,
11802
+ exclude = ['node_modules', '.git', 'dist', 'build'],
11803
+ maxDepth = DEFAULT_WALK_DEPTH,
11804
+ } = opts || {};
11753
11805
  const excludeSet = new Set(exclude);
11754
11806
 
11755
11807
  function walkDir(dir, depth) {
11756
- if (depth > 8) return [];
11808
+ if (depth > maxDepth) return [];
11757
11809
  let entries;
11758
11810
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return []; }
11759
11811
  const out = [];
@@ -11802,7 +11854,7 @@ __factories["./src/graph/builder"] = function(module, exports) {
11802
11854
  return build(files, cwd, ctx);
11803
11855
  }
11804
11856
 
11805
- module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias };
11857
+ module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias, _configuredSrcDirs, DEFAULT_SRC_DIRS, DEFAULT_WALK_DEPTH };
11806
11858
 
11807
11859
  };
11808
11860
 
@@ -12057,6 +12109,44 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12057
12109
  // Java: methods + constructors with braced bodies. Statement-shaped matches
12058
12110
  // (calls, control flow) are rejected because their `)` is followed by `;`,
12059
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
+
12060
12150
  function javaDefs(masked) {
12061
12151
  const defs = [];
12062
12152
  const seen = new Set();
@@ -12073,11 +12163,24 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12073
12163
  // skip `throws A, B` up to the body `{` (same line — multi-line headers are skipped)
12074
12164
  let k = close + 1;
12075
12165
  while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n' && masked[k] !== '=') k++;
12076
- if (masked[k] !== '{') continue;
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;
12077
12178
  const key = name + ':' + k;
12078
12179
  if (seen.has(key)) continue;
12079
12180
  seen.add(key);
12080
- defs.push({ name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
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, '{', '}') });
12081
12184
  }
12082
12185
  return defs;
12083
12186
  }
@@ -12131,7 +12234,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12131
12234
  const re = /([A-Za-z_$][\w$]*)\s*\(/g;
12132
12235
  let m;
12133
12236
  while ((m = re.exec(slice)) !== null) {
12134
- // skip a `.name(` method access (can't resolve the receiver deterministically)
12237
+ // skip a `.name(` method access resolved separately via receiverCallsInRange
12135
12238
  const before = slice[m.index - 1];
12136
12239
  if (before === '.') continue;
12137
12240
  if (!NON_CALL.has(m[1])) names.add(m[1]);
@@ -12139,16 +12242,75 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12139
12242
  return names;
12140
12243
  }
12141
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
+
12142
12286
  // ── Public API ───────────────────────────────────────────────────────────────
12143
12287
 
12144
- function _walk(dir, excludeSet, out, depth) {
12145
- if (depth > 8) return;
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;
12146
12308
  let entries;
12147
12309
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
12148
12310
  for (const e of entries) {
12149
12311
  if (excludeSet.has(e.name) || e.name.startsWith('.')) continue;
12150
12312
  const full = path.join(dir, e.name);
12151
- if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
12313
+ if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
12152
12314
  else if (e.isFile()) {
12153
12315
  const ext = path.extname(e.name).toLowerCase();
12154
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);
@@ -12174,9 +12336,13 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12174
12336
  const excludeSet = new Set(opts.exclude || ['node_modules', '.git', 'dist', 'build', 'coverage', 'vendor']);
12175
12337
  let files = opts.files ? opts.files.map((f) => path.resolve(f)) : [];
12176
12338
  if (!opts.files) {
12177
- for (const sd of (opts.srcDirs || ['src', 'app', 'lib'])) {
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) {
12178
12344
  const abs = path.resolve(cwd, sd);
12179
- if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0);
12345
+ if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0, opts.maxDepth);
12180
12346
  }
12181
12347
  }
12182
12348
 
@@ -12190,6 +12356,44 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12190
12356
  const normToAbs = new Map(); // normalized abs → abs
12191
12357
  const defs = new Map(); // symbolId → {file,name,line}
12192
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
+
12193
12397
  for (const f of files) {
12194
12398
  normToAbs.set(normalizePath(path.resolve(f)), path.resolve(f));
12195
12399
  let src;
@@ -12209,12 +12413,20 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12209
12413
 
12210
12414
  const forward = new Map();
12211
12415
  const reverse = new Map();
12212
- const addEdge = (from, to) => {
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) => {
12213
12420
  if (from === to) return;
12214
12421
  if (!forward.has(from)) forward.set(from, new Set());
12215
12422
  forward.get(from).add(to);
12216
12423
  if (!reverse.has(to)) reverse.set(to, new Set());
12217
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
+ }
12218
12430
  };
12219
12431
 
12220
12432
  for (const [f, fileDefs] of perFileDefs.entries()) {
@@ -12232,16 +12444,59 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12232
12444
  .sort();
12233
12445
  importedAbs.push(...siblings);
12234
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
+
12235
12460
  for (const d of fileDefs) {
12236
12461
  const callerId = symId(cwd, f, d.name);
12237
12462
  if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
12238
12463
  const callees = callsInRange(masked, d.bodyStart, d.bodyEnd);
12239
12464
  for (const nm of callees) {
12240
12465
  const local = (defsByName.get(f) || new Map()).get(nm);
12241
- 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; }
12242
12467
  for (const imp of importedAbs) {
12243
12468
  const ids = (defsByName.get(imp) || new Map()).get(nm);
12244
- 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);
12245
12500
  }
12246
12501
  }
12247
12502
  }
@@ -12252,7 +12507,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12252
12507
  for (const [k, set] of mapOfSets.entries()) out.set(k, [...set]);
12253
12508
  return out;
12254
12509
  };
12255
- return { forward: toArr(forward), reverse: toArr(reverse), defs };
12510
+ return { forward: toArr(forward), reverse: toArr(reverse), defs, edgeConfidence };
12256
12511
  }
12257
12512
 
12258
12513
  /**
@@ -12374,7 +12629,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12374
12629
  }
12375
12630
 
12376
12631
  module.exports = {
12377
- buildCallGraph, buildCallFileGraph, methodImpact, methodCallees,
12632
+ buildCallGraph, buildTypeMap, receiverCallsInRange, javaTypeDecl, DEFAULT_WALK_DEPTH, buildCallFileGraph, methodImpact, methodCallees,
12378
12633
  formatCallGraph, formatCallGraphJSON,
12379
12634
  extractDefs, maskJs, maskPy, maskRust,
12380
12635
  };
@@ -15250,6 +15505,7 @@ __factories["./src/mcp/install"] = function(module, exports) {
15250
15505
 
15251
15506
  // Config shapes the supported clients use.
15252
15507
  // - 'json' → { mcpServers: { sigmap: { command, args } } }
15508
+ // - 'vscode'→ { servers: { sigmap: { type: 'stdio', command, args } } }
15253
15509
  // - 'zed' → { context_servers: { sigmap: { command: { path, args } } } }
15254
15510
  // - 'yaml' → Codex CLI ~/.codex/config.yaml (mcpServers block, appended)
15255
15511
  const CLIENTS = {
@@ -15258,7 +15514,7 @@ __factories["./src/mcp/install"] = function(module, exports) {
15258
15514
  windsurf: { label: 'Windsurf', format: 'json', scope: 'both',
15259
15515
  project: ['.windsurf', 'mcp.json'],
15260
15516
  global: ['.codeium', 'windsurf', 'mcp_config.json'] },
15261
- vscode: { label: 'VS Code', format: 'json', scope: 'project', project: ['.vscode', 'mcp.json'] },
15517
+ vscode: { label: 'VS Code', format: 'vscode', scope: 'project', project: ['.vscode', 'mcp.json'] },
15262
15518
  opencode: { label: 'OpenCode', format: 'json', scope: 'both',
15263
15519
  project: ['opencode.json'],
15264
15520
  global: ['.config', 'opencode', 'config.json'] },
@@ -15313,6 +15569,31 @@ __factories["./src/mcp/install"] = function(module, exports) {
15313
15569
  return 'installed';
15314
15570
  }
15315
15571
 
15572
+ /**
15573
+ * Install into VS Code's `.vscode/mcp.json`, which keys servers under `servers`
15574
+ * (not `mcpServers`) and expects an explicit transport `type`. A config written
15575
+ * by an older SigMap under `mcpServers` is migrated rather than left in place,
15576
+ * so re-running repairs it instead of leaving two entries VS Code cannot read.
15577
+ */
15578
+ function _installVscode(filePath, scriptPath) {
15579
+ let settings = {};
15580
+ if (fs.existsSync(filePath)) {
15581
+ try { settings = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {}; }
15582
+ catch (_) { settings = {}; }
15583
+ }
15584
+ const stale = settings.mcpServers && settings.mcpServers.sigmap;
15585
+ if (stale) {
15586
+ delete settings.mcpServers.sigmap;
15587
+ if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
15588
+ }
15589
+ if (!settings.servers) settings.servers = {};
15590
+ if (settings.servers.sigmap && !stale) return 'already';
15591
+ settings.servers.sigmap = { type: 'stdio', command: 'node', args: serverArgs(scriptPath) };
15592
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15593
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
15594
+ return stale ? 'updated' : 'installed';
15595
+ }
15596
+
15316
15597
  /** Install into Zed's `context_servers` config (create file/dir if absent). */
15317
15598
  function _installZed(filePath, scriptPath) {
15318
15599
  let settings = {};
@@ -15365,7 +15646,8 @@ __factories["./src/mcp/install"] = function(module, exports) {
15365
15646
  const filePath = resolveTarget(spec, cwd, home, opts.global);
15366
15647
 
15367
15648
  let status;
15368
- if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
15649
+ if (spec.format === 'vscode') status = _installVscode(filePath, scriptPath);
15650
+ else if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
15369
15651
  else if (spec.format === 'yaml') status = _installYaml(filePath, scriptPath);
15370
15652
  else status = _installJson(filePath, scriptPath);
15371
15653
 
@@ -15397,7 +15679,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
15397
15679
 
15398
15680
  const SERVER_INFO = {
15399
15681
  name: 'sigmap',
15400
- version: '8.29.0',
15682
+ version: '8.31.0',
15401
15683
  description: 'SigMap MCP server — code signatures on demand',
15402
15684
  };
15403
15685
 
@@ -16760,26 +17042,52 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
16760
17042
  generatedCode: 0.3, // dist/build/.next in path
16761
17043
  docsFile: 0.2, // docs/doc/README in path
16762
17044
  nodeModules: 0.0, // node_modules (zero score)
17045
+ dataHolder: 0.3, // generated POJO/entity: almost entirely accessors
16763
17046
  };
16764
17047
 
17048
+ // A file whose members are overwhelmingly trivial accessors is a data holder,
17049
+ // not logic. Path-based detection cannot see these: generated JPA/MyBatis
17050
+ // entities live in ordinary source trees. They match a query on any column
17051
+ // name they happen to carry (`getNote`/`setNote` matches "note" as strongly as
17052
+ // the service that actually implements order notes), so on an entity-heavy
17053
+ // repo they crowd real code out of the top results.
17054
+ const ACCESSOR_RE = /^\s*(get|set|is)[A-Z]\w*\s*\(/;
17055
+ const DATA_HOLDER_RATIO = 0.8;
17056
+ const DATA_HOLDER_MIN_MEMBERS = 6;
17057
+
16765
17058
  // Query terms that mean the penalised category IS the target. Read from the
16766
17059
  // query tokens directly, NOT via detectIntent: that classifier is first-match-
16767
17060
  // wins over its pattern object, and `debug` precedes `test`, so "fix the failing
16768
17061
  // test" classifies as debug and never reaches the test branch.
16769
17062
  const WANTS_TESTS = new Set(['test', 'tests', 'spec', 'specs', 'unit', 'integration', 'e2e', 'assertion', 'assert', 'mock', 'fixture', 'coverage', 'testing']);
16770
17063
  const WANTS_DOCS = new Set(['doc', 'docs', 'documentation', 'readme', 'changelog', 'guide', 'tutorial']);
17064
+ const WANTS_MODELS = new Set(['entity', 'entities', 'model', 'models', 'pojo', 'dto', 'bean', 'getter', 'getters', 'setter', 'setters', 'accessor', 'accessors', 'field', 'fields', 'column', 'columns', 'schema']);
16771
17065
 
16772
17066
  /** Which penalised categories the query is explicitly asking for. */
16773
17067
  function _queryWants(queryTokens) {
16774
- const wants = { tests: false, docs: false };
17068
+ const wants = { tests: false, docs: false, models: false };
16775
17069
  for (const t of queryTokens || []) {
16776
17070
  if (WANTS_TESTS.has(t)) wants.tests = true;
16777
17071
  if (WANTS_DOCS.has(t)) wants.docs = true;
17072
+ if (WANTS_MODELS.has(t)) wants.models = true;
16778
17073
  }
16779
17074
  return wants;
16780
17075
  }
16781
17076
 
16782
- function _computePenalty(filePath, wants) {
17077
+ /**
17078
+ * True when a file's members are overwhelmingly trivial accessors — a generated
17079
+ * entity or POJO rather than logic. Type declarations are excluded from the
17080
+ * ratio so a small class is not misjudged by its own `class X` line.
17081
+ */
17082
+ function _isDataHolder(sigs) {
17083
+ if (!Array.isArray(sigs)) return false;
17084
+ const members = sigs.filter((line) => /^\s/.test(line) || !/^(class|interface|enum|struct|function|module\.exports)\b/.test(line));
17085
+ if (members.length < DATA_HOLDER_MIN_MEMBERS) return false;
17086
+ const accessors = members.filter((line) => ACCESSOR_RE.test(line)).length;
17087
+ return accessors / members.length >= DATA_HOLDER_RATIO;
17088
+ }
17089
+
17090
+ function _computePenalty(filePath, wants, sigs) {
16783
17091
  const pathLower = filePath.toLowerCase();
16784
17092
  if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
16785
17093
  // A penalty must never fire on the very thing the user asked for. Before
@@ -16792,6 +17100,11 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
16792
17100
  if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) {
16793
17101
  return (wants && wants.docs) ? 1.0 : PENALTY_SIGNALS.docsFile;
16794
17102
  }
17103
+ // Content-based, and last: a data holder is still a real source file, so it
17104
+ // is only demoted once the path-based categories have had their say.
17105
+ if (_isDataHolder(sigs)) {
17106
+ return (wants && wants.models) ? 1.0 : PENALTY_SIGNALS.dataHolder;
17107
+ }
16795
17108
  return 1.0;
16796
17109
  }
16797
17110
 
@@ -16849,7 +17162,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
16849
17162
  if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
16850
17163
 
16851
17164
  const w = weights || DEFAULT_WEIGHTS;
16852
- const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants) };
17165
+ const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants, sigs) };
16853
17166
 
16854
17167
  // Module-doc prose is excluded here on purpose. This signal measures overlap
16855
17168
  // with DECLARED IDENTIFIERS; prose relevance is BM25's job, where it is scored
@@ -17445,7 +17758,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
17445
17758
  return detectIntents(query)[0];
17446
17759
  }
17447
17760
 
17448
- module.exports = { rank, buildSigIndex, scoreFile, _queryWants, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
17761
+ module.exports = { rank, buildSigIndex, scoreFile, _queryWants, _isDataHolder, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
17449
17762
 
17450
17763
  };
17451
17764
 
@@ -18731,13 +19044,31 @@ __factories["./src/skills/skills"] = function(module, exports) {
18731
19044
  'Follow this loop before any file exploration in a repo with SigMap installed.',
18732
19045
  '',
18733
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".',
18734
- '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.',
18735
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.',
18736
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.',
18737
19050
  '5. **Checkpoint progress.** Use the `create_checkpoint` MCP tool or `sigmap note "<decision>"` so a follow-up session resumes without re-deriving state.',
18738
19051
  '6. **Watch the budget.** Check the `get_budget` MCP tool or `sigmap budget` (estimates from SigMap\'s local ledger — no LLM calls). Near the budget: summarize-then-drop older context instead of accumulating, and prefer terse output.',
18739
19052
  ].join('\n'),
18740
19053
  },
19054
+ 'sigmap-task': {
19055
+ title: 'SigMap task loop',
19056
+ kind: 'prompt',
19057
+ description: 'Do a coding task grounded in SigMap: look up before reading, edit by line anchor, verify before reporting.',
19058
+ argumentHint: 'the change you want, in plain words',
19059
+ body: [
19060
+ 'Work through these steps **in order**. Do not open any file before step 2.',
19061
+ 'Every command runs from the integrated terminal — do not ask the user to run them for you.',
19062
+ '',
19063
+ '1. **Look up, do not search.** `npx sigmap ask "<the task>"` — this writes `.context/query-context.md`.',
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.',
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.',
19066
+ '4. **Make the change.** Follow the conventions visible in the signatures — same layering, same response wrapper, same annotation style. Add no dependencies.',
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.',
19068
+ '6. **Refresh the map.** `npx sigmap` — your edits made it stale.',
19069
+ '7. **Report.** The files you changed, the ranges you actually read, the step-1 token count, and the step-5 verify result. Say so if you fell back to searching or if verify flagged something.',
19070
+ ].join('\n'),
19071
+ },
18741
19072
  'sigmap-config-optimizer': {
18742
19073
  title: 'SigMap config optimizer',
18743
19074
  description: 'Playbook for getting a correct SigMap config on any repo: detect with sigmap tune, review the per-change reasons, apply, validate.',
@@ -18762,7 +19093,9 @@ __factories["./src/skills/skills"] = function(module, exports) {
18762
19093
  windsurf: { label: 'Windsurf', parent: ['.windsurf'],
18763
19094
  target: (cwd, skill) => path.join(cwd, '.windsurf', 'rules', `${skill}.md`) },
18764
19095
  copilot: { label: 'GitHub Copilot', parent: ['.github'],
18765
- target: (cwd, skill) => path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`) },
19096
+ target: (cwd, skill) => (SKILLS[skill] && SKILLS[skill].kind === 'prompt'
19097
+ ? path.join(cwd, '.github', 'prompts', `${skill}.prompt.md`)
19098
+ : path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`)) },
18766
19099
  codex: { label: 'Codex CLI (AGENTS.md)', parent: ['AGENTS.md'],
18767
19100
  target: (cwd) => path.join(cwd, 'AGENTS.md'), inject: true },
18768
19101
  };
@@ -18783,6 +19116,10 @@ __factories["./src/skills/skills"] = function(module, exports) {
18783
19116
  return `---\ndescription: ${skill.description}\nalwaysApply: false\n---\n\n${body}`;
18784
19117
  }
18785
19118
  if (client === 'copilot') {
19119
+ if (skill.kind === 'prompt') {
19120
+ return `---\nname: ${skillName}\nagent: 'agent'\ndescription: ${skill.description}\n`
19121
+ + `argument-hint: ${skill.argumentHint}\n---\n\n${body}`;
19122
+ }
18786
19123
  return `---\napplyTo: "**"\n---\n\n${body}`;
18787
19124
  }
18788
19125
  return body; // windsurf: plain markdown
@@ -21636,7 +21973,7 @@ function __tryGit(args, opts = {}) {
21636
21973
  catch (_) { return ''; }
21637
21974
  }
21638
21975
 
21639
- const VERSION = '8.29.0';
21976
+ const VERSION = '8.31.0';
21640
21977
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
21641
21978
 
21642
21979
  function requireSourceOrBundled(key) {
@@ -23642,6 +23979,8 @@ Usage:
23642
23979
  ${cmd} tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
23643
23980
  ${cmd} skills list List skill clients (Claude/Cursor/Windsurf/Copilot/AGENTS.md) and install state (--json)
23644
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)
23645
23984
  ${cmd} note "<text>" Append a note to the cross-session decision log
23646
23985
  ${cmd} note List recent notes (also: note --list <N>)
23647
23986
  ${cmd} status Show repo state — branch, dirty files, index freshness, notes
@@ -25245,6 +25584,55 @@ function main() {
25245
25584
  process.exit(0);
25246
25585
  }
25247
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
+
25248
25636
  if (args[0] === 'note') {
25249
25637
  const jsonOut = args.includes('--json');
25250
25638
  const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');