sigmap 3.6.0 → 4.0.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,35 +12,40 @@ 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 v3.5.1 on 2026-04-14T23:05:11.451Z.
15
+ Below are the code signatures extracted by SigMap v4.0.0 on 2026-04-15T05:40:32.012Z.
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 v3.5.1 -->
21
+ <!-- Generated by SigMap gen-context.js v4.0.0 -->
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 — 6 hours ago)
27
+ ```
28
+ src/extractors/generic.js +extract
29
+ src/format/llm-txt.js +outputPath +format
30
+ src/format/llms-txt.js +outputPath +getShortCommit +detectVersion +format
31
+ packages/adapters/llm-full.js +outputPath +format +write
32
+ ```
33
+
26
34
  ## packages
27
35
 
28
- ### packages/core/index.js
36
+ ### packages/adapters/llm-full.js
29
37
  ```
30
- module.exports = { extract, rank, buildSigIndex, scan, score, adapt }
31
- function _resolveExtractor(language)
32
- function extract(src, language) → string[]
33
- function rank(query, sigIndex, opts) → { file: string, score: nu
34
- function buildSigIndex(cwd) → Map<string, string[]>
35
- function scan(sigs, filePath) → { safe: string[], redacte
36
- function score(cwd) → { * score: number, * grad
37
- function adapt(context, adapterName, opts = {}) → string
38
+ module.exports = { name: 'llm-full', format, outputPath, write }
39
+ function outputPath(cwd)
40
+ function format(context, opts)
41
+ function write(context, cwd, opts)
38
42
  ```
39
43
 
40
44
  ### packages/adapters/claude.js
41
45
  ```
42
46
  module.exports = { name, format, outputPath, write }
43
47
  function format(context, opts = {}) → string
48
+ function _confidenceMeta(opts)
44
49
  function outputPath(cwd) → string
45
50
  function write(context, cwd, opts = {})
46
51
  ```
@@ -57,6 +62,7 @@ function write(context, cwd, opts = {})
57
62
  ```
58
63
  module.exports = { name, format, outputPath, write }
59
64
  function format(context, opts = {}) → string
65
+ function _confidenceMeta(opts)
60
66
  function outputPath(cwd) → string
61
67
  function write(context, cwd, opts = {})
62
68
  ```
@@ -65,6 +71,7 @@ function write(context, cwd, opts = {})
65
71
  ```
66
72
  module.exports = { name, format, outputPath }
67
73
  function format(context, opts = {}) → string
74
+ function _confidenceMeta(opts)
68
75
  function outputPath(cwd) → string
69
76
  ```
70
77
 
@@ -74,6 +81,7 @@ module.exports = { name, format, outputPath, write }
74
81
  function format(context, opts = {}) → string
75
82
  function outputPath(cwd) → string
76
83
  function write(context, cwd, opts = {})
84
+ function _confidenceMeta(opts)
77
85
  ```
78
86
 
79
87
  ### packages/adapters/index.js
@@ -85,25 +93,19 @@ function adapt(context, adapterName, opts = {}) → string
85
93
  function outputsToAdapters(outputs) → string[]
86
94
  ```
87
95
 
88
- ### packages/adapters/llm-full.js
89
- ```
90
- module.exports = { name: 'llm-full', format, outputPath, write }
91
- function outputPath(cwd)
92
- function format(context, opts)
93
- function write(context, cwd, opts)
94
- ```
95
-
96
96
  ### packages/adapters/openai.js
97
97
  ```
98
98
  module.exports = { name, format, outputPath }
99
99
  function format(context, opts = {}) → string
100
100
  function outputPath(cwd) → string
101
+ function _confidenceMeta(opts)
101
102
  ```
102
103
 
103
104
  ### packages/adapters/windsurf.js
104
105
  ```
105
106
  module.exports = { name, format, outputPath }
106
107
  function format(context, opts = {}) → string
108
+ function _confidenceMeta(opts)
107
109
  function outputPath(cwd) → string
108
110
  ```
109
111
 
@@ -133,8 +135,49 @@ code-fence js
133
135
  code-fence ---
134
136
  ```
135
137
 
138
+ ### packages/core/index.js
139
+ ```
140
+ module.exports = { extract, rank, buildSigIndex, scan, score, adapt }
141
+ function _resolveExtractor(language)
142
+ function extract(src, language) → string[]
143
+ function rank(query, sigIndex, opts) → { file: string, score: nu
144
+ function buildSigIndex(cwd) → Map<string, string[]>
145
+ function scan(sigs, filePath) → { safe: string[], redacte
146
+ function score(cwd) → { * score: number, * grad
147
+ function adapt(context, adapterName, opts = {}) → string
148
+ ```
149
+
136
150
  ## src
137
151
 
152
+ ### src/extractors/generic.js
153
+ ```
154
+ module.exports = { extract }
155
+ function extract(src)
156
+ ```
157
+
158
+ ### src/format/llm-txt.js
159
+ ```
160
+ module.exports = { format, outputPath }
161
+ function outputPath(cwd)
162
+ function format(context, cwd, version)
163
+ ```
164
+
165
+ ### src/format/llms-txt.js
166
+ ```
167
+ module.exports = { format, outputPath }
168
+ function outputPath(cwd)
169
+ function getShortCommit(cwd)
170
+ function detectVersion(cwd)
171
+ function format(context, cwd, writtenFiles, sigmapVersion)
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)
179
+ ```
180
+
138
181
  ### src/config/defaults.js
139
182
  ```
140
183
  module.exports = { DEFAULTS }
@@ -161,63 +204,6 @@ function formatAnalysisTable(stats, showSlow) → string
161
204
  function formatAnalysisJSON(stats) → object
162
205
  ```
163
206
 
164
- ### src/extractors/markdown.js
165
- ```
166
- module.exports = { extract }
167
- function extract(src) → string[]
168
- ```
169
-
170
- ### src/extractors/patterns.js
171
- ```
172
- module.exports = { extract }
173
- function extract(src) → string[]
174
- ```
175
-
176
- ### src/extractors/properties.js
177
- ```
178
- module.exports = { extract }
179
- function extract(src) → string[]
180
- ```
181
-
182
- ### src/extractors/python_dataclass.js
183
- ```
184
- module.exports = { extract }
185
- function extract(src) → string[]
186
- ```
187
-
188
- ### src/extractors/toml.js
189
- ```
190
- module.exports = { extract }
191
- function extract(src) → string[]
192
- ```
193
-
194
- ### src/extractors/typescript_react.js
195
- ```
196
- module.exports = { extract }
197
- function extract(src) → string[]
198
- ```
199
-
200
- ### src/extractors/vue_sfc.js
201
- ```
202
- module.exports = { extract }
203
- function extract(src) → string[]
204
- ```
205
-
206
- ### src/extractors/xml.js
207
- ```
208
- module.exports = { extract }
209
- function extract(src) → string[]
210
- ```
211
-
212
- ### src/mcp/server.js
213
- ```
214
- module.exports = { start }
215
- function respond(id, result)
216
- function respondError(id, code, message)
217
- function dispatch(msg, cwd)
218
- function start(cwd)
219
- ```
220
-
221
207
  ### src/eval/runner.js
222
208
  ```
223
209
  module.exports = { run, rank, loadTasks, buildSigIndex, formatTable, formatMetrics, tokenize }
@@ -301,12 +287,6 @@ module.exports = { extract }
301
287
  function extract(src) → string[]
302
288
  ```
303
289
 
304
- ### src/extractors/generic.js
305
- ```
306
- module.exports = { extract }
307
- function extract(src)
308
- ```
309
-
310
290
  ### src/extractors/go.js
311
291
  ```
312
292
  module.exports = { extract }
@@ -359,6 +339,18 @@ function extractMembers(block)
359
339
  function normalizeParams(params)
360
340
  ```
361
341
 
342
+ ### src/extractors/markdown.js
343
+ ```
344
+ module.exports = { extract }
345
+ function extract(src) → string[]
346
+ ```
347
+
348
+ ### src/extractors/patterns.js
349
+ ```
350
+ module.exports = { extract }
351
+ function extract(src) → string[]
352
+ ```
353
+
362
354
  ### src/extractors/php.js
363
355
  ```
364
356
  module.exports = { extract }
@@ -376,6 +368,12 @@ function diffSignatures(baseSigs, currentSigs) → {added:string[], removed:
376
368
  function extractName(sig)
377
369
  ```
378
370
 
371
+ ### src/extractors/properties.js
372
+ ```
373
+ module.exports = { extract }
374
+ function extract(src) → string[]
375
+ ```
376
+
379
377
  ### src/extractors/protobuf.js
380
378
  ```
381
379
  module.exports = { extract }
@@ -395,6 +393,12 @@ function normalizeParams(params)
395
393
  function extractDocHint(src, fnName, fnSigLine)
396
394
  ```
397
395
 
396
+ ### src/extractors/python_dataclass.js
397
+ ```
398
+ module.exports = { extract }
399
+ function extract(src) → string[]
400
+ ```
401
+
398
402
  ### src/extractors/ruby.js
399
403
  ```
400
404
  module.exports = { extract }
@@ -467,6 +471,12 @@ module.exports = { extractTodos }
467
471
  function extractTodos(src) → {line:number, tag:string,
468
472
  ```
469
473
 
474
+ ### src/extractors/toml.js
475
+ ```
476
+ module.exports = { extract }
477
+ function extract(src) → string[]
478
+ ```
479
+
470
480
  ### src/extractors/typescript.js
471
481
  ```
472
482
  module.exports = { extract }
@@ -477,6 +487,12 @@ function extractClassMembers(block)
477
487
  function normalizeParams(params)
478
488
  ```
479
489
 
490
+ ### src/extractors/typescript_react.js
491
+ ```
492
+ module.exports = { extract }
493
+ function extract(src) → string[]
494
+ ```
495
+
480
496
  ### src/extractors/vue.js
481
497
  ```
482
498
  module.exports = { extract }
@@ -485,6 +501,18 @@ function normalizeParams(params)
485
501
  function normalizeType(type)
486
502
  ```
487
503
 
504
+ ### src/extractors/vue_sfc.js
505
+ ```
506
+ module.exports = { extract }
507
+ function extract(src) → string[]
508
+ ```
509
+
510
+ ### src/extractors/xml.js
511
+ ```
512
+ module.exports = { extract }
513
+ function extract(src) → string[]
514
+ ```
515
+
488
516
  ### src/extractors/yaml.js
489
517
  ```
490
518
  module.exports = { extract }
@@ -518,22 +546,6 @@ function generateDashboardHtml(cwd, health)
518
546
  function renderHistoryCharts(cwd, health)
519
547
  ```
520
548
 
521
- ### src/format/llm-txt.js
522
- ```
523
- module.exports = { format, outputPath }
524
- function outputPath(cwd)
525
- function format(context, cwd, version)
526
- ```
527
-
528
- ### src/format/llms-txt.js
529
- ```
530
- module.exports = { format, outputPath }
531
- function outputPath(cwd)
532
- function getShortCommit(cwd)
533
- function detectVersion(cwd)
534
- function format(context, cwd, writtenFiles, sigmapVersion)
535
- ```
536
-
537
549
  ### src/graph/builder.js
538
550
  ```
539
551
  module.exports = { build, buildFromCwd, extractFileDeps }
@@ -597,6 +609,15 @@ function queryContext(args, cwd)
597
609
  function getImpact(args, cwd)
598
610
  ```
599
611
 
612
+ ### src/mcp/server.js
613
+ ```
614
+ module.exports = { start }
615
+ function respond(id, result)
616
+ function respondError(id, code, message)
617
+ function dispatch(msg, cwd)
618
+ function start(cwd)
619
+ ```
620
+
600
621
  ### src/mcp/tools.js
601
622
  ```
602
623
  module.exports = { TOOLS }
package/gen-context.js CHANGED
@@ -4478,7 +4478,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
4478
4478
 
4479
4479
  const SERVER_INFO = {
4480
4480
  name: 'sigmap',
4481
- version: '3.5.0',
4481
+ version: '4.0.0',
4482
4482
  description: 'SigMap MCP server — code signatures on demand',
4483
4483
  };
4484
4484
 
@@ -6040,7 +6040,7 @@ const path = require('path');
6040
6040
  const os = require('os');
6041
6041
  const { execSync } = require('child_process');
6042
6042
 
6043
- const VERSION = '3.6.0';
6043
+ const VERSION = '4.0.0';
6044
6044
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
6045
6045
 
6046
6046
  function requireSourceOrBundled(key) {
@@ -6276,13 +6276,17 @@ function applyTokenBudget(fileEntries, maxTokens) {
6276
6276
  else if (isTestFile(e.filePath)) { priority = 8; dropReason = 'budget: test file'; }
6277
6277
  else if (isConfigFile(e.filePath)) { priority = 6; dropReason = 'budget: config file'; }
6278
6278
  else priority = 4;
6279
- return { ...e, priority, dropReason };
6279
+ // v4.0: signal quality = sigs per line-of-code (higher = more informative)
6280
+ const loc = e.content ? e.content.split('\n').length : 1;
6281
+ const signalQuality = loc > 0 ? (e.sigs ? e.sigs.length : 0) / loc : 0;
6282
+ return { ...e, priority, dropReason, signalQuality };
6280
6283
  });
6281
6284
 
6282
- // Within same priority, sort by mtime ascending (oldest first = drop first)
6285
+ // Within same priority: sort by mtime ascending (oldest first), then signalQuality ascending (least informative first)
6283
6286
  withPriority.sort((a, b) => {
6284
6287
  if (b.priority !== a.priority) return b.priority - a.priority;
6285
- return (a.mtime || 0) - (b.mtime || 0);
6288
+ if ((a.mtime || 0) !== (b.mtime || 0)) return (a.mtime || 0) - (b.mtime || 0);
6289
+ return (a.signalQuality || 0) - (b.signalQuality || 0);
6286
6290
  });
6287
6291
 
6288
6292
  const kept = [];
@@ -6716,11 +6720,17 @@ function writeClaude(content, cwd) {
6716
6720
  // ---------------------------------------------------------------------------
6717
6721
  // Report
6718
6722
  // ---------------------------------------------------------------------------
6719
- function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson, budgetLimit) {
6723
+ function _coverageBar(pct, width) {
6724
+ width = width || 16;
6725
+ const filled = Math.round(pct / 100 * width);
6726
+ return '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled);
6727
+ }
6728
+
6729
+ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson, budgetLimit, coverageResult) {
6720
6730
  const reduction = inputTokens > 0 ? (100 - (finalTokens / inputTokens) * 100).toFixed(1) : 0;
6721
6731
  const overBudget = finalTokens > (budgetLimit || 6000);
6722
6732
  if (asJson) {
6723
- process.stdout.write(JSON.stringify({
6733
+ const payload = {
6724
6734
  version: VERSION,
6725
6735
  timestamp: new Date().toISOString(),
6726
6736
  rawTokens: inputTokens,
@@ -6731,7 +6741,22 @@ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson,
6731
6741
  reductionPct: parseFloat(reduction),
6732
6742
  overBudget,
6733
6743
  budgetLimit: budgetLimit || 6000,
6734
- }) + '\n');
6744
+ };
6745
+ if (coverageResult) {
6746
+ payload.coverage = {
6747
+ score: coverageResult.score,
6748
+ grade: coverageResult.grade,
6749
+ confidence: coverageResult.confidence,
6750
+ totalFiles: coverageResult.total,
6751
+ includedFiles: coverageResult.included,
6752
+ droppedFiles: coverageResult.dropped,
6753
+ perModule: Object.fromEntries(
6754
+ Array.from(coverageResult.perModule.entries())
6755
+ .map(([k, v]) => [k, v])
6756
+ ),
6757
+ };
6758
+ }
6759
+ process.stdout.write(JSON.stringify(payload) + '\n');
6735
6760
  // Exit 1 in CI if over budget — lets pipelines fail fast
6736
6761
  if (overBudget) process.exitCode = 1;
6737
6762
  } else {
@@ -6743,6 +6768,20 @@ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson,
6743
6768
  console.log(` output tokens : ~${finalTokens}`);
6744
6769
  console.log(` budget limit : ${budgetLimit || 6000}`);
6745
6770
  console.log(` reduction : ${reduction}%`);
6771
+ if (coverageResult) {
6772
+ console.log(` coverage : ${coverageResult.grade} (${coverageResult.score}%) — ${coverageResult.included} of ${coverageResult.total} source files included`);
6773
+ console.log(` confidence : ${coverageResult.confidence}`);
6774
+ if (coverageResult.perModule && coverageResult.perModule.size > 0) {
6775
+ console.log('');
6776
+ console.log(' Module Coverage:');
6777
+ for (const [dir, mod] of coverageResult.perModule) {
6778
+ if (mod.total === 0) continue;
6779
+ const bar = _coverageBar(mod.pct);
6780
+ const attention = mod.pct < 50 ? ' \u2190 attention needed' : '';
6781
+ console.log(` ${dir.padEnd(18)} ${bar} ${String(mod.pct).padStart(3)}% (${mod.included}/${mod.total} files)${attention}`);
6782
+ }
6783
+ }
6784
+ }
6746
6785
  if (overBudget) console.warn(`[sigmap] WARNING: output (${finalTokens} tokens) exceeds budget (${budgetLimit || 6000})`);
6747
6786
  }
6748
6787
  }
@@ -7032,6 +7071,41 @@ function runDiff(cwd, config, stagedOnly, baseRef) {
7032
7071
  const scope = baseRef ? `diff-vs-${baseRef}` : (stagedOnly ? 'staged' : 'diff');
7033
7072
  console.warn(`[sigmap] ${scope} files: ${fileEntries.length}, diff tokens: ~${finalTokens}`);
7034
7073
 
7074
+ // v4.0: risk score per changed file
7075
+ try {
7076
+ const { buildFromCwd } = requireSourceOrBundled('./src/graph/builder');
7077
+ const graph = buildFromCwd(cwd, { silent: true });
7078
+ const reverseGraph = graph.reverse || new Map();
7079
+
7080
+ function _isRouteFile(f) {
7081
+ return /\/(routes?|pages?|controllers?|handlers?|api)\//i.test(f)
7082
+ || /\.(route|page|controller|handler)\.\w+$/.test(f);
7083
+ }
7084
+ function _riskScore(filePath, sigs, revGraph) {
7085
+ let s = 0;
7086
+ if (sigs.some(sig => sig.includes('export') || sig.includes('module.exports'))) s += 2;
7087
+ const deps = revGraph.get(filePath) || new Set();
7088
+ if (deps.size > 3) s += 2;
7089
+ if (_isRouteFile(filePath)) s += 1;
7090
+ if (/\.(config|env|settings)\.\w+$/.test(filePath)) s += 1;
7091
+ return s >= 4 ? 'HIGH' : s >= 2 ? 'MEDIUM' : 'LOW';
7092
+ }
7093
+
7094
+ console.warn(`[sigmap] Risk: Changed files (${fileEntries.length}):`);
7095
+ for (const fe of fileEntries) {
7096
+ const level = _riskScore(fe.filePath, fe.sigs, reverseGraph);
7097
+ const rel = path.relative(cwd, fe.filePath).replace(/\\/g, '/');
7098
+ const deps = reverseGraph.get(fe.filePath) || new Set();
7099
+ const reasons = [];
7100
+ if (fe.sigs.some(s => s.includes('export') || s.includes('module.exports'))) reasons.push('exports public API');
7101
+ if (deps.size > 0) reasons.push(`${deps.size} downstream dependent${deps.size === 1 ? '' : 's'}`);
7102
+ if (_isRouteFile(fe.filePath)) reasons.push('route file');
7103
+ if (/\.(config|env|settings)\.\w+$/.test(fe.filePath)) reasons.push('config file');
7104
+ const label = level === 'HIGH' ? '[HIGH] ' : level === 'MEDIUM' ? '[MEDIUM]' : '[LOW] ';
7105
+ console.warn(` ${rel.padEnd(40)} ${label}${reasons.length ? ' — ' + reasons.join(', ') : ''}`);
7106
+ }
7107
+ } catch (_) {}
7108
+
7035
7109
  if (process.argv.includes('--report')) {
7036
7110
  // Also show what the full run would cost for comparison
7037
7111
  const fullResult = runGenerate(cwd, config, true);
@@ -7176,11 +7250,17 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7176
7250
  const droppedCount = beforeCount - budgeted.length;
7177
7251
  const content = formatOutput(budgeted, cwd, false, config, null);
7178
7252
  const finalTokens = estimateTokens(content);
7179
- result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount };
7253
+ // v4.0: compute coverage score for --report heatmap
7254
+ let coverageResult = null;
7255
+ try {
7256
+ const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
7257
+ coverageResult = coverageScore(cwd, budgeted, config);
7258
+ } catch (_) {}
7259
+ result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount, coverageResult };
7180
7260
  }
7181
7261
 
7182
7262
  if (reportMode || process.argv.includes('--report')) {
7183
- printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, config.maxTokens);
7263
+ printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, config.maxTokens, result.coverageResult);
7184
7264
  }
7185
7265
 
7186
7266
  // Usage tracking (v0.9) — optional append-only NDJSON log
@@ -7212,17 +7292,26 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7212
7292
  const pct = result.inputTokenTotal > 0
7213
7293
  ? Math.round((1 - result.finalTokens / result.inputTokenTotal) * 100)
7214
7294
  : 0;
7215
- process.stderr.write([
7295
+ // v4.0: coverage score in post-run summary
7296
+ let coverageLine = '';
7297
+ try {
7298
+ const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
7299
+ const cov = coverageScore(cwd, fileEntries, config);
7300
+ coverageLine = ` Coverage : ${cov.grade} (${cov.score}%) \u2014 ${cov.included} of ${cov.total} source files included`;
7301
+ } catch (_) {}
7302
+ const lines = [
7216
7303
  bar,
7217
7304
  ` SigMap v${VERSION}`,
7218
7305
  ` Files scanned : ${result.fileCount}`,
7219
7306
  ` Symbols found : ${syms.toLocaleString()}`,
7220
7307
  ` Token reduction: ${pct}% (${result.inputTokenTotal.toLocaleString()} \u2192 ${result.finalTokens.toLocaleString()})`,
7221
- ` Output : .github/copilot-instructions.md`,
7222
- bar,
7223
- ` Try: "explain the architecture" \u00b7 "find the auth module"`,
7224
- bar, '',
7225
- ].join('\n'));
7308
+ ];
7309
+ if (coverageLine) lines.push(coverageLine);
7310
+ lines.push(` Output : .github/copilot-instructions.md`);
7311
+ lines.push(bar);
7312
+ lines.push(` Try: "explain the architecture" \u00b7 "find the auth module"`);
7313
+ lines.push(bar, '');
7314
+ process.stderr.write(lines.join('\n'));
7226
7315
  }
7227
7316
 
7228
7317
  return result;
@@ -7725,6 +7814,17 @@ function main() {
7725
7814
  if (args.includes('--health')) {
7726
7815
  const { score } = __require('./src/health/scorer');
7727
7816
  const result = score(cwd);
7817
+ // v4.0: compute live coverage score to include in health output
7818
+ let coverageResult = null;
7819
+ try {
7820
+ const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
7821
+ const { loadConfig: lc } = requireSourceOrBundled('./src/config/loader');
7822
+ const cfg = lc(cwd);
7823
+ // Use all files from srcDirs as proxies for "included" (no budget applied in health mode)
7824
+ const allFiles = buildFileList(cwd, cfg);
7825
+ const fakeEntries = allFiles.map(f => ({ filePath: f }));
7826
+ coverageResult = coverageScore(cwd, fakeEntries, cfg);
7827
+ } catch (_) {}
7728
7828
  if (args.includes('--json')) {
7729
7829
  // Feature 3 (VS Code) + Feature 5 (JetBrains): emit tokens + reduction for plugins
7730
7830
  const ctxPath = path.join(cwd, '.github', 'copilot-instructions.md');
@@ -7733,10 +7833,21 @@ function main() {
7733
7833
  if (fs.existsSync(ctxPath)) {
7734
7834
  try { tokens = estimateTokens(fs.readFileSync(ctxPath, 'utf8')); } catch (_) {}
7735
7835
  }
7736
- process.stdout.write(JSON.stringify({ ...result, tokens, reduction }) + '\n');
7836
+ const payload = { ...result, tokens, reduction };
7837
+ if (coverageResult) {
7838
+ payload.coverage = coverageResult.score;
7839
+ payload.coverageGrade = coverageResult.grade;
7840
+ payload.coverageConfidence = coverageResult.confidence;
7841
+ payload.coverageTotalFiles = coverageResult.total;
7842
+ payload.coverageIncludedFiles = coverageResult.included;
7843
+ }
7844
+ process.stdout.write(JSON.stringify(payload) + '\n');
7737
7845
  } else {
7738
7846
  console.log('[sigmap] health:');
7739
7847
  console.log(` score : ${result.score}/100 (grade ${result.grade})`);
7848
+ if (coverageResult) {
7849
+ console.log(` coverage : ${coverageResult.grade} (${coverageResult.score}%) — ${coverageResult.included} of ${coverageResult.total} source files`);
7850
+ }
7740
7851
  console.log(` strategy : ${result.strategy}`);
7741
7852
  console.log(` token reduction : ${result.tokenReductionPct !== null ? result.tokenReductionPct + '%' : 'no history'}`);
7742
7853
  console.log(` days since regen: ${result.daysSinceRegen !== null ? result.daysSinceRegen : 'context file not found'}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "3.6.0",
3
+ "version": "4.0.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": {
@@ -26,15 +26,26 @@ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js
26
26
  */
27
27
  function format(context, opts = {}) {
28
28
  if (!context || typeof context !== 'string') return '';
29
- const version = opts.version || 'unknown';
29
+ const version = opts.version || 'unknown';
30
30
  const timestamp = new Date().toISOString();
31
+ const meta = _confidenceMeta(opts);
31
32
  return [
32
33
  `<!-- Generated by SigMap v${version} — ${timestamp} -->`,
34
+ meta,
33
35
  '',
34
36
  context,
35
37
  ].join('\n');
36
38
  }
37
39
 
40
+ function _confidenceMeta(opts) {
41
+ const parts = [`version=${opts.version || 'unknown'}`];
42
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
43
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
44
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
45
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
46
+ return `<!-- sigmap: ${parts.join(' ')} -->`;
47
+ }
48
+
38
49
  /**
39
50
  * Return the output file path for this adapter.
40
51
  * @param {string} cwd - Project root
@@ -24,11 +24,13 @@ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js
24
24
  */
25
25
  function format(context, opts = {}) {
26
26
  if (!context || typeof context !== 'string') return '';
27
- const version = opts.version || 'unknown';
27
+ const version = opts.version || 'unknown';
28
28
  const timestamp = new Date().toISOString();
29
+ const meta = _confidenceMeta(opts);
29
30
  const header = [
30
31
  `<!-- Generated by SigMap gen-context.js v${version} -->`,
31
32
  `<!-- Updated: ${timestamp} -->`,
33
+ meta,
32
34
  `<!-- Do not edit below — regenerate with: node gen-context.js -->`,
33
35
  '',
34
36
  '# Code signatures',
@@ -37,6 +39,15 @@ function format(context, opts = {}) {
37
39
  return header + context;
38
40
  }
39
41
 
42
+ function _confidenceMeta(opts) {
43
+ const parts = [`version=${opts.version || 'unknown'}`];
44
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
45
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
46
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
47
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
48
+ return `<!-- sigmap: ${parts.join(' ')} -->`;
49
+ }
50
+
40
51
  /**
41
52
  * Return the output file path for this adapter.
42
53
  * @param {string} cwd - Project root
@@ -22,17 +22,28 @@ const name = 'cursor';
22
22
  */
23
23
  function format(context, opts = {}) {
24
24
  if (!context || typeof context !== 'string') return '';
25
- const version = opts.version || 'unknown';
25
+ const version = opts.version || 'unknown';
26
26
  const timestamp = new Date().toISOString();
27
+ const meta = _confidenceMeta(opts);
27
28
  const header = [
28
29
  `# Code signatures — generated by SigMap v${version}`,
29
30
  `# Updated: ${timestamp}`,
31
+ `# ${meta}`,
30
32
  `# Regenerate: node gen-context.js`,
31
33
  '',
32
34
  ].join('\n');
33
35
  return header + context;
34
36
  }
35
37
 
38
+ function _confidenceMeta(opts) {
39
+ const parts = [`version=${opts.version || 'unknown'}`];
40
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
41
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
42
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
43
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
44
+ return `sigmap: ${parts.join(' ')}`;
45
+ }
46
+
36
47
  /**
37
48
  * Return the output file path for this adapter.
38
49
  * @param {string} cwd - Project root
@@ -36,9 +36,11 @@ function format(context, opts = {}) {
36
36
  ? `Project: ${opts.projectName}\n`
37
37
  : '';
38
38
 
39
+ const meta = _confidenceMeta(opts);
39
40
  return [
40
41
  `You are a coding assistant with complete knowledge of this codebase.`,
41
42
  `The following code signatures were extracted by SigMap v${version} on ${timestamp}.`,
43
+ `<!-- ${meta} -->`,
42
44
  projectLine,
43
45
  `These signatures represent every public function, class, and type in the project.`,
44
46
  `Refer to them when answering questions about code structure, APIs, and implementation.`,
@@ -91,4 +93,13 @@ function write(context, cwd, opts = {}) {
91
93
  fs.writeFileSync(filePath, newContent, 'utf8');
92
94
  }
93
95
 
96
+ function _confidenceMeta(opts) {
97
+ const parts = [`version=${opts.version || 'unknown'}`];
98
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
99
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
100
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
101
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
102
+ return `sigmap: ${parts.join(' ')}`;
103
+ }
104
+
94
105
  module.exports = { name, format, outputPath, write };
@@ -34,9 +34,11 @@ function format(context, opts = {}) {
34
34
  ? `Project: ${opts.projectName}\n`
35
35
  : '';
36
36
 
37
+ const meta = _confidenceMeta(opts);
37
38
  return [
38
39
  `You are a coding assistant with full knowledge of this codebase.`,
39
40
  `Below are the code signatures extracted by SigMap v${version} on ${timestamp}.`,
41
+ `<!-- ${meta} -->`,
40
42
  projectLine,
41
43
  `Use these signatures to answer questions about the code accurately.`,
42
44
  `When the user asks about a specific file or function, refer to the signatures below.`,
@@ -57,4 +59,13 @@ function outputPath(cwd) {
57
59
  return path.join(cwd, '.github', 'openai-context.md');
58
60
  }
59
61
 
62
+ function _confidenceMeta(opts) {
63
+ const parts = [`version=${opts.version || 'unknown'}`];
64
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
65
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
66
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
67
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
68
+ return `sigmap: ${parts.join(' ')}`;
69
+ }
70
+
60
71
  module.exports = { name, format, outputPath };
@@ -22,17 +22,28 @@ const name = 'windsurf';
22
22
  */
23
23
  function format(context, opts = {}) {
24
24
  if (!context || typeof context !== 'string') return '';
25
- const version = opts.version || 'unknown';
25
+ const version = opts.version || 'unknown';
26
26
  const timestamp = new Date().toISOString();
27
+ const meta = _confidenceMeta(opts);
27
28
  const header = [
28
29
  `# Code signatures — generated by SigMap v${version}`,
29
30
  `# Updated: ${timestamp}`,
31
+ `# ${meta}`,
30
32
  `# Regenerate: node gen-context.js`,
31
33
  '',
32
34
  ].join('\n');
33
35
  return header + context;
34
36
  }
35
37
 
38
+ function _confidenceMeta(opts) {
39
+ const parts = [`version=${opts.version || 'unknown'}`];
40
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
41
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
42
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
43
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
44
+ return `sigmap: ${parts.join(' ')}`;
45
+ }
46
+
36
47
  /**
37
48
  * Return the output file path for this adapter.
38
49
  * @param {string} cwd - Project root
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "3.5.0",
3
+ "version": "4.0.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": "3.5.0",
3
+ "version": "4.0.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SigMap coverage scorer — v4.0.0
5
+ *
6
+ * Measures what fraction of source files made it into the context output
7
+ * after token-budget application. This is complementary to the health score:
8
+ * - Health score = context freshness / reduction quality / budget compliance
9
+ * - Coverage score = how much of the codebase is represented in context
10
+ *
11
+ * Grade scale: A ≥ 90% | B ≥ 75% | C ≥ 50% | D < 50%
12
+ *
13
+ * @param {string} cwd
14
+ * @param {Array<{filePath:string}>} fileEntries - files that made it into output
15
+ * @param {{srcDirs:string[], exclude:string[]}} config
16
+ * @returns {{
17
+ * score: number,
18
+ * grade: 'A'|'B'|'C'|'D',
19
+ * total: number,
20
+ * included: number,
21
+ * dropped: number,
22
+ * confidence: 'HIGH'|'MEDIUM'|'LOW',
23
+ * perModule: Map<string, {total:number, included:number, pct:number}>,
24
+ * }}
25
+ */
26
+ function coverageScore(cwd, fileEntries, config) {
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ const srcDirs = (config && Array.isArray(config.srcDirs) && config.srcDirs.length > 0)
31
+ ? config.srcDirs
32
+ : ['src', 'app', 'lib'];
33
+
34
+ const excludeSet = new Set([
35
+ 'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
36
+ '.next', 'coverage', 'target', 'vendor', '.context',
37
+ ]);
38
+ if (config && Array.isArray(config.exclude)) {
39
+ for (const x of config.exclude) excludeSet.add(String(x));
40
+ }
41
+
42
+ const includedSet = new Set((fileEntries || []).map(f => f.filePath));
43
+
44
+ // Walk all source files from srcDirs
45
+ const allSource = [];
46
+ for (const relDir of srcDirs) {
47
+ const absDir = path.resolve(cwd, relDir);
48
+ if (fs.existsSync(absDir)) _walk(absDir, excludeSet, allSource);
49
+ }
50
+
51
+ const total = allSource.length;
52
+ const included = allSource.filter(f => includedSet.has(f)).length;
53
+ const dropped = total - included;
54
+ const pct = total > 0 ? Math.round((included / total) * 100) : 100;
55
+
56
+ const grade = pct >= 90 ? 'A' : pct >= 75 ? 'B' : pct >= 50 ? 'C' : 'D';
57
+ const confidence = pct >= 90 ? 'HIGH' : pct >= 70 ? 'MEDIUM' : 'LOW';
58
+
59
+ // Per-module breakdown (one entry per srcDir)
60
+ const perModule = new Map();
61
+ for (const relDir of srcDirs) {
62
+ const absDir = path.resolve(cwd, relDir);
63
+ const modFiles = allSource.filter(f => f.startsWith(absDir + path.sep) || f === absDir);
64
+ const modIncl = modFiles.filter(f => includedSet.has(f)).length;
65
+ const modPct = modFiles.length > 0 ? Math.round((modIncl / modFiles.length) * 100) : 100;
66
+ perModule.set(relDir, { total: modFiles.length, included: modIncl, pct: modPct });
67
+ }
68
+
69
+ return { score: pct, grade, total, included, dropped, confidence, perModule };
70
+ }
71
+
72
+ function _walk(dir, excludeSet, out) {
73
+ const fs = require('fs');
74
+ const path = require('path');
75
+ let entries;
76
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
77
+ for (const e of entries) {
78
+ if (excludeSet.has(e.name)) continue;
79
+ const full = path.join(dir, e.name);
80
+ if (e.isDirectory()) { _walk(full, excludeSet, out); }
81
+ else if (e.isFile()) { out.push(full); }
82
+ }
83
+ }
84
+
85
+ module.exports = { coverageScore };
@@ -147,6 +147,9 @@ function analyzeFiles(files, cwd, opts) {
147
147
  const tokens = tokenCount(sigs);
148
148
  const covered = hasCoverage(filePath, cwd);
149
149
  const isSlow = slow && elapsedMs > slowMs;
150
+ // v4.0: signal quality = sigs per line-of-code (higher = more informative to LLMs)
151
+ const linesOfCode = content.split('\n').length;
152
+ const signalQuality = linesOfCode > 0 ? parseFloat((sigs.length / linesOfCode).toFixed(4)) : 0;
150
153
 
151
154
  stats.push({
152
155
  file: rel,
@@ -154,6 +157,8 @@ function analyzeFiles(files, cwd, opts) {
154
157
  sigs: sigs.length,
155
158
  tokens,
156
159
  covered,
160
+ linesOfCode,
161
+ signalQuality,
157
162
  elapsedMs: slow ? elapsedMs : undefined,
158
163
  slow: slow ? isSlow : undefined,
159
164
  });
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: '3.5.0',
21
+ version: '4.0.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24