sigmap 2.3.0 → 2.5.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/CHANGELOG.md +55 -0
- package/README.md +91 -16
- package/gen-context.js +248 -3
- package/package.json +7 -1
- package/packages/cli/index.js +63 -0
- package/packages/cli/package.json +26 -0
- package/packages/core/README.md +141 -0
- package/packages/core/index.js +215 -0
- package/packages/core/package.json +28 -0
- package/src/config/defaults.js +8 -0
- package/src/graph/builder.js +259 -0
- package/src/graph/impact.js +235 -0
- package/src/mcp/handlers.js +21 -1
- package/src/mcp/server.js +3 -2
- package/src/mcp/tools.js +24 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,61 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [2.6.0] — upcoming · [#16](https://github.com/manojmallick/sigmap/issues/16) · branch: `feat/v2.6-research-mode`
|
|
10
|
+
|
|
11
|
+
### Planned additions
|
|
12
|
+
- **`benchmarks/repos/`** — register 5 real open-source repos (express, flask, gin, spring-petclinic, rails) as git submodules or clone targets for evaluation
|
|
13
|
+
- **`benchmarks/tasks/retrieval-real.jsonl`** — 50 real evaluation tasks across all 5 repos; structured JSONL format compatible with the v2.1 benchmark runner
|
|
14
|
+
- **`--benchmark --repo <path>` CLI flag** — run benchmark against external repository; supports any git-cloned project
|
|
15
|
+
- **`--report --paper` CLI flag** — generates `benchmarks/reports/paper-metrics.md`: token reduction table (baseline vs SigMap per repo), hit@5 and MRR per repo, latency table (p50, p95, p99 in ms), LaTeX-ready table block for copy-paste into academic papers
|
|
16
|
+
- **`src/eval/paper.js`** — formats paper-ready markdown + LaTeX tables; zero dependencies
|
|
17
|
+
- **`test/integration/paper.test.js`** — 8 tests: `--report --paper` creates the report file, report contains all required sections, LaTeX table block present and syntactically valid, `--benchmark --repo <missing>` fails gracefully
|
|
18
|
+
|
|
19
|
+
### Go / No-go criteria
|
|
20
|
+
- All tests green (21 extractor + all integration suites)
|
|
21
|
+
- `--report --paper` generates a valid markdown file
|
|
22
|
+
- LaTeX table block present in report
|
|
23
|
+
- Overall hit@5 across all repos ≥ 0.75
|
|
24
|
+
- `--benchmark --repo .` completes in < 30 s on SigMap repo
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## [2.5.0] — upcoming · [#14](https://github.com/manojmallick/sigmap/issues/14) · branch: `feat/v2.5-impact-layer`
|
|
29
|
+
|
|
30
|
+
### Planned additions
|
|
31
|
+
- **`src/map/dep-graph.js`** — reverse-dependency graph built from the existing import-graph analysis. `getImpacted(filePath, graph)` walks the graph via BFS/DFS and returns every file that transitively imports the given file.
|
|
32
|
+
- **`src/map/impact.js`** — `analyzeImpact(changedFiles, cwd)` → `{ changed, impacted, totalFiles }`. Aggregates dep-graph traversal with optional signature lookup.
|
|
33
|
+
- **`--impact <file>` CLI flag** — prints all files impacted by changing `<file>`, with their signatures. Supports `--impact --json` (machine-readable) and `--impact --depth <n>` (BFS depth limit).
|
|
34
|
+
- **`get_impact` MCP tool** (9th tool) — `{ file: string, depth?: number }` → list of impacted files + signatures, usable live in any MCP session.
|
|
35
|
+
- **`impact` config key** — `{ depth: 0, includeSigs: true }` in `gen-context.config.json`.
|
|
36
|
+
- **`test/integration/impact.test.js`** — 15 tests: direct deps, transitive deps, circular deps (no infinite loop), depth limit, unknown file returns `[]`, JSON output shape, MCP tool shape.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## [2.4.0] — 2026-04-05
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
- **`packages/core/`** — new `sigmap-core` package exposing a stable programmatic API: `{ extract, rank, buildSigIndex, scan, score }`. Third-party tools can now `require('sigmap')` and use all extraction/retrieval/security/health APIs without spawning a CLI process.
|
|
44
|
+
- **`packages/cli/`** — new `sigmap-cli` thin wrapper that exposes `{ CLI_ENTRY, run }` for programmatic CLI invocation and forward-compat with the v3.0 adapter architecture.
|
|
45
|
+
- **`packages/core/README.md`** — full programmatic API reference with usage examples for all five exported functions.
|
|
46
|
+
- **`exports` field in `package.json`** — `require('sigmap')` resolves to `packages/core/index.js`; `require('sigmap/cli')` resolves to `packages/cli/index.js`.
|
|
47
|
+
- **`test/integration/core-api.test.js`** — 15 integration tests covering: all exports present, `extract` for JS/TS/Python, file-path extension detection, unknown language returns `[]`, never throws on bad input, `rank` with empty map, `rank` sorted shape, `scan` clean/redact, `score` shape, `buildSigIndex` returns Map, CLI `--version` backward compat, CLI `--help` no crash.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
- `package.json` `"version"` bumped to `2.4.0`.
|
|
51
|
+
- `package.json` `"files"` — added `"packages/"` so `sigmap-core` and `sigmap-cli` are published with the root package.
|
|
52
|
+
- `gen-context.js` `VERSION` constant bumped to `2.4.0`.
|
|
53
|
+
- `src/mcp/server.js` `SERVER_INFO.version` bumped to `2.4.0`.
|
|
54
|
+
|
|
55
|
+
### Validation gate
|
|
56
|
+
- 21/21 extractor unit tests passed
|
|
57
|
+
- 21/21 integration suites passed (0 failures, including new `core-api.test.js`)
|
|
58
|
+
- `node gen-context.js --version` → `2.4.0`
|
|
59
|
+
- `node -e "const { extract } = require('.'); console.log(extract('function hello(){}', 'javascript').length > 0 ? 'OK' : 'FAIL')"` → `OK`
|
|
60
|
+
- `require('sigmap')` works from any directory
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
9
64
|
## [2.3.0] — 2026-04-07
|
|
10
65
|
|
|
11
66
|
### Added
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
<!-- Status -->
|
|
13
13
|
[](https://www.npmjs.com/package/sigmap)
|
|
14
|
-
[](https://github.com/manojmallick/sigmap/tree/main/test)
|
|
15
15
|
[](package.json)
|
|
16
16
|
[](https://github.com/manojmallick/sigmap/commits/main)
|
|
17
17
|
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
| [VS Code extension](#-vs-code-extension) | Status bar, stale alerts, commands |
|
|
42
42
|
| [Languages supported](#-languages-supported) | 21 languages |
|
|
43
43
|
| [Context strategies](#-context-strategies) | full / per-module / hot-cold |
|
|
44
|
-
| [MCP server](#-mcp-server) |
|
|
44
|
+
| [MCP server](#-mcp-server) | 8 on-demand tools |
|
|
45
45
|
| [CLI reference](#-cli-reference) | All flags |
|
|
46
46
|
| [Configuration](#-configuration) | Config file + .contextignore |
|
|
47
47
|
| [Observability](#-observability) | Health score, reports, CI |
|
|
@@ -86,20 +86,49 @@ AI agent session starts with full context
|
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
|
89
|
-
##
|
|
89
|
+
## 🔭 What's next — v2.5-v2.6 (in progress · [#14](https://github.com/manojmallick/sigmap/issues/14) · [#16](https://github.com/manojmallick/sigmap/issues/16))
|
|
90
|
+
|
|
91
|
+
### v2.5 — Impact Layer
|
|
92
|
+
|
|
93
|
+
| Feature | Description |
|
|
94
|
+
|---|---|
|
|
95
|
+
| **`--impact <file>`** | Show every file that transitively depends on a changed file — instant blast-radius awareness |
|
|
96
|
+
| **`--impact --json`** | Machine-readable output for CI pipelines |
|
|
97
|
+
| **`get_impact` MCP tool** | 9th MCP tool — `{ file, depth? }` → impacted files + signatures |
|
|
98
|
+
| **`src/map/dep-graph.js`** | Reverse-dependency graph built from the import analysis; circular deps handled safely |
|
|
99
|
+
| **15 new tests** | `impact.test.js` — direct deps, transitive deps, depth limit, JSON output |
|
|
100
|
+
|
|
101
|
+
### v2.6 — Research Mode
|
|
102
|
+
|
|
103
|
+
| Feature | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| **`--benchmark --repo <path>`** | Run benchmarks against any external repository (express, flask, gin, spring-petclinic, rails) |
|
|
106
|
+
| **`--report --paper`** | Generate paper-ready metrics: markdown + LaTeX tables for academic publishing |
|
|
107
|
+
| **50 real eval tasks** | JSONL task file covering 5 real open-source repos — `benchmarks/tasks/retrieval-real.jsonl` |
|
|
108
|
+
| **`src/eval/paper.js`** | Zero-dependency LaTeX table formatter for token reduction, hit@5, MRR, latency (p50/p95/p99) |
|
|
109
|
+
| **8 new tests** | `paper.test.js` — report generation, LaTeX syntax validation, graceful failures |
|
|
110
|
+
|
|
111
|
+
## 🆕 What's new in 2.4
|
|
90
112
|
|
|
91
113
|
| Feature | Description |
|
|
92
114
|
|---|---|
|
|
93
|
-
| **
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
| **
|
|
97
|
-
| **Test coverage markers** | Per-function `✓`/`✗` hints by scanning test directories |
|
|
98
|
-
| **Structural diff mode** | `--diff <base-ref>` writes a signature-level diff section |
|
|
99
|
-
| **Impact radius hints** | Reverse dependency annotations (used by: ...) |
|
|
100
|
-
| **New helper extractors** | `deps.js`, `todos.js`, `coverage.js`, `prdiff.js` |
|
|
115
|
+
| **Programmatic API** | `require('sigmap')` — use `extract`, `rank`, `buildSigIndex`, `scan`, `score` directly, no CLI subprocess |
|
|
116
|
+
| **`packages/core/`** | New `sigmap-core` package with stable API surface for third-party integrations |
|
|
117
|
+
| **`packages/cli/`** | Thin `sigmap-cli` forward-compat shim for the v3.0 adapter architecture |
|
|
118
|
+
| **15 new tests** | `core-api.test.js` covers all exported functions, edge cases, and backward compat |
|
|
101
119
|
|
|
102
|
-
|
|
120
|
+
## 🆕 What's new in 2.3
|
|
121
|
+
|
|
122
|
+
| Feature | Description |
|
|
123
|
+
|---|---|
|
|
124
|
+
| **`--query "<text>"` CLI** | Rank all context files by relevance to a free-text query — scored table + top-3 signature blocks |
|
|
125
|
+
| **`--query --json`** | Machine-readable ranked results (`{ query, results[], totalResults }`) |
|
|
126
|
+
| **`--query --top <n>`** | Limit results (default 10, configurable via `retrieval.topK`) |
|
|
127
|
+
| **`query_context` MCP tool** | 8th MCP tool — `{ query, topK? }` returns ranked file list, usable live in any MCP session |
|
|
128
|
+
| **`--analyze` / `--diagnose-extractors`** | Per-file breakdown of sigs/tokens/extractor/coverage; self-tests all 21 extractors (v2.2) |
|
|
129
|
+
| **`--benchmark` / `--eval`** | Measure hit@5 and MRR retrieval quality against a JSONL task file (v2.1) |
|
|
130
|
+
|
|
131
|
+
> **Previous v2.0 additions:** enriched signatures, dependency map, TODO/FIXME section, test coverage markers, structural diff mode, impact radius hints. See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
103
132
|
|
|
104
133
|
---
|
|
105
134
|
|
|
@@ -258,7 +287,7 @@ Recently committed files are **hot** (auto-injected). Everything else is **cold*
|
|
|
258
287
|
|
|
259
288
|
## 🔌 MCP server
|
|
260
289
|
|
|
261
|
-
> Introduced in v0.3, expanded to
|
|
290
|
+
> Introduced in v0.3, expanded to 8 tools through v2.3.
|
|
262
291
|
|
|
263
292
|
Start the MCP server on stdio:
|
|
264
293
|
|
|
@@ -277,6 +306,7 @@ node gen-context.js --mcp
|
|
|
277
306
|
| `list_modules` | — | Token-count table of all top-level module directories |
|
|
278
307
|
| `create_checkpoint` | `{ summary: string }` | Write a session checkpoint to `.context/` |
|
|
279
308
|
| `get_routing` | — | Full model routing table |
|
|
309
|
+
| `query_context` | `{ query: string, topK?: number }` | Files ranked by relevance to the query (v2.3) |
|
|
280
310
|
|
|
281
311
|
Reads files on every call — no stale state, no restart needed.
|
|
282
312
|
|
|
@@ -296,6 +326,19 @@ node gen-context.js --diff Generate context for git-changed f
|
|
|
296
326
|
node gen-context.js --diff --staged Staged files only (pre-commit check)
|
|
297
327
|
node gen-context.js --mcp Start MCP server on stdio
|
|
298
328
|
|
|
329
|
+
node gen-context.js --query "<text>" Rank files by relevance to a query
|
|
330
|
+
node gen-context.js --query "<text>" --json Ranked results as JSON
|
|
331
|
+
node gen-context.js --query "<text>" --top <n> Limit results to top N files (default 10)
|
|
332
|
+
|
|
333
|
+
node gen-context.js --analyze Per-file breakdown (sigs / tokens / extractor / coverage)
|
|
334
|
+
node gen-context.js --analyze --json Analysis as JSON
|
|
335
|
+
node gen-context.js --analyze --slow Include extraction timing per file
|
|
336
|
+
node gen-context.js --diagnose-extractors Self-test all 21 extractors against fixtures
|
|
337
|
+
|
|
338
|
+
node gen-context.js --benchmark Run retrieval quality benchmark (hit@5 / MRR)
|
|
339
|
+
node gen-context.js --benchmark --json Benchmark results as JSON
|
|
340
|
+
node gen-context.js --eval Alias for --benchmark
|
|
341
|
+
|
|
299
342
|
node gen-context.js --report Token reduction stats
|
|
300
343
|
node gen-context.js --report --json Structured JSON report (exits 1 if over budget)
|
|
301
344
|
node gen-context.js --report --history Usage log summary
|
|
@@ -435,6 +478,31 @@ node gen-context.js --format cache
|
|
|
435
478
|
|
|
436
479
|
---
|
|
437
480
|
|
|
481
|
+
## 📦 Programmatic API (v2.4+)
|
|
482
|
+
|
|
483
|
+
Use SigMap as a library — no CLI subprocess needed:
|
|
484
|
+
|
|
485
|
+
```js
|
|
486
|
+
const { extract, rank, buildSigIndex, scan, score } = require('sigmap');
|
|
487
|
+
|
|
488
|
+
// Extract signatures from source code
|
|
489
|
+
const sigs = extract('function hello() {}', 'javascript');
|
|
490
|
+
|
|
491
|
+
// Build an index and rank files by query
|
|
492
|
+
const index = buildSigIndex('/path/to/project');
|
|
493
|
+
const results = rank('authentication middleware', index);
|
|
494
|
+
|
|
495
|
+
// Scan signatures for secrets before storing
|
|
496
|
+
const { safe, redacted } = scan(sigs, 'src/config.ts');
|
|
497
|
+
|
|
498
|
+
// Get a composite health score for a project
|
|
499
|
+
const health = score('/path/to/project');
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
📖 Full API reference: [packages/core/README.md](packages/core/README.md)
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
438
506
|
## 🧪 Testing
|
|
439
507
|
|
|
440
508
|
```bash
|
|
@@ -464,7 +532,7 @@ grep "require(" gen-context.js | grep -v "^.*//.*require"
|
|
|
464
532
|
|
|
465
533
|
# Gate 3 — MCP server responds correctly
|
|
466
534
|
echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node gen-context.js --mcp
|
|
467
|
-
# Expected: valid JSON with
|
|
535
|
+
# Expected: valid JSON with 8 tools
|
|
468
536
|
|
|
469
537
|
# Gate 4 — npm artifact is clean
|
|
470
538
|
npm pack --dry-run
|
|
@@ -481,9 +549,16 @@ sigmap/
|
|
|
481
549
|
├── gen-context.js ← PRIMARY ENTRY POINT — single file, zero deps
|
|
482
550
|
├── gen-project-map.js ← import graph, class hierarchy, route table
|
|
483
551
|
│
|
|
552
|
+
├── packages/
|
|
553
|
+
│ ├── core/ ← programmatic API — require('sigmap') (v2.4)
|
|
554
|
+
│ │ └── index.js ← extract, rank, buildSigIndex, scan, score
|
|
555
|
+
│ └── cli/ ← thin CLI wrapper / v3 compat shim (v2.4)
|
|
556
|
+
│
|
|
484
557
|
├── src/
|
|
485
558
|
│ ├── extractors/ ← 21 language extractors (one file per language)
|
|
486
|
-
│ ├──
|
|
559
|
+
│ ├── retrieval/ ← query-aware ranker + tokenizer (v2.3)
|
|
560
|
+
│ ├── eval/ ← benchmark runner + scorer (v2.1), analyzer (v2.2)
|
|
561
|
+
│ ├── mcp/ ← MCP stdio server — 8 tools
|
|
487
562
|
│ ├── security/ ← secret scanner — 10 patterns
|
|
488
563
|
│ ├── routing/ ← model routing hints
|
|
489
564
|
│ ├── tracking/ ← NDJSON usage logger
|
|
@@ -499,7 +574,7 @@ sigmap/
|
|
|
499
574
|
│ ├── fixtures/ ← one source file per language
|
|
500
575
|
│ ├── expected/ ← expected extractor output
|
|
501
576
|
│ ├── run.js ← zero-dep test runner
|
|
502
|
-
│ └── integration/ ←
|
|
577
|
+
│ └── integration/ ← 20 integration test files (304 tests)
|
|
503
578
|
│
|
|
504
579
|
├── docs/ ← documentation site (GitHub Pages)
|
|
505
580
|
│ ├── index.html ← homepage
|
package/gen-context.js
CHANGED
|
@@ -2462,6 +2462,182 @@ __factories["./src/map/route-table"] = function(module, exports) {
|
|
|
2462
2462
|
|
|
2463
2463
|
};
|
|
2464
2464
|
|
|
2465
|
+
// ── ./src/graph/builder ──
|
|
2466
|
+
__factories["./src/graph/builder"] = function(module, exports) {
|
|
2467
|
+
'use strict';
|
|
2468
|
+
const fs = require('fs');
|
|
2469
|
+
const path = require('path');
|
|
2470
|
+
const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
2471
|
+
const PY_EXTS = new Set(['.py', '.pyw']);
|
|
2472
|
+
const GO_EXTS = new Set(['.go']);
|
|
2473
|
+
const RS_EXTS = new Set(['.rs']);
|
|
2474
|
+
const JVM_EXTS = new Set(['.java', '.kt', '.kts', '.scala', '.sc']);
|
|
2475
|
+
const RB_EXTS = new Set(['.rb', '.rake']);
|
|
2476
|
+
function resolveJsPath(dir, importStr, fileSet) {
|
|
2477
|
+
const base = path.resolve(dir, importStr);
|
|
2478
|
+
const candidates = [base, base+'.ts', base+'.tsx', base+'.js', base+'.jsx', base+'.mjs', base+'.cjs', path.join(base,'index.ts'), path.join(base,'index.js')];
|
|
2479
|
+
for (const c of candidates) { if (fileSet.has(c)) return c; }
|
|
2480
|
+
return null;
|
|
2481
|
+
}
|
|
2482
|
+
function extractFileDeps(filePath, content, fileSet) {
|
|
2483
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
2484
|
+
const dir = path.dirname(filePath);
|
|
2485
|
+
const found = [];
|
|
2486
|
+
if (JS_EXTS.has(ext)) {
|
|
2487
|
+
const stripped = content.replace(/\/\/.*$/gm,'').replace(/\/\*[\s\S]*?\*\//g,'');
|
|
2488
|
+
const reEs = /(?:^|[\r\n])\s*import\s+(?:[^'";\r\n]*?\s+from\s+)?['"](\.[^'"]+)['"]/g;
|
|
2489
|
+
let m;
|
|
2490
|
+
while ((m = reEs.exec(stripped)) !== null) { const r = resolveJsPath(dir, m[1], fileSet); if (r) found.push(r); }
|
|
2491
|
+
const reCjs = /\brequire\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
|
|
2492
|
+
while ((m = reCjs.exec(stripped)) !== null) { const r = resolveJsPath(dir, m[1], fileSet); if (r) found.push(r); }
|
|
2493
|
+
}
|
|
2494
|
+
if (PY_EXTS.has(ext)) {
|
|
2495
|
+
const re = /^[ \t]*from\s+(\.+[\w.]*)\s+import/gm; let m;
|
|
2496
|
+
while ((m = re.exec(content)) !== null) {
|
|
2497
|
+
const dotCount = (m[1].match(/^\.+/)||[''])[0].length;
|
|
2498
|
+
const modPart = m[1].slice(dotCount).replace(/\./g,'/');
|
|
2499
|
+
let base = dir; for (let i=1;i<dotCount;i++) base=path.dirname(base);
|
|
2500
|
+
const candidate = modPart ? path.join(base,modPart+'.py') : null;
|
|
2501
|
+
if (candidate && fileSet.has(candidate)) found.push(candidate);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
if (GO_EXTS.has(ext)) {
|
|
2505
|
+
const imports = []; let m;
|
|
2506
|
+
const re = /import\s*\(\s*([\s\S]*?)\s*\)/g;
|
|
2507
|
+
const reInline = /import\s+"([^"]+)"/g;
|
|
2508
|
+
while ((m = re.exec(content)) !== null) { for (const imp of m[1].matchAll(/"([^"]+)"/g)) imports.push(imp[1]); }
|
|
2509
|
+
while ((m = reInline.exec(content)) !== null) imports.push(m[1]);
|
|
2510
|
+
for (const imp of imports) {
|
|
2511
|
+
const suffix = imp.split('/').pop();
|
|
2512
|
+
for (const f of fileSet) { if (f.endsWith(path.sep+suffix+'.go')||f.includes(path.sep+suffix+path.sep)) { found.push(f); break; } }
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
if (RS_EXTS.has(ext)) {
|
|
2516
|
+
const reMod = /^\s*(?:pub\s+)?mod\s+(\w+)\s*;/gm; let m;
|
|
2517
|
+
while ((m = reMod.exec(content)) !== null) {
|
|
2518
|
+
const c1 = path.join(dir,m[1]+'.rs'); if (fileSet.has(c1)) found.push(c1);
|
|
2519
|
+
const c2 = path.join(dir,m[1],'mod.rs'); if (fileSet.has(c2)) found.push(c2);
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
if (JVM_EXTS.has(ext)) {
|
|
2523
|
+
const re = /^\s*import\s+([\w.]+)\s*;?/gm; let m;
|
|
2524
|
+
while ((m = re.exec(content)) !== null) {
|
|
2525
|
+
const asPath = m[1].replace(/\./g,path.sep);
|
|
2526
|
+
for (const jvmExt of ['.java','.kt','.kts','.scala','.sc']) {
|
|
2527
|
+
for (const f of fileSet) { if (f.endsWith(asPath+jvmExt)) { found.push(f); break; } }
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
if (RB_EXTS.has(ext)) {
|
|
2532
|
+
const re = /^\s*require_relative\s+['"]([^'"]+)['"]/gm; let m;
|
|
2533
|
+
while ((m = re.exec(content)) !== null) {
|
|
2534
|
+
const base = path.resolve(dir,m[1]);
|
|
2535
|
+
const candidate = base.endsWith('.rb') ? base : base+'.rb';
|
|
2536
|
+
if (fileSet.has(candidate)) found.push(candidate);
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
return [...new Set(found)];
|
|
2540
|
+
}
|
|
2541
|
+
function build(files, cwd) {
|
|
2542
|
+
const fileSet = new Set(files.map((f) => path.resolve(f)));
|
|
2543
|
+
const forward = new Map(); const reverse = new Map();
|
|
2544
|
+
for (const f of fileSet) { if (!forward.has(f)) forward.set(f,[]); if (!reverse.has(f)) reverse.set(f,[]); }
|
|
2545
|
+
for (const filePath of fileSet) {
|
|
2546
|
+
let content; try { content = fs.readFileSync(filePath,'utf8'); } catch(_) { continue; }
|
|
2547
|
+
const deps = extractFileDeps(filePath, content, fileSet);
|
|
2548
|
+
if (deps.length > 0) {
|
|
2549
|
+
forward.set(filePath, deps);
|
|
2550
|
+
for (const dep of deps) { if (!reverse.has(dep)) reverse.set(dep,[]); reverse.get(dep).push(filePath); }
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
return { forward, reverse };
|
|
2554
|
+
}
|
|
2555
|
+
function buildFromCwd(cwd, opts) {
|
|
2556
|
+
const { srcDirs=['src','app','lib'], exclude=['node_modules','.git','dist','build'] } = opts||{};
|
|
2557
|
+
const excludeSet = new Set(exclude);
|
|
2558
|
+
function walkDir(dir,depth) {
|
|
2559
|
+
if (depth>8) return [];
|
|
2560
|
+
let entries; try { entries = fs.readdirSync(dir,{withFileTypes:true}); } catch(_) { return []; }
|
|
2561
|
+
const out=[];
|
|
2562
|
+
for (const e of entries) {
|
|
2563
|
+
if (excludeSet.has(e.name)||e.name.startsWith('.')) continue;
|
|
2564
|
+
const full = path.join(dir,e.name);
|
|
2565
|
+
if (e.isDirectory()) out.push(...walkDir(full,depth+1));
|
|
2566
|
+
else if (e.isFile()) {
|
|
2567
|
+
const ext = path.extname(e.name).toLowerCase();
|
|
2568
|
+
if (JS_EXTS.has(ext)||PY_EXTS.has(ext)||GO_EXTS.has(ext)||RS_EXTS.has(ext)||JVM_EXTS.has(ext)||RB_EXTS.has(ext)) out.push(full);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
return out;
|
|
2572
|
+
}
|
|
2573
|
+
const files=[];
|
|
2574
|
+
for (const sd of srcDirs) { const absDir=path.resolve(cwd,sd); if (fs.existsSync(absDir)) files.push(...walkDir(absDir,0)); }
|
|
2575
|
+
for (const rootFile of ['gen-context.js','index.js','main.js','app.js']) {
|
|
2576
|
+
const abs=path.resolve(cwd,rootFile); if (fs.existsSync(abs)) files.push(abs);
|
|
2577
|
+
}
|
|
2578
|
+
return build(files, cwd);
|
|
2579
|
+
}
|
|
2580
|
+
module.exports = { build, buildFromCwd, extractFileDeps };
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2583
|
+
// ── ./src/graph/impact ──
|
|
2584
|
+
__factories["./src/graph/impact"] = function(module, exports) {
|
|
2585
|
+
'use strict';
|
|
2586
|
+
const path = require('path');
|
|
2587
|
+
const { buildFromCwd } = __require('./src/graph/builder');
|
|
2588
|
+
const TEST_PATTERNS = [/[./\\](test|tests|spec|__tests__)[./\\]/,/\.(test|spec)\.[jt]sx?$/,/_test\.[jt]sx?$/,/_test\.py$/,/test_[^/\\]+\.py$/];
|
|
2589
|
+
const ROUTE_PATTERNS = [/router?\.[jt]sx?$/i,/routes?\.[jt]sx?$/i,/controller\.[jt]sx?$/i,/views?\.[jt]sx?$/i,/handlers?\.[jt]sx?$/i];
|
|
2590
|
+
function isTestFile(f) { return TEST_PATTERNS.some((re) => re.test(f.replace(/\\/g,'/'))); }
|
|
2591
|
+
function isRouteFile(f) { return ROUTE_PATTERNS.some((re) => re.test(f.replace(/\\/g,'/'))); }
|
|
2592
|
+
function bfs(startFile, reverseGraph, maxDepth) {
|
|
2593
|
+
const direct=new Set(); const transitive=new Set(); const visited=new Set([startFile]);
|
|
2594
|
+
const firstLevel = reverseGraph.get(startFile)||[];
|
|
2595
|
+
for (const f of firstLevel) { if (!visited.has(f)) { direct.add(f); visited.add(f); } }
|
|
2596
|
+
if (maxDepth===1) return {direct,transitive};
|
|
2597
|
+
let frontier=[...direct]; let depth=1;
|
|
2598
|
+
while (frontier.length>0 && (maxDepth===0||depth<maxDepth)) {
|
|
2599
|
+
const nextFrontier=[];
|
|
2600
|
+
for (const node of frontier) {
|
|
2601
|
+
const importers=reverseGraph.get(node)||[];
|
|
2602
|
+
for (const imp of importers) { if (!visited.has(imp)) { transitive.add(imp); visited.add(imp); nextFrontier.push(imp); } }
|
|
2603
|
+
}
|
|
2604
|
+
frontier=nextFrontier; depth++;
|
|
2605
|
+
}
|
|
2606
|
+
return {direct,transitive};
|
|
2607
|
+
}
|
|
2608
|
+
function getImpact(changedFile, graph, opts) {
|
|
2609
|
+
const {depth=0,cwd=process.cwd()} = opts||{};
|
|
2610
|
+
const absChanged = path.resolve(cwd,changedFile);
|
|
2611
|
+
if (!graph||!graph.reverse) return {changed:changedFile,direct:[],transitive:[],tests:[],routes:[],totalImpact:0};
|
|
2612
|
+
const {direct,transitive} = bfs(absChanged,graph.reverse,depth);
|
|
2613
|
+
const allImpacted=[...direct,...transitive];
|
|
2614
|
+
const tests=allImpacted.filter(isTestFile); const routes=allImpacted.filter(isRouteFile);
|
|
2615
|
+
const toRel=(f)=>path.relative(cwd,f).replace(/\\/g,'/');
|
|
2616
|
+
return {changed:toRel(absChanged),direct:[...direct].map(toRel),transitive:[...transitive].map(toRel),tests:tests.map(toRel),routes:routes.map(toRel),totalImpact:direct.size+transitive.size};
|
|
2617
|
+
}
|
|
2618
|
+
function analyzeImpact(changedFiles, cwd, opts) {
|
|
2619
|
+
const {depth=3}=opts||{};
|
|
2620
|
+
const files=Array.isArray(changedFiles)?changedFiles:[changedFiles];
|
|
2621
|
+
let graph;
|
|
2622
|
+
try { graph=buildFromCwd(cwd,opts); } catch(_) { graph={forward:new Map(),reverse:new Map()}; }
|
|
2623
|
+
return files.map((f)=>({file:f,impact:getImpact(f,graph,{depth,cwd})}));
|
|
2624
|
+
}
|
|
2625
|
+
function formatImpact(result) {
|
|
2626
|
+
const lines=[`## Impact: \`${result.changed}\``,``];
|
|
2627
|
+
if (result.direct.length===0&&result.transitive.length===0) { lines.push('_No files import this file — zero blast radius._'); return lines.join('\n'); }
|
|
2628
|
+
lines.push(`**Total impacted files:** ${result.totalImpact}`,``);
|
|
2629
|
+
if (result.direct.length>0) { lines.push('### Direct importers'); for (const f of result.direct) lines.push(`- \`${f}\``); lines.push(''); }
|
|
2630
|
+
if (result.transitive.length>0) { lines.push('### Transitive importers'); for (const f of result.transitive) lines.push(`- \`${f}\``); lines.push(''); }
|
|
2631
|
+
if (result.tests.length>0) { lines.push('### Affected tests'); for (const f of result.tests) lines.push(`- \`${f}\``); lines.push(''); }
|
|
2632
|
+
if (result.routes.length>0) { lines.push('### Affected routes / controllers'); for (const f of result.routes) lines.push(`- \`${f}\``); lines.push(''); }
|
|
2633
|
+
return lines.join('\n');
|
|
2634
|
+
}
|
|
2635
|
+
function formatImpactJSON(result) {
|
|
2636
|
+
return {changed:result.changed,direct:result.direct,transitive:result.transitive,tests:result.tests,routes:result.routes,totalImpact:result.totalImpact};
|
|
2637
|
+
}
|
|
2638
|
+
module.exports = { getImpact, analyzeImpact, formatImpact, formatImpactJSON };
|
|
2639
|
+
};
|
|
2640
|
+
|
|
2465
2641
|
// ── ./src/mcp/handlers ──
|
|
2466
2642
|
__factories["./src/mcp/handlers"] = function(module, exports) {
|
|
2467
2643
|
|
|
@@ -2895,7 +3071,19 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
|
|
|
2895
3071
|
}
|
|
2896
3072
|
}
|
|
2897
3073
|
|
|
2898
|
-
|
|
3074
|
+
function getImpact(args, cwd) {
|
|
3075
|
+
if (!args || !args.file) return 'Missing required argument: file';
|
|
3076
|
+
try {
|
|
3077
|
+
const { analyzeImpact, formatImpact } = __require('./src/graph/impact');
|
|
3078
|
+
const depth = Math.max(0, parseInt(args.depth, 10) || 3);
|
|
3079
|
+
const results = analyzeImpact(args.file, cwd, { depth });
|
|
3080
|
+
return results.map((r) => formatImpact(r.impact)).join('\n\n---\n\n');
|
|
3081
|
+
} catch (err) {
|
|
3082
|
+
return `_get_impact failed: ${err.message}_`;
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact };
|
|
2899
3087
|
};
|
|
2900
3088
|
|
|
2901
3089
|
// ── ./src/mcp/server ──
|
|
@@ -2915,7 +3103,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
2915
3103
|
|
|
2916
3104
|
const readline = require('readline');
|
|
2917
3105
|
const { TOOLS } = __require('./src/mcp/tools');
|
|
2918
|
-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext } = __require('./src/mcp/handlers');
|
|
3106
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact } = __require('./src/mcp/handlers');
|
|
2919
3107
|
|
|
2920
3108
|
const SERVER_INFO = {
|
|
2921
3109
|
name: 'sigmap',
|
|
@@ -2975,6 +3163,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
2975
3163
|
else if (name === 'explain_file') text = explainFile(args, cwd);
|
|
2976
3164
|
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
2977
3165
|
else if (name === 'query_context') text = queryContext(args, cwd);
|
|
3166
|
+
else if (name === 'get_impact') text = getImpact(args, cwd);
|
|
2978
3167
|
else {
|
|
2979
3168
|
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
2980
3169
|
return;
|
|
@@ -3178,6 +3367,30 @@ __factories["./src/mcp/tools"] = function(module, exports) {
|
|
|
3178
3367
|
required: ['query'],
|
|
3179
3368
|
},
|
|
3180
3369
|
},
|
|
3370
|
+
{
|
|
3371
|
+
name: 'get_impact',
|
|
3372
|
+
description:
|
|
3373
|
+
'Show every file that is impacted when a given file changes — direct importers, ' +
|
|
3374
|
+
'transitive importers, affected tests, and affected routes/controllers. ' +
|
|
3375
|
+
'Gives agents instant blast-radius awareness before making a change. ' +
|
|
3376
|
+
'Handles circular dependencies safely (no infinite loops).',
|
|
3377
|
+
inputSchema: {
|
|
3378
|
+
type: 'object',
|
|
3379
|
+
properties: {
|
|
3380
|
+
file: {
|
|
3381
|
+
type: 'string',
|
|
3382
|
+
description:
|
|
3383
|
+
'Relative path from the project root of the file that changed ' +
|
|
3384
|
+
'(e.g. "src/extractors/python.js"). Use forward slashes.',
|
|
3385
|
+
},
|
|
3386
|
+
depth: {
|
|
3387
|
+
type: 'number',
|
|
3388
|
+
description: 'BFS traversal depth limit (default: 3). Use 0 for unlimited.',
|
|
3389
|
+
},
|
|
3390
|
+
},
|
|
3391
|
+
required: ['file'],
|
|
3392
|
+
},
|
|
3393
|
+
},
|
|
3181
3394
|
];
|
|
3182
3395
|
|
|
3183
3396
|
module.exports = { TOOLS };
|
|
@@ -4091,7 +4304,7 @@ const path = require('path');
|
|
|
4091
4304
|
const os = require('os');
|
|
4092
4305
|
const { execSync } = require('child_process');
|
|
4093
4306
|
|
|
4094
|
-
const VERSION = '2.
|
|
4307
|
+
const VERSION = '2.5.0';
|
|
4095
4308
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
4096
4309
|
|
|
4097
4310
|
function requireSourceOrBundled(key) {
|
|
@@ -5307,6 +5520,9 @@ Usage:
|
|
|
5307
5520
|
node gen-context.js --query "<text>" Rank files by relevance to a query
|
|
5308
5521
|
node gen-context.js --query "<text>" --json Ranked results as JSON
|
|
5309
5522
|
node gen-context.js --query "<text>" --top <n> Limit results to top N files (default 10)
|
|
5523
|
+
node gen-context.js --impact <file> Show every file impacted by changing <file>
|
|
5524
|
+
node gen-context.js --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
|
|
5525
|
+
node gen-context.js --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
|
|
5310
5526
|
node gen-context.js --init Write example config + .contextignore scaffold
|
|
5311
5527
|
node gen-context.js --help Show this message
|
|
5312
5528
|
node gen-context.js --version Show version
|
|
@@ -5625,6 +5841,35 @@ function main() {
|
|
|
5625
5841
|
process.exit(0);
|
|
5626
5842
|
}
|
|
5627
5843
|
|
|
5844
|
+
// ── --impact <file> ────────────────────────────────────────────────────────
|
|
5845
|
+
if (args.includes('--impact')) {
|
|
5846
|
+
try {
|
|
5847
|
+
const impIdx = args.indexOf('--impact');
|
|
5848
|
+
const targetFile = (args[impIdx + 1] || '').trim();
|
|
5849
|
+
if (!targetFile || targetFile.startsWith('--')) {
|
|
5850
|
+
console.error('[sigmap] --impact requires a file path');
|
|
5851
|
+
console.error(' Example: node gen-context.js --impact src/extractors/python.js');
|
|
5852
|
+
process.exit(1);
|
|
5853
|
+
}
|
|
5854
|
+
const { analyzeImpact, formatImpact, formatImpactJSON } = requireSourceOrBundled('./src/graph/impact');
|
|
5855
|
+
const depthIdx = args.indexOf('--depth');
|
|
5856
|
+
const depth = depthIdx >= 0 ? Math.max(0, parseInt(args[depthIdx + 1], 10) || 3) : ((config && config.impact && config.impact.depth) || 3);
|
|
5857
|
+
const results = analyzeImpact(targetFile, cwd, { depth });
|
|
5858
|
+
if (args.includes('--json')) {
|
|
5859
|
+
const out = results.map((r) => formatImpactJSON(r.impact));
|
|
5860
|
+
process.stdout.write(JSON.stringify(out.length === 1 ? out[0] : out) + '\n');
|
|
5861
|
+
} else {
|
|
5862
|
+
for (const r of results) {
|
|
5863
|
+
process.stdout.write(formatImpact(r.impact) + '\n');
|
|
5864
|
+
}
|
|
5865
|
+
}
|
|
5866
|
+
} catch (err) {
|
|
5867
|
+
console.error(`[sigmap] impact error: ${err.message}`);
|
|
5868
|
+
process.exit(1);
|
|
5869
|
+
}
|
|
5870
|
+
process.exit(0);
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5628
5873
|
if (args.includes('--report')) {
|
|
5629
5874
|
if (args.includes('--history')) {
|
|
5630
5875
|
try {
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.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
|
+
"exports": {
|
|
7
|
+
".": "./packages/core/index.js",
|
|
8
|
+
"./cli": "./packages/cli/index.js",
|
|
9
|
+
"./core": "./packages/core/index.js"
|
|
10
|
+
},
|
|
6
11
|
"bin": {
|
|
7
12
|
"sigmap": "./gen-context.js",
|
|
8
13
|
"gen-context": "./gen-context.js",
|
|
@@ -26,6 +31,7 @@
|
|
|
26
31
|
"gen-context.js",
|
|
27
32
|
"gen-project-map.js",
|
|
28
33
|
"src/",
|
|
34
|
+
"packages/",
|
|
29
35
|
"README.md",
|
|
30
36
|
"LICENSE",
|
|
31
37
|
"CHANGELOG.md",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sigmap-cli — thin CLI wrapper around sigmap-core.
|
|
5
|
+
*
|
|
6
|
+
* This module is required by the root gen-context.js entry point.
|
|
7
|
+
* All --flag handling lives here; business logic lives in src/ or packages/core.
|
|
8
|
+
*
|
|
9
|
+
* NOTE: This file intentionally does NOT duplicate business logic.
|
|
10
|
+
* It re-exports the entry-point function from gen-context.js so that
|
|
11
|
+
* `require('sigmap-cli')` can be used by tooling that wraps SigMap.
|
|
12
|
+
*
|
|
13
|
+
* In v2.4 the root gen-context.js is kept fully intact for backward compat.
|
|
14
|
+
* packages/cli is a forward-compat shim for the v3.0 adapter architecture.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The CLI entry point path.
|
|
21
|
+
* External tools can use this to spawn the CLI as a child process.
|
|
22
|
+
*/
|
|
23
|
+
const CLI_ENTRY = path.resolve(__dirname, '..', '..', 'gen-context.js');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run the SigMap CLI programmatically with the given argv array.
|
|
27
|
+
*
|
|
28
|
+
* @param {string[]} [argv] - Arguments to pass (default: process.argv)
|
|
29
|
+
* @param {string} [cwd] - Working directory (default: process.cwd())
|
|
30
|
+
* @returns {void}
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const { run } = require('sigmap-cli');
|
|
34
|
+
* run(['--report'], '/path/to/project');
|
|
35
|
+
*/
|
|
36
|
+
function run(argv, cwd) {
|
|
37
|
+
const origArgv = process.argv;
|
|
38
|
+
const origCwd = process.cwd();
|
|
39
|
+
|
|
40
|
+
if (cwd) {
|
|
41
|
+
try { process.chdir(cwd); } catch (_) {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (argv) {
|
|
45
|
+
process.argv = [process.argv[0], CLI_ENTRY, ...argv];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
require(CLI_ENTRY);
|
|
50
|
+
} finally {
|
|
51
|
+
process.argv = origArgv;
|
|
52
|
+
if (cwd) {
|
|
53
|
+
try { process.chdir(origCwd); } catch (_) {}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
/** Absolute path to the gen-context.js entry point */
|
|
60
|
+
CLI_ENTRY,
|
|
61
|
+
/** Run the SigMap CLI programmatically */
|
|
62
|
+
run,
|
|
63
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sigmap-cli",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"sigmap",
|
|
8
|
+
"cli",
|
|
9
|
+
"ai-context",
|
|
10
|
+
"code-signatures"
|
|
11
|
+
],
|
|
12
|
+
"author": {
|
|
13
|
+
"name": "Manoj Mallick",
|
|
14
|
+
"url": "https://github.com/manojmallick"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/manojmallick/sigmap.git",
|
|
19
|
+
"directory": "packages/cli"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://manojmallick.github.io/sigmap/",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|