sigmap 7.4.0 → 7.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 +9 -0
- package/gen-context.js +130 -2
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/cache/freshen.js +123 -0
- package/src/mcp/handlers.js +2 -0
- package/src/mcp/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,15 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [7.5.0] — 2026-06-17
|
|
14
|
+
|
|
15
|
+
Minor release — read-time self-heal completes Layer 1 freshness.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **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.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
13
22
|
## [7.4.0] — 2026-06-17
|
|
14
23
|
|
|
15
24
|
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.
|
|
6629
|
+
version: '7.5.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.
|
|
12307
|
+
const VERSION = '7.5.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.
|
|
12
|
+
# Version: 7.5.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.
|
|
12
|
+
# Version: 7.5.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.
|
|
3
|
+
"version": "7.5.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": {
|
|
@@ -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 };
|
package/src/mcp/handlers.js
CHANGED
|
@@ -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