sigmap 6.13.0 → 6.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,18 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [6.14.0] — 2026-06-07
14
+
15
+ ### Added
16
+
17
+ - **`verify-ai-output` — Hallucination Guard prototype (Phase 1 MVP, #227, PR #228):**
18
+ - New command `sigmap verify-ai-output <answer.md> [--json]` flags fabricated claims in an AI answer against the real repository. Deterministic core — runs fully offline, no LLM.
19
+ - Three detectors: **fake-file** (referenced path absent on disk), **fake-import** (relative import does not resolve; bare import absent from `package.json` deps, with Node/Python builtins allow-listed and scoped packages handled), and **fake-symbol** (called function/class absent from the SigMap symbol index via `buildSigIndex`).
20
+ - Markdown report by default, `--json` for CI (`{ file, issues, summary }`). Exits `1` when any issue is found, `0` when clean.
21
+ - New modules `src/verify/parsers.js` (file/import/symbol/code-block extraction) and `src/verify/hallucination-guard.js` (`verify(answerText, cwd, opts)`); all external lookups are injectable so the core is unit-testable.
22
+
23
+ ---
24
+
13
25
  ## [6.13.0] — 2026-06-05
14
26
 
15
27
  ### Added
package/gen-context.js CHANGED
@@ -6154,7 +6154,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
6154
6154
 
6155
6155
  const SERVER_INFO = {
6156
6156
  name: 'sigmap',
6157
- version: '6.13.0',
6157
+ version: '6.14.0',
6158
6158
  description: 'SigMap MCP server — code signatures on demand',
6159
6159
  };
6160
6160
 
@@ -8978,7 +8978,7 @@ const path = require('path');
8978
8978
  const os = require('os');
8979
8979
  const { execSync } = require('child_process');
8980
8980
 
8981
- const VERSION = '6.13.0';
8981
+ const VERSION = '6.14.0';
8982
8982
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8983
8983
 
8984
8984
  function requireSourceOrBundled(key) {
@@ -10737,6 +10737,8 @@ Usage:
10737
10737
  ${cmd} --impact <file> Show every file impacted by changing <file>
10738
10738
  ${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
10739
10739
  ${cmd} --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
10740
+ ${cmd} verify-ai-output <answer.md> Flag fake files/imports/symbols in an AI answer
10741
+ ${cmd} verify-ai-output <answer.md> --json Hallucination report as JSON (exits 1 if issues)
10740
10742
  ${cmd} --init Write example config + .contextignore scaffold
10741
10743
  ${cmd} --help Show this message
10742
10744
  ${cmd} --version Show version
@@ -11858,6 +11860,45 @@ function main() {
11858
11860
  process.exit(0);
11859
11861
  }
11860
11862
 
11863
+ // `sigmap verify-ai-output <answer.md>` — Hallucination Guard (deterministic core)
11864
+ if (args[0] === 'verify-ai-output') {
11865
+ const target = args[1];
11866
+ const jsonOut = args.includes('--json');
11867
+ if (!target || target.startsWith('--')) {
11868
+ console.error('[sigmap] Usage: sigmap verify-ai-output <answer.md> [--json]');
11869
+ process.exit(1);
11870
+ }
11871
+ const absTarget = path.resolve(cwd, target);
11872
+ if (!fs.existsSync(absTarget)) {
11873
+ console.error(`[sigmap] file not found: ${target}`);
11874
+ process.exit(1);
11875
+ }
11876
+
11877
+ const answerText = fs.readFileSync(absTarget, 'utf8');
11878
+ const { verify } = requireSourceOrBundled('./src/verify/hallucination-guard');
11879
+ const { issues, summary } = verify(answerText, cwd);
11880
+
11881
+ if (jsonOut) {
11882
+ process.stdout.write(JSON.stringify({ file: path.relative(cwd, absTarget) || target, issues, summary }) + '\n');
11883
+ process.exit(summary.total > 0 ? 1 : 0);
11884
+ }
11885
+
11886
+ const rel = path.relative(cwd, absTarget) || target;
11887
+ if (summary.total === 0) {
11888
+ console.log(`[sigmap] ✓ ${rel} — no hallucinations detected (${summary.symbolsIndexed} symbols indexed)`);
11889
+ process.exit(0);
11890
+ }
11891
+
11892
+ const labels = { 'fake-file': 'Fake file', 'fake-import': 'Fake import', 'fake-symbol': 'Fake symbol' };
11893
+ console.log(`[sigmap] ✗ ${rel} — ${summary.total} issue${summary.total === 1 ? '' : 's'} found`);
11894
+ console.log(` fake-file: ${summary.byType['fake-file']} fake-import: ${summary.byType['fake-import']} fake-symbol: ${summary.byType['fake-symbol']}`);
11895
+ console.log('');
11896
+ for (const issue of issues) {
11897
+ console.log(` L${issue.line} [${labels[issue.type] || issue.type}] ${issue.message}`);
11898
+ }
11899
+ process.exit(1);
11900
+ }
11901
+
11861
11902
  // Feature 1: `sigmap explain <file>` — why a file is included or excluded
11862
11903
  if (args[0] === 'explain' || args.includes('--explain')) {
11863
11904
  const target = args[0] === 'explain'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "6.13.0",
3
+ "version": "6.14.0",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "6.13.0",
3
+ "version": "6.14.0",
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": "6.13.0",
3
+ "version": "6.14.0",
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: '6.13.0',
21
+ version: '6.14.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -0,0 +1,219 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Hallucination Guard — deterministic core (Phase 1 MVP).
5
+ *
6
+ * Given the text of an AI answer, flag claims that do not match the repo:
7
+ * - fake-file : a referenced path is not on disk
8
+ * - fake-import : a relative import does not resolve; a bare import is
9
+ * absent from package.json deps (builtins allow-listed)
10
+ * - fake-symbol : a called function/class is absent from the symbol index
11
+ *
12
+ * No network, no LLM. Reuses SigMap primitives (buildSigIndex) but every
13
+ * external dependency is injectable via `opts` so the core stays unit-testable.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const parsers = require('./parsers');
19
+
20
+ const NODE_BUILTINS = new Set([
21
+ 'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https', 'crypto',
22
+ 'child_process', 'url', 'querystring', 'assert', 'zlib', 'readline', 'net',
23
+ 'tls', 'dns', 'buffer', 'process', 'vm', 'module', 'console', 'timers',
24
+ 'string_decoder', 'perf_hooks', 'worker_threads', 'cluster', 'dgram', 'v8',
25
+ 'tty', 'repl', 'async_hooks', 'inspector', 'fs/promises', 'path/posix',
26
+ ]);
27
+
28
+ const PY_BUILTINS = new Set([
29
+ 'os', 'sys', 're', 'json', 'math', 'typing', 'collections', 'itertools',
30
+ 'functools', 'datetime', 'pathlib', 'subprocess', 'abc', 'dataclasses',
31
+ 'enum', 'io', 'time', 'random', 'logging', 'argparse', 'unittest', 'asyncio',
32
+ 'copy', 'hashlib', 'threading', 'string', 'csv', 'glob', 'shutil', 'tempfile',
33
+ ]);
34
+
35
+ const LANG_GLOBALS = new Set([
36
+ // JS
37
+ 'console', 'require', 'module', 'exports', 'process', 'Object', 'Array',
38
+ 'String', 'Number', 'Boolean', 'Math', 'JSON', 'Date', 'Promise', 'Map',
39
+ 'Set', 'WeakMap', 'WeakSet', 'RegExp', 'Error', 'Symbol', 'parseInt',
40
+ 'parseFloat', 'isNaN', 'setTimeout', 'setInterval', 'clearTimeout', 'fetch',
41
+ 'Buffer', 'Function', 'eval', 'encodeURIComponent', 'decodeURIComponent',
42
+ // Python
43
+ 'print', 'len', 'range', 'str', 'int', 'float', 'dict', 'list', 'tuple',
44
+ 'set', 'bool', 'open', 'enumerate', 'zip', 'map', 'filter', 'sorted',
45
+ 'sum', 'min', 'max', 'abs', 'isinstance', 'super', 'type', 'getattr',
46
+ 'setattr', 'hasattr',
47
+ ]);
48
+
49
+ const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
50
+ const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];
51
+
52
+ /** Build the set of known symbol identifiers from the SigMap signature index. */
53
+ function buildSymbolSet(cwd) {
54
+ const set = new Set();
55
+ let fileKeys = [];
56
+ try {
57
+ const { buildSigIndex } = require('../retrieval/ranker');
58
+ const idx = buildSigIndex(cwd);
59
+ fileKeys = [...idx.keys()];
60
+ for (const sigs of idx.values()) {
61
+ for (const sig of sigs) {
62
+ const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
63
+ const ids = cleaned.match(/[A-Za-z_$][\w$]*/g) || [];
64
+ for (const id of ids) set.add(id);
65
+ }
66
+ }
67
+ } catch (_) {}
68
+ return { set, fileKeys };
69
+ }
70
+
71
+ /** Load declared dependency names from package.json. */
72
+ function loadDeps(cwd) {
73
+ const deps = new Set();
74
+ let hasPkg = false;
75
+ try {
76
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
77
+ hasPkg = true;
78
+ for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
79
+ if (pkg[k] && typeof pkg[k] === 'object') {
80
+ for (const name of Object.keys(pkg[k])) deps.add(name);
81
+ }
82
+ }
83
+ } catch (_) {}
84
+ return { deps, hasPkg };
85
+ }
86
+
87
+ /** Default file-existence check: resolve a referenced path against cwd. */
88
+ function defaultFileExists(cwd, ref) {
89
+ const clean = ref.replace(/^\.\//, '');
90
+ for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
91
+ try {
92
+ if (fs.existsSync(c)) return true;
93
+ } catch (_) {}
94
+ }
95
+ return false;
96
+ }
97
+
98
+ /** Default relative-import resolver: fs candidates + basename match in index. */
99
+ function defaultRelativeResolvable(cwd, mod, fileBasenames) {
100
+ const base = path.resolve(cwd, mod);
101
+ for (const e of REL_EXTS) {
102
+ try {
103
+ if (fs.existsSync(base + e)) return true;
104
+ } catch (_) {}
105
+ }
106
+ for (const idx of REL_INDEX) {
107
+ try {
108
+ if (fs.existsSync(path.join(base, idx))) return true;
109
+ } catch (_) {}
110
+ }
111
+ // Fall back to basename match against the indexed file set (the answer's
112
+ // import is relative to a file we cannot know, so a name match is enough
113
+ // to avoid false positives).
114
+ const wantBase = path.basename(mod).replace(/\.[^.]+$/, '').toLowerCase();
115
+ return fileBasenames.has(wantBase);
116
+ }
117
+
118
+ /**
119
+ * Verify an AI answer against the repository.
120
+ *
121
+ * @param {string} answerText
122
+ * @param {string} cwd
123
+ * @param {object} [opts]
124
+ * @param {Set<string>} [opts.symbolSet] override known symbols
125
+ * @param {Set<string>} [opts.deps] override package deps
126
+ * @param {boolean} [opts.hasPkg] whether a package.json exists
127
+ * @param {(ref: string) => boolean} [opts.fileExists] override file check
128
+ * @param {(mod: string) => boolean} [opts.relativeResolvable] override rel-import check
129
+ * @returns {{ issues: object[], summary: object }}
130
+ */
131
+ function verify(answerText, cwd, opts = {}) {
132
+ let symbolSet = opts.symbolSet;
133
+ let fileBasenames = opts.fileBasenames;
134
+ if (!symbolSet) {
135
+ const built = buildSymbolSet(cwd);
136
+ symbolSet = built.set;
137
+ fileBasenames = new Set(built.fileKeys.map(
138
+ (k) => path.basename(k).replace(/\.[^.]+$/, '').toLowerCase()
139
+ ));
140
+ }
141
+ if (!fileBasenames) fileBasenames = new Set();
142
+
143
+ let deps = opts.deps;
144
+ let hasPkg = opts.hasPkg;
145
+ if (!deps) {
146
+ const loaded = loadDeps(cwd);
147
+ deps = loaded.deps;
148
+ if (hasPkg === undefined) hasPkg = loaded.hasPkg;
149
+ }
150
+
151
+ const fileExists = opts.fileExists || ((ref) => defaultFileExists(cwd, ref));
152
+ const relativeResolvable = opts.relativeResolvable
153
+ || ((mod) => defaultRelativeResolvable(cwd, mod, fileBasenames));
154
+
155
+ const issues = [];
156
+ const dedupe = new Set();
157
+ const add = (issue) => {
158
+ const key = `${issue.type}::${issue.value}`;
159
+ if (dedupe.has(key)) return;
160
+ dedupe.add(key);
161
+ issues.push(issue);
162
+ };
163
+
164
+ // 1. fake-file
165
+ for (const { path: p, line } of parsers.extractFilePaths(answerText)) {
166
+ if (!fileExists(p)) {
167
+ add({ type: 'fake-file', value: p, line, message: `File not found on disk: ${p}` });
168
+ }
169
+ }
170
+
171
+ // 2. fake-import
172
+ for (const imp of parsers.extractImports(answerText)) {
173
+ if (imp.relative) {
174
+ if (!relativeResolvable(imp.module)) {
175
+ add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Import does not resolve: ${imp.module}` });
176
+ }
177
+ continue;
178
+ }
179
+ // Bare module — only verifiable for JS when a package.json exists.
180
+ const top = imp.module.split('/')[0];
181
+ if (imp.kind === 'js') {
182
+ if (!hasPkg) continue;
183
+ if (NODE_BUILTINS.has(imp.module) || NODE_BUILTINS.has(top)) continue;
184
+ if (top.startsWith('@')) {
185
+ const scoped = imp.module.split('/').slice(0, 2).join('/');
186
+ if (deps.has(scoped) || deps.has(imp.module)) continue;
187
+ } else if (deps.has(top) || deps.has(imp.module)) {
188
+ continue;
189
+ }
190
+ add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Package not in dependencies: ${imp.module}` });
191
+ }
192
+ // Python bare imports: stdlib is unbounded offline — skip to keep precision.
193
+ }
194
+
195
+ // 3. fake-symbol
196
+ if (symbolSet.size > 0) {
197
+ for (const { name, line } of parsers.extractSymbols(answerText)) {
198
+ if (symbolSet.has(name)) continue;
199
+ if (LANG_GLOBALS.has(name) || NODE_BUILTINS.has(name) || PY_BUILTINS.has(name)) continue;
200
+ add({ type: 'fake-symbol', value: name, line, message: `Symbol not found in repo index: ${name}()` });
201
+ }
202
+ }
203
+
204
+ issues.sort((a, b) => a.line - b.line);
205
+
206
+ const byType = { 'fake-file': 0, 'fake-import': 0, 'fake-symbol': 0 };
207
+ for (const i of issues) byType[i.type] = (byType[i.type] || 0) + 1;
208
+
209
+ const summary = {
210
+ total: issues.length,
211
+ byType,
212
+ clean: issues.length === 0,
213
+ symbolsIndexed: symbolSet.size,
214
+ };
215
+
216
+ return { issues, summary };
217
+ }
218
+
219
+ module.exports = { verify, buildSymbolSet, loadDeps };
@@ -0,0 +1,149 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Parsers for the Hallucination Guard (verify-ai-output).
5
+ *
6
+ * Extract the verifiable claims an AI answer makes about a codebase:
7
+ * - file paths it references
8
+ * - import / require statements it shows
9
+ * - function / class symbols it calls
10
+ * - fenced code blocks (so callers can scope checks to code vs prose)
11
+ *
12
+ * Everything here is deterministic and offline — pure string analysis.
13
+ */
14
+
15
+ // Extensions we are confident name a source/code/config file (no slash required).
16
+ const KNOWN_CODE_EXT = new Set([
17
+ 'js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'py', 'pyw', 'rb', 'go', 'rs',
18
+ 'java', 'kt', 'swift', 'c', 'h', 'cpp', 'hpp', 'cs', 'php', 'r',
19
+ 'vue', 'svelte', 'css', 'scss', 'less', 'html', 'json', 'yml', 'yaml',
20
+ 'toml', 'xml', 'sql', 'graphql', 'gql', 'proto', 'tf', 'md', 'sh',
21
+ 'gd', 'gdscript',
22
+ ]);
23
+
24
+ /**
25
+ * Extract fenced code blocks.
26
+ * @param {string} text
27
+ * @returns {{ lang: string, content: string, line: number }[]}
28
+ */
29
+ function extractCodeBlocks(text) {
30
+ const blocks = [];
31
+ const lines = text.split('\n');
32
+ let inBlock = false;
33
+ let lang = '';
34
+ let buf = [];
35
+ let startLine = 0;
36
+ for (let i = 0; i < lines.length; i++) {
37
+ const m = lines[i].match(/^```(\w*)/);
38
+ if (m) {
39
+ if (!inBlock) {
40
+ inBlock = true;
41
+ lang = m[1] || '';
42
+ buf = [];
43
+ startLine = i + 2; // first content line (1-based)
44
+ } else {
45
+ blocks.push({ lang, content: buf.join('\n'), line: startLine });
46
+ inBlock = false;
47
+ }
48
+ continue;
49
+ }
50
+ if (inBlock) buf.push(lines[i]);
51
+ }
52
+ return blocks;
53
+ }
54
+
55
+ /**
56
+ * Extract file-path references (deduped, first-seen line kept).
57
+ * A token counts as a path when it has a `.<letter…>` extension AND
58
+ * either contains a `/` or carries a known code/config extension.
59
+ * @param {string} text
60
+ * @returns {{ path: string, line: number }[]}
61
+ */
62
+ function extractFilePaths(text) {
63
+ const lines = text.split('\n');
64
+ const seen = new Map();
65
+ const re = /(?:^|[\s`"'(\[<])([A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*)/g;
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const line = lines[i];
68
+ let m;
69
+ re.lastIndex = 0;
70
+ while ((m = re.exec(line)) !== null) {
71
+ const p = m[1];
72
+ if (/^https?:/i.test(p)) continue;
73
+ const ext = (p.split('.').pop() || '').toLowerCase();
74
+ const hasSlash = p.includes('/');
75
+ if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
76
+ if (!seen.has(p)) seen.set(p, i + 1);
77
+ }
78
+ }
79
+ return [...seen.entries()].map(([p, line]) => ({ path: p, line }));
80
+ }
81
+
82
+ /**
83
+ * Extract import / require statements.
84
+ * @param {string} text
85
+ * @returns {{ module: string, kind: 'js'|'py', relative: boolean, line: number, raw: string }[]}
86
+ */
87
+ function extractImports(text) {
88
+ const lines = text.split('\n');
89
+ const out = [];
90
+ const push = (module, kind, line, raw) => {
91
+ if (!module) return;
92
+ out.push({ module, kind, relative: /^[./]/.test(module), line, raw: raw.trim() });
93
+ };
94
+ for (let i = 0; i < lines.length; i++) {
95
+ const line = lines[i];
96
+ let m;
97
+ // JS/TS: import ... from 'x' | export ... from 'x'
98
+ if ((m = line.match(/\b(?:import|export)\b[^'"]*\bfrom\s*['"]([^'"]+)['"]/))) {
99
+ push(m[1], 'js', i + 1, line);
100
+ } else if ((m = line.match(/\bimport\s*['"]([^'"]+)['"]/))) {
101
+ // side-effect import 'x'
102
+ push(m[1], 'js', i + 1, line);
103
+ }
104
+ // require('x') / dynamic import('x') — may co-occur, scan separately
105
+ const reqRe = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
106
+ let r;
107
+ while ((r = reqRe.exec(line)) !== null) push(r[1], 'js', i + 1, line);
108
+
109
+ // Python: from x import y | import x
110
+ if ((m = line.match(/^\s*from\s+([.\w]+)\s+import\b/))) {
111
+ push(m[1], 'py', i + 1, line);
112
+ } else if ((m = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/))) {
113
+ push(m[1], 'py', i + 1, line);
114
+ }
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * Extract function/class symbol references that look like calls.
121
+ * Restricted to backtick-wrapped calls (`foo(...)`) for high precision.
122
+ * @param {string} text
123
+ * @returns {{ name: string, line: number }[]}
124
+ */
125
+ function extractSymbols(text) {
126
+ const lines = text.split('\n');
127
+ const out = [];
128
+ const seen = new Set();
129
+ const re = /`([A-Za-z_$][\w$]*)\s*\([^`]*\)`/g;
130
+ for (let i = 0; i < lines.length; i++) {
131
+ let m;
132
+ re.lastIndex = 0;
133
+ while ((m = re.exec(lines[i])) !== null) {
134
+ const name = m[1];
135
+ const key = name + '@' + (i + 1);
136
+ if (seen.has(key)) continue;
137
+ seen.add(key);
138
+ out.push({ name, line: i + 1 });
139
+ }
140
+ }
141
+ return out;
142
+ }
143
+
144
+ module.exports = {
145
+ extractCodeBlocks,
146
+ extractFilePaths,
147
+ extractImports,
148
+ extractSymbols,
149
+ };