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 +59 -53
- package/CHANGELOG.md +28 -0
- package/gen-context.js +367 -4
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/mcp/server.js +1 -1
- package/src/retrieval/ranker.js +19 -1
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.
|
|
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.
|
|
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/
|
|
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/
|
|
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
|
|
56
|
+
function write(context, cwd, opts = {})
|
|
69
57
|
```
|
|
70
58
|
|
|
71
|
-
### packages/adapters/
|
|
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/
|
|
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/
|
|
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 = {
|
|
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/
|
|
163
|
+
### src/retrieval/ranker.js
|
|
174
164
|
```
|
|
175
|
-
module.exports = {
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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('--
|
|
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
package/src/mcp/server.js
CHANGED
package/src/retrieval/ranker.js
CHANGED
|
@@ -307,4 +307,22 @@ function formatRankJSON(results, query) {
|
|
|
307
307
|
};
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
|
|
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 };
|