sigmap 8.7.1 → 8.8.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/CHANGELOG.md CHANGED
@@ -10,6 +10,29 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.8.1] — 2026-07-05
14
+
15
+ 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.
16
+
17
+ ### Fixed
18
+ - **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.
19
+ - **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.
20
+ - **Docs / CI serving (b236e08):** serve the OG banner image and the Google Search Console verification file; run the Python extractor tests in CI.
21
+
22
+ ### Changed
23
+ - **Repo hygiene (a49e9c5):** dropped orphaned demo GIFs and a dead script; relocated a stray test from `tests/` into `test/`.
24
+
25
+ ## [8.8.0] — 2026-07-05
26
+
27
+ 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.
28
+
29
+ ### Added
30
+ - **`squeeze_output` MCP tool (#437, PR #438):** the 19th MCP tool (18 → 19) wraps the existing `squeeze()` engine so an agent can compress noisy tool/command output — a stack trace, CI/build log, or JSON payload — before it enters context. Keeps the signal (error frames, failing steps, JSON shape), strips the noise, and enriches the top stack frame with its signature; returns the squeezed text plus token-reduction stats. Passes the input through unchanged when no squeezable structure is detected, and returns usage on empty input. No LLM, no network. New handler `squeezeOutput` in `src/mcp/handlers.js`, wired through `src/mcp/tools.js` and `src/mcp/server.js`.
31
+ - **`sigmap squeeze --response <file|->` (#437, PR #438):** names the agent/tool response input explicitly (mirroring the existing `sigmap judge --response` convention), routing it through the same squeeze engine. Supports `--json`. Documented in `--help`.
32
+
33
+ ### Fixed
34
+ - **Deterministic context output — three nondeterminism sources (#440, PR #441):** `gen-context`'s signature output was not byte-stable across runs on some repos, making the retrieval benchmark's hit@5 non-reproducible. Fixes: (1) the token-budget drop-order now has a final `filePath` tie-break, so which files survive the budget no longer falls through to filesystem `readdir` order (test files sharing a git-checkout mtime tied on every prior key); (2) the source-file walk sorts `readdir` entries by name for stable section order; (3) auto source-root detection sorts its candidate enumeration and adds tie-breaks to the score sorts, so the `MAX_ROOTS` cutoff admits a deterministic directory set. Adds a resolver determinism regression guard. A residual affecting a few large repos remains tracked in #440.
35
+
13
36
  ## [8.7.1] — 2026-07-05
14
37
 
15
38
  Patch release — **multi-model cost savings + verified pricing.** The quality benchmark reported API input-cost savings for GPT-4o only; it now reports GPT-4o, Claude Sonnet, and Claude Haiku, and the hardcoded Claude rates were corrected to current published pricing.
package/README.md CHANGED
@@ -57,10 +57,10 @@ That map is exactly what agentic grep is worst at: reproducible, auditable conte
57
57
 
58
58
  **Proof it pays off** (full benchmark below):
59
59
  <!--SM:whyMetrics-->
60
- - **86.7% hit@5** — right file found in top 5 results (vs 13.6% baseline)
60
+ - **87.8% hit@5** — right file found in top 5 results (vs 13.6% baseline)
61
61
  - **97.0% token reduction** — average across 21 real repos
62
62
  - **67.8% task success rate** — up from 10% without context
63
- - **1.46 prompts per task** — down from 2.84 (48.8% fewer retries)
63
+ - **1.44 prompts per task** — down from 2.84 (49.2% fewer retries)
64
64
  <!--/SM:whyMetrics-->
65
65
  - **<!--SM:languages-->33<!--/SM:languages--> languages supported** — TypeScript, Python, Go, Rust, Java, R, and more
66
66
  - **No vendor lock-in** — works with any AI assistant or local LLM
@@ -96,7 +96,7 @@ sigmap verify answer.md --report # standalone red/amber/green HTML report
96
96
  | Without SigMap | With SigMap |
97
97
  |---|---|
98
98
  | ❌ Non-reproducible agent guesses | ✅ Deterministic map — same input, same output, every time |
99
- | ❌ "Trust me" AI answers | ✅ Grounded — right file in context <!--SM:hitWhole-->87%<!--/SM:hitWhole--> of the time, every symbol on a real line anchor |
99
+ | ❌ "Trust me" AI answers | ✅ Grounded — right file in context <!--SM:hitWhole-->88%<!--/SM:hitWhole--> of the time, every symbol on a real line anchor |
100
100
  | ❌ Embeddings / vector DB required | ✅ Zero deps, no infra, fully offline |
101
101
 
102
102
  ---
@@ -120,12 +120,12 @@ Ask → Rank → Context → Validate → Judge → Learn
120
120
 
121
121
  <!--SM:benchmarkBlock-->
122
122
  ```
123
- Benchmark : sigmap-v8.7-main (21 repositories, including R language)
123
+ Benchmark : sigmap-v8.8-main (21 repositories, including R language)
124
124
  Date : 2026-07-05
125
125
 
126
- Hit@5 : 86.7% (baseline 13.6% — 6.4× lift)
126
+ Hit@5 : 87.8% (baseline 13.6% — 6.5× lift)
127
127
  Token reduction: 97.0% (across 21 repos)
128
- Prompt reduction : 48.8% (2.84 → 1.46 prompts per task)
128
+ Prompt reduction : 49.2% (2.84 → 1.44 prompts per task)
129
129
  Task success : 67.8% (baseline 10%)
130
130
  Repos tested : 21 (JavaScript, Python, Go, Rust, Java, R, C++, C#, Dart, Swift, Ruby, PHP, Scala, Kotlin, and more)
131
131
  ```
@@ -220,13 +220,13 @@ Use SigMap with open-source tools and fully self-hosted setups:
220
220
  | **JetBrains** | [Marketplace](https://plugins.jetbrains.com/plugin/31109-sigmap--ai-context-engine/) | [github.com/manojmallick/sigmap-jetbrains](https://github.com/manojmallick/sigmap-jetbrains) | IntelliJ IDEA, WebStorm, PyCharm, GoLand — tool window + actions |
221
221
  | **Neovim** | lazy.nvim / packer / vim-plug | [github.com/manojmallick/sigmap.nvim](https://github.com/manojmallick/sigmap.nvim) | `:SigMap`, `:SigMapQuery` float window, statusline widget |
222
222
 
223
- **MCP server** — 18 on-demand tools for Claude Code and Cursor:
223
+ **MCP server** — 19 on-demand tools for Claude Code and Cursor:
224
224
 
225
225
  ```bash
226
226
  sigmap --mcp
227
227
  ```
228
228
 
229
- Tools: `read_context`, `search_signatures`, `get_map`, `create_checkpoint`, `get_routing`, `explain_file`, `list_modules`, `query_context`, `get_impact`, `get_lines`, `read_memory`, `get_callee_signatures`, `get_diff_context` (changed files + signatures + blast radius), `get_architecture_overview` (modules, hub files, cycles), plus the live-index notifications `sigmap_notify_file_created`, `sigmap_notify_symbol_added`, and `sigmap_notify_file_deleted`. Full reference: [llms-full.txt](llms-full.txt).
229
+ Tools: `read_context`, `search_signatures`, `get_map`, `create_checkpoint`, `get_routing`, `explain_file`, `list_modules`, `query_context`, `get_impact`, `get_lines`, `read_memory`, `get_callee_signatures`, `get_diff_context` (changed files + signatures + blast radius), `get_architecture_overview` (modules, hub files, cycles), `verify_suggestion` (ground AI code against repo + installed libraries), `squeeze_output` (compress noisy tool/log/JSON output mid-session), plus the live-index notifications `sigmap_notify_file_created`, `sigmap_notify_symbol_added`, and `sigmap_notify_file_deleted`. Full reference: [llms-full.txt](llms-full.txt).
230
230
 
231
231
  ---
232
232
 
@@ -265,7 +265,7 @@ SigMap treats coding agents as **consumers, not competitors**: it hands them a d
265
265
 
266
266
  | Agent | One-time setup | How it consumes SigMap |
267
267
  |---|---|---|
268
- | **Claude Code** | `sigmap mcp install claude` | 17 MCP tools (`search_signatures`, `get_lines`, `get_diff_context`…) |
268
+ | **Claude Code** | `sigmap mcp install claude` | 19 MCP tools (`search_signatures`, `get_lines`, `get_diff_context`, `squeeze_output`…) |
269
269
  | **Cursor** | `sigmap mcp install cursor` | MCP tools, plus the `cursor` adapter writes `.cursorrules` |
270
270
  | **Cline** | `sigmap mcp install cursor` | Reads `.cursorrules`; same MCP server |
271
271
  | **Continue** | `sigmap mcp install vscode` | MCP tools inside the Continue extension |
package/gen-context.js CHANGED
@@ -3202,7 +3202,11 @@ __factories["./src/discovery/source-root-resolver"] = function(module, exports)
3202
3202
  score: scoreCandidate(name, full, context),
3203
3203
  }))
3204
3204
  .filter(c => c.score > 0)
3205
- .sort((a, b) => b.score - a.score);
3205
+ // Final tie-break on dir keeps selection deterministic when scores tie — the
3206
+ // top-MAX_ROOTS slice below would otherwise admit different dirs run to run
3207
+ // (candidate order comes from filesystem readdir), changing which files are
3208
+ // collected and making the generated context non-reproducible.
3209
+ .sort((a, b) => b.score - a.score || a.dir.localeCompare(b.dir));
3206
3210
 
3207
3211
  // Handle special rules
3208
3212
  let roots = _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks);
@@ -3245,9 +3249,12 @@ __factories["./src/discovery/source-root-resolver"] = function(module, exports)
3245
3249
  const candidates = [];
3246
3250
  const excSet = new Set(excludeList);
3247
3251
 
3248
- // Root-level dirs
3252
+ // Root-level dirs (sorted so candidate order — and downstream dedupe/selection
3253
+ // — is deterministic regardless of filesystem readdir order)
3249
3254
  try {
3250
- for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
3255
+ const rootEntries = fs.readdirSync(cwd, { withFileTypes: true })
3256
+ .sort((a, b) => a.name.localeCompare(b.name));
3257
+ for (const e of rootEntries) {
3251
3258
  if (!e.isDirectory()) continue;
3252
3259
  if (excSet.has(e.name)) continue;
3253
3260
  if (matchesIgnorePattern(e.name, ignorePatterns)) continue;
@@ -3316,7 +3323,7 @@ __factories["./src/discovery/source-root-resolver"] = function(module, exports)
3316
3323
  }
3317
3324
  }
3318
3325
  } catch (_) {}
3319
- roots.sort((a, b) => b.score - a.score);
3326
+ roots.sort((a, b) => b.score - a.score || a.dir.localeCompare(b.dir));
3320
3327
  }
3321
3328
 
3322
3329
  // Swift project dir: dirs with ≥3 .swift files
@@ -3331,7 +3338,7 @@ __factories["./src/discovery/source-root-resolver"] = function(module, exports)
3331
3338
  }
3332
3339
  }
3333
3340
  } catch (_) {}
3334
- roots.sort((a, b) => b.score - a.score);
3341
+ roots.sort((a, b) => b.score - a.score || a.dir.localeCompare(b.dir));
3335
3342
  }
3336
3343
 
3337
3344
  return roots;
@@ -13345,7 +13352,50 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
13345
13352
  return out.join('\n');
13346
13353
  }
13347
13354
 
13348
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion };
13355
+ /**
13356
+ * squeeze_output({ content }) → string
13357
+ *
13358
+ * Compress noisy tool/command/agent output (stack trace, CI/build log, or JSON
13359
+ * payload) through the deterministic squeeze engine. Enriches the top stack
13360
+ * frame with its signature when the symbol index is available. Passes the input
13361
+ * through unchanged when no squeezable structure is detected.
13362
+ */
13363
+ function squeezeOutput(args, cwd) {
13364
+ const content = args && typeof args.content === 'string' ? args.content : '';
13365
+ if (!content.trim()) {
13366
+ return 'Usage: squeeze_output({ content: "<raw tool/log/JSON output>" }) — provide the output to compress.';
13367
+ }
13368
+
13369
+ let sq;
13370
+ try {
13371
+ const { squeeze } = __require('./src/squeeze/index');
13372
+ let srcDirs;
13373
+ let symbolIndex = null;
13374
+ try {
13375
+ const { loadConfig } = __require('./src/config/loader');
13376
+ srcDirs = loadConfig(cwd).srcDirs;
13377
+ } catch (_) {}
13378
+ try {
13379
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
13380
+ symbolIndex = buildSigIndex(cwd);
13381
+ } catch (_) {}
13382
+ sq = squeeze(content, { srcDirs, symbolIndex });
13383
+ } catch (err) {
13384
+ return `_squeeze_output failed: ${err.message}_`;
13385
+ }
13386
+
13387
+ if (!sq.category || !sq.applies) {
13388
+ return `No squeezable structure detected — content unchanged (${sq.rawTokens} tokens).\n\n${content}`;
13389
+ }
13390
+
13391
+ const pct = Math.round(sq.reduction * 100);
13392
+ const header =
13393
+ `Squeezed ${sq.category} — ${sq.rawTokens} → ${sq.squeezedTokens} tokens ` +
13394
+ `(${pct}% smaller)${sq.enriched ? ' · signature-enriched' : ''}\n\n`;
13395
+ return header + sq.squeezed;
13396
+ }
13397
+
13398
+ module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
13349
13399
 
13350
13400
  };
13351
13401
 
@@ -13506,17 +13556,17 @@ __factories["./src/mcp/server"] = function(module, exports) {
13506
13556
  *
13507
13557
  * Supported methods:
13508
13558
  * initialize → serverInfo + capabilities
13509
- * tools/list → 18 tool definitions
13559
+ * tools/list → 19 tool definitions
13510
13560
  * tools/call → dispatch to handler, return result
13511
13561
  */
13512
13562
 
13513
13563
  const readline = require('readline');
13514
13564
  const { TOOLS } = __require('./src/mcp/tools');
13515
- const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion } = __require('./src/mcp/handlers');
13565
+ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = __require('./src/mcp/handlers');
13516
13566
 
13517
13567
  const SERVER_INFO = {
13518
13568
  name: 'sigmap',
13519
- version: '8.7.1',
13569
+ version: '8.8.1',
13520
13570
  description: 'SigMap MCP server — code signatures on demand',
13521
13571
  };
13522
13572
 
@@ -13582,6 +13632,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
13582
13632
  else if (name === 'get_diff_context') text = getDiffContext(args, cwd);
13583
13633
  else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
13584
13634
  else if (name === 'verify_suggestion') text = verifySuggestion(args, cwd);
13635
+ else if (name === 'squeeze_output') text = squeezeOutput(args, cwd);
13585
13636
  else {
13586
13637
  respondError(id, -32601, `Unknown tool: ${name}`);
13587
13638
  return;
@@ -13642,11 +13693,12 @@ __factories["./src/mcp/server"] = function(module, exports) {
13642
13693
  __factories["./src/mcp/tools"] = function(module, exports) {
13643
13694
 
13644
13695
  /**
13645
- * MCP tool definitions for SigMap (17 tools).
13696
+ * MCP tool definitions for SigMap (19 tools).
13646
13697
  * read_context, search_signatures, get_map, create_checkpoint, get_routing,
13647
13698
  * explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
13648
13699
  * get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
13649
- * sigmap_notify_file_deleted, get_diff_context, get_architecture_overview.
13700
+ * sigmap_notify_file_deleted, get_diff_context, get_architecture_overview,
13701
+ * verify_suggestion, squeeze_output.
13650
13702
  */
13651
13703
 
13652
13704
  const TOOLS = [
@@ -13977,6 +14029,27 @@ __factories["./src/mcp/tools"] = function(module, exports) {
13977
14029
  required: ['code'],
13978
14030
  },
13979
14031
  },
14032
+ {
14033
+ name: 'squeeze_output',
14034
+ description:
14035
+ 'Compress noisy tool, command, or agent output before it enters context — a stack ' +
14036
+ 'trace, CI/build log, or JSON payload. Same deterministic, offline engine as ' +
14037
+ '`sigmap squeeze`: keeps the signal (error frames, failing steps, JSON shape), strips ' +
14038
+ 'the noise, and enriches the top stack frame with its signature. Returns the squeezed ' +
14039
+ 'text plus token-reduction stats; passes the input through unchanged when no squeezable ' +
14040
+ 'structure is detected. No LLM, no network.',
14041
+ inputSchema: {
14042
+ type: 'object',
14043
+ properties: {
14044
+ content: {
14045
+ type: 'string',
14046
+ description:
14047
+ 'The raw output to compress (a stack trace, CI/build log, or JSON payload).',
14048
+ },
14049
+ },
14050
+ required: ['content'],
14051
+ },
14052
+ },
13980
14053
  ];
13981
14054
 
13982
14055
  module.exports = { TOOLS };
@@ -18085,7 +18158,7 @@ function __tryGit(args, opts = {}) {
18085
18158
  catch (_) { return ''; }
18086
18159
  }
18087
18160
 
18088
- const VERSION = '8.7.1';
18161
+ const VERSION = '8.8.1';
18089
18162
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
18090
18163
 
18091
18164
  function requireSourceOrBundled(key) {
@@ -18198,6 +18271,10 @@ function walkDir(dir, exclude, maxDepth, depth = 0) {
18198
18271
  } catch (_) {
18199
18272
  return [];
18200
18273
  }
18274
+ // Sort by name so the collected file list — and therefore the output section
18275
+ // order — is deterministic. Raw readdir order is filesystem-dependent and can
18276
+ // vary run to run, making the generated context non-byte-stable.
18277
+ entries.sort((a, b) => a.name.localeCompare(b.name));
18201
18278
  for (const entry of entries) {
18202
18279
  if (exclude.includes(entry.name)) continue;
18203
18280
  const full = path.join(dir, entry.name);
@@ -18291,6 +18368,26 @@ function annotateCoverage(sigs, testIndex, enabled) {
18291
18368
  // ---------------------------------------------------------------------------
18292
18369
  // Token budget enforcement
18293
18370
  // ---------------------------------------------------------------------------
18371
+ // Deterministic recency boost for recently-committed files so the token budget
18372
+ // doesn't drop them first. Previously this used `Date.now()`, which encoded the
18373
+ // (alphabetical) processing order as a monotonically increasing mtime — but on
18374
+ // repos where nearly every file is "recently changed", consecutive files often
18375
+ // landed on the *same* millisecond. Which files shared a millisecond (and so
18376
+ // fell through to the filePath tie-break instead of sorting by a distinct
18377
+ // mtime) shifted run to run, swapping which files survived at the budget cutoff
18378
+ // and making the output non-byte-stable. A monotonic integer counter preserves
18379
+ // the exact processing-order ranking Date.now() produced — recent files stay
18380
+ // ahead of non-recent ones, ordered among themselves by walk order — but is
18381
+ // collision-free and reproducible. RECENT_MTIME_BASE sits far above any real
18382
+ // filesystem mtime (ms since epoch ≈ 1.7e12) so boosted files always out-rank
18383
+ // non-boosted ones, and far below MAX_SAFE_INTEGER so the counter never
18384
+ // overflows. Call nextRecentMtime() once per boosted file, in walk order. (#440)
18385
+ const RECENT_MTIME_BASE = 1e15;
18386
+ let _recentMtimeSeq = 0;
18387
+ function nextRecentMtime() {
18388
+ return RECENT_MTIME_BASE + (_recentMtimeSeq++);
18389
+ }
18390
+
18294
18391
  function estimateTokens(str) {
18295
18392
  return Math.ceil(str.length / 4);
18296
18393
  }
@@ -18409,10 +18506,15 @@ function applyTokenBudget(fileEntries, maxTokens) {
18409
18506
  });
18410
18507
 
18411
18508
  // Best-first order: lowest drop-priority, then most-recent, then most-informative.
18509
+ // Final tie-break on filePath keeps the selection deterministic when priority,
18510
+ // mtime, and signalQuality all tie (common for test files sharing a git-checkout
18511
+ // mtime) — otherwise the choice fell through to filesystem readdir order, making
18512
+ // which files are kept vs dropped non-reproducible run to run.
18412
18513
  const bestFirst = withPriority.slice().sort((a, b) => {
18413
18514
  if (a.priority !== b.priority) return a.priority - b.priority;
18414
18515
  if ((b.mtime || 0) !== (a.mtime || 0)) return (b.mtime || 0) - (a.mtime || 0);
18415
- return (b.signalQuality || 0) - (a.signalQuality || 0);
18516
+ if ((b.signalQuality || 0) !== (a.signalQuality || 0)) return (b.signalQuality || 0) - (a.signalQuality || 0);
18517
+ return a.filePath.localeCompare(b.filePath);
18416
18518
  });
18417
18519
 
18418
18520
  const entryCost = (e) => estimateTokens(e.sigs.join('\n')) + sectionOverhead(e);
@@ -19266,7 +19368,7 @@ function runDiff(cwd, config, stagedOnly, baseRef) {
19266
19368
 
19267
19369
  sigs = annotateCoverage(sigs, testIndex, !!config.testCoverage);
19268
19370
 
19269
- fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime: Date.now() });
19371
+ fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime: nextRecentMtime() });
19270
19372
  }
19271
19373
 
19272
19374
  if (fileEntries.length === 0) {
@@ -19414,8 +19516,11 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
19414
19516
  mtime = fs.statSync(filePath).mtimeMs;
19415
19517
  } catch (_) {}
19416
19518
 
19417
- // Boost recently committed files (give them max mtime so they aren't dropped first)
19418
- if (recentFiles.has(filePath)) mtime = Date.now();
19519
+ // Boost recently committed files (give them a max mtime so they aren't
19520
+ // dropped first). A deterministic monotonic counter — not Date.now() — keeps
19521
+ // the budget selection byte-stable when many files are recent, while
19522
+ // preserving the original processing-order ranking (see nextRecentMtime, #440).
19523
+ if (recentFiles.has(filePath)) mtime = nextRecentMtime();
19419
19524
 
19420
19525
  fileEntries.push({ filePath, sigs, deps: extractFileDeps(filePath, content, config), content, mtime });
19421
19526
  }
@@ -19911,6 +20016,7 @@ Usage:
19911
20016
  ${cmd} review-pr --markdown PR Evidence Report — branded Markdown (signatures + blast radius + tests) to post as a PR comment
19912
20017
  ${cmd} create "<task>" Grounded-creation pipeline: scaffold → verify-plan → verify-ai-output → review-pr (--staged)
19913
20018
  ${cmd} squeeze <file|-> Minimize a pasted stacktrace/CI-log/JSON blob (--json for stats)
20019
+ ${cmd} squeeze --response <file|-> Minimize an agent/tool response (same engine; also exposed as the squeeze_output MCP tool)
19914
20020
  ${cmd} ask "<query>" --squeeze Auto-accept input minimization (no prompt; for scripts/CI)
19915
20021
  ${cmd} ask "<query>" --no-squeeze Disable input minimization entirely
19916
20022
  ${cmd} ask "<query>" --squeeze-threshold N Min reduction %% to prompt (default 30)
@@ -21681,7 +21787,20 @@ function main() {
21681
21787
  // v7.0.0: `sigmap squeeze <file|->` — minimize a pasted stacktrace / CI-log / JSON blob
21682
21788
  if (args[0] === 'squeeze') {
21683
21789
  const jsonOut = args.includes('--json');
21684
- const target = args[1] && !args[1].startsWith('--') ? args[1] : '-';
21790
+ // `--response <file|->` names the agent/tool response to squeeze explicitly
21791
+ // (mirrors `sigmap judge --response`); otherwise fall back to the positional arg.
21792
+ const respIdx = args.indexOf('--response');
21793
+ let target;
21794
+ if (respIdx !== -1) {
21795
+ const val = args[respIdx + 1];
21796
+ if (!val || val.startsWith('--')) {
21797
+ console.error('[sigmap] Usage: sigmap squeeze --response <file|-> [--json]');
21798
+ process.exit(1);
21799
+ }
21800
+ target = val;
21801
+ } else {
21802
+ target = args[1] && !args[1].startsWith('--') ? args[1] : '-';
21803
+ }
21685
21804
  let input = '';
21686
21805
  try {
21687
21806
  input = fs.readFileSync(target === '-' ? 0 : path.resolve(cwd, target), 'utf8');
package/llms-full.txt CHANGED
@@ -11,22 +11,22 @@ 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.7.1 | Benchmark: sigmap-v8.7-main (2026-07-05)
14
+ # Version: 8.8.1 | Benchmark: sigmap-v8.8-main (2026-07-05)
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.7-main, 2026-07-05)
20
+ ## Core metrics (benchmark: sigmap-v8.8-main, 2026-07-05)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
24
- | Retrieval hit@5 | 13.6% (random) | 86.7% (6.4× lift) |
24
+ | Retrieval hit@5 | 13.6% (random) | 87.8% (6.5× lift) |
25
25
  | Token reduction | — | 97.0% average |
26
26
  | Task success proxy | 10% | 67.8% |
27
- | Prompts per task | 2.84 | 1.46 (48.8% fewer) |
27
+ | Prompts per task | 2.84 | 1.44 (49.2% fewer) |
28
28
  | Supported languages | — | 33 |
29
- | MCP tools | — | 18 |
29
+ | MCP tools | — | 19 |
30
30
  | npm runtime dependencies | — | 0 |
31
31
 
32
32
  ---
@@ -113,6 +113,7 @@ sigmap review-pr Audit a diff — scope drift, god-node
113
113
  sigmap review-pr --markdown PR Evidence Report — branded Markdown (signatures + blast radius + tests) to post as a PR comment
114
114
  sigmap create "<task>" Grounded-creation pipeline: scaffold → verify-plan → verify-ai-output → review-pr (--staged)
115
115
  sigmap squeeze <file|-> Minimize a pasted stacktrace/CI-log/JSON blob (--json for stats)
116
+ sigmap squeeze --response <file|-> Minimize an agent/tool response (same engine; also exposed as the squeeze_output MCP tool)
116
117
  sigmap ask "<query>" --squeeze Auto-accept input minimization (no prompt; for scripts/CI)
117
118
  sigmap ask "<query>" --no-squeeze Disable input minimization entirely
118
119
  sigmap ask "<query>" --squeeze-threshold N Min reduction %% to prompt (default 30)
@@ -132,7 +133,7 @@ sigmap --version Show version
132
133
 
133
134
  ---
134
135
 
135
- ## MCP server — 18 tools
136
+ ## MCP server — 19 tools
136
137
 
137
138
  Start with `sigmap --mcp` (stdio JSON-RPC). Configure once:
138
139
 
@@ -284,6 +285,14 @@ Ground an AI code suggestion before writing it: verify a snippet or answer again
284
285
  Input: { code: string }
285
286
  ```
286
287
 
288
+ ### squeeze_output
289
+
290
+ Compress noisy tool, command, or agent output before it enters context — a stack trace, CI/build log, or JSON payload. Same deterministic, offline engine as `sigmap squeeze`: keeps the signal (error frames, failing steps, JSON shape), strips the noise, and enriches the top stack frame with its signature. Returns the squeezed text plus token-reduction stats; passes the input through unchanged when no squeezable structure is detected. No LLM, no network.
291
+
292
+ ```
293
+ Input: { content: string }
294
+ ```
295
+
287
296
  ---
288
297
 
289
298
  ## Configuration (gen-context.config.json)
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.7.1 | Benchmark: sigmap-v8.7-main (2026-07-05)
14
+ # Version: 8.8.1 | Benchmark: sigmap-v8.8-main (2026-07-05)
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,13 +23,13 @@ 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.7-main, 2026-07-05)
26
+ ## Core metrics (benchmark: sigmap-v8.8-main, 2026-07-05)
27
27
 
28
- - hit@5 retrieval: 86.7% vs 13.6% random baseline (6.4× lift)
28
+ - hit@5 retrieval: 87.8% vs 13.6% random baseline (6.5× lift)
29
29
  - Token reduction: 97.0% average across benchmark repos
30
30
  - Task success: 67.8% vs 10% without SigMap
31
- - Prompts per task: 1.46 vs 2.84 baseline (48.8% fewer)
32
- - Languages: 33 supported · MCP tools: 18
31
+ - Prompts per task: 1.44 vs 2.84 baseline (49.2% fewer)
32
+ - Languages: 33 supported · MCP tools: 19
33
33
  - Dependencies: zero npm runtime dependencies · fully offline
34
34
 
35
35
  ## Quick start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.7.1",
3
+ "version": "8.8.1",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.7.1",
3
+ "version": "8.8.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": "8.7.1",
3
+ "version": "8.8.1",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -43,7 +43,11 @@ function resolveSourceRoots(cwd, opts = {}) {
43
43
  score: scoreCandidate(name, full, context),
44
44
  }))
45
45
  .filter(c => c.score > 0)
46
- .sort((a, b) => b.score - a.score);
46
+ // Final tie-break on dir keeps selection deterministic when scores tie — the
47
+ // top-MAX_ROOTS slice below would otherwise admit different dirs run to run
48
+ // (candidate order comes from filesystem readdir), changing which files are
49
+ // collected and making the generated context non-reproducible.
50
+ .sort((a, b) => b.score - a.score || a.dir.localeCompare(b.dir));
47
51
 
48
52
  // Handle special rules
49
53
  let roots = _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks);
@@ -86,9 +90,12 @@ function _enumerateCandidates(cwd, isMonorepo, ignorePatterns, excludeList) {
86
90
  const candidates = [];
87
91
  const excSet = new Set(excludeList);
88
92
 
89
- // Root-level dirs
93
+ // Root-level dirs (sorted so candidate order — and downstream dedupe/selection
94
+ // — is deterministic regardless of filesystem readdir order)
90
95
  try {
91
- for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
96
+ const rootEntries = fs.readdirSync(cwd, { withFileTypes: true })
97
+ .sort((a, b) => a.name.localeCompare(b.name));
98
+ for (const e of rootEntries) {
92
99
  if (!e.isDirectory()) continue;
93
100
  if (excSet.has(e.name)) continue;
94
101
  if (matchesIgnorePattern(e.name, ignorePatterns)) continue;
@@ -157,7 +164,7 @@ function _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks) {
157
164
  }
158
165
  }
159
166
  } catch (_) {}
160
- roots.sort((a, b) => b.score - a.score);
167
+ roots.sort((a, b) => b.score - a.score || a.dir.localeCompare(b.dir));
161
168
  }
162
169
 
163
170
  // Swift project dir: dirs with ≥3 .swift files
@@ -172,7 +179,7 @@ function _applySpecialRules(scored, cwd, primaryFw, fwEntry, frameworks) {
172
179
  }
173
180
  }
174
181
  } catch (_) {}
175
- roots.sort((a, b) => b.score - a.score);
182
+ roots.sort((a, b) => b.score - a.score || a.dir.localeCompare(b.dir));
176
183
  }
177
184
 
178
185
  return roots;
@@ -898,4 +898,47 @@ function verifySuggestion(args, cwd) {
898
898
  return out.join('\n');
899
899
  }
900
900
 
901
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion };
901
+ /**
902
+ * squeeze_output({ content }) → string
903
+ *
904
+ * Compress noisy tool/command/agent output (stack trace, CI/build log, or JSON
905
+ * payload) through the deterministic squeeze engine. Enriches the top stack
906
+ * frame with its signature when the symbol index is available. Passes the input
907
+ * through unchanged when no squeezable structure is detected.
908
+ */
909
+ function squeezeOutput(args, cwd) {
910
+ const content = args && typeof args.content === 'string' ? args.content : '';
911
+ if (!content.trim()) {
912
+ return 'Usage: squeeze_output({ content: "<raw tool/log/JSON output>" }) — provide the output to compress.';
913
+ }
914
+
915
+ let sq;
916
+ try {
917
+ const { squeeze } = require('../squeeze/index');
918
+ let srcDirs;
919
+ let symbolIndex = null;
920
+ try {
921
+ const { loadConfig } = require('../config/loader');
922
+ srcDirs = loadConfig(cwd).srcDirs;
923
+ } catch (_) {}
924
+ try {
925
+ const { buildSigIndex } = require('../retrieval/ranker');
926
+ symbolIndex = buildSigIndex(cwd);
927
+ } catch (_) {}
928
+ sq = squeeze(content, { srcDirs, symbolIndex });
929
+ } catch (err) {
930
+ return `_squeeze_output failed: ${err.message}_`;
931
+ }
932
+
933
+ if (!sq.category || !sq.applies) {
934
+ return `No squeezable structure detected — content unchanged (${sq.rawTokens} tokens).\n\n${content}`;
935
+ }
936
+
937
+ const pct = Math.round(sq.reduction * 100);
938
+ const header =
939
+ `Squeezed ${sq.category} — ${sq.rawTokens} → ${sq.squeezedTokens} tokens ` +
940
+ `(${pct}% smaller)${sq.enriched ? ' · signature-enriched' : ''}\n\n`;
941
+ return header + sq.squeezed;
942
+ }
943
+
944
+ module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput };
package/src/mcp/server.js CHANGED
@@ -8,17 +8,17 @@
8
8
  *
9
9
  * Supported methods:
10
10
  * initialize → serverInfo + capabilities
11
- * tools/list → 18 tool definitions
11
+ * tools/list → 19 tool definitions
12
12
  * tools/call → dispatch to handler, return result
13
13
  */
14
14
 
15
15
  const readline = require('readline');
16
16
  const { TOOLS } = require('./tools');
17
- const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion } = require('./handlers');
17
+ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted, getDiffContext, getArchitectureOverview, verifySuggestion, squeezeOutput } = require('./handlers');
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.7.1',
21
+ version: '8.8.1',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -84,6 +84,7 @@ function dispatch(msg, cwd) {
84
84
  else if (name === 'get_diff_context') text = getDiffContext(args, cwd);
85
85
  else if (name === 'get_architecture_overview') text = getArchitectureOverview(args, cwd);
86
86
  else if (name === 'verify_suggestion') text = verifySuggestion(args, cwd);
87
+ else if (name === 'squeeze_output') text = squeezeOutput(args, cwd);
87
88
  else {
88
89
  respondError(id, -32601, `Unknown tool: ${name}`);
89
90
  return;
package/src/mcp/tools.js CHANGED
@@ -1,11 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * MCP tool definitions for SigMap (17 tools).
4
+ * MCP tool definitions for SigMap (19 tools).
5
5
  * read_context, search_signatures, get_map, create_checkpoint, get_routing,
6
6
  * explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
7
7
  * get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
8
- * sigmap_notify_file_deleted, get_diff_context, get_architecture_overview.
8
+ * sigmap_notify_file_deleted, get_diff_context, get_architecture_overview,
9
+ * verify_suggestion, squeeze_output.
9
10
  */
10
11
 
11
12
  const TOOLS = [
@@ -336,6 +337,27 @@ const TOOLS = [
336
337
  required: ['code'],
337
338
  },
338
339
  },
340
+ {
341
+ name: 'squeeze_output',
342
+ description:
343
+ 'Compress noisy tool, command, or agent output before it enters context — a stack ' +
344
+ 'trace, CI/build log, or JSON payload. Same deterministic, offline engine as ' +
345
+ '`sigmap squeeze`: keeps the signal (error frames, failing steps, JSON shape), strips ' +
346
+ 'the noise, and enriches the top stack frame with its signature. Returns the squeezed ' +
347
+ 'text plus token-reduction stats; passes the input through unchanged when no squeezable ' +
348
+ 'structure is detected. No LLM, no network.',
349
+ inputSchema: {
350
+ type: 'object',
351
+ properties: {
352
+ content: {
353
+ type: 'string',
354
+ description:
355
+ 'The raw output to compress (a stack trace, CI/build log, or JSON payload).',
356
+ },
357
+ },
358
+ required: ['content'],
359
+ },
360
+ },
339
361
  ];
340
362
 
341
363
  module.exports = { TOOLS };