sigmap 7.4.0 → 7.6.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
@@ -10,6 +10,24 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [7.6.0] — 2026-06-17
14
+
15
+ Minor release — the grounding benchmark (the offline GATE for grounded codegen).
16
+
17
+ ### Added
18
+ - **Grounding benchmark — `npm run benchmark:grounding` (#294):** a deterministic, offline callee-grounding ablation that measures how much ground truth SigMap actually gives an agent. For each corpus repo: `coverage = grounded / universe`, where universe is every symbol defined in the source and grounded is the subset SigMap surfaces in its index (resolvable by `get_callee_signatures`); baseline is 0 (no SigMap → guess every reference). `scripts/run-hallucination-benchmark.mjs` prints per-repo + aggregate coverage; `--save` writes `benchmarks/reports/hallucination.json`; `--gate <pct>` exits non-zero below a threshold. It's an honest *ground-truth-availability proxy*, not a measured LLM hallucination rate (the LLM A/B ablation is a documented follow-up needing an API key). No LLM, no network — runs in CI.
19
+
20
+ ---
21
+
22
+ ## [7.5.0] — 2026-06-17
23
+
24
+ Minor release — read-time self-heal completes Layer 1 freshness.
25
+
26
+ ### Added
27
+ - **Read-time self-heal — live index without agent hooks (#290):** the v7.4.0 write hooks kept the index fresh only if the agent called them; now `search_signatures` / `get_callee_signatures` reconcile the index with the source tree *on read*, so on-disk edits show up even when no hook fired — closing that single point of failure. New `src/cache/freshen.js` re-extracts files modified since the last `generate` (bounded to actual session edits, not the whole tree), persists to the sig-cache (which `buildSigIndex` merges), and is throttled per repo. Deletions stay the job of `sigmap_notify_file_deleted` (a cache entry may be a notify overlay for a not-yet-on-disk file). Verified end-to-end in the standalone bundle.
28
+
29
+ ---
30
+
13
31
  ## [7.4.0] — 2026-06-17
14
32
 
15
33
  Minor release — live-index write hooks (grounded codegen, Layer 1).
package/gen-context.js CHANGED
@@ -20,6 +20,132 @@ function __require(key) {
20
20
  }
21
21
 
22
22
 
23
+ // ── ./src/cache/freshen ──
24
+ __factories["./src/cache/freshen"] = function(module, exports) {
25
+
26
+ /**
27
+ * Read-time self-heal (IMPL.md Layer 1, "safety net" tier).
28
+ *
29
+ * Keeps the sig-cache in line with the current source tree so the index reflects
30
+ * on-disk reality even when no write hook was called. Re-extracts files modified
31
+ * since the context file was generated (bounded to actual session edits, not the
32
+ * whole tree), drops cache entries for deleted files, and persists. buildSigIndex
33
+ * already merges the cache, so the next read is fresh.
34
+ *
35
+ * Throttled per cwd. Skips entirely when there is no generated index to heal
36
+ * (a cold repo should run `generate` or use the notify hooks).
37
+ *
38
+ * Zero-dependency, bundle-safe (fs + dispatch + sig-cache).
39
+ */
40
+
41
+ const fs = require('fs');
42
+ const path = require('path');
43
+ const { loadCache, saveCache, getChangedFiles } = __require('./src/cache/sig-cache');
44
+ const { extractFile, langFor } = __require('./src/extractors/dispatch');
45
+
46
+ const DEFAULT_SRC_DIRS = ['src', 'app', 'lib', 'packages', 'services', 'api'];
47
+ const DEFAULT_EXCLUDE = [
48
+ 'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
49
+ '.next', 'coverage', 'target', 'vendor', '.context',
50
+ ];
51
+ const CONTEXT_PATHS = [
52
+ ['.github', 'copilot-instructions.md'],
53
+ ['CLAUDE.md'], ['AGENTS.md'], ['.github', 'context-cold.md'],
54
+ ];
55
+ const THROTTLE_MS = 1500;
56
+ const _lastRun = new Map();
57
+
58
+ function _readConfig(cwd) {
59
+ try {
60
+ const cfg = JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
61
+ return cfg && typeof cfg === 'object' ? cfg : {};
62
+ } catch (_) { return {}; }
63
+ }
64
+
65
+ function _pkgVersion(cwd) {
66
+ try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
67
+ catch (_) { return '0.0.0'; }
68
+ }
69
+
70
+ /** Newest mtime among existing generated context files, or 0 if none. */
71
+ function _contextMtime(cwd) {
72
+ let newest = 0;
73
+ for (const parts of CONTEXT_PATHS) {
74
+ try { newest = Math.max(newest, fs.statSync(path.join(cwd, ...parts)).mtimeMs); } catch (_) {}
75
+ }
76
+ return newest;
77
+ }
78
+
79
+ function _walk(dir, exclude, out, depth, maxDepth) {
80
+ if (depth > maxDepth) return;
81
+ let entries;
82
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
83
+ for (const e of entries) {
84
+ if (exclude.has(e.name)) continue;
85
+ const full = path.join(dir, e.name);
86
+ if (e.isDirectory()) _walk(full, exclude, out, depth + 1, maxDepth);
87
+ else if (e.isFile() && langFor(e.name)) out.push(full);
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Re-extract source files changed since the last generate; drop deleted files.
93
+ * @param {string} cwd
94
+ * @param {{force?:boolean, now?:number}} [opts]
95
+ * @returns {number} cache entries touched
96
+ */
97
+ function freshen(cwd, opts = {}) {
98
+ const now = opts.now != null ? opts.now : Date.now();
99
+ if (!opts.force) {
100
+ if (now - (_lastRun.get(cwd) || 0) < THROTTLE_MS) return 0;
101
+ }
102
+ _lastRun.set(cwd, now);
103
+
104
+ try {
105
+ const version = _pkgVersion(cwd);
106
+ const cache = loadCache(cwd, version);
107
+ const ctxMtime = _contextMtime(cwd);
108
+ // Nothing to heal: no generated context AND no live cache overlay.
109
+ if (ctxMtime === 0 && cache.size === 0) return 0;
110
+
111
+ const cfg = _readConfig(cwd);
112
+ const srcDirs = Array.isArray(cfg.srcDirs) && cfg.srcDirs.length ? cfg.srcDirs : DEFAULT_SRC_DIRS;
113
+ const exclude = new Set([...DEFAULT_EXCLUDE, ...(Array.isArray(cfg.exclude) ? cfg.exclude : [])]);
114
+ const maxDepth = Number.isFinite(cfg.maxDepth) ? cfg.maxDepth : 8;
115
+
116
+ const files = [];
117
+ for (const d of srcDirs) {
118
+ const abs = path.isAbsolute(d) ? d : path.join(cwd, d);
119
+ if (fs.existsSync(abs)) _walk(abs, exclude, files, 0, maxDepth);
120
+ }
121
+
122
+ // Candidates = files modified since the context was generated, or not yet cached.
123
+ const candidates = files.filter((f) => {
124
+ try { return fs.statSync(f).mtimeMs > ctxMtime || !cache.has(f); } catch (_) { return false; }
125
+ });
126
+ const { changed } = getChangedFiles(candidates, cache);
127
+
128
+ let touched = 0;
129
+ for (const f of changed) {
130
+ try {
131
+ const sigs = extractFile(f, fs.readFileSync(f, 'utf8'));
132
+ cache.set(f, { mtime: fs.statSync(f).mtimeMs, sigs });
133
+ touched++;
134
+ } catch (_) {}
135
+ }
136
+ // Note: deletions are NOT swept here — a cache entry may be a `notify`
137
+ // overlay for a file not yet on disk. Explicit removal is `notify_file_deleted`.
138
+
139
+ if (touched > 0) saveCache(cwd, version, cache);
140
+ return touched;
141
+ } catch (_) {
142
+ return 0;
143
+ }
144
+ }
145
+
146
+ module.exports = { freshen };
147
+
148
+ };
23
149
  // ── ./src/extractors/dispatch ──
24
150
  __factories["./src/extractors/dispatch"] = function(module, exports) {
25
151
 
@@ -5717,6 +5843,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
5717
5843
 
5718
5844
  const query = args.query.toLowerCase();
5719
5845
  try {
5846
+ try { __require('./src/cache/freshen').freshen(cwd); } catch (_) {}
5720
5847
  const { buildSigIndex } = __require('./src/retrieval/ranker');
5721
5848
  const index = buildSigIndex(cwd);
5722
5849
  if (index.size === 0) {
@@ -6204,6 +6331,7 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
6204
6331
  }
6205
6332
 
6206
6333
  try {
6334
+ try { __require('./src/cache/freshen').freshen(cwd); } catch (_) {}
6207
6335
  const { buildSigIndex } = __require('./src/retrieval/ranker');
6208
6336
  const { buildSymbolCandidates, closestMatch, formatSuggestion } = __require('./src/verify/closest-match');
6209
6337
  const index = buildSigIndex(cwd);
@@ -6498,7 +6626,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
6498
6626
 
6499
6627
  const SERVER_INFO = {
6500
6628
  name: 'sigmap',
6501
- version: '7.4.0',
6629
+ version: '7.6.0',
6502
6630
  description: 'SigMap MCP server — code signatures on demand',
6503
6631
  };
6504
6632
 
@@ -12176,7 +12304,7 @@ function __tryGit(args, opts = {}) {
12176
12304
  catch (_) { return ''; }
12177
12305
  }
12178
12306
 
12179
- const VERSION = '7.4.0';
12307
+ const VERSION = '7.6.0';
12180
12308
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
12181
12309
 
12182
12310
  function requireSourceOrBundled(key) {
package/llms-full.txt CHANGED
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
9
9
  grounded. Deterministic, offline, no embeddings or vector database. Works with
10
10
  Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
11
11
 
12
- # Version: 7.4.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.6.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
13
13
  # Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
14
14
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
15
15
 
package/llms.txt CHANGED
@@ -9,7 +9,7 @@ the files relevant to the task — cutting tokens ~97% while keeping answers
9
9
  grounded. Deterministic, offline, no embeddings or vector database. Works with
10
10
  Claude, Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
11
11
 
12
- # Version: 7.4.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
12
+ # Version: 7.6.0 | Benchmark: sigmap-v7.0-main (2026-06-14)
13
13
  # Source: auto-generated from package.json, version.json, src/mcp/tools.js, src/config/defaults.js
14
14
  # Regenerate: npm run generate:llms | Validate: npm run validate:llms
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "7.4.0",
3
+ "version": "7.6.0",
4
4
  "description": "97% token reduction for AI coding. Extracts function & class signatures with TF-IDF ranking to feed only the right files to Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP. Zero dependencies, runs offline via npx.",
5
5
  "main": "packages/core/index.js",
6
6
  "exports": {
@@ -38,7 +38,8 @@
38
38
  "version:sync": "node scripts/sync-versions.mjs",
39
39
  "generate:llms": "node scripts/generate-llms.mjs",
40
40
  "validate:llms": "node scripts/validate-llms.mjs",
41
- "prepublishOnly": "node scripts/check-bundle.mjs && node scripts/check-version-meta.mjs && node scripts/generate-llms.mjs"
41
+ "prepublishOnly": "node scripts/check-bundle.mjs && node scripts/check-version-meta.mjs && node scripts/generate-llms.mjs",
42
+ "benchmark:grounding": "node scripts/run-hallucination-benchmark.mjs"
42
43
  },
43
44
  "files": [
44
45
  "gen-context.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "7.4.0",
3
+ "version": "7.6.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "7.4.0",
3
+ "version": "7.6.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Read-time self-heal (IMPL.md Layer 1, "safety net" tier).
5
+ *
6
+ * Keeps the sig-cache in line with the current source tree so the index reflects
7
+ * on-disk reality even when no write hook was called. Re-extracts files modified
8
+ * since the context file was generated (bounded to actual session edits, not the
9
+ * whole tree), drops cache entries for deleted files, and persists. buildSigIndex
10
+ * already merges the cache, so the next read is fresh.
11
+ *
12
+ * Throttled per cwd. Skips entirely when there is no generated index to heal
13
+ * (a cold repo should run `generate` or use the notify hooks).
14
+ *
15
+ * Zero-dependency, bundle-safe (fs + dispatch + sig-cache).
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { loadCache, saveCache, getChangedFiles } = require('./sig-cache');
21
+ const { extractFile, langFor } = require('../extractors/dispatch');
22
+
23
+ const DEFAULT_SRC_DIRS = ['src', 'app', 'lib', 'packages', 'services', 'api'];
24
+ const DEFAULT_EXCLUDE = [
25
+ 'node_modules', '.git', 'dist', 'build', 'out', '__pycache__',
26
+ '.next', 'coverage', 'target', 'vendor', '.context',
27
+ ];
28
+ const CONTEXT_PATHS = [
29
+ ['.github', 'copilot-instructions.md'],
30
+ ['CLAUDE.md'], ['AGENTS.md'], ['.github', 'context-cold.md'],
31
+ ];
32
+ const THROTTLE_MS = 1500;
33
+ const _lastRun = new Map();
34
+
35
+ function _readConfig(cwd) {
36
+ try {
37
+ const cfg = JSON.parse(fs.readFileSync(path.join(cwd, 'gen-context.config.json'), 'utf8'));
38
+ return cfg && typeof cfg === 'object' ? cfg : {};
39
+ } catch (_) { return {}; }
40
+ }
41
+
42
+ function _pkgVersion(cwd) {
43
+ try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
44
+ catch (_) { return '0.0.0'; }
45
+ }
46
+
47
+ /** Newest mtime among existing generated context files, or 0 if none. */
48
+ function _contextMtime(cwd) {
49
+ let newest = 0;
50
+ for (const parts of CONTEXT_PATHS) {
51
+ try { newest = Math.max(newest, fs.statSync(path.join(cwd, ...parts)).mtimeMs); } catch (_) {}
52
+ }
53
+ return newest;
54
+ }
55
+
56
+ function _walk(dir, exclude, out, depth, maxDepth) {
57
+ if (depth > maxDepth) return;
58
+ let entries;
59
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
60
+ for (const e of entries) {
61
+ if (exclude.has(e.name)) continue;
62
+ const full = path.join(dir, e.name);
63
+ if (e.isDirectory()) _walk(full, exclude, out, depth + 1, maxDepth);
64
+ else if (e.isFile() && langFor(e.name)) out.push(full);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Re-extract source files changed since the last generate; drop deleted files.
70
+ * @param {string} cwd
71
+ * @param {{force?:boolean, now?:number}} [opts]
72
+ * @returns {number} cache entries touched
73
+ */
74
+ function freshen(cwd, opts = {}) {
75
+ const now = opts.now != null ? opts.now : Date.now();
76
+ if (!opts.force) {
77
+ if (now - (_lastRun.get(cwd) || 0) < THROTTLE_MS) return 0;
78
+ }
79
+ _lastRun.set(cwd, now);
80
+
81
+ try {
82
+ const version = _pkgVersion(cwd);
83
+ const cache = loadCache(cwd, version);
84
+ const ctxMtime = _contextMtime(cwd);
85
+ // Nothing to heal: no generated context AND no live cache overlay.
86
+ if (ctxMtime === 0 && cache.size === 0) return 0;
87
+
88
+ const cfg = _readConfig(cwd);
89
+ const srcDirs = Array.isArray(cfg.srcDirs) && cfg.srcDirs.length ? cfg.srcDirs : DEFAULT_SRC_DIRS;
90
+ const exclude = new Set([...DEFAULT_EXCLUDE, ...(Array.isArray(cfg.exclude) ? cfg.exclude : [])]);
91
+ const maxDepth = Number.isFinite(cfg.maxDepth) ? cfg.maxDepth : 8;
92
+
93
+ const files = [];
94
+ for (const d of srcDirs) {
95
+ const abs = path.isAbsolute(d) ? d : path.join(cwd, d);
96
+ if (fs.existsSync(abs)) _walk(abs, exclude, files, 0, maxDepth);
97
+ }
98
+
99
+ // Candidates = files modified since the context was generated, or not yet cached.
100
+ const candidates = files.filter((f) => {
101
+ try { return fs.statSync(f).mtimeMs > ctxMtime || !cache.has(f); } catch (_) { return false; }
102
+ });
103
+ const { changed } = getChangedFiles(candidates, cache);
104
+
105
+ let touched = 0;
106
+ for (const f of changed) {
107
+ try {
108
+ const sigs = extractFile(f, fs.readFileSync(f, 'utf8'));
109
+ cache.set(f, { mtime: fs.statSync(f).mtimeMs, sigs });
110
+ touched++;
111
+ } catch (_) {}
112
+ }
113
+ // Note: deletions are NOT swept here — a cache entry may be a `notify`
114
+ // overlay for a file not yet on disk. Explicit removal is `notify_file_deleted`.
115
+
116
+ if (touched > 0) saveCache(cwd, version, cache);
117
+ return touched;
118
+ } catch (_) {
119
+ return 0;
120
+ }
121
+ }
122
+
123
+ module.exports = { freshen };
@@ -72,6 +72,7 @@ function searchSignatures(args, cwd) {
72
72
 
73
73
  const query = args.query.toLowerCase();
74
74
  try {
75
+ try { require('../cache/freshen').freshen(cwd); } catch (_) {}
75
76
  const { buildSigIndex } = require('../retrieval/ranker');
76
77
  const index = buildSigIndex(cwd);
77
78
  if (index.size === 0) {
@@ -559,6 +560,7 @@ function getCalleeSignatures(args, cwd) {
559
560
  }
560
561
 
561
562
  try {
563
+ try { require('../cache/freshen').freshen(cwd); } catch (_) {}
562
564
  const { buildSigIndex } = require('../retrieval/ranker');
563
565
  const { buildSymbolCandidates, closestMatch, formatSuggestion } = require('../verify/closest-match');
564
566
  const index = buildSigIndex(cwd);
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: '7.4.0',
21
+ version: '7.6.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24