sigmap 3.6.0 → 4.0.1

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,43 +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-15T06:03:22.464Z.
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
- ## packages
27
-
28
- ### packages/core/index.js
26
+ ## changes (last 5 commits — 7 minutes ago)
29
27
  ```
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
28
+ src/analysis/coverage-score.js +coverageScore +_walk
29
+ src/eval/analyzer.js ~analyzeFiles
30
+ src/extractors/generic.js +extract
31
+ src/format/llm-txt.js +outputPath +format
32
+ src/format/llms-txt.js +outputPath +getShortCommit +detectVersion +format
33
+ packages/adapters/claude.js +_confidenceMeta ~format
34
+ packages/adapters/copilot.js +_confidenceMeta ~format
35
+ packages/adapters/cursor.js +_confidenceMeta ~format
36
+ packages/adapters/gemini.js +_confidenceMeta ~format ~write
37
+ packages/adapters/llm-full.js +outputPath +format +write
38
+ packages/adapters/openai.js +_confidenceMeta ~format ~outputPath
39
+ packages/adapters/windsurf.js +_confidenceMeta ~format
38
40
  ```
39
41
 
40
- ### packages/adapters/claude.js
41
- ```
42
- module.exports = { name, format, outputPath, write }
43
- function format(context, opts = {}) → string
44
- function outputPath(cwd) → string
45
- function write(context, cwd, opts = {})
46
- ```
42
+ ## packages
47
43
 
48
- ### packages/adapters/codex.js
44
+ ### packages/adapters/claude.js
49
45
  ```
50
46
  module.exports = { name, format, outputPath, write }
51
47
  function format(context, opts = {}) → string
48
+ function _confidenceMeta(opts)
52
49
  function outputPath(cwd) → string
53
50
  function write(context, cwd, opts = {})
54
51
  ```
@@ -57,6 +54,7 @@ function write(context, cwd, opts = {})
57
54
  ```
58
55
  module.exports = { name, format, outputPath, write }
59
56
  function format(context, opts = {}) → string
57
+ function _confidenceMeta(opts)
60
58
  function outputPath(cwd) → string
61
59
  function write(context, cwd, opts = {})
62
60
  ```
@@ -65,6 +63,7 @@ function write(context, cwd, opts = {})
65
63
  ```
66
64
  module.exports = { name, format, outputPath }
67
65
  function format(context, opts = {}) → string
66
+ function _confidenceMeta(opts)
68
67
  function outputPath(cwd) → string
69
68
  ```
70
69
 
@@ -74,15 +73,7 @@ module.exports = { name, format, outputPath, write }
74
73
  function format(context, opts = {}) → string
75
74
  function outputPath(cwd) → string
76
75
  function write(context, cwd, opts = {})
77
- ```
78
-
79
- ### packages/adapters/index.js
80
- ```
81
- module.exports = { getAdapter, listAdapters, adapt, outputsToAdapters }
82
- function getAdapter(name) → { name: string, format: F
83
- function listAdapters() → string[]
84
- function adapt(context, adapterName, opts = {}) → string
85
- function outputsToAdapters(outputs) → string[]
76
+ function _confidenceMeta(opts)
86
77
  ```
87
78
 
88
79
  ### packages/adapters/llm-full.js
@@ -98,15 +89,34 @@ function write(context, cwd, opts)
98
89
  module.exports = { name, format, outputPath }
99
90
  function format(context, opts = {}) → string
100
91
  function outputPath(cwd) → string
92
+ function _confidenceMeta(opts)
101
93
  ```
102
94
 
103
95
  ### packages/adapters/windsurf.js
104
96
  ```
105
97
  module.exports = { name, format, outputPath }
106
98
  function format(context, opts = {}) → string
99
+ function _confidenceMeta(opts)
107
100
  function outputPath(cwd) → string
108
101
  ```
109
102
 
103
+ ### packages/adapters/codex.js
104
+ ```
105
+ module.exports = { name, format, outputPath, write }
106
+ function format(context, opts = {}) → string
107
+ function outputPath(cwd) → string
108
+ function write(context, cwd, opts = {})
109
+ ```
110
+
111
+ ### packages/adapters/index.js
112
+ ```
113
+ module.exports = { getAdapter, listAdapters, adapt, outputsToAdapters }
114
+ function getAdapter(name) → { name: string, format: F
115
+ function listAdapters() → string[]
116
+ function adapt(context, adapterName, opts = {}) → string
117
+ function outputsToAdapters(outputs) → string[]
118
+ ```
119
+
110
120
  ### packages/cli/index.js
111
121
  ```
112
122
  module.exports = { CLI_ENTRY, run }
@@ -133,19 +143,25 @@ code-fence js
133
143
  code-fence ---
134
144
  ```
135
145
 
136
- ## src
137
-
138
- ### src/config/defaults.js
146
+ ### packages/core/index.js
139
147
  ```
140
- module.exports = { DEFAULTS }
148
+ module.exports = { extract, rank, buildSigIndex, scan, score, adapt }
149
+ function _resolveExtractor(language)
150
+ function extract(src, language) → string[]
151
+ function rank(query, sigIndex, opts) → { file: string, score: nu
152
+ function buildSigIndex(cwd) → Map<string, string[]>
153
+ function scan(sigs, filePath) → { safe: string[], redacte
154
+ function score(cwd) → { * score: number, * grad
155
+ function adapt(context, adapterName, opts = {}) → string
141
156
  ```
142
157
 
143
- ### src/config/loader.js
158
+ ## src
159
+
160
+ ### src/analysis/coverage-score.js
144
161
  ```
145
- module.exports = { loadConfig }
146
- function detectAutoSrcDirs(cwd, excludeList) → string[]
147
- function loadConfig(cwd) object
148
- function deepClone(obj)
162
+ module.exports = { coverageScore }
163
+ function coverageScore(cwd, fileEntries, config) → { * score: number, * grad
164
+ function _walk(dir, excludeSet, out)
149
165
  ```
150
166
 
151
167
  ### src/eval/analyzer.js
@@ -161,52 +177,26 @@ function formatAnalysisTable(stats, showSlow) → string
161
177
  function formatAnalysisJSON(stats) → object
162
178
  ```
163
179
 
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
180
+ ### src/extractors/generic.js
195
181
  ```
196
182
  module.exports = { extract }
197
- function extract(src) → string[]
183
+ function extract(src)
198
184
  ```
199
185
 
200
- ### src/extractors/vue_sfc.js
186
+ ### src/format/llm-txt.js
201
187
  ```
202
- module.exports = { extract }
203
- function extract(src) → string[]
188
+ module.exports = { format, outputPath }
189
+ function outputPath(cwd)
190
+ function format(context, cwd, version)
204
191
  ```
205
192
 
206
- ### src/extractors/xml.js
193
+ ### src/format/llms-txt.js
207
194
  ```
208
- module.exports = { extract }
209
- function extract(src) → string[]
195
+ module.exports = { format, outputPath }
196
+ function outputPath(cwd)
197
+ function getShortCommit(cwd)
198
+ function detectVersion(cwd)
199
+ function format(context, cwd, writtenFiles, sigmapVersion)
210
200
  ```
211
201
 
212
202
  ### src/mcp/server.js
@@ -218,6 +208,19 @@ function dispatch(msg, cwd)
218
208
  function start(cwd)
219
209
  ```
220
210
 
211
+ ### src/config/defaults.js
212
+ ```
213
+ module.exports = { DEFAULTS }
214
+ ```
215
+
216
+ ### src/config/loader.js
217
+ ```
218
+ module.exports = { loadConfig }
219
+ function detectAutoSrcDirs(cwd, excludeList) → string[]
220
+ function loadConfig(cwd) → object
221
+ function deepClone(obj)
222
+ ```
223
+
221
224
  ### src/eval/runner.js
222
225
  ```
223
226
  module.exports = { run, rank, loadTasks, buildSigIndex, formatTable, formatMetrics, tokenize }
@@ -301,12 +304,6 @@ module.exports = { extract }
301
304
  function extract(src) → string[]
302
305
  ```
303
306
 
304
- ### src/extractors/generic.js
305
- ```
306
- module.exports = { extract }
307
- function extract(src)
308
- ```
309
-
310
307
  ### src/extractors/go.js
311
308
  ```
312
309
  module.exports = { extract }
@@ -359,6 +356,18 @@ function extractMembers(block)
359
356
  function normalizeParams(params)
360
357
  ```
361
358
 
359
+ ### src/extractors/markdown.js
360
+ ```
361
+ module.exports = { extract }
362
+ function extract(src) → string[]
363
+ ```
364
+
365
+ ### src/extractors/patterns.js
366
+ ```
367
+ module.exports = { extract }
368
+ function extract(src) → string[]
369
+ ```
370
+
362
371
  ### src/extractors/php.js
363
372
  ```
364
373
  module.exports = { extract }
@@ -376,6 +385,12 @@ function diffSignatures(baseSigs, currentSigs) → {added:string[], removed:
376
385
  function extractName(sig)
377
386
  ```
378
387
 
388
+ ### src/extractors/properties.js
389
+ ```
390
+ module.exports = { extract }
391
+ function extract(src) → string[]
392
+ ```
393
+
379
394
  ### src/extractors/protobuf.js
380
395
  ```
381
396
  module.exports = { extract }
@@ -395,6 +410,12 @@ function normalizeParams(params)
395
410
  function extractDocHint(src, fnName, fnSigLine)
396
411
  ```
397
412
 
413
+ ### src/extractors/python_dataclass.js
414
+ ```
415
+ module.exports = { extract }
416
+ function extract(src) → string[]
417
+ ```
418
+
398
419
  ### src/extractors/ruby.js
399
420
  ```
400
421
  module.exports = { extract }
@@ -467,6 +488,12 @@ module.exports = { extractTodos }
467
488
  function extractTodos(src) → {line:number, tag:string,
468
489
  ```
469
490
 
491
+ ### src/extractors/toml.js
492
+ ```
493
+ module.exports = { extract }
494
+ function extract(src) → string[]
495
+ ```
496
+
470
497
  ### src/extractors/typescript.js
471
498
  ```
472
499
  module.exports = { extract }
@@ -477,6 +504,12 @@ function extractClassMembers(block)
477
504
  function normalizeParams(params)
478
505
  ```
479
506
 
507
+ ### src/extractors/typescript_react.js
508
+ ```
509
+ module.exports = { extract }
510
+ function extract(src) → string[]
511
+ ```
512
+
480
513
  ### src/extractors/vue.js
481
514
  ```
482
515
  module.exports = { extract }
@@ -485,6 +518,18 @@ function normalizeParams(params)
485
518
  function normalizeType(type)
486
519
  ```
487
520
 
521
+ ### src/extractors/vue_sfc.js
522
+ ```
523
+ module.exports = { extract }
524
+ function extract(src) → string[]
525
+ ```
526
+
527
+ ### src/extractors/xml.js
528
+ ```
529
+ module.exports = { extract }
530
+ function extract(src) → string[]
531
+ ```
532
+
488
533
  ### src/extractors/yaml.js
489
534
  ```
490
535
  module.exports = { extract }
@@ -518,22 +563,6 @@ function generateDashboardHtml(cwd, health)
518
563
  function renderHistoryCharts(cwd, health)
519
564
  ```
520
565
 
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
566
  ### src/graph/builder.js
538
567
  ```
539
568
  module.exports = { build, buildFromCwd, extractFileDeps }
package/CHANGELOG.md CHANGED
@@ -10,6 +10,62 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [4.0.1] — 2026-04-15 — Config auto-detection fix
14
+
15
+ ### Fixed
16
+ - **Bundled `loadConfig` lacked `detectAutoSrcDirs`**: the inline `__factories["./src/config/loader"]` copy inside `gen-context.js` was a stripped-down version that returned raw `DEFAULTS` without filesystem auto-detection. After `--init` wrote a config with 6 hardcoded `srcDirs`, auto-detection was bypassed and custom project directories were missed — causing coverage to drop for any project whose source lives outside those 6 dirs. The bundled loader is now fully in sync with `src/config/loader.js`.
17
+ - **`--init` config hardcoded `srcDirs`**: `gen-context.config.json.example` had `"srcDirs": ["src","app","lib","packages","services","api"]` as a plain value. Any project that ran `--init` would lock into those 6 dirs and lose auto-detection. The example now omits `srcDirs` entirely and uses `_comment` keys to explain that auto-detection runs automatically. Users who need custom dirs can add `srcDirs` manually.
18
+ - **`gen-context.config.json` (SigMap repo)**: restored explicit `"srcDirs": ["src","packages"]` so the repo's own context generation is not affected by auto-detection picking up `docs-vp/`, `scripts/`, `test/`, and `vscode-extension/`.
19
+ - **Example `outputs` updated**: `gen-context.config.json.example` now lists all four standard adapters — `["copilot","codex","claude","gemini"]` — matching the recommended setup.
20
+
21
+ ### Benchmarks (v4.0.1)
22
+ - Token reduction: **97.6% average** across 18 repos ✅
23
+ - Retrieval hit@5: **84.4%** (up from 83.3% in v4.0.0)
24
+
25
+ ---
26
+
27
+ ## [4.0.0] — 2026-04-15 — Intelligence Layer
28
+
29
+ ### Added
30
+ - **Coverage score** (`src/analysis/coverage-score.js`): measures what fraction of source files made it into context after token-budget application.
31
+ - Grade scale: A ≥ 90% · B ≥ 75% · C ≥ 50% · D < 50%
32
+ - Confidence indicator: HIGH / MEDIUM / LOW
33
+ - Per-module breakdown per srcDir via `perModule` Map
34
+ - **Confidence indicators in all output writers**: every generated file now includes a metadata comment:
35
+ ```
36
+ <!-- sigmap: version=4.0.0 confidence=HIGH coverage=94% dropped=9 commit=abc1234 -->
37
+ ```
38
+ Applies to: `copilot`, `claude`, `cursor`, `windsurf`, `openai`, `gemini` adapters.
39
+ - **`--report` module heatmap**: ASCII bar chart per srcDir showing coverage percentage:
40
+ ```
41
+ Module Coverage:
42
+ src ████████████████ 100% (64/64 files)
43
+ packages ██████████████░░ 86% (12/14 files)
44
+ ```
45
+ `--report --json` gains a `coverage` object with `score`, `grade`, `confidence`, `totalFiles`, `includedFiles`, `droppedFiles`, and `perModule`.
46
+ - **`--diff` risk score**: each changed file is now classified LOW / MEDIUM / HIGH based on reverse-dependency BFS, public API exports, route status, and config-file status:
47
+ ```
48
+ [sigmap] Risk: Changed files (3):
49
+ src/auth/service.ts [HIGH] — exports public API, 5 downstream dependents
50
+ src/config/database.ts [MEDIUM] — config file
51
+ src/utils/format.ts [LOW] — no dependents, internal utility
52
+ ```
53
+ - **Coverage in post-run summary**: every normal run now prints a `Coverage` line:
54
+ ```
55
+ Coverage : A (97%) — 76 of 78 source files included
56
+ ```
57
+ - **Coverage in `--health` and `--health --json`**: coverage grade, score, and file counts are included in both text and JSON health output. `--health --json` adds `coverage`, `coverageGrade`, `coverageConfidence`, `coverageTotalFiles`, `coverageIncludedFiles`.
58
+
59
+ ### Changed
60
+ - **Token budget drop order step 5**: now uses `signalQuality = sigs / linesOfCode` (least-informative files dropped first) instead of the previous "fewest sigs" heuristic.
61
+ - **`src/eval/analyzer.js` `analyzeFiles()` output**: each file stat now includes `linesOfCode` and `signalQuality` fields.
62
+
63
+ ### Benchmark (v4.0.0)
64
+ - Token reduction: **97.6% average** across 18 repos (target ≥ 97.0%) ✅
65
+ - Retrieval hit@5: 83.3% (retrieval improvement targeted in v4.5 with adaptive query)
66
+
67
+ ---
68
+
13
69
  ## [3.5.0] — 2026-04-14 — Phase C/D Intelligence Expansion
14
70
 
15
71
  ### Added
package/README.md CHANGED
@@ -19,7 +19,7 @@
19
19
  npx sigmap # 10 seconds. zero config. your AI never reads the wrong file again.
20
20
  ```
21
21
 
22
- > Latest: **v3.5.0** adds Phase C/D intelligence expansion with framework-specialized extractors and cross-module pattern detection.
22
+ > Latest: **v4.0.0** Intelligence Layer. Coverage score, confidence indicators in every output file, `--report` module heatmap, `--diff` risk scoring, and extractor quality-based drop order.
23
23
 
24
24
  <div align="center">
25
25
  <img src="demo.gif" alt="SigMap demo — reducing 80K tokens to 4K in under 10 seconds" width="760" />
@@ -145,7 +145,7 @@ Reproduced with `node scripts/run-benchmark.mjs` on public repos:
145
145
  | fastify | JavaScript | 54.4K | 2.6K | **95.3%** |
146
146
  | fastapi | Python | 178.4K | 5.2K | **97.1%** |
147
147
 
148
- **Average: 97.5% reduction across 18 repos (16 languages).** See [`benchmarks/reports/token-reduction.md`](benchmarks/reports/token-reduction.md) or reproduce with `node scripts/run-benchmark.mjs`.
148
+ **Average: 97.6% reduction across 18 repos (16 languages).** See [`benchmarks/reports/token-reduction.md`](benchmarks/reports/token-reduction.md) or reproduce with `node scripts/run-benchmark.mjs`.
149
149
 
150
150
  ---
151
151
 
@@ -746,17 +746,89 @@ If `output` is omitted, the default `.github/copilot-instructions.md` is used.
746
746
 
747
747
  ## 📊 Observability
748
748
 
749
+ ### Coverage score (v4.0)
750
+
751
+ Every run now prints a coverage line alongside token reduction:
752
+
753
+ ```
754
+ ───────────────────────────────────────────
755
+ SigMap v4.0.0
756
+ Files scanned : 76
757
+ Symbols found : 332
758
+ Token reduction: 94% (65,227 → 4,103)
759
+ Coverage : A (97%) — 76 of 78 source files included
760
+ Output : .github/copilot-instructions.md
761
+ ───────────────────────────────────────────
762
+ ```
763
+
764
+ The **coverage score** answers _how much of your codebase is represented in context_ after the token budget is applied. Grade scale: A ≥ 90% · B ≥ 75% · C ≥ 50% · D < 50%.
765
+
766
+ ### Module heatmap in `--report`
767
+
749
768
  ```bash
750
- # Append run metrics to .context/usage.ndjson
751
- sigmap --track
769
+ sigmap --report
770
+ ```
752
771
 
753
- # Structured JSON report for CI (exits 1 if over budget)
772
+ ```
773
+ [sigmap] report:
774
+ version : 4.0.0
775
+ files processed : 76
776
+ reduction : 93.7%
777
+ coverage : A (97%) — 76 of 78 source files included
778
+ confidence : HIGH
779
+
780
+ Module Coverage:
781
+ src ████████████████ 100% (64/64 files)
782
+ packages ██████████████░░ 86% (12/14 files)
783
+ ```
784
+
785
+ Machine-readable JSON (suitable for CI dashboards):
786
+
787
+ ```bash
754
788
  sigmap --report --json
755
- # { "version": "2.0.0", "finalTokens": 3200, "reductionPct": 92.4, "overBudget": false }
789
+ # { "version": "4.0.0", "finalTokens": 4103, "reductionPct": 93.7,
790
+ # "coverage": { "score": 97, "grade": "A", "confidence": "HIGH", ... } }
791
+ ```
792
+
793
+ ### Composite health score
756
794
 
757
- # Composite health score
795
+ ```bash
758
796
  sigmap --health
759
- # score: 95/100 (grade A) | reduction: 91.2% | 1 day since regen | 47 runs
797
+ ```
798
+
799
+ ```
800
+ [sigmap] health:
801
+ score : 80/100 (grade B)
802
+ coverage : A (97%) — 76 of 78 source files
803
+ strategy : full
804
+ ...
805
+ ```
806
+
807
+ ```bash
808
+ sigmap --health --json
809
+ # { "score": 80, "grade": "B", "coverage": 97, "coverageGrade": "A",
810
+ # "tokens": 4103, "reduction": 93.7, ... }
811
+ ```
812
+
813
+ ### Confidence indicators in generated files
814
+
815
+ Every output file now carries a metadata line so you can inspect freshness at a glance:
816
+
817
+ ```
818
+ <!-- sigmap: version=4.0.0 confidence=HIGH coverage=97% dropped=2 commit=8540612 -->
819
+ ```
820
+
821
+ ### Diff risk score
822
+
823
+ ```bash
824
+ sigmap --diff HEAD~3
825
+ ```
826
+
827
+ ```
828
+ [sigmap] Risk: Changed files (4):
829
+ src/auth/service.ts [HIGH] — exports public API, 5 downstream dependents
830
+ src/config/database.ts [MEDIUM] — config file
831
+ src/utils/format.ts [LOW] — no dependents, internal utility
760
832
  ```
761
833
 
762
834
  ### Self-healing CI
@@ -1,11 +1,14 @@
1
1
  {
2
- "_comment": "SigMap configuration — all keys are optional (defaults shown)",
2
+ "_comment": "SigMap configuration — all keys are optional (defaults shown). Copy to gen-context.config.json.",
3
3
 
4
4
  "output": ".github/copilot-instructions.md",
5
5
 
6
- "outputs": ["copilot", "codex"],
6
+ "outputs": ["copilot", "codex", "claude", "gemini"],
7
7
 
8
- "srcDirs": ["src", "app", "lib", "packages", "services", "api"],
8
+ "_srcDirs_comment": "Source directories to scan. OMIT this key to use auto-detection (recommended).",
9
+ "_srcDirs_comment2": "Auto-detection reads package.json/go.mod/Cargo.toml and scans top-level dirs automatically.",
10
+ "_srcDirs_comment3": "Only set this explicitly if auto-detection misses part of your project layout.",
11
+ "_srcDirs_example": ["src", "app", "lib", "packages", "services", "api"],
9
12
 
10
13
  "exclude": [
11
14
  "node_modules", ".git", "dist", "build", "out",
package/gen-context.js CHANGED
@@ -116,14 +116,98 @@ __factories["./src/config/defaults"] = function(module, exports) {
116
116
 
117
117
  // ── ./src/config/loader ──
118
118
  __factories["./src/config/loader"] = function(module, exports) {
119
-
119
+
120
120
  const fs = require('fs');
121
121
  const path = require('path');
122
122
  const { DEFAULTS } = __require('./src/config/defaults');
123
-
123
+
124
124
  // Keys that are valid in gen-context.config.json
125
125
  const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
126
-
126
+
127
+ // Common top-level folder names that reliably hold source code
128
+ const COMMON_CODE_DIRS = new Set([
129
+ 'src', 'app', 'lib', 'packages', 'services', 'api', 'core', 'cmd',
130
+ 'internal', 'pkg', 'handlers', 'controllers', 'models', 'views',
131
+ 'components', 'pages', 'routes', 'middleware', 'utils', 'helpers',
132
+ 'modules', 'plugins', 'extensions', 'adapters', 'drivers',
133
+ 'hooks', 'composables', 'stores', 'features', 'domain', 'infra',
134
+ 'infrastructure', 'application', 'data', 'Sources', 'Tests',
135
+ ]);
136
+
137
+ const SUPPORTED_CODE_EXTS = new Set([
138
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
139
+ '.py', '.pyw', '.java', '.kt', '.kts', '.go', '.rs', '.cs',
140
+ '.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.rake', '.php',
141
+ '.swift', '.dart', '.scala', '.sc', '.vue', '.svelte',
142
+ '.html', '.htm', '.css', '.scss', '.sass', '.less',
143
+ '.yml', '.yaml', '.sh', '.bash', '.zsh', '.fish',
144
+ '.sql', '.graphql', '.gql', '.tf', '.tfvars', '.proto',
145
+ '.toml', '.properties', '.xml', '.md',
146
+ ]);
147
+
148
+ function detectAutoSrcDirs(cwd, excludeList) {
149
+ const excludeSet = new Set(excludeList || []);
150
+ const candidates = new Set(DEFAULTS.srcDirs);
151
+
152
+ // Manifest-based detection
153
+ const pkgPath = path.join(cwd, 'package.json');
154
+ if (fs.existsSync(pkgPath)) {
155
+ try {
156
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
157
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
158
+ if (allDeps.react || allDeps.next)
159
+ for (const d of ['src', 'app', 'pages', 'components', 'hooks', 'lib', 'utils']) candidates.add(d);
160
+ if (allDeps['@angular/core'])
161
+ for (const d of ['src', 'projects', 'apps', 'libs']) candidates.add(d);
162
+ if (allDeps['@nestjs/core'])
163
+ for (const d of ['src', 'libs', 'apps']) candidates.add(d);
164
+ if (allDeps.vue)
165
+ for (const d of ['src', 'components', 'views', 'stores', 'composables', 'plugins']) candidates.add(d);
166
+ if (allDeps.svelte || allDeps['@sveltejs/kit'])
167
+ for (const d of ['src', 'lib', 'routes']) candidates.add(d);
168
+ if (allDeps.nx || allDeps.turbo || allDeps.lerna || pkg.workspaces)
169
+ for (const d of ['packages', 'apps', 'libs', 'services']) candidates.add(d);
170
+ } catch (_) {}
171
+ }
172
+ if (fs.existsSync(path.join(cwd, 'pyproject.toml')) || fs.existsSync(path.join(cwd, 'requirements.txt')) || fs.existsSync(path.join(cwd, 'setup.py')))
173
+ for (const d of ['src', 'app', 'apps', 'tests', 'examples', 'instance', 'blueprints']) candidates.add(d);
174
+ if (fs.existsSync(path.join(cwd, 'Gemfile')))
175
+ for (const d of ['app', 'lib', 'config', 'db', 'spec', 'test']) candidates.add(d);
176
+ if (fs.existsSync(path.join(cwd, 'composer.json')))
177
+ for (const d of ['app', 'resources', 'routes', 'database', 'tests']) candidates.add(d);
178
+ if (fs.existsSync(path.join(cwd, 'go.mod')))
179
+ for (const d of ['cmd', 'internal', 'pkg', 'api', 'handler', 'handlers', 'middleware', 'service']) candidates.add(d);
180
+ if (fs.existsSync(path.join(cwd, 'Cargo.toml')))
181
+ for (const d of ['src', 'crates', 'examples', 'tests', 'benches']) candidates.add(d);
182
+ if (fs.existsSync(path.join(cwd, 'pubspec.yaml')))
183
+ for (const d of ['lib', 'test', 'integration_test', 'example', 'bin']) candidates.add(d);
184
+ if (fs.existsSync(path.join(cwd, 'Package.swift')))
185
+ for (const d of ['Sources', 'Tests']) candidates.add(d);
186
+
187
+ // Top-level directory scan
188
+ try {
189
+ const entries = fs.readdirSync(cwd, { withFileTypes: true });
190
+ for (const entry of entries) {
191
+ if (!entry.isDirectory() || entry.name.startsWith('.') || excludeSet.has(entry.name)) continue;
192
+ const lname = entry.name.toLowerCase();
193
+ if (COMMON_CODE_DIRS.has(entry.name) || COMMON_CODE_DIRS.has(lname)) { candidates.add(entry.name); continue; }
194
+ const dirPath = path.join(cwd, entry.name);
195
+ try {
196
+ const subs = fs.readdirSync(dirPath, { withFileTypes: true });
197
+ if (subs.some(s => s.isFile() && (SUPPORTED_CODE_EXTS.has(path.extname(s.name).toLowerCase()) || s.name === 'Dockerfile'))) {
198
+ candidates.add(entry.name); continue;
199
+ }
200
+ if (subs.some(s => s.isDirectory() && ['src', 'lib', 'main', 'java', 'kotlin', 'scala', 'python'].includes(s.name)))
201
+ candidates.add(entry.name);
202
+ } catch (_) {}
203
+ }
204
+ } catch (_) {}
205
+
206
+ return Array.from(candidates).filter(d => {
207
+ try { return fs.statSync(path.join(cwd, d)).isDirectory(); } catch (_) { return false; }
208
+ });
209
+ }
210
+
127
211
  /**
128
212
  * Load and merge configuration for a given working directory.
129
213
  *
@@ -133,18 +217,24 @@ __factories["./src/config/loader"] = function(module, exports) {
133
217
  function loadConfig(cwd) {
134
218
  const configPath = path.join(cwd, 'gen-context.config.json');
135
219
  if (!fs.existsSync(configPath)) {
136
- return deepClone(DEFAULTS);
220
+ const cfg = deepClone(DEFAULTS);
221
+ const detected = detectAutoSrcDirs(cwd, cfg.exclude);
222
+ if (detected.length > 0) cfg.srcDirs = detected;
223
+ return cfg;
137
224
  }
138
-
225
+
139
226
  let userConfig;
140
227
  try {
141
228
  const raw = fs.readFileSync(configPath, 'utf8');
142
229
  userConfig = JSON.parse(raw);
143
230
  } catch (err) {
144
231
  console.warn(`[sigmap] config parse error in ${configPath}: ${err.message}`);
145
- return deepClone(DEFAULTS);
232
+ const cfg = deepClone(DEFAULTS);
233
+ const detected = detectAutoSrcDirs(cwd, cfg.exclude);
234
+ if (detected.length > 0) cfg.srcDirs = detected;
235
+ return cfg;
146
236
  }
147
-
237
+
148
238
  // Warn on unknown keys (helps catch typos)
149
239
  for (const key of Object.keys(userConfig)) {
150
240
  if (key.startsWith('_')) continue; // allow _comment etc.
@@ -152,7 +242,7 @@ __factories["./src/config/loader"] = function(module, exports) {
152
242
  console.warn(`[sigmap] unknown config key: "${key}" (ignored)`);
153
243
  }
154
244
  }
155
-
245
+
156
246
  // Deep merge: top-level known keys from user override defaults
157
247
  // For object values (e.g. mcp), merge one level deep
158
248
  const merged = deepClone(DEFAULTS);
@@ -167,6 +257,13 @@ __factories["./src/config/loader"] = function(module, exports) {
167
257
  merged[key] = val;
168
258
  }
169
259
  }
260
+
261
+ // If user didn't specify srcDirs, auto-detect; fall back to DEFAULTS if nothing found
262
+ if (!Array.isArray(userConfig.srcDirs)) {
263
+ const detected = detectAutoSrcDirs(cwd, merged.exclude);
264
+ merged.srcDirs = detected.length > 0 ? detected : deepClone(DEFAULTS.srcDirs);
265
+ }
266
+
170
267
  // Backward compat (v3.0+): if user specified 'adapters', use it as 'outputs' too.
171
268
  // If user specified only 'outputs' (old configs), mirror to 'adapters'.
172
269
  if (merged.adapters && !Array.isArray(merged.adapters)) merged.adapters = null;
@@ -177,13 +274,13 @@ __factories["./src/config/loader"] = function(module, exports) {
177
274
  }
178
275
  return merged;
179
276
  }
180
-
277
+
181
278
  function deepClone(obj) {
182
279
  return JSON.parse(JSON.stringify(obj));
183
280
  }
184
-
185
- module.exports = { loadConfig };
186
-
281
+
282
+ module.exports = { loadConfig, detectAutoSrcDirs };
283
+
187
284
  };
188
285
 
189
286
  // ── ./src/extractors/cpp ──
@@ -4478,7 +4575,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
4478
4575
 
4479
4576
  const SERVER_INFO = {
4480
4577
  name: 'sigmap',
4481
- version: '3.5.0',
4578
+ version: '4.0.1',
4482
4579
  description: 'SigMap MCP server — code signatures on demand',
4483
4580
  };
4484
4581
 
@@ -6040,7 +6137,7 @@ const path = require('path');
6040
6137
  const os = require('os');
6041
6138
  const { execSync } = require('child_process');
6042
6139
 
6043
- const VERSION = '3.6.0';
6140
+ const VERSION = '4.0.1';
6044
6141
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
6045
6142
 
6046
6143
  function requireSourceOrBundled(key) {
@@ -6276,13 +6373,17 @@ function applyTokenBudget(fileEntries, maxTokens) {
6276
6373
  else if (isTestFile(e.filePath)) { priority = 8; dropReason = 'budget: test file'; }
6277
6374
  else if (isConfigFile(e.filePath)) { priority = 6; dropReason = 'budget: config file'; }
6278
6375
  else priority = 4;
6279
- return { ...e, priority, dropReason };
6376
+ // v4.0: signal quality = sigs per line-of-code (higher = more informative)
6377
+ const loc = e.content ? e.content.split('\n').length : 1;
6378
+ const signalQuality = loc > 0 ? (e.sigs ? e.sigs.length : 0) / loc : 0;
6379
+ return { ...e, priority, dropReason, signalQuality };
6280
6380
  });
6281
6381
 
6282
- // Within same priority, sort by mtime ascending (oldest first = drop first)
6382
+ // Within same priority: sort by mtime ascending (oldest first), then signalQuality ascending (least informative first)
6283
6383
  withPriority.sort((a, b) => {
6284
6384
  if (b.priority !== a.priority) return b.priority - a.priority;
6285
- return (a.mtime || 0) - (b.mtime || 0);
6385
+ if ((a.mtime || 0) !== (b.mtime || 0)) return (a.mtime || 0) - (b.mtime || 0);
6386
+ return (a.signalQuality || 0) - (b.signalQuality || 0);
6286
6387
  });
6287
6388
 
6288
6389
  const kept = [];
@@ -6716,11 +6817,17 @@ function writeClaude(content, cwd) {
6716
6817
  // ---------------------------------------------------------------------------
6717
6818
  // Report
6718
6819
  // ---------------------------------------------------------------------------
6719
- function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson, budgetLimit) {
6820
+ function _coverageBar(pct, width) {
6821
+ width = width || 16;
6822
+ const filled = Math.round(pct / 100 * width);
6823
+ return '\u2588'.repeat(filled) + '\u2591'.repeat(width - filled);
6824
+ }
6825
+
6826
+ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson, budgetLimit, coverageResult) {
6720
6827
  const reduction = inputTokens > 0 ? (100 - (finalTokens / inputTokens) * 100).toFixed(1) : 0;
6721
6828
  const overBudget = finalTokens > (budgetLimit || 6000);
6722
6829
  if (asJson) {
6723
- process.stdout.write(JSON.stringify({
6830
+ const payload = {
6724
6831
  version: VERSION,
6725
6832
  timestamp: new Date().toISOString(),
6726
6833
  rawTokens: inputTokens,
@@ -6731,7 +6838,22 @@ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson,
6731
6838
  reductionPct: parseFloat(reduction),
6732
6839
  overBudget,
6733
6840
  budgetLimit: budgetLimit || 6000,
6734
- }) + '\n');
6841
+ };
6842
+ if (coverageResult) {
6843
+ payload.coverage = {
6844
+ score: coverageResult.score,
6845
+ grade: coverageResult.grade,
6846
+ confidence: coverageResult.confidence,
6847
+ totalFiles: coverageResult.total,
6848
+ includedFiles: coverageResult.included,
6849
+ droppedFiles: coverageResult.dropped,
6850
+ perModule: Object.fromEntries(
6851
+ Array.from(coverageResult.perModule.entries())
6852
+ .map(([k, v]) => [k, v])
6853
+ ),
6854
+ };
6855
+ }
6856
+ process.stdout.write(JSON.stringify(payload) + '\n');
6735
6857
  // Exit 1 in CI if over budget — lets pipelines fail fast
6736
6858
  if (overBudget) process.exitCode = 1;
6737
6859
  } else {
@@ -6743,6 +6865,20 @@ function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson,
6743
6865
  console.log(` output tokens : ~${finalTokens}`);
6744
6866
  console.log(` budget limit : ${budgetLimit || 6000}`);
6745
6867
  console.log(` reduction : ${reduction}%`);
6868
+ if (coverageResult) {
6869
+ console.log(` coverage : ${coverageResult.grade} (${coverageResult.score}%) — ${coverageResult.included} of ${coverageResult.total} source files included`);
6870
+ console.log(` confidence : ${coverageResult.confidence}`);
6871
+ if (coverageResult.perModule && coverageResult.perModule.size > 0) {
6872
+ console.log('');
6873
+ console.log(' Module Coverage:');
6874
+ for (const [dir, mod] of coverageResult.perModule) {
6875
+ if (mod.total === 0) continue;
6876
+ const bar = _coverageBar(mod.pct);
6877
+ const attention = mod.pct < 50 ? ' \u2190 attention needed' : '';
6878
+ console.log(` ${dir.padEnd(18)} ${bar} ${String(mod.pct).padStart(3)}% (${mod.included}/${mod.total} files)${attention}`);
6879
+ }
6880
+ }
6881
+ }
6746
6882
  if (overBudget) console.warn(`[sigmap] WARNING: output (${finalTokens} tokens) exceeds budget (${budgetLimit || 6000})`);
6747
6883
  }
6748
6884
  }
@@ -7032,6 +7168,41 @@ function runDiff(cwd, config, stagedOnly, baseRef) {
7032
7168
  const scope = baseRef ? `diff-vs-${baseRef}` : (stagedOnly ? 'staged' : 'diff');
7033
7169
  console.warn(`[sigmap] ${scope} files: ${fileEntries.length}, diff tokens: ~${finalTokens}`);
7034
7170
 
7171
+ // v4.0: risk score per changed file
7172
+ try {
7173
+ const { buildFromCwd } = requireSourceOrBundled('./src/graph/builder');
7174
+ const graph = buildFromCwd(cwd, { silent: true });
7175
+ const reverseGraph = graph.reverse || new Map();
7176
+
7177
+ function _isRouteFile(f) {
7178
+ return /\/(routes?|pages?|controllers?|handlers?|api)\//i.test(f)
7179
+ || /\.(route|page|controller|handler)\.\w+$/.test(f);
7180
+ }
7181
+ function _riskScore(filePath, sigs, revGraph) {
7182
+ let s = 0;
7183
+ if (sigs.some(sig => sig.includes('export') || sig.includes('module.exports'))) s += 2;
7184
+ const deps = revGraph.get(filePath) || new Set();
7185
+ if (deps.size > 3) s += 2;
7186
+ if (_isRouteFile(filePath)) s += 1;
7187
+ if (/\.(config|env|settings)\.\w+$/.test(filePath)) s += 1;
7188
+ return s >= 4 ? 'HIGH' : s >= 2 ? 'MEDIUM' : 'LOW';
7189
+ }
7190
+
7191
+ console.warn(`[sigmap] Risk: Changed files (${fileEntries.length}):`);
7192
+ for (const fe of fileEntries) {
7193
+ const level = _riskScore(fe.filePath, fe.sigs, reverseGraph);
7194
+ const rel = path.relative(cwd, fe.filePath).replace(/\\/g, '/');
7195
+ const deps = reverseGraph.get(fe.filePath) || new Set();
7196
+ const reasons = [];
7197
+ if (fe.sigs.some(s => s.includes('export') || s.includes('module.exports'))) reasons.push('exports public API');
7198
+ if (deps.size > 0) reasons.push(`${deps.size} downstream dependent${deps.size === 1 ? '' : 's'}`);
7199
+ if (_isRouteFile(fe.filePath)) reasons.push('route file');
7200
+ if (/\.(config|env|settings)\.\w+$/.test(fe.filePath)) reasons.push('config file');
7201
+ const label = level === 'HIGH' ? '[HIGH] ' : level === 'MEDIUM' ? '[MEDIUM]' : '[LOW] ';
7202
+ console.warn(` ${rel.padEnd(40)} ${label}${reasons.length ? ' — ' + reasons.join(', ') : ''}`);
7203
+ }
7204
+ } catch (_) {}
7205
+
7035
7206
  if (process.argv.includes('--report')) {
7036
7207
  // Also show what the full run would cost for comparison
7037
7208
  const fullResult = runGenerate(cwd, config, true);
@@ -7176,11 +7347,17 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7176
7347
  const droppedCount = beforeCount - budgeted.length;
7177
7348
  const content = formatOutput(budgeted, cwd, false, config, null);
7178
7349
  const finalTokens = estimateTokens(content);
7179
- result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount };
7350
+ // v4.0: compute coverage score for --report heatmap
7351
+ let coverageResult = null;
7352
+ try {
7353
+ const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
7354
+ coverageResult = coverageScore(cwd, budgeted, config);
7355
+ } catch (_) {}
7356
+ result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount, coverageResult };
7180
7357
  }
7181
7358
 
7182
7359
  if (reportMode || process.argv.includes('--report')) {
7183
- printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, config.maxTokens);
7360
+ printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, config.maxTokens, result.coverageResult);
7184
7361
  }
7185
7362
 
7186
7363
  // Usage tracking (v0.9) — optional append-only NDJSON log
@@ -7212,17 +7389,26 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
7212
7389
  const pct = result.inputTokenTotal > 0
7213
7390
  ? Math.round((1 - result.finalTokens / result.inputTokenTotal) * 100)
7214
7391
  : 0;
7215
- process.stderr.write([
7392
+ // v4.0: coverage score in post-run summary
7393
+ let coverageLine = '';
7394
+ try {
7395
+ const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
7396
+ const cov = coverageScore(cwd, fileEntries, config);
7397
+ coverageLine = ` Coverage : ${cov.grade} (${cov.score}%) \u2014 ${cov.included} of ${cov.total} source files included`;
7398
+ } catch (_) {}
7399
+ const lines = [
7216
7400
  bar,
7217
7401
  ` SigMap v${VERSION}`,
7218
7402
  ` Files scanned : ${result.fileCount}`,
7219
7403
  ` Symbols found : ${syms.toLocaleString()}`,
7220
7404
  ` 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'));
7405
+ ];
7406
+ if (coverageLine) lines.push(coverageLine);
7407
+ lines.push(` Output : .github/copilot-instructions.md`);
7408
+ lines.push(bar);
7409
+ lines.push(` Try: "explain the architecture" \u00b7 "find the auth module"`);
7410
+ lines.push(bar, '');
7411
+ process.stderr.write(lines.join('\n'));
7226
7412
  }
7227
7413
 
7228
7414
  return result;
@@ -7725,6 +7911,17 @@ function main() {
7725
7911
  if (args.includes('--health')) {
7726
7912
  const { score } = __require('./src/health/scorer');
7727
7913
  const result = score(cwd);
7914
+ // v4.0: compute live coverage score to include in health output
7915
+ let coverageResult = null;
7916
+ try {
7917
+ const { coverageScore } = requireSourceOrBundled('./src/analysis/coverage-score');
7918
+ const { loadConfig: lc } = requireSourceOrBundled('./src/config/loader');
7919
+ const cfg = lc(cwd);
7920
+ // Use all files from srcDirs as proxies for "included" (no budget applied in health mode)
7921
+ const allFiles = buildFileList(cwd, cfg);
7922
+ const fakeEntries = allFiles.map(f => ({ filePath: f }));
7923
+ coverageResult = coverageScore(cwd, fakeEntries, cfg);
7924
+ } catch (_) {}
7728
7925
  if (args.includes('--json')) {
7729
7926
  // Feature 3 (VS Code) + Feature 5 (JetBrains): emit tokens + reduction for plugins
7730
7927
  const ctxPath = path.join(cwd, '.github', 'copilot-instructions.md');
@@ -7733,10 +7930,21 @@ function main() {
7733
7930
  if (fs.existsSync(ctxPath)) {
7734
7931
  try { tokens = estimateTokens(fs.readFileSync(ctxPath, 'utf8')); } catch (_) {}
7735
7932
  }
7736
- process.stdout.write(JSON.stringify({ ...result, tokens, reduction }) + '\n');
7933
+ const payload = { ...result, tokens, reduction };
7934
+ if (coverageResult) {
7935
+ payload.coverage = coverageResult.score;
7936
+ payload.coverageGrade = coverageResult.grade;
7937
+ payload.coverageConfidence = coverageResult.confidence;
7938
+ payload.coverageTotalFiles = coverageResult.total;
7939
+ payload.coverageIncludedFiles = coverageResult.included;
7940
+ }
7941
+ process.stdout.write(JSON.stringify(payload) + '\n');
7737
7942
  } else {
7738
7943
  console.log('[sigmap] health:');
7739
7944
  console.log(` score : ${result.score}/100 (grade ${result.grade})`);
7945
+ if (coverageResult) {
7946
+ console.log(` coverage : ${coverageResult.grade} (${coverageResult.score}%) — ${coverageResult.included} of ${coverageResult.total} source files`);
7947
+ }
7740
7948
  console.log(` strategy : ${result.strategy}`);
7741
7949
  console.log(` token reduction : ${result.tokenReductionPct !== null ? result.tokenReductionPct + '%' : 'no history'}`);
7742
7950
  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.1",
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.1",
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.1",
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.1',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24