sigmap 7.21.0 → 7.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,27 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [7.22.1] — 2026-06-18
14
+
15
+ Patch release — hardens the `verify-ai-output` file-path extractor against the dominant false-positive class.
16
+
17
+ ### Fixed
18
+ - **`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.
19
+
20
+ ---
21
+
22
+ ## [7.22.0] — 2026-06-18
23
+
24
+ Minor release — realistic §9 ablation (real-symbol corpus, exact-signature grounding, --verbose) + Gemini model fix.
25
+
26
+ ### Added
27
+ - **Realistic §9 ablation — real-symbol corpus, exact-signature grounding, `--verbose` (#344):** the LLM A/B ablation now measures something meaningful. `buildGrounding` emits **exact signatures grouped by file** (what `get_callee_signatures` returns, bounded by `maxSignatures`) instead of a flat symbol-name dump — the real product behavior. New `scripts/gen-ablation-corpus.mjs` generates ~40 tasks from the repo's actual exported symbols/files (`benchmarks/llm-ablation-tasks.json`). `src/eval/llm-ablation.js` adds `scoreAnswerDetail` (count + issues) and `runAblation`'s `collectIssues`; the runner's `--verbose` prints every flagged item per arm. A 40-task Gemini run showed grounding reduced flagged errors 62.5 → 22.5 per 100 (directionally positive vs the earlier 4-task noise) — and `--verbose` revealed most flags are `verify-ai-output` file-path false-positives (e.g. "Node.js"), the next thing to harden before publishing a number.
28
+
29
+ ### Fixed
30
+ - **Gemini default model (#343):** the ablation runner's default `gemini-2.0-flash` was retired by AI Studio (404 NOT_FOUND); the default is now the live `gemini-2.5-flash`. The `--model` flag selects any model.
31
+
32
+ ---
33
+
13
34
  ## [7.21.0] — 2026-06-18
14
35
 
15
36
  Minor release — LLM ablation runner gains a Gemini (AI Studio) provider.
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-18
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.21.0',
7934
+ version: '7.22.1',
7935
7935
  description: 'SigMap MCP server — code signatures on demand',
7936
7936
  };
7937
7937
 
@@ -12756,207 +12756,220 @@ 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)(?:[-_.]|$)/i;
12791
+
12792
+ /**
12793
+ * Extract fenced code blocks.
12794
+ * @param {string} text
12795
+ * @returns {{ lang: string, content: string, line: number }[]}
12796
+ */
12797
+ function extractCodeBlocks(text) {
12798
+ const blocks = [];
12799
+ const lines = text.split('\n');
12800
+ let inBlock = false;
12801
+ let lang = '';
12802
+ let buf = [];
12803
+ let startLine = 0;
12804
+ for (let i = 0; i < lines.length; i++) {
12805
+ const m = lines[i].match(/^```(\w*)/);
12806
+ if (m) {
12807
+ if (!inBlock) {
12808
+ inBlock = true;
12809
+ lang = m[1] || '';
12810
+ buf = [];
12811
+ startLine = i + 2; // first content line (1-based)
12812
+ } else {
12813
+ blocks.push({ lang, content: buf.join('\n'), line: startLine });
12814
+ inBlock = false;
12815
+ }
12816
+ continue;
12805
12817
  }
12806
- continue;
12818
+ if (inBlock) buf.push(lines[i]);
12807
12819
  }
12808
- if (inBlock) buf.push(lines[i]);
12820
+ return blocks;
12809
12821
  }
12810
- return blocks;
12811
- }
12812
12822
 
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);
12823
+ /**
12824
+ * Extract file-path references (deduped, first-seen line kept).
12825
+ * A token counts as a path when it has a `.<letter…>` extension AND
12826
+ * either contains a `/` or carries a known code/config extension.
12827
+ * @param {string} text
12828
+ * @returns {{ path: string, line: number }[]}
12829
+ */
12830
+ function extractFilePaths(text) {
12831
+ const lines = text.split('\n');
12832
+ const seen = new Map();
12833
+ const re = /(?:^|[\s`"'(\[<])([A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*)/g;
12834
+ for (let i = 0; i < lines.length; i++) {
12835
+ const line = lines[i];
12836
+ let m;
12837
+ re.lastIndex = 0;
12838
+ while ((m = re.exec(line)) !== null) {
12839
+ const p = m[1];
12840
+ if (/^https?:/i.test(p)) continue;
12841
+ const ext = (p.split('.').pop() || '').toLowerCase();
12842
+ const hasSlash = p.includes('/');
12843
+ if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
12844
+ if (LIBRARY_TOKENS.has(p.toLowerCase())) continue;
12845
+ const base = p.split('/').pop();
12846
+ if (PLACEHOLDER_RE.test(base)) continue;
12847
+ if (!seen.has(p)) seen.set(p, i + 1);
12848
+ }
12835
12849
  }
12850
+ return [...seen.entries()].map(([p, line]) => ({ path: p, line }));
12836
12851
  }
12837
- return [...seen.entries()].map(([p, line]) => ({ path: p, line }));
12838
- }
12839
12852
 
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
12853
+ /**
12854
+ * Extract import / require statements.
12855
+ * @param {string} text
12856
+ * @returns {{ module: string, kind: 'js'|'py', relative: boolean, line: number, raw: string }[]}
12857
+ */
12858
+ function extractImports(text) {
12859
+ const lines = text.split('\n');
12860
+ const out = [];
12861
+ const push = (module, kind, line, raw) => {
12862
+ if (!module) return;
12863
+ out.push({ module, kind, relative: /^[./]/.test(module), line, raw: raw.trim() });
12864
+ };
12865
+ for (let i = 0; i < lines.length; i++) {
12866
+ const line = lines[i];
12867
+ let m;
12868
+ // JS/TS: import ... from 'x' | export ... from 'x'
12869
+ if ((m = line.match(/\b(?:import|export)\b[^'"]*\bfrom\s*['"]([^'"]+)['"]/))) {
12870
+ push(m[1], 'js', i + 1, line);
12871
+ } else if ((m = line.match(/\bimport\s*['"]([^'"]+)['"]/))) {
12872
+ // side-effect import 'x'
12873
+ push(m[1], 'js', i + 1, line);
12874
+ }
12875
+ // require('x') / dynamic import('x') — may co-occur, scan separately
12876
+ const reqRe = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
12877
+ let r;
12878
+ while ((r = reqRe.exec(line)) !== null) push(r[1], 'js', i + 1, line);
12879
+
12880
+ // TS: import X = require('mod')
12881
+ if ((m = line.match(/\bimport\s+[A-Za-z_$][\w$]*\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/))) {
12882
+ push(m[1], 'js', i + 1, line);
12883
+ }
12884
+
12885
+ // Python: from x import y | import x
12886
+ if ((m = line.match(/^\s*from\s+([.\w]+)\s+import\b/))) {
12887
+ push(m[1], 'py', i + 1, line);
12888
+ } else if ((m = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/))) {
12889
+ push(m[1], 'py', i + 1, line);
12890
+ }
12891
+ }
12892
+
12893
+ // Multi-line JS/TS imports, e.g.
12894
+ // import {
12895
+ // A as B,
12896
+ // } from './mod';
12897
+ // The per-line pass above misses these because `from '…'` sits on a later
12898
+ // line. Trigger only when the opening line has no quote and no `from` yet,
12899
+ // then gather forward until the source string appears.
12900
+ for (let i = 0; i < lines.length; i++) {
12901
+ const start = lines[i];
12902
+ if (!/^\s*(?:import|export)\b/.test(start)) continue;
12903
+ if (/['"]/.test(start) || /\bfrom\b/.test(start)) continue; // single-line, already handled
12904
+ let joined = start;
12905
+ for (let j = i + 1; j < Math.min(lines.length, i + 12); j++) {
12906
+ joined += ' ' + lines[j];
12907
+ const fm = joined.match(/\bfrom\s*['"]([^'"]+)['"]/);
12908
+ if (fm) { push(fm[1], 'js', i + 1, start.trim()); break; }
12909
+ if (/['"]/.test(lines[j]) && !/\bfrom\b/.test(joined)) break; // a string that isn't a source — bail
12910
+ }
12897
12911
  }
12912
+ return out;
12898
12913
  }
12899
- return out;
12900
- }
12901
12914
 
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 });
12915
+ /**
12916
+ * Extract npm/pnpm/yarn script invocations (`npm run <name>`).
12917
+ * Only the explicit `run` form is matched, to avoid confusing package-manager
12918
+ * subcommands (`yarn add`, `pnpm install`) with script names.
12919
+ * @param {string} text
12920
+ * @returns {{ name: string, line: number }[]}
12921
+ */
12922
+ function extractNpmScripts(text) {
12923
+ const lines = text.split('\n');
12924
+ const out = [];
12925
+ const seen = new Set();
12926
+ const re = /\b(?:npm|pnpm|yarn)\s+run(?:-script)?\s+([A-Za-z0-9:_-]+)/g;
12927
+ for (let i = 0; i < lines.length; i++) {
12928
+ let m;
12929
+ re.lastIndex = 0;
12930
+ while ((m = re.exec(lines[i])) !== null) {
12931
+ const name = m[1];
12932
+ if (seen.has(name)) continue;
12933
+ seen.add(name);
12934
+ out.push({ name, line: i + 1 });
12935
+ }
12922
12936
  }
12937
+ return out;
12923
12938
  }
12924
- return out;
12925
- }
12926
12939
 
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 });
12940
+ /**
12941
+ * Extract function/class symbol references that look like calls.
12942
+ * Restricted to backtick-wrapped calls (`foo(...)`) for high precision.
12943
+ * @param {string} text
12944
+ * @returns {{ name: string, line: number }[]}
12945
+ */
12946
+ function extractSymbols(text) {
12947
+ const lines = text.split('\n');
12948
+ const out = [];
12949
+ const seen = new Set();
12950
+ const re = /`([A-Za-z_$][\w$]*)\s*\([^`]*\)`/g;
12951
+ for (let i = 0; i < lines.length; i++) {
12952
+ let m;
12953
+ re.lastIndex = 0;
12954
+ while ((m = re.exec(lines[i])) !== null) {
12955
+ const name = m[1];
12956
+ const key = name + '@' + (i + 1);
12957
+ if (seen.has(key)) continue;
12958
+ seen.add(key);
12959
+ out.push({ name, line: i + 1 });
12960
+ }
12947
12961
  }
12962
+ return out;
12948
12963
  }
12949
- return out;
12950
- }
12951
-
12952
- module.exports = {
12953
- extractCodeBlocks,
12954
- extractFilePaths,
12955
- extractImports,
12956
- extractSymbols,
12957
- extractNpmScripts,
12958
- };
12959
12964
 
12965
+ module.exports = {
12966
+ extractCodeBlocks,
12967
+ extractFilePaths,
12968
+ extractImports,
12969
+ extractSymbols,
12970
+ extractNpmScripts,
12971
+ };
12972
+
12960
12973
  };
12961
12974
 
12962
12975
  // ── ./src/verify/closest-match ──
@@ -13609,7 +13622,7 @@ function __tryGit(args, opts = {}) {
13609
13622
  catch (_) { return ''; }
13610
13623
  }
13611
13624
 
13612
- const VERSION = '7.21.0';
13625
+ const VERSION = '7.22.1';
13613
13626
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
13614
13627
 
13615
13628
  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.21.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.22.1 | Benchmark: sigmap-v7.0-main (2026-06-18)
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-18)
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.21.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.22.1 | Benchmark: sigmap-v7.0-main (2026-06-18)
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-18)
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.21.0",
3
+ "version": "7.22.1",
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.21.0",
3
+ "version": "7.22.1",
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.21.0",
3
+ "version": "7.22.1",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -13,59 +13,82 @@
13
13
 
14
14
  const { verify } = require('../verify/hallucination-guard');
15
15
 
16
+ const path = require('path');
17
+
18
+ /** Strip a signature's trailing line anchor (` :12-20`) for prompt cleanliness. */
19
+ function _cleanSig(sig) {
20
+ return String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim();
21
+ }
22
+
16
23
  /**
17
24
  * Build the SigMap grounding block for a repo — what we prepend to a task
18
- * prompt in arm B. Conventions (the house style) + the known-symbol list
19
- * (so the model can reference real names instead of guessing).
25
+ * prompt in arm B. Conventions (the house style) + **exact signatures** grouped
26
+ * by file (what `get_callee_signatures` returns), so the model references the
27
+ * real surface instead of guessing — the actual product behavior, not a flat
28
+ * name dump.
20
29
  * @param {string} cwd
21
30
  * @param {object} [opts]
22
- * @param {number} [opts.maxSymbols=80]
31
+ * @param {number} [opts.maxSignatures=150] cap on signature lines (bounds prompt size)
23
32
  * @returns {string}
24
33
  */
25
34
  function buildGrounding(cwd, opts = {}) {
26
- const maxSymbols = opts.maxSymbols != null ? opts.maxSymbols : 80;
35
+ const maxSignatures = opts.maxSignatures != null ? opts.maxSignatures : 150;
27
36
  const parts = [];
28
37
 
38
+ let index = null;
29
39
  try {
30
- const { extractConventions } = require('../conventions/extract');
31
- const { renderConventionsBlock } = require('../conventions/inject');
32
- const { loadConfig } = require('../config/loader');
33
- let files = [];
34
- try {
35
- const cfg = loadConfig(cwd);
36
- const { buildSigIndex } = require('../retrieval/ranker');
37
- files = [...buildSigIndex(cwd).keys()];
38
- void cfg;
39
- } catch (_) {}
40
- const conv = extractConventions(cwd, files);
41
- parts.push(renderConventionsBlock(conv));
40
+ const { buildSigIndex } = require('../retrieval/ranker');
41
+ index = buildSigIndex(cwd);
42
42
  } catch (_) {}
43
43
 
44
44
  try {
45
- const { buildSymbolSet } = require('../verify/hallucination-guard');
46
- const { set } = buildSymbolSet(cwd);
47
- const names = [...set].slice(0, maxSymbols);
48
- if (names.length) parts.push(`## Known symbols (reference these exactly)\n${names.join(', ')}`);
45
+ const { extractConventions } = require('../conventions/extract');
46
+ const { renderConventionsBlock } = require('../conventions/inject');
47
+ const files = index ? [...index.keys()] : [];
48
+ parts.push(renderConventionsBlock(extractConventions(cwd, files)));
49
49
  } catch (_) {}
50
50
 
51
+ if (index) {
52
+ const lines = ['## Exact signatures (use these — do not invent symbols or paths)'];
53
+ let count = 0;
54
+ for (const [file, sigs] of index) {
55
+ if (count >= maxSignatures) break;
56
+ const rel = path.relative(cwd, file).replace(/\\/g, '/');
57
+ const clean = (sigs || []).map(_cleanSig).filter(Boolean);
58
+ if (!clean.length) continue;
59
+ lines.push(`### ${rel}`);
60
+ for (const s of clean) {
61
+ if (count >= maxSignatures) break;
62
+ lines.push(s);
63
+ count++;
64
+ }
65
+ }
66
+ if (count > 0) parts.push(lines.join('\n'));
67
+ }
68
+
51
69
  return parts.join('\n\n');
52
70
  }
53
71
 
54
72
  /**
55
- * Count flagged codebase-fact errors in an answer (the §9 metric).
73
+ * Score an answer: flagged codebase-fact errors + the issue list (the §9 metric).
56
74
  * @param {string} answerText
57
75
  * @param {string} cwd
58
- * @returns {number}
76
+ * @returns {{ total: number, issues: object[] }}
59
77
  */
60
- function scoreAnswer(answerText, cwd) {
78
+ function scoreAnswerDetail(answerText, cwd) {
61
79
  try {
62
- const { summary } = verify(String(answerText || ''), cwd);
63
- return summary.total || 0;
80
+ const { issues, summary } = verify(String(answerText || ''), cwd);
81
+ return { total: summary.total || 0, issues: issues || [] };
64
82
  } catch (_) {
65
- return 0;
83
+ return { total: 0, issues: [] };
66
84
  }
67
85
  }
68
86
 
87
+ /** Count flagged codebase-fact errors in an answer (the §9 metric). */
88
+ function scoreAnswer(answerText, cwd) {
89
+ return scoreAnswerDetail(answerText, cwd).total;
90
+ }
91
+
69
92
  /**
70
93
  * Run the A/B ablation over a task corpus.
71
94
  * @param {Array<{id:string, prompt:string}>} tasks
@@ -73,6 +96,7 @@ function scoreAnswer(answerText, cwd) {
73
96
  * @param {(prompt:string, meta:object)=>string} complete injected model call
74
97
  * @param {object} [opts]
75
98
  * @param {string} [opts.grounding] precomputed grounding (else built from cwd)
99
+ * @param {boolean} [opts.collectIssues] attach `aIssues`/`bIssues` per task
76
100
  * @returns {{ tasks: object[], aggregate: object }}
77
101
  */
78
102
  function runAblation(tasks, cwd, complete, opts = {}) {
@@ -88,11 +112,13 @@ function runAblation(tasks, cwd, complete, opts = {}) {
88
112
  const outA = String(complete(basePrompt, { id: task.id, grounded: false }) || '');
89
113
  const outB = String(complete(groundedPrompt, { id: task.id, grounded: true }) || '');
90
114
 
91
- const aFlagged = scoreAnswer(outA, cwd);
92
- const bFlagged = scoreAnswer(outB, cwd);
93
- sumA += aFlagged;
94
- sumB += bFlagged;
95
- rows.push({ id: task.id, aFlagged, bFlagged });
115
+ const a = scoreAnswerDetail(outA, cwd);
116
+ const b = scoreAnswerDetail(outB, cwd);
117
+ sumA += a.total;
118
+ sumB += b.total;
119
+ const row = { id: task.id, aFlagged: a.total, bFlagged: b.total };
120
+ if (opts.collectIssues) { row.aIssues = a.issues; row.bIssues = b.issues; }
121
+ rows.push(row);
96
122
  }
97
123
 
98
124
  const n = rows.length;
@@ -110,4 +136,4 @@ function runAblation(tasks, cwd, complete, opts = {}) {
110
136
  };
111
137
  }
112
138
 
113
- module.exports = { buildGrounding, scoreAnswer, runAblation };
139
+ module.exports = { buildGrounding, scoreAnswer, scoreAnswerDetail, runAblation };
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.21.0',
21
+ version: '7.22.1',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -21,6 +21,17 @@ 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)(?:[-_.]|$)/i;
34
+
24
35
  /**
25
36
  * Extract fenced code blocks.
26
37
  * @param {string} text
@@ -73,6 +84,9 @@ function extractFilePaths(text) {
73
84
  const ext = (p.split('.').pop() || '').toLowerCase();
74
85
  const hasSlash = p.includes('/');
75
86
  if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
87
+ if (LIBRARY_TOKENS.has(p.toLowerCase())) continue;
88
+ const base = p.split('/').pop();
89
+ if (PLACEHOLDER_RE.test(base)) continue;
76
90
  if (!seen.has(p)) seen.set(p, i + 1);
77
91
  }
78
92
  }