sigmap 4.1.1 → 4.2.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/AGENTS.md CHANGED
@@ -12,17 +12,22 @@ Use this marker block for all appendable context files:
12
12
  ## Auto-generated signatures
13
13
  <!-- Updated by gen-context.js -->
14
14
  You are a coding assistant with full knowledge of this codebase.
15
- Below are the code signatures extracted by SigMap v4.1.0 on 2026-04-15T08:05:43.080Z.
15
+ Below are the code signatures extracted by SigMap v4.1.2 on 2026-04-16T17:45:07.132Z.
16
16
 
17
17
  Use these signatures to answer questions about the code accurately.
18
18
 
19
19
  ## Code Signatures
20
20
 
21
- <!-- Generated by SigMap gen-context.js v4.1.0 -->
21
+ <!-- Generated by SigMap gen-context.js v4.1.2 -->
22
22
  <!-- DO NOT EDIT below the marker line — run gen-context.js to regenerate -->
23
23
 
24
24
  # Code signatures
25
25
 
26
+ ## changes (last 5 commits — 53 minutes ago)
27
+ ```
28
+ src/retrieval/ranker.js +_parseContextFile +buildSigIndex ~buildSigIndex ~rank
29
+ ```
30
+
26
31
  ## packages
27
32
 
28
33
  ### packages/adapters/claude.js
@@ -34,41 +39,24 @@ function outputPath(cwd) → string
34
39
  function write(context, cwd, opts = {})
35
40
  ```
36
41
 
37
- ### packages/adapters/copilot.js
42
+ ### packages/adapters/codex.js
38
43
  ```
39
44
  module.exports = { name, format, outputPath, write }
40
45
  function format(context, opts = {}) → string
41
- function _confidenceMeta(opts)
42
46
  function outputPath(cwd) → string
43
47
  function write(context, cwd, opts = {})
44
48
  ```
45
49
 
46
- ### packages/adapters/cursor.js
47
- ```
48
- module.exports = { name, format, outputPath }
49
- function format(context, opts = {}) → string
50
- function _confidenceMeta(opts)
51
- function outputPath(cwd) → string
52
- ```
53
-
54
- ### packages/adapters/gemini.js
50
+ ### packages/adapters/copilot.js
55
51
  ```
56
52
  module.exports = { name, format, outputPath, write }
57
53
  function format(context, opts = {}) → string
58
- function outputPath(cwd) → string
59
- function write(context, cwd, opts = {})
60
54
  function _confidenceMeta(opts)
61
- ```
62
-
63
- ### packages/adapters/openai.js
64
- ```
65
- module.exports = { name, format, outputPath }
66
- function format(context, opts = {}) → string
67
55
  function outputPath(cwd) → string
68
- function _confidenceMeta(opts)
56
+ function write(context, cwd, opts = {})
69
57
  ```
70
58
 
71
- ### packages/adapters/windsurf.js
59
+ ### packages/adapters/cursor.js
72
60
  ```
73
61
  module.exports = { name, format, outputPath }
74
62
  function format(context, opts = {}) → string
@@ -76,12 +64,13 @@ function _confidenceMeta(opts)
76
64
  function outputPath(cwd) → string
77
65
  ```
78
66
 
79
- ### packages/adapters/codex.js
67
+ ### packages/adapters/gemini.js
80
68
  ```
81
69
  module.exports = { name, format, outputPath, write }
82
70
  function format(context, opts = {}) → string
83
71
  function outputPath(cwd) → string
84
72
  function write(context, cwd, opts = {})
73
+ function _confidenceMeta(opts)
85
74
  ```
86
75
 
87
76
  ### packages/adapters/index.js
@@ -101,6 +90,22 @@ function format(context, opts)
101
90
  function write(context, cwd, opts)
102
91
  ```
103
92
 
93
+ ### packages/adapters/openai.js
94
+ ```
95
+ module.exports = { name, format, outputPath }
96
+ function format(context, opts = {}) → string
97
+ function outputPath(cwd) → string
98
+ function _confidenceMeta(opts)
99
+ ```
100
+
101
+ ### packages/adapters/windsurf.js
102
+ ```
103
+ module.exports = { name, format, outputPath }
104
+ function format(context, opts = {}) → string
105
+ function _confidenceMeta(opts)
106
+ function outputPath(cwd) → string
107
+ ```
108
+
104
109
  ### packages/cli/index.js
105
110
  ```
106
111
  module.exports = { CLI_ENTRY, run }
@@ -141,24 +146,9 @@ function adapt(context, adapterName, opts = {}) → string
141
146
 
142
147
  ## src
143
148
 
144
- ### src/analysis/coverage-score.js
145
- ```
146
- module.exports = { coverageScore }
147
- function coverageScore(cwd, fileEntries, config) → { * score: number, * grad
148
- function _walk(dir, excludeSet, out)
149
- ```
150
-
151
- ### src/eval/analyzer.js
149
+ ### src/config/defaults.js
152
150
  ```
153
- module.exports = { analyzeFiles, formatAnalysisTable, formatAnalysisJSON }
154
- function isDockerfile(name)
155
- function getExtractorName(filePath)
156
- function tokenCount(sigs)
157
- function hasCoverage(filePath, cwd)
158
- function loadExtractor(name, cwd)
159
- function analyzeFiles(files, cwd, opts) → object[]
160
- function formatAnalysisTable(stats, showSlow) → string
161
- function formatAnalysisJSON(stats) → object
151
+ module.exports = { DEFAULTS }
162
152
  ```
163
153
 
164
154
  ### src/mcp/server.js
@@ -170,9 +160,22 @@ function dispatch(msg, cwd)
170
160
  function start(cwd)
171
161
  ```
172
162
 
173
- ### src/config/defaults.js
163
+ ### src/retrieval/ranker.js
174
164
  ```
175
- module.exports = { DEFAULTS }
165
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS }
166
+ function scoreFile(filePath, sigs, queryTokens, weights) → number
167
+ function rank(query, sigIndex, opts) → { file: string, score: nu
168
+ function _parseContextFile(contextPath) → Map<string, string[]>
169
+ function buildSigIndex(cwd, opts) → Map<string, string[]>
170
+ function formatRankTable(results, query) → string
171
+ function formatRankJSON(results, query) → object
172
+ ```
173
+
174
+ ### src/analysis/coverage-score.js
175
+ ```
176
+ module.exports = { coverageScore }
177
+ function coverageScore(cwd, fileEntries, config) → { * score: number, * grad
178
+ function _walk(dir, excludeSet, out)
176
179
  ```
177
180
 
178
181
  ### src/config/loader.js
@@ -183,6 +186,19 @@ function loadConfig(cwd) → object
183
186
  function deepClone(obj)
184
187
  ```
185
188
 
189
+ ### src/eval/analyzer.js
190
+ ```
191
+ module.exports = { analyzeFiles, formatAnalysisTable, formatAnalysisJSON }
192
+ function isDockerfile(name)
193
+ function getExtractorName(filePath)
194
+ function tokenCount(sigs)
195
+ function hasCoverage(filePath, cwd)
196
+ function loadExtractor(name, cwd)
197
+ function analyzeFiles(files, cwd, opts) → object[]
198
+ function formatAnalysisTable(stats, showSlow) → string
199
+ function formatAnalysisJSON(stats) → object
200
+ ```
201
+
186
202
  ### src/eval/runner.js
187
203
  ```
188
204
  module.exports = { run, rank, loadTasks, buildSigIndex, formatTable, formatMetrics, tokenize }
@@ -615,16 +631,6 @@ function getImpact(args, cwd)
615
631
  module.exports = { TOOLS }
616
632
  ```
617
633
 
618
- ### src/retrieval/ranker.js
619
- ```
620
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS }
621
- function scoreFile(filePath, sigs, queryTokens, weights) → number
622
- function rank(query, sigIndex, opts) → { file: string, score: nu
623
- function buildSigIndex(cwd) → Map<string, string[]>
624
- function formatRankTable(results, query) → string
625
- function formatRankJSON(results, query) → object
626
- ```
627
-
628
634
  ### src/retrieval/tokenizer.js
629
635
  ```
630
636
  module.exports = { tokenize, STOP_WORDS }
package/CHANGELOG.md CHANGED
@@ -10,6 +10,60 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [4.2.0] — 2026-04-16
14
+
15
+ ### Added
16
+
17
+ - **`sigmap ask "<query>"`** — unified pipeline: intent detection → ranked mini-context → coverage check → cost estimate → risk level in one command. Supports `--json` for machine-readable output.
18
+ - **Intent detection** (`detectIntent`) — classifies queries as `debug`, `explain`, `refactor`, `review`, or `search` and adjusts ranking weights accordingly for higher-relevance results.
19
+ - **`sigmap query --context`** — writes a targeted mini-context (top-5 ranked files, ≤ 2 000 tokens) to `.context/query-context.md` for direct pasting into an LLM prompt.
20
+ - **`--cost [--model <name>] [--json]`** — prints per-model token/dollar cost comparison (raw source vs SigMap output). Supports `gpt-4o`, `gpt-4`, `claude-3-5-sonnet`, `claude-opus-4`, `gemini-1.5-pro`, and more.
21
+ - **`sigmap suggest-profile [--short]`** — reads the last git commit message and staged files to recommend a context profile (`debug`, `architecture`, `review`, or `default`).
22
+ - **`sigmap compare [--json]`** — human-readable CLI wrapper over the retrieval benchmark scripts, showing SigMap vs baseline hit@5, token counts, and lift multiplier.
23
+ - **`sigmap share`** — prints a shareable one-liner with live benchmark numbers and copies it to the clipboard via `pbcopy`/`xclip`.
24
+
25
+ ---
26
+
27
+ ## [4.1.2] — 2026-04-16 — Feat: --output <file> flag for custom context path
28
+
29
+ ### Added
30
+
31
+ - **`--output <file>` flag** — write signatures to any custom path, not just
32
+ an adapter's fixed location:
33
+ ```bash
34
+ sigmap --output .context/ai-context.md # default generation
35
+ sigmap --adapter claude --output shared/sigs.md # adapter + custom path
36
+ ```
37
+ The custom file is written **in addition to** the adapter's default output so
38
+ existing tooling is unaffected.
39
+
40
+ - **Automatic discovery for `--query`** — the resolved path is persisted to
41
+ `gen-context.config.json` as `customOutput` so subsequent `--query` runs
42
+ find it automatically without needing to pass `--output` again:
43
+ ```bash
44
+ sigmap --output .context/ai-context.md # generates + persists path
45
+ sigmap --query "add a new extractor" # auto-finds .context/ai-context.md
46
+ ```
47
+
48
+ - **Priority order for `--query` context resolution** (most specific first):
49
+ 1. `--output <file>` flag — explicit path
50
+ 2. `--adapter <name>` flag — adapter's fixed output path
51
+ 3. `customOutput` in `gen-context.config.json` — persisted from last `--output` run
52
+ 4. Probe all known adapter output paths — existing fallback behaviour
53
+
54
+ - **Nested directories created automatically** — `--output a/b/c/file.md`
55
+ creates any missing parent directories.
56
+
57
+ ### Tests
58
+
59
+ - Added `test/integration/output-flag.test.js` (13 tests) covering: custom
60
+ file creation, parseable headers, config persistence, nested dirs, missing
61
+ arg error, `--adapter` + `--output` combo, explicit `--query` with `--output`,
62
+ auto-discovery via persisted config, missing-file error, `--output` overrides
63
+ `--adapter` during `--query`.
64
+
65
+ ---
66
+
13
67
  ## [4.1.1] — 2026-04-16 — Fix: --query works with any adapter output
14
68
 
15
69
  ### Fixed
package/README.md CHANGED
@@ -353,6 +353,24 @@ Configure multiple adapters at once in `gen-context.config.json`:
353
353
 
354
354
  Use SigMap as a Node.js library without spawning a subprocess. See the [full API reference](#-programmatic-api) below.
355
355
 
356
+ ### Custom output path
357
+
358
+ Write signatures to any file location — useful for shared docs folders, monorepos,
359
+ or tooling that expects context at a non-standard path:
360
+
361
+ ```bash
362
+ sigmap --output .context/ai-context.md # write to custom path
363
+ sigmap --adapter claude --output shared/sigs.md # adapter + custom path
364
+ ```
365
+
366
+ The path is persisted to `gen-context.config.json`, so `--query` finds it
367
+ automatically on subsequent runs — no need to pass `--output` again:
368
+
369
+ ```bash
370
+ sigmap --output .context/ai-context.md # generates and saves the path
371
+ sigmap --query "add an extractor" # auto-discovers .context/ai-context.md
372
+ ```
373
+
356
374
  ### Query-aware retrieval
357
375
 
358
376
  Find the most relevant files for any task without reading the whole codebase:
@@ -361,6 +379,7 @@ Find the most relevant files for any task without reading the whole codebase:
361
379
  sigmap --query "authentication middleware" # ranked file list
362
380
  sigmap --query "auth" --json # machine-readable output
363
381
  sigmap --query "auth" --top 5 # top 5 results only
382
+ sigmap --query "auth" --adapter claude # query against CLAUDE.md specifically
364
383
  ```
365
384
 
366
385
  ### Diagnostic and evaluation tools
@@ -616,9 +635,14 @@ sigmap --diff Generate context for git-changed f
616
635
  sigmap --diff --staged Staged files only (pre-commit check)
617
636
  sigmap --mcp Start MCP server on stdio
618
637
 
638
+ sigmap --output <file> Write signatures to a custom path (persists for --query)
639
+ sigmap --output <file> --adapter <name> Adapter output + custom copy
640
+
619
641
  sigmap --query "<text>" Rank files by relevance to a query
620
642
  sigmap --query "<text>" --json Ranked results as JSON
621
643
  sigmap --query "<text>" --top <n> Limit results to top N files (default 10)
644
+ sigmap --query "<text>" --adapter <name> Query against a specific adapter's output file
645
+ sigmap --query "<text>" --output <file> Query against a specific custom file
622
646
 
623
647
  sigmap --analyze Per-file breakdown (sigs / tokens / extractor / coverage)
624
648
  sigmap --analyze --json Analysis as JSON
package/gen-context.js CHANGED
@@ -4654,7 +4654,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
4654
4654
 
4655
4655
  const SERVER_INFO = {
4656
4656
  name: 'sigmap',
4657
- version: '4.1.0',
4657
+ version: '4.2.0',
4658
4658
  description: 'SigMap MCP server — code signatures on demand',
4659
4659
  };
4660
4660
 
@@ -5479,8 +5479,19 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5479
5479
  return index;
5480
5480
  }
5481
5481
  function buildSigIndex(cwd, opts) {
5482
- const path = require('path');
5482
+ const fs = require('fs'); const path = require('path');
5483
5483
  if (opts && opts.contextPath) return _parseContextFile(opts.contextPath);
5484
+ // Check gen-context.config.json for a persisted customOutput path.
5485
+ try {
5486
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
5487
+ if (fs.existsSync(cfgPath)) {
5488
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
5489
+ if (cfg.customOutput) {
5490
+ const idx = _parseContextFile(path.resolve(cwd, cfg.customOutput));
5491
+ if (idx.size > 0) return idx;
5492
+ }
5493
+ }
5494
+ } catch (_) {}
5484
5495
  for (const parts of ADAPTER_OUTPUT_PATHS) {
5485
5496
  const contextPath = path.join(cwd, ...parts);
5486
5497
  const index = _parseContextFile(contextPath);
@@ -5504,7 +5515,20 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5504
5515
  function formatRankJSON(results, query) {
5505
5516
  return { query, results: (results || []).map((r, i) => ({ rank: i + 1, file: r.file, score: r.score, sigs: r.sigs, tokens: r.tokens })), totalResults: (results || []).length };
5506
5517
  }
5507
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS };
5518
+ const INTENT_PATTERNS = {
5519
+ debug: /\b(bug|fix|error|crash|exception|broken|failing|issue|problem|regression)\b/i,
5520
+ explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me)\b/i,
5521
+ refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify)\b/i,
5522
+ review: /\b(review|check|audit|security|pr|pull request|assess)\b/i,
5523
+ };
5524
+ function detectIntent(query) {
5525
+ if (!query || typeof query !== 'string') return 'search';
5526
+ for (const [intent, re] of Object.entries(INTENT_PATTERNS)) {
5527
+ if (re.test(query)) return intent;
5528
+ }
5529
+ return 'search';
5530
+ }
5531
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent };
5508
5532
  };
5509
5533
 
5510
5534
  // ── ./src/eval/scorer ──
@@ -6238,7 +6262,7 @@ const path = require('path');
6238
6262
  const os = require('os');
6239
6263
  const { execSync } = require('child_process');
6240
6264
 
6241
- const VERSION = '4.1.0';
6265
+ const VERSION = '4.2.0';
6242
6266
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
6243
6267
 
6244
6268
  function requireSourceOrBundled(key) {
@@ -6926,6 +6950,19 @@ function writeOutputs(content, targets, cwd, config) {
6926
6950
  if (ADAPTER_TARGETS.has(target)) {
6927
6951
  try {
6928
6952
  const adapterMod = __require('./packages/adapters/' + target);
6953
+ // copilot: honour config.output custom path (redirects away from default .github/copilot-instructions.md)
6954
+ if (target === 'copilot') {
6955
+ const outPath = resolveAdapterPath('copilot', cwd, config);
6956
+ const defaultPath = path.join(cwd, '.github', 'copilot-instructions.md');
6957
+ if (outPath !== defaultPath) {
6958
+ // custom path: format and write directly (no append logic)
6959
+ const formatted = adapterMod.format(content, { version: VERSION });
6960
+ ensureDir(outPath);
6961
+ fs.writeFileSync(outPath, formatted, 'utf8');
6962
+ console.warn(`[sigmap] wrote ${path.relative(cwd, outPath)}`);
6963
+ continue;
6964
+ }
6965
+ }
6929
6966
  if (typeof adapterMod.write === 'function') {
6930
6967
  adapterMod.write(content, cwd, { version: VERSION });
6931
6968
  const outPath = adapterMod.outputPath(cwd);
@@ -7507,6 +7544,25 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7507
7544
  writeOutputs(content, config.outputs, cwd, config);
7508
7545
  }
7509
7546
  if (formatValue === 'cache') writeCacheOutput(content, cwd);
7547
+
7548
+ // --output <file>: write a copy of the formatted context to the custom path.
7549
+ if (config.customOutput) {
7550
+ try {
7551
+ const absCustom = path.resolve(cwd, config.customOutput);
7552
+ const SIGMAP_HEADER = [
7553
+ `<!-- Generated by SigMap v${VERSION} -->`,
7554
+ `<!-- Updated: ${new Date().toISOString()} -->`,
7555
+ `<!-- Regenerate: node gen-context.js --output ${config.customOutput} -->`,
7556
+ '',
7557
+ ].join('\n');
7558
+ fs.mkdirSync(path.dirname(absCustom), { recursive: true });
7559
+ fs.writeFileSync(absCustom, SIGMAP_HEADER + content, 'utf8');
7560
+ console.warn(`[sigmap] wrote ${path.relative(cwd, absCustom)} (custom output)`);
7561
+ } catch (err) {
7562
+ console.warn(`[sigmap] --output write failed: ${err.message}`);
7563
+ }
7564
+ }
7565
+
7510
7566
  result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount };
7511
7567
  }
7512
7568
  } else {
@@ -7896,6 +7952,58 @@ function registerMcp(cwd, scriptPath) {
7896
7952
  console.warn(JSON.stringify({ mcpServers: { 'sigmap': serverEntry } }, null, 2));
7897
7953
  }
7898
7954
 
7955
+ // ---------------------------------------------------------------------------
7956
+ // v4.2 helpers
7957
+ // ---------------------------------------------------------------------------
7958
+ const MODEL_COSTS = {
7959
+ 'gpt-4': 0.030,
7960
+ 'gpt-4o': 0.005,
7961
+ 'gpt-4o-mini': 0.000150,
7962
+ 'claude-3-5-sonnet': 0.003,
7963
+ 'claude-3-haiku': 0.00025,
7964
+ 'claude-opus-4': 0.015,
7965
+ 'gemini-1.5-pro': 0.00125,
7966
+ };
7967
+
7968
+ function buildMiniContext(ranked, cwd) {
7969
+ const lines = ['# SigMap Query Context', `Generated: ${new Date().toISOString()}`, ''];
7970
+ for (const { file, sigs } of ranked) {
7971
+ lines.push(`## ${file}`, '```', ...sigs.slice(0, 20), '```', '');
7972
+ }
7973
+ return lines.join('\n');
7974
+ }
7975
+
7976
+ function computeCurrentRisk(cwd) {
7977
+ try {
7978
+ const { execSync } = require('child_process');
7979
+ const out = execSync('git diff --name-only HEAD', { cwd, timeout: 3000, encoding: 'utf8' });
7980
+ const count = out.trim().split('\n').filter(Boolean).length;
7981
+ if (count === 0) return 'NONE';
7982
+ if (count <= 3) return 'LOW';
7983
+ if (count <= 10) return 'MEDIUM';
7984
+ return 'HIGH';
7985
+ } catch (_) { return 'UNKNOWN'; }
7986
+ }
7987
+
7988
+ function getRawTokenCount(cwd, config) {
7989
+ let total = 0;
7990
+ const files = buildFileList(cwd, config);
7991
+ for (const fp of files) {
7992
+ try { total += estimateTokens(fs.readFileSync(fp, 'utf8')); } catch (_) {}
7993
+ }
7994
+ return total;
7995
+ }
7996
+
7997
+ function getIntentWeights(intent) {
7998
+ const { DEFAULT_WEIGHTS } = requireSourceOrBundled('./src/retrieval/ranker');
7999
+ const base = Object.assign({}, DEFAULT_WEIGHTS);
8000
+ if (intent === 'debug') return Object.assign({}, base, { recencyBoost: base.recencyBoost * 1.5 });
8001
+ if (intent === 'explain') return Object.assign({}, base, { symbolMatch: base.symbolMatch * 1.5 });
8002
+ if (intent === 'refactor') return Object.assign({}, base, { pathMatch: base.pathMatch * 1.5 });
8003
+ if (intent === 'review') return Object.assign({}, base, { exactToken: base.exactToken * 1.3 });
8004
+ return base;
8005
+ }
8006
+
7899
8007
  function main() {
7900
8008
  const args = process.argv.slice(2);
7901
8009
 
@@ -7931,6 +8039,39 @@ function main() {
7931
8039
 
7932
8040
  const config = loadConfig(cwd);
7933
8041
 
8042
+ // ── --output <file> — parse early so every subsequent block can use it ─────
8043
+ // Resolves the custom output path and merges it into config.customOutput.
8044
+ // Also persists the resolved relative path to gen-context.config.json so
8045
+ // future --query calls (without --output) find the file automatically.
8046
+ (function resolveOutputFlag() {
8047
+ const outIdx = args.indexOf('--output');
8048
+ if (outIdx < 0) return;
8049
+ const raw = (args[outIdx + 1] || '').trim();
8050
+ if (!raw || raw.startsWith('--')) {
8051
+ console.error('[sigmap] --output requires a file path');
8052
+ console.error(' Example: node gen-context.js --output .context/ai-context.md');
8053
+ process.exit(1);
8054
+ }
8055
+ const abs = path.resolve(cwd, raw);
8056
+ const rel = path.relative(cwd, abs);
8057
+ config.customOutput = rel; // consumed by runGenerate
8058
+
8059
+ // Persist to gen-context.config.json for future --query calls
8060
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
8061
+ try {
8062
+ let savedCfg = {};
8063
+ if (fs.existsSync(cfgPath)) {
8064
+ try { savedCfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); } catch (_) {}
8065
+ }
8066
+ if (savedCfg.customOutput !== rel) {
8067
+ savedCfg.customOutput = rel;
8068
+ fs.writeFileSync(cfgPath, JSON.stringify(savedCfg, null, 2) + '\n', 'utf8');
8069
+ }
8070
+ } catch (err) {
8071
+ console.warn(`[sigmap] could not persist customOutput to config: ${err.message}`);
8072
+ }
8073
+ })();
8074
+
7934
8075
  // Feature 2: `--mode fast|full|both`
7935
8076
  const modeIdx = args.indexOf('--mode');
7936
8077
  const mode = modeIdx !== -1
@@ -7942,6 +8083,170 @@ function main() {
7942
8083
  process.exit(1);
7943
8084
  }
7944
8085
 
8086
+ // v4.2: `sigmap ask "<query>"` — unified pipeline
8087
+ if (args[0] === 'ask') {
8088
+ const query = args[1];
8089
+ if (!query || query.startsWith('--')) {
8090
+ console.error('[sigmap] Usage: sigmap ask "<query>"');
8091
+ console.error(' Example: sigmap ask "fix the login bug"');
8092
+ process.exit(1);
8093
+ }
8094
+
8095
+ const { detectIntent, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');
8096
+ const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
8097
+
8098
+ const intent = detectIntent(query);
8099
+ const intentWeights = getIntentWeights(intent);
8100
+
8101
+ const sigIndex = buildSigIndex(cwd);
8102
+ if (sigIndex.size === 0) {
8103
+ console.error('[sigmap] no context file found. Run: sigmap (to generate first)');
8104
+ process.exit(1);
8105
+ }
8106
+
8107
+ const ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights });
8108
+ const miniCtx = buildMiniContext(ranked, cwd);
8109
+ const outPath = path.join(cwd, '.context', 'query-context.md');
8110
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
8111
+ fs.writeFileSync(outPath, miniCtx, 'utf8');
8112
+ const ctxTok = estimateTokens(miniCtx);
8113
+
8114
+ const allFiles = buildFileList(cwd, config);
8115
+ const fakeEntries = allFiles.map((f) => ({ filePath: f }));
8116
+ let coveragePct = 0;
8117
+ try { coveragePct = coverageScore(cwd, fakeEntries, config).score; } catch (_) {}
8118
+
8119
+ const rawTok = getRawTokenCount(cwd, config);
8120
+ const savings = rawTok > 0 ? Math.round((1 - ctxTok / rawTok) * 100) : 0;
8121
+ const model = args[args.indexOf('--model') + 1] || 'gpt-4o';
8122
+ const rateK = MODEL_COSTS[model] || MODEL_COSTS['gpt-4o'];
8123
+ const costRaw = ((rawTok / 1000) * rateK).toFixed(4);
8124
+ const costCtx = ((ctxTok / 1000) * rateK).toFixed(4);
8125
+
8126
+ const riskLevel = computeCurrentRisk(cwd);
8127
+
8128
+ if (args.includes('--json')) {
8129
+ process.stdout.write(JSON.stringify({
8130
+ intent, coverage: coveragePct, contextTokens: ctxTok,
8131
+ costBefore: costRaw, costAfter: costCtx, savingsPct: savings,
8132
+ riskLevel, contextPath: path.relative(cwd, outPath),
8133
+ }) + '\n');
8134
+ } else {
8135
+ const bar = '─'.repeat(44);
8136
+ console.log([
8137
+ bar,
8138
+ ` sigmap ask "${query}"`,
8139
+ ` Intent : ${intent}`,
8140
+ ` Context : ${ctxTok.toLocaleString()} tokens → ${path.relative(cwd, outPath)}`,
8141
+ ` Coverage : ${coveragePct}%`,
8142
+ ` Risk : ${riskLevel}`,
8143
+ ` Cost : $${costCtx}/query (was $${costRaw} · saved ${savings}%)`,
8144
+ bar,
8145
+ ].join('\n'));
8146
+ }
8147
+ process.exit(0);
8148
+ }
8149
+
8150
+ // v4.2: `sigmap suggest-profile` — auto-detect task type from git state
8151
+ if (args[0] === 'suggest-profile') {
8152
+ const short = args.includes('--short');
8153
+ let msg = '', diff = '';
8154
+ try {
8155
+ const { execSync } = require('child_process');
8156
+ msg = execSync('git log -1 --format=%s', { cwd, timeout: 3000, encoding: 'utf8' }).trim();
8157
+ diff = execSync('git diff --cached --name-only', { cwd, timeout: 3000, encoding: 'utf8' });
8158
+ } catch (_) {}
8159
+
8160
+ let profile = 'default';
8161
+ let reason = 'no strong signal in git state';
8162
+ if (/fix|bug|error|crash|exception/i.test(msg)) { profile = 'debug'; reason = `commit: "${msg.slice(0, 60)}"`; }
8163
+ else if (/refactor|architect|redesign|module/i.test(msg)) { profile = 'architecture'; reason = `commit: "${msg.slice(0, 60)}"`; }
8164
+ else if (/review|pr|pull.request|check/i.test(msg)) { profile = 'review'; reason = `commit: "${msg.slice(0, 60)}"`; }
8165
+ else if (diff.includes('.spec.') || diff.includes('.test.')) { profile = 'debug'; reason = 'staged test files detected'; }
8166
+
8167
+ if (short) {
8168
+ console.log(profile);
8169
+ } else {
8170
+ console.log(`[sigmap] suggested profile: --profile ${profile}`);
8171
+ console.log(` Reason: ${reason}`);
8172
+ }
8173
+ process.exit(0);
8174
+ }
8175
+
8176
+ // v4.2: `sigmap compare` — human-readable benchmark CLI
8177
+ if (args[0] === 'compare') {
8178
+ const { execSync } = require('child_process');
8179
+ console.log('[sigmap] Running comparison benchmark (this may take ~30s)...\n');
8180
+
8181
+ let raw = '';
8182
+ try {
8183
+ raw = execSync(
8184
+ `node ${JSON.stringify(path.join(__dirname, 'scripts', 'run-retrieval-benchmark.mjs'))} --compare`,
8185
+ { cwd, timeout: 90_000, encoding: 'utf8' }
8186
+ );
8187
+ } catch (e) { raw = (e && e.stdout) ? e.stdout : ''; }
8188
+
8189
+ let results = null;
8190
+ try { results = JSON.parse(raw); } catch (_) {}
8191
+
8192
+ if (!results) {
8193
+ console.error('[sigmap] Could not parse benchmark output.');
8194
+ console.error(' Run manually: node scripts/run-retrieval-benchmark.mjs --skip-run --json');
8195
+ process.exit(1);
8196
+ }
8197
+
8198
+ if (args.includes('--json')) {
8199
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
8200
+ } else {
8201
+ const pct = (v) => `${(v * 100).toFixed(1)}%`;
8202
+ const lift = (a, b) => (b > 0 ? (a / b).toFixed(1) : '∞');
8203
+ const bar = '─'.repeat(44);
8204
+ console.log([
8205
+ bar,
8206
+ ' SigMap vs Baseline',
8207
+ bar,
8208
+ ` hit@5 ${pct(results.sigmap.hitAt5)} vs ${pct(results.baseline.hitAt5)} (${lift(results.sigmap.hitAt5, results.baseline.hitAt5)}× lift)`,
8209
+ ` Avg tokens ${results.sigmap.tokens.toLocaleString()} vs ${results.baseline.tokens.toLocaleString()}`,
8210
+ bar,
8211
+ ].join('\n'));
8212
+ }
8213
+ process.exit(0);
8214
+ }
8215
+
8216
+ // v4.2: `sigmap share` — shareable one-liner with live benchmark numbers
8217
+ if (args[0] === 'share') {
8218
+ const histPath = path.join(cwd, '.context', 'benchmark-history.ndjson');
8219
+ let reduction = 97, hitAt5 = 88;
8220
+
8221
+ if (fs.existsSync(histPath)) {
8222
+ try {
8223
+ const entries = fs.readFileSync(histPath, 'utf8').trim().split('\n')
8224
+ .map((l) => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
8225
+ const tok = [...entries].reverse().find((e) => e.type === 'token-reduction');
8226
+ const ret = [...entries].reverse().find((e) => e.type === 'retrieval');
8227
+ if (tok && tok.reduction) reduction = tok.reduction;
8228
+ if (ret && ret.hitAt5) hitAt5 = Math.round(ret.hitAt5 * 100);
8229
+ } catch (_) {}
8230
+ }
8231
+
8232
+ const shareText = [
8233
+ 'Generated with SigMap — zero-dependency AI context engine',
8234
+ `${reduction}% fewer tokens · ${hitAt5}% retrieval accuracy · 6× better results`,
8235
+ 'https://sigmap.dev',
8236
+ ].join('\n');
8237
+
8238
+ console.log(shareText);
8239
+
8240
+ try {
8241
+ const { execSync } = require('child_process');
8242
+ const clipCmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip -selection clipboard';
8243
+ execSync(`printf '%s' ${JSON.stringify(shareText)} | ${clipCmd}`, { timeout: 2000 });
8244
+ console.log('\n[sigmap] Copied to clipboard.');
8245
+ } catch (_) {}
8246
+
8247
+ process.exit(0);
8248
+ }
8249
+
7945
8250
  // Feature 6: `sigmap sync` — write all outputs + llms.txt + print compact diff
7946
8251
  if (args[0] === 'sync') {
7947
8252
  try {
@@ -8355,19 +8660,28 @@ function main() {
8355
8660
  }
8356
8661
  const { rank, buildSigIndex, formatRankTable, formatRankJSON } = requireSourceOrBundled('./src/retrieval/ranker');
8357
8662
 
8358
- // Resolve an explicit context file path when --adapter is present.
8359
- // This lets `--adapter claude --query "..."` read CLAUDE.md instead of
8360
- // falling through to the default copilot-instructions.md probe.
8663
+ // Resolve the context file path to query against.
8664
+ // Priority: --output flag > --adapter flag > buildSigIndex probe order
8665
+ // (customOutput from config is handled inside buildSigIndex itself)
8361
8666
  let queryOpts;
8362
- const adpIdx = args.indexOf('--adapter');
8363
- if (adpIdx >= 0) {
8364
- const adapterName = (args[adpIdx + 1] || '').trim().toLowerCase();
8365
- const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
8366
- if (VALID_ADAPTERS.includes(adapterName)) {
8367
- try {
8368
- const adapterMod = __require('./packages/adapters/' + adapterName);
8369
- queryOpts = { contextPath: adapterMod.outputPath(cwd) };
8370
- } catch (_) {}
8667
+
8668
+ // 1. --output <file> pins to an explicit path
8669
+ if (config.customOutput) {
8670
+ queryOpts = { contextPath: path.resolve(cwd, config.customOutput) };
8671
+ }
8672
+
8673
+ // 2. --adapter <name> pins to that adapter's output path (if --output not given)
8674
+ if (!queryOpts) {
8675
+ const adpIdx = args.indexOf('--adapter');
8676
+ if (adpIdx >= 0) {
8677
+ const adapterName = (args[adpIdx + 1] || '').trim().toLowerCase();
8678
+ const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
8679
+ if (VALID_ADAPTERS.includes(adapterName)) {
8680
+ try {
8681
+ const adapterMod = __require('./packages/adapters/' + adapterName);
8682
+ queryOpts = { contextPath: adapterMod.outputPath(cwd) };
8683
+ } catch (_) {}
8684
+ }
8371
8685
  }
8372
8686
  }
8373
8687
 
@@ -8384,7 +8698,13 @@ function main() {
8384
8698
  : ((config && config.retrieval && config.retrieval.topK) || 10);
8385
8699
  const recencyBoost = (config && config.retrieval && config.retrieval.recencyBoost) || 1.5;
8386
8700
  const results = rank(query, index, { topK, recencyBoost });
8387
- if (args.includes('--json')) {
8701
+ if (args.includes('--context')) {
8702
+ const miniCtx = buildMiniContext(results, cwd);
8703
+ const ctxOut = path.join(cwd, '.context', 'query-context.md');
8704
+ fs.mkdirSync(path.dirname(ctxOut), { recursive: true });
8705
+ fs.writeFileSync(ctxOut, miniCtx, 'utf8');
8706
+ console.log(`[sigmap] query context → ${path.relative(cwd, ctxOut)} (${estimateTokens(miniCtx)} tokens)`);
8707
+ } else if (args.includes('--json')) {
8388
8708
  process.stdout.write(JSON.stringify(formatRankJSON(results, query)) + '\n');
8389
8709
  } else {
8390
8710
  process.stdout.write(formatRankTable(results, query));
@@ -8554,6 +8874,44 @@ function main() {
8554
8874
  process.exit(0);
8555
8875
  }
8556
8876
 
8877
+ // v4.2: `--cost` — show token/$ cost estimate before and after SigMap
8878
+ if (args.includes('--cost')) {
8879
+ const rawTok = getRawTokenCount(cwd, config);
8880
+ runGenerate(cwd, config, false);
8881
+
8882
+ const model = args[args.indexOf('--model') + 1] || 'gpt-4o';
8883
+ const rateK = MODEL_COSTS[model] || MODEL_COSTS['gpt-4o'];
8884
+
8885
+ const ctxPath = config.customOutput
8886
+ ? path.resolve(cwd, config.customOutput)
8887
+ : path.join(cwd, '.github', 'copilot-instructions.md');
8888
+ let outTok = 0;
8889
+ try { outTok = estimateTokens(fs.readFileSync(ctxPath, 'utf8')); } catch (_) {}
8890
+
8891
+ const savings = rawTok > 0 ? Math.round((1 - outTok / rawTok) * 100) : 0;
8892
+ const costRaw = (rawTok / 1000) * rateK;
8893
+ const costCtx = (outTok / 1000) * rateK;
8894
+
8895
+ const out = {
8896
+ model,
8897
+ rawTokens: rawTok,
8898
+ contextTokens: outTok,
8899
+ costRaw: costRaw.toFixed(4),
8900
+ costContext: costCtx.toFixed(4),
8901
+ savingsPct: savings,
8902
+ };
8903
+
8904
+ if (args.includes('--json')) {
8905
+ process.stdout.write(JSON.stringify(out) + '\n');
8906
+ } else {
8907
+ console.log(`\n Cost estimate (${model}):`);
8908
+ console.log(` Without SigMap : ${rawTok.toLocaleString()} tok $${out.costRaw}/query`);
8909
+ console.log(` With SigMap : ${outTok.toLocaleString()} tok $${out.costContext}/query`);
8910
+ console.log(` Savings : ${savings}% ($${(costRaw - costCtx).toFixed(4)} saved per query)\n`);
8911
+ }
8912
+ process.exit(0);
8913
+ }
8914
+
8557
8915
  // Default: generate once
8558
8916
  runGenerate(cwd, config, false);
8559
8917
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "4.1.1",
3
+ "version": "4.2.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": "4.1.0",
3
+ "version": "4.2.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": "4.1.0",
3
+ "version": "4.2.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: '4.1.0',
21
+ version: '4.2.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -206,26 +206,39 @@ function _parseContextFile(contextPath) {
206
206
  * Returns Map<filePath, string[]> where filePath is the relative path
207
207
  * as it appears in the ### headers of the context file.
208
208
  *
209
- * When `opts.contextPath` is provided, that specific file is used.
210
- * This is the case when the caller already knows the path (e.g. via
211
- * --adapter <name> or --output <file>).
212
- *
213
- * Otherwise all known adapter output paths are probed in order and the
214
- * first file that produces a non-empty index is returned.
209
+ * Resolution priority:
210
+ * 1. `opts.contextPath` explicit path from --output or --adapter flag
211
+ * 2. `customOutput` key in gen-context.config.json — persisted from a
212
+ * previous `--output <file>` generation run
213
+ * 3. All known adapter output paths probed in order (first non-empty wins)
215
214
  *
216
215
  * @param {string} cwd
217
216
  * @param {{ contextPath?: string }} [opts]
218
217
  * @returns {Map<string, string[]>}
219
218
  */
220
219
  function buildSigIndex(cwd, opts) {
220
+ const fs = require('fs');
221
221
  const path = require('path');
222
222
 
223
- // Caller supplied an explicit path — use it directly.
223
+ // 1. Caller supplied an explicit path — use it directly.
224
224
  if (opts && opts.contextPath) {
225
225
  return _parseContextFile(opts.contextPath);
226
226
  }
227
227
 
228
- // Probe all known adapter output paths; return first non-empty index.
228
+ // 2. Check gen-context.config.json for a persisted customOutput path.
229
+ try {
230
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
231
+ if (fs.existsSync(cfgPath)) {
232
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
233
+ if (cfg.customOutput) {
234
+ const customPath = path.resolve(cwd, cfg.customOutput);
235
+ const index = _parseContextFile(customPath);
236
+ if (index.size > 0) return index;
237
+ }
238
+ }
239
+ } catch (_) {}
240
+
241
+ // 3. Probe all known adapter output paths; return first non-empty index.
229
242
  for (const parts of ADAPTER_OUTPUT_PATHS) {
230
243
  const contextPath = path.join(cwd, ...parts);
231
244
  const index = _parseContextFile(contextPath);
@@ -294,4 +307,22 @@ function formatRankJSON(results, query) {
294
307
  };
295
308
  }
296
309
 
297
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS };
310
+ // ---------------------------------------------------------------------------
311
+ // Intent detection
312
+ // ---------------------------------------------------------------------------
313
+ const INTENT_PATTERNS = {
314
+ debug: /\b(bug|fix|error|crash|exception|broken|failing|issue|problem|regression)\b/i,
315
+ explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me)\b/i,
316
+ refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify)\b/i,
317
+ review: /\b(review|check|audit|security|pr|pull request|assess)\b/i,
318
+ };
319
+
320
+ function detectIntent(query) {
321
+ if (!query || typeof query !== 'string') return 'search';
322
+ for (const [intent, re] of Object.entries(INTENT_PATTERNS)) {
323
+ if (re.test(query)) return intent;
324
+ }
325
+ return 'search';
326
+ }
327
+
328
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent };