sigmap 7.22.0 → 7.22.2
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 +485 -460
- 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/hallucination-guard.js +10 -0
- package/src/verify/parsers.js +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,24 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [7.22.2] — 2026-06-19
|
|
14
|
+
|
|
15
|
+
Patch release — clears the two remaining `verify-ai-output` false-positive classes surfaced by the §9 ablation.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **`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.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## [7.22.1] — 2026-06-18
|
|
23
|
+
|
|
24
|
+
Patch release — hardens the `verify-ai-output` file-path extractor against the dominant false-positive class.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **`verify-ai-output` no longer flags runtime/library names or placeholder filenames (#347):** `extractFilePaths` now skips well-known `X.js` product names (`node.js`, `next.js`, `vue.js`, `express.js`, `three.js`, `d3.js`, …) and illustrative placeholder basenames (`example`/`sample`/`demo`/`placeholder`, including `minimal-example.js`). Genuine repo-shaped paths (`src/foo/bar.js`, `main.js`, `index.ts`) are still extracted, so real hallucinations are unaffected. This removes the dominant Hallucination Guard false-positive class — in the §9 ablation, 22 of ~34 flags were literally "Node.js" — turning the directional grounding delta into a clean signal. The bundled `src/verify/parsers` factory was regenerated for standalone-binary parity.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
13
31
|
## [7.22.0] — 2026-06-18
|
|
14
32
|
|
|
15
33
|
Minor release — realistic §9 ablation (real-symbol corpus, exact-signature grounding, --verbose) + Gemini model fix.
|
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
|
@@ -7931,7 +7931,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
7931
7931
|
|
|
7932
7932
|
const SERVER_INFO = {
|
|
7933
7933
|
name: 'sigmap',
|
|
7934
|
-
version: '7.22.
|
|
7934
|
+
version: '7.22.2',
|
|
7935
7935
|
description: 'SigMap MCP server — code signatures on demand',
|
|
7936
7936
|
};
|
|
7937
7937
|
|
|
@@ -12756,207 +12756,223 @@ module.exports = { checkStarNudge, readUsage, usagePath, showStarNudge, RUN_THRE
|
|
|
12756
12756
|
|
|
12757
12757
|
// ── ./src/verify/parsers ──
|
|
12758
12758
|
__factories["./src/verify/parsers"] = function(module, exports) {
|
|
12759
|
-
|
|
12759
|
+
|
|
12760
|
+
/**
|
|
12761
|
+
* Parsers for the Hallucination Guard (verify-ai-output).
|
|
12762
|
+
*
|
|
12763
|
+
* Extract the verifiable claims an AI answer makes about a codebase:
|
|
12764
|
+
* - file paths it references
|
|
12765
|
+
* - import / require statements it shows
|
|
12766
|
+
* - function / class symbols it calls
|
|
12767
|
+
* - fenced code blocks (so callers can scope checks to code vs prose)
|
|
12768
|
+
*
|
|
12769
|
+
* Everything here is deterministic and offline — pure string analysis.
|
|
12770
|
+
*/
|
|
12760
12771
|
|
|
12761
|
-
|
|
12762
|
-
|
|
12763
|
-
|
|
12764
|
-
|
|
12765
|
-
|
|
12766
|
-
|
|
12767
|
-
|
|
12768
|
-
|
|
12769
|
-
*
|
|
12770
|
-
* Everything here is deterministic and offline — pure string analysis.
|
|
12771
|
-
*/
|
|
12772
|
+
// Extensions we are confident name a source/code/config file (no slash required).
|
|
12773
|
+
const KNOWN_CODE_EXT = new Set([
|
|
12774
|
+
'js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'py', 'pyw', 'rb', 'go', 'rs',
|
|
12775
|
+
'java', 'kt', 'swift', 'c', 'h', 'cpp', 'hpp', 'cs', 'php', 'r',
|
|
12776
|
+
'vue', 'svelte', 'css', 'scss', 'less', 'html', 'json', 'yml', 'yaml',
|
|
12777
|
+
'toml', 'xml', 'sql', 'graphql', 'gql', 'proto', 'tf', 'md', 'sh',
|
|
12778
|
+
'gd', 'gdscript',
|
|
12779
|
+
]);
|
|
12772
12780
|
|
|
12773
|
-
//
|
|
12774
|
-
const
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
'gd', 'gdscript',
|
|
12780
|
-
]);
|
|
12781
|
+
// Well-known "X.js" runtime/library product names — never repo files.
|
|
12782
|
+
const LIBRARY_TOKENS = new Set([
|
|
12783
|
+
'node.js', 'next.js', 'nuxt.js', 'vue.js', 'react.js', 'express.js', 'koa.js',
|
|
12784
|
+
'nest.js', 'three.js', 'd3.js', 'chart.js', 'ember.js', 'backbone.js',
|
|
12785
|
+
'angular.js', 'meteor.js', 'moment.js', 'anime.js', 'p5.js', 'next.config.js',
|
|
12786
|
+
]);
|
|
12781
12787
|
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
12787
|
-
|
|
12788
|
-
|
|
12789
|
-
|
|
12790
|
-
|
|
12791
|
-
|
|
12792
|
-
|
|
12793
|
-
|
|
12794
|
-
|
|
12795
|
-
const
|
|
12796
|
-
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
inBlock
|
|
12788
|
+
// Illustrative placeholder names the model writes in prose, not repo claims:
|
|
12789
|
+
// e.g. example.js, minimal-example.js, sample.ts, demo.js, placeholder.js.
|
|
12790
|
+
const PLACEHOLDER_RE = /(?:^|[-_.])(?:example|sample|demo|placeholder)(?:[-_.]|s?$)/i;
|
|
12791
|
+
// camelCase / Pascal placeholders: myExample.js, exampleConfig.js, fooSample.ts.
|
|
12792
|
+
// Requires a case boundary so ordinary words (resample.js) are NOT suppressed.
|
|
12793
|
+
const PLACEHOLDER_CAMEL_RE = /(?:^|[a-z])(?:Example|Sample|Demo|Placeholder)|(?:^|[-_.])(?:example|sample|demo|placeholder)(?=[A-Z])/;
|
|
12794
|
+
|
|
12795
|
+
/**
|
|
12796
|
+
* Extract fenced code blocks.
|
|
12797
|
+
* @param {string} text
|
|
12798
|
+
* @returns {{ lang: string, content: string, line: number }[]}
|
|
12799
|
+
*/
|
|
12800
|
+
function extractCodeBlocks(text) {
|
|
12801
|
+
const blocks = [];
|
|
12802
|
+
const lines = text.split('\n');
|
|
12803
|
+
let inBlock = false;
|
|
12804
|
+
let lang = '';
|
|
12805
|
+
let buf = [];
|
|
12806
|
+
let startLine = 0;
|
|
12807
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12808
|
+
const m = lines[i].match(/^```(\w*)/);
|
|
12809
|
+
if (m) {
|
|
12810
|
+
if (!inBlock) {
|
|
12811
|
+
inBlock = true;
|
|
12812
|
+
lang = m[1] || '';
|
|
12813
|
+
buf = [];
|
|
12814
|
+
startLine = i + 2; // first content line (1-based)
|
|
12815
|
+
} else {
|
|
12816
|
+
blocks.push({ lang, content: buf.join('\n'), line: startLine });
|
|
12817
|
+
inBlock = false;
|
|
12818
|
+
}
|
|
12819
|
+
continue;
|
|
12805
12820
|
}
|
|
12806
|
-
|
|
12821
|
+
if (inBlock) buf.push(lines[i]);
|
|
12807
12822
|
}
|
|
12808
|
-
|
|
12823
|
+
return blocks;
|
|
12809
12824
|
}
|
|
12810
|
-
return blocks;
|
|
12811
|
-
}
|
|
12812
12825
|
|
|
12813
|
-
/**
|
|
12814
|
-
|
|
12815
|
-
|
|
12816
|
-
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
function extractFilePaths(text) {
|
|
12821
|
-
|
|
12822
|
-
|
|
12823
|
-
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
|
|
12828
|
-
|
|
12829
|
-
|
|
12830
|
-
|
|
12831
|
-
|
|
12832
|
-
|
|
12833
|
-
|
|
12834
|
-
|
|
12826
|
+
/**
|
|
12827
|
+
* Extract file-path references (deduped, first-seen line kept).
|
|
12828
|
+
* A token counts as a path when it has a `.<letter…>` extension AND
|
|
12829
|
+
* either contains a `/` or carries a known code/config extension.
|
|
12830
|
+
* @param {string} text
|
|
12831
|
+
* @returns {{ path: string, line: number }[]}
|
|
12832
|
+
*/
|
|
12833
|
+
function extractFilePaths(text) {
|
|
12834
|
+
const lines = text.split('\n');
|
|
12835
|
+
const seen = new Map();
|
|
12836
|
+
const re = /(?:^|[\s`"'(\[<])([A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*)/g;
|
|
12837
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12838
|
+
const line = lines[i];
|
|
12839
|
+
let m;
|
|
12840
|
+
re.lastIndex = 0;
|
|
12841
|
+
while ((m = re.exec(line)) !== null) {
|
|
12842
|
+
const p = m[1];
|
|
12843
|
+
if (/^https?:/i.test(p)) continue;
|
|
12844
|
+
const ext = (p.split('.').pop() || '').toLowerCase();
|
|
12845
|
+
const hasSlash = p.includes('/');
|
|
12846
|
+
if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
|
|
12847
|
+
if (LIBRARY_TOKENS.has(p.toLowerCase())) continue;
|
|
12848
|
+
const base = p.split('/').pop();
|
|
12849
|
+
if (PLACEHOLDER_RE.test(base) || PLACEHOLDER_CAMEL_RE.test(base)) continue;
|
|
12850
|
+
if (!seen.has(p)) seen.set(p, i + 1);
|
|
12851
|
+
}
|
|
12835
12852
|
}
|
|
12853
|
+
return [...seen.entries()].map(([p, line]) => ({ path: p, line }));
|
|
12836
12854
|
}
|
|
12837
|
-
return [...seen.entries()].map(([p, line]) => ({ path: p, line }));
|
|
12838
|
-
}
|
|
12839
12855
|
|
|
12840
|
-
/**
|
|
12841
|
-
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
function extractImports(text) {
|
|
12846
|
-
|
|
12847
|
-
|
|
12848
|
-
|
|
12849
|
-
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12857
|
-
|
|
12858
|
-
|
|
12859
|
-
|
|
12860
|
-
|
|
12861
|
-
|
|
12862
|
-
|
|
12863
|
-
|
|
12864
|
-
|
|
12865
|
-
|
|
12866
|
-
|
|
12867
|
-
|
|
12868
|
-
|
|
12869
|
-
|
|
12870
|
-
|
|
12871
|
-
|
|
12872
|
-
|
|
12873
|
-
|
|
12874
|
-
|
|
12875
|
-
|
|
12876
|
-
|
|
12877
|
-
|
|
12878
|
-
|
|
12879
|
-
|
|
12880
|
-
|
|
12881
|
-
|
|
12882
|
-
|
|
12883
|
-
|
|
12884
|
-
|
|
12885
|
-
|
|
12886
|
-
|
|
12887
|
-
|
|
12888
|
-
|
|
12889
|
-
|
|
12890
|
-
|
|
12891
|
-
|
|
12892
|
-
|
|
12893
|
-
|
|
12894
|
-
|
|
12895
|
-
|
|
12896
|
-
|
|
12856
|
+
/**
|
|
12857
|
+
* Extract import / require statements.
|
|
12858
|
+
* @param {string} text
|
|
12859
|
+
* @returns {{ module: string, kind: 'js'|'py', relative: boolean, line: number, raw: string }[]}
|
|
12860
|
+
*/
|
|
12861
|
+
function extractImports(text) {
|
|
12862
|
+
const lines = text.split('\n');
|
|
12863
|
+
const out = [];
|
|
12864
|
+
const push = (module, kind, line, raw) => {
|
|
12865
|
+
if (!module) return;
|
|
12866
|
+
out.push({ module, kind, relative: /^[./]/.test(module), line, raw: raw.trim() });
|
|
12867
|
+
};
|
|
12868
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12869
|
+
const line = lines[i];
|
|
12870
|
+
let m;
|
|
12871
|
+
// JS/TS: import ... from 'x' | export ... from 'x'
|
|
12872
|
+
if ((m = line.match(/\b(?:import|export)\b[^'"]*\bfrom\s*['"]([^'"]+)['"]/))) {
|
|
12873
|
+
push(m[1], 'js', i + 1, line);
|
|
12874
|
+
} else if ((m = line.match(/\bimport\s*['"]([^'"]+)['"]/))) {
|
|
12875
|
+
// side-effect import 'x'
|
|
12876
|
+
push(m[1], 'js', i + 1, line);
|
|
12877
|
+
}
|
|
12878
|
+
// require('x') / dynamic import('x') — may co-occur, scan separately
|
|
12879
|
+
const reqRe = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
12880
|
+
let r;
|
|
12881
|
+
while ((r = reqRe.exec(line)) !== null) push(r[1], 'js', i + 1, line);
|
|
12882
|
+
|
|
12883
|
+
// TS: import X = require('mod')
|
|
12884
|
+
if ((m = line.match(/\bimport\s+[A-Za-z_$][\w$]*\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/))) {
|
|
12885
|
+
push(m[1], 'js', i + 1, line);
|
|
12886
|
+
}
|
|
12887
|
+
|
|
12888
|
+
// Python: from x import y | import x
|
|
12889
|
+
if ((m = line.match(/^\s*from\s+([.\w]+)\s+import\b/))) {
|
|
12890
|
+
push(m[1], 'py', i + 1, line);
|
|
12891
|
+
} else if ((m = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/))) {
|
|
12892
|
+
push(m[1], 'py', i + 1, line);
|
|
12893
|
+
}
|
|
12894
|
+
}
|
|
12895
|
+
|
|
12896
|
+
// Multi-line JS/TS imports, e.g.
|
|
12897
|
+
// import {
|
|
12898
|
+
// A as B,
|
|
12899
|
+
// } from './mod';
|
|
12900
|
+
// The per-line pass above misses these because `from '…'` sits on a later
|
|
12901
|
+
// line. Trigger only when the opening line has no quote and no `from` yet,
|
|
12902
|
+
// then gather forward until the source string appears.
|
|
12903
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12904
|
+
const start = lines[i];
|
|
12905
|
+
if (!/^\s*(?:import|export)\b/.test(start)) continue;
|
|
12906
|
+
if (/['"]/.test(start) || /\bfrom\b/.test(start)) continue; // single-line, already handled
|
|
12907
|
+
let joined = start;
|
|
12908
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 12); j++) {
|
|
12909
|
+
joined += ' ' + lines[j];
|
|
12910
|
+
const fm = joined.match(/\bfrom\s*['"]([^'"]+)['"]/);
|
|
12911
|
+
if (fm) { push(fm[1], 'js', i + 1, start.trim()); break; }
|
|
12912
|
+
if (/['"]/.test(lines[j]) && !/\bfrom\b/.test(joined)) break; // a string that isn't a source — bail
|
|
12913
|
+
}
|
|
12897
12914
|
}
|
|
12915
|
+
return out;
|
|
12898
12916
|
}
|
|
12899
|
-
return out;
|
|
12900
|
-
}
|
|
12901
12917
|
|
|
12902
|
-
/**
|
|
12903
|
-
|
|
12904
|
-
|
|
12905
|
-
|
|
12906
|
-
|
|
12907
|
-
|
|
12908
|
-
|
|
12909
|
-
function extractNpmScripts(text) {
|
|
12910
|
-
|
|
12911
|
-
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
|
|
12918
|
-
|
|
12919
|
-
|
|
12920
|
-
|
|
12921
|
-
|
|
12918
|
+
/**
|
|
12919
|
+
* Extract npm/pnpm/yarn script invocations (`npm run <name>`).
|
|
12920
|
+
* Only the explicit `run` form is matched, to avoid confusing package-manager
|
|
12921
|
+
* subcommands (`yarn add`, `pnpm install`) with script names.
|
|
12922
|
+
* @param {string} text
|
|
12923
|
+
* @returns {{ name: string, line: number }[]}
|
|
12924
|
+
*/
|
|
12925
|
+
function extractNpmScripts(text) {
|
|
12926
|
+
const lines = text.split('\n');
|
|
12927
|
+
const out = [];
|
|
12928
|
+
const seen = new Set();
|
|
12929
|
+
const re = /\b(?:npm|pnpm|yarn)\s+run(?:-script)?\s+([A-Za-z0-9:_-]+)/g;
|
|
12930
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12931
|
+
let m;
|
|
12932
|
+
re.lastIndex = 0;
|
|
12933
|
+
while ((m = re.exec(lines[i])) !== null) {
|
|
12934
|
+
const name = m[1];
|
|
12935
|
+
if (seen.has(name)) continue;
|
|
12936
|
+
seen.add(name);
|
|
12937
|
+
out.push({ name, line: i + 1 });
|
|
12938
|
+
}
|
|
12922
12939
|
}
|
|
12940
|
+
return out;
|
|
12923
12941
|
}
|
|
12924
|
-
return out;
|
|
12925
|
-
}
|
|
12926
12942
|
|
|
12927
|
-
/**
|
|
12928
|
-
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
function extractSymbols(text) {
|
|
12934
|
-
|
|
12935
|
-
|
|
12936
|
-
|
|
12937
|
-
|
|
12938
|
-
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
12942
|
-
|
|
12943
|
-
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
|
|
12943
|
+
/**
|
|
12944
|
+
* Extract function/class symbol references that look like calls.
|
|
12945
|
+
* Restricted to backtick-wrapped calls (`foo(...)`) for high precision.
|
|
12946
|
+
* @param {string} text
|
|
12947
|
+
* @returns {{ name: string, line: number }[]}
|
|
12948
|
+
*/
|
|
12949
|
+
function extractSymbols(text) {
|
|
12950
|
+
const lines = text.split('\n');
|
|
12951
|
+
const out = [];
|
|
12952
|
+
const seen = new Set();
|
|
12953
|
+
const re = /`([A-Za-z_$][\w$]*)\s*\([^`]*\)`/g;
|
|
12954
|
+
for (let i = 0; i < lines.length; i++) {
|
|
12955
|
+
let m;
|
|
12956
|
+
re.lastIndex = 0;
|
|
12957
|
+
while ((m = re.exec(lines[i])) !== null) {
|
|
12958
|
+
const name = m[1];
|
|
12959
|
+
const key = name + '@' + (i + 1);
|
|
12960
|
+
if (seen.has(key)) continue;
|
|
12961
|
+
seen.add(key);
|
|
12962
|
+
out.push({ name, line: i + 1 });
|
|
12963
|
+
}
|
|
12947
12964
|
}
|
|
12965
|
+
return out;
|
|
12948
12966
|
}
|
|
12949
|
-
return out;
|
|
12950
|
-
}
|
|
12951
|
-
|
|
12952
|
-
module.exports = {
|
|
12953
|
-
extractCodeBlocks,
|
|
12954
|
-
extractFilePaths,
|
|
12955
|
-
extractImports,
|
|
12956
|
-
extractSymbols,
|
|
12957
|
-
extractNpmScripts,
|
|
12958
|
-
};
|
|
12959
12967
|
|
|
12968
|
+
module.exports = {
|
|
12969
|
+
extractCodeBlocks,
|
|
12970
|
+
extractFilePaths,
|
|
12971
|
+
extractImports,
|
|
12972
|
+
extractSymbols,
|
|
12973
|
+
extractNpmScripts,
|
|
12974
|
+
};
|
|
12975
|
+
|
|
12960
12976
|
};
|
|
12961
12977
|
|
|
12962
12978
|
// ── ./src/verify/closest-match ──
|
|
@@ -13280,317 +13296,326 @@ module.exports = { renderReportHtml, renderReportMarkdown, escapeHtml };
|
|
|
13280
13296
|
|
|
13281
13297
|
// ── ./src/verify/hallucination-guard ──
|
|
13282
13298
|
__factories["./src/verify/hallucination-guard"] = function(module, exports) {
|
|
13283
|
-
|
|
13299
|
+
|
|
13300
|
+
/**
|
|
13301
|
+
* Hallucination Guard — deterministic core (Reliable MVP, v6.15.0).
|
|
13302
|
+
*
|
|
13303
|
+
* Given the text of an AI answer, flag claims that do not match the repo:
|
|
13304
|
+
* - fake-file : a referenced path is not on disk
|
|
13305
|
+
* - fake-test-file : a referenced *test* path is not on disk (sub-type)
|
|
13306
|
+
* - fake-import : a relative import does not resolve; a bare import is
|
|
13307
|
+
* absent from package.json deps (builtins allow-listed)
|
|
13308
|
+
* - fake-symbol : a called function/class is absent from the symbol index
|
|
13309
|
+
* - fake-npm-script: `npm run X` where X is not a package.json script
|
|
13310
|
+
*
|
|
13311
|
+
* Each issue carries a `confidence` (detection certainty) and, where a near
|
|
13312
|
+
* match exists, a heuristic `suggestion` ("Did you mean …?"). No network, no
|
|
13313
|
+
* LLM. Reuses SigMap primitives (buildSigIndex) but every external dependency
|
|
13314
|
+
* is injectable via `opts` so the core stays unit-testable.
|
|
13315
|
+
*/
|
|
13284
13316
|
|
|
13285
|
-
|
|
13286
|
-
|
|
13287
|
-
|
|
13288
|
-
|
|
13289
|
-
|
|
13290
|
-
|
|
13291
|
-
|
|
13292
|
-
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13300
|
-
|
|
13317
|
+
const fs = require('fs');
|
|
13318
|
+
const path = require('path');
|
|
13319
|
+
const parsers = __require('./src/verify/parsers');
|
|
13320
|
+
const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
|
|
13321
|
+
|
|
13322
|
+
// A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
|
|
13323
|
+
// a tests/__tests__ directory). Used to flag fake-test-file separately.
|
|
13324
|
+
const TEST_PATH_RE = /(?:\.(?:test|spec)\.[mc]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)|(?:(?:^|\/)test_[^/]+\.py$)|(?:_test\.py$)|(?:(?:^|\/)tests?\/)/i;
|
|
13325
|
+
function isTestPath(p) { return TEST_PATH_RE.test(p); }
|
|
13326
|
+
|
|
13327
|
+
const NODE_BUILTINS = new Set([
|
|
13328
|
+
'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https', 'crypto',
|
|
13329
|
+
'child_process', 'url', 'querystring', 'assert', 'zlib', 'readline', 'net',
|
|
13330
|
+
'tls', 'dns', 'buffer', 'process', 'vm', 'module', 'console', 'timers',
|
|
13331
|
+
'string_decoder', 'perf_hooks', 'worker_threads', 'cluster', 'dgram', 'v8',
|
|
13332
|
+
'tty', 'repl', 'async_hooks', 'inspector', 'fs/promises', 'path/posix',
|
|
13333
|
+
]);
|
|
13301
13334
|
|
|
13302
|
-
const
|
|
13303
|
-
|
|
13304
|
-
|
|
13305
|
-
|
|
13306
|
-
|
|
13307
|
-
|
|
13308
|
-
// a tests/__tests__ directory). Used to flag fake-test-file separately.
|
|
13309
|
-
const TEST_PATH_RE = /(?:\.(?:test|spec)\.[mc]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)|(?:(?:^|\/)test_[^/]+\.py$)|(?:_test\.py$)|(?:(?:^|\/)tests?\/)/i;
|
|
13310
|
-
function isTestPath(p) { return TEST_PATH_RE.test(p); }
|
|
13311
|
-
|
|
13312
|
-
const NODE_BUILTINS = new Set([
|
|
13313
|
-
'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https', 'crypto',
|
|
13314
|
-
'child_process', 'url', 'querystring', 'assert', 'zlib', 'readline', 'net',
|
|
13315
|
-
'tls', 'dns', 'buffer', 'process', 'vm', 'module', 'console', 'timers',
|
|
13316
|
-
'string_decoder', 'perf_hooks', 'worker_threads', 'cluster', 'dgram', 'v8',
|
|
13317
|
-
'tty', 'repl', 'async_hooks', 'inspector', 'fs/promises', 'path/posix',
|
|
13318
|
-
]);
|
|
13319
|
-
|
|
13320
|
-
const PY_BUILTINS = new Set([
|
|
13321
|
-
'os', 'sys', 're', 'json', 'math', 'typing', 'collections', 'itertools',
|
|
13322
|
-
'functools', 'datetime', 'pathlib', 'subprocess', 'abc', 'dataclasses',
|
|
13323
|
-
'enum', 'io', 'time', 'random', 'logging', 'argparse', 'unittest', 'asyncio',
|
|
13324
|
-
'copy', 'hashlib', 'threading', 'string', 'csv', 'glob', 'shutil', 'tempfile',
|
|
13325
|
-
]);
|
|
13326
|
-
|
|
13327
|
-
const LANG_GLOBALS = new Set([
|
|
13328
|
-
// JS
|
|
13329
|
-
'console', 'require', 'module', 'exports', 'process', 'Object', 'Array',
|
|
13330
|
-
'String', 'Number', 'Boolean', 'Math', 'JSON', 'Date', 'Promise', 'Map',
|
|
13331
|
-
'Set', 'WeakMap', 'WeakSet', 'RegExp', 'Error', 'Symbol', 'parseInt',
|
|
13332
|
-
'parseFloat', 'isNaN', 'setTimeout', 'setInterval', 'clearTimeout', 'fetch',
|
|
13333
|
-
'Buffer', 'Function', 'eval', 'encodeURIComponent', 'decodeURIComponent',
|
|
13334
|
-
// Python
|
|
13335
|
-
'print', 'len', 'range', 'str', 'int', 'float', 'dict', 'list', 'tuple',
|
|
13336
|
-
'set', 'bool', 'open', 'enumerate', 'zip', 'map', 'filter', 'sorted',
|
|
13337
|
-
'sum', 'min', 'max', 'abs', 'isinstance', 'super', 'type', 'getattr',
|
|
13338
|
-
'setattr', 'hasattr',
|
|
13339
|
-
]);
|
|
13340
|
-
|
|
13341
|
-
const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
|
|
13342
|
-
const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];
|
|
13335
|
+
const PY_BUILTINS = new Set([
|
|
13336
|
+
'os', 'sys', 're', 'json', 'math', 'typing', 'collections', 'itertools',
|
|
13337
|
+
'functools', 'datetime', 'pathlib', 'subprocess', 'abc', 'dataclasses',
|
|
13338
|
+
'enum', 'io', 'time', 'random', 'logging', 'argparse', 'unittest', 'asyncio',
|
|
13339
|
+
'copy', 'hashlib', 'threading', 'string', 'csv', 'glob', 'shutil', 'tempfile',
|
|
13340
|
+
]);
|
|
13343
13341
|
|
|
13344
|
-
|
|
13345
|
-
|
|
13346
|
-
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
13354
|
-
|
|
13355
|
-
|
|
13356
|
-
|
|
13357
|
-
for (const sig of sigs) {
|
|
13358
|
-
const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
13359
|
-
const ids = cleaned.match(/[A-Za-z_$][\w$]*/g) || [];
|
|
13360
|
-
for (const id of ids) set.add(id);
|
|
13361
|
-
}
|
|
13362
|
-
}
|
|
13363
|
-
symbolCandidates = buildSymbolCandidates(idx);
|
|
13364
|
-
} catch (_) {}
|
|
13365
|
-
return { set, fileKeys, symbolCandidates };
|
|
13366
|
-
}
|
|
13342
|
+
const LANG_GLOBALS = new Set([
|
|
13343
|
+
// JS
|
|
13344
|
+
'console', 'require', 'module', 'exports', 'process', 'Object', 'Array',
|
|
13345
|
+
'String', 'Number', 'Boolean', 'Math', 'JSON', 'Date', 'Promise', 'Map',
|
|
13346
|
+
'Set', 'WeakMap', 'WeakSet', 'RegExp', 'Error', 'Symbol', 'parseInt',
|
|
13347
|
+
'parseFloat', 'isNaN', 'setTimeout', 'setInterval', 'clearTimeout', 'fetch',
|
|
13348
|
+
'Buffer', 'Function', 'eval', 'encodeURIComponent', 'decodeURIComponent',
|
|
13349
|
+
// Python
|
|
13350
|
+
'print', 'len', 'range', 'str', 'int', 'float', 'dict', 'list', 'tuple',
|
|
13351
|
+
'set', 'bool', 'open', 'enumerate', 'zip', 'map', 'filter', 'sorted',
|
|
13352
|
+
'sum', 'min', 'max', 'abs', 'isinstance', 'super', 'type', 'getattr',
|
|
13353
|
+
'setattr', 'hasattr',
|
|
13354
|
+
]);
|
|
13367
13355
|
|
|
13368
|
-
|
|
13369
|
-
|
|
13370
|
-
const deps = new Set();
|
|
13371
|
-
let hasPkg = false;
|
|
13372
|
-
try {
|
|
13373
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
13374
|
-
hasPkg = true;
|
|
13375
|
-
for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
13376
|
-
if (pkg[k] && typeof pkg[k] === 'object') {
|
|
13377
|
-
for (const name of Object.keys(pkg[k])) deps.add(name);
|
|
13378
|
-
}
|
|
13379
|
-
}
|
|
13380
|
-
} catch (_) {}
|
|
13381
|
-
return { deps, hasPkg };
|
|
13382
|
-
}
|
|
13356
|
+
const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
|
|
13357
|
+
const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];
|
|
13383
13358
|
|
|
13384
|
-
|
|
13385
|
-
|
|
13386
|
-
const
|
|
13387
|
-
|
|
13388
|
-
|
|
13389
|
-
|
|
13390
|
-
|
|
13391
|
-
|
|
13392
|
-
} catch (_) {}
|
|
13393
|
-
return scripts;
|
|
13394
|
-
}
|
|
13359
|
+
// Obvious documentation-placeholder imports the model writes in illustrative
|
|
13360
|
+
// snippets — not real dependency claims. e.g. @scope/utils, some-module, ./local-file.
|
|
13361
|
+
const PLACEHOLDER_IMPORT_RE = new RegExp([
|
|
13362
|
+
'^@(?:scope|org|your-org|my-org|company|example)(?:/|$)', // @scope/utils
|
|
13363
|
+
'(?:^|/)(?:some|your|my)-(?:module|package|lib|component|file|dep)(?:$|/)', // some-module
|
|
13364
|
+
'(?:^|/)(?:local-file|your-file|my-file|module-name|package-name|your-package|example-package)(?:$|/)',
|
|
13365
|
+
'(?:^|/)path/to/', // ./path/to/x
|
|
13366
|
+
].join('|'), 'i');
|
|
13395
13367
|
|
|
13396
|
-
/**
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
|
|
13368
|
+
/**
|
|
13369
|
+
* Build the set of known symbol identifiers from the SigMap signature index,
|
|
13370
|
+
* plus `{ name, file, line }` candidates (for closest-match suggestions).
|
|
13371
|
+
*/
|
|
13372
|
+
function buildSymbolSet(cwd) {
|
|
13373
|
+
const set = new Set();
|
|
13374
|
+
let fileKeys = [];
|
|
13375
|
+
let symbolCandidates = [];
|
|
13400
13376
|
try {
|
|
13401
|
-
|
|
13377
|
+
const { buildSigIndex } = __require('./src/retrieval/ranker');
|
|
13378
|
+
const idx = buildSigIndex(cwd);
|
|
13379
|
+
fileKeys = [...idx.keys()];
|
|
13380
|
+
for (const sigs of idx.values()) {
|
|
13381
|
+
for (const sig of sigs) {
|
|
13382
|
+
const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
|
|
13383
|
+
const ids = cleaned.match(/[A-Za-z_$][\w$]*/g) || [];
|
|
13384
|
+
for (const id of ids) set.add(id);
|
|
13385
|
+
}
|
|
13386
|
+
}
|
|
13387
|
+
symbolCandidates = buildSymbolCandidates(idx);
|
|
13402
13388
|
} catch (_) {}
|
|
13389
|
+
return { set, fileKeys, symbolCandidates };
|
|
13403
13390
|
}
|
|
13404
|
-
return false;
|
|
13405
|
-
}
|
|
13406
13391
|
|
|
13407
|
-
/**
|
|
13408
|
-
function
|
|
13409
|
-
|
|
13410
|
-
|
|
13392
|
+
/** Load declared dependency names from package.json. */
|
|
13393
|
+
function loadDeps(cwd) {
|
|
13394
|
+
const deps = new Set();
|
|
13395
|
+
let hasPkg = false;
|
|
13411
13396
|
try {
|
|
13412
|
-
|
|
13397
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
13398
|
+
hasPkg = true;
|
|
13399
|
+
for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
13400
|
+
if (pkg[k] && typeof pkg[k] === 'object') {
|
|
13401
|
+
for (const name of Object.keys(pkg[k])) deps.add(name);
|
|
13402
|
+
}
|
|
13403
|
+
}
|
|
13413
13404
|
} catch (_) {}
|
|
13405
|
+
return { deps, hasPkg };
|
|
13414
13406
|
}
|
|
13415
|
-
|
|
13407
|
+
|
|
13408
|
+
/** Load the set of npm script names declared in package.json. */
|
|
13409
|
+
function loadScripts(cwd) {
|
|
13410
|
+
const scripts = new Set();
|
|
13416
13411
|
try {
|
|
13417
|
-
|
|
13412
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
13413
|
+
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
13414
|
+
for (const name of Object.keys(pkg.scripts)) scripts.add(name);
|
|
13415
|
+
}
|
|
13418
13416
|
} catch (_) {}
|
|
13417
|
+
return scripts;
|
|
13419
13418
|
}
|
|
13420
|
-
// Fall back to basename match against the indexed file set (the answer's
|
|
13421
|
-
// import is relative to a file we cannot know, so a name match is enough
|
|
13422
|
-
// to avoid false positives).
|
|
13423
|
-
const wantBase = path.basename(mod).replace(/\.[^.]+$/, '').toLowerCase();
|
|
13424
|
-
return fileBasenames.has(wantBase);
|
|
13425
|
-
}
|
|
13426
|
-
|
|
13427
|
-
/**
|
|
13428
|
-
* Verify an AI answer against the repository.
|
|
13429
|
-
*
|
|
13430
|
-
* Each issue has the shape:
|
|
13431
|
-
* { type, value, line, location, message, confidence, suggestion }
|
|
13432
|
-
* where `confidence` is the *detection* certainty ('high' for path/dep/script
|
|
13433
|
-
* checks, 'medium' for symbol checks) and `suggestion` is a heuristic
|
|
13434
|
-
* closest-match hint (or null).
|
|
13435
|
-
*
|
|
13436
|
-
* @param {string} answerText
|
|
13437
|
-
* @param {string} cwd
|
|
13438
|
-
* @param {object} [opts]
|
|
13439
|
-
* @param {Set<string>} [opts.symbolSet] override known symbols
|
|
13440
|
-
* @param {Array} [opts.symbolCandidates] override { name, file, line } list
|
|
13441
|
-
* @param {Array<string>} [opts.fileCandidates] override repo file paths (suggestions)
|
|
13442
|
-
* @param {Set<string>} [opts.deps] override package deps
|
|
13443
|
-
* @param {Set<string>} [opts.scripts] override package.json script names
|
|
13444
|
-
* @param {boolean} [opts.hasPkg] whether a package.json exists
|
|
13445
|
-
* @param {(ref: string) => boolean} [opts.fileExists] override file check
|
|
13446
|
-
* @param {(mod: string) => boolean} [opts.relativeResolvable] override rel-import check
|
|
13447
|
-
* @returns {{ issues: object[], summary: object }}
|
|
13448
|
-
*/
|
|
13449
|
-
function verify(answerText, cwd, opts = {}) {
|
|
13450
|
-
let symbolSet = opts.symbolSet;
|
|
13451
|
-
let fileBasenames = opts.fileBasenames;
|
|
13452
|
-
let symbolCandidates = opts.symbolCandidates || [];
|
|
13453
|
-
let fileCandidates = opts.fileCandidates || [];
|
|
13454
|
-
if (!symbolSet) {
|
|
13455
|
-
const built = buildSymbolSet(cwd);
|
|
13456
|
-
symbolSet = built.set;
|
|
13457
|
-
fileBasenames = new Set(built.fileKeys.map(
|
|
13458
|
-
(k) => path.basename(k).replace(/\.[^.]+$/, '').toLowerCase()
|
|
13459
|
-
));
|
|
13460
|
-
symbolCandidates = built.symbolCandidates;
|
|
13461
|
-
fileCandidates = built.fileKeys;
|
|
13462
|
-
}
|
|
13463
|
-
if (!fileBasenames) fileBasenames = new Set();
|
|
13464
|
-
|
|
13465
|
-
let deps = opts.deps;
|
|
13466
|
-
let hasPkg = opts.hasPkg;
|
|
13467
|
-
if (!deps) {
|
|
13468
|
-
const loaded = loadDeps(cwd);
|
|
13469
|
-
deps = loaded.deps;
|
|
13470
|
-
if (hasPkg === undefined) hasPkg = loaded.hasPkg;
|
|
13471
|
-
}
|
|
13472
|
-
const scripts = opts.scripts || (hasPkg ? loadScripts(cwd) : new Set());
|
|
13473
|
-
|
|
13474
|
-
const fileExists = opts.fileExists || ((ref) => defaultFileExists(cwd, ref));
|
|
13475
|
-
const relativeResolvable = opts.relativeResolvable
|
|
13476
|
-
|| ((mod) => defaultRelativeResolvable(cwd, mod, fileBasenames));
|
|
13477
|
-
|
|
13478
|
-
// Pre-derive basename candidates for file suggestions (compare on basename so
|
|
13479
|
-
// a wrong directory still surfaces the right file).
|
|
13480
|
-
const fileBasenameCandidates = fileCandidates.map((f) => ({ name: path.basename(f), file: f }));
|
|
13481
|
-
|
|
13482
|
-
const issues = [];
|
|
13483
|
-
const dedupe = new Set();
|
|
13484
|
-
const add = (issue) => {
|
|
13485
|
-
const key = `${issue.type}::${issue.value}`;
|
|
13486
|
-
if (dedupe.has(key)) return;
|
|
13487
|
-
dedupe.add(key);
|
|
13488
|
-
if (!('suggestion' in issue)) issue.suggestion = null;
|
|
13489
|
-
issue.location = `L${issue.line}`;
|
|
13490
|
-
issues.push(issue);
|
|
13491
|
-
};
|
|
13492
13419
|
|
|
13493
|
-
|
|
13494
|
-
|
|
13495
|
-
|
|
13496
|
-
const
|
|
13497
|
-
|
|
13498
|
-
|
|
13499
|
-
|
|
13500
|
-
|
|
13501
|
-
|
|
13502
|
-
message: `${isTest ? 'Test file' : 'File'} not found on disk: ${p}`,
|
|
13503
|
-
confidence: 'high',
|
|
13504
|
-
suggestion: match ? formatSuggestion(match, false) : null,
|
|
13505
|
-
});
|
|
13420
|
+
/** Default file-existence check: resolve a referenced path against cwd. */
|
|
13421
|
+
function defaultFileExists(cwd, ref) {
|
|
13422
|
+
const clean = ref.replace(/^\.\//, '');
|
|
13423
|
+
for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
|
|
13424
|
+
try {
|
|
13425
|
+
if (fs.existsSync(c)) return true;
|
|
13426
|
+
} catch (_) {}
|
|
13427
|
+
}
|
|
13428
|
+
return false;
|
|
13506
13429
|
}
|
|
13507
13430
|
|
|
13508
|
-
|
|
13509
|
-
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13431
|
+
/** Default relative-import resolver: fs candidates + basename match in index. */
|
|
13432
|
+
function defaultRelativeResolvable(cwd, mod, fileBasenames) {
|
|
13433
|
+
const base = path.resolve(cwd, mod);
|
|
13434
|
+
for (const e of REL_EXTS) {
|
|
13435
|
+
try {
|
|
13436
|
+
if (fs.existsSync(base + e)) return true;
|
|
13437
|
+
} catch (_) {}
|
|
13515
13438
|
}
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13520
|
-
if (NODE_BUILTINS.has(imp.module) || NODE_BUILTINS.has(top)) continue;
|
|
13521
|
-
if (top.startsWith('@')) {
|
|
13522
|
-
const scoped = imp.module.split('/').slice(0, 2).join('/');
|
|
13523
|
-
if (deps.has(scoped) || deps.has(imp.module)) continue;
|
|
13524
|
-
} else if (deps.has(top) || deps.has(imp.module)) {
|
|
13525
|
-
continue;
|
|
13526
|
-
}
|
|
13527
|
-
const match = closestMatch(top, [...deps], { minLen: 3 });
|
|
13528
|
-
add({
|
|
13529
|
-
type: 'fake-import',
|
|
13530
|
-
value: imp.module,
|
|
13531
|
-
line: imp.line,
|
|
13532
|
-
message: `Package not in dependencies: ${imp.module}`,
|
|
13533
|
-
confidence: 'high',
|
|
13534
|
-
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
13535
|
-
});
|
|
13439
|
+
for (const idx of REL_INDEX) {
|
|
13440
|
+
try {
|
|
13441
|
+
if (fs.existsSync(path.join(base, idx))) return true;
|
|
13442
|
+
} catch (_) {}
|
|
13536
13443
|
}
|
|
13537
|
-
//
|
|
13444
|
+
// Fall back to basename match against the indexed file set (the answer's
|
|
13445
|
+
// import is relative to a file we cannot know, so a name match is enough
|
|
13446
|
+
// to avoid false positives).
|
|
13447
|
+
const wantBase = path.basename(mod).replace(/\.[^.]+$/, '').toLowerCase();
|
|
13448
|
+
return fileBasenames.has(wantBase);
|
|
13538
13449
|
}
|
|
13539
13450
|
|
|
13540
|
-
|
|
13541
|
-
|
|
13542
|
-
|
|
13543
|
-
|
|
13544
|
-
|
|
13545
|
-
|
|
13546
|
-
|
|
13547
|
-
|
|
13548
|
-
|
|
13549
|
-
|
|
13550
|
-
|
|
13551
|
-
|
|
13552
|
-
|
|
13553
|
-
|
|
13451
|
+
/**
|
|
13452
|
+
* Verify an AI answer against the repository.
|
|
13453
|
+
*
|
|
13454
|
+
* Each issue has the shape:
|
|
13455
|
+
* { type, value, line, location, message, confidence, suggestion }
|
|
13456
|
+
* where `confidence` is the *detection* certainty ('high' for path/dep/script
|
|
13457
|
+
* checks, 'medium' for symbol checks) and `suggestion` is a heuristic
|
|
13458
|
+
* closest-match hint (or null).
|
|
13459
|
+
*
|
|
13460
|
+
* @param {string} answerText
|
|
13461
|
+
* @param {string} cwd
|
|
13462
|
+
* @param {object} [opts]
|
|
13463
|
+
* @param {Set<string>} [opts.symbolSet] override known symbols
|
|
13464
|
+
* @param {Array} [opts.symbolCandidates] override { name, file, line } list
|
|
13465
|
+
* @param {Array<string>} [opts.fileCandidates] override repo file paths (suggestions)
|
|
13466
|
+
* @param {Set<string>} [opts.deps] override package deps
|
|
13467
|
+
* @param {Set<string>} [opts.scripts] override package.json script names
|
|
13468
|
+
* @param {boolean} [opts.hasPkg] whether a package.json exists
|
|
13469
|
+
* @param {(ref: string) => boolean} [opts.fileExists] override file check
|
|
13470
|
+
* @param {(mod: string) => boolean} [opts.relativeResolvable] override rel-import check
|
|
13471
|
+
* @returns {{ issues: object[], summary: object }}
|
|
13472
|
+
*/
|
|
13473
|
+
function verify(answerText, cwd, opts = {}) {
|
|
13474
|
+
let symbolSet = opts.symbolSet;
|
|
13475
|
+
let fileBasenames = opts.fileBasenames;
|
|
13476
|
+
let symbolCandidates = opts.symbolCandidates || [];
|
|
13477
|
+
let fileCandidates = opts.fileCandidates || [];
|
|
13478
|
+
if (!symbolSet) {
|
|
13479
|
+
const built = buildSymbolSet(cwd);
|
|
13480
|
+
symbolSet = built.set;
|
|
13481
|
+
fileBasenames = new Set(built.fileKeys.map(
|
|
13482
|
+
(k) => path.basename(k).replace(/\.[^.]+$/, '').toLowerCase()
|
|
13483
|
+
));
|
|
13484
|
+
symbolCandidates = built.symbolCandidates;
|
|
13485
|
+
fileCandidates = built.fileKeys;
|
|
13554
13486
|
}
|
|
13555
|
-
|
|
13487
|
+
if (!fileBasenames) fileBasenames = new Set();
|
|
13488
|
+
|
|
13489
|
+
let deps = opts.deps;
|
|
13490
|
+
let hasPkg = opts.hasPkg;
|
|
13491
|
+
if (!deps) {
|
|
13492
|
+
const loaded = loadDeps(cwd);
|
|
13493
|
+
deps = loaded.deps;
|
|
13494
|
+
if (hasPkg === undefined) hasPkg = loaded.hasPkg;
|
|
13495
|
+
}
|
|
13496
|
+
const scripts = opts.scripts || (hasPkg ? loadScripts(cwd) : new Set());
|
|
13556
13497
|
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13498
|
+
const fileExists = opts.fileExists || ((ref) => defaultFileExists(cwd, ref));
|
|
13499
|
+
const relativeResolvable = opts.relativeResolvable
|
|
13500
|
+
|| ((mod) => defaultRelativeResolvable(cwd, mod, fileBasenames));
|
|
13501
|
+
|
|
13502
|
+
// Pre-derive basename candidates for file suggestions (compare on basename so
|
|
13503
|
+
// a wrong directory still surfaces the right file).
|
|
13504
|
+
const fileBasenameCandidates = fileCandidates.map((f) => ({ name: path.basename(f), file: f }));
|
|
13505
|
+
|
|
13506
|
+
const issues = [];
|
|
13507
|
+
const dedupe = new Set();
|
|
13508
|
+
const add = (issue) => {
|
|
13509
|
+
const key = `${issue.type}::${issue.value}`;
|
|
13510
|
+
if (dedupe.has(key)) return;
|
|
13511
|
+
dedupe.add(key);
|
|
13512
|
+
if (!('suggestion' in issue)) issue.suggestion = null;
|
|
13513
|
+
issue.location = `L${issue.line}`;
|
|
13514
|
+
issues.push(issue);
|
|
13515
|
+
};
|
|
13516
|
+
|
|
13517
|
+
// 1. fake-file / fake-test-file
|
|
13518
|
+
for (const { path: p, line } of parsers.extractFilePaths(answerText)) {
|
|
13519
|
+
if (fileExists(p)) continue;
|
|
13520
|
+
const isTest = isTestPath(p);
|
|
13521
|
+
const match = closestMatch(path.basename(p), fileBasenameCandidates, { minLen: 4 });
|
|
13562
13522
|
add({
|
|
13563
|
-
type: 'fake-
|
|
13564
|
-
value:
|
|
13523
|
+
type: isTest ? 'fake-test-file' : 'fake-file',
|
|
13524
|
+
value: p,
|
|
13565
13525
|
line,
|
|
13566
|
-
message:
|
|
13526
|
+
message: `${isTest ? 'Test file' : 'File'} not found on disk: ${p}`,
|
|
13567
13527
|
confidence: 'high',
|
|
13568
|
-
suggestion: match ? formatSuggestion(
|
|
13528
|
+
suggestion: match ? formatSuggestion(match, false) : null,
|
|
13569
13529
|
});
|
|
13570
13530
|
}
|
|
13571
|
-
}
|
|
13572
13531
|
|
|
13573
|
-
|
|
13532
|
+
// 2. fake-import
|
|
13533
|
+
for (const imp of parsers.extractImports(answerText)) {
|
|
13534
|
+
if (PLACEHOLDER_IMPORT_RE.test(imp.module)) continue;
|
|
13535
|
+
if (imp.relative) {
|
|
13536
|
+
if (!relativeResolvable(imp.module)) {
|
|
13537
|
+
add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Import does not resolve: ${imp.module}`, confidence: 'high' });
|
|
13538
|
+
}
|
|
13539
|
+
continue;
|
|
13540
|
+
}
|
|
13541
|
+
// Bare module — only verifiable for JS when a package.json exists.
|
|
13542
|
+
const top = imp.module.split('/')[0];
|
|
13543
|
+
if (imp.kind === 'js') {
|
|
13544
|
+
if (!hasPkg) continue;
|
|
13545
|
+
if (NODE_BUILTINS.has(imp.module) || NODE_BUILTINS.has(top)) continue;
|
|
13546
|
+
if (top.startsWith('@')) {
|
|
13547
|
+
const scoped = imp.module.split('/').slice(0, 2).join('/');
|
|
13548
|
+
if (deps.has(scoped) || deps.has(imp.module)) continue;
|
|
13549
|
+
} else if (deps.has(top) || deps.has(imp.module)) {
|
|
13550
|
+
continue;
|
|
13551
|
+
}
|
|
13552
|
+
const match = closestMatch(top, [...deps], { minLen: 3 });
|
|
13553
|
+
add({
|
|
13554
|
+
type: 'fake-import',
|
|
13555
|
+
value: imp.module,
|
|
13556
|
+
line: imp.line,
|
|
13557
|
+
message: `Package not in dependencies: ${imp.module}`,
|
|
13558
|
+
confidence: 'high',
|
|
13559
|
+
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
13560
|
+
});
|
|
13561
|
+
}
|
|
13562
|
+
// Python bare imports: stdlib is unbounded offline — skip to keep precision.
|
|
13563
|
+
}
|
|
13564
|
+
|
|
13565
|
+
// 3. fake-symbol
|
|
13566
|
+
if (symbolSet.size > 0) {
|
|
13567
|
+
for (const { name, line } of parsers.extractSymbols(answerText)) {
|
|
13568
|
+
if (symbolSet.has(name)) continue;
|
|
13569
|
+
if (LANG_GLOBALS.has(name) || NODE_BUILTINS.has(name) || PY_BUILTINS.has(name)) continue;
|
|
13570
|
+
const match = closestMatch(name, symbolCandidates, { minLen: 4 });
|
|
13571
|
+
add({
|
|
13572
|
+
type: 'fake-symbol',
|
|
13573
|
+
value: name,
|
|
13574
|
+
line,
|
|
13575
|
+
message: `Symbol not found in repo index: ${name}()`,
|
|
13576
|
+
confidence: 'medium',
|
|
13577
|
+
suggestion: match ? formatSuggestion(match, true) : null,
|
|
13578
|
+
});
|
|
13579
|
+
}
|
|
13580
|
+
}
|
|
13574
13581
|
|
|
13575
|
-
|
|
13576
|
-
|
|
13577
|
-
|
|
13578
|
-
|
|
13579
|
-
|
|
13580
|
-
|
|
13581
|
-
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
13585
|
-
|
|
13586
|
-
|
|
13587
|
-
|
|
13582
|
+
// 4. fake-npm-script
|
|
13583
|
+
if (hasPkg && scripts.size > 0) {
|
|
13584
|
+
for (const { name, line } of parsers.extractNpmScripts(answerText)) {
|
|
13585
|
+
if (scripts.has(name)) continue;
|
|
13586
|
+
const match = closestMatch(name, [...scripts], { minLen: 2 });
|
|
13587
|
+
add({
|
|
13588
|
+
type: 'fake-npm-script',
|
|
13589
|
+
value: name,
|
|
13590
|
+
line,
|
|
13591
|
+
message: `npm script not in package.json: ${name}`,
|
|
13592
|
+
confidence: 'high',
|
|
13593
|
+
suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
|
|
13594
|
+
});
|
|
13595
|
+
}
|
|
13596
|
+
}
|
|
13588
13597
|
|
|
13589
|
-
|
|
13590
|
-
|
|
13598
|
+
issues.sort((a, b) => a.line - b.line);
|
|
13599
|
+
|
|
13600
|
+
const byType = {
|
|
13601
|
+
'fake-file': 0, 'fake-test-file': 0, 'fake-import': 0,
|
|
13602
|
+
'fake-symbol': 0, 'fake-npm-script': 0,
|
|
13603
|
+
};
|
|
13604
|
+
for (const i of issues) byType[i.type] = (byType[i.type] || 0) + 1;
|
|
13605
|
+
|
|
13606
|
+
const summary = {
|
|
13607
|
+
total: issues.length,
|
|
13608
|
+
byType,
|
|
13609
|
+
clean: issues.length === 0,
|
|
13610
|
+
symbolsIndexed: symbolSet.size,
|
|
13611
|
+
withSuggestion: issues.filter((i) => i.suggestion).length,
|
|
13612
|
+
};
|
|
13591
13613
|
|
|
13592
|
-
|
|
13614
|
+
return { issues, summary };
|
|
13615
|
+
}
|
|
13593
13616
|
|
|
13617
|
+
module.exports = { verify, buildSymbolSet, loadDeps, loadScripts, isTestPath };
|
|
13618
|
+
|
|
13594
13619
|
};
|
|
13595
13620
|
|
|
13596
13621
|
const fs = require('fs');
|
|
@@ -13609,7 +13634,7 @@ function __tryGit(args, opts = {}) {
|
|
|
13609
13634
|
catch (_) { return ''; }
|
|
13610
13635
|
}
|
|
13611
13636
|
|
|
13612
|
-
const VERSION = '7.22.
|
|
13637
|
+
const VERSION = '7.22.2';
|
|
13613
13638
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
13614
13639
|
|
|
13615
13640
|
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.22.
|
|
12
|
+
# Version: 7.22.2 | 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.22.
|
|
12
|
+
# Version: 7.22.2 | 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.22.
|
|
3
|
+
"version": "7.22.2",
|
|
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/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
|
@@ -21,6 +21,20 @@ const KNOWN_CODE_EXT = new Set([
|
|
|
21
21
|
'gd', 'gdscript',
|
|
22
22
|
]);
|
|
23
23
|
|
|
24
|
+
// Well-known "X.js" runtime/library product names — never repo files.
|
|
25
|
+
const LIBRARY_TOKENS = new Set([
|
|
26
|
+
'node.js', 'next.js', 'nuxt.js', 'vue.js', 'react.js', 'express.js', 'koa.js',
|
|
27
|
+
'nest.js', 'three.js', 'd3.js', 'chart.js', 'ember.js', 'backbone.js',
|
|
28
|
+
'angular.js', 'meteor.js', 'moment.js', 'anime.js', 'p5.js', 'next.config.js',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// Illustrative placeholder names the model writes in prose, not repo claims:
|
|
32
|
+
// e.g. example.js, minimal-example.js, sample.ts, demo.js, placeholder.js.
|
|
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])/;
|
|
37
|
+
|
|
24
38
|
/**
|
|
25
39
|
* Extract fenced code blocks.
|
|
26
40
|
* @param {string} text
|
|
@@ -73,6 +87,9 @@ function extractFilePaths(text) {
|
|
|
73
87
|
const ext = (p.split('.').pop() || '').toLowerCase();
|
|
74
88
|
const hasSlash = p.includes('/');
|
|
75
89
|
if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
|
|
90
|
+
if (LIBRARY_TOKENS.has(p.toLowerCase())) continue;
|
|
91
|
+
const base = p.split('/').pop();
|
|
92
|
+
if (PLACEHOLDER_RE.test(base) || PLACEHOLDER_CAMEL_RE.test(base)) continue;
|
|
76
93
|
if (!seen.has(p)) seen.set(p, i + 1);
|
|
77
94
|
}
|
|
78
95
|
}
|