sigmap 7.22.1 → 7.23.0

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