sigmap 8.26.1 → 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 +25 -0
- package/README.md +7 -7
- package/gen-context.js +250 -60
- package/llms-full.txt +4 -4
- package/llms.txt +4 -4
- package/package.json +2 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/extractors/javascript.js +57 -27
- package/src/extractors/scan.js +91 -0
- package/src/extractors/typescript.js +96 -31
- package/src/mcp/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,31 @@ 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
|
+
|
|
26
|
+
## [8.26.2] — 2026-08-18
|
|
27
|
+
|
|
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.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- **Benchmark cross-suite determinism (#522, PR #524):** two measured root causes. (1) The quality suite carried a stale 13-entry local copy of the retrieval harness's 21-entry `CONFIG_OVERRIDES` table and regenerated every repo unconditionally — the missing entries (express, flask, spring-petclinic, serilog) got default-srcDirs contexts, silently overwriting the canonical ones (measured: honest hit@5 77.6% → 66.4% after a quality run; flask 0.875 → 0). (2) The 20-task `retrieval` set scores against the **live SigMap repo**, whose context legitimately changes every release (measured: 0.85 → 0.55 over three releases) — real development drift, previously indistinguishable from harness noise. Fixes: a single canonical `benchmarks/config-overrides.json` loaded by both suites; the quality suite now mirrors the retrieval harness's apply/restore semantics exactly (always apply · regenerate · restore in `finally`); the honest report labels the self-repo row (`selfRepo: true`, `*` + note in output). New gate `npm run validate:benchmark-determinism` runs honest → quality → honest and fails on any divergence — verified green post-fix (identical at 77.6% / 125 tasks). 5 CI-safe source-level guard tests.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- **Headline honesty, again:** with the harness stable, the honest baseline resettles — the previously published 82.4% hit@5 included ~5pt of unlabeled self-repo drift. The refreshed numbers in this release are the first produced under the determinism gate. 134 test files.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
13
38
|
## [8.26.1] — 2026-08-18
|
|
14
39
|
|
|
15
40
|
Patch release — **"Trust Quick Wins II" (G1)**: the extraction layer gets the same honesty treatment the benchmarks got in v8.19.
|
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
|
-
- **
|
|
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.
|
|
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-->
|
|
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.
|
|
126
|
-
Date : 2026-08-
|
|
125
|
+
Benchmark : sigmap-v8.27-main (21 repositories, including R language)
|
|
126
|
+
Date : 2026-08-18
|
|
127
127
|
|
|
128
|
-
Hit@5 :
|
|
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 :
|
|
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
|
-
//
|
|
6589
|
-
//
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
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(
|
|
6595
|
-
// End line for a function whose
|
|
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 =
|
|
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,
|
|
6608
|
-
const block =
|
|
6609
|
-
|
|
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*\(
|
|
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
|
-
|
|
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(
|
|
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+)?\(
|
|
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(
|
|
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(
|
|
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*\(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6683
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
8765
|
-
//
|
|
8766
|
-
//
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
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(
|
|
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,
|
|
8809
|
-
const block =
|
|
8810
|
-
const
|
|
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*\(
|
|
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(
|
|
8821
|
-
const
|
|
8822
|
-
const retType =
|
|
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 =
|
|
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+)?\(
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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*\(
|
|
8947
|
-
for (const m of
|
|
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
|
|
8951
|
-
const
|
|
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(
|
|
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
|
|
8957
|
-
const retType =
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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) |
|
|
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.
|
|
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.
|
|
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
|
+
## Core metrics (benchmark: sigmap-v8.27-main, 2026-08-18)
|
|
27
27
|
|
|
28
|
-
- hit@5 retrieval:
|
|
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.
|
|
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.
|
|
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": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"benchmark:centrality-blend": "node scripts/run-centrality-blend-benchmark.mjs --save",
|
|
35
35
|
"benchmark:surface-enrichment": "node scripts/run-surface-enrichment-benchmark.mjs --save",
|
|
36
36
|
"validate:squeeze": "node scripts/run-squeeze-benchmark.mjs --gate",
|
|
37
|
+
"validate:benchmark-determinism": "node scripts/check-benchmark-determinism.mjs --cross-suite",
|
|
37
38
|
"health": "node gen-context.js --health",
|
|
38
39
|
"map": "node gen-project-map.js",
|
|
39
40
|
"mcp": "node gen-context.js --mcp",
|
|
@@ -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
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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(
|
|
31
|
-
// End line for a function whose
|
|
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 =
|
|
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,
|
|
44
|
-
const block =
|
|
45
|
-
|
|
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*\(
|
|
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
|
-
|
|
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(
|
|
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+)?\(
|
|
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(
|
|
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(
|
|
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*\(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
119
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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(
|
|
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,
|
|
71
|
-
const block =
|
|
72
|
-
const
|
|
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*\(
|
|
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(
|
|
83
|
-
const
|
|
84
|
-
const retType =
|
|
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 =
|
|
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+)?\(
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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*\(
|
|
209
|
-
for (const m of
|
|
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
|
|
213
|
-
const
|
|
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(
|
|
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
|
|
219
|
-
const retType =
|
|
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(
|
|
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
|
-
|
|
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