sigmap 8.29.0 → 8.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,16 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [8.30.0] — 2026-09-07
14
+
15
+ ### Added
16
+ - `sigmap-task` — an invokable prompt skill that drives the full grounding loop from the CLI, for environments where MCP is unavailable. Installs to `.github/prompts/sigmap-task.prompt.md` for Copilot (`/sigmap-task`) and through the normal skill path for other clients (#554)
17
+
18
+ ### Fixed
19
+ - Java extractor: lifted three hard-coded caps that hid 85% of the API surface on real Java repos — an 8-member-per-class limit applied silently, a 25-signature-per-file cap that shadowed `maxSigsPerFile`, and a 5,000-character class-body scan limit. Omissions are now disclosed with a `… +N more` marker, matching the JS/TS path (#552)
20
+ - Retrieval: generated data holders no longer outrank real logic. A file whose members are overwhelmingly trivial accessors is demoted like other generated code, with an escape hatch when the query asks for an entity, model, DTO or accessor. Entities remain retrievable by their own symbols (#552)
21
+ - `sigmap mcp install vscode` wrote a config VS Code ignores — it emitted a top-level `mcpServers` key where VS Code requires `servers` with an explicit `type`. A config written by an earlier version is now migrated rather than left in place (#557)
22
+
13
23
  ## [8.29.0] — 2026-09-01
14
24
 
15
25
  Minor release — **"Retrieval Index Split" (v8.29)**: the ranker stops reading the token-budgeted prompt artifact, and the ranking features that were silently inert start executing. Plus the first benchmark corpus this project did not author itself.
package/README.md CHANGED
@@ -122,8 +122,8 @@ Ask → Rank → Context → Validate → Judge → Learn
122
122
 
123
123
  <!--SM:benchmarkBlock-->
124
124
  ```
125
- Benchmark : sigmap-v8.29-main (21 repositories, including R language)
126
- Date : 2026-09-01
125
+ Benchmark : sigmap-v8.30-main (21 repositories, including R language)
126
+ Date : 2026-09-07
127
127
 
128
128
  Hit@5 : 81.1% (grep-agent baseline 44.0% — 1.73× lift)
129
129
  Token reduction: 96.8% (across 21 repos)
package/gen-context.js CHANGED
@@ -6424,6 +6424,19 @@ __factories["./src/extractors/html"] = function(module, exports) {
6424
6424
  __factories["./src/extractors/java"] = function(module, exports) {
6425
6425
 
6426
6426
  const { lineAt, withAnchor } = __require('./src/extractors/line-anchor');
6427
+ const { capWithNotice, capMembersWithNotice } = __require('./src/util/truncate');
6428
+
6429
+ // Class bodies are scanned to this many characters. Generated JVM sources
6430
+ // (MyBatis/JPA entities) routinely run past 10KB, so the ceiling only guards
6431
+ // against pathological input rather than trimming ordinary classes.
6432
+ const MAX_CLASS_BODY_CHARS = 200000;
6433
+
6434
+ // Per-class member ceiling. Sits above the default `maxSigsPerFile` so the
6435
+ // caller's configured budget governs the output rather than this file.
6436
+ const MAX_MEMBERS_PER_CLASS = 120;
6437
+
6438
+ // Per-file signature ceiling, likewise above the configured default.
6439
+ const MAX_SIGS_PER_FILE = 200;
6427
6440
 
6428
6441
  /**
6429
6442
  * Extract signatures from Java source code.
@@ -6451,17 +6464,20 @@ __factories["./src/extractors/java"] = function(module, exports) {
6451
6464
  const block = extractBlock(stripped, bodyStart);
6452
6465
  sigs.push(hinted(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[2]));
6453
6466
  for (const meth of extractMembers(block)) {
6454
- sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)), meth.name));
6467
+ // The disclosure marker carries no offsets; anchor it at the class body.
6468
+ const declIdx = meth.declIdx || 0;
6469
+ const endIdx = meth.endIdx || 0;
6470
+ sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + declIdx), lineAt(stripped, bodyStart + endIdx)), meth.name));
6455
6471
  }
6456
6472
  }
6457
6473
 
6458
- return sigs.slice(0, 25);
6474
+ return capWithNotice(sigs, MAX_SIGS_PER_FILE, 'signatures');
6459
6475
  }
6460
6476
 
6461
6477
  function extractBlock(src, startIndex) {
6462
6478
  let depth = 1;
6463
6479
  let i = startIndex;
6464
- const end = Math.min(src.length, startIndex + 5000);
6480
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
6465
6481
  while (i < end && depth > 0) {
6466
6482
  if (src[i] === '{') depth++;
6467
6483
  else if (src[i] === '}') depth--;
@@ -6483,7 +6499,7 @@ __factories["./src/extractors/java"] = function(module, exports) {
6483
6499
  endIdx: m.index + m[0].length,
6484
6500
  });
6485
6501
  }
6486
- return members.slice(0, 8);
6502
+ return capMembersWithNotice(members, MAX_MEMBERS_PER_CLASS);
6487
6503
  }
6488
6504
 
6489
6505
  function normalizeParams(params) {
@@ -15250,6 +15266,7 @@ __factories["./src/mcp/install"] = function(module, exports) {
15250
15266
 
15251
15267
  // Config shapes the supported clients use.
15252
15268
  // - 'json' → { mcpServers: { sigmap: { command, args } } }
15269
+ // - 'vscode'→ { servers: { sigmap: { type: 'stdio', command, args } } }
15253
15270
  // - 'zed' → { context_servers: { sigmap: { command: { path, args } } } }
15254
15271
  // - 'yaml' → Codex CLI ~/.codex/config.yaml (mcpServers block, appended)
15255
15272
  const CLIENTS = {
@@ -15258,7 +15275,7 @@ __factories["./src/mcp/install"] = function(module, exports) {
15258
15275
  windsurf: { label: 'Windsurf', format: 'json', scope: 'both',
15259
15276
  project: ['.windsurf', 'mcp.json'],
15260
15277
  global: ['.codeium', 'windsurf', 'mcp_config.json'] },
15261
- vscode: { label: 'VS Code', format: 'json', scope: 'project', project: ['.vscode', 'mcp.json'] },
15278
+ vscode: { label: 'VS Code', format: 'vscode', scope: 'project', project: ['.vscode', 'mcp.json'] },
15262
15279
  opencode: { label: 'OpenCode', format: 'json', scope: 'both',
15263
15280
  project: ['opencode.json'],
15264
15281
  global: ['.config', 'opencode', 'config.json'] },
@@ -15313,6 +15330,31 @@ __factories["./src/mcp/install"] = function(module, exports) {
15313
15330
  return 'installed';
15314
15331
  }
15315
15332
 
15333
+ /**
15334
+ * Install into VS Code's `.vscode/mcp.json`, which keys servers under `servers`
15335
+ * (not `mcpServers`) and expects an explicit transport `type`. A config written
15336
+ * by an older SigMap under `mcpServers` is migrated rather than left in place,
15337
+ * so re-running repairs it instead of leaving two entries VS Code cannot read.
15338
+ */
15339
+ function _installVscode(filePath, scriptPath) {
15340
+ let settings = {};
15341
+ if (fs.existsSync(filePath)) {
15342
+ try { settings = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {}; }
15343
+ catch (_) { settings = {}; }
15344
+ }
15345
+ const stale = settings.mcpServers && settings.mcpServers.sigmap;
15346
+ if (stale) {
15347
+ delete settings.mcpServers.sigmap;
15348
+ if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
15349
+ }
15350
+ if (!settings.servers) settings.servers = {};
15351
+ if (settings.servers.sigmap && !stale) return 'already';
15352
+ settings.servers.sigmap = { type: 'stdio', command: 'node', args: serverArgs(scriptPath) };
15353
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15354
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
15355
+ return stale ? 'updated' : 'installed';
15356
+ }
15357
+
15316
15358
  /** Install into Zed's `context_servers` config (create file/dir if absent). */
15317
15359
  function _installZed(filePath, scriptPath) {
15318
15360
  let settings = {};
@@ -15365,7 +15407,8 @@ __factories["./src/mcp/install"] = function(module, exports) {
15365
15407
  const filePath = resolveTarget(spec, cwd, home, opts.global);
15366
15408
 
15367
15409
  let status;
15368
- if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
15410
+ if (spec.format === 'vscode') status = _installVscode(filePath, scriptPath);
15411
+ else if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
15369
15412
  else if (spec.format === 'yaml') status = _installYaml(filePath, scriptPath);
15370
15413
  else status = _installJson(filePath, scriptPath);
15371
15414
 
@@ -15397,7 +15440,7 @@ __factories["./src/mcp/server"] = function(module, exports) {
15397
15440
 
15398
15441
  const SERVER_INFO = {
15399
15442
  name: 'sigmap',
15400
- version: '8.29.0',
15443
+ version: '8.30.0',
15401
15444
  description: 'SigMap MCP server — code signatures on demand',
15402
15445
  };
15403
15446
 
@@ -16760,26 +16803,52 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
16760
16803
  generatedCode: 0.3, // dist/build/.next in path
16761
16804
  docsFile: 0.2, // docs/doc/README in path
16762
16805
  nodeModules: 0.0, // node_modules (zero score)
16806
+ dataHolder: 0.3, // generated POJO/entity: almost entirely accessors
16763
16807
  };
16764
16808
 
16809
+ // A file whose members are overwhelmingly trivial accessors is a data holder,
16810
+ // not logic. Path-based detection cannot see these: generated JPA/MyBatis
16811
+ // entities live in ordinary source trees. They match a query on any column
16812
+ // name they happen to carry (`getNote`/`setNote` matches "note" as strongly as
16813
+ // the service that actually implements order notes), so on an entity-heavy
16814
+ // repo they crowd real code out of the top results.
16815
+ const ACCESSOR_RE = /^\s*(get|set|is)[A-Z]\w*\s*\(/;
16816
+ const DATA_HOLDER_RATIO = 0.8;
16817
+ const DATA_HOLDER_MIN_MEMBERS = 6;
16818
+
16765
16819
  // Query terms that mean the penalised category IS the target. Read from the
16766
16820
  // query tokens directly, NOT via detectIntent: that classifier is first-match-
16767
16821
  // wins over its pattern object, and `debug` precedes `test`, so "fix the failing
16768
16822
  // test" classifies as debug and never reaches the test branch.
16769
16823
  const WANTS_TESTS = new Set(['test', 'tests', 'spec', 'specs', 'unit', 'integration', 'e2e', 'assertion', 'assert', 'mock', 'fixture', 'coverage', 'testing']);
16770
16824
  const WANTS_DOCS = new Set(['doc', 'docs', 'documentation', 'readme', 'changelog', 'guide', 'tutorial']);
16825
+ const WANTS_MODELS = new Set(['entity', 'entities', 'model', 'models', 'pojo', 'dto', 'bean', 'getter', 'getters', 'setter', 'setters', 'accessor', 'accessors', 'field', 'fields', 'column', 'columns', 'schema']);
16771
16826
 
16772
16827
  /** Which penalised categories the query is explicitly asking for. */
16773
16828
  function _queryWants(queryTokens) {
16774
- const wants = { tests: false, docs: false };
16829
+ const wants = { tests: false, docs: false, models: false };
16775
16830
  for (const t of queryTokens || []) {
16776
16831
  if (WANTS_TESTS.has(t)) wants.tests = true;
16777
16832
  if (WANTS_DOCS.has(t)) wants.docs = true;
16833
+ if (WANTS_MODELS.has(t)) wants.models = true;
16778
16834
  }
16779
16835
  return wants;
16780
16836
  }
16781
16837
 
16782
- function _computePenalty(filePath, wants) {
16838
+ /**
16839
+ * True when a file's members are overwhelmingly trivial accessors — a generated
16840
+ * entity or POJO rather than logic. Type declarations are excluded from the
16841
+ * ratio so a small class is not misjudged by its own `class X` line.
16842
+ */
16843
+ function _isDataHolder(sigs) {
16844
+ if (!Array.isArray(sigs)) return false;
16845
+ const members = sigs.filter((line) => /^\s/.test(line) || !/^(class|interface|enum|struct|function|module\.exports)\b/.test(line));
16846
+ if (members.length < DATA_HOLDER_MIN_MEMBERS) return false;
16847
+ const accessors = members.filter((line) => ACCESSOR_RE.test(line)).length;
16848
+ return accessors / members.length >= DATA_HOLDER_RATIO;
16849
+ }
16850
+
16851
+ function _computePenalty(filePath, wants, sigs) {
16783
16852
  const pathLower = filePath.toLowerCase();
16784
16853
  if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
16785
16854
  // A penalty must never fire on the very thing the user asked for. Before
@@ -16792,6 +16861,11 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
16792
16861
  if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) {
16793
16862
  return (wants && wants.docs) ? 1.0 : PENALTY_SIGNALS.docsFile;
16794
16863
  }
16864
+ // Content-based, and last: a data holder is still a real source file, so it
16865
+ // is only demoted once the path-based categories have had their say.
16866
+ if (_isDataHolder(sigs)) {
16867
+ return (wants && wants.models) ? 1.0 : PENALTY_SIGNALS.dataHolder;
16868
+ }
16795
16869
  return 1.0;
16796
16870
  }
16797
16871
 
@@ -16849,7 +16923,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
16849
16923
  if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
16850
16924
 
16851
16925
  const w = weights || DEFAULT_WEIGHTS;
16852
- const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants) };
16926
+ const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants, sigs) };
16853
16927
 
16854
16928
  // Module-doc prose is excluded here on purpose. This signal measures overlap
16855
16929
  // with DECLARED IDENTIFIERS; prose relevance is BM25's job, where it is scored
@@ -17445,7 +17519,7 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
17445
17519
  return detectIntents(query)[0];
17446
17520
  }
17447
17521
 
17448
- module.exports = { rank, buildSigIndex, scoreFile, _queryWants, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
17522
+ module.exports = { rank, buildSigIndex, scoreFile, _queryWants, _isDataHolder, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
17449
17523
 
17450
17524
  };
17451
17525
 
@@ -18738,6 +18812,24 @@ __factories["./src/skills/skills"] = function(module, exports) {
18738
18812
  '6. **Watch the budget.** Check the `get_budget` MCP tool or `sigmap budget` (estimates from SigMap\'s local ledger — no LLM calls). Near the budget: summarize-then-drop older context instead of accumulating, and prefer terse output.',
18739
18813
  ].join('\n'),
18740
18814
  },
18815
+ 'sigmap-task': {
18816
+ title: 'SigMap task loop',
18817
+ kind: 'prompt',
18818
+ description: 'Do a coding task grounded in SigMap: look up before reading, edit by line anchor, verify before reporting.',
18819
+ argumentHint: 'the change you want, in plain words',
18820
+ body: [
18821
+ 'Work through these steps **in order**. Do not open any file before step 2.',
18822
+ 'Every command runs from the integrated terminal — do not ask the user to run them for you.',
18823
+ '',
18824
+ '1. **Look up, do not search.** `npx sigmap ask "<the task>"` — this writes `.context/query-context.md`.',
18825
+ '2. **Read the map.** `cat .context/query-context.md`. It ranks the relevant files and lists their signatures with `:start-end` line anchors — a few hundred tokens where the same files read whole are tens of thousands. Say which files it surfaced before continuing. If nothing relevant appears, re-run step 1 with different wording; fall back to search only after two attempts, and say so.',
18826
+ '3. **Open only the anchored ranges.** A signature ending `:425-425` means read line 425, not the whole file. Never read a file in full when you hold an anchor for it.',
18827
+ '4. **Make the change.** Follow the conventions visible in the signatures — same layering, same response wrapper, same annotation style. Add no dependencies.',
18828
+ '5. **Verify before reporting.** Write what you changed to `.sigmap-notes.md`, naming every file by its **full repository-relative path** (a bare filename is reported as fake), then run `npx sigmap verify-ai-output .sigmap-notes.md`. It checks every name against the real index, offline, with no model call. Fix anything it flags and re-run before you reply.',
18829
+ '6. **Refresh the map.** `npx sigmap` — your edits made it stale.',
18830
+ '7. **Report.** The files you changed, the ranges you actually read, the step-1 token count, and the step-5 verify result. Say so if you fell back to searching or if verify flagged something.',
18831
+ ].join('\n'),
18832
+ },
18741
18833
  'sigmap-config-optimizer': {
18742
18834
  title: 'SigMap config optimizer',
18743
18835
  description: 'Playbook for getting a correct SigMap config on any repo: detect with sigmap tune, review the per-change reasons, apply, validate.',
@@ -18762,7 +18854,9 @@ __factories["./src/skills/skills"] = function(module, exports) {
18762
18854
  windsurf: { label: 'Windsurf', parent: ['.windsurf'],
18763
18855
  target: (cwd, skill) => path.join(cwd, '.windsurf', 'rules', `${skill}.md`) },
18764
18856
  copilot: { label: 'GitHub Copilot', parent: ['.github'],
18765
- target: (cwd, skill) => path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`) },
18857
+ target: (cwd, skill) => (SKILLS[skill] && SKILLS[skill].kind === 'prompt'
18858
+ ? path.join(cwd, '.github', 'prompts', `${skill}.prompt.md`)
18859
+ : path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`)) },
18766
18860
  codex: { label: 'Codex CLI (AGENTS.md)', parent: ['AGENTS.md'],
18767
18861
  target: (cwd) => path.join(cwd, 'AGENTS.md'), inject: true },
18768
18862
  };
@@ -18783,6 +18877,10 @@ __factories["./src/skills/skills"] = function(module, exports) {
18783
18877
  return `---\ndescription: ${skill.description}\nalwaysApply: false\n---\n\n${body}`;
18784
18878
  }
18785
18879
  if (client === 'copilot') {
18880
+ if (skill.kind === 'prompt') {
18881
+ return `---\nname: ${skillName}\nagent: 'agent'\ndescription: ${skill.description}\n`
18882
+ + `argument-hint: ${skill.argumentHint}\n---\n\n${body}`;
18883
+ }
18786
18884
  return `---\napplyTo: "**"\n---\n\n${body}`;
18787
18885
  }
18788
18886
  return body; // windsurf: plain markdown
@@ -21636,7 +21734,7 @@ function __tryGit(args, opts = {}) {
21636
21734
  catch (_) { return ''; }
21637
21735
  }
21638
21736
 
21639
- const VERSION = '8.29.0';
21737
+ const VERSION = '8.30.0';
21640
21738
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
21641
21739
 
21642
21740
  function requireSourceOrBundled(key) {
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.29.0 | Benchmark: sigmap-v8.29-main (2026-09-01)
14
+ # Version: 8.30.0 | Benchmark: sigmap-v8.30-main (2026-09-07)
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.29-main, 2026-09-01)
20
+ ## Core metrics (benchmark: sigmap-v8.30-main, 2026-09-07)
21
21
 
22
22
  | Metric | Without SigMap | With SigMap |
23
23
  |--------|----------------|-------------|
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.29.0 | Benchmark: sigmap-v8.29-main (2026-09-01)
14
+ # Version: 8.30.0 | Benchmark: sigmap-v8.30-main (2026-09-07)
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.29-main, 2026-09-01)
26
+ ## Core metrics (benchmark: sigmap-v8.30-main, 2026-09-07)
27
27
 
28
28
  - hit@5 retrieval: 81.1% vs 44.0% single-shot grep baseline (1.73× lift)
29
29
  - Token reduction: 96.8% average across benchmark repos
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "8.29.0",
3
+ "version": "8.30.0",
4
4
  "description": "The deterministic, verifiable grounding layer for AI code work — a zero-dependency signature-and-evidence map that grounds Claude, Cursor, Copilot, Aider, Windsurf, local LLMs & MCP agents against your real code (repo + installed libraries) so they stop hallucinating files, imports & APIs. Runs offline via npx; byte-stable output; ~97% token reduction as proof.",
5
5
  "main": "packages/core/index.js",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "8.29.0",
3
+ "version": "8.30.0",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "8.29.0",
3
+ "version": "8.30.0",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -1,6 +1,19 @@
1
1
  'use strict';
2
2
 
3
3
  const { lineAt, withAnchor } = require('./line-anchor');
4
+ const { capWithNotice, capMembersWithNotice } = require('../util/truncate');
5
+
6
+ // Class bodies are scanned to this many characters. Generated JVM sources
7
+ // (MyBatis/JPA entities) routinely run past 10KB, so the ceiling only guards
8
+ // against pathological input rather than trimming ordinary classes.
9
+ const MAX_CLASS_BODY_CHARS = 200000;
10
+
11
+ // Per-class member ceiling. Sits above the default `maxSigsPerFile` so the
12
+ // caller's configured budget governs the output rather than this file.
13
+ const MAX_MEMBERS_PER_CLASS = 120;
14
+
15
+ // Per-file signature ceiling, likewise above the configured default.
16
+ const MAX_SIGS_PER_FILE = 200;
4
17
 
5
18
  /**
6
19
  * Extract signatures from Java source code.
@@ -28,17 +41,20 @@ function extract(src) {
28
41
  const block = extractBlock(stripped, bodyStart);
29
42
  sigs.push(hinted(withAnchor(`${m[1]} ${m[2]}`, lineAt(stripped, m.index), lineAt(stripped, bodyStart + block.length)), m[2]));
30
43
  for (const meth of extractMembers(block)) {
31
- sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + meth.declIdx), lineAt(stripped, bodyStart + meth.endIdx)), meth.name));
44
+ // The disclosure marker carries no offsets; anchor it at the class body.
45
+ const declIdx = meth.declIdx || 0;
46
+ const endIdx = meth.endIdx || 0;
47
+ sigs.push(hinted(withAnchor(` ${meth.text}`, lineAt(stripped, bodyStart + declIdx), lineAt(stripped, bodyStart + endIdx)), meth.name));
32
48
  }
33
49
  }
34
50
 
35
- return sigs.slice(0, 25);
51
+ return capWithNotice(sigs, MAX_SIGS_PER_FILE, 'signatures');
36
52
  }
37
53
 
38
54
  function extractBlock(src, startIndex) {
39
55
  let depth = 1;
40
56
  let i = startIndex;
41
- const end = Math.min(src.length, startIndex + 5000);
57
+ const end = Math.min(src.length, startIndex + MAX_CLASS_BODY_CHARS);
42
58
  while (i < end && depth > 0) {
43
59
  if (src[i] === '{') depth++;
44
60
  else if (src[i] === '}') depth--;
@@ -60,7 +76,7 @@ function extractMembers(block) {
60
76
  endIdx: m.index + m[0].length,
61
77
  });
62
78
  }
63
- return members.slice(0, 8);
79
+ return capMembersWithNotice(members, MAX_MEMBERS_PER_CLASS);
64
80
  }
65
81
 
66
82
  function normalizeParams(params) {
@@ -17,6 +17,7 @@ const os = require('os');
17
17
 
18
18
  // Config shapes the supported clients use.
19
19
  // - 'json' → { mcpServers: { sigmap: { command, args } } }
20
+ // - 'vscode'→ { servers: { sigmap: { type: 'stdio', command, args } } }
20
21
  // - 'zed' → { context_servers: { sigmap: { command: { path, args } } } }
21
22
  // - 'yaml' → Codex CLI ~/.codex/config.yaml (mcpServers block, appended)
22
23
  const CLIENTS = {
@@ -25,7 +26,7 @@ const CLIENTS = {
25
26
  windsurf: { label: 'Windsurf', format: 'json', scope: 'both',
26
27
  project: ['.windsurf', 'mcp.json'],
27
28
  global: ['.codeium', 'windsurf', 'mcp_config.json'] },
28
- vscode: { label: 'VS Code', format: 'json', scope: 'project', project: ['.vscode', 'mcp.json'] },
29
+ vscode: { label: 'VS Code', format: 'vscode', scope: 'project', project: ['.vscode', 'mcp.json'] },
29
30
  opencode: { label: 'OpenCode', format: 'json', scope: 'both',
30
31
  project: ['opencode.json'],
31
32
  global: ['.config', 'opencode', 'config.json'] },
@@ -80,6 +81,31 @@ function _installJson(filePath, scriptPath) {
80
81
  return 'installed';
81
82
  }
82
83
 
84
+ /**
85
+ * Install into VS Code's `.vscode/mcp.json`, which keys servers under `servers`
86
+ * (not `mcpServers`) and expects an explicit transport `type`. A config written
87
+ * by an older SigMap under `mcpServers` is migrated rather than left in place,
88
+ * so re-running repairs it instead of leaving two entries VS Code cannot read.
89
+ */
90
+ function _installVscode(filePath, scriptPath) {
91
+ let settings = {};
92
+ if (fs.existsSync(filePath)) {
93
+ try { settings = JSON.parse(fs.readFileSync(filePath, 'utf8')) || {}; }
94
+ catch (_) { settings = {}; }
95
+ }
96
+ const stale = settings.mcpServers && settings.mcpServers.sigmap;
97
+ if (stale) {
98
+ delete settings.mcpServers.sigmap;
99
+ if (Object.keys(settings.mcpServers).length === 0) delete settings.mcpServers;
100
+ }
101
+ if (!settings.servers) settings.servers = {};
102
+ if (settings.servers.sigmap && !stale) return 'already';
103
+ settings.servers.sigmap = { type: 'stdio', command: 'node', args: serverArgs(scriptPath) };
104
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
105
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + '\n');
106
+ return stale ? 'updated' : 'installed';
107
+ }
108
+
83
109
  /** Install into Zed's `context_servers` config (create file/dir if absent). */
84
110
  function _installZed(filePath, scriptPath) {
85
111
  let settings = {};
@@ -132,7 +158,8 @@ function installClient(client, opts = {}) {
132
158
  const filePath = resolveTarget(spec, cwd, home, opts.global);
133
159
 
134
160
  let status;
135
- if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
161
+ if (spec.format === 'vscode') status = _installVscode(filePath, scriptPath);
162
+ else if (spec.format === 'zed') status = _installZed(filePath, scriptPath);
136
163
  else if (spec.format === 'yaml') status = _installYaml(filePath, scriptPath);
137
164
  else status = _installJson(filePath, scriptPath);
138
165
 
package/src/mcp/server.js CHANGED
@@ -18,7 +18,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
18
18
 
19
19
  const SERVER_INFO = {
20
20
  name: 'sigmap',
21
- version: '8.29.0',
21
+ version: '8.30.0',
22
22
  description: 'SigMap MCP server — code signatures on demand',
23
23
  };
24
24
 
@@ -72,26 +72,52 @@ const PENALTY_SIGNALS = {
72
72
  generatedCode: 0.3, // dist/build/.next in path
73
73
  docsFile: 0.2, // docs/doc/README in path
74
74
  nodeModules: 0.0, // node_modules (zero score)
75
+ dataHolder: 0.3, // generated POJO/entity: almost entirely accessors
75
76
  };
76
77
 
78
+ // A file whose members are overwhelmingly trivial accessors is a data holder,
79
+ // not logic. Path-based detection cannot see these: generated JPA/MyBatis
80
+ // entities live in ordinary source trees. They match a query on any column
81
+ // name they happen to carry (`getNote`/`setNote` matches "note" as strongly as
82
+ // the service that actually implements order notes), so on an entity-heavy
83
+ // repo they crowd real code out of the top results.
84
+ const ACCESSOR_RE = /^\s*(get|set|is)[A-Z]\w*\s*\(/;
85
+ const DATA_HOLDER_RATIO = 0.8;
86
+ const DATA_HOLDER_MIN_MEMBERS = 6;
87
+
77
88
  // Query terms that mean the penalised category IS the target. Read from the
78
89
  // query tokens directly, NOT via detectIntent: that classifier is first-match-
79
90
  // wins over its pattern object, and `debug` precedes `test`, so "fix the failing
80
91
  // test" classifies as debug and never reaches the test branch.
81
92
  const WANTS_TESTS = new Set(['test', 'tests', 'spec', 'specs', 'unit', 'integration', 'e2e', 'assertion', 'assert', 'mock', 'fixture', 'coverage', 'testing']);
82
93
  const WANTS_DOCS = new Set(['doc', 'docs', 'documentation', 'readme', 'changelog', 'guide', 'tutorial']);
94
+ const WANTS_MODELS = new Set(['entity', 'entities', 'model', 'models', 'pojo', 'dto', 'bean', 'getter', 'getters', 'setter', 'setters', 'accessor', 'accessors', 'field', 'fields', 'column', 'columns', 'schema']);
83
95
 
84
96
  /** Which penalised categories the query is explicitly asking for. */
85
97
  function _queryWants(queryTokens) {
86
- const wants = { tests: false, docs: false };
98
+ const wants = { tests: false, docs: false, models: false };
87
99
  for (const t of queryTokens || []) {
88
100
  if (WANTS_TESTS.has(t)) wants.tests = true;
89
101
  if (WANTS_DOCS.has(t)) wants.docs = true;
102
+ if (WANTS_MODELS.has(t)) wants.models = true;
90
103
  }
91
104
  return wants;
92
105
  }
93
106
 
94
- function _computePenalty(filePath, wants) {
107
+ /**
108
+ * True when a file's members are overwhelmingly trivial accessors — a generated
109
+ * entity or POJO rather than logic. Type declarations are excluded from the
110
+ * ratio so a small class is not misjudged by its own `class X` line.
111
+ */
112
+ function _isDataHolder(sigs) {
113
+ if (!Array.isArray(sigs)) return false;
114
+ const members = sigs.filter((line) => /^\s/.test(line) || !/^(class|interface|enum|struct|function|module\.exports)\b/.test(line));
115
+ if (members.length < DATA_HOLDER_MIN_MEMBERS) return false;
116
+ const accessors = members.filter((line) => ACCESSOR_RE.test(line)).length;
117
+ return accessors / members.length >= DATA_HOLDER_RATIO;
118
+ }
119
+
120
+ function _computePenalty(filePath, wants, sigs) {
95
121
  const pathLower = filePath.toLowerCase();
96
122
  if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
97
123
  // A penalty must never fire on the very thing the user asked for. Before
@@ -104,6 +130,11 @@ function _computePenalty(filePath, wants) {
104
130
  if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) {
105
131
  return (wants && wants.docs) ? 1.0 : PENALTY_SIGNALS.docsFile;
106
132
  }
133
+ // Content-based, and last: a data holder is still a real source file, so it
134
+ // is only demoted once the path-based categories have had their say.
135
+ if (_isDataHolder(sigs)) {
136
+ return (wants && wants.models) ? 1.0 : PENALTY_SIGNALS.dataHolder;
137
+ }
107
138
  return 1.0;
108
139
  }
109
140
 
@@ -161,7 +192,7 @@ function scoreFile(filePath, sigs, queryTokens, weights, wants) {
161
192
  if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
162
193
 
163
194
  const w = weights || DEFAULT_WEIGHTS;
164
- const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants) };
195
+ const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath, wants, sigs) };
165
196
 
166
197
  // Module-doc prose is excluded here on purpose. This signal measures overlap
167
198
  // with DECLARED IDENTIFIERS; prose relevance is BM25's job, where it is scored
@@ -757,4 +788,4 @@ function detectIntent(query) {
757
788
  return detectIntents(query)[0];
758
789
  }
759
790
 
760
- module.exports = { rank, buildSigIndex, scoreFile, _queryWants, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
791
+ module.exports = { rank, buildSigIndex, scoreFile, _queryWants, _isDataHolder, detectIntents, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, CENTRALITY_BLEND_WEIGHT, detectIntent };
@@ -34,6 +34,24 @@ const SKILLS = {
34
34
  '6. **Watch the budget.** Check the `get_budget` MCP tool or `sigmap budget` (estimates from SigMap\'s local ledger — no LLM calls). Near the budget: summarize-then-drop older context instead of accumulating, and prefer terse output.',
35
35
  ].join('\n'),
36
36
  },
37
+ 'sigmap-task': {
38
+ title: 'SigMap task loop',
39
+ kind: 'prompt',
40
+ description: 'Do a coding task grounded in SigMap: look up before reading, edit by line anchor, verify before reporting.',
41
+ argumentHint: 'the change you want, in plain words',
42
+ body: [
43
+ 'Work through these steps **in order**. Do not open any file before step 2.',
44
+ 'Every command runs from the integrated terminal — do not ask the user to run them for you.',
45
+ '',
46
+ '1. **Look up, do not search.** `npx sigmap ask "<the task>"` — this writes `.context/query-context.md`.',
47
+ '2. **Read the map.** `cat .context/query-context.md`. It ranks the relevant files and lists their signatures with `:start-end` line anchors — a few hundred tokens where the same files read whole are tens of thousands. Say which files it surfaced before continuing. If nothing relevant appears, re-run step 1 with different wording; fall back to search only after two attempts, and say so.',
48
+ '3. **Open only the anchored ranges.** A signature ending `:425-425` means read line 425, not the whole file. Never read a file in full when you hold an anchor for it.',
49
+ '4. **Make the change.** Follow the conventions visible in the signatures — same layering, same response wrapper, same annotation style. Add no dependencies.',
50
+ '5. **Verify before reporting.** Write what you changed to `.sigmap-notes.md`, naming every file by its **full repository-relative path** (a bare filename is reported as fake), then run `npx sigmap verify-ai-output .sigmap-notes.md`. It checks every name against the real index, offline, with no model call. Fix anything it flags and re-run before you reply.',
51
+ '6. **Refresh the map.** `npx sigmap` — your edits made it stale.',
52
+ '7. **Report.** The files you changed, the ranges you actually read, the step-1 token count, and the step-5 verify result. Say so if you fell back to searching or if verify flagged something.',
53
+ ].join('\n'),
54
+ },
37
55
  'sigmap-config-optimizer': {
38
56
  title: 'SigMap config optimizer',
39
57
  description: 'Playbook for getting a correct SigMap config on any repo: detect with sigmap tune, review the per-change reasons, apply, validate.',
@@ -58,7 +76,9 @@ const SKILL_CLIENTS = {
58
76
  windsurf: { label: 'Windsurf', parent: ['.windsurf'],
59
77
  target: (cwd, skill) => path.join(cwd, '.windsurf', 'rules', `${skill}.md`) },
60
78
  copilot: { label: 'GitHub Copilot', parent: ['.github'],
61
- target: (cwd, skill) => path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`) },
79
+ target: (cwd, skill) => (SKILLS[skill] && SKILLS[skill].kind === 'prompt'
80
+ ? path.join(cwd, '.github', 'prompts', `${skill}.prompt.md`)
81
+ : path.join(cwd, '.github', 'instructions', `${skill}.instructions.md`)) },
62
82
  codex: { label: 'Codex CLI (AGENTS.md)', parent: ['AGENTS.md'],
63
83
  target: (cwd) => path.join(cwd, 'AGENTS.md'), inject: true },
64
84
  };
@@ -79,6 +99,10 @@ function renderSkill(client, skillName, version) {
79
99
  return `---\ndescription: ${skill.description}\nalwaysApply: false\n---\n\n${body}`;
80
100
  }
81
101
  if (client === 'copilot') {
102
+ if (skill.kind === 'prompt') {
103
+ return `---\nname: ${skillName}\nagent: 'agent'\ndescription: ${skill.description}\n`
104
+ + `argument-hint: ${skill.argumentHint}\n---\n\n${body}`;
105
+ }
82
106
  return `---\napplyTo: "**"\n---\n\n${body}`;
83
107
  }
84
108
  return body; // windsurf: plain markdown