sigmap 8.8.0 → 8.9.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 +19 -0
- package/README.md +2 -2
- package/gen-context.js +197 -5
- package/llms-full.txt +3 -2
- package/llms.txt +2 -2
- package/package.json +2 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/daemon/daemon.js +124 -0
- package/src/mcp/server.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,25 @@ Format: [Semantic Versioning](https://semver.org/)
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## [8.9.0] — 2026-07-06
|
|
14
|
+
|
|
15
|
+
Minor release — **the watcher, detached (D1).** `sigmap --watch` keeps the signature index fresh but held a terminal in the foreground. This adds a managed background daemon so you can start it once and forget it — the roadmap's #1 friction win. Zero-dependency, shell-free, deterministic.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- **Detached watch daemon — `sigmap daemon start|stop|status` (#447, PR #448):** runs the existing `--watch` mode as a background process, launched with `spawn(process.execPath, ['gen-context.js', '--watch'], { detached: true })` — an arguments array, never a shell command string — and tracked by a PID file under `.context/` (`daemon.pid`; output → `.context/daemon.log`). `start` does an initial generate then detaches, is idempotent (a second start reports "already running", spawns no second process), and clears a stale PID file first; `stop` sends SIGTERM and removes the PID file (a no-op when nothing is running); `status` reports the running PID and log path, exits 0 when running / 1 when not, and self-cleans a stale PID file. Every subcommand supports `--json`. New module `src/daemon/daemon.js`; CLI dispatch mirrors `sigmap mcp <sub>` and is listed in `--help`.
|
|
19
|
+
|
|
20
|
+
## [8.8.1] — 2026-07-05
|
|
21
|
+
|
|
22
|
+
Patch release — **byte-stable context, reproducible benchmark.** Closes the determinism residual tracked in #440: `gen-context` output is now byte-identical run-to-run across all 43 benchmark repos, and the retrieval benchmark reproduces a single hit@5 (87.8%) instead of flapping 85.6–87.8%. Plus a test-count derivation fix, docs/CI serving fixes, and repo hygiene.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- **gen-context determinism residual — token-budget recency boost (#440, PR #444):** the recency boost stamped `mtime = Date.now()` on every recently-committed file. On repos where nearly every file is "recently changed", consecutive files often landed on the *same millisecond* — so which equal-priority files shared a millisecond (and thus fell through to the `filePath` tie-break instead of sorting by a distinct mtime) shifted run to run, swapping which files survived at the budget cutoff and making the output non-byte-stable. The `Date.now()` value wasn't just a boost: it encoded the alphabetical walk order that the budget's best-first sort relied on; the nondeterminism was only the millisecond *collisions*. Replaced it with a deterministic monotonic counter (`nextRecentMtime`) that reproduces the exact same processing-order ranking without collisions. Result: all 43 benchmark repos are byte-identical across two clean runs (excluding the `Updated:` timestamp), and retrieval hit@5 reproduces at **87.8%** with identical per-repo results. Adds an integration regression guard (`test/integration/gen-context-determinism.test.js`) that runs gen-context twice on a committed fixture and asserts byte-equality — verified to fail on the old `Date.now()` behaviour.
|
|
26
|
+
- **Derived test count after test relocation (503a6e5):** the test-count derivation globbed `tests/**/*.py`; after relocating `test_python_ast_extractor.py` into `test/` it matched neither pattern and dropped the count, failing `check:metrics` in CI. Now counts `test/**/test_*.py` (unittest-named), which matches the relocated test and excludes the fixture.
|
|
27
|
+
- **Docs / CI serving (b236e08):** serve the OG banner image and the Google Search Console verification file; run the Python extractor tests in CI.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
- **Repo hygiene (a49e9c5):** dropped orphaned demo GIFs and a dead script; relocated a stray test from `tests/` into `test/`.
|
|
31
|
+
|
|
13
32
|
## [8.8.0] — 2026-07-05
|
|
14
33
|
|
|
15
34
|
Minor release — **the squeeze engine, exposed mid-session (D6).** The always-on squeeze engine (`src/squeeze/`) that powers `sigmap squeeze` was reachable only from the CLI on pasted input. This release exposes it as an MCP tool an agent can call *mid-session*, and adds a named CLI entry point for compressing an agent/tool *response*. Zero-dependency, offline, deterministic — the last A+ ceiling item (Machine 9→10): the engine already shipped, this just exposes it.
|
package/README.md
CHANGED
|
@@ -120,8 +120,8 @@ Ask → Rank → Context → Validate → Judge → Learn
|
|
|
120
120
|
|
|
121
121
|
<!--SM:benchmarkBlock-->
|
|
122
122
|
```
|
|
123
|
-
Benchmark : sigmap-v8.
|
|
124
|
-
Date : 2026-07-
|
|
123
|
+
Benchmark : sigmap-v8.9-main (21 repositories, including R language)
|
|
124
|
+
Date : 2026-07-06
|
|
125
125
|
|
|
126
126
|
Hit@5 : 87.8% (baseline 13.6% — 6.5× lift)
|
|
127
127
|
Token reduction: 97.0% (across 21 repos)
|
package/gen-context.js
CHANGED
|
@@ -2592,6 +2592,134 @@ __factories["./src/create/orchestrate"] = function(module, exports) {
|
|
|
2592
2592
|
|
|
2593
2593
|
};
|
|
2594
2594
|
|
|
2595
|
+
// ── ./src/daemon/daemon ──
|
|
2596
|
+
__factories["./src/daemon/daemon"] = function(module, exports) {
|
|
2597
|
+
|
|
2598
|
+
/**
|
|
2599
|
+
* Detached watch daemon (D1).
|
|
2600
|
+
*
|
|
2601
|
+
* Runs the existing `--watch` mode as a background process so the index stays
|
|
2602
|
+
* fresh without holding a terminal. State lives under `.context/` (consistent
|
|
2603
|
+
* with session/usage): `daemon.pid` records the watcher's PID, `daemon.log`
|
|
2604
|
+
* captures its output.
|
|
2605
|
+
*
|
|
2606
|
+
* Zero-dependency and shell-free: the watcher is launched with
|
|
2607
|
+
* `spawn(process.execPath, [gen-context.js, '--watch'], { detached: true })` —
|
|
2608
|
+
* an arguments array, never a shell command string.
|
|
2609
|
+
*/
|
|
2610
|
+
|
|
2611
|
+
const fs = require('fs');
|
|
2612
|
+
const path = require('path');
|
|
2613
|
+
const { spawn } = require('child_process');
|
|
2614
|
+
|
|
2615
|
+
module.exports = { start, stop, status, pidFile, logFile, isAlive, readPid };
|
|
2616
|
+
|
|
2617
|
+
function daemonDir(cwd) {
|
|
2618
|
+
return path.join(cwd, '.context');
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
function pidFile(cwd) {
|
|
2622
|
+
return path.join(daemonDir(cwd), 'daemon.pid');
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
function logFile(cwd) {
|
|
2626
|
+
return path.join(daemonDir(cwd), 'daemon.log');
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
/** True if a process with this PID exists (signal 0 probes without killing). */
|
|
2630
|
+
function isAlive(pid) {
|
|
2631
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
2632
|
+
try {
|
|
2633
|
+
process.kill(pid, 0);
|
|
2634
|
+
return true;
|
|
2635
|
+
} catch (err) {
|
|
2636
|
+
// EPERM = the process exists but is owned by another user — treat as alive.
|
|
2637
|
+
return err.code === 'EPERM';
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
/** Read the recorded PID, or null if the file is missing/unparseable. */
|
|
2642
|
+
function readPid(cwd) {
|
|
2643
|
+
try {
|
|
2644
|
+
const raw = fs.readFileSync(pidFile(cwd), 'utf8').trim();
|
|
2645
|
+
const pid = parseInt(raw, 10);
|
|
2646
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
2647
|
+
} catch (_) {
|
|
2648
|
+
return null;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
function removePidFile(cwd) {
|
|
2653
|
+
try {
|
|
2654
|
+
fs.unlinkSync(pidFile(cwd));
|
|
2655
|
+
} catch (_) {}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
/**
|
|
2659
|
+
* @returns {{ running: boolean, pid: number|null, pidFile: string, logFile: string }}
|
|
2660
|
+
*/
|
|
2661
|
+
function status(cwd) {
|
|
2662
|
+
const pid = readPid(cwd);
|
|
2663
|
+
const running = pid != null && isAlive(pid);
|
|
2664
|
+
// A recorded-but-dead PID is stale — clean it up so the state stays truthful.
|
|
2665
|
+
if (pid != null && !running) removePidFile(cwd);
|
|
2666
|
+
return { running, pid: running ? pid : null, pidFile: pidFile(cwd), logFile: logFile(cwd) };
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
/**
|
|
2670
|
+
* Launch a detached `--watch` process. Idempotent: if one is already running
|
|
2671
|
+
* this is a no-op that reports the existing PID.
|
|
2672
|
+
*
|
|
2673
|
+
* @param {string} cwd
|
|
2674
|
+
* @param {{ scriptPath: string }} opts - path to gen-context.js (the CLI entry)
|
|
2675
|
+
* @returns {{ status: 'started'|'already', pid: number, logFile: string }}
|
|
2676
|
+
*/
|
|
2677
|
+
function start(cwd, opts = {}) {
|
|
2678
|
+
const scriptPath = opts.scriptPath;
|
|
2679
|
+
if (!scriptPath) throw new Error('daemon.start requires opts.scriptPath');
|
|
2680
|
+
|
|
2681
|
+
const current = status(cwd);
|
|
2682
|
+
if (current.running) {
|
|
2683
|
+
return { status: 'already', pid: current.pid, logFile: logFile(cwd) };
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
fs.mkdirSync(daemonDir(cwd), { recursive: true });
|
|
2687
|
+
const out = fs.openSync(logFile(cwd), 'a');
|
|
2688
|
+
try {
|
|
2689
|
+
const child = spawn(process.execPath, [scriptPath, '--watch'], {
|
|
2690
|
+
cwd,
|
|
2691
|
+
detached: true,
|
|
2692
|
+
stdio: ['ignore', out, out],
|
|
2693
|
+
});
|
|
2694
|
+
child.unref();
|
|
2695
|
+
fs.writeFileSync(pidFile(cwd), String(child.pid) + '\n');
|
|
2696
|
+
return { status: 'started', pid: child.pid, logFile: logFile(cwd) };
|
|
2697
|
+
} finally {
|
|
2698
|
+
try { fs.closeSync(out); } catch (_) {}
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
/**
|
|
2703
|
+
* Stop the running watcher (SIGTERM) and clear its PID file.
|
|
2704
|
+
*
|
|
2705
|
+
* @returns {{ status: 'stopped'|'not-running'|'stale', pid?: number }}
|
|
2706
|
+
*/
|
|
2707
|
+
function stop(cwd) {
|
|
2708
|
+
const pid = readPid(cwd);
|
|
2709
|
+
if (pid == null) return { status: 'not-running' };
|
|
2710
|
+
if (!isAlive(pid)) {
|
|
2711
|
+
removePidFile(cwd);
|
|
2712
|
+
return { status: 'stale', pid };
|
|
2713
|
+
}
|
|
2714
|
+
try {
|
|
2715
|
+
process.kill(pid, 'SIGTERM');
|
|
2716
|
+
} catch (_) {}
|
|
2717
|
+
removePidFile(cwd);
|
|
2718
|
+
return { status: 'stopped', pid };
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
};
|
|
2722
|
+
|
|
2595
2723
|
// ── ./src/discovery/framework-detector ──
|
|
2596
2724
|
__factories["./src/discovery/framework-detector"] = function(module, exports) {
|
|
2597
2725
|
|
|
@@ -13566,7 +13694,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
|
|
|
13566
13694
|
|
|
13567
13695
|
const SERVER_INFO = {
|
|
13568
13696
|
name: 'sigmap',
|
|
13569
|
-
version: '8.
|
|
13697
|
+
version: '8.9.0',
|
|
13570
13698
|
description: 'SigMap MCP server — code signatures on demand',
|
|
13571
13699
|
};
|
|
13572
13700
|
|
|
@@ -18158,7 +18286,7 @@ function __tryGit(args, opts = {}) {
|
|
|
18158
18286
|
catch (_) { return ''; }
|
|
18159
18287
|
}
|
|
18160
18288
|
|
|
18161
|
-
const VERSION = '8.
|
|
18289
|
+
const VERSION = '8.9.0';
|
|
18162
18290
|
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
18163
18291
|
|
|
18164
18292
|
function requireSourceOrBundled(key) {
|
|
@@ -18368,6 +18496,26 @@ function annotateCoverage(sigs, testIndex, enabled) {
|
|
|
18368
18496
|
// ---------------------------------------------------------------------------
|
|
18369
18497
|
// Token budget enforcement
|
|
18370
18498
|
// ---------------------------------------------------------------------------
|
|
18499
|
+
// Deterministic recency boost for recently-committed files so the token budget
|
|
18500
|
+
// doesn't drop them first. Previously this used `Date.now()`, which encoded the
|
|
18501
|
+
// (alphabetical) processing order as a monotonically increasing mtime — but on
|
|
18502
|
+
// repos where nearly every file is "recently changed", consecutive files often
|
|
18503
|
+
// landed on the *same* millisecond. Which files shared a millisecond (and so
|
|
18504
|
+
// fell through to the filePath tie-break instead of sorting by a distinct
|
|
18505
|
+
// mtime) shifted run to run, swapping which files survived at the budget cutoff
|
|
18506
|
+
// and making the output non-byte-stable. A monotonic integer counter preserves
|
|
18507
|
+
// the exact processing-order ranking Date.now() produced — recent files stay
|
|
18508
|
+
// ahead of non-recent ones, ordered among themselves by walk order — but is
|
|
18509
|
+
// collision-free and reproducible. RECENT_MTIME_BASE sits far above any real
|
|
18510
|
+
// filesystem mtime (ms since epoch ≈ 1.7e12) so boosted files always out-rank
|
|
18511
|
+
// non-boosted ones, and far below MAX_SAFE_INTEGER so the counter never
|
|
18512
|
+
// overflows. Call nextRecentMtime() once per boosted file, in walk order. (#440)
|
|
18513
|
+
const RECENT_MTIME_BASE = 1e15;
|
|
18514
|
+
let _recentMtimeSeq = 0;
|
|
18515
|
+
function nextRecentMtime() {
|
|
18516
|
+
return RECENT_MTIME_BASE + (_recentMtimeSeq++);
|
|
18517
|
+
}
|
|
18518
|
+
|
|
18371
18519
|
function estimateTokens(str) {
|
|
18372
18520
|
return Math.ceil(str.length / 4);
|
|
18373
18521
|
}
|
|
@@ -19348,7 +19496,7 @@ function runDiff(cwd, config, stagedOnly, baseRef) {
|
|
|
19348
19496
|
|
|
19349
19497
|
sigs = annotateCoverage(sigs, testIndex, !!config.testCoverage);
|
|
19350
19498
|
|
|
19351
|
-
fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime:
|
|
19499
|
+
fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime: nextRecentMtime() });
|
|
19352
19500
|
}
|
|
19353
19501
|
|
|
19354
19502
|
if (fileEntries.length === 0) {
|
|
@@ -19496,8 +19644,11 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
|
19496
19644
|
mtime = fs.statSync(filePath).mtimeMs;
|
|
19497
19645
|
} catch (_) {}
|
|
19498
19646
|
|
|
19499
|
-
// Boost recently committed files (give them max mtime so they aren't
|
|
19500
|
-
|
|
19647
|
+
// Boost recently committed files (give them a max mtime so they aren't
|
|
19648
|
+
// dropped first). A deterministic monotonic counter — not Date.now() — keeps
|
|
19649
|
+
// the budget selection byte-stable when many files are recent, while
|
|
19650
|
+
// preserving the original processing-order ranking (see nextRecentMtime, #440).
|
|
19651
|
+
if (recentFiles.has(filePath)) mtime = nextRecentMtime();
|
|
19501
19652
|
|
|
19502
19653
|
fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime });
|
|
19503
19654
|
}
|
|
@@ -19939,6 +20090,7 @@ Usage:
|
|
|
19939
20090
|
${cmd} --track Append run metrics to .context/usage.ndjson
|
|
19940
20091
|
${cmd} --watch Generate + watch for file changes
|
|
19941
20092
|
${cmd} --setup Generate + install git hook + watch
|
|
20093
|
+
${cmd} daemon start|stop|status Run --watch as a detached background daemon
|
|
19942
20094
|
${cmd} --mcp Start MCP server on stdio
|
|
19943
20095
|
${cmd} --report Token reduction stats to stdout
|
|
19944
20096
|
${cmd} --report --json Token report as JSON (for CI; exits 1 if over budget)
|
|
@@ -21480,6 +21632,46 @@ function main() {
|
|
|
21480
21632
|
process.exit(1);
|
|
21481
21633
|
}
|
|
21482
21634
|
|
|
21635
|
+
// `sigmap daemon start|stop|status` — run `--watch` as a detached background
|
|
21636
|
+
// process (D1). PID + log live under .context/. Mirrors the `mcp` sub-dispatch.
|
|
21637
|
+
if (args[0] === 'daemon') {
|
|
21638
|
+
const daemon = requireSourceOrBundled('./src/daemon/daemon');
|
|
21639
|
+
const sub = args[1];
|
|
21640
|
+
const jsonOut = args.includes('--json');
|
|
21641
|
+
|
|
21642
|
+
if (sub === 'start') {
|
|
21643
|
+
const r = daemon.start(cwd, { scriptPath });
|
|
21644
|
+
if (jsonOut) { process.stdout.write(JSON.stringify(r) + '\n'); process.exit(0); }
|
|
21645
|
+
if (r.status === 'already') {
|
|
21646
|
+
console.log(`[sigmap] daemon already running (pid ${r.pid})`);
|
|
21647
|
+
} else {
|
|
21648
|
+
console.log(`[sigmap] daemon started (pid ${r.pid}) — watching for changes`);
|
|
21649
|
+
console.log(`[sigmap] logs: ${_displayPath(r.logFile, cwd)} stop with: sigmap daemon stop`);
|
|
21650
|
+
}
|
|
21651
|
+
process.exit(0);
|
|
21652
|
+
}
|
|
21653
|
+
|
|
21654
|
+
if (sub === 'stop') {
|
|
21655
|
+
const r = daemon.stop(cwd);
|
|
21656
|
+
if (jsonOut) { process.stdout.write(JSON.stringify(r) + '\n'); process.exit(0); }
|
|
21657
|
+
if (r.status === 'not-running') console.log('[sigmap] daemon not running');
|
|
21658
|
+
else if (r.status === 'stale') console.log(`[sigmap] removed stale pid file (pid ${r.pid} no longer alive)`);
|
|
21659
|
+
else console.log(`[sigmap] daemon stopped (pid ${r.pid})`);
|
|
21660
|
+
process.exit(0);
|
|
21661
|
+
}
|
|
21662
|
+
|
|
21663
|
+
if (sub === 'status') {
|
|
21664
|
+
const r = daemon.status(cwd);
|
|
21665
|
+
if (jsonOut) { process.stdout.write(JSON.stringify(r) + '\n'); process.exit(r.running ? 0 : 1); }
|
|
21666
|
+
if (r.running) console.log(`[sigmap] daemon running (pid ${r.pid}) — logs: ${_displayPath(r.logFile, cwd)}`);
|
|
21667
|
+
else console.log('[sigmap] daemon not running');
|
|
21668
|
+
process.exit(r.running ? 0 : 1);
|
|
21669
|
+
}
|
|
21670
|
+
|
|
21671
|
+
console.error('[sigmap] usage: sigmap daemon start | stop | status');
|
|
21672
|
+
process.exit(1);
|
|
21673
|
+
}
|
|
21674
|
+
|
|
21483
21675
|
// `sigmap doctor` — diagnose config, index, freshness, coverage, and MCP
|
|
21484
21676
|
// wiring; print an actionable fix for anything wrong. Exit 1 on a hard
|
|
21485
21677
|
// failure (no context file / invalid config) so it is usable in CI.
|
package/llms-full.txt
CHANGED
|
@@ -11,13 +11,13 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.9.0 | Benchmark: sigmap-v8.9-main (2026-07-06)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
20
|
+
## Core metrics (benchmark: sigmap-v8.9-main, 2026-07-06)
|
|
21
21
|
|
|
22
22
|
| Metric | Without SigMap | With SigMap |
|
|
23
23
|
|--------|----------------|-------------|
|
|
@@ -59,6 +59,7 @@ sigmap --format cache Also write Anthropic prompt-cache JSON
|
|
|
59
59
|
sigmap --track Append run metrics to .context/usage.ndjson
|
|
60
60
|
sigmap --watch Generate + watch for file changes
|
|
61
61
|
sigmap --setup Generate + install git hook + watch
|
|
62
|
+
sigmap daemon start|stop|status Run --watch as a detached background daemon
|
|
62
63
|
sigmap --mcp Start MCP server on stdio
|
|
63
64
|
sigmap --report Token reduction stats to stdout
|
|
64
65
|
sigmap --report --json Token report as JSON (for CI; exits 1 if over budget)
|
package/llms.txt
CHANGED
|
@@ -11,7 +11,7 @@ ranking keeps the relevant context in scope (cutting tokens ~97% as a side
|
|
|
11
11
|
effect), with no LLM calls, embeddings, or vector database. Works with Claude,
|
|
12
12
|
Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
13
13
|
|
|
14
|
-
# Version: 8.
|
|
14
|
+
# Version: 8.9.0 | Benchmark: sigmap-v8.9-main (2026-07-06)
|
|
15
15
|
# Source: auto-generated from package.json, version.json, benchmarks/latest.json, src/mcp/tools.js, src/config/defaults.js
|
|
16
16
|
# Regenerate: npm run generate:llms | Validate: npm run validate:llms
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@ Cursor, GitHub Copilot, Aider, Windsurf, local LLMs, and MCP.
|
|
|
23
23
|
- No blast-radius awareness before editing a hub file — `--impact` shows every file a change touches.
|
|
24
24
|
- Pasted stack traces, CI logs, and JSON bloat the prompt — `squeeze` minimizes them and enriches the top frame from the symbol index.
|
|
25
25
|
|
|
26
|
-
## Core metrics (benchmark: sigmap-v8.
|
|
26
|
+
## Core metrics (benchmark: sigmap-v8.9-main, 2026-07-06)
|
|
27
27
|
|
|
28
28
|
- hit@5 retrieval: 87.8% vs 13.6% random baseline (6.5× lift)
|
|
29
29
|
- Token reduction: 97.0% average across benchmark repos
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sigmap",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.9.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": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"test": "node test/run.js && node test/r-language.test.js",
|
|
18
18
|
"test:integration": "node test/integration/strategy.test.js && node test/integration/secret-scan.test.js && node test/integration/token-budget.test.js && node test/integration/auto-budget.test.js && node test/integration/mcp/server.test.js && node test/integration/verify-ai-output.test.js && node test/integration/memory-tools.test.js && node test/integration/squeeze.test.js && node test/integration/context-consistency.test.js && node test/integration/features/llms-current.test.js",
|
|
19
19
|
"test:integration:all": "node test/integration/all.js",
|
|
20
|
+
"test:python": "python3 test/test_python_ast_extractor.py",
|
|
20
21
|
"test:all": "node test/run.js && node test/r-language.test.js && node test/integration/strategy.test.js && node test/integration/secret-scan.test.js",
|
|
21
22
|
"generate": "node gen-context.js",
|
|
22
23
|
"watch": "node gen-context.js --watch",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detached watch daemon (D1).
|
|
5
|
+
*
|
|
6
|
+
* Runs the existing `--watch` mode as a background process so the index stays
|
|
7
|
+
* fresh without holding a terminal. State lives under `.context/` (consistent
|
|
8
|
+
* with session/usage): `daemon.pid` records the watcher's PID, `daemon.log`
|
|
9
|
+
* captures its output.
|
|
10
|
+
*
|
|
11
|
+
* Zero-dependency and shell-free: the watcher is launched with
|
|
12
|
+
* `spawn(process.execPath, [gen-context.js, '--watch'], { detached: true })` —
|
|
13
|
+
* an arguments array, never a shell command string.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { spawn } = require('child_process');
|
|
19
|
+
|
|
20
|
+
module.exports = { start, stop, status, pidFile, logFile, isAlive, readPid };
|
|
21
|
+
|
|
22
|
+
function daemonDir(cwd) {
|
|
23
|
+
return path.join(cwd, '.context');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pidFile(cwd) {
|
|
27
|
+
return path.join(daemonDir(cwd), 'daemon.pid');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function logFile(cwd) {
|
|
31
|
+
return path.join(daemonDir(cwd), 'daemon.log');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** True if a process with this PID exists (signal 0 probes without killing). */
|
|
35
|
+
function isAlive(pid) {
|
|
36
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
37
|
+
try {
|
|
38
|
+
process.kill(pid, 0);
|
|
39
|
+
return true;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
// EPERM = the process exists but is owned by another user — treat as alive.
|
|
42
|
+
return err.code === 'EPERM';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Read the recorded PID, or null if the file is missing/unparseable. */
|
|
47
|
+
function readPid(cwd) {
|
|
48
|
+
try {
|
|
49
|
+
const raw = fs.readFileSync(pidFile(cwd), 'utf8').trim();
|
|
50
|
+
const pid = parseInt(raw, 10);
|
|
51
|
+
return Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
52
|
+
} catch (_) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function removePidFile(cwd) {
|
|
58
|
+
try {
|
|
59
|
+
fs.unlinkSync(pidFile(cwd));
|
|
60
|
+
} catch (_) {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @returns {{ running: boolean, pid: number|null, pidFile: string, logFile: string }}
|
|
65
|
+
*/
|
|
66
|
+
function status(cwd) {
|
|
67
|
+
const pid = readPid(cwd);
|
|
68
|
+
const running = pid != null && isAlive(pid);
|
|
69
|
+
// A recorded-but-dead PID is stale — clean it up so the state stays truthful.
|
|
70
|
+
if (pid != null && !running) removePidFile(cwd);
|
|
71
|
+
return { running, pid: running ? pid : null, pidFile: pidFile(cwd), logFile: logFile(cwd) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Launch a detached `--watch` process. Idempotent: if one is already running
|
|
76
|
+
* this is a no-op that reports the existing PID.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} cwd
|
|
79
|
+
* @param {{ scriptPath: string }} opts - path to gen-context.js (the CLI entry)
|
|
80
|
+
* @returns {{ status: 'started'|'already', pid: number, logFile: string }}
|
|
81
|
+
*/
|
|
82
|
+
function start(cwd, opts = {}) {
|
|
83
|
+
const scriptPath = opts.scriptPath;
|
|
84
|
+
if (!scriptPath) throw new Error('daemon.start requires opts.scriptPath');
|
|
85
|
+
|
|
86
|
+
const current = status(cwd);
|
|
87
|
+
if (current.running) {
|
|
88
|
+
return { status: 'already', pid: current.pid, logFile: logFile(cwd) };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
fs.mkdirSync(daemonDir(cwd), { recursive: true });
|
|
92
|
+
const out = fs.openSync(logFile(cwd), 'a');
|
|
93
|
+
try {
|
|
94
|
+
const child = spawn(process.execPath, [scriptPath, '--watch'], {
|
|
95
|
+
cwd,
|
|
96
|
+
detached: true,
|
|
97
|
+
stdio: ['ignore', out, out],
|
|
98
|
+
});
|
|
99
|
+
child.unref();
|
|
100
|
+
fs.writeFileSync(pidFile(cwd), String(child.pid) + '\n');
|
|
101
|
+
return { status: 'started', pid: child.pid, logFile: logFile(cwd) };
|
|
102
|
+
} finally {
|
|
103
|
+
try { fs.closeSync(out); } catch (_) {}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Stop the running watcher (SIGTERM) and clear its PID file.
|
|
109
|
+
*
|
|
110
|
+
* @returns {{ status: 'stopped'|'not-running'|'stale', pid?: number }}
|
|
111
|
+
*/
|
|
112
|
+
function stop(cwd) {
|
|
113
|
+
const pid = readPid(cwd);
|
|
114
|
+
if (pid == null) return { status: 'not-running' };
|
|
115
|
+
if (!isAlive(pid)) {
|
|
116
|
+
removePidFile(cwd);
|
|
117
|
+
return { status: 'stale', pid };
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
process.kill(pid, 'SIGTERM');
|
|
121
|
+
} catch (_) {}
|
|
122
|
+
removePidFile(cwd);
|
|
123
|
+
return { status: 'stopped', pid };
|
|
124
|
+
}
|
package/src/mcp/server.js
CHANGED