sigmap 8.27.0 → 8.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/gen-context.js +220 -3
- package/llms-full.txt +2 -2
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/mcp/server.js +1 -1
- package/src/verify/arity.js +180 -0
- package/src/verify/hallucination-guard.js +34 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.28.0] — 2026-08-18
|
|
14
|
+
|
|
15
|
+
Minor release — **"Arity Guard" (v8.28, D1)**: verification now checks not just *does this function exist* but *is it being called with a plausible number of arguments* — the payoff of the v8.27 balanced scanner.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Arity-checked verification (#529, PR #530):** new `src/verify/arity.js` — `parseParams` turns a signature's exact parameter list into an arity range (`=` defaults and TS `?`-optionals lower `min`; `...rest`/`*args`/`**kwargs` mark the signature variadic; destructuring patterns count as one parameter; depth- and quote-aware throughout), `buildArityIndex` builds a per-name range from top-level callables in exact-param languages only (JS/TS via the balanced scanner, Python via AST; names whose signatures disagree across files are marked ambiguous and never checked; indented members excluded), `extractCallArgCounts` reads calls from answer code over masked text (nested calls and comma-containing strings count correctly; argument emptiness judged on the original text; dotted calls, definitions, `new`-expressions, and control keywords skipped), `checkArity` (variadic signatures flag only too-few). Wired into the Hallucination Guard as detector 3b — **`arity-mismatch` at medium confidence**, with the repo signature + file as the suggestion; `verify_suggestion` (MCP) and `sigmap verify-ai-output` inherit automatically; `opts.arityIndex` keeps hermetic callers unchanged. Conservative by construction: uniquely-resolved · top-level · non-variadic · undotted · known-symbol calls in JS/TS/Python code blocks only — unknowns stay `fake-symbol`. `KNOWN_LIMITATIONS.md` documents the checks and their gates.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- 6 new integration tests incl. end-to-end through a real context file (136 test files); bundle rebuilt (151 modules); zero new dependencies.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
13
25
|
## [8.27.0] — 2026-08-18
|
|
14
26
|
|
|
15
27
|
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).
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
122
122
|
|
|
123
123
|
<!--SM:benchmarkBlock-->
|
|
124
124
|
```
|
|
125
|
-
Benchmark : sigmap-v8.
|
|
125
|
+
Benchmark : sigmap-v8.28-main (21 repositories, including R language)
|
|
126
126
|
Date : 2026-08-18
|
|
127
127
|
|
|
128
128
|
Hit@5 : 81.1% (grep-agent baseline 44.0% — 1.73× lift)
|
package/gen-context.js
CHANGED
|
@@ -15385,7 +15385,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
15385
15385
|
|
|
15386
15386
|
const SERVER_INFO = {
|
|
15387
15387
|
name: 'sigmap',
|
|
15388
|
-
version: '8.
|
|
15388
|
+
version: '8.28.0',
|
|
15389
15389
|
description: 'SigMap MCP server — code signatures on demand',
|
|
15390
15390
|
};
|
|
15391
15391
|
|
|
@@ -19579,6 +19579,190 @@ __factories["./src/util/truncate"] = function(module, exports) {
|
|
|
19579
19579
|
|
|
19580
19580
|
};
|
|
19581
19581
|
|
|
19582
|
+
// ── ./src/verify/arity ──
|
|
19583
|
+
__factories["./src/verify/arity"] = function(module, exports) {
|
|
19584
|
+
|
|
19585
|
+
/**
|
|
19586
|
+
* Arity-checked verification (D1, #529).
|
|
19587
|
+
*
|
|
19588
|
+
* With JS/TS params exact (v8.27 balanced scanner) and Python params from the
|
|
19589
|
+
* AST, the signature index carries real parameter lists — so verification can
|
|
19590
|
+
* check not just "does this function exist" but "is this call's argument
|
|
19591
|
+
* count plausible". Deliberately conservative: only uniquely-resolved,
|
|
19592
|
+
* non-variadic, top-level functions from exact-param languages are checked,
|
|
19593
|
+
* and dotted method calls are never flagged.
|
|
19594
|
+
*/
|
|
19595
|
+
|
|
19596
|
+
const path = require('path');
|
|
19597
|
+
const { maskCode, readBalanced } = __require('./src/extractors/scan');
|
|
19598
|
+
|
|
19599
|
+
// Files whose signature params are exact (JS/TS via scan.js, Python via AST).
|
|
19600
|
+
const EXACT_PARAM_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py']);
|
|
19601
|
+
|
|
19602
|
+
const CTRL_KEYWORDS = new Set([
|
|
19603
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'await',
|
|
19604
|
+
'do', 'else', 'try', 'finally', 'new', 'in', 'of', 'not', 'and', 'or',
|
|
19605
|
+
'print', 'super', 'this',
|
|
19606
|
+
]);
|
|
19607
|
+
|
|
19608
|
+
// Top-level callable sig shapes (indented member sigs are excluded on purpose
|
|
19609
|
+
// — method calls are dotted in answers and dotted calls are skipped anyway).
|
|
19610
|
+
const CALLABLE_RES = [
|
|
19611
|
+
/^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/,
|
|
19612
|
+
/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(/,
|
|
19613
|
+
/^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/,
|
|
19614
|
+
];
|
|
19615
|
+
|
|
19616
|
+
/** Strip the ` :start-end` anchor and ` # hint` tail from a sig line. */
|
|
19617
|
+
function cleanSig(sig) {
|
|
19618
|
+
return String(sig).replace(/\s{2}#\s.*$/, '').replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
19619
|
+
}
|
|
19620
|
+
|
|
19621
|
+
/**
|
|
19622
|
+
* Parse a parameter-list string into an arity range.
|
|
19623
|
+
* Depth- and quote-aware top-level comma split; `=` defaults and trailing `?`
|
|
19624
|
+
* lower `min`; `...rest` / `*args` / `**kwargs` mark the signature variadic;
|
|
19625
|
+
* destructuring patterns count as one parameter.
|
|
19626
|
+
* @param {string} paramText text between the signature's parens
|
|
19627
|
+
* @returns {{ min: number, max: number, variadic: boolean }}
|
|
19628
|
+
*/
|
|
19629
|
+
function parseParams(paramText) {
|
|
19630
|
+
const text = String(paramText || '').trim();
|
|
19631
|
+
if (!text) return { min: 0, max: 0, variadic: false };
|
|
19632
|
+
const masked = maskCode(text);
|
|
19633
|
+
const pieces = [];
|
|
19634
|
+
let depth = 0;
|
|
19635
|
+
let start = 0;
|
|
19636
|
+
for (let i = 0; i < masked.length; i++) {
|
|
19637
|
+
const ch = masked[i];
|
|
19638
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
19639
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
19640
|
+
else if (ch === ',' && depth === 0) { pieces.push({ raw: text.slice(start, i), masked: masked.slice(start, i) }); start = i + 1; }
|
|
19641
|
+
}
|
|
19642
|
+
pieces.push({ raw: text.slice(start), masked: masked.slice(start) });
|
|
19643
|
+
|
|
19644
|
+
let min = 0;
|
|
19645
|
+
let max = 0;
|
|
19646
|
+
let variadic = false;
|
|
19647
|
+
for (const piece of pieces) {
|
|
19648
|
+
const p = piece.raw.trim();
|
|
19649
|
+
if (!p) continue;
|
|
19650
|
+
if (/^(\.\.\.|\*)/.test(p)) { variadic = true; continue; }
|
|
19651
|
+
max++;
|
|
19652
|
+
// Optional: a top-level `=` default (scan the masked piece at depth 0) or
|
|
19653
|
+
// a `?`-suffixed name (TS optional, survives type stripping as `x?`).
|
|
19654
|
+
let d = 0;
|
|
19655
|
+
let optional = /^[A-Za-z_$][\w$]*\s*\?$/.test(p);
|
|
19656
|
+
const pm = piece.masked;
|
|
19657
|
+
for (let i = 0; i < pm.length && !optional; i++) {
|
|
19658
|
+
const ch = pm[i];
|
|
19659
|
+
if (ch === '(' || ch === '[' || ch === '{') d++;
|
|
19660
|
+
else if (ch === ')' || ch === ']' || ch === '}') d--;
|
|
19661
|
+
else if (ch === '=' && d === 0 && pm[i + 1] !== '>' && pm[i - 1] !== '=' && pm[i - 1] !== '!' && pm[i - 1] !== '<' && pm[i - 1] !== '>') optional = true;
|
|
19662
|
+
}
|
|
19663
|
+
if (!optional) min++;
|
|
19664
|
+
}
|
|
19665
|
+
return { min, max, variadic };
|
|
19666
|
+
}
|
|
19667
|
+
|
|
19668
|
+
/**
|
|
19669
|
+
* Build a per-name arity index from a SigMap signature index.
|
|
19670
|
+
* Only top-level callables from exact-param languages are included; a name
|
|
19671
|
+
* whose signatures disagree across files is marked ambiguous (never checked).
|
|
19672
|
+
* @param {Map<string, string[]>} sigIndex Map<file, sigs[]>
|
|
19673
|
+
* @returns {Map<string, { min, max, variadic, file, sig } | 'ambiguous'>}
|
|
19674
|
+
*/
|
|
19675
|
+
function buildArityIndex(sigIndex) {
|
|
19676
|
+
const index = new Map();
|
|
19677
|
+
if (!sigIndex || !(sigIndex instanceof Map)) return index;
|
|
19678
|
+
for (const [file, sigs] of sigIndex.entries()) {
|
|
19679
|
+
if (!EXACT_PARAM_EXTS.has(path.extname(file))) continue;
|
|
19680
|
+
for (const sig of sigs || []) {
|
|
19681
|
+
const cleaned = cleanSig(sig);
|
|
19682
|
+
let name = null;
|
|
19683
|
+
for (const re of CALLABLE_RES) {
|
|
19684
|
+
const m = cleaned.match(re);
|
|
19685
|
+
if (m) { name = m[1]; break; }
|
|
19686
|
+
}
|
|
19687
|
+
if (!name) continue;
|
|
19688
|
+
const openIdx = cleaned.indexOf('(', cleaned.indexOf(name));
|
|
19689
|
+
if (openIdx === -1) continue;
|
|
19690
|
+
const masked = maskCode(cleaned);
|
|
19691
|
+
const closeIdx = readBalanced(masked, openIdx);
|
|
19692
|
+
if (closeIdx === -1) continue;
|
|
19693
|
+
const arity = parseParams(cleaned.slice(openIdx + 1, closeIdx));
|
|
19694
|
+
const entry = { ...arity, file, sig: cleaned.trim() };
|
|
19695
|
+
const existing = index.get(name);
|
|
19696
|
+
if (existing === undefined) index.set(name, entry);
|
|
19697
|
+
else if (existing === 'ambiguous') continue;
|
|
19698
|
+
else if (existing.min !== entry.min || existing.max !== entry.max || existing.variadic !== entry.variadic) {
|
|
19699
|
+
index.set(name, 'ambiguous');
|
|
19700
|
+
}
|
|
19701
|
+
}
|
|
19702
|
+
}
|
|
19703
|
+
return index;
|
|
19704
|
+
}
|
|
19705
|
+
|
|
19706
|
+
/**
|
|
19707
|
+
* Extract call sites with argument counts from answer code.
|
|
19708
|
+
* Dotted/property calls and keyword-preceded definitions are skipped for
|
|
19709
|
+
* precision; nested calls and comma-containing strings count correctly
|
|
19710
|
+
* because the scan is over masked text.
|
|
19711
|
+
* @param {string} code
|
|
19712
|
+
* @returns {{ name: string, args: number, line: number }[]}
|
|
19713
|
+
*/
|
|
19714
|
+
function extractCallArgCounts(code) {
|
|
19715
|
+
const src = String(code || '');
|
|
19716
|
+
const masked = maskCode(src);
|
|
19717
|
+
const calls = [];
|
|
19718
|
+
const re = /([A-Za-z_$][\w$]*)\s*\(/g;
|
|
19719
|
+
let m;
|
|
19720
|
+
while ((m = re.exec(masked)) !== null) {
|
|
19721
|
+
const name = m[1];
|
|
19722
|
+
if (CTRL_KEYWORDS.has(name)) continue;
|
|
19723
|
+
let k = m.index - 1;
|
|
19724
|
+
while (k >= 0 && (masked[k] === ' ' || masked[k] === '\t')) k--;
|
|
19725
|
+
if (k >= 0 && (masked[k] === '.' || masked[k] === '$')) continue;
|
|
19726
|
+
const before = masked.slice(Math.max(0, m.index - 12), m.index);
|
|
19727
|
+
if (/(?:function|def|class|new)\s+$/.test(before)) continue;
|
|
19728
|
+
const openIdx = m.index + m[0].length - 1;
|
|
19729
|
+
const closeIdx = readBalanced(masked, openIdx);
|
|
19730
|
+
if (closeIdx === -1) continue;
|
|
19731
|
+
const inner = masked.slice(openIdx + 1, closeIdx);
|
|
19732
|
+
let args = 0;
|
|
19733
|
+
// Emptiness is judged on the ORIGINAL text — masking blanks string
|
|
19734
|
+
// contents, so `f("a,b")` would otherwise look like zero arguments.
|
|
19735
|
+
if (src.slice(openIdx + 1, closeIdx).trim()) {
|
|
19736
|
+
args = 1;
|
|
19737
|
+
let depth = 0;
|
|
19738
|
+
for (let i = 0; i < inner.length; i++) {
|
|
19739
|
+
const ch = inner[i];
|
|
19740
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
19741
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
19742
|
+
else if (ch === ',' && depth === 0) args++;
|
|
19743
|
+
}
|
|
19744
|
+
}
|
|
19745
|
+
calls.push({ name, args, line: src.slice(0, m.index).split('\n').length });
|
|
19746
|
+
}
|
|
19747
|
+
return calls;
|
|
19748
|
+
}
|
|
19749
|
+
|
|
19750
|
+
/**
|
|
19751
|
+
* Check one call against the arity index.
|
|
19752
|
+
* @returns {null | { min, max, variadic, file, sig }} the offended entry, or null when fine/unknowable
|
|
19753
|
+
*/
|
|
19754
|
+
function checkArity(name, argCount, arityIndex) {
|
|
19755
|
+
const entry = arityIndex.get(name);
|
|
19756
|
+
if (!entry || entry === 'ambiguous') return null;
|
|
19757
|
+
if (entry.variadic) return argCount < entry.min ? entry : null;
|
|
19758
|
+
if (argCount < entry.min || argCount > entry.max) return entry;
|
|
19759
|
+
return null;
|
|
19760
|
+
}
|
|
19761
|
+
|
|
19762
|
+
module.exports = { parseParams, buildArityIndex, extractCallArgCounts, checkArity, cleanSig, EXACT_PARAM_EXTS };
|
|
19763
|
+
|
|
19764
|
+
};
|
|
19765
|
+
|
|
19582
19766
|
// ── ./src/verify/closest-match ──
|
|
19583
19767
|
__factories["./src/verify/closest-match"] = function(module, exports) {
|
|
19584
19768
|
|
|
@@ -19753,6 +19937,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19753
19937
|
const parsers = __require('./src/verify/parsers');
|
|
19754
19938
|
const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
|
|
19755
19939
|
const { buildLibraryIndex } = __require('./src/verify/lib-index');
|
|
19940
|
+
const { buildArityIndex, extractCallArgCounts, checkArity } = __require('./src/verify/arity');
|
|
19756
19941
|
|
|
19757
19942
|
// A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
|
|
19758
19943
|
// a tests/__tests__ directory). Used to flag fake-test-file separately.
|
|
@@ -19808,9 +19993,11 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19808
19993
|
const set = new Set();
|
|
19809
19994
|
let fileKeys = [];
|
|
19810
19995
|
let symbolCandidates = [];
|
|
19996
|
+
let sigIndex = null;
|
|
19811
19997
|
try {
|
|
19812
19998
|
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
19813
19999
|
const idx = buildSigIndex(cwd);
|
|
20000
|
+
sigIndex = idx;
|
|
19814
20001
|
fileKeys = [...idx.keys()];
|
|
19815
20002
|
for (const sigs of idx.values()) {
|
|
19816
20003
|
for (const sig of sigs) {
|
|
@@ -19821,7 +20008,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19821
20008
|
}
|
|
19822
20009
|
symbolCandidates = buildSymbolCandidates(idx);
|
|
19823
20010
|
} catch (_) {}
|
|
19824
|
-
return { set, fileKeys, symbolCandidates };
|
|
20011
|
+
return { set, fileKeys, symbolCandidates, sigIndex };
|
|
19825
20012
|
}
|
|
19826
20013
|
|
|
19827
20014
|
/** Load declared dependency names from package.json. */
|
|
@@ -19910,6 +20097,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19910
20097
|
let fileBasenames = opts.fileBasenames;
|
|
19911
20098
|
let symbolCandidates = opts.symbolCandidates || [];
|
|
19912
20099
|
let fileCandidates = opts.fileCandidates || [];
|
|
20100
|
+
let arityIndex = opts.arityIndex || null;
|
|
19913
20101
|
if (!symbolSet) {
|
|
19914
20102
|
const built = buildSymbolSet(cwd);
|
|
19915
20103
|
symbolSet = built.set;
|
|
@@ -19918,6 +20106,9 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19918
20106
|
));
|
|
19919
20107
|
symbolCandidates = built.symbolCandidates;
|
|
19920
20108
|
fileCandidates = built.fileKeys;
|
|
20109
|
+
if (!arityIndex && built.sigIndex) {
|
|
20110
|
+
try { arityIndex = buildArityIndex(built.sigIndex); } catch (_) {}
|
|
20111
|
+
}
|
|
19921
20112
|
}
|
|
19922
20113
|
if (!fileBasenames) fileBasenames = new Set();
|
|
19923
20114
|
|
|
@@ -20036,6 +20227,32 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
20036
20227
|
}
|
|
20037
20228
|
}
|
|
20038
20229
|
|
|
20230
|
+
// 3b. arity-mismatch (D1, #529) — a call to a KNOWN repo function whose
|
|
20231
|
+
// argument count falls outside the signature's [min, max]. Conservative by
|
|
20232
|
+
// construction: only uniquely-resolved, top-level functions from
|
|
20233
|
+
// exact-param languages (JS/TS via the balanced scanner, Python via AST);
|
|
20234
|
+
// variadic signatures only flag too-few; dotted calls never flag.
|
|
20235
|
+
if (arityIndex && arityIndex.size > 0) {
|
|
20236
|
+
for (const block of parsers.extractCodeBlocks(answerText)) {
|
|
20237
|
+
if (block.lang && !/^(js|jsx|ts|tsx|javascript|typescript|python|py)$/i.test(block.lang)) continue;
|
|
20238
|
+
for (const call of extractCallArgCounts(block.content)) {
|
|
20239
|
+
if (!symbolSet.has(call.name)) continue; // unknown symbols stay fake-symbol territory
|
|
20240
|
+
const entry = checkArity(call.name, call.args, arityIndex);
|
|
20241
|
+
if (!entry) continue;
|
|
20242
|
+
const range = entry.variadic ? `at least ${entry.min}`
|
|
20243
|
+
: (entry.min === entry.max ? String(entry.max) : `${entry.min}–${entry.max}`);
|
|
20244
|
+
add({
|
|
20245
|
+
type: 'arity-mismatch',
|
|
20246
|
+
value: `${call.name}(${call.args} args)`,
|
|
20247
|
+
line: block.line + call.line - 1,
|
|
20248
|
+
message: `${call.name}() called with ${call.args} argument(s) — repo signature takes ${range}`,
|
|
20249
|
+
confidence: 'medium',
|
|
20250
|
+
suggestion: `${entry.sig} (${entry.file})`,
|
|
20251
|
+
});
|
|
20252
|
+
}
|
|
20253
|
+
}
|
|
20254
|
+
}
|
|
20255
|
+
|
|
20039
20256
|
// 4. fake-npm-script
|
|
20040
20257
|
if (hasPkg && scripts.size > 0) {
|
|
20041
20258
|
for (const { name, line } of parsers.extractNpmScripts(answerText)) {
|
|
@@ -20997,7 +21214,7 @@ function __tryGit(args, opts = {}) {
|
|
|
20997
21214
|
catch (_) { return ''; }
|
|
20998
21215
|
}
|
|
20999
21216
|
|
|
21000
|
-
const VERSION = '8.
|
|
21217
|
+
const VERSION = '8.28.0';
|
|
21001
21218
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
21002
21219
|
|
|
21003
21220
|
function requireSourceOrBundled(key) {
|
package/llms-full.txt
CHANGED
|
@@ -11,13 +11,13 @@ 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.28.0 | Benchmark: sigmap-v8.28-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.28-main, 2026-08-18)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
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.28.0 | Benchmark: sigmap-v8.28-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,7 +23,7 @@ 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.28-main, 2026-08-18)
|
|
27
27
|
|
|
28
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.28.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": {
|
package/src/mcp/server.js
CHANGED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Arity-checked verification (D1, #529).
|
|
5
|
+
*
|
|
6
|
+
* With JS/TS params exact (v8.27 balanced scanner) and Python params from the
|
|
7
|
+
* AST, the signature index carries real parameter lists — so verification can
|
|
8
|
+
* check not just "does this function exist" but "is this call's argument
|
|
9
|
+
* count plausible". Deliberately conservative: only uniquely-resolved,
|
|
10
|
+
* non-variadic, top-level functions from exact-param languages are checked,
|
|
11
|
+
* and dotted method calls are never flagged.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { maskCode, readBalanced } = require('../extractors/scan');
|
|
16
|
+
|
|
17
|
+
// Files whose signature params are exact (JS/TS via scan.js, Python via AST).
|
|
18
|
+
const EXACT_PARAM_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py']);
|
|
19
|
+
|
|
20
|
+
const CTRL_KEYWORDS = new Set([
|
|
21
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'await',
|
|
22
|
+
'do', 'else', 'try', 'finally', 'new', 'in', 'of', 'not', 'and', 'or',
|
|
23
|
+
'print', 'super', 'this',
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
// Top-level callable sig shapes (indented member sigs are excluded on purpose
|
|
27
|
+
// — method calls are dotted in answers and dotted calls are skipped anyway).
|
|
28
|
+
const CALLABLE_RES = [
|
|
29
|
+
/^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/,
|
|
30
|
+
/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(/,
|
|
31
|
+
/^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/,
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/** Strip the ` :start-end` anchor and ` # hint` tail from a sig line. */
|
|
35
|
+
function cleanSig(sig) {
|
|
36
|
+
return String(sig).replace(/\s{2}#\s.*$/, '').replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a parameter-list string into an arity range.
|
|
41
|
+
* Depth- and quote-aware top-level comma split; `=` defaults and trailing `?`
|
|
42
|
+
* lower `min`; `...rest` / `*args` / `**kwargs` mark the signature variadic;
|
|
43
|
+
* destructuring patterns count as one parameter.
|
|
44
|
+
* @param {string} paramText text between the signature's parens
|
|
45
|
+
* @returns {{ min: number, max: number, variadic: boolean }}
|
|
46
|
+
*/
|
|
47
|
+
function parseParams(paramText) {
|
|
48
|
+
const text = String(paramText || '').trim();
|
|
49
|
+
if (!text) return { min: 0, max: 0, variadic: false };
|
|
50
|
+
const masked = maskCode(text);
|
|
51
|
+
const pieces = [];
|
|
52
|
+
let depth = 0;
|
|
53
|
+
let start = 0;
|
|
54
|
+
for (let i = 0; i < masked.length; i++) {
|
|
55
|
+
const ch = masked[i];
|
|
56
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
57
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
58
|
+
else if (ch === ',' && depth === 0) { pieces.push({ raw: text.slice(start, i), masked: masked.slice(start, i) }); start = i + 1; }
|
|
59
|
+
}
|
|
60
|
+
pieces.push({ raw: text.slice(start), masked: masked.slice(start) });
|
|
61
|
+
|
|
62
|
+
let min = 0;
|
|
63
|
+
let max = 0;
|
|
64
|
+
let variadic = false;
|
|
65
|
+
for (const piece of pieces) {
|
|
66
|
+
const p = piece.raw.trim();
|
|
67
|
+
if (!p) continue;
|
|
68
|
+
if (/^(\.\.\.|\*)/.test(p)) { variadic = true; continue; }
|
|
69
|
+
max++;
|
|
70
|
+
// Optional: a top-level `=` default (scan the masked piece at depth 0) or
|
|
71
|
+
// a `?`-suffixed name (TS optional, survives type stripping as `x?`).
|
|
72
|
+
let d = 0;
|
|
73
|
+
let optional = /^[A-Za-z_$][\w$]*\s*\?$/.test(p);
|
|
74
|
+
const pm = piece.masked;
|
|
75
|
+
for (let i = 0; i < pm.length && !optional; i++) {
|
|
76
|
+
const ch = pm[i];
|
|
77
|
+
if (ch === '(' || ch === '[' || ch === '{') d++;
|
|
78
|
+
else if (ch === ')' || ch === ']' || ch === '}') d--;
|
|
79
|
+
else if (ch === '=' && d === 0 && pm[i + 1] !== '>' && pm[i - 1] !== '=' && pm[i - 1] !== '!' && pm[i - 1] !== '<' && pm[i - 1] !== '>') optional = true;
|
|
80
|
+
}
|
|
81
|
+
if (!optional) min++;
|
|
82
|
+
}
|
|
83
|
+
return { min, max, variadic };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build a per-name arity index from a SigMap signature index.
|
|
88
|
+
* Only top-level callables from exact-param languages are included; a name
|
|
89
|
+
* whose signatures disagree across files is marked ambiguous (never checked).
|
|
90
|
+
* @param {Map<string, string[]>} sigIndex Map<file, sigs[]>
|
|
91
|
+
* @returns {Map<string, { min, max, variadic, file, sig } | 'ambiguous'>}
|
|
92
|
+
*/
|
|
93
|
+
function buildArityIndex(sigIndex) {
|
|
94
|
+
const index = new Map();
|
|
95
|
+
if (!sigIndex || !(sigIndex instanceof Map)) return index;
|
|
96
|
+
for (const [file, sigs] of sigIndex.entries()) {
|
|
97
|
+
if (!EXACT_PARAM_EXTS.has(path.extname(file))) continue;
|
|
98
|
+
for (const sig of sigs || []) {
|
|
99
|
+
const cleaned = cleanSig(sig);
|
|
100
|
+
let name = null;
|
|
101
|
+
for (const re of CALLABLE_RES) {
|
|
102
|
+
const m = cleaned.match(re);
|
|
103
|
+
if (m) { name = m[1]; break; }
|
|
104
|
+
}
|
|
105
|
+
if (!name) continue;
|
|
106
|
+
const openIdx = cleaned.indexOf('(', cleaned.indexOf(name));
|
|
107
|
+
if (openIdx === -1) continue;
|
|
108
|
+
const masked = maskCode(cleaned);
|
|
109
|
+
const closeIdx = readBalanced(masked, openIdx);
|
|
110
|
+
if (closeIdx === -1) continue;
|
|
111
|
+
const arity = parseParams(cleaned.slice(openIdx + 1, closeIdx));
|
|
112
|
+
const entry = { ...arity, file, sig: cleaned.trim() };
|
|
113
|
+
const existing = index.get(name);
|
|
114
|
+
if (existing === undefined) index.set(name, entry);
|
|
115
|
+
else if (existing === 'ambiguous') continue;
|
|
116
|
+
else if (existing.min !== entry.min || existing.max !== entry.max || existing.variadic !== entry.variadic) {
|
|
117
|
+
index.set(name, 'ambiguous');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return index;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract call sites with argument counts from answer code.
|
|
126
|
+
* Dotted/property calls and keyword-preceded definitions are skipped for
|
|
127
|
+
* precision; nested calls and comma-containing strings count correctly
|
|
128
|
+
* because the scan is over masked text.
|
|
129
|
+
* @param {string} code
|
|
130
|
+
* @returns {{ name: string, args: number, line: number }[]}
|
|
131
|
+
*/
|
|
132
|
+
function extractCallArgCounts(code) {
|
|
133
|
+
const src = String(code || '');
|
|
134
|
+
const masked = maskCode(src);
|
|
135
|
+
const calls = [];
|
|
136
|
+
const re = /([A-Za-z_$][\w$]*)\s*\(/g;
|
|
137
|
+
let m;
|
|
138
|
+
while ((m = re.exec(masked)) !== null) {
|
|
139
|
+
const name = m[1];
|
|
140
|
+
if (CTRL_KEYWORDS.has(name)) continue;
|
|
141
|
+
let k = m.index - 1;
|
|
142
|
+
while (k >= 0 && (masked[k] === ' ' || masked[k] === '\t')) k--;
|
|
143
|
+
if (k >= 0 && (masked[k] === '.' || masked[k] === '$')) continue;
|
|
144
|
+
const before = masked.slice(Math.max(0, m.index - 12), m.index);
|
|
145
|
+
if (/(?:function|def|class|new)\s+$/.test(before)) continue;
|
|
146
|
+
const openIdx = m.index + m[0].length - 1;
|
|
147
|
+
const closeIdx = readBalanced(masked, openIdx);
|
|
148
|
+
if (closeIdx === -1) continue;
|
|
149
|
+
const inner = masked.slice(openIdx + 1, closeIdx);
|
|
150
|
+
let args = 0;
|
|
151
|
+
// Emptiness is judged on the ORIGINAL text — masking blanks string
|
|
152
|
+
// contents, so `f("a,b")` would otherwise look like zero arguments.
|
|
153
|
+
if (src.slice(openIdx + 1, closeIdx).trim()) {
|
|
154
|
+
args = 1;
|
|
155
|
+
let depth = 0;
|
|
156
|
+
for (let i = 0; i < inner.length; i++) {
|
|
157
|
+
const ch = inner[i];
|
|
158
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
159
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
160
|
+
else if (ch === ',' && depth === 0) args++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
calls.push({ name, args, line: src.slice(0, m.index).split('\n').length });
|
|
164
|
+
}
|
|
165
|
+
return calls;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check one call against the arity index.
|
|
170
|
+
* @returns {null | { min, max, variadic, file, sig }} the offended entry, or null when fine/unknowable
|
|
171
|
+
*/
|
|
172
|
+
function checkArity(name, argCount, arityIndex) {
|
|
173
|
+
const entry = arityIndex.get(name);
|
|
174
|
+
if (!entry || entry === 'ambiguous') return null;
|
|
175
|
+
if (entry.variadic) return argCount < entry.min ? entry : null;
|
|
176
|
+
if (argCount < entry.min || argCount > entry.max) return entry;
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = { parseParams, buildArityIndex, extractCallArgCounts, checkArity, cleanSig, EXACT_PARAM_EXTS };
|
|
@@ -22,6 +22,7 @@ const path = require('path');
|
|
|
22
22
|
const parsers = require('./parsers');
|
|
23
23
|
const { closestMatch, buildSymbolCandidates, formatSuggestion } = require('./closest-match');
|
|
24
24
|
const { buildLibraryIndex } = require('./lib-index');
|
|
25
|
+
const { buildArityIndex, extractCallArgCounts, checkArity } = require('./arity');
|
|
25
26
|
|
|
26
27
|
// A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
|
|
27
28
|
// a tests/__tests__ directory). Used to flag fake-test-file separately.
|
|
@@ -77,9 +78,11 @@ function buildSymbolSet(cwd) {
|
|
|
77
78
|
const set = new Set();
|
|
78
79
|
let fileKeys = [];
|
|
79
80
|
let symbolCandidates = [];
|
|
81
|
+
let sigIndex = null;
|
|
80
82
|
try {
|
|
81
83
|
const { buildSigIndex } = require('../retrieval/ranker');
|
|
82
84
|
const idx = buildSigIndex(cwd);
|
|
85
|
+
sigIndex = idx;
|
|
83
86
|
fileKeys = [...idx.keys()];
|
|
84
87
|
for (const sigs of idx.values()) {
|
|
85
88
|
for (const sig of sigs) {
|
|
@@ -90,7 +93,7 @@ function buildSymbolSet(cwd) {
|
|
|
90
93
|
}
|
|
91
94
|
symbolCandidates = buildSymbolCandidates(idx);
|
|
92
95
|
} catch (_) {}
|
|
93
|
-
return { set, fileKeys, symbolCandidates };
|
|
96
|
+
return { set, fileKeys, symbolCandidates, sigIndex };
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
/** Load declared dependency names from package.json. */
|
|
@@ -179,6 +182,7 @@ function verify(answerText, cwd, opts = {}) {
|
|
|
179
182
|
let fileBasenames = opts.fileBasenames;
|
|
180
183
|
let symbolCandidates = opts.symbolCandidates || [];
|
|
181
184
|
let fileCandidates = opts.fileCandidates || [];
|
|
185
|
+
let arityIndex = opts.arityIndex || null;
|
|
182
186
|
if (!symbolSet) {
|
|
183
187
|
const built = buildSymbolSet(cwd);
|
|
184
188
|
symbolSet = built.set;
|
|
@@ -187,6 +191,9 @@ function verify(answerText, cwd, opts = {}) {
|
|
|
187
191
|
));
|
|
188
192
|
symbolCandidates = built.symbolCandidates;
|
|
189
193
|
fileCandidates = built.fileKeys;
|
|
194
|
+
if (!arityIndex && built.sigIndex) {
|
|
195
|
+
try { arityIndex = buildArityIndex(built.sigIndex); } catch (_) {}
|
|
196
|
+
}
|
|
190
197
|
}
|
|
191
198
|
if (!fileBasenames) fileBasenames = new Set();
|
|
192
199
|
|
|
@@ -305,6 +312,32 @@ function verify(answerText, cwd, opts = {}) {
|
|
|
305
312
|
}
|
|
306
313
|
}
|
|
307
314
|
|
|
315
|
+
// 3b. arity-mismatch (D1, #529) — a call to a KNOWN repo function whose
|
|
316
|
+
// argument count falls outside the signature's [min, max]. Conservative by
|
|
317
|
+
// construction: only uniquely-resolved, top-level functions from
|
|
318
|
+
// exact-param languages (JS/TS via the balanced scanner, Python via AST);
|
|
319
|
+
// variadic signatures only flag too-few; dotted calls never flag.
|
|
320
|
+
if (arityIndex && arityIndex.size > 0) {
|
|
321
|
+
for (const block of parsers.extractCodeBlocks(answerText)) {
|
|
322
|
+
if (block.lang && !/^(js|jsx|ts|tsx|javascript|typescript|python|py)$/i.test(block.lang)) continue;
|
|
323
|
+
for (const call of extractCallArgCounts(block.content)) {
|
|
324
|
+
if (!symbolSet.has(call.name)) continue; // unknown symbols stay fake-symbol territory
|
|
325
|
+
const entry = checkArity(call.name, call.args, arityIndex);
|
|
326
|
+
if (!entry) continue;
|
|
327
|
+
const range = entry.variadic ? `at least ${entry.min}`
|
|
328
|
+
: (entry.min === entry.max ? String(entry.max) : `${entry.min}–${entry.max}`);
|
|
329
|
+
add({
|
|
330
|
+
type: 'arity-mismatch',
|
|
331
|
+
value: `${call.name}(${call.args} args)`,
|
|
332
|
+
line: block.line + call.line - 1,
|
|
333
|
+
message: `${call.name}() called with ${call.args} argument(s) — repo signature takes ${range}`,
|
|
334
|
+
confidence: 'medium',
|
|
335
|
+
suggestion: `${entry.sig} (${entry.file})`,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
308
341
|
// 4. fake-npm-script
|
|
309
342
|
if (hasPkg && scripts.size > 0) {
|
|
310
343
|
for (const { name, line } of parsers.extractNpmScripts(answerText)) {
|