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 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
  [![npm version](https://img.shields.io/npm/v/sigmap?color=7c6af7&label=latest&logo=npm)](https://www.npmjs.com/package/sigmap)
14
- [![Tests](https://img.shields.io/badge/tests-262%20passing-22c55e)](https://github.com/manojmallick/sigmap/tree/main/test)
14
+ [![Tests](https://img.shields.io/badge/tests-340%20passing-22c55e)](https://github.com/manojmallick/sigmap/tree/main/test)
15
15
  [![Zero deps](https://img.shields.io/badge/dependencies-zero-22c55e)](package.json)
16
16
  [![Last commit](https://img.shields.io/github/last-commit/manojmallick/sigmap?color=7c6af7)](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) | 7 on-demand tools |
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
- ## 🆕 What's new in 2.0
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
- | **Enriched signatures** | Return types, type hints, and schema field collapse (Python `@dataclass` / `BaseModel`) |
94
- | **Dependency map** | Compact import dependency section at the top of output (~50–100 extra tokens) |
95
- | **TODO/FIXME section** | Auto-harvested TODO/FIXME/HACK/XXX comments (max 20 entries) |
96
- | **Recent changes section** | Git-based recent changes summary in output |
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
- Several v2 enhancements (deps map, TODOs, recent changes) are enabled by default. All v2 sections can be tuned or disabled via `gen-context.config.json`.
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 7 tools through v1.4.
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 7 tools
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
- │ ├── mcp/ MCP stdio server 7 tools
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/ ← 17 integration test files (241 tests)
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
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext };
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.3.0';
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.0",
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
+ }