sigmap 7.22.1 → 7.23.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 +18 -0
- package/README.md +1 -1
- package/gen-context.js +463 -280
- 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/eval/llm-ablation.js +30 -1
- package/src/mcp/server.js +1 -1
- package/src/verify/hallucination-guard.js +10 -0
- package/src/verify/parsers.js +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [7.23.0] — 2026-06-19
|
|
14
|
+
|
|
15
|
+
Minor release — make the §9 LLM ablation produce a statistically stable number.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **§9 ablation: `--runs N` averaging + 100-task corpus (#353):** the cleaned-guard §9 result is directionally clear (grounding cuts flagged codebase-fact errors ~13 → 3 per 100) but at N=40 with single-digit raw counts a single pass is noisy. `scripts/run-llm-ablation.mjs` gains `--runs N` (default 1) that runs the full task set N times with **fresh model calls per pass** and prints a mean ± [min–max] summary; `src/eval/llm-ablation.js` adds a pure, unit-tested `aggregateRuns(aggregates[])` (mean/min/max of without/with per-100 and delta). The committed corpus (`benchmarks/llm-ablation-tasks.json`) expands from 40 to **100** real-symbol tasks (`gen-ablation-corpus.mjs` default 40 → 100) for a tighter single-run estimate. The network touch stays confined to `scripts/`; the offline harness is unchanged. Run the robust headline with `npm run benchmark:llm-ablation -- --runs 5 --save`.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [7.22.2] — 2026-06-19
|
|
23
|
+
|
|
24
|
+
Patch release — clears the two remaining `verify-ai-output` false-positive classes surfaced by the §9 ablation.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **`verify-ai-output` no longer flags camelCase placeholders or documentation-placeholder imports (#350):** continuing from #347, the Hallucination Guard now also skips camelCase/Pascal placeholder filenames (`myExample.js`, `exampleConfig.ts`) via a case-boundary rule that still flags ordinary words (`resample.js`), and the `fake-import` detector skips obvious documentation placeholders (`@scope/utils`, `some-module`, `./local-file`, `./path/to/…`) while still flagging genuine missing packages and unresolved relative imports. In the §9 re-run after #347, grounding genuinely fixed 6 mis-path flags but the guard re-flagged 4 illustrative tokens (net +2); suppressing those exposes the true grounding signal (on those outputs, with-grounding flags drop 10 → 6, delta +2 → +9). The bundled `src/verify/parsers` and `src/verify/hallucination-guard` factories were regenerated for standalone-binary parity.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
13
31
|
## [7.22.1] — 2026-06-18
|
|
14
32
|
|
|
15
33
|
Patch release — hardens the `verify-ai-output` file-path extractor against the dominant false-positive class.
|
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
88
88
|
|
|
89
89
|
```
|
|
90
90
|
Benchmark : sigmap-v7.0-main (21 repositories, including R language)
|
|
91
|
-
Date : 2026-06-
|
|
91
|
+
Date : 2026-06-19
|
|
92
92
|
|
|
93
93
|
Hit@5 : 75.6% (baseline 13.6% — 5.6× lift)
|
|
94
94
|
Token reduction: 97.0% (across 21 repos)
|
package/gen-context.js
CHANGED
|
@@ -32,6 +32,177 @@ function __require(key) {
|
|
|
32
32
|
// ── ./src/conventions/report ──
|
|
33
33
|
// ── ./src/conventions/ci ──
|
|
34
34
|
// ── ./src/eval/llm-ablation ──
|
|
35
|
+
__factories["./src/eval/llm-ablation"] = function(module, exports) {
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* LLM A/B hallucination ablation (IMPL.md §9) — the honest measurement.
|
|
39
|
+
*
|
|
40
|
+
* Runs a model twice per task — (A) no SigMap context, (B) with SigMap
|
|
41
|
+
* grounding — pipes both outputs through the hallucination guard, and reports
|
|
42
|
+
* the measured delta in flagged codebase-fact errors. The model call is
|
|
43
|
+
* INJECTED (`complete(prompt) → text`), so the harness itself is pure and
|
|
44
|
+
* offline-testable; the live model adapter lives in `scripts/run-llm-ablation.mjs`.
|
|
45
|
+
* Zero-dependency, bundle-safe (no network here).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
const { verify } = __require('./src/verify/hallucination-guard');
|
|
49
|
+
|
|
50
|
+
const path = require('path');
|
|
51
|
+
|
|
52
|
+
/** Strip a signature's trailing line anchor (` :12-20`) for prompt cleanliness. */
|
|
53
|
+
function _cleanSig(sig) {
|
|
54
|
+
return String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Build the SigMap grounding block for a repo — what we prepend to a task
|
|
59
|
+
* prompt in arm B. Conventions (the house style) + **exact signatures** grouped
|
|
60
|
+
* by file (what `get_callee_signatures` returns), so the model references the
|
|
61
|
+
* real surface instead of guessing — the actual product behavior, not a flat
|
|
62
|
+
* name dump.
|
|
63
|
+
* @param {string} cwd
|
|
64
|
+
* @param {object} [opts]
|
|
65
|
+
* @param {number} [opts.maxSignatures=150] cap on signature lines (bounds prompt size)
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function buildGrounding(cwd, opts = {}) {
|
|
69
|
+
const maxSignatures = opts.maxSignatures != null ? opts.maxSignatures : 150;
|
|
70
|
+
const parts = [];
|
|
71
|
+
|
|
72
|
+
let index = null;
|
|
73
|
+
try {
|
|
74
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
75
|
+
index = buildSigIndex(cwd);
|
|
76
|
+
} catch (_) {}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const { extractConventions } = __require('./src/conventions/extract');
|
|
80
|
+
const { renderConventionsBlock } = __require('./src/conventions/inject');
|
|
81
|
+
const files = index ? [...index.keys()] : [];
|
|
82
|
+
parts.push(renderConventionsBlock(extractConventions(cwd, files)));
|
|
83
|
+
} catch (_) {}
|
|
84
|
+
|
|
85
|
+
if (index) {
|
|
86
|
+
const lines = ['## Exact signatures (use these — do not invent symbols or paths)'];
|
|
87
|
+
let count = 0;
|
|
88
|
+
for (const [file, sigs] of index) {
|
|
89
|
+
if (count >= maxSignatures) break;
|
|
90
|
+
const rel = path.relative(cwd, file).replace(/\\/g, '/');
|
|
91
|
+
const clean = (sigs || []).map(_cleanSig).filter(Boolean);
|
|
92
|
+
if (!clean.length) continue;
|
|
93
|
+
lines.push(`### ${rel}`);
|
|
94
|
+
for (const s of clean) {
|
|
95
|
+
if (count >= maxSignatures) break;
|
|
96
|
+
lines.push(s);
|
|
97
|
+
count++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (count > 0) parts.push(lines.join('\n'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return parts.join('\n\n');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Score an answer: flagged codebase-fact errors + the issue list (the §9 metric).
|
|
108
|
+
* @param {string} answerText
|
|
109
|
+
* @param {string} cwd
|
|
110
|
+
* @returns {{ total: number, issues: object[] }}
|
|
111
|
+
*/
|
|
112
|
+
function scoreAnswerDetail(answerText, cwd) {
|
|
113
|
+
try {
|
|
114
|
+
const { issues, summary } = verify(String(answerText || ''), cwd);
|
|
115
|
+
return { total: summary.total || 0, issues: issues || [] };
|
|
116
|
+
} catch (_) {
|
|
117
|
+
return { total: 0, issues: [] };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Count flagged codebase-fact errors in an answer (the §9 metric). */
|
|
122
|
+
function scoreAnswer(answerText, cwd) {
|
|
123
|
+
return scoreAnswerDetail(answerText, cwd).total;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Run the A/B ablation over a task corpus.
|
|
128
|
+
* @param {Array<{id:string, prompt:string}>} tasks
|
|
129
|
+
* @param {string} cwd
|
|
130
|
+
* @param {(prompt:string, meta:object)=>string} complete injected model call
|
|
131
|
+
* @param {object} [opts]
|
|
132
|
+
* @param {string} [opts.grounding] precomputed grounding (else built from cwd)
|
|
133
|
+
* @param {boolean} [opts.collectIssues] attach `aIssues`/`bIssues` per task
|
|
134
|
+
* @returns {{ tasks: object[], aggregate: object }}
|
|
135
|
+
*/
|
|
136
|
+
function runAblation(tasks, cwd, complete, opts = {}) {
|
|
137
|
+
const grounding = opts.grounding != null ? opts.grounding : buildGrounding(cwd);
|
|
138
|
+
const rows = [];
|
|
139
|
+
let sumA = 0;
|
|
140
|
+
let sumB = 0;
|
|
141
|
+
|
|
142
|
+
for (const task of tasks || []) {
|
|
143
|
+
const basePrompt = task.prompt || '';
|
|
144
|
+
const groundedPrompt = grounding ? `${grounding}\n\n---\n\n${basePrompt}` : basePrompt;
|
|
145
|
+
|
|
146
|
+
const outA = String(complete(basePrompt, { id: task.id, grounded: false }) || '');
|
|
147
|
+
const outB = String(complete(groundedPrompt, { id: task.id, grounded: true }) || '');
|
|
148
|
+
|
|
149
|
+
const a = scoreAnswerDetail(outA, cwd);
|
|
150
|
+
const b = scoreAnswerDetail(outB, cwd);
|
|
151
|
+
sumA += a.total;
|
|
152
|
+
sumB += b.total;
|
|
153
|
+
const row = { id: task.id, aFlagged: a.total, bFlagged: b.total };
|
|
154
|
+
if (opts.collectIssues) { row.aIssues = a.issues; row.bIssues = b.issues; }
|
|
155
|
+
rows.push(row);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const n = rows.length;
|
|
159
|
+
const per100 = (sum) => (n > 0 ? (sum / n) * 100 : 0);
|
|
160
|
+
return {
|
|
161
|
+
tasks: rows,
|
|
162
|
+
aggregate: {
|
|
163
|
+
n,
|
|
164
|
+
withoutFlagged: sumA,
|
|
165
|
+
withFlagged: sumB,
|
|
166
|
+
delta: sumA - sumB,
|
|
167
|
+
withoutPer100: per100(sumA),
|
|
168
|
+
withPer100: per100(sumB),
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** mean/min/max of a number list (0s for an empty list). */
|
|
174
|
+
function _stats(nums) {
|
|
175
|
+
if (!nums.length) return { mean: 0, min: 0, max: 0 };
|
|
176
|
+
const sum = nums.reduce((a, b) => a + b, 0);
|
|
177
|
+
return { mean: sum / nums.length, min: Math.min(...nums), max: Math.max(...nums) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Aggregate several `runAblation` passes into a stable estimate — mean ± range
|
|
182
|
+
* of the without/with per-100 flag rates and their delta. At N=40 with tiny raw
|
|
183
|
+
* counts a single pass is noisy; averaging repeated passes gives a publishable
|
|
184
|
+
* number with an honest spread.
|
|
185
|
+
* @param {object[]} aggregates the `.aggregate` object from each runAblation pass
|
|
186
|
+
* @returns {{ runs:number, n:number, withoutPer100:object, withPer100:object, deltaPer100:object }}
|
|
187
|
+
*/
|
|
188
|
+
function aggregateRuns(aggregates) {
|
|
189
|
+
const runs = (aggregates || []).filter(Boolean);
|
|
190
|
+
const without = runs.map((a) => a.withoutPer100);
|
|
191
|
+
const withG = runs.map((a) => a.withPer100);
|
|
192
|
+
const delta = runs.map((a) => a.withoutPer100 - a.withPer100);
|
|
193
|
+
return {
|
|
194
|
+
runs: runs.length,
|
|
195
|
+
n: runs.length ? runs[0].n : 0,
|
|
196
|
+
withoutPer100: _stats(without),
|
|
197
|
+
withPer100: _stats(withG),
|
|
198
|
+
deltaPer100: _stats(delta),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = { buildGrounding, scoreAnswer, scoreAnswerDetail, runAblation, aggregateRuns };
|
|
203
|
+
|
|
204
|
+
};
|
|
205
|
+
|
|
35
206
|
// ── ./src/conventions/fix ──
|
|
36
207
|
// ── ./src/conventions/update ──
|
|
37
208
|
// ── ./src/scaffold/persist ──
|
|
@@ -7931,7 +8102,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
7931
8102
|
|
|
7932
8103
|
const SERVER_INFO = {
|
|
7933
8104
|
name: 'sigmap',
|
|
7934
|
-
version: '7.
|
|
8105
|
+
version: '7.23.0',
|
|
7935
8106
|
description: 'SigMap MCP server — code signatures on demand',
|
|
7936
8107
|
};
|
|
7937
8108
|
|
|
@@ -12787,7 +12958,10 @@ __factories["./src/verify/parsers"] = function(module, exports) {
|
|
|
12787
12958
|
|
|
12788
12959
|
// Illustrative placeholder names the model writes in prose, not repo claims:
|
|
12789
12960
|
// e.g. example.js, minimal-example.js, sample.ts, demo.js, placeholder.js.
|
|
12790
|
-
const PLACEHOLDER_RE = /(?:^|[-_.])(?:example|sample|demo|placeholder)(?:[-_.]
|
|
12961
|
+
const PLACEHOLDER_RE = /(?:^|[-_.])(?:example|sample|demo|placeholder)(?:[-_.]|s?$)/i;
|
|
12962
|
+
// camelCase / Pascal placeholders: myExample.js, exampleConfig.js, fooSample.ts.
|
|
12963
|
+
// Requires a case boundary so ordinary words (resample.js) are NOT suppressed.
|
|
12964
|
+
const PLACEHOLDER_CAMEL_RE = /(?:^|[a-z])(?:Example|Sample|Demo|Placeholder)|(?:^|[-_.])(?:example|sample|demo|placeholder)(?=[A-Z])/;
|
|
12791
12965
|
|
|
12792
12966
|
/**
|
|
12793
12967
|
* Extract fenced code blocks.
|
|
@@ -12843,7 +13017,7 @@ __factories["./src/verify/parsers"] = function(module, exports) {
|
|
|
12843
13017
|
if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
|
|
12844
13018
|
if (LIBRARY_TOKENS.has(p.toLowerCase())) continue;
|
|
12845
13019
|
const base = p.split('/').pop();
|
|
12846
|
-
if (PLACEHOLDER_RE.test(base)) continue;
|
|
13020
|
+
if (PLACEHOLDER_RE.test(base) || PLACEHOLDER_CAMEL_RE.test(base)) continue;
|
|
12847
13021
|
if (!seen.has(p)) seen.set(p, i + 1);
|
|
12848
13022
|
}
|
|
12849
13023
|
}
|
|
@@ -13293,317 +13467,326 @@ module.exports = { renderReportHtml, renderReportMarkdown, escapeHtml };
|
|
|
13293
13467
|
|
|
13294
13468
|
// ── ./src/verify/hallucination-guard ──
|
|
13295
13469
|
__factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
13296
|
-
|
|
13470
|
+
|
|
13471
|
+
/**
|
|
13472
|
+
* Hallucination Guard — deterministic core (Reliable MVP, v6.15.0).
|
|
13473
|
+
*
|
|
13474
|
+
* Given the text of an AI answer, flag claims that do not match the repo:
|
|
13475
|
+
* - fake-file : a referenced path is not on disk
|
|
13476
|
+
* - fake-test-file : a referenced *test* path is not on disk (sub-type)
|
|
13477
|
+
* - fake-import : a relative import does not resolve; a bare import is
|
|
13478
|
+
* absent from package.json deps (builtins allow-listed)
|
|
13479
|
+
* - fake-symbol : a called function/class is absent from the symbol index
|
|
13480
|
+
* - fake-npm-script: `npm run X` where X is not a package.json script
|
|
13481
|
+
*
|
|
13482
|
+
* Each issue carries a `confidence` (detection certainty) and, where a near
|
|
13483
|
+
* match exists, a heuristic `suggestion` ("Did you mean …?"). No network, no
|
|
13484
|
+
* LLM. Reuses SigMap primitives (buildSigIndex) but every external dependency
|
|
13485
|
+
* is injectable via `opts` so the core stays unit-testable.
|
|
13486
|
+
*/
|
|
13297
13487
|
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13301
|
-
|
|
13302
|
-
|
|
13303
|
-
|
|
13304
|
-
|
|
13305
|
-
|
|
13306
|
-
|
|
13307
|
-
|
|
13308
|
-
|
|
13309
|
-
|
|
13310
|
-
|
|
13311
|
-
|
|
13312
|
-
|
|
13313
|
-
|
|
13488
|
+
const fs = require('fs');
|
|
13489
|
+
const path = require('path');
|
|
13490
|
+
const parsers = __require('./src/verify/parsers');
|
|
13491
|
+
const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
|
|
13492
|
+
|
|
13493
|
+
// A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
|
|
13494
|
+
// a tests/__tests__ directory). Used to flag fake-test-file separately.
|
|
13495
|
+
const TEST_PATH_RE = /(?:\.(?:test|spec)\.[mc]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)|(?:(?:^|\/)test_[^/]+\.py$)|(?:_test\.py$)|(?:(?:^|\/)tests?\/)/i;
|
|
13496
|
+
function isTestPath(p) { return TEST_PATH_RE.test(p); }
|
|
13497
|
+
|
|
13498
|
+
const NODE_BUILTINS = new Set([
|
|
13499
|
+
'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https', 'crypto',
|
|
13500
|
+
'child_process', 'url', 'querystring', 'assert', 'zlib', 'readline', 'net',
|
|
13501
|
+
'tls', 'dns', 'buffer', 'process', 'vm', 'module', 'console', 'timers',
|
|
13502
|
+
'string_decoder', 'perf_hooks', 'worker_threads', 'cluster', 'dgram', 'v8',
|
|
13503
|
+
'tty', 'repl', 'async_hooks', 'inspector', 'fs/promises', 'path/posix',
|
|
13504
|
+
]);
|
|
13314
13505
|
|
|
13315
|
-
const
|
|
13316
|
-
|
|
13317
|
-
|
|
13318
|
-
|
|
13319
|
-
|
|
13320
|
-
|
|
13321
|
-
// a tests/__tests__ directory). Used to flag fake-test-file separately.
|
|
13322
|
-
const TEST_PATH_RE = /(?:\.(?:test|spec)\.[mc]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)|(?:(?:^|\/)test_[^/]+\.py$)|(?:_test\.py$)|(?:(?:^|\/)tests?\/)/i;
|
|
13323
|
-
function isTestPath(p) { return TEST_PATH_RE.test(p); }
|
|
13324
|
-
|
|
13325
|
-
const NODE_BUILTINS = new Set([
|
|
13326
|
-
'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https', 'crypto',
|
|
13327
|
-
'child_process', 'url', 'querystring', 'assert', 'zlib', 'readline', 'net',
|
|
13328
|
-
'tls', 'dns', 'buffer', 'process', 'vm', 'module', 'console', 'timers',
|
|
13329
|
-
'string_decoder', 'perf_hooks', 'worker_threads', 'cluster', 'dgram', 'v8',
|
|
13330
|
-
'tty', 'repl', 'async_hooks', 'inspector', 'fs/promises', 'path/posix',
|
|
13331
|
-
]);
|
|
13332
|
-
|
|
13333
|
-
const PY_BUILTINS = new Set([
|
|
13334
|
-
'os', 'sys', 're', 'json', 'math', 'typing', 'collections', 'itertools',
|
|
13335
|
-
'functools', 'datetime', 'pathlib', 'subprocess', 'abc', 'dataclasses',
|
|
13336
|
-
'enum', 'io', 'time', 'random', 'logging', 'argparse', 'unittest', 'asyncio',
|
|
13337
|
-
'copy', 'hashlib', 'threading', 'string', 'csv', 'glob', 'shutil', 'tempfile',
|
|
13338
|
-
]);
|
|
13339
|
-
|
|
13340
|
-
const LANG_GLOBALS = new Set([
|
|
13341
|
-
// JS
|
|
13342
|
-
'console', 'require', 'module', 'exports', 'process', 'Object', 'Array',
|
|
13343
|
-
'String', 'Number', 'Boolean', 'Math', 'JSON', 'Date', 'Promise', 'Map',
|
|
13344
|
-
'Set', 'WeakMap', 'WeakSet', 'RegExp', 'Error', 'Symbol', 'parseInt',
|
|
13345
|
-
'parseFloat', 'isNaN', 'setTimeout', 'setInterval', 'clearTimeout', 'fetch',
|
|
13346
|
-
'Buffer', 'Function', 'eval', 'encodeURIComponent', 'decodeURIComponent',
|
|
13347
|
-
// Python
|
|
13348
|
-
'print', 'len', 'range', 'str', 'int', 'float', 'dict', 'list', 'tuple',
|
|
13349
|
-
'set', 'bool', 'open', 'enumerate', 'zip', 'map', 'filter', 'sorted',
|
|
13350
|
-
'sum', 'min', 'max', 'abs', 'isinstance', 'super', 'type', 'getattr',
|
|
13351
|
-
'setattr', 'hasattr',
|
|
13352
|
-
]);
|
|
13353
|
-
|
|
13354
|
-
const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
|
|
13355
|
-
const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];
|
|
13506
|
+
const PY_BUILTINS = new Set([
|
|
13507
|
+
'os', 'sys', 're', 'json', 'math', 'typing', 'collections', 'itertools',
|
|
13508
|
+
'functools', 'datetime', 'pathlib', 'subprocess', 'abc', 'dataclasses',
|
|
13509
|
+
'enum', 'io', 'time', 'random', 'logging', 'argparse', 'unittest', 'asyncio',
|
|
13510
|
+
'copy', 'hashlib', 'threading', 'string', 'csv', 'glob', 'shutil', 'tempfile',
|
|
13511
|
+
]);
|
|
13356
13512
|
|
|
13357
|
-
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
|
|
13361
|
-
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
for (const sig of sigs) {
|
|
13371
|
-
const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
13372
|
-
const ids = cleaned.match(/[A-Za-z_$][\w$]*/g) || [];
|
|
13373
|
-
for (const id of ids) set.add(id);
|
|
13374
|
-
}
|
|
13375
|
-
}
|
|
13376
|
-
symbolCandidates = buildSymbolCandidates(idx);
|
|
13377
|
-
} catch (_) {}
|
|
13378
|
-
return { set, fileKeys, symbolCandidates };
|
|
13379
|
-
}
|
|
13513
|
+
const LANG_GLOBALS = new Set([
|
|
13514
|
+
// JS
|
|
13515
|
+
'console', 'require', 'module', 'exports', 'process', 'Object', 'Array',
|
|
13516
|
+
'String', 'Number', 'Boolean', 'Math', 'JSON', 'Date', 'Promise', 'Map',
|
|
13517
|
+
'Set', 'WeakMap', 'WeakSet', 'RegExp', 'Error', 'Symbol', 'parseInt',
|
|
13518
|
+
'parseFloat', 'isNaN', 'setTimeout', 'setInterval', 'clearTimeout', 'fetch',
|
|
13519
|
+
'Buffer', 'Function', 'eval', 'encodeURIComponent', 'decodeURIComponent',
|
|
13520
|
+
// Python
|
|
13521
|
+
'print', 'len', 'range', 'str', 'int', 'float', 'dict', 'list', 'tuple',
|
|
13522
|
+
'set', 'bool', 'open', 'enumerate', 'zip', 'map', 'filter', 'sorted',
|
|
13523
|
+
'sum', 'min', 'max', 'abs', 'isinstance', 'super', 'type', 'getattr',
|
|
13524
|
+
'setattr', 'hasattr',
|
|
13525
|
+
]);
|
|
13380
13526
|
|
|
13381
|
-
|
|
13382
|
-
|
|
13383
|
-
const deps = new Set();
|
|
13384
|
-
let hasPkg = false;
|
|
13385
|
-
try {
|
|
13386
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
13387
|
-
hasPkg = true;
|
|
13388
|
-
for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
13389
|
-
if (pkg[k] && typeof pkg[k] === 'object') {
|
|
13390
|
-
for (const name of Object.keys(pkg[k])) deps.add(name);
|
|
13391
|
-
}
|
|
13392
|
-
}
|
|
13393
|
-
} catch (_) {}
|
|
13394
|
-
return { deps, hasPkg };
|
|
13395
|
-
}
|
|
13527
|
+
const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
|
|
13528
|
+
const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];
|
|
13396
13529
|
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
const
|
|
13400
|
-
|
|
13401
|
-
|
|
13402
|
-
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
} catch (_) {}
|
|
13406
|
-
return scripts;
|
|
13407
|
-
}
|
|
13530
|
+
// Obvious documentation-placeholder imports the model writes in illustrative
|
|
13531
|
+
// snippets — not real dependency claims. e.g. @scope/utils, some-module, ./local-file.
|
|
13532
|
+
const PLACEHOLDER_IMPORT_RE = new RegExp([
|
|
13533
|
+
'^@(?:scope|org|your-org|my-org|company|example)(?:/|$)', // @scope/utils
|
|
13534
|
+
'(?:^|/)(?:some|your|my)-(?:module|package|lib|component|file|dep)(?:$|/)', // some-module
|
|
13535
|
+
'(?:^|/)(?:local-file|your-file|my-file|module-name|package-name|your-package|example-package)(?:$|/)',
|
|
13536
|
+
'(?:^|/)path/to/', // ./path/to/x
|
|
13537
|
+
].join('|'), 'i');
|
|
13408
13538
|
|
|
13409
|
-
/**
|
|
13410
|
-
|
|
13411
|
-
|
|
13412
|
-
|
|
13539
|
+
/**
|
|
13540
|
+
* Build the set of known symbol identifiers from the SigMap signature index,
|
|
13541
|
+
* plus `{ name, file, line }` candidates (for closest-match suggestions).
|
|
13542
|
+
*/
|
|
13543
|
+
function buildSymbolSet(cwd) {
|
|
13544
|
+
const set = new Set();
|
|
13545
|
+
let fileKeys = [];
|
|
13546
|
+
let symbolCandidates = [];
|
|
13413
13547
|
try {
|
|
13414
|
-
|
|
13548
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
13549
|
+
const idx = buildSigIndex(cwd);
|
|
13550
|
+
fileKeys = [...idx.keys()];
|
|
13551
|
+
for (const sigs of idx.values()) {
|
|
13552
|
+
for (const sig of sigs) {
|
|
13553
|
+
const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
13554
|
+
const ids = cleaned.match(/[A-Za-z_$][\w$]*/g) || [];
|
|
13555
|
+
for (const id of ids) set.add(id);
|
|
13556
|
+
}
|
|
13557
|
+
}
|
|
13558
|
+
symbolCandidates = buildSymbolCandidates(idx);
|
|
13415
13559
|
} catch (_) {}
|
|
13560
|
+
return { set, fileKeys, symbolCandidates };
|
|
13416
13561
|
}
|
|
13417
|
-
return false;
|
|
13418
|
-
}
|
|
13419
13562
|
|
|
13420
|
-
/**
|
|
13421
|
-
function
|
|
13422
|
-
|
|
13423
|
-
|
|
13563
|
+
/** Load declared dependency names from package.json. */
|
|
13564
|
+
function loadDeps(cwd) {
|
|
13565
|
+
const deps = new Set();
|
|
13566
|
+
let hasPkg = false;
|
|
13424
13567
|
try {
|
|
13425
|
-
|
|
13568
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
13569
|
+
hasPkg = true;
|
|
13570
|
+
for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
13571
|
+
if (pkg[k] && typeof pkg[k] === 'object') {
|
|
13572
|
+
for (const name of Object.keys(pkg[k])) deps.add(name);
|
|
13573
|
+
}
|
|
13574
|
+
}
|
|
13426
13575
|
} catch (_) {}
|
|
13576
|
+
return { deps, hasPkg };
|
|
13427
13577
|
}
|
|
13428
|
-
|
|
13578
|
+
|
|
13579
|
+
/** Load the set of npm script names declared in package.json. */
|
|
13580
|
+
function loadScripts(cwd) {
|
|
13581
|
+
const scripts = new Set();
|
|
13429
13582
|
try {
|
|
13430
|
-
|
|
13583
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
13584
|
+
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
13585
|
+
for (const name of Object.keys(pkg.scripts)) scripts.add(name);
|
|
13586
|
+
}
|
|
13431
13587
|
} catch (_) {}
|
|
13588
|
+
return scripts;
|
|
13432
13589
|
}
|
|
13433
|
-
// Fall back to basename match against the indexed file set (the answer's
|
|
13434
|
-
// import is relative to a file we cannot know, so a name match is enough
|
|
13435
|
-
// to avoid false positives).
|
|
13436
|
-
const wantBase = path.basename(mod).replace(/\.[^.]+$/, '').toLowerCase();
|
|
13437
|
-
return fileBasenames.has(wantBase);
|
|
13438
|
-
}
|
|
13439
|
-
|
|
13440
|
-
/**
|
|
13441
|
-
* Verify an AI answer against the repository.
|
|
13442
|
-
*
|
|
13443
|
-
* Each issue has the shape:
|
|
13444
|
-
* { type, value, line, location, message, confidence, suggestion }
|
|
13445
|
-
* where `confidence` is the *detection* certainty ('high' for path/dep/script
|
|
13446
|
-
* checks, 'medium' for symbol checks) and `suggestion` is a heuristic
|
|
13447
|
-
* closest-match hint (or null).
|
|
13448
|
-
*
|
|
13449
|
-
* @param {string} answerText
|
|
13450
|
-
* @param {string} cwd
|
|
13451
|
-
* @param {object} [opts]
|
|
13452
|
-
* @param {Set<string>} [opts.symbolSet] override known symbols
|
|
13453
|
-
* @param {Array} [opts.symbolCandidates] override { name, file, line } list
|
|
13454
|
-
* @param {Array<string>} [opts.fileCandidates] override repo file paths (suggestions)
|
|
13455
|
-
* @param {Set<string>} [opts.deps] override package deps
|
|
13456
|
-
* @param {Set<string>} [opts.scripts] override package.json script names
|
|
13457
|
-
* @param {boolean} [opts.hasPkg] whether a package.json exists
|
|
13458
|
-
* @param {(ref: string) => boolean} [opts.fileExists] override file check
|
|
13459
|
-
* @param {(mod: string) => boolean} [opts.relativeResolvable] override rel-import check
|
|
13460
|
-
* @returns {{ issues: object[], summary: object }}
|
|
13461
|
-
*/
|
|
13462
|
-
function verify(answerText, cwd, opts = {}) {
|
|
13463
|
-
let symbolSet = opts.symbolSet;
|
|
13464
|
-
let fileBasenames = opts.fileBasenames;
|
|
13465
|
-
let symbolCandidates = opts.symbolCandidates || [];
|
|
13466
|
-
let fileCandidates = opts.fileCandidates || [];
|
|
13467
|
-
if (!symbolSet) {
|
|
13468
|
-
const built = buildSymbolSet(cwd);
|
|
13469
|
-
symbolSet = built.set;
|
|
13470
|
-
fileBasenames = new Set(built.fileKeys.map(
|
|
13471
|
-
(k) => path.basename(k).replace(/\.[^.]+$/, '').toLowerCase()
|
|
13472
|
-
));
|
|
13473
|
-
symbolCandidates = built.symbolCandidates;
|
|
13474
|
-
fileCandidates = built.fileKeys;
|
|
13475
|
-
}
|
|
13476
|
-
if (!fileBasenames) fileBasenames = new Set();
|
|
13477
|
-
|
|
13478
|
-
let deps = opts.deps;
|
|
13479
|
-
let hasPkg = opts.hasPkg;
|
|
13480
|
-
if (!deps) {
|
|
13481
|
-
const loaded = loadDeps(cwd);
|
|
13482
|
-
deps = loaded.deps;
|
|
13483
|
-
if (hasPkg === undefined) hasPkg = loaded.hasPkg;
|
|
13484
|
-
}
|
|
13485
|
-
const scripts = opts.scripts || (hasPkg ? loadScripts(cwd) : new Set());
|
|
13486
|
-
|
|
13487
|
-
const fileExists = opts.fileExists || ((ref) => defaultFileExists(cwd, ref));
|
|
13488
|
-
const relativeResolvable = opts.relativeResolvable
|
|
13489
|
-
|| ((mod) => defaultRelativeResolvable(cwd, mod, fileBasenames));
|
|
13490
|
-
|
|
13491
|
-
// Pre-derive basename candidates for file suggestions (compare on basename so
|
|
13492
|
-
// a wrong directory still surfaces the right file).
|
|
13493
|
-
const fileBasenameCandidates = fileCandidates.map((f) => ({ name: path.basename(f), file: f }));
|
|
13494
|
-
|
|
13495
|
-
const issues = [];
|
|
13496
|
-
const dedupe = new Set();
|
|
13497
|
-
const add = (issue) => {
|
|
13498
|
-
const key = `${issue.type}::${issue.value}`;
|
|
13499
|
-
if (dedupe.has(key)) return;
|
|
13500
|
-
dedupe.add(key);
|
|
13501
|
-
if (!('suggestion' in issue)) issue.suggestion = null;
|
|
13502
|
-
issue.location = `L${issue.line}`;
|
|
13503
|
-
issues.push(issue);
|
|
13504
|
-
};
|
|
13505
13590
|
|
|
13506
|
-
|
|
13507
|
-
|
|
13508
|
-
|
|
13509
|
-
const
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
message: `${isTest ? 'Test file' : 'File'} not found on disk: ${p}`,
|
|
13516
|
-
confidence: 'high',
|
|
13517
|
-
suggestion: match ? formatSuggestion(match, false) : null,
|
|
13518
|
-
});
|
|
13591
|
+
/** Default file-existence check: resolve a referenced path against cwd. */
|
|
13592
|
+
function defaultFileExists(cwd, ref) {
|
|
13593
|
+
const clean = ref.replace(/^\.\//, '');
|
|
13594
|
+
for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
|
|
13595
|
+
try {
|
|
13596
|
+
if (fs.existsSync(c)) return true;
|
|
13597
|
+
} catch (_) {}
|
|
13598
|
+
}
|
|
13599
|
+
return false;
|
|
13519
13600
|
}
|
|
13520
13601
|
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
|
|
13602
|
+
/** Default relative-import resolver: fs candidates + basename match in index. */
|
|
13603
|
+
function defaultRelativeResolvable(cwd, mod, fileBasenames) {
|
|
13604
|
+
const base = path.resolve(cwd, mod);
|
|
13605
|
+
for (const e of REL_EXTS) {
|
|
13606
|
+
try {
|
|
13607
|
+
if (fs.existsSync(base + e)) return true;
|
|
13608
|
+
} catch (_) {}
|
|
13528
13609
|
}
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
13533
|
-
if (NODE_BUILTINS.has(imp.module) || NODE_BUILTINS.has(top)) continue;
|
|
13534
|
-
if (top.startsWith('@')) {
|
|
13535
|
-
const scoped = imp.module.split('/').slice(0, 2).join('/');
|
|
13536
|
-
if (deps.has(scoped) || deps.has(imp.module)) continue;
|
|
13537
|
-
} else if (deps.has(top) || deps.has(imp.module)) {
|
|
13538
|
-
continue;
|
|
13539
|
-
}
|
|
13540
|
-
const match = closestMatch(top, [...deps], { minLen: 3 });
|
|
13541
|
-
add({
|
|
13542
|
-
type: 'fake-import',
|
|
13543
|
-
value: imp.module,
|
|
13544
|
-
line: imp.line,
|
|
13545
|
-
message: `Package not in dependencies: ${imp.module}`,
|
|
13546
|
-
confidence: 'high',
|
|
13547
|
-
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
13548
|
-
});
|
|
13610
|
+
for (const idx of REL_INDEX) {
|
|
13611
|
+
try {
|
|
13612
|
+
if (fs.existsSync(path.join(base, idx))) return true;
|
|
13613
|
+
} catch (_) {}
|
|
13549
13614
|
}
|
|
13550
|
-
//
|
|
13615
|
+
// Fall back to basename match against the indexed file set (the answer's
|
|
13616
|
+
// import is relative to a file we cannot know, so a name match is enough
|
|
13617
|
+
// to avoid false positives).
|
|
13618
|
+
const wantBase = path.basename(mod).replace(/\.[^.]+$/, '').toLowerCase();
|
|
13619
|
+
return fileBasenames.has(wantBase);
|
|
13551
13620
|
}
|
|
13552
13621
|
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13622
|
+
/**
|
|
13623
|
+
* Verify an AI answer against the repository.
|
|
13624
|
+
*
|
|
13625
|
+
* Each issue has the shape:
|
|
13626
|
+
* { type, value, line, location, message, confidence, suggestion }
|
|
13627
|
+
* where `confidence` is the *detection* certainty ('high' for path/dep/script
|
|
13628
|
+
* checks, 'medium' for symbol checks) and `suggestion` is a heuristic
|
|
13629
|
+
* closest-match hint (or null).
|
|
13630
|
+
*
|
|
13631
|
+
* @param {string} answerText
|
|
13632
|
+
* @param {string} cwd
|
|
13633
|
+
* @param {object} [opts]
|
|
13634
|
+
* @param {Set<string>} [opts.symbolSet] override known symbols
|
|
13635
|
+
* @param {Array} [opts.symbolCandidates] override { name, file, line } list
|
|
13636
|
+
* @param {Array<string>} [opts.fileCandidates] override repo file paths (suggestions)
|
|
13637
|
+
* @param {Set<string>} [opts.deps] override package deps
|
|
13638
|
+
* @param {Set<string>} [opts.scripts] override package.json script names
|
|
13639
|
+
* @param {boolean} [opts.hasPkg] whether a package.json exists
|
|
13640
|
+
* @param {(ref: string) => boolean} [opts.fileExists] override file check
|
|
13641
|
+
* @param {(mod: string) => boolean} [opts.relativeResolvable] override rel-import check
|
|
13642
|
+
* @returns {{ issues: object[], summary: object }}
|
|
13643
|
+
*/
|
|
13644
|
+
function verify(answerText, cwd, opts = {}) {
|
|
13645
|
+
let symbolSet = opts.symbolSet;
|
|
13646
|
+
let fileBasenames = opts.fileBasenames;
|
|
13647
|
+
let symbolCandidates = opts.symbolCandidates || [];
|
|
13648
|
+
let fileCandidates = opts.fileCandidates || [];
|
|
13649
|
+
if (!symbolSet) {
|
|
13650
|
+
const built = buildSymbolSet(cwd);
|
|
13651
|
+
symbolSet = built.set;
|
|
13652
|
+
fileBasenames = new Set(built.fileKeys.map(
|
|
13653
|
+
(k) => path.basename(k).replace(/\.[^.]+$/, '').toLowerCase()
|
|
13654
|
+
));
|
|
13655
|
+
symbolCandidates = built.symbolCandidates;
|
|
13656
|
+
fileCandidates = built.fileKeys;
|
|
13567
13657
|
}
|
|
13568
|
-
|
|
13658
|
+
if (!fileBasenames) fileBasenames = new Set();
|
|
13659
|
+
|
|
13660
|
+
let deps = opts.deps;
|
|
13661
|
+
let hasPkg = opts.hasPkg;
|
|
13662
|
+
if (!deps) {
|
|
13663
|
+
const loaded = loadDeps(cwd);
|
|
13664
|
+
deps = loaded.deps;
|
|
13665
|
+
if (hasPkg === undefined) hasPkg = loaded.hasPkg;
|
|
13666
|
+
}
|
|
13667
|
+
const scripts = opts.scripts || (hasPkg ? loadScripts(cwd) : new Set());
|
|
13668
|
+
|
|
13669
|
+
const fileExists = opts.fileExists || ((ref) => defaultFileExists(cwd, ref));
|
|
13670
|
+
const relativeResolvable = opts.relativeResolvable
|
|
13671
|
+
|| ((mod) => defaultRelativeResolvable(cwd, mod, fileBasenames));
|
|
13569
13672
|
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13573
|
-
|
|
13574
|
-
|
|
13673
|
+
// Pre-derive basename candidates for file suggestions (compare on basename so
|
|
13674
|
+
// a wrong directory still surfaces the right file).
|
|
13675
|
+
const fileBasenameCandidates = fileCandidates.map((f) => ({ name: path.basename(f), file: f }));
|
|
13676
|
+
|
|
13677
|
+
const issues = [];
|
|
13678
|
+
const dedupe = new Set();
|
|
13679
|
+
const add = (issue) => {
|
|
13680
|
+
const key = `${issue.type}::${issue.value}`;
|
|
13681
|
+
if (dedupe.has(key)) return;
|
|
13682
|
+
dedupe.add(key);
|
|
13683
|
+
if (!('suggestion' in issue)) issue.suggestion = null;
|
|
13684
|
+
issue.location = `L${issue.line}`;
|
|
13685
|
+
issues.push(issue);
|
|
13686
|
+
};
|
|
13687
|
+
|
|
13688
|
+
// 1. fake-file / fake-test-file
|
|
13689
|
+
for (const { path: p, line } of parsers.extractFilePaths(answerText)) {
|
|
13690
|
+
if (fileExists(p)) continue;
|
|
13691
|
+
const isTest = isTestPath(p);
|
|
13692
|
+
const match = closestMatch(path.basename(p), fileBasenameCandidates, { minLen: 4 });
|
|
13575
13693
|
add({
|
|
13576
|
-
type: 'fake-
|
|
13577
|
-
value:
|
|
13694
|
+
type: isTest ? 'fake-test-file' : 'fake-file',
|
|
13695
|
+
value: p,
|
|
13578
13696
|
line,
|
|
13579
|
-
message:
|
|
13697
|
+
message: `${isTest ? 'Test file' : 'File'} not found on disk: ${p}`,
|
|
13580
13698
|
confidence: 'high',
|
|
13581
|
-
suggestion: match ? formatSuggestion(
|
|
13699
|
+
suggestion: match ? formatSuggestion(match, false) : null,
|
|
13582
13700
|
});
|
|
13583
13701
|
}
|
|
13584
|
-
}
|
|
13585
13702
|
|
|
13586
|
-
|
|
13703
|
+
// 2. fake-import
|
|
13704
|
+
for (const imp of parsers.extractImports(answerText)) {
|
|
13705
|
+
if (PLACEHOLDER_IMPORT_RE.test(imp.module)) continue;
|
|
13706
|
+
if (imp.relative) {
|
|
13707
|
+
if (!relativeResolvable(imp.module)) {
|
|
13708
|
+
add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Import does not resolve: ${imp.module}`, confidence: 'high' });
|
|
13709
|
+
}
|
|
13710
|
+
continue;
|
|
13711
|
+
}
|
|
13712
|
+
// Bare module — only verifiable for JS when a package.json exists.
|
|
13713
|
+
const top = imp.module.split('/')[0];
|
|
13714
|
+
if (imp.kind === 'js') {
|
|
13715
|
+
if (!hasPkg) continue;
|
|
13716
|
+
if (NODE_BUILTINS.has(imp.module) || NODE_BUILTINS.has(top)) continue;
|
|
13717
|
+
if (top.startsWith('@')) {
|
|
13718
|
+
const scoped = imp.module.split('/').slice(0, 2).join('/');
|
|
13719
|
+
if (deps.has(scoped) || deps.has(imp.module)) continue;
|
|
13720
|
+
} else if (deps.has(top) || deps.has(imp.module)) {
|
|
13721
|
+
continue;
|
|
13722
|
+
}
|
|
13723
|
+
const match = closestMatch(top, [...deps], { minLen: 3 });
|
|
13724
|
+
add({
|
|
13725
|
+
type: 'fake-import',
|
|
13726
|
+
value: imp.module,
|
|
13727
|
+
line: imp.line,
|
|
13728
|
+
message: `Package not in dependencies: ${imp.module}`,
|
|
13729
|
+
confidence: 'high',
|
|
13730
|
+
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
13731
|
+
});
|
|
13732
|
+
}
|
|
13733
|
+
// Python bare imports: stdlib is unbounded offline — skip to keep precision.
|
|
13734
|
+
}
|
|
13735
|
+
|
|
13736
|
+
// 3. fake-symbol
|
|
13737
|
+
if (symbolSet.size > 0) {
|
|
13738
|
+
for (const { name, line } of parsers.extractSymbols(answerText)) {
|
|
13739
|
+
if (symbolSet.has(name)) continue;
|
|
13740
|
+
if (LANG_GLOBALS.has(name) || NODE_BUILTINS.has(name) || PY_BUILTINS.has(name)) continue;
|
|
13741
|
+
const match = closestMatch(name, symbolCandidates, { minLen: 4 });
|
|
13742
|
+
add({
|
|
13743
|
+
type: 'fake-symbol',
|
|
13744
|
+
value: name,
|
|
13745
|
+
line,
|
|
13746
|
+
message: `Symbol not found in repo index: ${name}()`,
|
|
13747
|
+
confidence: 'medium',
|
|
13748
|
+
suggestion: match ? formatSuggestion(match, true) : null,
|
|
13749
|
+
});
|
|
13750
|
+
}
|
|
13751
|
+
}
|
|
13752
|
+
|
|
13753
|
+
// 4. fake-npm-script
|
|
13754
|
+
if (hasPkg && scripts.size > 0) {
|
|
13755
|
+
for (const { name, line } of parsers.extractNpmScripts(answerText)) {
|
|
13756
|
+
if (scripts.has(name)) continue;
|
|
13757
|
+
const match = closestMatch(name, [...scripts], { minLen: 2 });
|
|
13758
|
+
add({
|
|
13759
|
+
type: 'fake-npm-script',
|
|
13760
|
+
value: name,
|
|
13761
|
+
line,
|
|
13762
|
+
message: `npm script not in package.json: ${name}`,
|
|
13763
|
+
confidence: 'high',
|
|
13764
|
+
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
13765
|
+
});
|
|
13766
|
+
}
|
|
13767
|
+
}
|
|
13587
13768
|
|
|
13588
|
-
|
|
13589
|
-
'fake-file': 0, 'fake-test-file': 0, 'fake-import': 0,
|
|
13590
|
-
'fake-symbol': 0, 'fake-npm-script': 0,
|
|
13591
|
-
};
|
|
13592
|
-
for (const i of issues) byType[i.type] = (byType[i.type] || 0) + 1;
|
|
13593
|
-
|
|
13594
|
-
const summary = {
|
|
13595
|
-
total: issues.length,
|
|
13596
|
-
byType,
|
|
13597
|
-
clean: issues.length === 0,
|
|
13598
|
-
symbolsIndexed: symbolSet.size,
|
|
13599
|
-
withSuggestion: issues.filter((i) => i.suggestion).length,
|
|
13600
|
-
};
|
|
13769
|
+
issues.sort((a, b) => a.line - b.line);
|
|
13601
13770
|
|
|
13602
|
-
|
|
13603
|
-
|
|
13771
|
+
const byType = {
|
|
13772
|
+
'fake-file': 0, 'fake-test-file': 0, 'fake-import': 0,
|
|
13773
|
+
'fake-symbol': 0, 'fake-npm-script': 0,
|
|
13774
|
+
};
|
|
13775
|
+
for (const i of issues) byType[i.type] = (byType[i.type] || 0) + 1;
|
|
13776
|
+
|
|
13777
|
+
const summary = {
|
|
13778
|
+
total: issues.length,
|
|
13779
|
+
byType,
|
|
13780
|
+
clean: issues.length === 0,
|
|
13781
|
+
symbolsIndexed: symbolSet.size,
|
|
13782
|
+
withSuggestion: issues.filter((i) => i.suggestion).length,
|
|
13783
|
+
};
|
|
13604
13784
|
|
|
13605
|
-
|
|
13785
|
+
return { issues, summary };
|
|
13786
|
+
}
|
|
13606
13787
|
|
|
13788
|
+
module.exports = { verify, buildSymbolSet, loadDeps, loadScripts, isTestPath };
|
|
13789
|
+
|
|
13607
13790
|
};
|
|
13608
13791
|
|
|
13609
13792
|
const fs = require('fs');
|
|
@@ -13622,7 +13805,7 @@ function __tryGit(args, opts = {}) {
|
|
|
13622
13805
|
catch (_) { return ''; }
|
|
13623
13806
|
}
|
|
13624
13807
|
|
|
13625
|
-
const VERSION = '7.
|
|
13808
|
+
const VERSION = '7.23.0';
|
|
13626
13809
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
13627
13810
|
|
|
13628
13811
|
function requireSourceOrBundled(key) {
|
package/llms-full.txt
CHANGED
|
@@ -9,13 +9,13 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.
|
|
12
|
+
# Version: 7.23.0 | Benchmark: sigmap-v7.0-main (2026-06-19)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
## Core metrics (benchmark: sigmap-v7.0-main, 2026-06-
|
|
18
|
+
## Core metrics (benchmark: sigmap-v7.0-main, 2026-06-19)
|
|
19
19
|
|
|
20
20
|
| Metric | Without SigMap | With SigMap |
|
|
21
21
|
|--------|----------------|-------------|
|
package/llms.txt
CHANGED
|
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
|
|
|
9
9
|
grounded. Deterministic, offline, no embeddings or vector database. Works with
|
|
10
10
|
Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
11
11
|
|
|
12
|
-
# Version: 7.
|
|
12
|
+
# Version: 7.23.0 | Benchmark: sigmap-v7.0-main (2026-06-19)
|
|
13
13
|
# Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
|
|
14
14
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
15
15
|
|
|
@@ -21,7 +21,7 @@ Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
21
21
|
- No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
|
|
22
22
|
- Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
|
|
23
23
|
|
|
24
|
-
## Core metrics (benchmark: sigmap-v7.0-main, 2026-06-
|
|
24
|
+
## Core metrics (benchmark: sigmap-v7.0-main, 2026-06-19)
|
|
25
25
|
|
|
26
26
|
- hit@5 retrieval: 75.6% vs 13.6% random baseline (5.6× lift)
|
|
27
27
|
- Token reduction: 97.0% average across benchmark repos
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.23.0",
|
|
4
4
|
"description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
|
|
5
5
|
"main": "packages/core/index.js",
|
|
6
6
|
"exports": {
|
package/src/eval/llm-ablation.js
CHANGED
|
@@ -136,4 +136,33 @@ function runAblation(tasks, cwd, complete, opts = {}) {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
/** mean/min/max of a number list (0s for an empty list). */
|
|
140
|
+
function _stats(nums) {
|
|
141
|
+
if (!nums.length) return { mean: 0, min: 0, max: 0 };
|
|
142
|
+
const sum = nums.reduce((a, b) => a + b, 0);
|
|
143
|
+
return { mean: sum / nums.length, min: Math.min(...nums), max: Math.max(...nums) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Aggregate several `runAblation` passes into a stable estimate — mean ± range
|
|
148
|
+
* of the without/with per-100 flag rates and their delta. At N=40 with tiny raw
|
|
149
|
+
* counts a single pass is noisy; averaging repeated passes gives a publishable
|
|
150
|
+
* number with an honest spread.
|
|
151
|
+
* @param {object[]} aggregates the `.aggregate` object from each runAblation pass
|
|
152
|
+
* @returns {{ runs:number, n:number, withoutPer100:object, withPer100:object, deltaPer100:object }}
|
|
153
|
+
*/
|
|
154
|
+
function aggregateRuns(aggregates) {
|
|
155
|
+
const runs = (aggregates || []).filter(Boolean);
|
|
156
|
+
const without = runs.map((a) => a.withoutPer100);
|
|
157
|
+
const withG = runs.map((a) => a.withPer100);
|
|
158
|
+
const delta = runs.map((a) => a.withoutPer100 - a.withPer100);
|
|
159
|
+
return {
|
|
160
|
+
runs: runs.length,
|
|
161
|
+
n: runs.length ? runs[0].n : 0,
|
|
162
|
+
withoutPer100: _stats(without),
|
|
163
|
+
withPer100: _stats(withG),
|
|
164
|
+
deltaPer100: _stats(delta),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { buildGrounding, scoreAnswer, scoreAnswerDetail, runAblation, aggregateRuns };
|
package/src/mcp/server.js
CHANGED
|
@@ -59,6 +59,15 @@ const LANG_GLOBALS = new Set([
|
|
|
59
59
|
const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
|
|
60
60
|
const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];
|
|
61
61
|
|
|
62
|
+
// Obvious documentation-placeholder imports the model writes in illustrative
|
|
63
|
+
// snippets — not real dependency claims. e.g. @scope/utils, some-module, ./local-file.
|
|
64
|
+
const PLACEHOLDER_IMPORT_RE = new RegExp([
|
|
65
|
+
'^@(?:scope|org|your-org|my-org|company|example)(?:/|$)', // @scope/utils
|
|
66
|
+
'(?:^|/)(?:some|your|my)-(?:module|package|lib|component|file|dep)(?:$|/)', // some-module
|
|
67
|
+
'(?:^|/)(?:local-file|your-file|my-file|module-name|package-name|your-package|example-package)(?:$|/)',
|
|
68
|
+
'(?:^|/)path/to/', // ./path/to/x
|
|
69
|
+
].join('|'), 'i');
|
|
70
|
+
|
|
62
71
|
/**
|
|
63
72
|
* Build the set of known symbol identifiers from the SigMap signature index,
|
|
64
73
|
* plus `{ name, file, line }` candidates (for closest-match suggestions).
|
|
@@ -225,6 +234,7 @@ function verify(answerText, cwd, opts = {}) {
|
|
|
225
234
|
|
|
226
235
|
// 2. fake-import
|
|
227
236
|
for (const imp of parsers.extractImports(answerText)) {
|
|
237
|
+
if (PLACEHOLDER_IMPORT_RE.test(imp.module)) continue;
|
|
228
238
|
if (imp.relative) {
|
|
229
239
|
if (!relativeResolvable(imp.module)) {
|
|
230
240
|
add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Import does not resolve: ${imp.module}`, confidence: 'high' });
|
package/src/verify/parsers.js
CHANGED
|
@@ -30,7 +30,10 @@ const LIBRARY_TOKENS = new Set([
|
|
|
30
30
|
|
|
31
31
|
// Illustrative placeholder names the model writes in prose, not repo claims:
|
|
32
32
|
// e.g. example.js, minimal-example.js, sample.ts, demo.js, placeholder.js.
|
|
33
|
-
const PLACEHOLDER_RE = /(?:^|[-_.])(?:example|sample|demo|placeholder)(?:[-_.]
|
|
33
|
+
const PLACEHOLDER_RE = /(?:^|[-_.])(?:example|sample|demo|placeholder)(?:[-_.]|s?$)/i;
|
|
34
|
+
// camelCase / Pascal placeholders: myExample.js, exampleConfig.js, fooSample.ts.
|
|
35
|
+
// Requires a case boundary so ordinary words (resample.js) are NOT suppressed.
|
|
36
|
+
const PLACEHOLDER_CAMEL_RE = /(?:^|[a-z])(?:Example|Sample|Demo|Placeholder)|(?:^|[-_.])(?:example|sample|demo|placeholder)(?=[A-Z])/;
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* Extract fenced code blocks.
|
|
@@ -86,7 +89,7 @@ function extractFilePaths(text) {
|
|
|
86
89
|
if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
|
|
87
90
|
if (LIBRARY_TOKENS.has(p.toLowerCase())) continue;
|
|
88
91
|
const base = p.split('/').pop();
|
|
89
|
-
if (PLACEHOLDER_RE.test(base)) continue;
|
|
92
|
+
if (PLACEHOLDER_RE.test(base) || PLACEHOLDER_CAMEL_RE.test(base)) continue;
|
|
90
93
|
if (!seen.has(p)) seen.set(p, i + 1);
|
|
91
94
|
}
|
|
92
95
|
}
|