sigmap 8.34.0 → 8.35.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.
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { capWithNotice } = require('../util/truncate');
4
+
3
5
  /**
4
6
  * Lightweight XML config extractor.
5
7
  * Captures root tags, key config tags, and id/name/class attributes.
@@ -40,7 +42,7 @@ function extract(src) {
40
42
  if (cls) sigs.push(`${tag} -> ${cls[1]}`);
41
43
  }
42
44
 
43
- return Array.from(new Set(sigs)).slice(0, 50);
45
+ return capWithNotice(Array.from(new Set(sigs)), 200, 'signatures');
44
46
  }
45
47
 
46
48
  module.exports = { extract };
@@ -4,7 +4,7 @@ const { capWithNotice } = require('../util/truncate');
4
4
 
5
5
  // Ceiling sits above the default `maxSigsPerFile` so the configured budget
6
6
  // governs output rather than a literal buried here, and omissions are disclosed (#576).
7
- const PER_FILE_LIMIT = 25;
7
+ const PER_FILE_LIMIT = 200;
8
8
 
9
9
  /**
10
10
  * Extract signatures from YAML configuration files.
package/src/mcp/server.js CHANGED
@@ -7,8 +7,8 @@
7
7
  * One JSON object per line on both stdin and stdout.
8
8
  *
9
9
  * Supported methods:
10
- * initialize → serverInfo + capabilities
11
- * tools/list → 19 tool definitions
10
+ * initialize → serverInfo + capabilities + negotiated protocolVersion
11
+ * tools/list → 21 tool definitions
12
12
  * tools/call → dispatch to handler, return result
13
13
  */
14
14
 
@@ -18,10 +18,22 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.34.0',
21
+ version: '8.35.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
25
+ // Protocol revisions this server actually speaks. The tools-only surface —
26
+ // initialize / tools/list / tools/call plus the initialized/cancelled
27
+ // notifications — is identical across these revisions, and the
28
+ // @hasmcp/mcp-spec-test suite passes every applicable 2025-11-25 case against
29
+ // it. 2026-07-28 is deliberately absent: that revision requires
30
+ // server/discover, which is not implemented. Newest first: a client offering
31
+ // a version outside this list is downgraded to [0], never echoed back —
32
+ // echoing an unspeakable version is itself a spec violation (#544), and it
33
+ // made the conformance suite believe 2026-07-28 was supported, producing six
34
+ // phantom server/discover failures on a revision never really offered (#545).
35
+ const SUPPORTED_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'];
36
+
25
37
  // ---------------------------------------------------------------------------
26
38
  // JSON-RPC helpers
27
39
  // ---------------------------------------------------------------------------
@@ -46,9 +58,32 @@ function dispatch(msg, cwd) {
46
58
  return;
47
59
  }
48
60
 
61
+ // server/discover (spec 2026-07-28) — session-less discovery, answerable
62
+ // before any handshake, so a client can learn the honest version list
63
+ // instead of offering versions and hoping. The response is a
64
+ // CacheableResult: deterministic, so the TTL promise of stability holds.
65
+ // Note supportedVersions does NOT include 2026-07-28 — answering discover
66
+ // is forward-compatible plumbing, not a claim to serve that revision's
67
+ // whole surface (per-result envelopes, inline _meta negotiation).
68
+ if (method === 'server/discover') {
69
+ respond(id, {
70
+ resultType: 'complete',
71
+ cacheScope: 'public',
72
+ ttlMs: 3600000,
73
+ supportedVersions: SUPPORTED_PROTOCOL_VERSIONS,
74
+ capabilities: { tools: {} },
75
+ serverInfo: SERVER_INFO,
76
+ instructions: 'SigMap serves code signatures for the working directory. Call query_context or search_signatures to rank and fetch signature blocks instead of reading whole files.',
77
+ });
78
+ return;
79
+ }
80
+
49
81
  if (method === 'initialize') {
82
+ const offered = params && params.protocolVersion;
50
83
  respond(id, {
51
- protocolVersion: (params && params.protocolVersion) || '2024-11-05',
84
+ protocolVersion: SUPPORTED_PROTOCOL_VERSIONS.includes(offered)
85
+ ? offered
86
+ : SUPPORTED_PROTOCOL_VERSIONS[0],
52
87
  serverInfo: SERVER_INFO,
53
88
  capabilities: { tools: {} },
54
89
  });
@@ -56,6 +91,13 @@ function dispatch(msg, cwd) {
56
91
  }
57
92
 
58
93
  if (method === 'tools/list') {
94
+ // No pagination: the full list fits one page, so any cursor a client
95
+ // presents is one this server never issued — reject it (-32602, per the
96
+ // spec's SHOULD) rather than silently restarting from page one.
97
+ if (params && params.cursor !== undefined) {
98
+ respondError(id, -32602, `Invalid cursor: ${String(params.cursor)}`);
99
+ return;
100
+ }
59
101
  respond(id, { tools: TOOLS });
60
102
  return;
61
103
  }
@@ -376,9 +376,15 @@ function rank(query, sigIndex, opts) {
376
376
  const hop1Files = new Set(); // normalised keys that received a hop1 boost
377
377
  const hop1Seeds = []; // original (un-normalised) paths, for hop-2 lookup
378
378
 
379
- // Hop 1: direct neighbors of scored files
380
- for (const entry of scored) {
381
- if (entry.score <= 0) continue;
379
+ // Hop 1: direct neighbors of scored files. Seeds snapshotted BEFORE the
380
+ // loop (as the call-graph block below already does) so boosts never
381
+ // cascade: a file whose only score is a hop-1 boost must not become a seed
382
+ // itself mid-loop, or the effective seed set — and every boost total —
383
+ // depends on index insertion order, which git history changes via the
384
+ // recent-commits hoist. That made gate scores differ between a shallow CI
385
+ // checkout and a developer clone of the same commit (#596).
386
+ const hop1SeedEntries = scored.filter((e) => e.score > 0);
387
+ for (const entry of hop1SeedEntries) {
382
388
  const neighbors = _graphGet(graph.forward, path.resolve(cwd, entry.file)) || [];
383
389
  for (const neighborAbs of neighbors) {
384
390
  const nk = path.normalize(neighborAbs);
@@ -393,7 +399,10 @@ function rank(query, sigIndex, opts) {
393
399
  }
394
400
  }
395
401
 
396
- // Hop 2: neighbors of hop1 files (only if they didn't get a direct score)
402
+ // Hop 2: neighbors of hop1 files (only if they didn't get a direct score).
403
+ // Eligibility frozen after hop-1 for the same reason: a file whose first
404
+ // score is a hop-2 boost must not become hop-2-eligible mid-loop (#596).
405
+ const hop2Eligible = scored.map((e) => e.score > 0);
397
406
  for (const hop1Key of hop1Seeds) {
398
407
  if (_graphGet(keyToIdx, hop1Key) === undefined) continue; // skip files not in index
399
408
  const neighbors = _graphGet(graph.forward, hop1Key) || [];
@@ -402,7 +411,7 @@ function rank(query, sigIndex, opts) {
402
411
  if (_isHub(nk) || hubs.has(nk) || hubs.has(nk.toLowerCase())) continue;
403
412
  if (hop1Files.has(nk)) continue; // skip already hop1-boosted
404
413
  const idx = _graphGet(keyToIdx, nk);
405
- if (idx !== undefined && scored[idx].score > 0) {
414
+ if (idx !== undefined && hop2Eligible[idx]) {
406
415
  // Only boost files that have some baseline score (not noise)
407
416
  scored[idx].score += GRAPH_BOOST_AMOUNTS.hop2;
408
417
  scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop2;