sigmap 8.30.0 → 8.32.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
@@ -5439,6 +5439,14 @@ __factories["./src/extractors/coverage"] = function(module, exports) {
5439
5439
  // ── ./src/extractors/cpp ──
5440
5440
  __factories["./src/extractors/cpp"] = function(module, exports) {
5441
5441
 
5442
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
5443
+
5444
+ // Ceilings sit above the default `maxSigsPerFile` so the configured budget
5445
+ // governs output rather than a literal buried here, and omissions are disclosed
5446
+ // — an undisclosed cap looks like a class that simply has eight methods (#576).
5447
+ const MEMBER_LIMIT = 8;
5448
+ const PER_FILE_LIMIT = 25;
5449
+
5442
5450
  /**
5443
5451
  * Extract signatures from C/C++ source code.
5444
5452
  * @param {string} src - Raw file content
@@ -5469,7 +5477,7 @@ __factories["./src/extractors/cpp"] = function(module, exports) {
5469
5477
  sigs.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
5470
5478
  }
5471
5479
 
5472
- return sigs.slice(0, 25);
5480
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
5473
5481
  }
5474
5482
 
5475
5483
  function extractBlock(src, startIndex) {
@@ -5492,7 +5500,7 @@ __factories["./src/extractors/cpp"] = function(module, exports) {
5492
5500
  const retStr = ret ? ` → ${ret}` : '';
5493
5501
  members.push(`${m[2]}(${normalizeParams(m[3])})${retStr}`);
5494
5502
  }
5495
- return members.slice(0, 8);
5503
+ return capWithNotice(members, MEMBER_LIMIT, 'members');
5496
5504
  }
5497
5505
 
5498
5506
  function normalizeParams(params) {
@@ -5513,6 +5521,13 @@ __factories["./src/extractors/cpp"] = function(module, exports) {
5513
5521
  __factories["./src/extractors/csharp"] = function(module, exports) {
5514
5522
 
5515
5523
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
5524
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
5525
+
5526
+ // Ceilings sit above the default `maxSigsPerFile` so the configured budget
5527
+ // governs output rather than a literal buried here, and omissions are disclosed
5528
+ // — an undisclosed cap looks like a class that simply has eight methods (#576).
5529
+ const MEMBER_LIMIT = 8;
5530
+ const PER_FILE_LIMIT = 25;
5516
5531
 
5517
5532
  /**
5518
5533
  * Extract signatures from C# source code.
@@ -5537,11 +5552,12 @@ __factories["./src/extractors/csharp"] = function(module, exports) {
5537
5552
  const block = extractBlock(stripped, bodyStart);
5538
5553
  sigs.push(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, declIdx), lineAt(stripped, bodyStart + block.length)));
5539
5554
  for (const meth of extractMembers(block)) {
5540
- sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
5555
+ // The disclosure marker carries no offsets; anchor it at the class body.
5556
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + (meth.declIdx || 0)), lineAt(stripped, bodyStart + (meth.endIdx || 0))));
5541
5557
  }
5542
5558
  }
5543
5559
 
5544
- return sigs.slice(0, 25);
5560
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
5545
5561
  }
5546
5562
 
5547
5563
  function extractBlock(src, startIndex) {
@@ -5567,7 +5583,7 @@ __factories["./src/extractors/csharp"] = function(module, exports) {
5567
5583
  endIdx: m.index + m[0].length,
5568
5584
  });
5569
5585
  }
5570
- return members.slice(0, 8);
5586
+ return capMembersWithNotice(members, MEMBER_LIMIT);
5571
5587
  }
5572
5588
 
5573
5589
  function normalizeParams(params) {
@@ -5587,6 +5603,12 @@ __factories["./src/extractors/csharp"] = function(module, exports) {
5587
5603
  // ── ./src/extractors/css ──
5588
5604
  __factories["./src/extractors/css"] = function(module, exports) {
5589
5605
 
5606
+ const { capWithNotice } = __require('./src/util/truncate');
5607
+
5608
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
5609
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
5610
+ const PER_FILE_LIMIT = 25;
5611
+
5590
5612
  /**
5591
5613
  * Extract signatures from CSS/SCSS/SASS/Less source code.
5592
5614
  * @param {string} src - Raw file content
@@ -5650,7 +5672,7 @@ __factories["./src/extractors/css"] = function(module, exports) {
5650
5672
  for (const name of selected) sigs.push(`.${name}`);
5651
5673
  }
5652
5674
 
5653
- return sigs.slice(0, 25);
5675
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
5654
5676
  }
5655
5677
 
5656
5678
  module.exports = { extract };
@@ -5661,6 +5683,13 @@ __factories["./src/extractors/css"] = function(module, exports) {
5661
5683
  __factories["./src/extractors/dart"] = function(module, exports) {
5662
5684
 
5663
5685
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
5686
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
5687
+
5688
+ // Ceilings sit above the default `maxSigsPerFile` so the configured budget
5689
+ // governs output rather than a literal buried here, and omissions are disclosed
5690
+ // — an undisclosed cap looks like a class that simply has eight methods (#576).
5691
+ const MEMBER_LIMIT = 8;
5692
+ const PER_FILE_LIMIT = 25;
5664
5693
 
5665
5694
  /**
5666
5695
  * Extract signatures from Dart source code.
@@ -5696,7 +5725,8 @@ __factories["./src/extractors/dart"] = function(module, exports) {
5696
5725
  const block = extractBlock(stripped, bodyStart);
5697
5726
  sigs.push(withAnchor(`${abs}class ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
5698
5727
  for (const meth of extractMembers(block)) {
5699
- sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
5728
+ // The disclosure marker carries no offsets; anchor it at the class body.
5729
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + (meth.declIdx || 0)), lineAt(stripped, bodyStart + (meth.endIdx || 0))));
5700
5730
  }
5701
5731
  }
5702
5732
 
@@ -5708,7 +5738,7 @@ __factories["./src/extractors/dart"] = function(module, exports) {
5708
5738
  sigs.push(withAnchor(`${m[2]}(${normalizeParams(m[3])})${retStr}`, s, e));
5709
5739
  }
5710
5740
 
5711
- return sigs.slice(0, 25);
5741
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
5712
5742
  }
5713
5743
 
5714
5744
  function extractBlock(src, startIndex) {
@@ -5733,7 +5763,7 @@ __factories["./src/extractors/dart"] = function(module, exports) {
5733
5763
  endIdx: m.index + m[0].length,
5734
5764
  });
5735
5765
  }
5736
- return members.slice(0, 8);
5766
+ return capMembersWithNotice(members, MEMBER_LIMIT);
5737
5767
  }
5738
5768
 
5739
5769
  function normalizeParams(params) {
@@ -5978,6 +6008,12 @@ __factories["./src/extractors/dispatch"] = function(module, exports) {
5978
6008
  // ── ./src/extractors/dockerfile ──
5979
6009
  __factories["./src/extractors/dockerfile"] = function(module, exports) {
5980
6010
 
6011
+ const { capWithNotice } = __require('./src/util/truncate');
6012
+
6013
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
6014
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
6015
+ const PER_FILE_LIMIT = 25;
6016
+
5981
6017
  /**
5982
6018
  * Extract signatures from Dockerfiles.
5983
6019
  * @param {string} src - Raw file content
@@ -6021,7 +6057,7 @@ __factories["./src/extractors/dockerfile"] = function(module, exports) {
6021
6057
  if (m) sigs.push(`ARG ${m[1]}`);
6022
6058
  }
6023
6059
 
6024
- return sigs.slice(0, 25);
6060
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
6025
6061
  }
6026
6062
 
6027
6063
  module.exports = { extract };
@@ -6031,6 +6067,16 @@ __factories["./src/extractors/dockerfile"] = function(module, exports) {
6031
6067
  // ── ./src/extractors/gdscript ──
6032
6068
  __factories["./src/extractors/gdscript"] = function(module, exports) {
6033
6069
 
6070
+ const { capWithNotice } = __require('./src/util/truncate');
6071
+
6072
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
6073
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
6074
+ const PER_FILE_LIMIT = 25;
6075
+
6076
+ // Ceilings disclose what they drop rather than truncating silently (#576).
6077
+ const MEMBER_LIMIT = 6;
6078
+ const ENUM_LIMIT = 24;
6079
+
6034
6080
  /**
6035
6081
  * Extract signatures from Godot GDScript source code.
6036
6082
  * @param {string} src - Raw file content
@@ -6075,7 +6121,7 @@ __factories["./src/extractors/gdscript"] = function(module, exports) {
6075
6121
  .split(',')
6076
6122
  .map((s) => s.trim().split(/\s*=/)[0].trim())
6077
6123
  .filter(Boolean);
6078
- sigs.push(`${indent}enum ${m[1]} { ${members.slice(0, 6).join(', ')} }`);
6124
+ sigs.push(`${indent}enum ${m[1]} { ${capWithNotice(members, ENUM_LIMIT, 'values').join(', ')} }`);
6079
6125
  }
6080
6126
 
6081
6127
  let constCount = 0;
@@ -6123,7 +6169,7 @@ __factories["./src/extractors/gdscript"] = function(module, exports) {
6123
6169
  }
6124
6170
  }
6125
6171
 
6126
- return sigs.slice(0, 25);
6172
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
6127
6173
  }
6128
6174
 
6129
6175
  function extractInnerMembers(stripped, startIndex) {
@@ -6141,7 +6187,7 @@ __factories["./src/extractors/gdscript"] = function(module, exports) {
6141
6187
  members.push(`${staticKw}func ${fm[2]}(${params})${retStr}`);
6142
6188
  }
6143
6189
  }
6144
- return members.slice(0, 6);
6190
+ return capWithNotice(members, MEMBER_LIMIT, 'members');
6145
6191
  }
6146
6192
 
6147
6193
  function normalizeParams(params) {
@@ -6198,6 +6244,11 @@ __factories["./src/extractors/generic"] = function(module, exports) {
6198
6244
  __factories["./src/extractors/go"] = function(module, exports) {
6199
6245
 
6200
6246
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
6247
+ const { capWithNotice } = __require('./src/util/truncate');
6248
+
6249
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
6250
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
6251
+ const PER_FILE_LIMIT = 25;
6201
6252
 
6202
6253
  /**
6203
6254
  * Extract signatures from Go source code.
@@ -6246,7 +6297,7 @@ __factories["./src/extractors/go"] = function(module, exports) {
6246
6297
  sigs.push(hinted(withAnchor(`func ${receiver}${m[2]}(${normalizeParams(m[3])})${retStr}`, lineAt(stripped, m.index), lineAt(stripped, end)), m[2]));
6247
6298
  }
6248
6299
 
6249
- return sigs.slice(0, 25);
6300
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
6250
6301
  }
6251
6302
 
6252
6303
  function extractBlock(src, startIndex) {
@@ -6380,6 +6431,12 @@ __factories["./src/extractors/graphql"] = function(module, exports) {
6380
6431
  // ── ./src/extractors/html ──
6381
6432
  __factories["./src/extractors/html"] = function(module, exports) {
6382
6433
 
6434
+ const { capWithNotice } = __require('./src/util/truncate');
6435
+
6436
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
6437
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
6438
+ const PER_FILE_LIMIT = 25;
6439
+
6383
6440
  /**
6384
6441
  * Extract signatures from HTML files.
6385
6442
  * Focuses on id/class attributes, forms, and script tags.
@@ -6413,7 +6470,7 @@ __factories["./src/extractors/html"] = function(module, exports) {
6413
6470
  sigs.push(`data-${m[0].match(/data-(\w[\w-]*)/i)[1]}: ${m[1]}`);
6414
6471
  }
6415
6472
 
6416
- return sigs.slice(0, 25);
6473
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
6417
6474
  }
6418
6475
 
6419
6476
  module.exports = { extract };
@@ -6772,6 +6829,13 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6772
6829
  __factories["./src/extractors/kotlin"] = function(module, exports) {
6773
6830
 
6774
6831
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
6832
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
6833
+
6834
+ // Ceilings sit above the default `maxSigsPerFile` so the configured budget
6835
+ // governs output rather than a literal buried here, and omissions are disclosed
6836
+ // — an undisclosed cap looks like a class that simply has eight methods (#576).
6837
+ const MEMBER_LIMIT = 8;
6838
+ const PER_FILE_LIMIT = 25;
6775
6839
 
6776
6840
  /**
6777
6841
  * Extract signatures from Kotlin source code.
@@ -6806,7 +6870,8 @@ __factories["./src/extractors/kotlin"] = function(module, exports) {
6806
6870
  const block = extractBlock(stripped, bodyStart);
6807
6871
  sigs.push(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
6808
6872
  for (const meth of extractMembers(block)) {
6809
- sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
6873
+ // The disclosure marker carries no offsets; anchor it at the class body.
6874
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + (meth.declIdx || 0)), lineAt(stripped, bodyStart + (meth.endIdx || 0))));
6810
6875
  }
6811
6876
  }
6812
6877
 
@@ -6819,7 +6884,7 @@ __factories["./src/extractors/kotlin"] = function(module, exports) {
6819
6884
  sigs.push(withAnchor(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e));
6820
6885
  }
6821
6886
 
6822
- return sigs.slice(0, 25);
6887
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
6823
6888
  }
6824
6889
 
6825
6890
  function extractBlock(src, startIndex) {
@@ -6846,7 +6911,7 @@ __factories["./src/extractors/kotlin"] = function(module, exports) {
6846
6911
  endIdx: m.index + m[0].length,
6847
6912
  });
6848
6913
  }
6849
- return members.slice(0, 8);
6914
+ return capMembersWithNotice(members, MEMBER_LIMIT);
6850
6915
  }
6851
6916
 
6852
6917
  function normalizeParams(params) {
@@ -7095,6 +7160,13 @@ __factories["./src/extractors/patterns"] = function(module, exports) {
7095
7160
  __factories["./src/extractors/php"] = function(module, exports) {
7096
7161
 
7097
7162
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
7163
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
7164
+
7165
+ // Ceilings sit above the default `maxSigsPerFile` so the configured budget
7166
+ // governs output rather than a literal buried here, and omissions are disclosed
7167
+ // — an undisclosed cap looks like a class that simply has eight methods (#576).
7168
+ const MEMBER_LIMIT = 8;
7169
+ const PER_FILE_LIMIT = 25;
7098
7170
 
7099
7171
  /**
7100
7172
  * Extract signatures from PHP source code.
@@ -7134,7 +7206,8 @@ __factories["./src/extractors/php"] = function(module, exports) {
7134
7206
  const block = extractBlock(stripped, bodyStart);
7135
7207
  sigs.push(withAnchor(`${kind} ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
7136
7208
  for (const meth of extractMembers(block)) {
7137
- sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)));
7209
+ // The disclosure marker carries no offsets; anchor it at the class body.
7210
+ sigs.push(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + (meth.declIdx || 0)), lineAt(stripped, bodyStart + (meth.endIdx || 0))));
7138
7211
  }
7139
7212
  }
7140
7213
 
@@ -7146,7 +7219,7 @@ __factories["./src/extractors/php"] = function(module, exports) {
7146
7219
  sigs.push(withAnchor(`function ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e));
7147
7220
  }
7148
7221
 
7149
- return sigs.slice(0, 25);
7222
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
7150
7223
  }
7151
7224
 
7152
7225
  function extractBlock(src, startIndex) {
@@ -7174,7 +7247,7 @@ __factories["./src/extractors/php"] = function(module, exports) {
7174
7247
  endIdx: m.index + m[0].length,
7175
7248
  });
7176
7249
  }
7177
- return members.slice(0, 8);
7250
+ return capMembersWithNotice(members, MEMBER_LIMIT);
7178
7251
  }
7179
7252
 
7180
7253
  function normalizeParams(params) {
@@ -7378,6 +7451,11 @@ __factories["./src/extractors/python"] = function(module, exports) {
7378
7451
 
7379
7452
  const path = require('path');
7380
7453
  const { lineAt } = __require('./src/extractors/line-anchor');
7454
+ const { capWithNotice } = __require('./src/util/truncate');
7455
+
7456
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
7457
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
7458
+ const PER_FILE_LIMIT = 30;
7381
7459
 
7382
7460
  /**
7383
7461
  * 1-based line of the last source line belonging to a top-level (indent 0)
@@ -7512,7 +7590,7 @@ __factories["./src/extractors/python"] = function(module, exports) {
7512
7590
  }
7513
7591
  }
7514
7592
 
7515
- return sigs.slice(0, 30);
7593
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
7516
7594
  }
7517
7595
 
7518
7596
  function extractClassMethods(stripped, startIndex) {
@@ -7723,6 +7801,12 @@ __factories["./src/extractors/python_dataclass"] = function(module, exports) {
7723
7801
  // ── ./src/extractors/r ──
7724
7802
  __factories["./src/extractors/r"] = function(module, exports) {
7725
7803
 
7804
+ const { capWithNotice } = __require('./src/util/truncate');
7805
+
7806
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
7807
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
7808
+ const PER_FILE_LIMIT = 30;
7809
+
7726
7810
  /**
7727
7811
  * Extract signatures from R source code.
7728
7812
  *
@@ -7836,7 +7920,7 @@ __factories["./src/extractors/r"] = function(module, exports) {
7836
7920
  sigs.push(`setClass("${sm[1]}")`);
7837
7921
  }
7838
7922
 
7839
- return sigs.slice(0, 30);
7923
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
7840
7924
  }
7841
7925
 
7842
7926
  /**
@@ -8000,6 +8084,12 @@ __factories["./src/extractors/r"] = function(module, exports) {
8000
8084
  // ── ./src/extractors/ruby ──
8001
8085
  __factories["./src/extractors/ruby"] = function(module, exports) {
8002
8086
 
8087
+ const { capWithNotice } = __require('./src/util/truncate');
8088
+
8089
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
8090
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
8091
+ const PER_FILE_LIMIT = 25;
8092
+
8003
8093
  /**
8004
8094
  * Extract signatures from Ruby source code.
8005
8095
  * @param {string} src - Raw file content
@@ -8034,7 +8124,7 @@ __factories["./src/extractors/ruby"] = function(module, exports) {
8034
8124
  sigs.push(`def ${m[1]}${params}${retStr}`);
8035
8125
  }
8036
8126
 
8037
- return sigs.slice(0, 25);
8127
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
8038
8128
  }
8039
8129
 
8040
8130
  function normalizeParams(params) {
@@ -8059,6 +8149,11 @@ __factories["./src/extractors/ruby"] = function(module, exports) {
8059
8149
  __factories["./src/extractors/rust"] = function(module, exports) {
8060
8150
 
8061
8151
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
8152
+ const { capWithNotice } = __require('./src/util/truncate');
8153
+
8154
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
8155
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
8156
+ const PER_FILE_LIMIT = 25;
8062
8157
 
8063
8158
  /**
8064
8159
  * Extract signatures from Rust source code.
@@ -8128,7 +8223,7 @@ __factories["./src/extractors/rust"] = function(module, exports) {
8128
8223
  sigs.push(hinted(withAnchor(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e), m[1]));
8129
8224
  }
8130
8225
 
8131
- return sigs.slice(0, 25);
8226
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
8132
8227
  }
8133
8228
 
8134
8229
  function extractBlock(src, startIndex) {
@@ -8201,6 +8296,13 @@ __factories["./src/extractors/rust"] = function(module, exports) {
8201
8296
  __factories["./src/extractors/scala"] = function(module, exports) {
8202
8297
 
8203
8298
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
8299
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
8300
+
8301
+ // Ceilings sit above the default `maxSigsPerFile` so the configured budget
8302
+ // governs output rather than a literal buried here, and omissions are disclosed
8303
+ // — an undisclosed cap looks like a class that simply has eight methods (#576).
8304
+ const MEMBER_LIMIT = 8;
8305
+ const PER_FILE_LIMIT = 25;
8204
8306
 
8205
8307
  /**
8206
8308
  * Extract signatures from Scala source code.
@@ -8227,7 +8329,8 @@ __factories["./src/extractors/scala"] = function(module, exports) {
8227
8329
  const block = extractBlock(stripped, bodyStart);
8228
8330
  sigs.push(withAnchor(`${kind} ${m[1]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
8229
8331
  for (const fn of extractMembers(block)) {
8230
- sigs.push(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)));
8332
+ // The disclosure marker carries no offsets; anchor it at the class body.
8333
+ sigs.push(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + (fn.declIdx || 0)), lineAt(stripped, bodyStart + (fn.endIdx || 0))));
8231
8334
  }
8232
8335
  }
8233
8336
 
@@ -8241,7 +8344,7 @@ __factories["./src/extractors/scala"] = function(module, exports) {
8241
8344
  sigs.push(withAnchor(`def ${m[1]}${params}${retStr}`, line, line));
8242
8345
  }
8243
8346
 
8244
- return sigs.slice(0, 25);
8347
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
8245
8348
  }
8246
8349
 
8247
8350
  function extractBlock(src, startIndex) {
@@ -8268,7 +8371,7 @@ __factories["./src/extractors/scala"] = function(module, exports) {
8268
8371
  endIdx: m.index + m[0].length,
8269
8372
  });
8270
8373
  }
8271
- return members.slice(0, 8);
8374
+ return capMembersWithNotice(members, MEMBER_LIMIT);
8272
8375
  }
8273
8376
 
8274
8377
  function normalizeParams(params) {
@@ -8387,6 +8490,12 @@ __factories["./src/extractors/scan"] = function(module, exports) {
8387
8490
  // ── ./src/extractors/shell ──
8388
8491
  __factories["./src/extractors/shell"] = function(module, exports) {
8389
8492
 
8493
+ const { capWithNotice } = __require('./src/util/truncate');
8494
+
8495
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
8496
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
8497
+ const PER_FILE_LIMIT = 25;
8498
+
8390
8499
  /**
8391
8500
  * Extract signatures from shell scripts (bash, zsh, fish).
8392
8501
  * @param {string} src - Raw file content
@@ -8424,7 +8533,7 @@ __factories["./src/extractors/shell"] = function(module, exports) {
8424
8533
  }
8425
8534
  }
8426
8535
 
8427
- return sigs.slice(0, 25);
8536
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
8428
8537
  }
8429
8538
 
8430
8539
  module.exports = { extract };
@@ -8531,6 +8640,12 @@ __factories["./src/extractors/sql"] = function(module, exports) {
8531
8640
  // ── ./src/extractors/svelte ──
8532
8641
  __factories["./src/extractors/svelte"] = function(module, exports) {
8533
8642
 
8643
+ const { capWithNotice } = __require('./src/util/truncate');
8644
+
8645
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
8646
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
8647
+ const PER_FILE_LIMIT = 25;
8648
+
8534
8649
  /**
8535
8650
  * Extract signatures from Svelte components.
8536
8651
  * @param {string} src - Raw file content
@@ -8573,7 +8688,7 @@ __factories["./src/extractors/svelte"] = function(module, exports) {
8573
8688
  sigs.push(`$: ${m[1]}`);
8574
8689
  }
8575
8690
 
8576
- return sigs.slice(0, 25);
8691
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
8577
8692
  }
8578
8693
 
8579
8694
  function normalizeParams(params) {
@@ -8594,6 +8709,13 @@ __factories["./src/extractors/svelte"] = function(module, exports) {
8594
8709
  __factories["./src/extractors/swift"] = function(module, exports) {
8595
8710
 
8596
8711
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
8712
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
8713
+
8714
+ // Ceilings sit above the default `maxSigsPerFile` so the configured budget
8715
+ // governs output rather than a literal buried here, and omissions are disclosed
8716
+ // — an undisclosed cap looks like a class that simply has eight methods (#576).
8717
+ const MEMBER_LIMIT = 8;
8718
+ const PER_FILE_LIMIT = 25;
8597
8719
 
8598
8720
  /**
8599
8721
  * Extract signatures from Swift source code.
@@ -8629,7 +8751,8 @@ __factories["./src/extractors/swift"] = function(module, exports) {
8629
8751
  const block = extractBlock(stripped, bodyStart);
8630
8752
  sigs.push(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)));
8631
8753
  for (const fn of extractMembers(block)) {
8632
- sigs.push(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + fn.declIdx), lineAt(stripped, bodyStart + fn.endIdx)));
8754
+ // The disclosure marker carries no offsets; anchor it at the class body.
8755
+ sigs.push(withAnchor(` ${fn.text}`, lineAt(stripped, bodyStart + (fn.declIdx || 0)), lineAt(stripped, bodyStart + (fn.endIdx || 0))));
8633
8756
  }
8634
8757
  }
8635
8758
 
@@ -8641,7 +8764,7 @@ __factories["./src/extractors/swift"] = function(module, exports) {
8641
8764
  sigs.push(withAnchor(`${asyncKw}func ${m[1]}(${normalizeParams(m[2])})${retStr}`, s, e));
8642
8765
  }
8643
8766
 
8644
- return sigs.slice(0, 25);
8767
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
8645
8768
  }
8646
8769
 
8647
8770
  function extractBlock(src, startIndex) {
@@ -8667,7 +8790,7 @@ __factories["./src/extractors/swift"] = function(module, exports) {
8667
8790
  endIdx: m.index + m[0].length,
8668
8791
  });
8669
8792
  }
8670
- return members.slice(0, 8);
8793
+ return capMembersWithNotice(members, MEMBER_LIMIT);
8671
8794
  }
8672
8795
 
8673
8796
  function normalizeParams(params) {
@@ -9242,6 +9365,12 @@ __factories["./src/extractors/typescript_react"] = function(module, exports) {
9242
9365
  // ── ./src/extractors/vue ──
9243
9366
  __factories["./src/extractors/vue"] = function(module, exports) {
9244
9367
 
9368
+ const { capWithNotice } = __require('./src/util/truncate');
9369
+
9370
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
9371
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
9372
+ const PER_FILE_LIMIT = 25;
9373
+
9245
9374
  /**
9246
9375
  * Extract signatures from Vue single-file components.
9247
9376
  * @param {string} src - Raw file content
@@ -9306,7 +9435,7 @@ __factories["./src/extractors/vue"] = function(module, exports) {
9306
9435
  const emitsMatch = script.match(/(?:defineEmits|emits)\s*(?::\s*|\(\s*)(\[[\s\S]*?\])/);
9307
9436
  if (emitsMatch) sigs.push(`emits: ${emitsMatch[1].replace(/\s+/g, ' ')}`);
9308
9437
 
9309
- return sigs.slice(0, 25);
9438
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
9310
9439
  }
9311
9440
 
9312
9441
  function normalizeParams(params) {
@@ -9479,6 +9608,12 @@ __factories["./src/extractors/xml"] = function(module, exports) {
9479
9608
  // ── ./src/extractors/yaml ──
9480
9609
  __factories["./src/extractors/yaml"] = function(module, exports) {
9481
9610
 
9611
+ const { capWithNotice } = __require('./src/util/truncate');
9612
+
9613
+ // Ceiling sits above the default `maxSigsPerFile` so the configured budget
9614
+ // governs output rather than a literal buried here, and omissions are disclosed (#576).
9615
+ const PER_FILE_LIMIT = 25;
9616
+
9482
9617
  /**
9483
9618
  * Extract signatures from YAML configuration files.
9484
9619
  * @param {string} src - Raw file content
@@ -9532,7 +9667,7 @@ __factories["./src/extractors/yaml"] = function(module, exports) {
9532
9667
  }
9533
9668
  }
9534
9669
 
9535
- return sigs.slice(0, 25);
9670
+ return capWithNotice(sigs, PER_FILE_LIMIT, 'signatures');
9536
9671
  }
9537
9672
 
9538
9673
  module.exports = { extract };
@@ -11752,24 +11887,60 @@ __factories["./src/graph/builder"] = function(module, exports) {
11752
11887
  return { forward, reverse };
11753
11888
  }
11754
11889
 
11890
+ // Directory names assumed when neither the caller nor the project config says
11891
+ // otherwise. A Maven/Gradle module (`mall-portal/`, `service-api/`) matches none
11892
+ // of them, which is why the config is consulted first.
11893
+ const DEFAULT_SRC_DIRS = ['src', 'app', 'lib', 'R', 'inst'];
11894
+
11895
+ // Walk depth measured from EACH srcDir root, not from cwd — so this is not the
11896
+ // same quantity as the extractor's cwd-relative `maxDepth` and must not be read
11897
+ // from it. A standard Maven tree reaches `src/main/java/<group>/<artifact>/…`
11898
+ // nine directories below its module root, so the previous ceiling of 8 silently
11899
+ // dropped the deepest packages (on macrozheng/mall: every `service/impl/` class).
11900
+ const DEFAULT_WALK_DEPTH = 12;
11901
+
11902
+ /**
11903
+ * Source directories declared in the project's own config, or null when there
11904
+ * is no readable config. Read directly rather than through `loadConfig`, which
11905
+ * can fetch `extends` over the network and spawn a child process — neither is
11906
+ * acceptable inside a graph build.
11907
+ */
11908
+ function _configuredSrcDirs(cwd) {
11909
+ try {
11910
+ const raw = fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8');
11911
+ const cfg = JSON.parse(raw);
11912
+ if (Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0) return cfg.srcDirs;
11913
+ } catch (_) { /* absent or unparsable — fall back to the defaults */ }
11914
+ return null;
11915
+ }
11916
+
11755
11917
  /**
11756
11918
  * Build a dependency graph scoped to a single cwd by walking all JS/TS/Py/Go
11757
11919
  * files under srcDirs. Useful for the MCP tool handler.
11758
11920
  *
11921
+ * srcDirs resolution order: explicit `opts.srcDirs` → `gen-context.config.json`
11922
+ * → DEFAULT_SRC_DIRS. Without the config step the graph is empty on any repo
11923
+ * whose sources do not sit under a conventionally-named directory.
11924
+ *
11759
11925
  * @param {string} cwd
11760
11926
  * @param {object} [opts]
11761
11927
  * @param {string[]} [opts.srcDirs]
11762
11928
  * @param {string[]} [opts.exclude]
11929
+ * @param {number} [opts.maxDepth] - walk depth from each srcDir root
11763
11930
  * @returns {{ forward: Map<string,string[]>, reverse: Map<string,string[]> }}
11764
11931
  */
11765
11932
  function buildFromCwd(cwd, opts) {
11766
11933
  // R-package layouts use `R/` and `inst/`; Shiny apps put helpers in `R/`.
11767
11934
  // The existence check below makes these no-ops in non-R projects.
11768
- const { srcDirs = ['src', 'app', 'lib', 'R', 'inst'], exclude = ['node_modules', '.git', 'dist', 'build'] } = opts || {};
11935
+ const {
11936
+ srcDirs = _configuredSrcDirs(cwd) || DEFAULT_SRC_DIRS,
11937
+ exclude = ['node_modules', '.git', 'dist', 'build'],
11938
+ maxDepth = DEFAULT_WALK_DEPTH,
11939
+ } = opts || {};
11769
11940
  const excludeSet = new Set(exclude);
11770
11941
 
11771
11942
  function walkDir(dir, depth) {
11772
- if (depth > 8) return [];
11943
+ if (depth > maxDepth) return [];
11773
11944
  let entries;
11774
11945
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return []; }
11775
11946
  const out = [];
@@ -11818,7 +11989,7 @@ __factories["./src/graph/builder"] = function(module, exports) {
11818
11989
  return build(files, cwd, ctx);
11819
11990
  }
11820
11991
 
11821
- module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias };
11992
+ module.exports = { build, buildFromCwd, extractFileDeps, normalizePath, loadAliasMap, resolveAlias, _configuredSrcDirs, DEFAULT_SRC_DIRS, DEFAULT_WALK_DEPTH };
11822
11993
 
11823
11994
  };
11824
11995
 
@@ -12073,6 +12244,44 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12073
12244
  // Java: methods + constructors with braced bodies. Statement-shaped matches
12074
12245
  // (calls, control flow) are rejected because their `)` is followed by `;`,
12075
12246
  // and keyword headers (`if`, `while`, …) fall to the NON_CALL guard.
12247
+ // Words that can precede `name(` in a STATEMENT, so their presence means the
12248
+ // line is not a method declaration.
12249
+ const STMT_KEYWORDS = new Set([
12250
+ 'return', 'throw', 'else', 'do', 'try', 'case', 'yield', 'assert', 'new',
12251
+ 'if', 'while', 'for', 'switch', 'catch', 'synchronized', 'instanceof', 'await',
12252
+ ]);
12253
+
12254
+ // Types a Java class declares it implements or extends, with generic arguments
12255
+ // stripped and any package qualifier dropped: `implements Foo<Bar, Baz>` yields
12256
+ // ['Foo'], never 'Baz>'. Also records whether the class is a Spring bean and
12257
+ // whether it is @Primary, which is what disambiguates several implementations.
12258
+ function javaTypeDecl(masked) {
12259
+ const m = /(?:^|\n)[^\n]*?\bclass\s+([A-Za-z_$][\w$]*)([^{]*)\{/.exec(masked);
12260
+ if (!m) return null;
12261
+ const [, className, tail] = m;
12262
+ const supers = [];
12263
+ for (const kw of ['implements', 'extends']) {
12264
+ const k = new RegExp('\\b' + kw + '\\s+([^{]*?)(?=\\b(?:implements|extends)\\b|$)').exec(tail);
12265
+ if (!k) continue;
12266
+ let depth = 0;
12267
+ let cur = '';
12268
+ for (const ch of k[1]) {
12269
+ if (ch === '<') { depth++; continue; }
12270
+ if (ch === '>') { depth--; continue; }
12271
+ if (ch === ',' && depth === 0) { if (cur.trim()) supers.push(cur.trim()); cur = ''; continue; }
12272
+ if (depth === 0) cur += ch;
12273
+ }
12274
+ if (cur.trim()) supers.push(cur.trim());
12275
+ }
12276
+ const head = masked.slice(0, m.index + m[0].length);
12277
+ return {
12278
+ className,
12279
+ supers: supers.map((t) => t.split('.').pop().trim()).filter(Boolean),
12280
+ isBean: /@(Service|Component|Repository|Controller|RestController)\b/.test(head),
12281
+ isPrimary: /@Primary\b/.test(head),
12282
+ };
12283
+ }
12284
+
12076
12285
  function javaDefs(masked) {
12077
12286
  const defs = [];
12078
12287
  const seen = new Set();
@@ -12089,11 +12298,24 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12089
12298
  // skip `throws A, B` up to the body `{` (same line — multi-line headers are skipped)
12090
12299
  let k = close + 1;
12091
12300
  while (k < masked.length && masked[k] !== '{' && masked[k] !== ';' && masked[k] !== '\n' && masked[k] !== '=') k++;
12092
- if (masked[k] !== '{') continue;
12301
+ // A `;` here is an interface or abstract method DECLARATION. It owns no body,
12302
+ // so it emits no calls — but in Spring the declared interface is what callers
12303
+ // name, so without it as a node every controller→service edge has no target.
12304
+ // Recorded with an empty body range: can receive edges, never produces them.
12305
+ // `before` is everything between line start and the method name. A real
12306
+ // declaration has modifiers or a return type there (`void chargeCard(`);
12307
+ // a call statement has only whitespace (`identity(1);`) or a statement
12308
+ // keyword (`return helper(a);`) — neither may be read as a declaration,
12309
+ // or the call resolves to a phantom local def instead of the real target.
12310
+ const headWord = (before.match(/([A-Za-z_$][\w$]*)\s*$/) || [])[1];
12311
+ const isDecl = masked[k] === ';' && /\S/.test(before) && !STMT_KEYWORDS.has(headWord);
12312
+ if (masked[k] !== '{' && !isDecl) continue;
12093
12313
  const key = name + ':' + k;
12094
12314
  if (seen.has(key)) continue;
12095
12315
  seen.add(key);
12096
- defs.push({ name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
12316
+ defs.push(isDecl
12317
+ ? { name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: k }
12318
+ : { name, line: lineAt(masked, m.index + 1), bodyStart: k, bodyEnd: matchDelim(masked, k, '{', '}') });
12097
12319
  }
12098
12320
  return defs;
12099
12321
  }
@@ -12147,7 +12369,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12147
12369
  const re = /([A-Za-z_$][\w$]*)\s*\(/g;
12148
12370
  let m;
12149
12371
  while ((m = re.exec(slice)) !== null) {
12150
- // skip a `.name(` method access (can't resolve the receiver deterministically)
12372
+ // skip a `.name(` method access resolved separately via receiverCallsInRange
12151
12373
  const before = slice[m.index - 1];
12152
12374
  if (before === '.') continue;
12153
12375
  if (!NON_CALL.has(m[1])) names.add(m[1]);
@@ -12155,16 +12377,75 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12155
12377
  return names;
12156
12378
  }
12157
12379
 
12380
+ // Collect `receiver.method(` pairs within [start,end). A chained or computed
12381
+ // receiver (`a.b().c(`, `arr[0].c(`) is skipped: only a plain identifier can be
12382
+ // looked up in the declaration map, and guessing is worse than no edge.
12383
+ function receiverCallsInRange(masked, start, end) {
12384
+ const slice = masked.slice(start, end);
12385
+ const out = [];
12386
+ const re = /([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)\s*\(/g;
12387
+ let m;
12388
+ while ((m = re.exec(slice)) !== null) {
12389
+ const before = slice[m.index - 1];
12390
+ if (before === '.' || before === ')' || before === ']') continue;
12391
+ if (NON_CALL.has(m[2])) continue;
12392
+ out.push({ receiver: m[1], method: m[2] });
12393
+ }
12394
+ return out;
12395
+ }
12396
+
12397
+ // `private UserService userService;` / `UserService svc = new UserService();`
12398
+ // / `for (OmsOrderItem item : list)` → { userService: 'UserService', … }.
12399
+ // Declarations only: a bare assignment carries no type and is not inferred.
12400
+ 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;
12401
+
12402
+ function buildTypeMap(masked) {
12403
+ const map = new Map();
12404
+ let m;
12405
+ DECL_RE.lastIndex = 0;
12406
+ while ((m = DECL_RE.exec(masked)) !== null) {
12407
+ const [, type, name] = m;
12408
+ if (JVM_KEYWORDS.has(type) || JVM_KEYWORDS.has(name)) continue;
12409
+ if (!map.has(name)) map.set(name, type); // first declaration wins — deterministic
12410
+ }
12411
+ return map;
12412
+ }
12413
+
12414
+ // Type names that are never a user class, so never a resolvable receiver type.
12415
+ const JVM_KEYWORDS = new Set([
12416
+ 'return', 'new', 'if', 'else', 'for', 'while', 'switch', 'case', 'throw', 'catch',
12417
+ 'String', 'Integer', 'Long', 'Boolean', 'Double', 'Float', 'Object', 'List', 'Map',
12418
+ 'Set', 'Collection', 'Optional', 'Override', 'Autowired', 'Resource', 'Deprecated',
12419
+ ]);
12420
+
12158
12421
  // ── Public API ───────────────────────────────────────────────────────────────
12159
12422
 
12160
- function _walk(dir, excludeSet, out, depth) {
12161
- if (depth > 8) return;
12423
+ // Walk depth from each srcDir root (not from cwd). A Maven module reaches
12424
+ // `src/main/java/<group>/<artifact>/service/impl` nine directories down, so the
12425
+ // previous ceiling of 8 never saw the classes that own the method bodies.
12426
+ const DEFAULT_WALK_DEPTH = 12;
12427
+
12428
+ /**
12429
+ * Source directories declared in the project's own config, or null. Read
12430
+ * directly rather than through `loadConfig`, which can fetch `extends` over the
12431
+ * network and spawn a child process — neither belongs inside a graph build.
12432
+ */
12433
+ function _configuredSrcDirs(cwd) {
12434
+ try {
12435
+ const cfg = JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
12436
+ if (Array.isArray(cfg.srcDirs) && cfg.srcDirs.length > 0) return cfg.srcDirs;
12437
+ } catch (_) { /* absent or unparsable — fall back to the defaults */ }
12438
+ return null;
12439
+ }
12440
+
12441
+ function _walk(dir, excludeSet, out, depth, maxDepth) {
12442
+ if (depth > (maxDepth === undefined ? DEFAULT_WALK_DEPTH : maxDepth)) return;
12162
12443
  let entries;
12163
12444
  try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
12164
12445
  for (const e of entries) {
12165
12446
  if (excludeSet.has(e.name) || e.name.startsWith('.')) continue;
12166
12447
  const full = path.join(dir, e.name);
12167
- if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1);
12448
+ if (e.isDirectory()) _walk(full, excludeSet, out, depth + 1, maxDepth);
12168
12449
  else if (e.isFile()) {
12169
12450
  const ext = path.extname(e.name).toLowerCase();
12170
12451
  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 +12471,13 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12190
12471
  const excludeSet = new Set(opts.exclude || ['node_modules', '.git', 'dist', 'build', 'coverage', 'vendor']);
12191
12472
  let files = opts.files ? opts.files.map((f) => path.resolve(f)) : [];
12192
12473
  if (!opts.files) {
12193
- for (const sd of (opts.srcDirs || ['src', 'app', 'lib'])) {
12474
+ // Same resolution order as the dependency graph (#560): explicit opts →
12475
+ // the project's own config → the historical defaults. Without the config
12476
+ // step this is empty on any repo whose sources are not under src/app/lib.
12477
+ const srcDirs = opts.srcDirs || _configuredSrcDirs(cwd) || ['src', 'app', 'lib'];
12478
+ for (const sd of srcDirs) {
12194
12479
  const abs = path.resolve(cwd, sd);
12195
- if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0);
12480
+ if (fs.existsSync(abs)) _walk(abs, excludeSet, files, 0, opts.maxDepth);
12196
12481
  }
12197
12482
  }
12198
12483
 
@@ -12206,6 +12491,44 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12206
12491
  const normToAbs = new Map(); // normalized abs → abs
12207
12492
  const defs = new Map(); // symbolId → {file,name,line}
12208
12493
 
12494
+ // JVM convention: a public type lives in a file of the same name. This is the
12495
+ // deterministic type→file mapping receiver resolution needs, with no AST.
12496
+ const fileByTypeName = new Map(); // 'UserService' → [absFile]
12497
+ for (const f of files) {
12498
+ const ext = path.extname(f).toLowerCase();
12499
+ if (JAVA_EXTS.has(ext)) {
12500
+ const base = path.basename(f, path.extname(f));
12501
+ if (!fileByTypeName.has(base)) fileByTypeName.set(base, []);
12502
+ fileByTypeName.get(base).push(f);
12503
+ }
12504
+ }
12505
+
12506
+ // interface/superclass name → implementing files, for the Spring hop below.
12507
+ const implsByType = new Map(); // 'PaymentService' → [{ file, isBean, isPrimary }]
12508
+ for (const f of files) {
12509
+ if (!JAVA_EXTS.has(path.extname(f).toLowerCase())) continue;
12510
+ let decl;
12511
+ try { decl = javaTypeDecl(maskJs(fs.readFileSync(f, 'utf8'))); } catch (_) { continue; }
12512
+ if (!decl) continue;
12513
+ for (const sup of decl.supers) {
12514
+ if (!implsByType.has(sup)) implsByType.set(sup, []);
12515
+ implsByType.get(sup).push({ file: f, isBean: decl.isBean, isPrimary: decl.isPrimary });
12516
+ }
12517
+ }
12518
+
12519
+ /**
12520
+ * The single implementing file for a type, or null when it is ambiguous.
12521
+ * One implementation resolves outright; several resolve only via @Primary.
12522
+ * Anything still ambiguous yields no edge — polymorphism is not guessed.
12523
+ */
12524
+ const soleImpl = (typeName) => {
12525
+ const cands = implsByType.get(typeName) || [];
12526
+ if (cands.length === 1) return cands[0].file;
12527
+ const primary = cands.filter((c) => c.isPrimary);
12528
+ if (primary.length === 1) return primary[0].file;
12529
+ return null;
12530
+ };
12531
+
12209
12532
  for (const f of files) {
12210
12533
  normToAbs.set(normalizePath(path.resolve(f)), path.resolve(f));
12211
12534
  let src;
@@ -12225,12 +12548,20 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12225
12548
 
12226
12549
  const forward = new Map();
12227
12550
  const reverse = new Map();
12228
- const addEdge = (from, to) => {
12551
+ // Additive: `forward`/`reverse` keep their existing shape, so every current
12552
+ // consumer is unaffected. Confidence is looked up by "from\u0000to".
12553
+ const edgeConfidence = new Map();
12554
+ const addEdge = (from, to, confidence) => {
12229
12555
  if (from === to) return;
12230
12556
  if (!forward.has(from)) forward.set(from, new Set());
12231
12557
  forward.get(from).add(to);
12232
12558
  if (!reverse.has(to)) reverse.set(to, new Set());
12233
12559
  reverse.get(to).add(from);
12560
+ if (confidence) {
12561
+ const k = from + '\u0000' + to;
12562
+ // A 'high' resolution never loses to a later 'medium' one.
12563
+ if (edgeConfidence.get(k) !== 'high') edgeConfidence.set(k, confidence);
12564
+ }
12234
12565
  };
12235
12566
 
12236
12567
  for (const [f, fileDefs] of perFileDefs.entries()) {
@@ -12248,16 +12579,59 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12248
12579
  .sort();
12249
12580
  importedAbs.push(...siblings);
12250
12581
  }
12582
+ // Receiver types come from declarations anywhere in the file: fields are
12583
+ // declared outside any method body, locals inside one.
12584
+ const typeMap = JAVA_EXTS.has(ext) ? buildTypeMap(masked) : null;
12585
+ // Types reachable from this file, by name — imports first, then same-package
12586
+ // siblings, so an import always wins over a coincidental sibling name.
12587
+ const scopeByTypeName = new Map();
12588
+ if (typeMap) {
12589
+ for (const imp of importedAbs) {
12590
+ const base = path.basename(imp, path.extname(imp));
12591
+ if (!scopeByTypeName.has(base)) scopeByTypeName.set(base, imp);
12592
+ }
12593
+ }
12594
+
12251
12595
  for (const d of fileDefs) {
12252
12596
  const callerId = symId(cwd, f, d.name);
12253
12597
  if (!forward.has(callerId)) forward.set(callerId, new Set()); // ensure node exists
12254
12598
  const callees = callsInRange(masked, d.bodyStart, d.bodyEnd);
12255
12599
  for (const nm of callees) {
12256
12600
  const local = (defsByName.get(f) || new Map()).get(nm);
12257
- if (local && local.length) { for (const id of local) addEdge(callerId, id); continue; }
12601
+ if (local && local.length) { for (const id of local) addEdge(callerId, id, 'high'); continue; }
12258
12602
  for (const imp of importedAbs) {
12259
12603
  const ids = (defsByName.get(imp) || new Map()).get(nm);
12260
- if (ids && ids.length) { for (const id of ids) addEdge(callerId, id); break; }
12604
+ if (ids && ids.length) { for (const id of ids) addEdge(callerId, id, 'high'); break; }
12605
+ }
12606
+ }
12607
+
12608
+ // `receiver.method(` — resolve the receiver's declared type to a file.
12609
+ if (!typeMap) continue;
12610
+ for (const { receiver, method } of receiverCallsInRange(masked, d.bodyStart, d.bodyEnd)) {
12611
+ // A receiver that is itself a type name is a static call: `Foo.bar()`.
12612
+ const typeName = typeMap.get(receiver)
12613
+ || (fileByTypeName.has(receiver) ? receiver : null);
12614
+ if (!typeName) continue; // unknown receiver → no edge, never a guess
12615
+
12616
+ let target = scopeByTypeName.get(typeName);
12617
+ let confidence = 'high'; // typed receiver, resolved in scope
12618
+ if (!target) {
12619
+ const candidates = fileByTypeName.get(typeName) || [];
12620
+ if (candidates.length !== 1) continue; // ambiguous or absent → no edge
12621
+ target = candidates[0];
12622
+ confidence = 'medium'; // type known, but not in this file's scope
12623
+ }
12624
+ const ids = (defsByName.get(target) || new Map()).get(method);
12625
+ if (ids && ids.length) for (const id of ids) addEdge(callerId, id, confidence);
12626
+
12627
+ // Spring: the call names the interface, but the code that runs — and
12628
+ // that a reviewer changes — lives in the implementation. Both edges are
12629
+ // true, so both are recorded; without the second, blast radius on an
12630
+ // implementation is empty.
12631
+ const implFile = soleImpl(typeName);
12632
+ if (implFile && implFile !== target) {
12633
+ const implIds = (defsByName.get(implFile) || new Map()).get(method);
12634
+ if (implIds && implIds.length) for (const id of implIds) addEdge(callerId, id, confidence);
12261
12635
  }
12262
12636
  }
12263
12637
  }
@@ -12268,7 +12642,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12268
12642
  for (const [k, set] of mapOfSets.entries()) out.set(k, [...set]);
12269
12643
  return out;
12270
12644
  };
12271
- return { forward: toArr(forward), reverse: toArr(reverse), defs };
12645
+ return { forward: toArr(forward), reverse: toArr(reverse), defs, edgeConfidence };
12272
12646
  }
12273
12647
 
12274
12648
  /**
@@ -12390,7 +12764,7 @@ __factories["./src/graph/call-graph"] = function(module, exports) {
12390
12764
  }
12391
12765
 
12392
12766
  module.exports = {
12393
- buildCallGraph, buildCallFileGraph, methodImpact, methodCallees,
12767
+ buildCallGraph, buildTypeMap, receiverCallsInRange, javaTypeDecl, DEFAULT_WALK_DEPTH, buildCallFileGraph, methodImpact, methodCallees,
12394
12768
  formatCallGraph, formatCallGraphJSON,
12395
12769
  extractDefs, maskJs, maskPy, maskRust,
12396
12770
  };
@@ -15440,7 +15814,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
15440
15814
 
15441
15815
  const SERVER_INFO = {
15442
15816
  name: 'sigmap',
15443
- version: '8.30.0',
15817
+ version: '8.32.0',
15444
15818
  description: 'SigMap MCP server — code signatures on demand',
15445
15819
  };
15446
15820
 
@@ -18805,7 +19179,7 @@ __factories["./src/skills/skills"] = function(module, exports) {
18805
19179
  'Follow this loop before any file exploration in a repo with SigMap installed.',
18806
19180
  '',
18807
19181
  '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.',
19182
+ '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
19183
  '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
19184
  '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
19185
  '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 +19197,7 @@ __factories["./src/skills/skills"] = function(module, exports) {
18823
19197
  '',
18824
19198
  '1. **Look up, do not search.** `npx sigmap ask "<the task>"` — this writes `.context/query-context.md`.',
18825
19199
  '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. **Open only the anchored ranges.** A signature ending `:425-425` means read line 425, not the whole file. Never read a file in full when you hold an anchor for it.',
19200
+ '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
19201
  '4. **Make the change.** Follow the conventions visible in the signatures — same layering, same response wrapper, same annotation style. Add no dependencies.',
18828
19202
  '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
19203
  '6. **Refresh the map.** `npx sigmap` — your edits made it stale.',
@@ -21734,7 +22108,7 @@ function __tryGit(args, opts = {}) {
21734
22108
  catch (_) { return ''; }
21735
22109
  }
21736
22110
 
21737
- const VERSION = '8.30.0';
22111
+ const VERSION = '8.32.0';
21738
22112
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
21739
22113
 
21740
22114
  function requireSourceOrBundled(key) {
@@ -23740,6 +24114,8 @@ Usage:
23740
24114
  ${cmd} tune --apply Write the recommendations into gen-context.config.json (merges; your keys preserved)
23741
24115
  ${cmd} skills list List skill clients (Claude/Cursor/Windsurf/Copilot/AGENTS.md) and install state (--json)
23742
24116
  ${cmd} skills install Install the SigMap agent playbooks for detected clients (--client <name> | --all)
24117
+ ${cmd} lines <file> <start>-<end> Print an exact line range — CLI twin of get_lines (secrets redacted)
24118
+ ${cmd} lines <file> :<line> --context <n> Window around one signature anchor (default ±10)
23743
24119
  ${cmd} note "<text>" Append a note to the cross-session decision log
23744
24120
  ${cmd} note List recent notes (also: note --list <N>)
23745
24121
  ${cmd} status Show repo state — branch, dirty files, index freshness, notes
@@ -25343,6 +25719,55 @@ function main() {
25343
25719
  process.exit(0);
25344
25720
  }
25345
25721
 
25722
+ // `sigmap lines <file> <start>-<end>` — the CLI twin of the get_lines MCP
25723
+ // tool. Without it, an agent in an MCP-less environment receives precise
25724
+ // `:start-end` anchors from `ask` and has no sanctioned way to spend them,
25725
+ // so it falls back to reading whole files and throws the saving away.
25726
+ // Delegates to the same handler as MCP so both paths share the sandbox,
25727
+ // the bounds clamping and the secret redaction.
25728
+ if (args[0] === 'lines') {
25729
+ const valOf = (f) => { const i = args.indexOf(f); return i >= 0 && args[i + 1] ? args[i + 1] : null; };
25730
+ const positional = [];
25731
+ const VALUE_FLAGS = new Set(['--cwd', '--context']);
25732
+ for (let i = 1; i < args.length; i++) {
25733
+ const a = args[i];
25734
+ if (a.startsWith('--')) { if (VALUE_FLAGS.has(a)) i++; continue; }
25735
+ positional.push(a);
25736
+ }
25737
+ const file = positional[0];
25738
+ const range = positional[1];
25739
+ if (!file || !range) {
25740
+ console.error('[sigmap] usage: sigmap lines <file> <start>-<end> (or :<line> --context <n>)');
25741
+ process.exit(2);
25742
+ }
25743
+
25744
+ // Accept `84-104`, a bare `94`, or the `:94` form copied straight off a
25745
+ // signature anchor — the whole point is to paste what `ask` printed.
25746
+ const ctx = Math.max(0, parseInt(valOf('--context') || '10', 10));
25747
+ let start;
25748
+ let end;
25749
+ const span = /^:?(\d+)\s*-\s*(\d+)$/.exec(range);
25750
+ const single = /^:?(\d+)$/.exec(range);
25751
+ if (span) { start = parseInt(span[1], 10); end = parseInt(span[2], 10); }
25752
+ else if (single) { const n = parseInt(single[1], 10); start = Math.max(1, n - ctx); end = n + ctx; }
25753
+ else {
25754
+ console.error(`[sigmap] lines: could not read range "${range}" — expected <start>-<end> or :<line>`);
25755
+ process.exit(2);
25756
+ }
25757
+
25758
+ const { getLines } = requireSourceOrBundled('./src/mcp/handlers');
25759
+ const out = getLines({ file, start, end }, cwd);
25760
+ // The handler reports its own failures as prose; surface them on stderr
25761
+ // with a non-zero exit so a script can tell a hit from a miss.
25762
+ if (/^(Missing required argument|Refused:|File not found:|Could not read |Arguments )/.test(out)
25763
+ || /has only \d+ lines; requested/.test(out)) {
25764
+ console.error('[sigmap] ' + out);
25765
+ process.exit(1);
25766
+ }
25767
+ process.stdout.write(out + '\n');
25768
+ process.exit(0);
25769
+ }
25770
+
25346
25771
  if (args[0] === 'note') {
25347
25772
  const jsonOut = args.includes('--json');
25348
25773
  const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');