sigmap 8.26.2 → 8.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,19 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.27.0] — 2026-08-18
14
+
15
+ Minor release — **"Tokenizer Core I" (v8.27, G4 increment 1)**: the hand-rolled balanced scanner lands and JS/TS extraction stops truncating at the first `)` — the first slice of the v9.0 grounding track and the stated precondition for arity-checked verification (D1).
16
+
17
+ ### Added
18
+ - **Shared balanced scanner (#526, PR #527):** new `src/extractors/scan.js` — three deterministic, length- and newline-preserving passes: `stripComments` (string-aware — `//` inside a string literal survives, fixing the `url = "https://x"` corruption class), `maskCode` (comments + string/template contents blanked so every delimiter found is structural), and `readBalanced` (depth-matched close index, capped). Generalizes the repo's own `maskJs`/`readBalancedParens` patterns; explicitly NOT tree-sitter — zero dependencies, deterministic by construction.
19
+
20
+ ### Changed
21
+ - **JS/TS extraction is balanced (#526, PR #527):** every `\(([^)]*)\)` param capture in `javascript.js`/`typescript.js` (top-level functions, arrow consts, class members, TS interface methods) is replaced with match-to-`(` + depth-matched close — `f(a, b = g(x))`, `c = ")"`, and destructuring-brace params now capture fully, with body/anchor endpoints computed from the real close index. JS class members gain the TS control-keyword guard. TS `normalizeParams` strips types depth- and quote-aware: `(cb: (x: number) => void)` → `cb`, generics consumed with the annotation, defaults after typed params preserved (`m: Map<K,V> = new Map()` → `m = new Map()`). Shipped behind the byte-identical-or-better gate: the full existing suite passes unchanged; 12 new adversarial tests (135 test files). `KNOWN_LIMITATIONS.md` updated — nested-paren gap fixed for JS/TS, the 9 remaining Tier-2 languages stay listed for later G4 increments. Extractor-module count honestly 42 → 43 (`scan` registered as a helper so the language count stays 33).
22
+ - **Measured trade, documented honestly (v8.20 precedent):** fuller param text adds lexical tokens that dilute BM25 on the file-discovery corpus — retrieval hit@5 **82.2% → 81.1%** (one task partial → wrong), honest lift 1.76× → 1.73×, token reduction unchanged at 96.8%. **Gate-verified real** (`validate:benchmark-determinism` identical cross-suite), not harness noise. The point of the trade: params are now *exact*, which is what D1 arity-checked verification consumes — correctness over corpus points, per the grounding-first north star.
23
+
24
+ ---
25
+
13
26
  ## [8.26.2] — 2026-08-18
14
27
 
15
28
  Patch release — **"Honest Harness" (#522)**: the benchmark suite's cross-suite instability is diagnosed, fixed, and gated — and the headline retrieval number honestly resettles on the now-stable harness.
package/README.md CHANGED
@@ -59,10 +59,10 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
59
59
 
60
60
  **Proof it pays off** (full benchmark below):
61
61
  <!--SM:whyMetrics-->
62
- - **82.2% hit@5** — right file in top 5 results (vs 44.0% single-shot grep baseline — 1.76× lift)
62
+ - **81.1% hit@5** — right file in top 5 results (vs 44.0% single-shot grep baseline — 1.73× lift)
63
63
  - **96.8% token reduction** — average across 21 real repos
64
64
  - **64.8% task-success proxy** — modeled from retrieval tiers, not measured LLM sessions
65
- - **1.53 prompts per task** — down from 2.84 (46.1% fewer retries, modeled)
65
+ - **1.54 prompts per task** — down from 2.84 (45.7% fewer retries, modeled)
66
66
  <!--/SM:whyMetrics-->
67
67
  - **<!--SM:languages-->33<!--/SM:languages--> languages supported** — TypeScript, Python, Go, Rust, Java, R, and more
68
68
  - **No vendor lock-in** — works with any AI assistant or local LLM
@@ -98,7 +98,7 @@ sigmap verify answer.md --report # standalone red/amber/green HTML report
98
98
  | Without SigMap | With SigMap |
99
99
  |---|---|
100
100
  | ❌ Non-reproducible agent guesses | ✅ Deterministic map — same input, same output, every time |
101
- | ❌ "Trust me" AI answers | ✅ Grounded — right file in context <!--SM:hitWhole-->82%<!--/SM:hitWhole--> of the time, every symbol on a real line anchor |
101
+ | ❌ "Trust me" AI answers | ✅ Grounded — right file in context <!--SM:hitWhole-->81%<!--/SM:hitWhole--> of the time, every symbol on a real line anchor |
102
102
  | ❌ Embeddings / vector DB required | ✅ Zero deps, no infra, fully offline |
103
103
 
104
104
  ---
@@ -122,12 +122,12 @@ Ask → Rank → Context → Validate → Judge → Learn
122
122
 
123
123
  <!--SM:benchmarkBlock-->
124
124
  ```
125
- Benchmark : sigmap-v8.26-main (21 repositories, including R language)
125
+ Benchmark : sigmap-v8.27-main (21 repositories, including R language)
126
126
  Date : 2026-08-18
127
127
 
128
- Hit@5 : 82.2% (grep-agent baseline 44.0% — 1.76× lift)
128
+ Hit@5 : 81.1% (grep-agent baseline 44.0% — 1.73× lift)
129
129
  Token reduction: 96.8% (across 21 repos)
130
- Prompt reduction : 46.1% (2.84 → 1.53 prompts per task, modeled)
130
+ Prompt reduction : 45.7% (2.84 → 1.54 prompts per task, modeled)
131
131
  Task success : 64.8% (proxy — modeled from retrieval tiers)
132
132
  Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift, Ruby, PHP, Scala, Kotlin, and more)
133
133
  ```
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.27.0',
15199
15389
  description: 'SigMap MCP server — code signatures on demand',
15200
15390
  };
15201
15391
 
@@ -20807,7 +20997,7 @@ function __tryGit(args, opts = {}) {
20807
20997
  catch (_) { return ''; }
20808
20998
  }
20809
20999
 
20810
- const VERSION = '8.26.2';
21000
+ const VERSION = '8.27.0';
20811
21001
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
20812
21002
 
20813
21003
  function requireSourceOrBundled(key) {
package/llms-full.txt CHANGED
@@ -11,20 +11,20 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.26.2 | Benchmark: sigmap-v8.26-main (2026-08-18)
14
+ # Version: 8.27.0 | Benchmark: sigmap-v8.27-main (2026-08-18)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
18
18
  ---
19
19
 
20
- ## Core metrics (benchmark: sigmap-v8.26-main, 2026-08-18)
20
+ ## Core metrics (benchmark: sigmap-v8.27-main, 2026-08-18)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
24
- | Retrieval hit@5 | 44.0% (single-shot grep) | 82.2% (1.76× lift) |
24
+ | Retrieval hit@5 | 44.0% (single-shot grep) | 81.1% (1.73× lift) |
25
25
  | Token reduction | — | 96.8% average |
26
26
  | Task-success proxy (modeled) | — | 64.8% |
27
- | Prompts per task | 2.84 | 1.53 (46.1% fewer) |
27
+ | Prompts per task | 2.84 | 1.54 (45.7% fewer) |
28
28
  | Supported languages | — | 33 |
29
29
  | MCP tools | — | 21 |
30
30
  | npm runtime dependencies | — | 0 |
package/llms.txt CHANGED
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
11
11
  effect), with no LLM calls, embeddings, or vector database. Works with Claude,
12
12
  Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
13
13
 
14
- # Version: 8.26.2 | Benchmark: sigmap-v8.26-main (2026-08-18)
14
+ # Version: 8.27.0 | Benchmark: sigmap-v8.27-main (2026-08-18)
15
15
  # Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
16
16
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
17
17
 
@@ -23,12 +23,12 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
23
23
  - No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
24
24
  - Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
25
25
 
26
- ## Core metrics (benchmark: sigmap-v8.26-main, 2026-08-18)
26
+ ## Core metrics (benchmark: sigmap-v8.27-main, 2026-08-18)
27
27
 
28
- - hit@5 retrieval: 82.2% vs 44.0% single-shot grep baseline (1.76× lift)
28
+ - hit@5 retrieval: 81.1% vs 44.0% single-shot grep baseline (1.73× lift)
29
29
  - Token reduction: 96.8% average across benchmark repos
30
30
  - Task-success proxy: 64.8% (modeled from retrieval tiers, not measured LLM sessions)
31
- - Prompts per task: 1.53 vs 2.84 baseline (46.1% fewer, modeled)
31
+ - Prompts per task: 1.54 vs 2.84 baseline (45.7% fewer, modeled)
32
32
  - Languages: 33 supported · MCP tools: 21
33
33
  - Dependencies: zero npm runtime dependencies · fully offline
34
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.26.2",
3
+ "version": "8.27.0",
4
4
  "description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
5
5
  "main": "packages/core/index.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.26.2",
3
+ "version": "8.27.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.26.2",
3
+ "version": "8.27.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { lineAt, withAnchor } = require('./line-anchor');
4
4
  const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
5
+ const { stripComments, maskCode, readBalanced } = require('./scan');
5
6
 
6
7
  /**
7
8
  * Extract signatures from JavaScript source code.
@@ -21,16 +22,29 @@ function extract(src) {
21
22
  const returnHints = buildReturnHints(src);
22
23
  const docHints = buildDocHints(src);
23
24
 
24
- // Block comments are blanked newline-by-newline (non-newline chars spaces)
25
- // so character offsets AND line numbers stay exact for anchors.
26
- const stripped = src
27
- .replace(/\/\/.*$/gm, '')
28
- .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
25
+ // stripComments is string-aware (a `//` inside a string literal survives);
26
+ // maskCode additionally blanks string/template contents so every delimiter
27
+ // found on it is structural. Both are length- and newline-preserving, so
28
+ // offsets and line anchors align across all three views (#526).
29
+ const stripped = stripComments(src);
30
+ const masked = maskCode(src);
31
+
32
+ // Full params for a declaration whose `(` sits at openIdx: depth-matched
33
+ // close over masked text; TEXT sliced from stripped so string defaults keep
34
+ // their real content. Falls back to first-`)` when unbalanced (cap hit).
35
+ const paramsFrom = (openIdx) => {
36
+ const closeIdx = readBalanced(masked, openIdx);
37
+ if (closeIdx === -1) {
38
+ const naive = stripped.indexOf(')', openIdx);
39
+ return { params: stripped.slice(openIdx + 1, naive === -1 ? openIdx + 1 : naive), closeIdx: naive };
40
+ }
41
+ return { params: stripped.slice(openIdx + 1, closeIdx), closeIdx };
42
+ };
29
43
 
30
- const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
31
- // End line for a function whose match ends at `matchEnd` (before its body brace).
44
+ const blockEndIdx = (bodyStart) => bodyStart + extractBlock(masked, bodyStart).length;
45
+ // End line for a function whose params close just before `matchEnd`.
32
46
  const fnEndLine = (matchEnd, startLn) => {
33
- const brace = stripped.indexOf('{', matchEnd);
47
+ const brace = masked.indexOf('{', matchEnd);
34
48
  return brace !== -1 ? lineAt(stripped, blockEndIdx(brace + 1)) : startLn;
35
49
  };
36
50
 
@@ -39,33 +53,38 @@ function extract(src) {
39
53
  for (const m of stripped.matchAll(classRegex)) {
40
54
  const prefix = m[1] ? m[1].trim() + ' ' : '';
41
55
  const bodyStart = m.index + m[0].length;
56
+ const blockEnd = blockEndIdx(bodyStart);
42
57
  sigs.push(`${prefix}class ${m[2]}`);
43
- anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
44
- const block = extractBlock(stripped, bodyStart);
45
- for (const meth of extractClassMembers(block, returnHints)) {
58
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEnd)]);
59
+ const block = stripped.slice(bodyStart, blockEnd);
60
+ const maskedBlock = masked.slice(bodyStart, blockEnd);
61
+ for (const meth of extractClassMembers(block, maskedBlock, returnHints)) {
46
62
  sigs.push(` ${meth.text}`);
47
63
  anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
48
64
  }
49
65
  }
50
66
 
51
67
  // Exported named functions
52
- for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
68
+ for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(/gm)) {
53
69
  const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
54
70
  const retStr = formatReturnHint(returnHints.get(m[1]));
55
71
  const startLn = lineAt(stripped, m.index);
56
- sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
72
+ const { params, closeIdx } = paramsFrom(m.index + m[0].length - 1);
73
+ sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(params)})${retStr}`);
57
74
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
58
- anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
75
+ anchors.push([startLn, fnEndLine(closeIdx + 1, startLn)]);
59
76
  }
60
77
 
61
78
  // Exported arrow functions
62
- for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/gm)) {
79
+ for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(/gm)) {
80
+ const { params, closeIdx } = paramsFrom(m.index + m[0].length - 1);
81
+ if (closeIdx === -1 || !/^\s*=>/.test(masked.slice(closeIdx + 1, closeIdx + 40))) continue;
63
82
  const asyncKw = m[0].includes('async') ? 'async ' : '';
64
83
  const retStr = formatReturnHint(returnHints.get(m[1]));
65
84
  const startLn = lineAt(stripped, m.index);
66
- sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>${retStr}`);
85
+ sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(params)}) =>${retStr}`);
67
86
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
68
- anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
87
+ anchors.push([startLn, fnEndLine(closeIdx + 1, startLn)]);
69
88
  }
70
89
 
71
90
  // module.exports = { ... }
@@ -80,13 +99,14 @@ function extract(src) {
80
99
  }
81
100
 
82
101
  // Top-level named functions (non-exported)
83
- for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
102
+ for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(/gm)) {
84
103
  const asyncKw = m[0].startsWith('async') ? 'async ' : '';
85
104
  const retStr = formatReturnHint(returnHints.get(m[1]));
86
105
  const startLn = lineAt(stripped, m.index);
87
- sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})${retStr}`);
106
+ const { params, closeIdx } = paramsFrom(m.index + m[0].length - 1);
107
+ sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(params)})${retStr}`);
88
108
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
89
- anchors.push([startLn, fnEndLine(m.index + m[0].length, startLn)]);
109
+ anchors.push([startLn, fnEndLine(closeIdx + 1, startLn)]);
90
110
  }
91
111
 
92
112
  const withAnchors = sigs.map((s, i) => {
@@ -108,21 +128,31 @@ function extractBlock(src, startIndex) {
108
128
  return src.slice(startIndex, i - 1);
109
129
  }
110
130
 
131
+ const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
132
+
111
133
  // Returns members as { text, start, end } where start/end are char offsets
112
134
  // WITHIN `block` (end = the method's closing brace), so the caller can resolve
113
- // per-method line anchors that span the method body.
114
- function extractClassMembers(block, returnHints) {
135
+ // per-method line anchors that span the method body. `maskedBlock` is the
136
+ // same-offset maskCode slice used for balanced-delimiter scanning.
137
+ function extractClassMembers(block, maskedBlock, returnHints) {
115
138
  const members = [];
116
- for (const m of block.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(([^)]*)\)\s*\{/gm)) {
139
+ for (const m of maskedBlock.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(/gm)) {
117
140
  if (/^_/.test(m[1])) continue;
118
- const bodyStart = m.index + m[0].length; // just past the opening brace
119
- const end = bodyStart + extractBlock(block, bodyStart).length;
141
+ if (_CTRL_KEYWORDS.has(m[1])) continue;
142
+ const openIdx = m.index + m[0].length - 1;
143
+ const closeIdx = readBalanced(maskedBlock, openIdx);
144
+ if (closeIdx === -1) continue;
145
+ const braceMatch = maskedBlock.slice(closeIdx + 1, closeIdx + 40).match(/^\s*\{/);
146
+ if (!braceMatch) continue;
147
+ const params = block.slice(openIdx + 1, closeIdx);
148
+ const bodyStart = closeIdx + 1 + braceMatch[0].length; // just past the opening brace
149
+ const end = bodyStart + extractBlock(maskedBlock, bodyStart).length;
120
150
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
121
- if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
151
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(params)})`, start, end }); continue; }
122
152
  const isAsync = m[0].includes('async ') ? 'async ' : '';
123
153
  const isStatic = m[0].includes('static ') ? 'static ' : '';
124
154
  const retStr = formatReturnHint(returnHints.get(m[1]));
125
- members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
155
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(params)})${retStr}`, start, end });
126
156
  }
127
157
  return capMembersWithNotice(members, 8, 'methods');
128
158
  }
@@ -0,0 +1,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Shared tokenizer-grade scanning core (G4 increment 1, #526).
5
+ *
6
+ * Hand-rolled string/comment state + delimiter depth — generalizes the
7
+ * masking in src/graph/call-graph.js (maskJs) and the balanced reader in the
8
+ * R extractor. NOT a parser, NOT tree-sitter: three small, deterministic,
9
+ * zero-dependency passes that let extractors find real declaration
10
+ * boundaries instead of truncating at the first `)`.
11
+ *
12
+ * All transforms are length- and newline-preserving, so character offsets
13
+ * and line anchors computed on the output align 1:1 with the input.
14
+ */
15
+
16
+ /**
17
+ * Blank comments only — string-aware, so `//` or `/*` INSIDE a string
18
+ * literal survives (the naive regex strip corrupted e.g. `url = "https://x"`).
19
+ * Comment bytes become spaces; newlines and everything else are preserved.
20
+ * @param {string} src
21
+ * @returns {string} same length, comments blanked
22
+ */
23
+ function stripComments(src) {
24
+ const out = src.split('');
25
+ const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
26
+ let i = 0; const n = src.length;
27
+ while (i < n) {
28
+ const c = src[i], d = src[i + 1];
29
+ if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
30
+ 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; }
31
+ if (c === '"' || c === "'" || c === '`') {
32
+ let j = i + 1;
33
+ while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c) break; if (c !== '`' && src[j] === '\n') break; j++; }
34
+ i = Math.min(n, j + 1); continue;
35
+ }
36
+ i++;
37
+ }
38
+ return out.join('');
39
+ }
40
+
41
+ /**
42
+ * Blank comments AND string/template contents (quotes included) — the
43
+ * boundary-scanning surface: delimiters found here are always structural.
44
+ * @param {string} src
45
+ * @returns {string} same length, comments + strings blanked
46
+ */
47
+ function maskCode(src) {
48
+ const out = src.split('');
49
+ const blank = (a, b) => { for (let k = a; k < b; k++) if (out[k] !== '\n') out[k] = ' '; };
50
+ let i = 0; const n = src.length;
51
+ while (i < n) {
52
+ const c = src[i], d = src[i + 1];
53
+ if (c === '/' && d === '/') { let j = i + 2; while (j < n && src[j] !== '\n') j++; blank(i, j); i = j; continue; }
54
+ 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; }
55
+ if (c === '"' || c === "'" || c === '`') {
56
+ let j = i + 1;
57
+ while (j < n) { if (src[j] === '\\') { j += 2; continue; } if (src[j] === c) break; if (c !== '`' && src[j] === '\n') break; j++; }
58
+ j = Math.min(n, j + 1); blank(i, j); i = j; continue;
59
+ }
60
+ i++;
61
+ }
62
+ return out.join('');
63
+ }
64
+
65
+ /**
66
+ * Index of the delimiter that closes the one open at `openIdx`, matched by
67
+ * depth over MASKED text (strings/comments already blanked, so every
68
+ * delimiter seen is structural). -1 when unbalanced within the cap.
69
+ * @param {string} masked output of maskCode
70
+ * @param {number} openIdx index of the opening delimiter
71
+ * @param {string} [open='(']
72
+ * @param {string} [close=')']
73
+ * @param {number} [cap=4000] scan ceiling in chars
74
+ * @returns {number}
75
+ */
76
+ function readBalanced(masked, openIdx, open = '(', close = ')', cap = 4000) {
77
+ if (masked[openIdx] !== open) return -1;
78
+ let depth = 1;
79
+ const end = Math.min(masked.length, openIdx + cap);
80
+ for (let i = openIdx + 1; i < end; i++) {
81
+ const ch = masked[i];
82
+ if (ch === open) depth++;
83
+ else if (ch === close) {
84
+ depth--;
85
+ if (depth === 0) return i;
86
+ }
87
+ }
88
+ return -1;
89
+ }
90
+
91
+ module.exports = { stripComments, maskCode, readBalanced };
@@ -2,6 +2,7 @@
2
2
 
3
3
  const { lineAt, withAnchor } = require('./line-anchor');
4
4
  const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
5
+ const { stripComments, maskCode, readBalanced } = require('./scan');
5
6
 
6
7
  /**
7
8
  * Extract signatures from TypeScript source code.
@@ -23,15 +24,25 @@ function extract(src) {
23
24
  // anchors are applied once at return.
24
25
  const anchors = [];
25
26
 
26
- // Strip comments to simplify matching. Block comments are blanked
27
- // newline-by-newline (non-newline chars spaces) so character offsets AND
28
- // line numbers stay exact. Line comments preserve their trailing newline.
29
- const stripped = src
30
- .replace(/\/\/.*$/gm, '')
31
- .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
27
+ // stripComments is string-aware (a `//` inside a string literal survives);
28
+ // maskCode additionally blanks string/template contents so every delimiter
29
+ // found on it is structural. Both are length- and newline-preserving, so
30
+ // offsets and line anchors align across all three views (#526).
31
+ const stripped = stripComments(src);
32
+ const masked = maskCode(src);
33
+
34
+ // Full params for a declaration whose `(` sits at openIdx (see javascript.js).
35
+ const paramsFrom = (openIdx) => {
36
+ const closeIdx = readBalanced(masked, openIdx);
37
+ if (closeIdx === -1) {
38
+ const naive = stripped.indexOf(')', openIdx);
39
+ return { params: stripped.slice(openIdx + 1, naive === -1 ? openIdx + 1 : naive), closeIdx: naive };
40
+ }
41
+ return { params: stripped.slice(openIdx + 1, closeIdx), closeIdx };
42
+ };
32
43
 
33
44
  // Index of the closing brace for a block whose body starts at bodyStart.
34
- const blockEndIdx = (bodyStart) => bodyStart + extractBlock(stripped, bodyStart).length;
45
+ const blockEndIdx = (bodyStart) => bodyStart + extractBlock(masked, bodyStart).length;
35
46
 
36
47
  // Exported interfaces
37
48
  for (const m of stripped.matchAll(/^export\s+interface\s+(\w+)(?:<[^{]*>)?\s*(?:extends\s+[^{]+)?\{/gm)) {
@@ -66,10 +77,12 @@ function extract(src) {
66
77
  const prefix = m[1] ? 'export ' : '';
67
78
  const abs = m[2] ? 'abstract ' : '';
68
79
  const bodyStart = m.index + m[0].length;
80
+ const blockEnd = blockEndIdx(bodyStart);
69
81
  sigs.push(`${prefix}${abs}class ${m[3]}`);
70
- anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
71
- const block = extractBlock(stripped, bodyStart);
72
- const methods = extractClassMembers(block);
82
+ anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEnd)]);
83
+ const block = stripped.slice(bodyStart, blockEnd);
84
+ const maskedBlock = masked.slice(bodyStart, blockEnd);
85
+ const methods = extractClassMembers(block, maskedBlock);
73
86
  for (const meth of methods) {
74
87
  sigs.push(` ${meth.text}`);
75
88
  anchors.push([lineAt(stripped, bodyStart + meth.start), lineAt(stripped, bodyStart + meth.end)]);
@@ -77,13 +90,19 @@ function extract(src) {
77
90
  }
78
91
 
79
92
  // Exported top-level functions (not methods)
80
- for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{]+)?\s*\{/gm)) {
93
+ for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(/gm)) {
94
+ const { params: rawParams, closeIdx } = paramsFrom(m.index + m[0].length - 1);
95
+ if (closeIdx === -1) continue;
96
+ // Declaration shape check + return-type capture, mirroring the old
97
+ // `\)(?:\s*:\s*[^{]+)?\s*\{` tail against the text after the real close.
98
+ const tail = masked.slice(closeIdx + 1, closeIdx + 200).match(/^(\s*:\s*[^{]+?)?\s*\{/);
99
+ if (!tail) continue;
81
100
  const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
82
- const params = normalizeParams(m[2]);
83
- const retMatch = m[0].match(/\)\s*:\s*([^{]+)\s*\{/);
84
- const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 30) : '';
101
+ const params = normalizeParams(rawParams);
102
+ const retRaw = tail[1] ? stripped.slice(closeIdx + 1, closeIdx + 1 + tail[1].length).replace(/^\s*:\s*/, '') : '';
103
+ const retType = retRaw ? retRaw.trim().replace(/\s+/g, ' ').slice(0, 30) : '';
85
104
  const retStr = retType ? ` → ${retType}` : '';
86
- const bodyStart = m.index + m[0].length;
105
+ const bodyStart = closeIdx + 1 + tail[0].length;
87
106
  sigs.push(`export ${asyncKw}function ${m[1]}(${params})${retStr}`);
88
107
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
89
108
  anchors.push([lineAt(stripped, m.index), lineAt(stripped, blockEndIdx(bodyStart))]);
@@ -106,15 +125,21 @@ function extract(src) {
106
125
  }
107
126
 
108
127
  // Exported arrow functions / const functions
109
- for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=>{]+)?\s*=>/gm)) {
128
+ for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(/gm)) {
129
+ const { params: rawParams, closeIdx } = paramsFrom(m.index + m[0].length - 1);
130
+ if (closeIdx === -1) continue;
131
+ // Arrow shape check, mirroring the old `\)\s*(?::\s*[^=>{]+)?\s*=>` tail.
132
+ const tail = masked.slice(closeIdx + 1, closeIdx + 200).match(/^\s*(?::\s*[^=>{]+)?\s*=>/);
133
+ if (!tail) continue;
110
134
  const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
111
- const params = normalizeParams(m[2]);
135
+ const params = normalizeParams(rawParams);
112
136
  sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
113
137
  docHintFor[sigs.length - 1] = docHints.get(m[1]);
114
- const bodyStart = stripped.indexOf('{', m.index + m[0].length);
138
+ const matchEnd = closeIdx + 1 + tail[0].length;
139
+ const bodyStart = masked.indexOf('{', matchEnd);
115
140
  const endLn = bodyStart !== -1
116
141
  ? lineAt(stripped, blockEndIdx(bodyStart + 1))
117
- : lineAt(stripped, m.index + m[0].length);
142
+ : lineAt(stripped, matchEnd);
118
143
  anchors.push([lineAt(stripped, m.index), endLn]);
119
144
 
120
145
  // Hooks: capture compact return object shape for use* functions.
@@ -182,6 +207,7 @@ function extractBlock(src, startIndex) {
182
207
  // Returns members as { text, start, end } where start/end are char offsets
183
208
  // WITHIN `block`, so the caller can resolve member line anchors.
184
209
  function extractInterfaceMembers(block) {
210
+ const maskedBlock = maskCode(block);
185
211
  const members = [];
186
212
  for (const m of block.matchAll(/^\s+(readonly\s+)?(\w+)(\??):\s*([^;]+);/gm)) {
187
213
  const readonly = m[1] ? 'readonly ' : '';
@@ -190,9 +216,12 @@ function extractInterfaceMembers(block) {
190
216
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
191
217
  members.push({ text: `${readonly}${m[2]}${optional}: ${typeStr}`, start, end: m.index + m[0].length });
192
218
  }
193
- for (const m of block.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)\s*:/gm)) {
219
+ for (const m of maskedBlock.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(/gm)) {
220
+ const openIdx = m.index + m[0].length - 1;
221
+ const closeIdx = readBalanced(maskedBlock, openIdx);
222
+ if (closeIdx === -1 || !/^\s*:/.test(maskedBlock.slice(closeIdx + 1, closeIdx + 40))) continue;
194
223
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
195
- members.push({ text: `${m[1]}(${normalizeParams(m[2])})`, start, end: m.index + m[0].length });
224
+ members.push({ text: `${m[1]}(${normalizeParams(block.slice(openIdx + 1, closeIdx))})`, start, end: closeIdx + 1 });
196
225
  }
197
226
  return capMembersWithNotice(members, 8, 'members');
198
227
  }
@@ -202,30 +231,66 @@ const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'ca
202
231
  // Returns members as { text, start, end } where start/end are char offsets
203
232
  // WITHIN `block` (end = the method's closing brace), so the caller can resolve
204
233
  // per-method line anchors that span the method body.
205
- function extractClassMembers(block) {
234
+ function extractClassMembers(block, maskedBlock) {
235
+ const masked = maskedBlock || maskCode(block);
206
236
  const members = [];
207
237
  // Public methods (skip private/protected/_ prefixed and control-flow keywords)
208
- const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{;]+)?\s*\{/gm;
209
- for (const m of block.matchAll(methodRe)) {
238
+ const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(/gm;
239
+ for (const m of masked.matchAll(methodRe)) {
210
240
  if (_CTRL_KEYWORDS.has(m[1])) continue;
211
241
  if (/^(private|protected|_)/.test(m[1])) continue;
212
- const bodyStart = m.index + m[0].length; // just past the opening brace
213
- const end = bodyStart + extractBlock(block, bodyStart).length;
242
+ const openIdx = m.index + m[0].length - 1;
243
+ const closeIdx = readBalanced(masked, openIdx);
244
+ if (closeIdx === -1) continue;
245
+ // Declaration tail check + return-type capture, mirroring the old
246
+ // `\)(?:\s*:\s*[^{;]+)?\s*\{` shape against the text after the real close.
247
+ const tail = masked.slice(closeIdx + 1, closeIdx + 200).match(/^(\s*:\s*[^{;]+?)?\s*\{/);
248
+ if (!tail) continue;
249
+ const params = block.slice(openIdx + 1, closeIdx);
250
+ const bodyStart = closeIdx + 1 + tail[0].length; // just past the opening brace
251
+ const end = bodyStart + extractBlock(masked, bodyStart).length;
214
252
  const start = m.index + (m[0].length - m[0].replace(/^\s+/, '').length);
215
- if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(m[2])})`, start, end }); continue; }
253
+ if (m[1] === 'constructor') { members.push({ text: `constructor(${normalizeParams(params)})`, start, end }); continue; }
216
254
  const isAsync = m[0].includes('async ') ? 'async ' : '';
217
255
  const isStatic = m[0].includes('static ') ? 'static ' : '';
218
- const retMatch = m[0].match(/\)\s*:\s*([^{;]+)\s*\{/);
219
- const retType = retMatch ? retMatch[1].trim().replace(/\s+/g, ' ').slice(0, 20) : '';
256
+ const retRaw = tail[1] ? block.slice(closeIdx + 1, closeIdx + 1 + tail[1].length).replace(/^\s*:\s*/, '') : '';
257
+ const retType = retRaw ? retRaw.trim().replace(/\s+/g, ' ').slice(0, 20) : '';
220
258
  const retStr = retType ? ` → ${retType}` : '';
221
- members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})${retStr}`, start, end });
259
+ members.push({ text: `${isStatic}${isAsync}${m[1]}(${normalizeParams(params)})${retStr}`, start, end });
222
260
  }
223
261
  return capMembersWithNotice(members, 8, 'methods');
224
262
  }
225
263
 
226
264
  function normalizeParams(params) {
227
265
  if (!params) return '';
228
- return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
266
+ const compact = params.trim().replace(/\s+/g, ' ');
267
+ // Strip `: type` annotations at top level only — nested delimiters in types
268
+ // (e.g. `cb: (x: number) => void`, `m: Map<string, X>`) are consumed with
269
+ // their annotation instead of truncating at the first `)` or `,` (#526).
270
+ let out = '';
271
+ let depth = 0;
272
+ let inType = false;
273
+ let quote = null;
274
+ for (let i = 0; i < compact.length; i++) {
275
+ const ch = compact[i];
276
+ if (quote) {
277
+ if (ch === '\\') { if (!inType) out += ch + (compact[i + 1] || ''); i++; continue; }
278
+ if (ch === quote) quote = null;
279
+ if (!inType) out += ch;
280
+ continue;
281
+ }
282
+ if (ch === '"' || ch === "'" || ch === '`') { quote = ch; if (!inType) out += ch; continue; }
283
+ if (ch === '(' || ch === '[' || ch === '{' || ch === '<') depth++;
284
+ else if (ch === ')' || ch === ']' || ch === '}' || (ch === '>' && compact[i - 1] !== '=')) depth--;
285
+ if (inType) {
286
+ if (depth <= 0 && ch === ',') { inType = false; out += ch; }
287
+ else if (depth <= 0 && ch === '=' && compact[i + 1] !== '>') { inType = false; out += ' ='; }
288
+ continue;
289
+ }
290
+ if (depth === 0 && ch === ':') { inType = true; continue; }
291
+ out += ch;
292
+ }
293
+ return out.replace(/\s*,\s*/g, ', ').replace(/\s+/g, ' ').trim();
229
294
  }
230
295
 
231
296
  // First prose sentence of the JSDoc block immediately preceding an exported
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.26.2',
21
+ version: '8.27.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24