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 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-14
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.0',
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
- 'use strict';
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
- * Parsers for the Hallucination Guard (verify-ai-output).
12763
- *
12764
- * Extract the verifiable claims an AI answer makes about a codebase:
12765
- * - file paths it references
12766
- * - import / require statements it shows
12767
- * - function / class symbols it calls
12768
- * - fenced code blocks (so callers can scope checks to code vs prose)
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
- // Extensions we are confident name a source/code/config file (no slash required).
12774
- const KNOWN_CODE_EXT = new Set([
12775
- 'js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'py', 'pyw', 'rb', 'go', 'rs',
12776
- 'java', 'kt', 'swift', 'c', 'h', 'cpp', 'hpp', 'cs', 'php', 'r',
12777
- 'vue', 'svelte', 'css', 'scss', 'less', 'html', 'json', 'yml', 'yaml',
12778
- 'toml', 'xml', 'sql', 'graphql', 'gql', 'proto', 'tf', 'md', 'sh',
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
- * Extract fenced code blocks.
12784
- * @param {string} text
12785
- * @returns {{ lang: string, content: string, line: number }[]}
12786
- */
12787
- function extractCodeBlocks(text) {
12788
- const blocks = [];
12789
- const lines = text.split('\n');
12790
- let inBlock = false;
12791
- let lang = '';
12792
- let buf = [];
12793
- let startLine = 0;
12794
- for (let i = 0; i < lines.length; i++) {
12795
- const m = lines[i].match(/^```(\w*)/);
12796
- if (m) {
12797
- if (!inBlock) {
12798
- inBlock = true;
12799
- lang = m[1] || '';
12800
- buf = [];
12801
- startLine = i + 2; // first content line (1-based)
12802
- } else {
12803
- blocks.push({ lang, content: buf.join('\n'), line: startLine });
12804
- inBlock = false;
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
- continue;
12821
+ if (inBlock) buf.push(lines[i]);
12807
12822
  }
12808
- if (inBlock) buf.push(lines[i]);
12823
+ return blocks;
12809
12824
  }
12810
- return blocks;
12811
- }
12812
12825
 
12813
- /**
12814
- * Extract file-path references (deduped, first-seen line kept).
12815
- * A token counts as a path when it has a `.<letter…>` extension AND
12816
- * either contains a `/` or carries a known code/config extension.
12817
- * @param {string} text
12818
- * @returns {{ path: string, line: number }[]}
12819
- */
12820
- function extractFilePaths(text) {
12821
- const lines = text.split('\n');
12822
- const seen = new Map();
12823
- const re = /(?:^|[\s`"'(\[<])([A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*)/g;
12824
- for (let i = 0; i < lines.length; i++) {
12825
- const line = lines[i];
12826
- let m;
12827
- re.lastIndex = 0;
12828
- while ((m = re.exec(line)) !== null) {
12829
- const p = m[1];
12830
- if (/^https?:/i.test(p)) continue;
12831
- const ext = (p.split('.').pop() || '').toLowerCase();
12832
- const hasSlash = p.includes('/');
12833
- if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
12834
- if (!seen.has(p)) seen.set(p, i + 1);
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
- * Extract import / require statements.
12842
- * @param {string} text
12843
- * @returns {{ module: string, kind: 'js'|'py', relative: boolean, line: number, raw: string }[]}
12844
- */
12845
- function extractImports(text) {
12846
- const lines = text.split('\n');
12847
- const out = [];
12848
- const push = (module, kind, line, raw) => {
12849
- if (!module) return;
12850
- out.push({ module, kind, relative: /^[./]/.test(module), line, raw: raw.trim() });
12851
- };
12852
- for (let i = 0; i < lines.length; i++) {
12853
- const line = lines[i];
12854
- let m;
12855
- // JS/TS: import ... from 'x' | export ... from 'x'
12856
- if ((m = line.match(/\b(?:import|export)\b[^'"]*\bfrom\s*['"]([^'"]+)['"]/))) {
12857
- push(m[1], 'js', i + 1, line);
12858
- } else if ((m = line.match(/\bimport\s*['"]([^'"]+)['"]/))) {
12859
- // side-effect import 'x'
12860
- push(m[1], 'js', i + 1, line);
12861
- }
12862
- // require('x') / dynamic import('x') — may co-occur, scan separately
12863
- const reqRe = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
12864
- let r;
12865
- while ((r = reqRe.exec(line)) !== null) push(r[1], 'js', i + 1, line);
12866
-
12867
- // TS: import X = require('mod')
12868
- if ((m = line.match(/\bimport\s+[A-Za-z_$][\w$]*\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/))) {
12869
- push(m[1], 'js', i + 1, line);
12870
- }
12871
-
12872
- // Python: from x import y | import x
12873
- if ((m = line.match(/^\s*from\s+([.\w]+)\s+import\b/))) {
12874
- push(m[1], 'py', i + 1, line);
12875
- } else if ((m = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/))) {
12876
- push(m[1], 'py', i + 1, line);
12877
- }
12878
- }
12879
-
12880
- // Multi-line JS/TS imports, e.g.
12881
- // import {
12882
- // A as B,
12883
- // } from './mod';
12884
- // The per-line pass above misses these because `from '…'` sits on a later
12885
- // line. Trigger only when the opening line has no quote and no `from` yet,
12886
- // then gather forward until the source string appears.
12887
- for (let i = 0; i < lines.length; i++) {
12888
- const start = lines[i];
12889
- if (!/^\s*(?:import|export)\b/.test(start)) continue;
12890
- if (/['"]/.test(start) || /\bfrom\b/.test(start)) continue; // single-line, already handled
12891
- let joined = start;
12892
- for (let j = i + 1; j < Math.min(lines.length, i + 12); j++) {
12893
- joined += ' ' + lines[j];
12894
- const fm = joined.match(/\bfrom\s*['"]([^'"]+)['"]/);
12895
- if (fm) { push(fm[1], 'js', i + 1, start.trim()); break; }
12896
- if (/['"]/.test(lines[j]) && !/\bfrom\b/.test(joined)) break; // a string that isn't a source — bail
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
- * Extract npm/pnpm/yarn script invocations (`npm run <name>`).
12904
- * Only the explicit `run` form is matched, to avoid confusing package-manager
12905
- * subcommands (`yarn add`, `pnpm install`) with script names.
12906
- * @param {string} text
12907
- * @returns {{ name: string, line: number }[]}
12908
- */
12909
- function extractNpmScripts(text) {
12910
- const lines = text.split('\n');
12911
- const out = [];
12912
- const seen = new Set();
12913
- const re = /\b(?:npm|pnpm|yarn)\s+run(?:-script)?\s+([A-Za-z0-9:_-]+)/g;
12914
- for (let i = 0; i < lines.length; i++) {
12915
- let m;
12916
- re.lastIndex = 0;
12917
- while ((m = re.exec(lines[i])) !== null) {
12918
- const name = m[1];
12919
- if (seen.has(name)) continue;
12920
- seen.add(name);
12921
- out.push({ name, line: i + 1 });
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
- * Extract function/class symbol references that look like calls.
12929
- * Restricted to backtick-wrapped calls (`foo(...)`) for high precision.
12930
- * @param {string} text
12931
- * @returns {{ name: string, line: number }[]}
12932
- */
12933
- function extractSymbols(text) {
12934
- const lines = text.split('\n');
12935
- const out = [];
12936
- const seen = new Set();
12937
- const re = /`([A-Za-z_$][\w$]*)\s*\([^`]*\)`/g;
12938
- for (let i = 0; i < lines.length; i++) {
12939
- let m;
12940
- re.lastIndex = 0;
12941
- while ((m = re.exec(lines[i])) !== null) {
12942
- const name = m[1];
12943
- const key = name + '@' + (i + 1);
12944
- if (seen.has(key)) continue;
12945
- seen.add(key);
12946
- out.push({ name, line: i + 1 });
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
- 'use strict';
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
- * Hallucination Guard — deterministic core (Reliable MVP, v6.15.0).
13287
- *
13288
- * Given the text of an AI answer, flag claims that do not match the repo:
13289
- * - fake-file : a referenced path is not on disk
13290
- * - fake-test-file : a referenced *test* path is not on disk (sub-type)
13291
- * - fake-import : a relative import does not resolve; a bare import is
13292
- * absent from package.json deps (builtins allow-listed)
13293
- * - fake-symbol : a called function/class is absent from the symbol index
13294
- * - fake-npm-script: `npm run X` where X is not a package.json script
13295
- *
13296
- * Each issue carries a `confidence` (detection certainty) and, where a near
13297
- * match exists, a heuristic `suggestion` ("Did you mean …?"). No network, no
13298
- * LLM. Reuses SigMap primitives (buildSigIndex) but every external dependency
13299
- * is injectable via `opts` so the core stays unit-testable.
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 fs = require('fs');
13303
- const path = require('path');
13304
- const parsers = __require('./src/verify/parsers');
13305
- const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
13306
-
13307
- // A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
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
- * Build the set of known symbol identifiers from the SigMap signature index,
13346
- * plus `{ name, file, line }` candidates (for closest-match suggestions).
13347
- */
13348
- function buildSymbolSet(cwd) {
13349
- const set = new Set();
13350
- let fileKeys = [];
13351
- let symbolCandidates = [];
13352
- try {
13353
- const { buildSigIndex } = __require('./src/retrieval/ranker');
13354
- const idx = buildSigIndex(cwd);
13355
- fileKeys = [...idx.keys()];
13356
- for (const sigs of idx.values()) {
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
- /** Load declared dependency names from package.json. */
13369
- function loadDeps(cwd) {
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
- /** Load the set of npm script names declared in package.json. */
13385
- function loadScripts(cwd) {
13386
- const scripts = new Set();
13387
- try {
13388
- const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
13389
- if (pkg.scripts && typeof pkg.scripts === 'object') {
13390
- for (const name of Object.keys(pkg.scripts)) scripts.add(name);
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
- /** Default file-existence check: resolve a referenced path against cwd. */
13397
- function defaultFileExists(cwd, ref) {
13398
- const clean = ref.replace(/^\.\//, '');
13399
- for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
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
- if (fs.existsSync(c)) return true;
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
- /** Default relative-import resolver: fs candidates + basename match in index. */
13408
- function defaultRelativeResolvable(cwd, mod, fileBasenames) {
13409
- const base = path.resolve(cwd, mod);
13410
- for (const e of REL_EXTS) {
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
- if (fs.existsSync(base + e)) return true;
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
- for (const idx of REL_INDEX) {
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
- if (fs.existsSync(path.join(base, idx))) return true;
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
- // 1. fake-file / fake-test-file
13494
- for (const { path: p, line } of parsers.extractFilePaths(answerText)) {
13495
- if (fileExists(p)) continue;
13496
- const isTest = isTestPath(p);
13497
- const match = closestMatch(path.basename(p), fileBasenameCandidates, { minLen: 4 });
13498
- add({
13499
- type: isTest ? 'fake-test-file' : 'fake-file',
13500
- value: p,
13501
- line,
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
- // 2. fake-import
13509
- for (const imp of parsers.extractImports(answerText)) {
13510
- if (imp.relative) {
13511
- if (!relativeResolvable(imp.module)) {
13512
- add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Import does not resolve: ${imp.module}`, confidence: 'high' });
13513
- }
13514
- continue;
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
- // Bare module — only verifiable for JS when a package.json exists.
13517
- const top = imp.module.split('/')[0];
13518
- if (imp.kind === 'js') {
13519
- if (!hasPkg) continue;
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
- // Python bare imports: stdlib is unbounded offline skip to keep precision.
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
- // 3. fake-symbol
13541
- if (symbolSet.size > 0) {
13542
- for (const { name, line } of parsers.extractSymbols(answerText)) {
13543
- if (symbolSet.has(name)) continue;
13544
- if (LANG_GLOBALS.has(name) || NODE_BUILTINS.has(name) || PY_BUILTINS.has(name)) continue;
13545
- const match = closestMatch(name, symbolCandidates, { minLen: 4 });
13546
- add({
13547
- type: 'fake-symbol',
13548
- value: name,
13549
- line,
13550
- message: `Symbol not found in repo index: ${name}()`,
13551
- confidence: 'medium',
13552
- suggestion: match ? formatSuggestion(match, true) : null,
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
- // 4. fake-npm-script
13558
- if (hasPkg && scripts.size > 0) {
13559
- for (const { name, line } of parsers.extractNpmScripts(answerText)) {
13560
- if (scripts.has(name)) continue;
13561
- const match = closestMatch(name, [...scripts], { minLen: 2 });
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-npm-script',
13564
- value: name,
13523
+ type: isTest ? 'fake-test-file' : 'fake-file',
13524
+ value: p,
13565
13525
  line,
13566
- message: `npm script not in package.json: ${name}`,
13526
+ message: `${isTest ? 'Test file' : 'File'} not found on disk: ${p}`,
13567
13527
  confidence: 'high',
13568
- suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
13528
+ suggestion: match ? formatSuggestion(match, false) : null,
13569
13529
  });
13570
13530
  }
13571
- }
13572
13531
 
13573
- issues.sort((a, b) => a.line - b.line);
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
- const byType = {
13576
- 'fake-file': 0, 'fake-test-file': 0, 'fake-import': 0,
13577
- 'fake-symbol': 0, 'fake-npm-script': 0,
13578
- };
13579
- for (const i of issues) byType[i.type] = (byType[i.type] || 0) + 1;
13580
-
13581
- const summary = {
13582
- total: issues.length,
13583
- byType,
13584
- clean: issues.length === 0,
13585
- symbolsIndexed: symbolSet.size,
13586
- withSuggestion: issues.filter((i) => i.suggestion).length,
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
- return { issues, summary };
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
- module.exports = { verify, buildSymbolSet, loadDeps, loadScripts, isTestPath };
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.0';
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.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
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-14)
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.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
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-14)
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.0",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "7.22.0",
3
+ "version": "7.22.2",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "7.22.0",
3
+ "version": "7.22.2",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '7.22.0',
21
+ version: '7.22.2',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -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' });
@@ -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
  }