sigmap 4.1.0 → 4.1.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,51 @@ Format: [Semantic Versioning](https://semver.org/)
10
10
 
11
11
  ---
12
12
 
13
+ ## [4.1.1] — 2026-04-16 — Fix: --query works with any adapter output
14
+
15
+ ### Fixed
16
+
17
+ - **`--query` fails after `--adapter` generation** (`[sigmap] no context file found`):
18
+ `buildSigIndex` hardcoded `.github/copilot-instructions.md` as the only
19
+ context file path, so `--query` always failed when any adapter other than
20
+ `copilot` wrote to a different location (`CLAUDE.md`, `AGENTS.md`,
21
+ `.cursorrules`, `.windsurfrules`, etc.).
22
+
23
+ `buildSigIndex` now probes all nine known adapter output paths in priority
24
+ order and returns the first non-empty index:
25
+ ```
26
+ copilot → claude → codex → cursor → windsurf → openai → gemini → llm-full → llm
27
+ ```
28
+ Human-written preamble before the `## Auto-generated signatures` marker
29
+ (e.g. custom content in `CLAUDE.md`) is skipped so those `###` sections
30
+ don't pollute the signature index.
31
+
32
+ - **`--adapter <name> --query "..."` combination ignored the adapter flag**:
33
+ The `--query` handler now detects a co-present `--adapter` flag, resolves
34
+ that adapter's output path, and reads from it directly — so both forms work:
35
+ ```bash
36
+ # generate with claude adapter, then query without re-specifying adapter
37
+ node gen-context.js --adapter claude
38
+ node gen-context.js --query "add a new extractor"
39
+
40
+ # or pin explicitly in one command
41
+ node gen-context.js --adapter claude --query "add a new extractor"
42
+ ```
43
+
44
+ - **`--analyze --json` output truncated at ~8 KB on macOS**:
45
+ Calling `process.exit(0)` immediately after `process.stdout.write(largeJson)`
46
+ truncated output because the underlying pipe write is asynchronous even
47
+ when `write()` returns `true`. Fixed by using the write callback so the
48
+ process exits only after the OS has accepted all bytes.
49
+
50
+ ### Tests
51
+
52
+ - Added `test/integration/query-adapter.test.js` (17 tests) covering every
53
+ adapter output path (unit + CLI), probe order, marker-skipping, explicit
54
+ `opts.contextPath` override, and empty-project fallback.
55
+
56
+ ---
57
+
13
58
  ## [4.1.0] — 2026-04-15 — Smart Budget: auto-scaling token budget
14
59
 
15
60
  ### Added
package/gen-context.js CHANGED
@@ -5449,12 +5449,24 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5449
5449
  scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
5450
5450
  return scored.slice(0, topK);
5451
5451
  }
5452
- function buildSigIndex(cwd) {
5453
- const fs = require('fs'); const path = require('path');
5454
- const contextPath = path.join(cwd, '.github', 'copilot-instructions.md');
5452
+ const ADAPTER_OUTPUT_PATHS = [
5453
+ ['.github', 'copilot-instructions.md'],
5454
+ ['CLAUDE.md'],
5455
+ ['AGENTS.md'],
5456
+ ['.cursorrules'],
5457
+ ['.windsurfrules'],
5458
+ ['.github', 'openai-context.md'],
5459
+ ['.github', 'gemini-context.md'],
5460
+ ['llm-full.txt'],
5461
+ ['llm.txt'],
5462
+ ];
5463
+ function _parseContextFile(contextPath) {
5464
+ const fs = require('fs');
5455
5465
  const index = new Map();
5456
5466
  if (!fs.existsSync(contextPath)) return index;
5457
- const content = fs.readFileSync(contextPath, 'utf8');
5467
+ let content = fs.readFileSync(contextPath, 'utf8');
5468
+ const markerIdx = content.indexOf('## Auto-generated signatures');
5469
+ if (markerIdx !== -1) content = content.slice(markerIdx);
5458
5470
  const lines = content.split('\n');
5459
5471
  let currentFile = null; let inBlock = false; let sigs = [];
5460
5472
  for (const line of lines) {
@@ -5466,6 +5478,16 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
5466
5478
  if (currentFile !== null) index.set(currentFile, sigs);
5467
5479
  return index;
5468
5480
  }
5481
+ function buildSigIndex(cwd, opts) {
5482
+ const path = require('path');
5483
+ if (opts && opts.contextPath) return _parseContextFile(opts.contextPath);
5484
+ for (const parts of ADAPTER_OUTPUT_PATHS) {
5485
+ const contextPath = path.join(cwd, ...parts);
5486
+ const index = _parseContextFile(contextPath);
5487
+ if (index.size > 0) return index;
5488
+ }
5489
+ return new Map();
5490
+ }
5469
5491
  function formatRankTable(results, query) {
5470
5492
  if (!results || results.length === 0) return `No matching files found for query: "${query}"\n`;
5471
5493
  const lines = [`## Query: ${query}`, '', '| Rank | File | Score | Sigs | Tokens |', '|------|------|-------|------|--------|',
@@ -8220,7 +8242,13 @@ function main() {
8220
8242
  const stats = analyzeFiles(allFiles, cwd, { slow, maxSigs: cfg.maxSigsPerFile || 25 });
8221
8243
 
8222
8244
  if (args.includes('--json')) {
8223
- process.stdout.write(JSON.stringify(formatAnalysisJSON(stats)) + '\n');
8245
+ const out = JSON.stringify(formatAnalysisJSON(stats)) + '\n';
8246
+ // Use the write callback to exit only after the OS has accepted all
8247
+ // bytes. Calling process.exit(0) synchronously after write() truncates
8248
+ // large outputs because the underlying pipe write is asynchronous even
8249
+ // when write() returns true.
8250
+ process.stdout.write(out, 'utf8', () => process.exit(0));
8251
+ return; // exit is handled by the callback above
8224
8252
  } else {
8225
8253
  const table = formatAnalysisTable(stats, slow);
8226
8254
  process.stdout.write(table);
@@ -8326,9 +8354,29 @@ function main() {
8326
8354
  process.exit(1);
8327
8355
  }
8328
8356
  const { rank, buildSigIndex, formatRankTable, formatRankJSON } = requireSourceOrBundled('./src/retrieval/ranker');
8329
- const index = buildSigIndex(cwd);
8357
+
8358
+ // Resolve an explicit context file path when --adapter is present.
8359
+ // This lets `--adapter claude --query "..."` read CLAUDE.md instead of
8360
+ // falling through to the default copilot-instructions.md probe.
8361
+ let queryOpts;
8362
+ const adpIdx = args.indexOf('--adapter');
8363
+ if (adpIdx >= 0) {
8364
+ const adapterName = (args[adpIdx + 1] || '').trim().toLowerCase();
8365
+ const VALID_ADAPTERS = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
8366
+ if (VALID_ADAPTERS.includes(adapterName)) {
8367
+ try {
8368
+ const adapterMod = __require('./packages/adapters/' + adapterName);
8369
+ queryOpts = { contextPath: adapterMod.outputPath(cwd) };
8370
+ } catch (_) {}
8371
+ }
8372
+ }
8373
+
8374
+ const index = buildSigIndex(cwd, queryOpts);
8330
8375
  if (index.size === 0) {
8331
8376
  console.error('[sigmap] no context file found. Run: node gen-context.js');
8377
+ if (adpIdx >= 0) {
8378
+ console.error(' (tried the path for --adapter ' + (args[adpIdx + 1] || '') + ')');
8379
+ }
8332
8380
  process.exit(1);
8333
8381
  }
8334
8382
  const topIdx = args.indexOf('--top');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
4
4
  "description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
5
5
  "main": "gen-context.js",
6
6
  "exports": {
@@ -141,24 +141,45 @@ function rank(query, sigIndex, opts) {
141
141
  }
142
142
 
143
143
  /**
144
- * Build a signature index from the generated context file.
145
- * Returns Map<filePath, string[]> where filePath is the relative path
146
- * as it appears in the ### headers of copilot-instructions.md.
144
+ * All paths where sigmap adapters write their context files, in probe order.
145
+ * The first existing file with a non-empty index wins when no explicit path
146
+ * is supplied.
147
+ */
148
+ const ADAPTER_OUTPUT_PATHS = [
149
+ ['.github', 'copilot-instructions.md'], // copilot (default)
150
+ ['CLAUDE.md'], // claude
151
+ ['AGENTS.md'], // codex
152
+ ['.cursorrules'], // cursor
153
+ ['.windsurfrules'], // windsurf
154
+ ['.github', 'openai-context.md'], // openai
155
+ ['.github', 'gemini-context.md'], // gemini
156
+ ['llm-full.txt'], // llm-full
157
+ ['llm.txt'], // llm
158
+ ];
159
+
160
+ /**
161
+ * Parse a single context file into a Map<filePath, string[]>.
147
162
  *
148
- * @param {string} cwd
163
+ * Files that contain human-written content before an
164
+ * "## Auto-generated signatures" marker (e.g. CLAUDE.md) are handled
165
+ * by skipping everything above the marker before scanning for ### headers.
166
+ *
167
+ * @param {string} contextPath - absolute path to the context file
149
168
  * @returns {Map<string, string[]>}
150
169
  */
151
- function buildSigIndex(cwd) {
152
- const fs = require('fs');
153
- const path = require('path');
154
- const contextPath = path.join(cwd, '.github', 'copilot-instructions.md');
170
+ function _parseContextFile(contextPath) {
171
+ const fs = require('fs');
155
172
  const index = new Map();
156
173
 
157
174
  if (!fs.existsSync(contextPath)) return index;
158
175
 
159
- const content = fs.readFileSync(contextPath, 'utf8');
160
- const lines = content.split('\n');
176
+ let content = fs.readFileSync(contextPath, 'utf8');
161
177
 
178
+ // Skip any human-written preamble that sits above the auto-generated block.
179
+ const markerIdx = content.indexOf('## Auto-generated signatures');
180
+ if (markerIdx !== -1) content = content.slice(markerIdx);
181
+
182
+ const lines = content.split('\n');
162
183
  let currentFile = null;
163
184
  let inBlock = false;
164
185
  let sigs = [];
@@ -180,6 +201,40 @@ function buildSigIndex(cwd) {
180
201
  return index;
181
202
  }
182
203
 
204
+ /**
205
+ * Build a signature index from the generated context file.
206
+ * Returns Map<filePath, string[]> where filePath is the relative path
207
+ * as it appears in the ### headers of the context file.
208
+ *
209
+ * When `opts.contextPath` is provided, that specific file is used.
210
+ * This is the case when the caller already knows the path (e.g. via
211
+ * --adapter <name> or --output <file>).
212
+ *
213
+ * Otherwise all known adapter output paths are probed in order and the
214
+ * first file that produces a non-empty index is returned.
215
+ *
216
+ * @param {string} cwd
217
+ * @param {{ contextPath?: string }} [opts]
218
+ * @returns {Map<string, string[]>}
219
+ */
220
+ function buildSigIndex(cwd, opts) {
221
+ const path = require('path');
222
+
223
+ // Caller supplied an explicit path — use it directly.
224
+ if (opts && opts.contextPath) {
225
+ return _parseContextFile(opts.contextPath);
226
+ }
227
+
228
+ // Probe all known adapter output paths; return first non-empty index.
229
+ for (const parts of ADAPTER_OUTPUT_PATHS) {
230
+ const contextPath = path.join(cwd, ...parts);
231
+ const index = _parseContextFile(contextPath);
232
+ if (index.size > 0) return index;
233
+ }
234
+
235
+ return new Map();
236
+ }
237
+
183
238
  /**
184
239
  * Format ranked results as a markdown table string.
185
240
  *