sigmap 3.5.1 → 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,49 +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.0 on 2026-04-14T21:58:46.968Z.
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.0 -->
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 — 19 minutes ago)
26
+ ## changes (last 5 commits — 6 hours ago)
27
27
  ```
28
- src/config/loader.js ~detectAutoSrcDirs
29
- src/eval/analyzer.js ~isDockerfile
30
- src/extractors/markdown.js +extract
31
- src/extractors/patterns.js +extract +implementing
32
- src/extractors/properties.js +extract
33
- src/extractors/python_dataclass.js +extract +definitions
34
- src/extractors/toml.js +extract
35
- src/extractors/typescript_react.js +extract +declarations
36
- src/extractors/vue_sfc.js +extract
37
- src/extractors/xml.js +attributes +extract
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
38
32
  ```
39
33
 
40
34
  ## packages
41
35
 
42
- ### packages/core/index.js
36
+ ### packages/adapters/llm-full.js
43
37
  ```
44
- module.exports = { extract, rank, buildSigIndex, scan, score, adapt }
45
- function _resolveExtractor(language)
46
- function extract(src, language) → string[]
47
- function rank(query, sigIndex, opts) → { file: string, score: nu
48
- function buildSigIndex(cwd) → Map<string, string[]>
49
- function scan(sigs, filePath) → { safe: string[], redacte
50
- function score(cwd) → { * score: number, * grad
51
- 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)
52
42
  ```
53
43
 
54
44
  ### packages/adapters/claude.js
55
45
  ```
56
46
  module.exports = { name, format, outputPath, write }
57
47
  function format(context, opts = {}) → string
48
+ function _confidenceMeta(opts)
58
49
  function outputPath(cwd) → string
59
50
  function write(context, cwd, opts = {})
60
51
  ```
@@ -71,6 +62,7 @@ function write(context, cwd, opts = {})
71
62
  ```
72
63
  module.exports = { name, format, outputPath, write }
73
64
  function format(context, opts = {}) → string
65
+ function _confidenceMeta(opts)
74
66
  function outputPath(cwd) → string
75
67
  function write(context, cwd, opts = {})
76
68
  ```
@@ -79,6 +71,7 @@ function write(context, cwd, opts = {})
79
71
  ```
80
72
  module.exports = { name, format, outputPath }
81
73
  function format(context, opts = {}) → string
74
+ function _confidenceMeta(opts)
82
75
  function outputPath(cwd) → string
83
76
  ```
84
77
 
@@ -88,6 +81,7 @@ module.exports = { name, format, outputPath, write }
88
81
  function format(context, opts = {}) → string
89
82
  function outputPath(cwd) → string
90
83
  function write(context, cwd, opts = {})
84
+ function _confidenceMeta(opts)
91
85
  ```
92
86
 
93
87
  ### packages/adapters/index.js
@@ -104,12 +98,14 @@ function outputsToAdapters(outputs) → string[]
104
98
  module.exports = { name, format, outputPath }
105
99
  function format(context, opts = {}) → string
106
100
  function outputPath(cwd) → string
101
+ function _confidenceMeta(opts)
107
102
  ```
108
103
 
109
104
  ### packages/adapters/windsurf.js
110
105
  ```
111
106
  module.exports = { name, format, outputPath }
112
107
  function format(context, opts = {}) → string
108
+ function _confidenceMeta(opts)
113
109
  function outputPath(cwd) → string
114
110
  ```
115
111
 
@@ -139,8 +135,49 @@ code-fence js
139
135
  code-fence ---
140
136
  ```
141
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
+
142
150
  ## src
143
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
+
144
181
  ### src/config/defaults.js
145
182
  ```
146
183
  module.exports = { DEFAULTS }
@@ -167,89 +204,6 @@ function formatAnalysisTable(stats, showSlow) → string
167
204
  function formatAnalysisJSON(stats) → object
168
205
  ```
169
206
 
170
- ### src/extractors/graphql.js
171
- ```
172
- module.exports = { extract }
173
- function extract(src) → string[]
174
- ```
175
-
176
- ### src/extractors/markdown.js
177
- ```
178
- module.exports = { extract }
179
- function extract(src) → string[]
180
- ```
181
-
182
- ### src/extractors/patterns.js
183
- ```
184
- module.exports = { extract }
185
- function extract(src) → string[]
186
- ```
187
-
188
- ### src/extractors/properties.js
189
- ```
190
- module.exports = { extract }
191
- function extract(src) → string[]
192
- ```
193
-
194
- ### src/extractors/protobuf.js
195
- ```
196
- module.exports = { extract }
197
- function extract(src) → string[]
198
- ```
199
-
200
- ### src/extractors/python_dataclass.js
201
- ```
202
- module.exports = { extract }
203
- function extract(src) → string[]
204
- ```
205
-
206
- ### src/extractors/sql.js
207
- ```
208
- module.exports = { extract }
209
- function extract(src) → string[]
210
- function _cleanName(raw)
211
- function _normalizeParams(raw)
212
- ```
213
-
214
- ### src/extractors/terraform.js
215
- ```
216
- module.exports = { extract }
217
- function extract(src) → string[]
218
- ```
219
-
220
- ### src/extractors/toml.js
221
- ```
222
- module.exports = { extract }
223
- function extract(src) → string[]
224
- ```
225
-
226
- ### src/extractors/typescript_react.js
227
- ```
228
- module.exports = { extract }
229
- function extract(src) → string[]
230
- ```
231
-
232
- ### src/extractors/vue_sfc.js
233
- ```
234
- module.exports = { extract }
235
- function extract(src) → string[]
236
- ```
237
-
238
- ### src/extractors/xml.js
239
- ```
240
- module.exports = { extract }
241
- function extract(src) → string[]
242
- ```
243
-
244
- ### src/mcp/server.js
245
- ```
246
- module.exports = { start }
247
- function respond(id, result)
248
- function respondError(id, code, message)
249
- function dispatch(msg, cwd)
250
- function start(cwd)
251
- ```
252
-
253
207
  ### src/eval/runner.js
254
208
  ```
255
209
  module.exports = { run, rank, loadTasks, buildSigIndex, formatTable, formatMetrics, tokenize }
@@ -342,6 +296,12 @@ function extractInterfaceMethods(block)
342
296
  function normalizeParams(params)
343
297
  ```
344
298
 
299
+ ### src/extractors/graphql.js
300
+ ```
301
+ module.exports = { extract }
302
+ function extract(src) → string[]
303
+ ```
304
+
345
305
  ### src/extractors/html.js
346
306
  ```
347
307
  module.exports = { extract }
@@ -379,6 +339,18 @@ function extractMembers(block)
379
339
  function normalizeParams(params)
380
340
  ```
381
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
+
382
354
  ### src/extractors/php.js
383
355
  ```
384
356
  module.exports = { extract }
@@ -396,6 +368,18 @@ function diffSignatures(baseSigs, currentSigs) → {added:string[], removed:
396
368
  function extractName(sig)
397
369
  ```
398
370
 
371
+ ### src/extractors/properties.js
372
+ ```
373
+ module.exports = { extract }
374
+ function extract(src) → string[]
375
+ ```
376
+
377
+ ### src/extractors/protobuf.js
378
+ ```
379
+ module.exports = { extract }
380
+ function extract(src) → string[]
381
+ ```
382
+
399
383
  ### src/extractors/python.js
400
384
  ```
401
385
  module.exports = { extract }
@@ -409,6 +393,12 @@ function normalizeParams(params)
409
393
  function extractDocHint(src, fnName, fnSigLine)
410
394
  ```
411
395
 
396
+ ### src/extractors/python_dataclass.js
397
+ ```
398
+ module.exports = { extract }
399
+ function extract(src) → string[]
400
+ ```
401
+
412
402
  ### src/extractors/ruby.js
413
403
  ```
414
404
  module.exports = { extract }
@@ -443,6 +433,14 @@ module.exports = { extract }
443
433
  function extract(src) → string[]
444
434
  ```
445
435
 
436
+ ### src/extractors/sql.js
437
+ ```
438
+ module.exports = { extract }
439
+ function extract(src) → string[]
440
+ function _cleanName(raw)
441
+ function _normalizeParams(raw)
442
+ ```
443
+
446
444
  ### src/extractors/svelte.js
447
445
  ```
448
446
  module.exports = { extract }
@@ -461,12 +459,24 @@ function normalizeParams(params)
461
459
  function extractArrowType(str)
462
460
  ```
463
461
 
462
+ ### src/extractors/terraform.js
463
+ ```
464
+ module.exports = { extract }
465
+ function extract(src) → string[]
466
+ ```
467
+
464
468
  ### src/extractors/todos.js
465
469
  ```
466
470
  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 }
@@ -581,6 +609,15 @@ function queryContext(args, cwd)
581
609
  function getImpact(args, cwd)
582
610
  ```
583
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
+
584
621
  ### src/mcp/tools.js
585
622
  ```
586
623
  module.exports = { TOOLS }
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- <img src="docs-vp/.vitepress/public/logo.svg" alt="SigMap logo" width="64" height="84" />
3
+ <img src="assets/logo.png" alt="SigMap logo" width="80" height="80" />
4
4
 
5
5
  <h1>⚡ SigMap</h1>
6
6
 
@@ -456,6 +456,10 @@ Activates on startup (`onStartupFinished`) — loads within 3 s, never blocks ed
456
456
 
457
457
  The official SigMap JetBrains plugin brings the same features to IntelliJ-based IDEs. Install it from the JetBrains Marketplace and it works identically to the VS Code extension.
458
458
 
459
+ <div align="center">
460
+ <img src="assets/intelij.gif" alt="SigMap JetBrains plugin — status bar health grade, regenerate action, and context auto-refresh in IntelliJ IDEA" width="700" />
461
+ </div>
462
+
459
463
  | Feature | Detail |
460
464
  |---|---|
461
465
  | **Status bar widget** | Shows health grade (`A`-`F`) + time since last regen; updates every 60 s |
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.5.1';
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) {
@@ -6270,35 +6270,47 @@ function applyTokenBudget(fileEntries, maxTokens) {
6270
6270
  // Sort by drop priority (drop first = index 0)
6271
6271
  const withPriority = fileEntries.map((e) => {
6272
6272
  let priority = 0;
6273
- if (isGeneratedFile(e.filePath)) priority = 10;
6274
- else if (isMockFile(e.filePath)) priority = 9;
6275
- else if (isTestFile(e.filePath)) priority = 8;
6276
- else if (isConfigFile(e.filePath)) priority = 6;
6273
+ let dropReason = 'budget: low recency';
6274
+ if (isGeneratedFile(e.filePath)) { priority = 10; dropReason = 'budget: generated file'; }
6275
+ else if (isMockFile(e.filePath)) { priority = 9; dropReason = 'budget: mock file'; }
6276
+ else if (isTestFile(e.filePath)) { priority = 8; dropReason = 'budget: test file'; }
6277
+ else if (isConfigFile(e.filePath)) { priority = 6; dropReason = 'budget: config file'; }
6277
6278
  else priority = 4;
6278
- return { ...e, priority };
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 };
6279
6283
  });
6280
6284
 
6281
- // 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)
6282
6286
  withPriority.sort((a, b) => {
6283
6287
  if (b.priority !== a.priority) return b.priority - a.priority;
6284
- 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);
6285
6290
  });
6286
6291
 
6287
6292
  const kept = [];
6288
- let dropped = 0;
6293
+ const verboseDropped = [];
6289
6294
  // Iterate forward: highest drop-priority files (generated=10, mock=9, test=8) are at index 0
6290
6295
  // Drop those first until we're under budget, then keep everything else
6291
6296
  for (const entry of withPriority) {
6292
6297
  const entryTokens = estimateTokens(entry.sigs.join('\n'));
6293
6298
  if (total > effectiveBudget) {
6294
6299
  total -= entryTokens;
6295
- dropped++;
6300
+ verboseDropped.push({ filePath: entry.filePath, reason: entry.dropReason });
6296
6301
  } else {
6297
6302
  kept.push(entry);
6298
6303
  }
6299
6304
  }
6300
- if (dropped > 0) {
6301
- console.warn(`[sigmap] budget: dropped ${dropped} files to stay under ${maxTokens} tokens`);
6305
+ if (verboseDropped.length > 0) {
6306
+ console.warn(`[sigmap] budget: dropped ${verboseDropped.length} files to stay under ${maxTokens} tokens`);
6307
+ // Feature 7: --verbose — print per-file drop reason
6308
+ if (process.argv.includes('--verbose')) {
6309
+ for (const { filePath, reason } of verboseDropped) {
6310
+ console.warn(`[sigmap] dropped: ${path.relative(process.cwd(), filePath)} — ${reason}`);
6311
+ }
6312
+ console.warn(`[sigmap] included: ${kept.length} files, dropped: ${verboseDropped.length}`);
6313
+ }
6302
6314
  }
6303
6315
  return kept;
6304
6316
  }
@@ -6708,11 +6720,17 @@ function writeClaude(content, cwd) {
6708
6720
  // ---------------------------------------------------------------------------
6709
6721
  // Report
6710
6722
  // ---------------------------------------------------------------------------
6711
- 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) {
6712
6730
  const reduction = inputTokens > 0 ? (100 - (finalTokens / inputTokens) * 100).toFixed(1) : 0;
6713
6731
  const overBudget = finalTokens > (budgetLimit || 6000);
6714
6732
  if (asJson) {
6715
- process.stdout.write(JSON.stringify({
6733
+ const payload = {
6716
6734
  version: VERSION,
6717
6735
  timestamp: new Date().toISOString(),
6718
6736
  rawTokens: inputTokens,
@@ -6723,7 +6741,22 @@ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson,
6723
6741
  reductionPct: parseFloat(reduction),
6724
6742
  overBudget,
6725
6743
  budgetLimit: budgetLimit || 6000,
6726
- }) + '\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');
6727
6760
  // Exit 1 in CI if over budget — lets pipelines fail fast
6728
6761
  if (overBudget) process.exitCode = 1;
6729
6762
  } else {
@@ -6735,6 +6768,20 @@ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson,
6735
6768
  console.log(` output tokens : ~${finalTokens}`);
6736
6769
  console.log(` budget limit : ${budgetLimit || 6000}`);
6737
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
+ }
6738
6785
  if (overBudget) console.warn(`[sigmap] WARNING: output (${finalTokens} tokens) exceeds budget (${budgetLimit || 6000})`);
6739
6786
  }
6740
6787
  }
@@ -7024,6 +7071,41 @@ function runDiff(cwd, config, stagedOnly, baseRef) {
7024
7071
  const scope = baseRef ? `diff-vs-${baseRef}` : (stagedOnly ? 'staged' : 'diff');
7025
7072
  console.warn(`[sigmap] ${scope} files: ${fileEntries.length}, diff tokens: ~${finalTokens}`);
7026
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
+
7027
7109
  if (process.argv.includes('--report')) {
7028
7110
  // Also show what the full run would cost for comparison
7029
7111
  const fullResult = runGenerate(cwd, config, true);
@@ -7168,11 +7250,17 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7168
7250
  const droppedCount = beforeCount - budgeted.length;
7169
7251
  const content = formatOutput(budgeted, cwd, false, config, null);
7170
7252
  const finalTokens = estimateTokens(content);
7171
- 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 };
7172
7260
  }
7173
7261
 
7174
7262
  if (reportMode || process.argv.includes('--report')) {
7175
- 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);
7176
7264
  }
7177
7265
 
7178
7266
  // Usage tracking (v0.9) — optional append-only NDJSON log
@@ -7204,17 +7292,26 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7204
7292
  const pct = result.inputTokenTotal > 0
7205
7293
  ? Math.round((1 - result.finalTokens / result.inputTokenTotal) * 100)
7206
7294
  : 0;
7207
- 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 = [
7208
7303
  bar,
7209
7304
  ` SigMap v${VERSION}`,
7210
7305
  ` Files scanned : ${result.fileCount}`,
7211
7306
  ` Symbols found : ${syms.toLocaleString()}`,
7212
7307
  ` Token reduction: ${pct}% (${result.inputTokenTotal.toLocaleString()} \u2192 ${result.finalTokens.toLocaleString()})`,
7213
- ` Output : .github/copilot-instructions.md`,
7214
- bar,
7215
- ` Try: "explain the architecture" \u00b7 "find the auth module"`,
7216
- bar, '',
7217
- ].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'));
7218
7315
  }
7219
7316
 
7220
7317
  return result;
@@ -7628,6 +7725,87 @@ function main() {
7628
7725
  process.exit(0);
7629
7726
  }
7630
7727
 
7728
+ // Feature 1: `sigmap explain <file>` — why a file is included or excluded
7729
+ if (args[0] === 'explain' || args.includes('--explain')) {
7730
+ const target = args[0] === 'explain'
7731
+ ? args[1]
7732
+ : args[args.indexOf('--explain') + 1];
7733
+
7734
+ if (!target || target.startsWith('--')) {
7735
+ console.error('[sigmap] Usage: sigmap explain <file>');
7736
+ process.exit(1);
7737
+ }
7738
+
7739
+ const absPath = path.resolve(cwd, target);
7740
+ const rel = path.relative(cwd, absPath);
7741
+ const jsonOut = args.includes('--json');
7742
+
7743
+ // 1. Check .contextignore
7744
+ const ignorePatterns = loadIgnorePatterns(cwd);
7745
+ const ignored = matchesIgnore(rel.replace(/\\/g, '/'), ignorePatterns);
7746
+ if (ignored) {
7747
+ if (jsonOut) {
7748
+ process.stdout.write(JSON.stringify({ status: 'excluded', reason: '.contextignore', fix: `remove the pattern or add '!${rel}' as an exception` }) + '\n');
7749
+ } else {
7750
+ console.log(`[sigmap] ${rel} — EXCLUDED`);
7751
+ console.log(` Reason: matched .contextignore`);
7752
+ console.log(` Fix: remove the pattern or add '!${rel}' as an exception`);
7753
+ }
7754
+ process.exit(0);
7755
+ }
7756
+
7757
+ // 2. Check srcDirs membership
7758
+ const inSrcDirs = (config.srcDirs || []).some(d => absPath.startsWith(path.resolve(cwd, d)));
7759
+ if (!inSrcDirs) {
7760
+ if (jsonOut) {
7761
+ process.stdout.write(JSON.stringify({ status: 'excluded', reason: 'not in srcDirs', srcDirs: config.srcDirs, fix: 'add the containing directory to srcDirs in gen-context.config.json' }) + '\n');
7762
+ } else {
7763
+ console.log(`[sigmap] ${rel} — EXCLUDED`);
7764
+ console.log(` Reason: not under any srcDir (${(config.srcDirs || []).join(', ')})`);
7765
+ console.log(` Fix: add the containing directory to srcDirs in gen-context.config.json`);
7766
+ }
7767
+ process.exit(0);
7768
+ }
7769
+
7770
+ // 3. Detect extractor and extract sigs
7771
+ const base = path.basename(absPath);
7772
+ const ext = path.extname(base).toLowerCase();
7773
+ let extractorName = EXT_MAP[ext] || null;
7774
+ if (!extractorName && isDockerfile(base)) extractorName = 'dockerfile';
7775
+ if (!extractorName) extractorName = 'generic';
7776
+
7777
+ let sigs = [];
7778
+ if (fs.existsSync(absPath)) {
7779
+ try {
7780
+ const content = fs.readFileSync(absPath, 'utf8');
7781
+ const extractor = getExtractor(extractorName);
7782
+ if (extractor) sigs = extractor.extract(content).slice(0, config.maxSigsPerFile || 25);
7783
+ } catch (_) {}
7784
+ }
7785
+
7786
+ if (!sigs.length) {
7787
+ if (jsonOut) {
7788
+ process.stdout.write(JSON.stringify({ status: 'excluded', reason: 'no signatures', extractor: extractorName, fix: 'check that the file contains function/class definitions' }) + '\n');
7789
+ } else {
7790
+ console.log(`[sigmap] ${rel} — EXCLUDED`);
7791
+ console.log(` Reason: no extractable signatures (extractor: ${extractorName})`);
7792
+ console.log(` Fix: check that the file contains function/class definitions`);
7793
+ }
7794
+ process.exit(0);
7795
+ }
7796
+
7797
+ // 4. Included
7798
+ if (jsonOut) {
7799
+ process.stdout.write(JSON.stringify({ status: 'included', extractor: extractorName, signatures: sigs.length, preview: sigs.slice(0, 3), sigs }) + '\n');
7800
+ } else {
7801
+ console.log(`[sigmap] ${rel} — INCLUDED`);
7802
+ console.log(` Extractor : ${extractorName}`);
7803
+ console.log(` Signatures: ${sigs.length}`);
7804
+ console.log(` Preview : ${sigs.slice(0, 3).join(' · ')}`);
7805
+ }
7806
+ process.exit(0);
7807
+ }
7808
+
7631
7809
  if (args.includes('--init')) {
7632
7810
  writeInitConfig(cwd);
7633
7811
  process.exit(0);
@@ -7636,11 +7814,40 @@ function main() {
7636
7814
  if (args.includes('--health')) {
7637
7815
  const { score } = __require('./src/health/scorer');
7638
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 (_) {}
7639
7828
  if (args.includes('--json')) {
7640
- process.stdout.write(JSON.stringify(result) + '\n');
7829
+ // Feature 3 (VS Code) + Feature 5 (JetBrains): emit tokens + reduction for plugins
7830
+ const ctxPath = path.join(cwd, '.github', 'copilot-instructions.md');
7831
+ let tokens = 0;
7832
+ let reduction = result.tokenReductionPct || 0;
7833
+ if (fs.existsSync(ctxPath)) {
7834
+ try { tokens = estimateTokens(fs.readFileSync(ctxPath, 'utf8')); } catch (_) {}
7835
+ }
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');
7641
7845
  } else {
7642
7846
  console.log('[sigmap] health:');
7643
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
+ }
7644
7851
  console.log(` strategy : ${result.strategy}`);
7645
7852
  console.log(` token reduction : ${result.tokenReductionPct !== null ? result.tokenReductionPct + '%' : 'no history'}`);
7646
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.5.1",
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