sigmap 8.26.2 → 8.28.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
@@ -6566,6 +6566,7 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6566
6566
 
6567
6567
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
6568
6568
  const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
6569
+ const { stripComments, maskCode, readBalanced } = __require('./src/extractors/scan');
6569
6570
 
6570
6571
  /**
6571
6572
  * Extract signatures from JavaScript source code.
@@ -6585,16 +6586,29 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6585
6586
  const returnHints = buildReturnHints(src);
6586
6587
  const docHints = buildDocHints(src);
6587
6588
 
6588
- // Block comments are blanked newline-by-newline (non-newline chars spaces)
6589
- // so character offsets AND line numbers stay exact for anchors.
6590
- const stripped = src
6591
- .replace(/\/\/.*$/gm, '')
6592
- .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
6589
+ // stripComments is string-aware (a `//` inside a string literal survives);
6590
+ // maskCode additionally blanks string/template contents so every delimiter
6591
+ // found on it is structural. Both are length- and newline-preserving, so
6592
+ // offsets and line anchors align across all three views (#526).
6593
+ const stripped = stripComments(src);
6594
+ const masked = maskCode(src);
6595
+
6596
+ // Full params for a declaration whose `(` sits at openIdx: depth-matched
6597
+ // close over masked text; TEXT sliced from stripped so string defaults keep
6598
+ // their real content. Falls back to first-`)` when unbalanced (cap hit).
6599
+ const paramsFrom = (openIdx) => {
6600
+ const closeIdx = readBalanced(masked, openIdx);
6601
+ if (closeIdx === -1) {
6602
+ const naive = stripped.indexOf(')', openIdx);
6603
+ return { params: stripped.slice(openIdx + 1, naive === -1 ? openIdx + 1 : naive), closeIdx: naive };
6604
+ }
6605
+ return { params: stripped.slice(openIdx + 1, closeIdx), closeIdx };
6606
+ };
6593
6607
 
6594
- const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
6595
- // End line for a function whose match ends at `matchEnd` (before its body brace).
6608
+ const blockEndIdx = (bodyStart) => bodyStart + extractBlock(masked, bodyStart).length;
6609
+ // End line for a function whose params close just before `matchEnd`.
6596
6610
  const fnEndLine = (matchEnd, startLn) => {
6597
- const brace = stripped.indexOf('{', matchEnd);
6611
+ const brace = masked.indexOf('{', matchEnd);
6598
6612
  return brace !== -1 ? lineAt(stripped, blockEndIdx(brace + 1)) : startLn;
6599
6613
  };
6600
6614
 
@@ -6603,33 +6617,38 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6603
6617
  for (const m of stripped.matchAll(classRegex)) {
6604
6618
  const prefix = m[1] ? m[1].trim() + ' ' : '';
6605
6619
  const bodyStart = m.index + m[0].length;
6620
+ const blockEnd = blockEndIdx(bodyStart);
6606
6621
  sigs.push(`${prefix}class ${m[2]}`);
6607
- anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
6608
- const block = extractBlock(stripped, bodyStart);
6609
- for (const meth of extractClassMembers(block, returnHints)) {
6622
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEnd)]);
6623
+ const block = stripped.slice(bodyStart, blockEnd);
6624
+ const maskedBlock = masked.slice(bodyStart, blockEnd);
6625
+ for (const meth of extractClassMembers(block, maskedBlock, returnHints)) {
6610
6626
  sigs.push(` ${meth.text}`);
6611
6627
  anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
6612
6628
  }
6613
6629
  }
6614
6630
 
6615
6631
  // Exported named functions
6616
- for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
6632
+ for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(/gm)) {
6617
6633
  const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
6618
6634
  const retStr = formatReturnHint(returnHints.get(m[1]));
6619
6635
  const startLn = lineAt(stripped, m.index);
6620
- sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
6636
+ const { params, closeIdx } = paramsFrom(m.index + m[0].length - 1);
6637
+ sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(params)})${retStr}`);
6621
6638
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
6622
- anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
6639
+ anchors.push([startLn, fnEndLine(closeIdx + 1, startLn)]);
6623
6640
  }
6624
6641
 
6625
6642
  // Exported arrow functions
6626
- for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/gm)) {
6643
+ for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/gm)) {
6644
+ const { params, closeIdx } = paramsFrom(m.index + m[0].length - 1);
6645
+ if (closeIdx === -1 || !/^\s*=>/.test(masked.slice(closeIdx + 1, closeIdx + 40))) continue;
6627
6646
  const asyncKw = m[0].includes('async') ? 'async ' : '';
6628
6647
  const retStr = formatReturnHint(returnHints.get(m[1]));
6629
6648
  const startLn = lineAt(stripped, m.index);
6630
- sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
6649
+ sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(params)}) =>${retStr}`);
6631
6650
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
6632
- anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
6651
+ anchors.push([startLn, fnEndLine(closeIdx + 1, startLn)]);
6633
6652
  }
6634
6653
 
6635
6654
  // module.exports = { ... }
@@ -6644,13 +6663,14 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6644
6663
  }
6645
6664
 
6646
6665
  // Top-level named functions (non-exported)
6647
- for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
6666
+ for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(/gm)) {
6648
6667
  const asyncKw = m[0].startsWith('async') ? 'async ' : '';
6649
6668
  const retStr = formatReturnHint(returnHints.get(m[1]));
6650
6669
  const startLn = lineAt(stripped, m.index);
6651
- sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
6670
+ const { params, closeIdx } = paramsFrom(m.index + m[0].length - 1);
6671
+ sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(params)})${retStr}`);
6652
6672
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
6653
- anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
6673
+ anchors.push([startLn, fnEndLine(closeIdx + 1, startLn)]);
6654
6674
  }
6655
6675
 
6656
6676
  const withAnchors = sigs.map((s, i) => {
@@ -6672,21 +6692,31 @@ __factories["./src/extractors/javascript"] = function(module, exports) {
6672
6692
  return src.slice(startIndex, i - 1);
6673
6693
  }
6674
6694
 
6695
+ const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
6696
+
6675
6697
  // Returns members as { text, start, end } where start/end are char offsets
6676
6698
  // WITHIN `block` (end = the method's closing brace), so the caller can resolve
6677
- // per-method line anchors that span the method body.
6678
- function extractClassMembers(block, returnHints) {
6699
+ // per-method line anchors that span the method body. `maskedBlock` is the
6700
+ // same-offset maskCode slice used for balanced-delimiter scanning.
6701
+ function extractClassMembers(block, maskedBlock, returnHints) {
6679
6702
  const members = [];
6680
- for (const m of block.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(([^)]*)\)\s*\{/gm)) {
6703
+ for (const m of maskedBlock.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(/gm)) {
6681
6704
  if (/^_/.test(m[1])) continue;
6682
- const bodyStart = m.index + m[0].length; // just past the opening brace
6683
- const end = bodyStart + extractBlock(block, bodyStart).length;
6705
+ if (_CTRL_KEYWORDS.has(m[1])) continue;
6706
+ const openIdx = m.index + m[0].length - 1;
6707
+ const closeIdx = readBalanced(maskedBlock, openIdx);
6708
+ if (closeIdx === -1) continue;
6709
+ const braceMatch = maskedBlock.slice(closeIdx + 1, closeIdx + 40).match(/^\s*\{/);
6710
+ if (!braceMatch) continue;
6711
+ const params = block.slice(openIdx + 1, closeIdx);
6712
+ const bodyStart = closeIdx + 1 + braceMatch[0].length; // just past the opening brace
6713
+ const end = bodyStart + extractBlock(maskedBlock, bodyStart).length;
6684
6714
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
6685
- if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
6715
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(params)})`, start, end }); continue; }
6686
6716
  const isAsync = m[0].includes('async ') ? 'async ' : '';
6687
6717
  const isStatic = m[0].includes('static ') ? 'static ' : '';
6688
6718
  const retStr = formatReturnHint(returnHints.get(m[1]));
6689
- members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
6719
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(params)})${retStr}`, start, end });
6690
6720
  }
6691
6721
  return capMembersWithNotice(members, 8, 'methods');
6692
6722
  }
@@ -8274,6 +8304,101 @@ __factories["./src/extractors/scala"] = function(module, exports) {
8274
8304
 
8275
8305
  };
8276
8306
 
8307
+ // ── ./src/extractors/scan ──
8308
+ __factories["./src/extractors/scan"] = function(module, exports) {
8309
+
8310
+ /**
8311
+ * Shared tokenizer-grade scanning core (G4 increment 1, #526).
8312
+ *
8313
+ * Hand-rolled string/comment state + delimiter depth — generalizes the
8314
+ * masking in src/graph/call-graph.js (maskJs) and the balanced reader in the
8315
+ * R extractor. NOT a parser, NOT tree-sitter: three small, deterministic,
8316
+ * zero-dependency passes that let extractors find real declaration
8317
+ * boundaries instead of truncating at the first `)`.
8318
+ *
8319
+ * All transforms are length- and newline-preserving, so character offsets
8320
+ * and line anchors computed on the output align 1:1 with the input.
8321
+ */
8322
+
8323
+ /**
8324
+ * Blank comments only — string-aware, so `//` or `/*` INSIDE a string
8325
+ * literal survives (the naive regex strip corrupted e.g. `url = "https://x"`).
8326
+ * Comment bytes become spaces; newlines and everything else are preserved.
8327
+ * @param {string} src
8328
+ * @returns {string} same length, comments blanked
8329
+ */
8330
+ function stripComments(src) {
8331
+ const out = src.split('');
8332
+ const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
8333
+ let i = 0; const n = src.length;
8334
+ while (i < n) {
8335
+ const c = src[i], d = src[i + 1];
8336
+ if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
8337
+ if (c === '/' && d === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; j = Math.min(n, j + 2); blank(i, j); i = j; continue; }
8338
+ if (c === '"' || c === "'" || c === '`') {
8339
+ let j = i + 1;
8340
+ while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c) break; if (c !== '`' && src[j] === '\n') break; j++; }
8341
+ i = Math.min(n, j + 1); continue;
8342
+ }
8343
+ i++;
8344
+ }
8345
+ return out.join('');
8346
+ }
8347
+
8348
+ /**
8349
+ * Blank comments AND string/template contents (quotes included) — the
8350
+ * boundary-scanning surface: delimiters found here are always structural.
8351
+ * @param {string} src
8352
+ * @returns {string} same length, comments + strings blanked
8353
+ */
8354
+ function maskCode(src) {
8355
+ const out = src.split('');
8356
+ const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
8357
+ let i = 0; const n = src.length;
8358
+ while (i < n) {
8359
+ const c = src[i], d = src[i + 1];
8360
+ if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
8361
+ if (c === '/' && d === '*') { let j = i + 2; while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j++; j = Math.min(n, j + 2); blank(i, j); i = j; continue; }
8362
+ if (c === '"' || c === "'" || c === '`') {
8363
+ let j = i + 1;
8364
+ while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c) break; if (c !== '`' && src[j] === '\n') break; j++; }
8365
+ j = Math.min(n, j + 1); blank(i, j); i = j; continue;
8366
+ }
8367
+ i++;
8368
+ }
8369
+ return out.join('');
8370
+ }
8371
+
8372
+ /**
8373
+ * Index of the delimiter that closes the one open at `openIdx`, matched by
8374
+ * depth over MASKED text (strings/comments already blanked, so every
8375
+ * delimiter seen is structural). -1 when unbalanced within the cap.
8376
+ * @param {string} masked output of maskCode
8377
+ * @param {number} openIdx index of the opening delimiter
8378
+ * @param {string} [open='(']
8379
+ * @param {string} [close=')']
8380
+ * @param {number} [cap=4000] scan ceiling in chars
8381
+ * @returns {number}
8382
+ */
8383
+ function readBalanced(masked, openIdx, open = '(', close = ')', cap = 4000) {
8384
+ if (masked[openIdx] !== open) return -1;
8385
+ let depth = 1;
8386
+ const end = Math.min(masked.length, openIdx + cap);
8387
+ for (let i = openIdx + 1; i < end; i++) {
8388
+ const ch = masked[i];
8389
+ if (ch === open) depth++;
8390
+ else if (ch === close) {
8391
+ depth--;
8392
+ if (depth === 0) return i;
8393
+ }
8394
+ }
8395
+ return -1;
8396
+ }
8397
+
8398
+ module.exports = { stripComments, maskCode, readBalanced };
8399
+
8400
+ };
8401
+
8277
8402
  // ── ./src/extractors/shell ──
8278
8403
  __factories["./src/extractors/shell"] = function(module, exports) {
8279
8404
 
@@ -8740,6 +8865,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8740
8865
 
8741
8866
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
8742
8867
  const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
8868
+ const { stripComments, maskCode, readBalanced } = __require('./src/extractors/scan');
8743
8869
 
8744
8870
  /**
8745
8871
  * Extract signatures from TypeScript source code.
@@ -8761,15 +8887,25 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8761
8887
  // anchors are applied once at return.
8762
8888
  const anchors = [];
8763
8889
 
8764
- // Strip comments to simplify matching. Block comments are blanked
8765
- // newline-by-newline (non-newline chars spaces) so character offsets AND
8766
- // line numbers stay exact. Line comments preserve their trailing newline.
8767
- const stripped = src
8768
- .replace(/\/\/.*$/gm, '')
8769
- .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
8890
+ // stripComments is string-aware (a `//` inside a string literal survives);
8891
+ // maskCode additionally blanks string/template contents so every delimiter
8892
+ // found on it is structural. Both are length- and newline-preserving, so
8893
+ // offsets and line anchors align across all three views (#526).
8894
+ const stripped = stripComments(src);
8895
+ const masked = maskCode(src);
8896
+
8897
+ // Full params for a declaration whose `(` sits at openIdx (see javascript.js).
8898
+ const paramsFrom = (openIdx) => {
8899
+ const closeIdx = readBalanced(masked, openIdx);
8900
+ if (closeIdx === -1) {
8901
+ const naive = stripped.indexOf(')', openIdx);
8902
+ return { params: stripped.slice(openIdx + 1, naive === -1 ? openIdx + 1 : naive), closeIdx: naive };
8903
+ }
8904
+ return { params: stripped.slice(openIdx + 1, closeIdx), closeIdx };
8905
+ };
8770
8906
 
8771
8907
  // Index of the closing brace for a block whose body starts at bodyStart.
8772
- const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
8908
+ const blockEndIdx = (bodyStart) => bodyStart + extractBlock(masked, bodyStart).length;
8773
8909
 
8774
8910
  // Exported interfaces
8775
8911
  for (const m of stripped.matchAll(/^export\s+interface\s+(\w+)(?:<[^{]*>)?\s*(?:extends\s+[^{]+)?\{/gm)) {
@@ -8804,10 +8940,12 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8804
8940
  const prefix = m[1] ? 'export ' : '';
8805
8941
  const abs = m[2] ? 'abstract ' : '';
8806
8942
  const bodyStart = m.index + m[0].length;
8943
+ const blockEnd = blockEndIdx(bodyStart);
8807
8944
  sigs.push(`${prefix}${abs}class ${m[3]}`);
8808
- anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
8809
- const block = extractBlock(stripped, bodyStart);
8810
- const methods = extractClassMembers(block);
8945
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEnd)]);
8946
+ const block = stripped.slice(bodyStart, blockEnd);
8947
+ const maskedBlock = masked.slice(bodyStart, blockEnd);
8948
+ const methods = extractClassMembers(block, maskedBlock);
8811
8949
  for (const meth of methods) {
8812
8950
  sigs.push(` ${meth.text}`);
8813
8951
  anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
@@ -8815,13 +8953,19 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8815
8953
  }
8816
8954
 
8817
8955
  // Exported top-level functions (not methods)
8818
- for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{]+)?\s*\{/gm)) {
8956
+ for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(/gm)) {
8957
+ const { params: rawParams, closeIdx } = paramsFrom(m.index + m[0].length - 1);
8958
+ if (closeIdx === -1) continue;
8959
+ // Declaration shape check + return-type capture, mirroring the old
8960
+ // `\)(?:\s*:\s*[^{]+)?\s*\{` tail against the text after the real close.
8961
+ const tail = masked.slice(closeIdx + 1, closeIdx + 200).match(/^(\s*:\s*[^{]+?)?\s*\{/);
8962
+ if (!tail) continue;
8819
8963
  const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
8820
- const params = normalizeParams(m[2]);
8821
- const retMatch = m[0].match(/\)\s*:\s*([^{]+)\s*\{/);
8822
- const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 30) : '';
8964
+ const params = normalizeParams(rawParams);
8965
+ const retRaw = tail[1] ? stripped.slice(closeIdx + 1, closeIdx + 1 + tail[1].length).replace(/^\s*:\s*/, '') : '';
8966
+ const retType = retRaw ? retRaw.trim().replace(/\s+/g, ' ').slice(0, 30) : '';
8823
8967
  const retStr = retType ? ` → ${retType}` : '';
8824
- const bodyStart = m.index + m[0].length;
8968
+ const bodyStart = closeIdx + 1 + tail[0].length;
8825
8969
  sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
8826
8970
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
8827
8971
  anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
@@ -8844,15 +8988,21 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8844
8988
  }
8845
8989
 
8846
8990
  // Exported arrow functions / const functions
8847
- for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=>{]+)?\s*=>/gm)) {
8991
+ for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(/gm)) {
8992
+ const { params: rawParams, closeIdx } = paramsFrom(m.index + m[0].length - 1);
8993
+ if (closeIdx === -1) continue;
8994
+ // Arrow shape check, mirroring the old `\)\s*(?::\s*[^=>{]+)?\s*=>` tail.
8995
+ const tail = masked.slice(closeIdx + 1, closeIdx + 200).match(/^\s*(?::\s*[^=>{]+)?\s*=>/);
8996
+ if (!tail) continue;
8848
8997
  const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
8849
- const params = normalizeParams(m[2]);
8998
+ const params = normalizeParams(rawParams);
8850
8999
  sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
8851
9000
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
8852
- const bodyStart = stripped.indexOf('{', m.index + m[0].length);
9001
+ const matchEnd = closeIdx + 1 + tail[0].length;
9002
+ const bodyStart = masked.indexOf('{', matchEnd);
8853
9003
  const endLn = bodyStart !== -1
8854
9004
  ? lineAt(stripped, blockEndIdx(bodyStart + 1))
8855
- : lineAt(stripped, m.index + m[0].length);
9005
+ : lineAt(stripped, matchEnd);
8856
9006
  anchors.push([lineAt(stripped, m.index), endLn]);
8857
9007
 
8858
9008
  // Hooks: capture compact return object shape for use* functions.
@@ -8920,6 +9070,7 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8920
9070
  // Returns members as { text, start, end } where start/end are char offsets
8921
9071
  // WITHIN `block`, so the caller can resolve member line anchors.
8922
9072
  function extractInterfaceMembers(block) {
9073
+ const maskedBlock = maskCode(block);
8923
9074
  const members = [];
8924
9075
  for (const m of block.matchAll(/^\s+(readonly\s+)?(\w+)(\??):\s*([^;]+);/gm)) {
8925
9076
  const readonly = m[1] ? 'readonly ' : '';
@@ -8928,9 +9079,12 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8928
9079
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
8929
9080
  members.push({ text: `${readonly}${m[2]}${optional}: ${typeStr}`, start, end: m.index + m[0].length });
8930
9081
  }
8931
- for (const m of block.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)\s*:/gm)) {
9082
+ for (const m of maskedBlock.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(/gm)) {
9083
+ const openIdx = m.index + m[0].length - 1;
9084
+ const closeIdx = readBalanced(maskedBlock, openIdx);
9085
+ if (closeIdx === -1 || !/^\s*:/.test(maskedBlock.slice(closeIdx + 1, closeIdx + 40))) continue;
8932
9086
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
8933
- members.push({ text: `${m[1]}(${normalizeParams(m[2])})`, start, end: m.index + m[0].length });
9087
+ members.push({ text: `${m[1]}(${normalizeParams(block.slice(openIdx + 1, closeIdx))})`, start, end: closeIdx + 1 });
8934
9088
  }
8935
9089
  return capMembersWithNotice(members, 8, 'members');
8936
9090
  }
@@ -8940,30 +9094,66 @@ __factories["./src/extractors/typescript"] = function(module, exports) {
8940
9094
  // Returns members as { text, start, end } where start/end are char offsets
8941
9095
  // WITHIN `block` (end = the method's closing brace), so the caller can resolve
8942
9096
  // per-method line anchors that span the method body.
8943
- function extractClassMembers(block) {
9097
+ function extractClassMembers(block, maskedBlock) {
9098
+ const masked = maskedBlock || maskCode(block);
8944
9099
  const members = [];
8945
9100
  // Public methods (skip private/protected/_ prefixed and control-flow keywords)
8946
- const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{;]+)?\s*\{/gm;
8947
- for (const m of block.matchAll(methodRe)) {
9101
+ const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(/gm;
9102
+ for (const m of masked.matchAll(methodRe)) {
8948
9103
  if (_CTRL_KEYWORDS.has(m[1])) continue;
8949
9104
  if (/^(private|protected|_)/.test(m[1])) continue;
8950
- const bodyStart = m.index + m[0].length; // just past the opening brace
8951
- const end = bodyStart + extractBlock(block, bodyStart).length;
9105
+ const openIdx = m.index + m[0].length - 1;
9106
+ const closeIdx = readBalanced(masked, openIdx);
9107
+ if (closeIdx === -1) continue;
9108
+ // Declaration tail check + return-type capture, mirroring the old
9109
+ // `\)(?:\s*:\s*[^{;]+)?\s*\{` shape against the text after the real close.
9110
+ const tail = masked.slice(closeIdx + 1, closeIdx + 200).match(/^(\s*:\s*[^{;]+?)?\s*\{/);
9111
+ if (!tail) continue;
9112
+ const params = block.slice(openIdx + 1, closeIdx);
9113
+ const bodyStart = closeIdx + 1 + tail[0].length; // just past the opening brace
9114
+ const end = bodyStart + extractBlock(masked, bodyStart).length;
8952
9115
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
8953
- if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
9116
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(params)})`, start, end }); continue; }
8954
9117
  const isAsync = m[0].includes('async ') ? 'async ' : '';
8955
9118
  const isStatic = m[0].includes('static ') ? 'static ' : '';
8956
- const retMatch = m[0].match(/\)\s*:\s*([^{;]+)\s*\{/);
8957
- const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 20) : '';
9119
+ const retRaw = tail[1] ? block.slice(closeIdx + 1, closeIdx + 1 + tail[1].length).replace(/^\s*:\s*/, '') : '';
9120
+ const retType = retRaw ? retRaw.trim().replace(/\s+/g, ' ').slice(0, 20) : '';
8958
9121
  const retStr = retType ? ` → ${retType}` : '';
8959
- members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
9122
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(params)})${retStr}`, start, end });
8960
9123
  }
8961
9124
  return capMembersWithNotice(members, 8, 'methods');
8962
9125
  }
8963
9126
 
8964
9127
  function normalizeParams(params) {
8965
9128
  if (!params) return '';
8966
- return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
9129
+ const compact = params.trim().replace(/\s+/g, ' ');
9130
+ // Strip `: type` annotations at top level only — nested delimiters in types
9131
+ // (e.g. `cb: (x: number) => void`, `m: Map<string, X>`) are consumed with
9132
+ // their annotation instead of truncating at the first `)` or `,` (#526).
9133
+ let out = '';
9134
+ let depth = 0;
9135
+ let inType = false;
9136
+ let quote = null;
9137
+ for (let i = 0; i < compact.length; i++) {
9138
+ const ch = compact[i];
9139
+ if (quote) {
9140
+ if (ch === '\\') { if (!inType) out += ch + (compact[i + 1] || ''); i++; continue; }
9141
+ if (ch === quote) quote = null;
9142
+ if (!inType) out += ch;
9143
+ continue;
9144
+ }
9145
+ if (ch === '"' || ch === "'" || ch === '`') { quote = ch; if (!inType) out += ch; continue; }
9146
+ if (ch === '(' || ch === '[' || ch === '{' || ch === '<') depth++;
9147
+ else if (ch === ')' || ch === ']' || ch === '}' || (ch === '>' && compact[i - 1] !== '=')) depth--;
9148
+ if (inType) {
9149
+ if (depth <= 0 && ch === ',') { inType = false; out += ch; }
9150
+ else if (depth <= 0 && ch === '=' && compact[i + 1] !== '>') { inType = false; out += ' ='; }
9151
+ continue;
9152
+ }
9153
+ if (depth === 0 && ch === ':') { inType = true; continue; }
9154
+ out += ch;
9155
+ }
9156
+ return out.replace(/\s*,\s*/g, ', ').replace(/\s+/g, ' ').trim();
8967
9157
  }
8968
9158
 
8969
9159
  // First prose sentence of the JSDoc block immediately preceding an exported
@@ -15195,7 +15385,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
15195
15385
 
15196
15386
  const SERVER_INFO = {
15197
15387
  name: 'sigmap',
15198
- version: '8.26.2',
15388
+ version: '8.28.0',
15199
15389
  description: 'SigMap MCP server — code signatures on demand',
15200
15390
  };
15201
15391
 
@@ -19389,6 +19579,190 @@ __factories["./src/util/truncate"] = function(module, exports) {
19389
19579
 
19390
19580
  };
19391
19581
 
19582
+ // ── ./src/verify/arity ──
19583
+ __factories["./src/verify/arity"] = function(module, exports) {
19584
+
19585
+ /**
19586
+ * Arity-checked verification (D1, #529).
19587
+ *
19588
+ * With JS/TS params exact (v8.27 balanced scanner) and Python params from the
19589
+ * AST, the signature index carries real parameter lists — so verification can
19590
+ * check not just "does this function exist" but "is this call's argument
19591
+ * count plausible". Deliberately conservative: only uniquely-resolved,
19592
+ * non-variadic, top-level functions from exact-param languages are checked,
19593
+ * and dotted method calls are never flagged.
19594
+ */
19595
+
19596
+ const path = require('path');
19597
+ const { maskCode, readBalanced } = __require('./src/extractors/scan');
19598
+
19599
+ // Files whose signature params are exact (JS/TS via scan.js, Python via AST).
19600
+ const EXACT_PARAM_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py']);
19601
+
19602
+ const CTRL_KEYWORDS = new Set([
19603
+ 'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'await',
19604
+ 'do', 'else', 'try', 'finally', 'new', 'in', 'of', 'not', 'and', 'or',
19605
+ 'print', 'super', 'this',
19606
+ ]);
19607
+
19608
+ // Top-level callable sig shapes (indented member sigs are excluded on purpose
19609
+ // — method calls are dotted in answers and dotted calls are skipped anyway).
19610
+ const CALLABLE_RES = [
19611
+ /^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/,
19612
+ /^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(/,
19613
+ /^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/,
19614
+ ];
19615
+
19616
+ /** Strip the ` :start-end` anchor and ` # hint` tail from a sig line. */
19617
+ function cleanSig(sig) {
19618
+ return String(sig).replace(/\s{2}#\s.*$/, '').replace(/\s*:\d+(?:-\d+)?\s*$/, '');
19619
+ }
19620
+
19621
+ /**
19622
+ * Parse a parameter-list string into an arity range.
19623
+ * Depth- and quote-aware top-level comma split; `=` defaults and trailing `?`
19624
+ * lower `min`; `...rest` / `*args` / `**kwargs` mark the signature variadic;
19625
+ * destructuring patterns count as one parameter.
19626
+ * @param {string} paramText text between the signature's parens
19627
+ * @returns {{ min: number, max: number, variadic: boolean }}
19628
+ */
19629
+ function parseParams(paramText) {
19630
+ const text = String(paramText || '').trim();
19631
+ if (!text) return { min: 0, max: 0, variadic: false };
19632
+ const masked = maskCode(text);
19633
+ const pieces = [];
19634
+ let depth = 0;
19635
+ let start = 0;
19636
+ for (let i = 0; i < masked.length; i++) {
19637
+ const ch = masked[i];
19638
+ if (ch === '(' || ch === '[' || ch === '{') depth++;
19639
+ else if (ch === ')' || ch === ']' || ch === '}') depth--;
19640
+ else if (ch === ',' && depth === 0) { pieces.push({ raw: text.slice(start, i), masked: masked.slice(start, i) }); start = i + 1; }
19641
+ }
19642
+ pieces.push({ raw: text.slice(start), masked: masked.slice(start) });
19643
+
19644
+ let min = 0;
19645
+ let max = 0;
19646
+ let variadic = false;
19647
+ for (const piece of pieces) {
19648
+ const p = piece.raw.trim();
19649
+ if (!p) continue;
19650
+ if (/^(\.\.\.|\*)/.test(p)) { variadic = true; continue; }
19651
+ max++;
19652
+ // Optional: a top-level `=` default (scan the masked piece at depth 0) or
19653
+ // a `?`-suffixed name (TS optional, survives type stripping as `x?`).
19654
+ let d = 0;
19655
+ let optional = /^[A-Za-z_$][\w$]*\s*\?$/.test(p);
19656
+ const pm = piece.masked;
19657
+ for (let i = 0; i < pm.length && !optional; i++) {
19658
+ const ch = pm[i];
19659
+ if (ch === '(' || ch === '[' || ch === '{') d++;
19660
+ else if (ch === ')' || ch === ']' || ch === '}') d--;
19661
+ else if (ch === '=' && d === 0 && pm[i + 1] !== '>' && pm[i - 1] !== '=' && pm[i - 1] !== '!' && pm[i - 1] !== '<' && pm[i - 1] !== '>') optional = true;
19662
+ }
19663
+ if (!optional) min++;
19664
+ }
19665
+ return { min, max, variadic };
19666
+ }
19667
+
19668
+ /**
19669
+ * Build a per-name arity index from a SigMap signature index.
19670
+ * Only top-level callables from exact-param languages are included; a name
19671
+ * whose signatures disagree across files is marked ambiguous (never checked).
19672
+ * @param {Map<string, string[]>} sigIndex Map<file, sigs[]>
19673
+ * @returns {Map<string, { min, max, variadic, file, sig } | 'ambiguous'>}
19674
+ */
19675
+ function buildArityIndex(sigIndex) {
19676
+ const index = new Map();
19677
+ if (!sigIndex || !(sigIndex instanceof Map)) return index;
19678
+ for (const [file, sigs] of sigIndex.entries()) {
19679
+ if (!EXACT_PARAM_EXTS.has(path.extname(file))) continue;
19680
+ for (const sig of sigs || []) {
19681
+ const cleaned = cleanSig(sig);
19682
+ let name = null;
19683
+ for (const re of CALLABLE_RES) {
19684
+ const m = cleaned.match(re);
19685
+ if (m) { name = m[1]; break; }
19686
+ }
19687
+ if (!name) continue;
19688
+ const openIdx = cleaned.indexOf('(', cleaned.indexOf(name));
19689
+ if (openIdx === -1) continue;
19690
+ const masked = maskCode(cleaned);
19691
+ const closeIdx = readBalanced(masked, openIdx);
19692
+ if (closeIdx === -1) continue;
19693
+ const arity = parseParams(cleaned.slice(openIdx + 1, closeIdx));
19694
+ const entry = { ...arity, file, sig: cleaned.trim() };
19695
+ const existing = index.get(name);
19696
+ if (existing === undefined) index.set(name, entry);
19697
+ else if (existing === 'ambiguous') continue;
19698
+ else if (existing.min !== entry.min || existing.max !== entry.max || existing.variadic !== entry.variadic) {
19699
+ index.set(name, 'ambiguous');
19700
+ }
19701
+ }
19702
+ }
19703
+ return index;
19704
+ }
19705
+
19706
+ /**
19707
+ * Extract call sites with argument counts from answer code.
19708
+ * Dotted/property calls and keyword-preceded definitions are skipped for
19709
+ * precision; nested calls and comma-containing strings count correctly
19710
+ * because the scan is over masked text.
19711
+ * @param {string} code
19712
+ * @returns {{ name: string, args: number, line: number }[]}
19713
+ */
19714
+ function extractCallArgCounts(code) {
19715
+ const src = String(code || '');
19716
+ const masked = maskCode(src);
19717
+ const calls = [];
19718
+ const re = /([A-Za-z_$][\w$]*)\s*\(/g;
19719
+ let m;
19720
+ while ((m = re.exec(masked)) !== null) {
19721
+ const name = m[1];
19722
+ if (CTRL_KEYWORDS.has(name)) continue;
19723
+ let k = m.index - 1;
19724
+ while (k >= 0 && (masked[k] === ' ' || masked[k] === '\t')) k--;
19725
+ if (k >= 0 && (masked[k] === '.' || masked[k] === '$')) continue;
19726
+ const before = masked.slice(Math.max(0, m.index - 12), m.index);
19727
+ if (/(?:function|def|class|new)\s+$/.test(before)) continue;
19728
+ const openIdx = m.index + m[0].length - 1;
19729
+ const closeIdx = readBalanced(masked, openIdx);
19730
+ if (closeIdx === -1) continue;
19731
+ const inner = masked.slice(openIdx + 1, closeIdx);
19732
+ let args = 0;
19733
+ // Emptiness is judged on the ORIGINAL text — masking blanks string
19734
+ // contents, so `f("a,b")` would otherwise look like zero arguments.
19735
+ if (src.slice(openIdx + 1, closeIdx).trim()) {
19736
+ args = 1;
19737
+ let depth = 0;
19738
+ for (let i = 0; i < inner.length; i++) {
19739
+ const ch = inner[i];
19740
+ if (ch === '(' || ch === '[' || ch === '{') depth++;
19741
+ else if (ch === ')' || ch === ']' || ch === '}') depth--;
19742
+ else if (ch === ',' && depth === 0) args++;
19743
+ }
19744
+ }
19745
+ calls.push({ name, args, line: src.slice(0, m.index).split('\n').length });
19746
+ }
19747
+ return calls;
19748
+ }
19749
+
19750
+ /**
19751
+ * Check one call against the arity index.
19752
+ * @returns {null | { min, max, variadic, file, sig }} the offended entry, or null when fine/unknowable
19753
+ */
19754
+ function checkArity(name, argCount, arityIndex) {
19755
+ const entry = arityIndex.get(name);
19756
+ if (!entry || entry === 'ambiguous') return null;
19757
+ if (entry.variadic) return argCount < entry.min ? entry : null;
19758
+ if (argCount < entry.min || argCount > entry.max) return entry;
19759
+ return null;
19760
+ }
19761
+
19762
+ module.exports = { parseParams, buildArityIndex, extractCallArgCounts, checkArity, cleanSig, EXACT_PARAM_EXTS };
19763
+
19764
+ };
19765
+
19392
19766
  // ── ./src/verify/closest-match ──
19393
19767
  __factories["./src/verify/closest-match"] = function(module, exports) {
19394
19768
 
@@ -19563,6 +19937,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
19563
19937
  const parsers = __require('./src/verify/parsers');
19564
19938
  const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
19565
19939
  const { buildLibraryIndex } = __require('./src/verify/lib-index');
19940
+ const { buildArityIndex, extractCallArgCounts, checkArity } = __require('./src/verify/arity');
19566
19941
 
19567
19942
  // A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
19568
19943
  // a tests/__tests__ directory). Used to flag fake-test-file separately.
@@ -19618,9 +19993,11 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
19618
19993
  const set = new Set();
19619
19994
  let fileKeys = [];
19620
19995
  let symbolCandidates = [];
19996
+ let sigIndex = null;
19621
19997
  try {
19622
19998
  const { buildSigIndex } = __require('./src/retrieval/ranker');
19623
19999
  const idx = buildSigIndex(cwd);
20000
+ sigIndex = idx;
19624
20001
  fileKeys = [...idx.keys()];
19625
20002
  for (const sigs of idx.values()) {
19626
20003
  for (const sig of sigs) {
@@ -19631,7 +20008,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
19631
20008
  }
19632
20009
  symbolCandidates = buildSymbolCandidates(idx);
19633
20010
  } catch (_) {}
19634
- return { set, fileKeys, symbolCandidates };
20011
+ return { set, fileKeys, symbolCandidates, sigIndex };
19635
20012
  }
19636
20013
 
19637
20014
  /** Load declared dependency names from package.json. */
@@ -19720,6 +20097,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
19720
20097
  let fileBasenames = opts.fileBasenames;
19721
20098
  let symbolCandidates = opts.symbolCandidates || [];
19722
20099
  let fileCandidates = opts.fileCandidates || [];
20100
+ let arityIndex = opts.arityIndex || null;
19723
20101
  if (!symbolSet) {
19724
20102
  const built = buildSymbolSet(cwd);
19725
20103
  symbolSet = built.set;
@@ -19728,6 +20106,9 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
19728
20106
  ));
19729
20107
  symbolCandidates = built.symbolCandidates;
19730
20108
  fileCandidates = built.fileKeys;
20109
+ if (!arityIndex && built.sigIndex) {
20110
+ try { arityIndex = buildArityIndex(built.sigIndex); } catch (_) {}
20111
+ }
19731
20112
  }
19732
20113
  if (!fileBasenames) fileBasenames = new Set();
19733
20114
 
@@ -19846,6 +20227,32 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
19846
20227
  }
19847
20228
  }
19848
20229
 
20230
+ // 3b. arity-mismatch (D1, #529) — a call to a KNOWN repo function whose
20231
+ // argument count falls outside the signature's [min, max]. Conservative by
20232
+ // construction: only uniquely-resolved, top-level functions from
20233
+ // exact-param languages (JS/TS via the balanced scanner, Python via AST);
20234
+ // variadic signatures only flag too-few; dotted calls never flag.
20235
+ if (arityIndex && arityIndex.size > 0) {
20236
+ for (const block of parsers.extractCodeBlocks(answerText)) {
20237
+ if (block.lang && !/^(js|jsx|ts|tsx|javascript|typescript|python|py)$/i.test(block.lang)) continue;
20238
+ for (const call of extractCallArgCounts(block.content)) {
20239
+ if (!symbolSet.has(call.name)) continue; // unknown symbols stay fake-symbol territory
20240
+ const entry = checkArity(call.name, call.args, arityIndex);
20241
+ if (!entry) continue;
20242
+ const range = entry.variadic ? `at least ${entry.min}`
20243
+ : (entry.min === entry.max ? String(entry.max) : `${entry.min}–${entry.max}`);
20244
+ add({
20245
+ type: 'arity-mismatch',
20246
+ value: `${call.name}(${call.args} args)`,
20247
+ line: block.line + call.line - 1,
20248
+ message: `${call.name}() called with ${call.args} argument(s) — repo signature takes ${range}`,
20249
+ confidence: 'medium',
20250
+ suggestion: `${entry.sig} (${entry.file})`,
20251
+ });
20252
+ }
20253
+ }
20254
+ }
20255
+
19849
20256
  // 4. fake-npm-script
19850
20257
  if (hasPkg && scripts.size > 0) {
19851
20258
  for (const { name, line } of parsers.extractNpmScripts(answerText)) {
@@ -20807,7 +21214,7 @@ function __tryGit(args, opts = {}) {
20807
21214
  catch (_) { return ''; }
20808
21215
  }
20809
21216
 
20810
- const VERSION = '8.26.2';
21217
+ const VERSION = '8.28.0';
20811
21218
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
20812
21219
 
20813
21220
  function requireSourceOrBundled(key) {