sigmap 4.1.2 → 4.3.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,34 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [4.3.0] — 2026-04-16
14
+
15
+ ### Added
16
+
17
+ - **`sigmap validate`** — validates config (srcDirs exist, exclude patterns, maxTokens range), computes coverage as sig-index size / total source files, warns when coverage < 70%, exits 1 on hard errors. Optional `--query "<q>"` checks that PascalCase/camelCase symbols in the query appear in top-5 ranked context. Supports `--json`.
18
+ - **`sigmap --ci [--min-coverage N] [--json]`** — GitHub Actions exit gate: exits 0 when coverage ≥ threshold (default 80%), exits 1 otherwise. Uses sig-index vs source file count for a budget-aware coverage metric. Ready for `npx sigmap --ci` in CI workflows.
19
+ - **`extractQuerySymbols(query)`** — internal helper that extracts PascalCase and camelCase identifiers from a query string for symbol-level coverage checks in `sigmap validate`.
20
+
21
+ ### Changed
22
+
23
+ - **`sigmap ask`** — now emits a stderr warning when coverage < 70%, pointing users to `sigmap validate` for diagnosis.
24
+
25
+ ---
26
+
27
+ ## [4.2.0] — 2026-04-16
28
+
29
+ ### Added
30
+
31
+ - **`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.
32
+ - **Intent detection** (`detectIntent`) — classifies queries as `debug`, `explain`, `refactor`, `review`, or `search` and adjusts ranking weights accordingly for higher-relevance results.
33
+ - **`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.
34
+ - **`--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.
35
+ - **`sigmap suggest-profile [--short]`** — reads the last git commit message and staged files to recommend a context profile (`debug`, `architecture`, `review`, or `default`).
36
+ - **`sigmap compare [--json]`** — human-readable CLI wrapper over the retrieval benchmark scripts, showing SigMap vs baseline hit@5, token counts, and lift multiplier.
37
+ - **`sigmap share`** — prints a shareable one-liner with live benchmark numbers and copies it to the clipboard via `pbcopy`/`xclip`.
38
+
39
+ ---
40
+
13
41
  ## [4.1.2] — 2026-04-16 — Feat: --output <file> flag for custom context path
14
42
 
15
43
  ### Added
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.3.0',
4658
4658
  description: 'SigMap MCP server — code signatures on demand',
4659
4659
  };
4660
4660
 
@@ -5515,7 +5515,20 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5515
5515
  function formatRankJSON(results, query) {
5516
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 };
5517
5517
  }
5518
- 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 };
5519
5532
  };
5520
5533
 
5521
5534
  // ── ./src/eval/scorer ──
@@ -6249,7 +6262,7 @@ const path = require('path');
6249
6262
  const os = require('os');
6250
6263
  const { execSync } = require('child_process');
6251
6264
 
6252
- const VERSION = '4.1.2';
6265
+ const VERSION = '4.3.0';
6253
6266
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
6254
6267
 
6255
6268
  function requireSourceOrBundled(key) {
@@ -7939,6 +7952,62 @@ function registerMcp(cwd, scriptPath) {
7939
7952
  console.warn(JSON.stringify({ mcpServers: { 'sigmap': serverEntry } }, null, 2));
7940
7953
  }
7941
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
+
8007
+ function extractQuerySymbols(query) {
8008
+ return (query.match(/\b[A-Z][a-zA-Z]+|[a-z]+(?:[A-Z][a-z]+)+\b/g) || []);
8009
+ }
8010
+
7942
8011
  function main() {
7943
8012
  const args = process.argv.slice(2);
7944
8013
 
@@ -8018,6 +8087,232 @@ function main() {
8018
8087
  process.exit(1);
8019
8088
  }
8020
8089
 
8090
+ // v4.2: `sigmap ask "<query>"` — unified pipeline
8091
+ if (args[0] === 'ask') {
8092
+ const query = args[1];
8093
+ if (!query || query.startsWith('--')) {
8094
+ console.error('[sigmap] Usage: sigmap ask "<query>"');
8095
+ console.error(' Example: sigmap ask "fix the login bug"');
8096
+ process.exit(1);
8097
+ }
8098
+
8099
+ const { detectIntent, buildSigIndex, rank } = requireSourceOrBundled('./src/retrieval/ranker');
8100
+ const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
8101
+
8102
+ const intent = detectIntent(query);
8103
+ const intentWeights = getIntentWeights(intent);
8104
+
8105
+ const sigIndex = buildSigIndex(cwd);
8106
+ if (sigIndex.size === 0) {
8107
+ console.error('[sigmap] no context file found. Run: sigmap (to generate first)');
8108
+ process.exit(1);
8109
+ }
8110
+
8111
+ const ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights });
8112
+ const miniCtx = buildMiniContext(ranked, cwd);
8113
+ const outPath = path.join(cwd, '.context', 'query-context.md');
8114
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
8115
+ fs.writeFileSync(outPath, miniCtx, 'utf8');
8116
+ const ctxTok = estimateTokens(miniCtx);
8117
+
8118
+ const allFiles = buildFileList(cwd, config);
8119
+ const fakeEntries = allFiles.map((f) => ({ filePath: f }));
8120
+ let coveragePct = 0;
8121
+ try { coveragePct = coverageScore(cwd, fakeEntries, config).score; } catch (_) {}
8122
+
8123
+ const rawTok = getRawTokenCount(cwd, config);
8124
+ const savings = rawTok > 0 ? Math.round((1 - ctxTok / rawTok) * 100) : 0;
8125
+ const model = args[args.indexOf('--model') + 1] || 'gpt-4o';
8126
+ const rateK = MODEL_COSTS[model] || MODEL_COSTS['gpt-4o'];
8127
+ const costRaw = ((rawTok / 1000) * rateK).toFixed(4);
8128
+ const costCtx = ((ctxTok / 1000) * rateK).toFixed(4);
8129
+
8130
+ const riskLevel = computeCurrentRisk(cwd);
8131
+
8132
+ if (args.includes('--json')) {
8133
+ process.stdout.write(JSON.stringify({
8134
+ intent, coverage: coveragePct, contextTokens: ctxTok,
8135
+ costBefore: costRaw, costAfter: costCtx, savingsPct: savings,
8136
+ riskLevel, contextPath: path.relative(cwd, outPath),
8137
+ }) + '\n');
8138
+ } else {
8139
+ if (coveragePct < 70) {
8140
+ process.stderr.write(`[sigmap] ⚠ coverage ${coveragePct}% — consider running: sigmap validate\n`);
8141
+ }
8142
+ const bar = '─'.repeat(44);
8143
+ console.log([
8144
+ bar,
8145
+ ` sigmap ask "${query}"`,
8146
+ ` Intent : ${intent}`,
8147
+ ` Context : ${ctxTok.toLocaleString()} tokens → ${path.relative(cwd, outPath)}`,
8148
+ ` Coverage : ${coveragePct}%`,
8149
+ ` Risk : ${riskLevel}`,
8150
+ ` Cost : $${costCtx}/query (was $${costRaw} · saved ${savings}%)`,
8151
+ bar,
8152
+ ].join('\n'));
8153
+ }
8154
+ process.exit(0);
8155
+ }
8156
+
8157
+ // v4.2: `sigmap suggest-profile` — auto-detect task type from git state
8158
+ if (args[0] === 'suggest-profile') {
8159
+ const short = args.includes('--short');
8160
+ let msg = '', diff = '';
8161
+ try {
8162
+ const { execSync } = require('child_process');
8163
+ msg = execSync('git log -1 --format=%s', { cwd, timeout: 3000, encoding: 'utf8' }).trim();
8164
+ diff = execSync('git diff --cached --name-only', { cwd, timeout: 3000, encoding: 'utf8' });
8165
+ } catch (_) {}
8166
+
8167
+ let profile = 'default';
8168
+ let reason = 'no strong signal in git state';
8169
+ if (/fix|bug|error|crash|exception/i.test(msg)) { profile = 'debug'; reason = `commit: "${msg.slice(0, 60)}"`; }
8170
+ else if (/refactor|architect|redesign|module/i.test(msg)) { profile = 'architecture'; reason = `commit: "${msg.slice(0, 60)}"`; }
8171
+ else if (/review|pr|pull.request|check/i.test(msg)) { profile = 'review'; reason = `commit: "${msg.slice(0, 60)}"`; }
8172
+ else if (diff.includes('.spec.') || diff.includes('.test.')) { profile = 'debug'; reason = 'staged test files detected'; }
8173
+
8174
+ if (short) {
8175
+ console.log(profile);
8176
+ } else {
8177
+ console.log(`[sigmap] suggested profile: --profile ${profile}`);
8178
+ console.log(` Reason: ${reason}`);
8179
+ }
8180
+ process.exit(0);
8181
+ }
8182
+
8183
+ // v4.2: `sigmap compare` — human-readable benchmark CLI
8184
+ if (args[0] === 'compare') {
8185
+ const { execSync } = require('child_process');
8186
+ console.log('[sigmap] Running comparison benchmark (this may take ~30s)...\n');
8187
+
8188
+ let raw = '';
8189
+ try {
8190
+ raw = execSync(
8191
+ `node ${JSON.stringify(path.join(__dirname, 'scripts', 'run-retrieval-benchmark.mjs'))} --compare`,
8192
+ { cwd, timeout: 90_000, encoding: 'utf8' }
8193
+ );
8194
+ } catch (e) { raw = (e && e.stdout) ? e.stdout : ''; }
8195
+
8196
+ let results = null;
8197
+ try { results = JSON.parse(raw); } catch (_) {}
8198
+
8199
+ if (!results) {
8200
+ console.error('[sigmap] Could not parse benchmark output.');
8201
+ console.error(' Run manually: node scripts/run-retrieval-benchmark.mjs --skip-run --json');
8202
+ process.exit(1);
8203
+ }
8204
+
8205
+ if (args.includes('--json')) {
8206
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
8207
+ } else {
8208
+ const pct = (v) => `${(v * 100).toFixed(1)}%`;
8209
+ const lift = (a, b) => (b > 0 ? (a / b).toFixed(1) : '∞');
8210
+ const bar = '─'.repeat(44);
8211
+ console.log([
8212
+ bar,
8213
+ ' SigMap vs Baseline',
8214
+ bar,
8215
+ ` hit@5 ${pct(results.sigmap.hitAt5)} vs ${pct(results.baseline.hitAt5)} (${lift(results.sigmap.hitAt5, results.baseline.hitAt5)}× lift)`,
8216
+ ` Avg tokens ${results.sigmap.tokens.toLocaleString()} vs ${results.baseline.tokens.toLocaleString()}`,
8217
+ bar,
8218
+ ].join('\n'));
8219
+ }
8220
+ process.exit(0);
8221
+ }
8222
+
8223
+ // v4.2: `sigmap share` — shareable one-liner with live benchmark numbers
8224
+ if (args[0] === 'share') {
8225
+ const histPath = path.join(cwd, '.context', 'benchmark-history.ndjson');
8226
+ let reduction = 97, hitAt5 = 88;
8227
+
8228
+ if (fs.existsSync(histPath)) {
8229
+ try {
8230
+ const entries = fs.readFileSync(histPath, 'utf8').trim().split('\n')
8231
+ .map((l) => { try { return JSON.parse(l); } catch (_) { return null; } }).filter(Boolean);
8232
+ const tok = [...entries].reverse().find((e) => e.type === 'token-reduction');
8233
+ const ret = [...entries].reverse().find((e) => e.type === 'retrieval');
8234
+ if (tok && tok.reduction) reduction = tok.reduction;
8235
+ if (ret && ret.hitAt5) hitAt5 = Math.round(ret.hitAt5 * 100);
8236
+ } catch (_) {}
8237
+ }
8238
+
8239
+ const shareText = [
8240
+ 'Generated with SigMap — zero-dependency AI context engine',
8241
+ `${reduction}% fewer tokens · ${hitAt5}% retrieval accuracy · 6× better results`,
8242
+ 'https://sigmap.dev',
8243
+ ].join('\n');
8244
+
8245
+ console.log(shareText);
8246
+
8247
+ try {
8248
+ const { execSync } = require('child_process');
8249
+ const clipCmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip -selection clipboard';
8250
+ execSync(`printf '%s' ${JSON.stringify(shareText)} | ${clipCmd}`, { timeout: 2000 });
8251
+ console.log('\n[sigmap] Copied to clipboard.');
8252
+ } catch (_) {}
8253
+
8254
+ process.exit(0);
8255
+ }
8256
+
8257
+ // v4.3: `sigmap validate` — config + coverage + optional query symbol check
8258
+ if (args[0] === 'validate') {
8259
+ const issues = [];
8260
+ const warnings = [];
8261
+
8262
+ // Config checks
8263
+ for (const d of (config.srcDirs || [])) {
8264
+ if (!fs.existsSync(path.join(cwd, d)))
8265
+ issues.push(`srcDir '${d}' does not exist`);
8266
+ }
8267
+ if ((config.exclude || []).some((p) => p === 'src/**'))
8268
+ issues.push(`exclude pattern 'src/**' will exclude all source files`);
8269
+ if ((config.maxTokens || 0) < 1000)
8270
+ warnings.push(`maxTokens ${config.maxTokens} is very low — consider ≥ 4000`);
8271
+ if ((config.maxTokens || 0) > 50000)
8272
+ warnings.push(`maxTokens ${config.maxTokens} is very high — may exceed LLM context windows`);
8273
+
8274
+ // Coverage check: files actually in context vs total source files
8275
+ const { buildSigIndex: valBuildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
8276
+ const valSigIndex = valBuildSigIndex(cwd);
8277
+ const valTotal = buildFileList(cwd, config).length;
8278
+ const coveragePct = valTotal > 0 ? Math.round((valSigIndex.size / valTotal) * 100) : 0;
8279
+ if (coveragePct < 70)
8280
+ warnings.push(`coverage ${coveragePct}% is below recommended 70% — increase maxTokens or expand srcDirs`);
8281
+
8282
+ // Optional query symbol check
8283
+ const valQueryIdx = args.indexOf('--query');
8284
+ if (valQueryIdx !== -1) {
8285
+ const q = (args[valQueryIdx + 1] || '').trim();
8286
+ if (q && !q.startsWith('--')) {
8287
+ try {
8288
+ const { rank, buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
8289
+ const ranked = rank(q, buildSigIndex(cwd), { topK: 5 });
8290
+ const symbols = extractQuerySymbols(q);
8291
+ const missing = symbols.filter((sym) =>
8292
+ !ranked.some((r) => r.sigs && r.sigs.some((s) => s.toLowerCase().includes(sym.toLowerCase())))
8293
+ );
8294
+ if (missing.length > 0)
8295
+ warnings.push(`query "${q}" references symbols not in top-5 context: ${missing.join(', ')}`);
8296
+ else if (symbols.length > 0)
8297
+ console.log(`[sigmap] ✓ query coverage OK — all ${symbols.length} symbols found`);
8298
+ } catch (_) {}
8299
+ }
8300
+ }
8301
+
8302
+ if (args.includes('--json')) {
8303
+ process.stdout.write(JSON.stringify({ valid: issues.length === 0, issues, warnings, coverage: coveragePct }) + '\n');
8304
+ } else {
8305
+ for (const w of warnings) console.warn(`[sigmap] ⚠ ${w}`);
8306
+ if (issues.length === 0) {
8307
+ console.log(`[sigmap] ✓ config valid coverage: ${coveragePct}%`);
8308
+ } else {
8309
+ for (const iss of issues) console.error(`[sigmap] ✗ ${iss}`);
8310
+ process.exit(1);
8311
+ }
8312
+ }
8313
+ process.exit(0);
8314
+ }
8315
+
8021
8316
  // Feature 6: `sigmap sync` — write all outputs + llms.txt + print compact diff
8022
8317
  if (args[0] === 'sync') {
8023
8318
  try {
@@ -8469,7 +8764,13 @@ function main() {
8469
8764
  : ((config && config.retrieval && config.retrieval.topK) || 10);
8470
8765
  const recencyBoost = (config && config.retrieval && config.retrieval.recencyBoost) || 1.5;
8471
8766
  const results = rank(query, index, { topK, recencyBoost });
8472
- if (args.includes('--json')) {
8767
+ if (args.includes('--context')) {
8768
+ const miniCtx = buildMiniContext(results, cwd);
8769
+ const ctxOut = path.join(cwd, '.context', 'query-context.md');
8770
+ fs.mkdirSync(path.dirname(ctxOut), { recursive: true });
8771
+ fs.writeFileSync(ctxOut, miniCtx, 'utf8');
8772
+ console.log(`[sigmap] query context → ${path.relative(cwd, ctxOut)} (${estimateTokens(miniCtx)} tokens)`);
8773
+ } else if (args.includes('--json')) {
8473
8774
  process.stdout.write(JSON.stringify(formatRankJSON(results, query)) + '\n');
8474
8775
  } else {
8475
8776
  process.stdout.write(formatRankTable(results, query));
@@ -8639,6 +8940,68 @@ function main() {
8639
8940
  process.exit(0);
8640
8941
  }
8641
8942
 
8943
+ // v4.2: `--cost` — show token/$ cost estimate before and after SigMap
8944
+ if (args.includes('--cost')) {
8945
+ const rawTok = getRawTokenCount(cwd, config);
8946
+ runGenerate(cwd, config, false);
8947
+
8948
+ const model = args[args.indexOf('--model') + 1] || 'gpt-4o';
8949
+ const rateK = MODEL_COSTS[model] || MODEL_COSTS['gpt-4o'];
8950
+
8951
+ const ctxPath = config.customOutput
8952
+ ? path.resolve(cwd, config.customOutput)
8953
+ : path.join(cwd, '.github', 'copilot-instructions.md');
8954
+ let outTok = 0;
8955
+ try { outTok = estimateTokens(fs.readFileSync(ctxPath, 'utf8')); } catch (_) {}
8956
+
8957
+ const savings = rawTok > 0 ? Math.round((1 - outTok / rawTok) * 100) : 0;
8958
+ const costRaw = (rawTok / 1000) * rateK;
8959
+ const costCtx = (outTok / 1000) * rateK;
8960
+
8961
+ const out = {
8962
+ model,
8963
+ rawTokens: rawTok,
8964
+ contextTokens: outTok,
8965
+ costRaw: costRaw.toFixed(4),
8966
+ costContext: costCtx.toFixed(4),
8967
+ savingsPct: savings,
8968
+ };
8969
+
8970
+ if (args.includes('--json')) {
8971
+ process.stdout.write(JSON.stringify(out) + '\n');
8972
+ } else {
8973
+ console.log(`\n Cost estimate (${model}):`);
8974
+ console.log(` Without SigMap : ${rawTok.toLocaleString()} tok $${out.costRaw}/query`);
8975
+ console.log(` With SigMap : ${outTok.toLocaleString()} tok $${out.costContext}/query`);
8976
+ console.log(` Savings : ${savings}% ($${(costRaw - costCtx).toFixed(4)} saved per query)\n`);
8977
+ }
8978
+ process.exit(0);
8979
+ }
8980
+
8981
+ // v4.3: `--ci [--min-coverage N] [--json]` — GitHub Actions exit gate
8982
+ if (args.includes('--ci')) {
8983
+ const minCovIdx = args.indexOf('--min-coverage');
8984
+ const minCoverage = minCovIdx !== -1 ? Math.max(0, Math.min(100, parseInt(args[minCovIdx + 1], 10) || 80)) : 80;
8985
+
8986
+ // Coverage = files actually in context / total source files
8987
+ const { buildSigIndex: ciBuildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
8988
+ const ciSigIndex = ciBuildSigIndex(cwd);
8989
+ const ciTotal = buildFileList(cwd, config).length;
8990
+ const coveragePct = ciTotal > 0 ? Math.round((ciSigIndex.size / ciTotal) * 100) : 0;
8991
+
8992
+ const pass = coveragePct >= minCoverage;
8993
+
8994
+ if (args.includes('--json')) {
8995
+ process.stdout.write(JSON.stringify({ pass, coverage: coveragePct, threshold: minCoverage }) + '\n');
8996
+ } else if (pass) {
8997
+ console.log(`[sigmap] ✓ CI gate passed — coverage ${coveragePct}% ≥ ${minCoverage}%`);
8998
+ } else {
8999
+ console.error(`[sigmap] ✗ CI gate FAILED — coverage ${coveragePct}% < ${minCoverage}%`);
9000
+ console.error(` Fix: increase maxTokens or expand srcDirs in gen-context.config.json`);
9001
+ }
9002
+ process.exit(pass ? 0 : 1);
9003
+ }
9004
+
8642
9005
  // Default: generate once
8643
9006
  runGenerate(cwd, config, false);
8644
9007
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "4.1.2",
3
+ "version": "4.3.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.3.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.3.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.3.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -307,4 +307,22 @@ function formatRankJSON(results, query) {
307
307
  };
308
308
  }
309
309
 
310
- 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 };