sigmap 8.27.0 → 8.28.1
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 +2 -2
- package/gen-context.js +255 -17
- 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/graph/builder.js +20 -12
- package/src/mcp/server.js +1 -1
- package/src/retrieval/ranker.js +15 -2
- package/src/verify/arity.js +180 -0
- package/src/verify/hallucination-guard.js +34 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,31 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.28.1] — 2026-08-22
|
|
14
|
+
|
|
15
|
+
Patch release — two silent-failure bug fixes: a false "zero importers" in the Python import graph, and an empty index under the per-module strategy.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Python absolute imports resolve through any ancestor root (#532, PR #533)** — reported and precisely diagnosed by **@ruurdboeke**, thank you: `extractFileDeps` probed only the importing file's directory and one parent for `from package.module import`, so `src/`-layout projects (source root on `sys.path`) silently produced no graph edge whenever the importer sat two or more directories below the root — and `get_impact` then reported **zero importers**, exactly the signal that says a change is safe to make. Fix: an ancestor walk from the importing file up to the project root (nearest first, 16-level cap), probing `<module>.py` and `<module>/__init__.py` per level; nearest-first preserves the old first-match semantics for same-dir/one-parent cases. 4 new tests incl. the verbatim issue repro and `get_impact` end-to-end.
|
|
19
|
+
- **Per-module strategy: `ask`/`query_context` find signatures again (#534, PR #535):** the per-module strategy writes one `context-<module>.md` per top-level srcDir and leaves the primary file as a thin overview — but the sig-index strategy enrichment merged only `context-cold.md` (hot-cold) plus the cache, so `sigmap ask` failed with "no context file found" and the `query_context` MCP tool returned an empty index on every per-module repo. Fix: enumerate `.github/context-*.md` (sorted, deterministic) and merge each — the cold-file merge generalized to every strategy split. 4 new tests on a real generated two-module fixture incl. the exact reported failure; hot-cold and full strategies verified unchanged.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- 8 new integration tests (138 test files); bundle rebuilt; zero new dependencies.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [8.28.0] — 2026-08-18
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- **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.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- 6 new integration tests incl. end-to-end through a real context file (136 test files); bundle rebuilt (151 modules); zero new dependencies.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
13
38
|
## [8.27.0] — 2026-08-18
|
|
14
39
|
|
|
15
40
|
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,8 +122,8 @@ 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.28-main (21 repositories, including R language)
|
|
126
|
+
Date : 2026-08-22
|
|
127
127
|
|
|
128
128
|
Hit@5 : 81.1% (grep-agent baseline 44.0% — 1.73× lift)
|
|
129
129
|
Token reduction: 96.8% (across 21 repos)
|
package/gen-context.js
CHANGED
|
@@ -11573,23 +11573,31 @@ __factories["./src/graph/builder"] = function(module, exports) {
|
|
|
11573
11573
|
}
|
|
11574
11574
|
}
|
|
11575
11575
|
|
|
11576
|
-
// Absolute imports: from package.module import ... (infer from project
|
|
11576
|
+
// Absolute imports: from package.module import ... (infer from project
|
|
11577
|
+
// structure). The module is resolved against EVERY ancestor of the
|
|
11578
|
+
// importing file up to the project root, nearest first — any of them can
|
|
11579
|
+
// be the source root on sys.path (src/ layouts, pytest rootdir). The old
|
|
11580
|
+
// dir + one-parent probe silently dropped edges for files nested two or
|
|
11581
|
+
// more directories below the source root (#532), and a false "zero
|
|
11582
|
+
// importers" from get_impact is exactly the signal that says a change is
|
|
11583
|
+
// safe to make.
|
|
11577
11584
|
const reAbs = /^[ \t]*from\s+([\w.]+)\s+import/gm;
|
|
11578
11585
|
while ((m = reAbs.exec(content)) !== null) {
|
|
11579
11586
|
const modulePath = m[1].replace(/\./g, '/');
|
|
11580
|
-
const
|
|
11581
|
-
|
|
11582
|
-
|
|
11583
|
-
|
|
11584
|
-
path.
|
|
11585
|
-
|
|
11586
|
-
|
|
11587
|
-
const normC = normalizePath(c);
|
|
11588
|
-
if (fileSet.has(normC)) {
|
|
11589
|
-
found.push(normC);
|
|
11590
|
-
break;
|
|
11587
|
+
const normCwd = normalizePath(path.resolve(cwd));
|
|
11588
|
+
let base = dir;
|
|
11589
|
+
let hit = null;
|
|
11590
|
+
for (let depth = 0; depth < 16 && !hit; depth++) {
|
|
11591
|
+
for (const c of [path.join(base, modulePath + '.py'), path.join(base, modulePath, '__init__.py')]) {
|
|
11592
|
+
const normC = normalizePath(c);
|
|
11593
|
+
if (fileSet.has(normC)) { hit = normC; break; }
|
|
11591
11594
|
}
|
|
11595
|
+
if (normalizePath(base) === normCwd) break;
|
|
11596
|
+
const parent = path.dirname(base);
|
|
11597
|
+
if (parent === base) break;
|
|
11598
|
+
base = parent;
|
|
11592
11599
|
}
|
|
11600
|
+
if (hit) found.push(hit);
|
|
11593
11601
|
}
|
|
11594
11602
|
}
|
|
11595
11603
|
|
|
@@ -15385,7 +15393,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
15385
15393
|
|
|
15386
15394
|
const SERVER_INFO = {
|
|
15387
15395
|
name: 'sigmap',
|
|
15388
|
-
version: '8.
|
|
15396
|
+
version: '8.28.1',
|
|
15389
15397
|
description: 'SigMap MCP server — code signatures on demand',
|
|
15390
15398
|
};
|
|
15391
15399
|
|
|
@@ -16975,9 +16983,22 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
|
|
|
16975
16983
|
* @returns {Map<string, string[]>}
|
|
16976
16984
|
*/
|
|
16977
16985
|
function _enrichSigIndexFromStrategy(cwd, index) {
|
|
16986
|
+
const fs = require('fs');
|
|
16978
16987
|
const path = require('path');
|
|
16979
|
-
|
|
16980
|
-
|
|
16988
|
+
// Merge every strategy split file: context-cold.md (hot-cold) AND each
|
|
16989
|
+
// per-module context-<module>.md — the per-module strategy stores ALL
|
|
16990
|
+
// signatures in these, leaving the primary file as a thin overview, so
|
|
16991
|
+
// skipping them made ask/query_context see an empty index (#534).
|
|
16992
|
+
// Sorted for deterministic merge order.
|
|
16993
|
+
try {
|
|
16994
|
+
const ghDir = path.join(cwd, '.github');
|
|
16995
|
+
const splits = fs.readdirSync(ghDir)
|
|
16996
|
+
.filter((f) => /^context-[\w.-]+\.md$/.test(f))
|
|
16997
|
+
.sort();
|
|
16998
|
+
for (const f of splits) {
|
|
16999
|
+
_mergeSigIndex(index, _parseContextFile(path.join(ghDir, f)));
|
|
17000
|
+
}
|
|
17001
|
+
} catch (_) {}
|
|
16981
17002
|
_mergeSigIndex(index, _buildSigIndexFromCache(cwd));
|
|
16982
17003
|
return index;
|
|
16983
17004
|
}
|
|
@@ -19579,6 +19600,190 @@ __factories["./src/util/truncate"] = function(module, exports) {
|
|
|
19579
19600
|
|
|
19580
19601
|
};
|
|
19581
19602
|
|
|
19603
|
+
// ── ./src/verify/arity ──
|
|
19604
|
+
__factories["./src/verify/arity"] = function(module, exports) {
|
|
19605
|
+
|
|
19606
|
+
/**
|
|
19607
|
+
* Arity-checked verification (D1, #529).
|
|
19608
|
+
*
|
|
19609
|
+
* With JS/TS params exact (v8.27 balanced scanner) and Python params from the
|
|
19610
|
+
* AST, the signature index carries real parameter lists — so verification can
|
|
19611
|
+
* check not just "does this function exist" but "is this call's argument
|
|
19612
|
+
* count plausible". Deliberately conservative: only uniquely-resolved,
|
|
19613
|
+
* non-variadic, top-level functions from exact-param languages are checked,
|
|
19614
|
+
* and dotted method calls are never flagged.
|
|
19615
|
+
*/
|
|
19616
|
+
|
|
19617
|
+
const path = require('path');
|
|
19618
|
+
const { maskCode, readBalanced } = __require('./src/extractors/scan');
|
|
19619
|
+
|
|
19620
|
+
// Files whose signature params are exact (JS/TS via scan.js, Python via AST).
|
|
19621
|
+
const EXACT_PARAM_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py']);
|
|
19622
|
+
|
|
19623
|
+
const CTRL_KEYWORDS = new Set([
|
|
19624
|
+
'if', 'for', 'while', 'switch', 'catch', 'return', 'typeof', 'await',
|
|
19625
|
+
'do', 'else', 'try', 'finally', 'new', 'in', 'of', 'not', 'and', 'or',
|
|
19626
|
+
'print', 'super', 'this',
|
|
19627
|
+
]);
|
|
19628
|
+
|
|
19629
|
+
// Top-level callable sig shapes (indented member sigs are excluded on purpose
|
|
19630
|
+
// — method calls are dotted in answers and dotted calls are skipped anyway).
|
|
19631
|
+
const CALLABLE_RES = [
|
|
19632
|
+
/^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/,
|
|
19633
|
+
/^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?\(/,
|
|
19634
|
+
/^(?:async\s+)?def\s+([A-Za-z_]\w*)\s*\(/,
|
|
19635
|
+
];
|
|
19636
|
+
|
|
19637
|
+
/** Strip the ` :start-end` anchor and ` # hint` tail from a sig line. */
|
|
19638
|
+
function cleanSig(sig) {
|
|
19639
|
+
return String(sig).replace(/\s{2}#\s.*$/, '').replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
19640
|
+
}
|
|
19641
|
+
|
|
19642
|
+
/**
|
|
19643
|
+
* Parse a parameter-list string into an arity range.
|
|
19644
|
+
* Depth- and quote-aware top-level comma split; `=` defaults and trailing `?`
|
|
19645
|
+
* lower `min`; `...rest` / `*args` / `**kwargs` mark the signature variadic;
|
|
19646
|
+
* destructuring patterns count as one parameter.
|
|
19647
|
+
* @param {string} paramText text between the signature's parens
|
|
19648
|
+
* @returns {{ min: number, max: number, variadic: boolean }}
|
|
19649
|
+
*/
|
|
19650
|
+
function parseParams(paramText) {
|
|
19651
|
+
const text = String(paramText || '').trim();
|
|
19652
|
+
if (!text) return { min: 0, max: 0, variadic: false };
|
|
19653
|
+
const masked = maskCode(text);
|
|
19654
|
+
const pieces = [];
|
|
19655
|
+
let depth = 0;
|
|
19656
|
+
let start = 0;
|
|
19657
|
+
for (let i = 0; i < masked.length; i++) {
|
|
19658
|
+
const ch = masked[i];
|
|
19659
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
19660
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
19661
|
+
else if (ch === ',' && depth === 0) { pieces.push({ raw: text.slice(start, i), masked: masked.slice(start, i) }); start = i + 1; }
|
|
19662
|
+
}
|
|
19663
|
+
pieces.push({ raw: text.slice(start), masked: masked.slice(start) });
|
|
19664
|
+
|
|
19665
|
+
let min = 0;
|
|
19666
|
+
let max = 0;
|
|
19667
|
+
let variadic = false;
|
|
19668
|
+
for (const piece of pieces) {
|
|
19669
|
+
const p = piece.raw.trim();
|
|
19670
|
+
if (!p) continue;
|
|
19671
|
+
if (/^(\.\.\.|\*)/.test(p)) { variadic = true; continue; }
|
|
19672
|
+
max++;
|
|
19673
|
+
// Optional: a top-level `=` default (scan the masked piece at depth 0) or
|
|
19674
|
+
// a `?`-suffixed name (TS optional, survives type stripping as `x?`).
|
|
19675
|
+
let d = 0;
|
|
19676
|
+
let optional = /^[A-Za-z_$][\w$]*\s*\?$/.test(p);
|
|
19677
|
+
const pm = piece.masked;
|
|
19678
|
+
for (let i = 0; i < pm.length && !optional; i++) {
|
|
19679
|
+
const ch = pm[i];
|
|
19680
|
+
if (ch === '(' || ch === '[' || ch === '{') d++;
|
|
19681
|
+
else if (ch === ')' || ch === ']' || ch === '}') d--;
|
|
19682
|
+
else if (ch === '=' && d === 0 && pm[i + 1] !== '>' && pm[i - 1] !== '=' && pm[i - 1] !== '!' && pm[i - 1] !== '<' && pm[i - 1] !== '>') optional = true;
|
|
19683
|
+
}
|
|
19684
|
+
if (!optional) min++;
|
|
19685
|
+
}
|
|
19686
|
+
return { min, max, variadic };
|
|
19687
|
+
}
|
|
19688
|
+
|
|
19689
|
+
/**
|
|
19690
|
+
* Build a per-name arity index from a SigMap signature index.
|
|
19691
|
+
* Only top-level callables from exact-param languages are included; a name
|
|
19692
|
+
* whose signatures disagree across files is marked ambiguous (never checked).
|
|
19693
|
+
* @param {Map<string, string[]>} sigIndex Map<file, sigs[]>
|
|
19694
|
+
* @returns {Map<string, { min, max, variadic, file, sig } | 'ambiguous'>}
|
|
19695
|
+
*/
|
|
19696
|
+
function buildArityIndex(sigIndex) {
|
|
19697
|
+
const index = new Map();
|
|
19698
|
+
if (!sigIndex || !(sigIndex instanceof Map)) return index;
|
|
19699
|
+
for (const [file, sigs] of sigIndex.entries()) {
|
|
19700
|
+
if (!EXACT_PARAM_EXTS.has(path.extname(file))) continue;
|
|
19701
|
+
for (const sig of sigs || []) {
|
|
19702
|
+
const cleaned = cleanSig(sig);
|
|
19703
|
+
let name = null;
|
|
19704
|
+
for (const re of CALLABLE_RES) {
|
|
19705
|
+
const m = cleaned.match(re);
|
|
19706
|
+
if (m) { name = m[1]; break; }
|
|
19707
|
+
}
|
|
19708
|
+
if (!name) continue;
|
|
19709
|
+
const openIdx = cleaned.indexOf('(', cleaned.indexOf(name));
|
|
19710
|
+
if (openIdx === -1) continue;
|
|
19711
|
+
const masked = maskCode(cleaned);
|
|
19712
|
+
const closeIdx = readBalanced(masked, openIdx);
|
|
19713
|
+
if (closeIdx === -1) continue;
|
|
19714
|
+
const arity = parseParams(cleaned.slice(openIdx + 1, closeIdx));
|
|
19715
|
+
const entry = { ...arity, file, sig: cleaned.trim() };
|
|
19716
|
+
const existing = index.get(name);
|
|
19717
|
+
if (existing === undefined) index.set(name, entry);
|
|
19718
|
+
else if (existing === 'ambiguous') continue;
|
|
19719
|
+
else if (existing.min !== entry.min || existing.max !== entry.max || existing.variadic !== entry.variadic) {
|
|
19720
|
+
index.set(name, 'ambiguous');
|
|
19721
|
+
}
|
|
19722
|
+
}
|
|
19723
|
+
}
|
|
19724
|
+
return index;
|
|
19725
|
+
}
|
|
19726
|
+
|
|
19727
|
+
/**
|
|
19728
|
+
* Extract call sites with argument counts from answer code.
|
|
19729
|
+
* Dotted/property calls and keyword-preceded definitions are skipped for
|
|
19730
|
+
* precision; nested calls and comma-containing strings count correctly
|
|
19731
|
+
* because the scan is over masked text.
|
|
19732
|
+
* @param {string} code
|
|
19733
|
+
* @returns {{ name: string, args: number, line: number }[]}
|
|
19734
|
+
*/
|
|
19735
|
+
function extractCallArgCounts(code) {
|
|
19736
|
+
const src = String(code || '');
|
|
19737
|
+
const masked = maskCode(src);
|
|
19738
|
+
const calls = [];
|
|
19739
|
+
const re = /([A-Za-z_$][\w$]*)\s*\(/g;
|
|
19740
|
+
let m;
|
|
19741
|
+
while ((m = re.exec(masked)) !== null) {
|
|
19742
|
+
const name = m[1];
|
|
19743
|
+
if (CTRL_KEYWORDS.has(name)) continue;
|
|
19744
|
+
let k = m.index - 1;
|
|
19745
|
+
while (k >= 0 && (masked[k] === ' ' || masked[k] === '\t')) k--;
|
|
19746
|
+
if (k >= 0 && (masked[k] === '.' || masked[k] === '$')) continue;
|
|
19747
|
+
const before = masked.slice(Math.max(0, m.index - 12), m.index);
|
|
19748
|
+
if (/(?:function|def|class|new)\s+$/.test(before)) continue;
|
|
19749
|
+
const openIdx = m.index + m[0].length - 1;
|
|
19750
|
+
const closeIdx = readBalanced(masked, openIdx);
|
|
19751
|
+
if (closeIdx === -1) continue;
|
|
19752
|
+
const inner = masked.slice(openIdx + 1, closeIdx);
|
|
19753
|
+
let args = 0;
|
|
19754
|
+
// Emptiness is judged on the ORIGINAL text — masking blanks string
|
|
19755
|
+
// contents, so `f("a,b")` would otherwise look like zero arguments.
|
|
19756
|
+
if (src.slice(openIdx + 1, closeIdx).trim()) {
|
|
19757
|
+
args = 1;
|
|
19758
|
+
let depth = 0;
|
|
19759
|
+
for (let i = 0; i < inner.length; i++) {
|
|
19760
|
+
const ch = inner[i];
|
|
19761
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
19762
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
19763
|
+
else if (ch === ',' && depth === 0) args++;
|
|
19764
|
+
}
|
|
19765
|
+
}
|
|
19766
|
+
calls.push({ name, args, line: src.slice(0, m.index).split('\n').length });
|
|
19767
|
+
}
|
|
19768
|
+
return calls;
|
|
19769
|
+
}
|
|
19770
|
+
|
|
19771
|
+
/**
|
|
19772
|
+
* Check one call against the arity index.
|
|
19773
|
+
* @returns {null | { min, max, variadic, file, sig }} the offended entry, or null when fine/unknowable
|
|
19774
|
+
*/
|
|
19775
|
+
function checkArity(name, argCount, arityIndex) {
|
|
19776
|
+
const entry = arityIndex.get(name);
|
|
19777
|
+
if (!entry || entry === 'ambiguous') return null;
|
|
19778
|
+
if (entry.variadic) return argCount < entry.min ? entry : null;
|
|
19779
|
+
if (argCount < entry.min || argCount > entry.max) return entry;
|
|
19780
|
+
return null;
|
|
19781
|
+
}
|
|
19782
|
+
|
|
19783
|
+
module.exports = { parseParams, buildArityIndex, extractCallArgCounts, checkArity, cleanSig, EXACT_PARAM_EXTS };
|
|
19784
|
+
|
|
19785
|
+
};
|
|
19786
|
+
|
|
19582
19787
|
// ── ./src/verify/closest-match ──
|
|
19583
19788
|
__factories["./src/verify/closest-match"] = function(module, exports) {
|
|
19584
19789
|
|
|
@@ -19753,6 +19958,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19753
19958
|
const parsers = __require('./src/verify/parsers');
|
|
19754
19959
|
const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
|
|
19755
19960
|
const { buildLibraryIndex } = __require('./src/verify/lib-index');
|
|
19961
|
+
const { buildArityIndex, extractCallArgCounts, checkArity } = __require('./src/verify/arity');
|
|
19756
19962
|
|
|
19757
19963
|
// A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
|
|
19758
19964
|
// a tests/__tests__ directory). Used to flag fake-test-file separately.
|
|
@@ -19808,9 +20014,11 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19808
20014
|
const set = new Set();
|
|
19809
20015
|
let fileKeys = [];
|
|
19810
20016
|
let symbolCandidates = [];
|
|
20017
|
+
let sigIndex = null;
|
|
19811
20018
|
try {
|
|
19812
20019
|
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
19813
20020
|
const idx = buildSigIndex(cwd);
|
|
20021
|
+
sigIndex = idx;
|
|
19814
20022
|
fileKeys = [...idx.keys()];
|
|
19815
20023
|
for (const sigs of idx.values()) {
|
|
19816
20024
|
for (const sig of sigs) {
|
|
@@ -19821,7 +20029,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19821
20029
|
}
|
|
19822
20030
|
symbolCandidates = buildSymbolCandidates(idx);
|
|
19823
20031
|
} catch (_) {}
|
|
19824
|
-
return { set, fileKeys, symbolCandidates };
|
|
20032
|
+
return { set, fileKeys, symbolCandidates, sigIndex };
|
|
19825
20033
|
}
|
|
19826
20034
|
|
|
19827
20035
|
/** Load declared dependency names from package.json. */
|
|
@@ -19910,6 +20118,7 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19910
20118
|
let fileBasenames = opts.fileBasenames;
|
|
19911
20119
|
let symbolCandidates = opts.symbolCandidates || [];
|
|
19912
20120
|
let fileCandidates = opts.fileCandidates || [];
|
|
20121
|
+
let arityIndex = opts.arityIndex || null;
|
|
19913
20122
|
if (!symbolSet) {
|
|
19914
20123
|
const built = buildSymbolSet(cwd);
|
|
19915
20124
|
symbolSet = built.set;
|
|
@@ -19918,6 +20127,9 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
19918
20127
|
));
|
|
19919
20128
|
symbolCandidates = built.symbolCandidates;
|
|
19920
20129
|
fileCandidates = built.fileKeys;
|
|
20130
|
+
if (!arityIndex && built.sigIndex) {
|
|
20131
|
+
try { arityIndex = buildArityIndex(built.sigIndex); } catch (_) {}
|
|
20132
|
+
}
|
|
19921
20133
|
}
|
|
19922
20134
|
if (!fileBasenames) fileBasenames = new Set();
|
|
19923
20135
|
|
|
@@ -20036,6 +20248,32 @@ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
|
20036
20248
|
}
|
|
20037
20249
|
}
|
|
20038
20250
|
|
|
20251
|
+
// 3b. arity-mismatch (D1, #529) — a call to a KNOWN repo function whose
|
|
20252
|
+
// argument count falls outside the signature's [min, max]. Conservative by
|
|
20253
|
+
// construction: only uniquely-resolved, top-level functions from
|
|
20254
|
+
// exact-param languages (JS/TS via the balanced scanner, Python via AST);
|
|
20255
|
+
// variadic signatures only flag too-few; dotted calls never flag.
|
|
20256
|
+
if (arityIndex && arityIndex.size > 0) {
|
|
20257
|
+
for (const block of parsers.extractCodeBlocks(answerText)) {
|
|
20258
|
+
if (block.lang && !/^(js|jsx|ts|tsx|javascript|typescript|python|py)$/i.test(block.lang)) continue;
|
|
20259
|
+
for (const call of extractCallArgCounts(block.content)) {
|
|
20260
|
+
if (!symbolSet.has(call.name)) continue; // unknown symbols stay fake-symbol territory
|
|
20261
|
+
const entry = checkArity(call.name, call.args, arityIndex);
|
|
20262
|
+
if (!entry) continue;
|
|
20263
|
+
const range = entry.variadic ? `at least ${entry.min}`
|
|
20264
|
+
: (entry.min === entry.max ? String(entry.max) : `${entry.min}–${entry.max}`);
|
|
20265
|
+
add({
|
|
20266
|
+
type: 'arity-mismatch',
|
|
20267
|
+
value: `${call.name}(${call.args} args)`,
|
|
20268
|
+
line: block.line + call.line - 1,
|
|
20269
|
+
message: `${call.name}() called with ${call.args} argument(s) — repo signature takes ${range}`,
|
|
20270
|
+
confidence: 'medium',
|
|
20271
|
+
suggestion: `${entry.sig} (${entry.file})`,
|
|
20272
|
+
});
|
|
20273
|
+
}
|
|
20274
|
+
}
|
|
20275
|
+
}
|
|
20276
|
+
|
|
20039
20277
|
// 4. fake-npm-script
|
|
20040
20278
|
if (hasPkg && scripts.size > 0) {
|
|
20041
20279
|
for (const { name, line } of parsers.extractNpmScripts(answerText)) {
|
|
@@ -20997,7 +21235,7 @@ function __tryGit(args, opts = {}) {
|
|
|
20997
21235
|
catch (_) { return ''; }
|
|
20998
21236
|
}
|
|
20999
21237
|
|
|
21000
|
-
const VERSION = '8.
|
|
21238
|
+
const VERSION = '8.28.1';
|
|
21001
21239
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
21002
21240
|
|
|
21003
21241
|
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.1 | Benchmark: sigmap-v8.28-main (2026-08-22)
|
|
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-22)
|
|
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.1 | Benchmark: sigmap-v8.28-main (2026-08-22)
|
|
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-22)
|
|
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.1",
|
|
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/graph/builder.js
CHANGED
|
@@ -240,23 +240,31 @@ function extractFileDeps(filePath, content, fileSet, cwd, ctx) {
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
// Absolute imports: from package.module import ... (infer from project
|
|
243
|
+
// Absolute imports: from package.module import ... (infer from project
|
|
244
|
+
// structure). The module is resolved against EVERY ancestor of the
|
|
245
|
+
// importing file up to the project root, nearest first — any of them can
|
|
246
|
+
// be the source root on sys.path (src/ layouts, pytest rootdir). The old
|
|
247
|
+
// dir + one-parent probe silently dropped edges for files nested two or
|
|
248
|
+
// more directories below the source root (#532), and a false "zero
|
|
249
|
+
// importers" from get_impact is exactly the signal that says a change is
|
|
250
|
+
// safe to make.
|
|
244
251
|
const reAbs = /^[ \t]*from\s+([\w.]+)\s+import/gm;
|
|
245
252
|
while ((m = reAbs.exec(content)) !== null) {
|
|
246
253
|
const modulePath = m[1].replace(/\./g, '/');
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
path.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const normC = normalizePath(c);
|
|
255
|
-
if (fileSet.has(normC)) {
|
|
256
|
-
found.push(normC);
|
|
257
|
-
break;
|
|
254
|
+
const normCwd = normalizePath(path.resolve(cwd));
|
|
255
|
+
let base = dir;
|
|
256
|
+
let hit = null;
|
|
257
|
+
for (let depth = 0; depth < 16 && !hit; depth++) {
|
|
258
|
+
for (const c of [path.join(base, modulePath + '.py'), path.join(base, modulePath, '__init__.py')]) {
|
|
259
|
+
const normC = normalizePath(c);
|
|
260
|
+
if (fileSet.has(normC)) { hit = normC; break; }
|
|
258
261
|
}
|
|
262
|
+
if (normalizePath(base) === normCwd) break;
|
|
263
|
+
const parent = path.dirname(base);
|
|
264
|
+
if (parent === base) break;
|
|
265
|
+
base = parent;
|
|
259
266
|
}
|
|
267
|
+
if (hit) found.push(hit);
|
|
260
268
|
}
|
|
261
269
|
}
|
|
262
270
|
|
package/src/mcp/server.js
CHANGED
package/src/retrieval/ranker.js
CHANGED
|
@@ -473,9 +473,22 @@ function _buildSigIndexFromCache(cwd) {
|
|
|
473
473
|
* @returns {Map<string, string[]>}
|
|
474
474
|
*/
|
|
475
475
|
function _enrichSigIndexFromStrategy(cwd, index) {
|
|
476
|
+
const fs = require('fs');
|
|
476
477
|
const path = require('path');
|
|
477
|
-
|
|
478
|
-
|
|
478
|
+
// Merge every strategy split file: context-cold.md (hot-cold) AND each
|
|
479
|
+
// per-module context-<module>.md — the per-module strategy stores ALL
|
|
480
|
+
// signatures in these, leaving the primary file as a thin overview, so
|
|
481
|
+
// skipping them made ask/query_context see an empty index (#534).
|
|
482
|
+
// Sorted for deterministic merge order.
|
|
483
|
+
try {
|
|
484
|
+
const ghDir = path.join(cwd, '.github');
|
|
485
|
+
const splits = fs.readdirSync(ghDir)
|
|
486
|
+
.filter((f) => /^context-[\w.-]+\.md$/.test(f))
|
|
487
|
+
.sort();
|
|
488
|
+
for (const f of splits) {
|
|
489
|
+
_mergeSigIndex(index, _parseContextFile(path.join(ghDir, f)));
|
|
490
|
+
}
|
|
491
|
+
} catch (_) {}
|
|
479
492
|
_mergeSigIndex(index, _buildSigIndexFromCache(cwd));
|
|
480
493
|
return index;
|
|
481
494
|
}
|
|
@@ -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)) {
|