sigmap 5.9.0 → 6.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/gen-context.js CHANGED
@@ -4520,6 +4520,71 @@ __factories["./src/map/route-table"] = function(module, exports) {
4520
4520
 
4521
4521
  };
4522
4522
 
4523
+ // ── ./src/cache/sig-cache ──
4524
+ __factories["./src/cache/sig-cache"] = function(module, exports) {
4525
+ 'use strict';
4526
+
4527
+ const fs = require('fs');
4528
+ const path = require('path');
4529
+
4530
+ const CACHE_FILE = '.sigmap-cache.json';
4531
+
4532
+ function cachePath(cwd) {
4533
+ return path.join(cwd, CACHE_FILE);
4534
+ }
4535
+
4536
+ function loadCache(cwd, currentVersion) {
4537
+ try {
4538
+ const raw = fs.readFileSync(cachePath(cwd), 'utf8');
4539
+ const data = JSON.parse(raw);
4540
+ if (data.sigmapVersion !== currentVersion) return new Map();
4541
+ return new Map(Object.entries(data.entries || {}));
4542
+ } catch (_) {
4543
+ return new Map();
4544
+ }
4545
+ }
4546
+
4547
+ function saveCache(cwd, currentVersion, cache) {
4548
+ try {
4549
+ const data = {
4550
+ sigmapVersion: currentVersion,
4551
+ entries: Object.fromEntries(cache),
4552
+ };
4553
+ fs.writeFileSync(cachePath(cwd), JSON.stringify(data), 'utf8');
4554
+ } catch (_) {}
4555
+ }
4556
+
4557
+ function getChangedFiles(files, cache) {
4558
+ const changed = [];
4559
+ const unchanged = [];
4560
+ for (const f of files) {
4561
+ try {
4562
+ const mtime = fs.statSync(f).mtimeMs;
4563
+ const cached = cache.get(f);
4564
+ if (!cached || cached.mtime !== mtime) {
4565
+ changed.push(f);
4566
+ } else {
4567
+ unchanged.push(f);
4568
+ }
4569
+ } catch (_) {
4570
+ changed.push(f);
4571
+ }
4572
+ }
4573
+ return { changed, unchanged };
4574
+ }
4575
+
4576
+ function updateCacheEntries(cache, extracted) {
4577
+ for (const { file, sigs } of extracted) {
4578
+ try {
4579
+ const mtime = fs.statSync(file).mtimeMs;
4580
+ cache.set(file, { mtime, sigs });
4581
+ } catch (_) {}
4582
+ }
4583
+ }
4584
+
4585
+ module.exports = { loadCache, saveCache, getChangedFiles, updateCacheEntries };
4586
+ };
4587
+
4523
4588
  // ── ./src/graph/builder ──
4524
4589
  __factories["./src/graph/builder"] = function(module, exports) {
4525
4590
  'use strict';
@@ -5291,7 +5356,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
5291
5356
 
5292
5357
  const SERVER_INFO = {
5293
5358
  name: 'sigmap',
5294
- version: '5.9.0',
5359
+ version: '6.0.1',
5295
5360
  description: 'SigMap MCP server — code signatures on demand',
5296
5361
  };
5297
5362
 
@@ -7009,7 +7074,7 @@ const path = require('path');
7009
7074
  const os = require('os');
7010
7075
  const { execSync } = require('child_process');
7011
7076
 
7012
- const VERSION = '5.9.0';
7077
+ const VERSION = '6.0.1';
7013
7078
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
7014
7079
 
7015
7080
  function requireSourceOrBundled(key) {
@@ -8844,10 +8909,30 @@ function main() {
8844
8909
  }
8845
8910
 
8846
8911
  const invokedFrom = process.cwd();
8847
- const cwd = resolveProjectRoot(invokedFrom);
8912
+
8913
+ // --cwd <dir>: restrict scanning to that directory instead of the project root
8914
+ const cwdFlagIdx = args.indexOf('--cwd');
8915
+ const cwdFlag = cwdFlagIdx !== -1 ? (args[cwdFlagIdx + 1] || '').trim() : null;
8916
+ if (cwdFlag !== null && (!cwdFlag || cwdFlag.startsWith('--'))) {
8917
+ console.error('[sigmap] --cwd requires a directory path');
8918
+ process.exit(1);
8919
+ }
8920
+ const cwd = cwdFlag
8921
+ ? path.resolve(invokedFrom, cwdFlag)
8922
+ : resolveProjectRoot(invokedFrom);
8848
8923
  const scriptPath = process.argv[1] || path.join(invokedFrom, 'gen-context.js');
8849
8924
 
8850
- if (cwd !== invokedFrom) {
8925
+ if (cwdFlag) {
8926
+ if (!fs.existsSync(cwd)) {
8927
+ console.error(`[sigmap] --cwd directory does not exist: ${cwd}`);
8928
+ process.exit(1);
8929
+ }
8930
+ if (!fs.statSync(cwd).isDirectory()) {
8931
+ console.error(`[sigmap] --cwd must be a directory, not a file: ${cwd}`);
8932
+ process.exit(1);
8933
+ }
8934
+ console.warn(`[sigmap] --cwd: restricting scan to ${cwd}`);
8935
+ } else if (cwd !== invokedFrom) {
8851
8936
  console.warn(`[sigmap] using project root: ${cwd}`);
8852
8937
  }
8853
8938
 
@@ -8870,6 +8955,11 @@ function main() {
8870
8955
 
8871
8956
  const config = loadConfig(cwd);
8872
8957
 
8958
+ // --cwd restricts scanning: override srcDirs so only the given directory is scanned
8959
+ if (cwdFlag) {
8960
+ config.srcDirs = ['.'];
8961
+ }
8962
+
8873
8963
  // ── --output <file> — parse early so every subsequent block can use it ─────
8874
8964
  // Resolves the custom output path and merges it into config.customOutput.
8875
8965
  // Also persists the resolved relative path to gen-context.config.json so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "5.9.0",
3
+ "version": "6.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": {
@@ -14,19 +14,19 @@
14
14
 
15
15
  const path = require('path');
16
16
  const fs = require('fs');
17
- const openai = require('./openai');
18
17
 
19
18
  const name = 'codex';
20
19
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
21
20
 
22
21
  /**
23
- * Format context using the OpenAI adapter format.
22
+ * Format context for AGENTS.md clean markdown, no LLM preamble.
24
23
  * @param {string} context - Raw signature context string
25
24
  * @param {object} [opts]
26
25
  * @returns {string}
27
26
  */
28
27
  function format(context, opts = {}) {
29
- return openai.format(context, opts);
28
+ if (!context || typeof context !== 'string' || !context.trim()) return '';
29
+ return `# Code signatures\n\n${context}`;
30
30
  }
31
31
 
32
32
  /**
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "5.9.0",
3
+ "version": "6.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": "5.9.0",
3
+ "version": "6.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,105 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Incremental extraction cache (v6.1).
5
+ *
6
+ * Stores extracted signatures keyed by file path + mtime so only
7
+ * modified files are re-extracted on subsequent runs.
8
+ *
9
+ * Cache file: .sigmap-cache.json in cwd (gitignored).
10
+ * Format:
11
+ * { sigmapVersion: string, entries: { [absPath]: { mtime: number, sigs: string[] } } }
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const CACHE_FILE = '.sigmap-cache.json';
18
+
19
+ function cachePath(cwd) {
20
+ return path.join(cwd, CACHE_FILE);
21
+ }
22
+
23
+ /**
24
+ * Load the cache from disk.
25
+ * Returns a Map<absPath, { mtime: number, sigs: string[] }>.
26
+ * Returns an empty Map if cache is absent, corrupt, or from a different version.
27
+ *
28
+ * @param {string} cwd
29
+ * @param {string} currentVersion - sigmap VERSION constant
30
+ * @returns {Map<string, { mtime: number, sigs: string[] }>}
31
+ */
32
+ function loadCache(cwd, currentVersion) {
33
+ try {
34
+ const raw = fs.readFileSync(cachePath(cwd), 'utf8');
35
+ const data = JSON.parse(raw);
36
+ // Bust cache on version change to avoid stale sig formats
37
+ if (data.sigmapVersion !== currentVersion) return new Map();
38
+ return new Map(Object.entries(data.entries || {}));
39
+ } catch (_) {
40
+ return new Map();
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Persist the cache to disk.
46
+ *
47
+ * @param {string} cwd
48
+ * @param {string} currentVersion
49
+ * @param {Map<string, { mtime: number, sigs: string[] }>} cache
50
+ */
51
+ function saveCache(cwd, currentVersion, cache) {
52
+ try {
53
+ const data = {
54
+ sigmapVersion: currentVersion,
55
+ entries: Object.fromEntries(cache),
56
+ };
57
+ fs.writeFileSync(cachePath(cwd), JSON.stringify(data), 'utf8');
58
+ } catch (_) {
59
+ // Non-fatal: cache save failure just means a full re-extract next run
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Given a list of absolute file paths, return only those whose mtime
65
+ * differs from the cached value (or that are not cached at all).
66
+ *
67
+ * @param {string[]} files - absolute paths
68
+ * @param {Map<string, { mtime: number, sigs: string[] }>} cache
69
+ * @returns {{ changed: string[], unchanged: string[] }}
70
+ */
71
+ function getChangedFiles(files, cache) {
72
+ const changed = [];
73
+ const unchanged = [];
74
+ for (const f of files) {
75
+ try {
76
+ const mtime = fs.statSync(f).mtimeMs;
77
+ const cached = cache.get(f);
78
+ if (!cached || cached.mtime !== mtime) {
79
+ changed.push(f);
80
+ } else {
81
+ unchanged.push(f);
82
+ }
83
+ } catch (_) {
84
+ changed.push(f); // file unreadable → treat as changed
85
+ }
86
+ }
87
+ return { changed, unchanged };
88
+ }
89
+
90
+ /**
91
+ * Update cache entries for a batch of files after fresh extraction.
92
+ *
93
+ * @param {Map<string, { mtime: number, sigs: string[] }>} cache
94
+ * @param {{ file: string, sigs: string[] }[]} extracted - freshly extracted results
95
+ */
96
+ function updateCacheEntries(cache, extracted) {
97
+ for (const { file, sigs } of extracted) {
98
+ try {
99
+ const mtime = fs.statSync(file).mtimeMs;
100
+ cache.set(file, { mtime, sigs });
101
+ } catch (_) {}
102
+ }
103
+ }
104
+
105
+ module.exports = { loadCache, saveCache, getChangedFiles, updateCacheEntries };
@@ -146,11 +146,14 @@ function extractInterfaceMembers(block) {
146
146
  return members.slice(0, 8);
147
147
  }
148
148
 
149
+ const _CTRL_KEYWORDS = new Set(['if', 'for', 'while', 'switch', 'do', 'try', 'catch', 'finally', 'else', 'return']);
150
+
149
151
  function extractClassMembers(block) {
150
152
  const members = [];
151
- // Public methods (skip private/protected/_ prefixed)
153
+ // Public methods (skip private/protected/_ prefixed and control-flow keywords)
152
154
  const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{;]+)?\s*\{/gm;
153
155
  for (const m of block.matchAll(methodRe)) {
156
+ if (_CTRL_KEYWORDS.has(m[1])) continue;
154
157
  if (/^(private|protected|_)/.test(m[1])) continue;
155
158
  if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
156
159
  const isAsync = m[0].includes('async ') ? 'async ' : '';
@@ -446,11 +446,15 @@ function queryContext(args, cwd) {
446
446
 
447
447
  try {
448
448
  const { rank, buildSigIndex, formatRankTable } = require('../retrieval/ranker');
449
+ const { buildFromCwd } = require('../graph/builder');
449
450
  const index = buildSigIndex(cwd);
450
451
  if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
451
452
 
452
453
  const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
453
- const results = rank(args.query, index, { topK, cwd });
454
+ // Build dependency graph for neighbor boost non-fatal if it fails
455
+ let graph = null;
456
+ try { graph = buildFromCwd(cwd); } catch (_) {}
457
+ const results = rank(args.query, index, { topK, cwd, graph });
454
458
  return formatRankTable(results, args.query);
455
459
  } catch (err) {
456
460
  return `_query_context failed: ${err.message}_`;
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: '5.9.0',
21
+ version: '6.0.1',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -29,6 +29,7 @@ const DEFAULT_WEIGHTS = {
29
29
  prefixMatch: 0.3, // partial prefix hit (query token ≥ 4 chars)
30
30
  pathMatch: 0.8, // query token appears in the file path
31
31
  recencyBoost: 1.5, // multiplier applied when file is in recencySet
32
+ graphBoost: 0.4, // additive bonus for 1-hop import neighbors of matching files
32
33
  };
33
34
 
34
35
  /**
@@ -99,6 +100,7 @@ function scoreFile(filePath, sigs, queryTokens, weights) {
99
100
  * @param {Set<string>} [opts.recencySet] - set of recently-changed file paths
100
101
  * @param {object} [opts.weights] - override scoring weights
101
102
  * @param {string} [opts.cwd] - project root for learned ranking weights
103
+ * @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
102
104
  * @returns {{ file: string, score: number, sigs: string[], tokens: number }[]}
103
105
  */
104
106
  function rank(query, sigIndex, opts) {
@@ -110,6 +112,8 @@ function rank(query, sigIndex, opts) {
110
112
  const recencySet = (opts && opts.recencySet) || null;
111
113
  const weights = (opts && opts.weights) ? Object.assign({}, DEFAULT_WEIGHTS, opts.weights) : DEFAULT_WEIGHTS;
112
114
  const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;
115
+ const graph = (opts && opts.graph && opts.graph.forward instanceof Map) ? opts.graph : null;
116
+ const cwd = (opts && opts.cwd) || null;
113
117
 
114
118
  const queryTokens = tokenize(query);
115
119
  if (queryTokens.length === 0) {
@@ -143,6 +147,30 @@ function rank(query, sigIndex, opts) {
143
147
  });
144
148
  }
145
149
 
150
+ // Graph neighbor boost: for each file with score > 0, add graphBoost to 1-hop forward
151
+ // neighbors that are also in the index. sigIndex uses relative paths; graph uses absolute.
152
+ if (graph && cwd) {
153
+ const path = require('path');
154
+ // Build a map: relative path → index position in scored array for O(1) lookup
155
+ const relToIdx = new Map();
156
+ for (let i = 0; i < scored.length; i++) {
157
+ relToIdx.set(scored[i].file, i);
158
+ }
159
+ for (const entry of scored) {
160
+ if (entry.score <= 0) continue;
161
+ // Resolve relative path to absolute for graph lookup
162
+ const abs = path.resolve(cwd, entry.file);
163
+ const neighbors = graph.forward.get(abs) || [];
164
+ for (const neighborAbs of neighbors) {
165
+ const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
166
+ const idx = relToIdx.get(neighborRel);
167
+ if (idx !== undefined) {
168
+ scored[idx].score += weights.graphBoost;
169
+ }
170
+ }
171
+ }
172
+ }
173
+
146
174
  scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
147
175
  return scored.slice(0, topK);
148
176
  }