sigmap 6.15.0 → 7.0.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/gen-context.js CHANGED
@@ -236,12 +236,11 @@ __factories["./src/config/loader"] = function(module, exports) {
236
236
  }
237
237
  }
238
238
  try {
239
- const { execSync } = require('child_process');
240
- const proto = extendsVal.startsWith('https') ? 'https' : 'http';
241
- const out = execSync(
242
- `node -e "const h=require('${proto}');let d='';h.get(${JSON.stringify(extendsVal)},r=>{r.on('data',c=>d+=c);r.on('end',()=>process.stdout.write(d))}).on('error',()=>process.exit(1))"`,
243
- { timeout: 10000, encoding: 'utf8' }
244
- );
239
+ // Shell-free: run the node binary directly with the URL passed as an argv
240
+ // value (never interpolated into a command string, never via /bin/sh).
241
+ const { execFileSync } = require('child_process');
242
+ const SYNC_FETCH = "const u=process.argv[1];const h=require(u.startsWith('https')?'https':'http');let d='';h.get(u,r=>{r.on('data',c=>d+=c);r.on('end',()=>process.stdout.write(d))}).on('error',()=>process.exit(1))";
243
+ const out = execFileSync(process.execPath, ['-e', SYNC_FETCH, extendsVal], { timeout: 10000, encoding: 'utf8' });
245
244
  const parsed = JSON.parse(out);
246
245
  if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
247
246
  fs.writeFileSync(cachePath, JSON.stringify(parsed), 'utf8');
@@ -3164,55 +3163,78 @@ __factories["./src/extractors/coverage"] = function(module, exports) {
3164
3163
 
3165
3164
  // ── ./src/extractors/prdiff ──
3166
3165
  __factories["./src/extractors/prdiff"] = function(module, exports) {
3166
+ 'use strict';
3167
3167
 
3168
- 'use strict';
3169
-
3170
- /**
3171
- * Compare signature arrays and produce compact diff markers.
3172
- * @param {string[]} baseSigs
3173
- * @param {string[]} currentSigs
3174
- * @returns {{added:string[], removed:string[], modified:string[]}}
3175
- */
3176
- function diffSignatures(baseSigs, currentSigs) {
3177
- const base = new Set(baseSigs || []);
3178
- const curr = new Set(currentSigs || []);
3179
-
3180
- const added = [...curr].filter((s) => !base.has(s));
3181
- const removed = [...base].filter((s) => !curr.has(s));
3182
-
3183
- const byName = (arr) => {
3184
- const m = new Map();
3185
- for (const s of arr) {
3186
- const n = extractName(s);
3187
- if (!n) continue;
3188
- if (!m.has(n)) m.set(n, []);
3189
- m.get(n).push(s);
3190
- }
3191
- return m;
3192
- };
3193
-
3194
- const aBy = byName(added);
3195
- const rBy = byName(removed);
3196
- const modified = [];
3168
+ /**
3169
+ * Compare signature arrays and produce compact diff markers.
3170
+ * @param {string[]} baseSigs
3171
+ * @param {string[]} currentSigs
3172
+ * @returns {{added:string[], removed:string[], modified:string[]}}
3173
+ */
3174
+ function diffSignatures(baseSigs, currentSigs) {
3175
+ const base = new Set(baseSigs || []);
3176
+ const curr = new Set(currentSigs || []);
3177
+
3178
+ const added = [...curr].filter((s) => !base.has(s));
3179
+ const removed = [...base].filter((s) => !curr.has(s));
3180
+
3181
+ const byName = (arr) => {
3182
+ const m = new Map();
3183
+ for (const s of arr) {
3184
+ const n = extractName(s);
3185
+ if (!n) continue;
3186
+ if (!m.has(n)) m.set(n, []);
3187
+ m.get(n).push(s);
3188
+ }
3189
+ return m;
3190
+ };
3197
3191
 
3198
- for (const [name] of aBy) {
3199
- if (rBy.has(name)) modified.push(name);
3200
- }
3192
+ const aBy = byName(added);
3193
+ const rBy = byName(removed);
3194
+ const modified = [];
3201
3195
 
3202
- return { added, removed, modified };
3196
+ for (const [name] of aBy) {
3197
+ if (rBy.has(name)) modified.push(name);
3203
3198
  }
3204
3199
 
3205
- function extractName(sig) {
3206
- if (!sig) return '';
3207
- const t = sig.trim();
3208
- const m = t.match(/(?:def|function|func|class|interface|trait|struct|enum|record)?\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:\(|$)/);
3209
- return m ? m[1] : '';
3210
- }
3200
+ return { added, removed, modified };
3201
+ }
3202
+
3203
+ /**
3204
+ * Extract the declared symbol name from a signature line.
3205
+ *
3206
+ * Robust to the real forms SigMap emits — `export class X`, `const x = () =>`,
3207
+ * `async function x`, members, a trailing `:start-end` anchor, and `→ return`
3208
+ * suffixes. Anchored so it never returns a mid-string fragment (the old regex
3209
+ * could turn a signature into a 2-char name like `is`), and returns '' for
3210
+ * non-symbol lines (`module.exports = {…}`, markdown headers) instead of guessing.
3211
+ */
3212
+ function extractName(sig) {
3213
+ if (!sig) return '';
3214
+ // Drop a trailing `:start-end` (or `:line`) line anchor.
3215
+ let t = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim();
3216
+ // Re-export / barrel lines carry no single declared name.
3217
+ if (/^(?:module\.)?exports\b/.test(t) || /^export\s*\{/.test(t) || /^export\s+\*/.test(t)) return '';
3218
+ // Strip leading modifiers so the keyword/name is at the start.
3219
+ t = t.replace(/^export\s+/, '')
3220
+ .replace(/^default\s+/, '')
3221
+ .replace(/^(?:public|private|protected|static|abstract|final|override|readonly)\s+/g, '')
3222
+ .replace(/^async\s+/, '');
3223
+ let m;
3224
+ // Declared forms: function/def/func/fn/class/interface/trait/struct/enum/record/type <name>
3225
+ if ((m = t.match(/^(?:def|function|func|fn|class|interface|trait|struct|enum|record|type)\s+([A-Za-z_$][\w$]*)/))) return m[1];
3226
+ // const/let/var/val <name> = … (arrow functions, assigned values)
3227
+ if ((m = t.match(/^(?:const|let|var|val)\s+([A-Za-z_$][\w$]*)/))) return m[1];
3228
+ // Call / method form: <name>(…)
3229
+ if ((m = t.match(/^([A-Za-z_$][\w$]*)\s*\(/))) return m[1];
3230
+ // Lone identifier (e.g. a collapsed `symbol` after the anchor was stripped).
3231
+ if ((m = t.match(/^([A-Za-z_$][\w$]*)$/))) return m[1];
3232
+ return '';
3233
+ }
3211
3234
 
3212
- module.exports = { diffSignatures, extractName };
3235
+ module.exports = { diffSignatures, extractName };
3213
3236
 
3214
3237
  };
3215
-
3216
3238
  // ── ./src/extractors/r ──
3217
3239
  __factories["./src/extractors/r"] = function(module, exports) {
3218
3240
 
@@ -5510,7 +5532,6 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
5510
5532
 
5511
5533
  const fs = require('fs');
5512
5534
  const path = require('path');
5513
- const { execSync } = require('child_process');
5514
5535
 
5515
5536
  const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
5516
5537
  const CONTEXT_COLD_FILE = path.join('.github', 'context-cold.md');
@@ -5657,19 +5678,14 @@ function createCheckpoint(args, cwd) {
5657
5678
  // ── Git info ────────────────────────────────────────────────────────────
5658
5679
  lines.push('## Git state');
5659
5680
  try {
5660
- const branch = execSync('git rev-parse --abbrev-ref HEAD', {
5661
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
5662
- }).trim();
5681
+ const branch = __git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }).trim();
5663
5682
  lines.push(`**Branch:** ${branch}`);
5664
5683
  } catch (_) {
5665
5684
  lines.push('**Branch:** (not a git repo)');
5666
5685
  }
5667
5686
 
5668
5687
  try {
5669
- const log = execSync(
5670
- 'git log --oneline -5 --no-decorate 2>/dev/null',
5671
- { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
5672
- ).trim();
5688
+ const log = __git(['log', '--oneline', '-5', '--no-decorate'], { cwd }).trim();
5673
5689
  if (log) {
5674
5690
  lines.push('');
5675
5691
  lines.push('**Recent commits:**');
@@ -6238,7 +6254,7 @@ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, exp
6238
6254
 
6239
6255
  const SERVER_INFO = {
6240
6256
  name: 'sigmap',
6241
- version: '6.15.0',
6257
+ version: '7.0.1',
6242
6258
  description: 'SigMap MCP server — code signatures on demand',
6243
6259
  };
6244
6260
 
@@ -7129,8 +7145,6 @@ __factories["./src/session/notes"] = function(module, exports) {
7129
7145
 
7130
7146
  const fs = require('fs');
7131
7147
  const path = require('path');
7132
- const { execSync } = require('child_process');
7133
-
7134
7148
  const NOTES_FILE = path.join('.context', 'notes.ndjson');
7135
7149
  const MAX_TEXT = 2000;
7136
7150
 
@@ -7139,13 +7153,7 @@ function notesPath(cwd) {
7139
7153
  }
7140
7154
 
7141
7155
  function _currentBranch(cwd) {
7142
- try {
7143
- return execSync('git rev-parse --abbrev-ref HEAD', {
7144
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
7145
- }).trim() || null;
7146
- } catch (_) {
7147
- return null;
7148
- }
7156
+ return __tryGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }) || null;
7149
7157
  }
7150
7158
 
7151
7159
  /**
@@ -7988,334 +7996,641 @@ __factories["./src/eval/usefulness-scorer"] = function(module, exports) {
7988
7996
 
7989
7997
  // ── ./packages/adapters/copilot (bundled) ──
7990
7998
  __factories["./packages/adapters/copilot"] = function(module, exports) {
7991
- const path = require('path');
7992
- const fs = require('fs');
7993
- const name = 'copilot';
7994
- const COPILOT_MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
7995
- function format(context, opts = {}) {
7996
- if (!context || typeof context !== 'string') return '';
7997
- const version = (opts && opts.version) || 'unknown';
7998
- const timestamp = new Date().toISOString();
7999
- return [
8000
- `<!-- Generated by SigMap gen-context.js v${version} -->`,
8001
- `<!-- Updated: ${timestamp} -->`,
8002
- `<!-- Do not edit below — regenerate with: node gen-context.js -->`,
8003
- '',
8004
- '## SigMap commands',
8005
- '',
8006
- '| When | Command |',
8007
- '|------|---------|',
8008
- '| Before answering a question about code | `sigmap ask "<your question>"` |',
8009
- '| After changing config or source dirs | `sigmap validate` |',
8010
- '| To verify an AI answer is grounded | `sigmap judge --response <file>` |',
8011
- '',
8012
- '# Code signatures',
8013
- '',
8014
- context,
8015
- ].join('\n');
7999
+ 'use strict';
8000
+
8001
+ /**
8002
+ * Copilot adapter writes to .github/copilot-instructions.md
8003
+ * GitHub Copilot reads this file automatically in every workspace.
8004
+ *
8005
+ * Contract:
8006
+ * format(context, opts?) string
8007
+ * outputPath(cwd) → string
8008
+ */
8009
+
8010
+ const path = require('path');
8011
+ const fs = require('fs');
8012
+
8013
+ const name = 'copilot';
8014
+ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8015
+
8016
+ /**
8017
+ * Format context for GitHub Copilot instructions.
8018
+ * @param {string} context - Raw signature context string
8019
+ * @param {object} [opts]
8020
+ * @param {string} [opts.version] - SigMap version string
8021
+ * @returns {string}
8022
+ */
8023
+ function format(context, opts = {}) {
8024
+ if (!context || typeof context !== 'string') return '';
8025
+ const version = opts.version || 'unknown';
8026
+ const timestamp = new Date().toISOString();
8027
+ const meta = _confidenceMeta(opts);
8028
+ const header = [
8029
+ `<!-- Generated by SigMap gen-context.js v${version} -->`,
8030
+ `<!-- Updated: ${timestamp} -->`,
8031
+ meta,
8032
+ `<!-- Do not edit below — regenerate with: node gen-context.js -->`,
8033
+ '',
8034
+ '# Code signatures',
8035
+ '',
8036
+ ].join('\n');
8037
+ return header + context;
8038
+ }
8039
+
8040
+ function _confidenceMeta(opts) {
8041
+ const parts = [`version=${opts.version || 'unknown'}`];
8042
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8043
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8044
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8045
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8046
+ return `<!-- sigmap: ${parts.join(' ')} -->`;
8047
+ }
8048
+
8049
+ /**
8050
+ * Return the output file path for this adapter.
8051
+ * @param {string} cwd - Project root
8052
+ * @returns {string}
8053
+ */
8054
+ function outputPath(cwd) {
8055
+ return path.join(cwd, '.github', 'copilot-instructions.md');
8056
+ }
8057
+
8058
+ /**
8059
+ * Write signatures into copilot-instructions.md using append-under-marker.
8060
+ * If marker exists, content above marker is preserved.
8061
+ * If legacy generated content exists without marker, replace it cleanly.
8062
+ * @param {string} context - Raw signature context string
8063
+ * @param {string} cwd - Project root
8064
+ * @param {object} [opts]
8065
+ */
8066
+ function write(context, cwd, opts = {}) {
8067
+ const filePath = outputPath(cwd);
8068
+ let existing = '';
8069
+ if (fs.existsSync(filePath)) {
8070
+ existing = fs.readFileSync(filePath, 'utf8');
8016
8071
  }
8017
- function outputPath(cwd) { return path.join(cwd, '.github', 'copilot-instructions.md'); }
8018
- function write(context, cwd, opts = {}) {
8019
- const filePath = outputPath(cwd);
8020
- let existing = '';
8021
- if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
8022
- const formatted = format(context, opts);
8023
- const markerIdx = existing.indexOf('## Auto-generated signatures');
8024
- const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js') || existing.includes('# Code signatures');
8025
- const newContent = markerIdx !== -1
8026
- ? existing.slice(0, markerIdx) + COPILOT_MARKER.trimStart() + formatted
8027
- : (isLegacyGenerated ? COPILOT_MARKER.trimStart() + formatted : existing + COPILOT_MARKER + formatted);
8028
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
8029
- fs.writeFileSync(filePath, newContent, 'utf8');
8072
+
8073
+ const formatted = format(context, opts);
8074
+ const markerIdx = existing.indexOf('## Auto-generated signatures');
8075
+
8076
+ let newContent;
8077
+ if (markerIdx !== -1) {
8078
+ newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
8079
+ } else {
8080
+ const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
8081
+ || existing.includes('# Code signatures');
8082
+ newContent = isLegacyGenerated
8083
+ ? MARKER.trimStart() + formatted
8084
+ : existing + MARKER + formatted;
8030
8085
  }
8031
- module.exports = { name, format, outputPath, write };
8032
- };
8033
8086
 
8087
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
8088
+ fs.writeFileSync(filePath, newContent, 'utf8');
8089
+ }
8090
+
8091
+ module.exports = { name, format, outputPath, write };
8092
+
8093
+ };
8034
8094
  // ── ./packages/adapters/claude (bundled) ──
8035
8095
  __factories["./packages/adapters/claude"] = function(module, exports) {
8036
- const path = require('path');
8037
- const fs = require('fs');
8038
- const name = 'claude';
8039
- const CLAUDE_MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8040
- const CLAUDE_ALLOWLIST_MARKER = '<!-- sigmap-bash-allowlist -->';
8041
- const CLAUDE_ALLOWLIST_BLOCK = [
8042
- '## Bash allowlist',
8043
- '',
8044
- CLAUDE_ALLOWLIST_MARKER,
8045
- '',
8046
- 'The following sigmap commands are pre-approved — Claude Code will not prompt for confirmation:',
8047
- '',
8048
- '```json',
8049
- JSON.stringify({
8050
- permissions: {
8051
- allow: [
8052
- 'Bash(sigmap ask*)',
8053
- 'Bash(sigmap validate*)',
8054
- 'Bash(sigmap judge*)',
8055
- 'Bash(sigmap weights*)',
8056
- 'Bash(sigmap history*)',
8057
- 'Bash(sigmap --query*)',
8058
- 'Bash(sigmap --diff*)',
8059
- 'Bash(sigmap --health*)',
8060
- 'Bash(sigmap --coverage*)',
8061
- 'Bash(node gen-context.js*)',
8062
- ],
8063
- },
8064
- }, null, 2),
8065
- '```',
8066
- '',
8067
- 'Add the `permissions.allow` array above to `.claude/settings.json` to activate.',
8096
+ 'use strict';
8097
+
8098
+ /**
8099
+ * Claude adapter appends to CLAUDE.md under a marker line.
8100
+ * Never overwrites human-written content above the marker.
8101
+ *
8102
+ * Contract:
8103
+ * format(context, opts?) → string
8104
+ * outputPath(cwd) → string
8105
+ * write(context, cwd, opts?) → void (handles append logic)
8106
+ */
8107
+
8108
+ const path = require('path');
8109
+ const fs = require('fs');
8110
+
8111
+ const name = 'claude';
8112
+
8113
+ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8114
+ const ALLOWLIST_MARKER = '<!-- sigmap-bash-allowlist -->';
8115
+
8116
+ const ALLOWLIST_BLOCK = [
8117
+ '## Bash allowlist',
8118
+ '',
8119
+ ALLOWLIST_MARKER,
8120
+ '',
8121
+ 'The following sigmap commands are pre-approved — Claude Code will not prompt for confirmation:',
8122
+ '',
8123
+ '```json',
8124
+ JSON.stringify({
8125
+ permissions: {
8126
+ allow: [
8127
+ 'Bash(sigmap ask*)',
8128
+ 'Bash(sigmap validate*)',
8129
+ 'Bash(sigmap judge*)',
8130
+ 'Bash(sigmap weights*)',
8131
+ 'Bash(sigmap history*)',
8132
+ 'Bash(sigmap --query*)',
8133
+ 'Bash(sigmap --diff*)',
8134
+ 'Bash(sigmap --health*)',
8135
+ 'Bash(sigmap --coverage*)',
8136
+ 'Bash(node gen-context.js*)',
8137
+ ],
8138
+ },
8139
+ }, null, 2),
8140
+ '```',
8141
+ '',
8142
+ 'Add the `permissions.allow` array above to `.claude/settings.json` to activate.',
8143
+ '',
8144
+ ].join('\n');
8145
+
8146
+ /**
8147
+ * Format context suited for CLAUDE.md.
8148
+ * @param {string} context - Raw signature context string
8149
+ * @param {object} [opts]
8150
+ * @param {string} [opts.version] - SigMap version string
8151
+ * @returns {string}
8152
+ */
8153
+ function format(context, opts = {}) {
8154
+ if (!context || typeof context !== 'string') return '';
8155
+ const version = opts.version || 'unknown';
8156
+ const timestamp = new Date().toISOString();
8157
+ const meta = _confidenceMeta(opts);
8158
+ return [
8159
+ `<!-- Generated by SigMap v${version} — ${timestamp} -->`,
8160
+ meta,
8068
8161
  '',
8162
+ context,
8069
8163
  ].join('\n');
8070
- function format(context, opts = {}) {
8071
- if (!context || typeof context !== 'string') return '';
8072
- const version = (opts && opts.version) || 'unknown';
8073
- const timestamp = new Date().toISOString();
8074
- return [
8075
- '## SigMap',
8076
- '- Before searching for files, run `sigmap ask "<query>"` — ranks by TF-IDF relevance',
8077
- '- After config changes, run `sigmap validate` to confirm coverage',
8078
- '- To verify answer groundedness, run `sigmap judge --response <file> --context CLAUDE.md`',
8079
- '- For PR-scoped context, run `sigmap --diff`',
8080
- '',
8081
- `<!-- Generated by SigMap v${version} — ${timestamp} -->`,
8082
- '',
8083
- context,
8084
- ].join('\n');
8164
+ }
8165
+
8166
+ function _confidenceMeta(opts) {
8167
+ const parts = [`version=${opts.version || 'unknown'}`];
8168
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8169
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8170
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8171
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8172
+ return `<!-- sigmap: ${parts.join(' ')} -->`;
8173
+ }
8174
+
8175
+ /**
8176
+ * Return the output file path for this adapter.
8177
+ * @param {string} cwd - Project root
8178
+ * @returns {string}
8179
+ */
8180
+ function outputPath(cwd) {
8181
+ return path.join(cwd, 'CLAUDE.md');
8182
+ }
8183
+
8184
+ /**
8185
+ * Write signatures into CLAUDE.md using the append-under-marker strategy.
8186
+ * Human content above the marker is never touched.
8187
+ * @param {string} context - Raw signature context string
8188
+ * @param {string} cwd - Project root
8189
+ * @param {object} [opts]
8190
+ */
8191
+ function write(context, cwd, opts = {}) {
8192
+ const filePath = outputPath(cwd);
8193
+ let existing = '';
8194
+ if (fs.existsSync(filePath)) {
8195
+ existing = fs.readFileSync(filePath, 'utf8');
8085
8196
  }
8086
- function outputPath(cwd) { return path.join(cwd, 'CLAUDE.md'); }
8087
- function write(context, cwd, opts = {}) {
8088
- const filePath = outputPath(cwd);
8089
- let existing = '';
8090
- if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
8091
- const formatted = format(context, opts);
8092
- const markerIdx = existing.indexOf('## Auto-generated signatures');
8093
- let newContent = markerIdx !== -1
8094
- ? existing.slice(0, markerIdx) + CLAUDE_MARKER.trimStart() + formatted
8095
- : existing + CLAUDE_MARKER + formatted;
8096
- if (!newContent.includes(CLAUDE_ALLOWLIST_MARKER)) {
8097
- const sigPos = newContent.indexOf('## Auto-generated signatures');
8098
- if (sigPos !== -1) {
8099
- newContent = newContent.slice(0, sigPos) + CLAUDE_ALLOWLIST_BLOCK + '\n' + newContent.slice(sigPos);
8100
- } else {
8101
- newContent = CLAUDE_ALLOWLIST_BLOCK + '\n' + newContent;
8102
- }
8197
+ const formatted = format(context, opts);
8198
+ const markerIdx = existing.indexOf('## Auto-generated signatures');
8199
+ let newContent;
8200
+ if (markerIdx !== -1) {
8201
+ newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
8202
+ } else {
8203
+ newContent = existing + MARKER + formatted;
8204
+ }
8205
+
8206
+ // Inject ## Bash allowlist above the sig marker if not already present
8207
+ if (!newContent.includes(ALLOWLIST_MARKER)) {
8208
+ const sigMarkerPos = newContent.indexOf('## Auto-generated signatures');
8209
+ if (sigMarkerPos !== -1) {
8210
+ newContent = newContent.slice(0, sigMarkerPos) + ALLOWLIST_BLOCK + '\n' + newContent.slice(sigMarkerPos);
8211
+ } else {
8212
+ newContent = ALLOWLIST_BLOCK + '\n' + newContent;
8103
8213
  }
8104
- fs.writeFileSync(filePath, newContent, 'utf8');
8105
8214
  }
8106
- module.exports = { name, format, outputPath, write };
8107
- };
8108
8215
 
8216
+ fs.writeFileSync(filePath, newContent, 'utf8');
8217
+ }
8218
+
8219
+ module.exports = { name, format, outputPath, write };
8220
+
8221
+ };
8109
8222
  // ── ./packages/adapters/cursor (bundled) ──
8110
8223
  __factories["./packages/adapters/cursor"] = function(module, exports) {
8111
- const path = require('path');
8112
- const name = 'cursor';
8113
- function format(context, opts = {}) {
8114
- if (!context || typeof context !== 'string') return '';
8115
- const version = (opts && opts.version) || 'unknown';
8116
- const timestamp = new Date().toISOString();
8117
- return [
8118
- `# Code signatures generated by SigMap v${version}`,
8119
- `# Updated: ${timestamp}`,
8120
- `# Regenerate: node gen-context.js`,
8121
- '#',
8122
- '# SigMap: before answering, run: sigmap ask "<query>" — finds relevant files',
8123
- '# SigMap: after config changes, run: sigmap validate',
8124
- '# SigMap: to verify answers, run: sigmap judge --response <file>',
8125
- '',
8126
- context,
8127
- ].join('\n');
8128
- }
8129
- function outputPath(cwd) { return path.join(cwd, '.cursorrules'); }
8130
- module.exports = { name, format, outputPath };
8131
- };
8224
+ 'use strict';
8225
+
8226
+ /**
8227
+ * Cursor adapter writes to .cursorrules
8228
+ * Cursor reads .cursorrules automatically in every workspace.
8229
+ *
8230
+ * Contract:
8231
+ * format(context, opts?) string
8232
+ * outputPath(cwd) → string
8233
+ */
8234
+
8235
+ const path = require('path');
8236
+
8237
+ const name = 'cursor';
8238
+
8239
+ /**
8240
+ * Format context for Cursor rules file.
8241
+ * @param {string} context - Raw signature context string
8242
+ * @param {object} [opts]
8243
+ * @param {string} [opts.version] - SigMap version string
8244
+ * @returns {string}
8245
+ */
8246
+ function format(context, opts = {}) {
8247
+ if (!context || typeof context !== 'string') return '';
8248
+ const version = opts.version || 'unknown';
8249
+ const timestamp = new Date().toISOString();
8250
+ const meta = _confidenceMeta(opts);
8251
+ const header = [
8252
+ `# Code signatures — generated by SigMap v${version}`,
8253
+ `# Updated: ${timestamp}`,
8254
+ `# ${meta}`,
8255
+ `# Regenerate: node gen-context.js`,
8256
+ '',
8257
+ ].join('\n');
8258
+ return header + context;
8259
+ }
8260
+
8261
+ function _confidenceMeta(opts) {
8262
+ const parts = [`version=${opts.version || 'unknown'}`];
8263
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8264
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8265
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8266
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8267
+ return `sigmap: ${parts.join(' ')}`;
8268
+ }
8269
+
8270
+ /**
8271
+ * Return the output file path for this adapter.
8272
+ * @param {string} cwd - Project root
8273
+ * @returns {string}
8274
+ */
8275
+ function outputPath(cwd) {
8276
+ return path.join(cwd, '.cursorrules');
8277
+ }
8132
8278
 
8279
+ module.exports = { name, format, outputPath };
8280
+
8281
+ };
8133
8282
  // ── ./packages/adapters/windsurf (bundled) ──
8134
8283
  __factories["./packages/adapters/windsurf"] = function(module, exports) {
8135
- const path = require('path');
8136
- const name = 'windsurf';
8137
- function format(context, opts = {}) {
8138
- if (!context || typeof context !== 'string') return '';
8139
- const version = (opts && opts.version) || 'unknown';
8140
- const timestamp = new Date().toISOString();
8141
- return [
8142
- `# Code signatures — generated by SigMap v${version}`,
8143
- `# Updated: ${timestamp}`,
8144
- `# Regenerate: node gen-context.js`,
8145
- '#',
8146
- '# SigMap: before answering, run: sigmap ask "<query>" — finds relevant files',
8147
- '# SigMap: after config changes, run: sigmap validate',
8148
- '# SigMap: to verify answers, run: sigmap judge --response <file>',
8149
- '',
8150
- context,
8151
- ].join('\n');
8152
- }
8153
- function outputPath(cwd) { return path.join(cwd, '.windsurfrules'); }
8154
- module.exports = { name, format, outputPath };
8155
- };
8284
+ 'use strict';
8156
8285
 
8157
- // ── ./packages/adapters/openai (bundled) ──
8158
- __factories["./packages/adapters/openai"] = function(module, exports) {
8159
- const path = require('path');
8160
- const name = 'openai';
8161
- function format(context, opts = {}) {
8162
- if (!context || typeof context !== 'string') return '';
8163
- const version = (opts && opts.version) || 'unknown';
8164
- const timestamp = new Date().toISOString();
8165
- const projectLine = (opts && opts.projectName) ? `Project: ${opts.projectName}\n` : '';
8166
- return [
8167
- `You are a coding assistant with full knowledge of this codebase.`,
8168
- `Below are the code signatures extracted by SigMap v${version} on ${timestamp}.`,
8169
- projectLine,
8170
- `Use these signatures to answer questions about the code accurately.`,
8171
- `When the user asks about a specific file or function, refer to the signatures below.`,
8172
- `Before answering questions about specific code areas, prefer running \`sigmap ask "<query>"\` to get the most relevant files. After config changes, \`sigmap validate\` confirms coverage.`,
8173
- ``,
8174
- `## Code Signatures`,
8175
- ``,
8176
- context,
8177
- ].join('\n');
8178
- }
8179
- function outputPath(cwd) { return path.join(cwd, '.github', 'openai-context.md'); }
8180
- module.exports = { name, format, outputPath };
8181
- };
8286
+ /**
8287
+ * Windsurf adapter — writes to .windsurfrules
8288
+ * Windsurf reads .windsurfrules automatically in every workspace.
8289
+ *
8290
+ * Contract:
8291
+ * format(context, opts?) string
8292
+ * outputPath(cwd) string
8293
+ */
8182
8294
 
8183
- // ── ./packages/adapters/gemini (bundled) ──
8184
- __factories["./packages/adapters/gemini"] = function(module, exports) {
8185
- const path = require('path');
8186
- const fs = require('fs');
8187
- const name = 'gemini';
8188
- const GEMINI_MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8189
- function format(context, opts = {}) {
8190
- if (!context || typeof context !== 'string') return '';
8191
- const version = (opts && opts.version) || 'unknown';
8192
- const timestamp = new Date().toISOString();
8193
- const projectLine = (opts && opts.projectName) ? `Project: ${opts.projectName}\n` : '';
8194
- return [
8195
- `You are a coding assistant with complete knowledge of this codebase.`,
8196
- `The following code signatures were extracted by SigMap v${version} on ${timestamp}.`,
8197
- projectLine,
8198
- `These signatures represent every public function, class, and type in the project.`,
8199
- `Refer to them when answering questions about code structure, APIs, and implementation.`,
8200
- `Before answering questions about specific code areas, suggest running \`sigmap ask "<query>"\` to get the most relevant files. After config changes, \`sigmap validate\` confirms coverage.`,
8201
- ``,
8202
- `## Code Signatures`,
8203
- ``,
8204
- context,
8205
- ].join('\n');
8206
- }
8207
- function outputPath(cwd) { return path.join(cwd, '.github', 'gemini-context.md'); }
8208
- function write(context, cwd, opts = {}) {
8209
- const filePath = outputPath(cwd);
8210
- let existing = '';
8211
- if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
8212
- const formatted = format(context, opts);
8213
- const markerIdx = existing.indexOf('## Auto-generated signatures');
8214
- const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js') || existing.includes('## Code Signatures');
8215
- const newContent = markerIdx !== -1
8216
- ? existing.slice(0, markerIdx) + GEMINI_MARKER.trimStart() + formatted
8217
- : (isLegacyGenerated ? GEMINI_MARKER.trimStart() + formatted : existing + GEMINI_MARKER + formatted);
8218
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
8219
- fs.writeFileSync(filePath, newContent, 'utf8');
8220
- }
8221
- module.exports = { name, format, outputPath, write };
8222
- };
8295
+ const path = require('path');
8223
8296
 
8224
- // ── ./packages/adapters/codex (bundled) ──
8225
- __factories["./packages/adapters/codex"] = function(module, exports) {
8226
- const path = require('path');
8227
- const fs = require('fs');
8228
- const name = 'codex';
8229
- const CODEX_MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8230
- const CODEX_TOOLS_MARKER = '<!-- sigmap-tools -->';
8231
- const CODEX_TOOLS_BLOCK = [
8232
- '## Tools',
8233
- '',
8234
- CODEX_TOOLS_MARKER,
8235
- '',
8236
- '```json',
8237
- JSON.stringify([
8238
- { name: 'sigmap_ask', description: 'Rank source files by relevance to a natural-language query. Run before exploring the codebase.', command: 'sigmap ask "$QUERY"' },
8239
- { name: 'sigmap_validate', description: 'Validate SigMap config and measure context coverage. Run after changing config or source dirs.', command: 'sigmap validate' },
8240
- { name: 'sigmap_judge', description: 'Score an LLM response for groundedness against source context. Use to verify answer quality.', command: 'sigmap judge --response "$RESPONSE" --context "$CONTEXT"' },
8241
- { name: 'sigmap_query', description: 'Rank all files by relevance using TF-IDF and write a focused mini-context.', command: 'sigmap --query "$QUERY" --context' },
8242
- { name: 'sigmap_weights', description: 'Show learned file-ranking multipliers accumulated from past sessions.', command: 'sigmap weights' },
8243
- ], null, 2),
8244
- '```',
8297
+ const name = 'windsurf';
8298
+
8299
+ /**
8300
+ * Format context for Windsurf rules file.
8301
+ * @param {string} context - Raw signature context string
8302
+ * @param {object} [opts]
8303
+ * @param {string} [opts.version] - SigMap version string
8304
+ * @returns {string}
8305
+ */
8306
+ function format(context, opts = {}) {
8307
+ if (!context || typeof context !== 'string') return '';
8308
+ const version = opts.version || 'unknown';
8309
+ const timestamp = new Date().toISOString();
8310
+ const meta = _confidenceMeta(opts);
8311
+ const header = [
8312
+ `# Code signatures generated by SigMap v${version}`,
8313
+ `# Updated: ${timestamp}`,
8314
+ `# ${meta}`,
8315
+ `# Regenerate: node gen-context.js`,
8245
8316
  '',
8246
8317
  ].join('\n');
8247
- function format(context, opts = {}) {
8248
- if (!context || typeof context !== 'string' || !context.trim()) return '';
8249
- const toolBlock = [
8250
- '## SigMap commands',
8251
- '',
8252
- '| When | Command |',
8253
- '|------|---------|',
8254
- '| Before answering a question | `sigmap ask "<your question>"` |',
8255
- '| After code changes | `sigmap validate` |',
8256
- '| To query by topic | `sigmap --query "<topic>"` |',
8257
- '',
8258
- 'Always run `sigmap ask` or `sigmap --query` before searching for files relevant to a task.',
8259
- '',
8260
- ].join('\n');
8261
- return '# Code signatures\n\n' + toolBlock + context;
8262
- }
8263
- function outputPath(cwd) { return path.join(cwd, 'AGENTS.md'); }
8264
- function write(context, cwd, opts = {}) {
8265
- const filePath = outputPath(cwd);
8266
- let existing = '';
8267
- if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
8268
- const formatted = format(context, opts);
8269
- const markerIdx = existing.indexOf('## Auto-generated signatures');
8270
- const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js') || existing.includes('## Code Signatures') || existing.includes('# Code signatures');
8271
- let newContent = markerIdx !== -1
8272
- ? existing.slice(0, markerIdx) + CODEX_MARKER.trimStart() + formatted
8273
- : (isLegacyGenerated ? CODEX_MARKER.trimStart() + formatted : existing + CODEX_MARKER + formatted);
8274
- if (!newContent.includes(CODEX_TOOLS_MARKER)) {
8275
- const sigPos = newContent.indexOf('## Auto-generated signatures');
8276
- if (sigPos !== -1) {
8277
- newContent = newContent.slice(0, sigPos) + CODEX_TOOLS_BLOCK + '\n' + newContent.slice(sigPos);
8278
- } else {
8279
- newContent = CODEX_TOOLS_BLOCK + '\n' + newContent;
8280
- }
8281
- }
8282
- fs.writeFileSync(filePath, newContent, 'utf8');
8283
- }
8284
- module.exports = { name, format, outputPath, write };
8285
- };
8318
+ return header + context;
8319
+ }
8320
+
8321
+ function _confidenceMeta(opts) {
8322
+ const parts = [`version=${opts.version || 'unknown'}`];
8323
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8324
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8325
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8326
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8327
+ return `sigmap: ${parts.join(' ')}`;
8328
+ }
8329
+
8330
+ /**
8331
+ * Return the output file path for this adapter.
8332
+ * @param {string} cwd - Project root
8333
+ * @returns {string}
8334
+ */
8335
+ function outputPath(cwd) {
8336
+ return path.join(cwd, '.windsurfrules');
8337
+ }
8338
+
8339
+ module.exports = { name, format, outputPath };
8286
8340
 
8287
- // ── ./packages/adapters/index (bundled) ──
8288
- __factories["./packages/adapters/index"] = function(module, exports) {
8289
- const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
8290
- const _adapterCache = {};
8291
- function getAdapter(name) {
8292
- if (!name || typeof name !== 'string') return null;
8293
- const key = name.toLowerCase();
8294
- if (!ADAPTER_NAMES.includes(key)) return null;
8295
- if (!_adapterCache[key]) {
8296
- try { _adapterCache[key] = __require('./packages/adapters/' + key); } catch (_) { return null; }
8297
- }
8298
- return _adapterCache[key];
8299
- }
8300
- function listAdapters() { return ADAPTER_NAMES.slice(); }
8301
- function adapt(context, adapterName, opts = {}) {
8302
- if (!context || typeof context !== 'string') return '';
8303
- const adapter = getAdapter(adapterName);
8304
- if (!adapter || typeof adapter.format !== 'function') return '';
8305
- try { return adapter.format(context, opts); } catch (_) { return ''; }
8306
- }
8307
- function outputsToAdapters(outputs) {
8308
- if (!Array.isArray(outputs)) return ['copilot'];
8309
- return outputs.map((o) => ADAPTER_NAMES.includes(o) ? o : o);
8310
- }
8311
- module.exports = { getAdapter, listAdapters, adapt, outputsToAdapters };
8312
8341
  };
8342
+ // ── ./packages/adapters/openai (bundled) ──
8343
+ __factories["./packages/adapters/openai"] = function(module, exports) {
8344
+ 'use strict';
8313
8345
 
8314
- // ── ./src/extractors/generic ──
8315
- __factories["./src/extractors/generic"] = function(module, exports) {
8316
- 'use strict';
8317
- const PATTERNS = [
8318
- /^(?:pub\s+)?(?:async\s+)?function\s+\w+\s*\(/,
8346
+ /**
8347
+ * OpenAI adapter — formats context as an OpenAI system message.
8348
+ * Use the output as the `content` field of a system role message.
8349
+ *
8350
+ * Example usage in code:
8351
+ * const { format } = require('sigmap/adapters/openai');
8352
+ * const systemPrompt = format(context);
8353
+ * // Pass to: openai.chat.completions.create({ messages: [{ role: 'system', content: systemPrompt }] })
8354
+ *
8355
+ * Contract:
8356
+ * format(context, opts?) → string
8357
+ * outputPath(cwd) → string
8358
+ */
8359
+
8360
+ const path = require('path');
8361
+
8362
+ const name = 'openai';
8363
+
8364
+ /**
8365
+ * Format context as an OpenAI system prompt.
8366
+ * @param {string} context - Raw signature context string
8367
+ * @param {object} [opts]
8368
+ * @param {string} [opts.version] - SigMap version string
8369
+ * @param {string} [opts.projectName] - Optional project name
8370
+ * @returns {string}
8371
+ */
8372
+ function format(context, opts = {}) {
8373
+ if (!context || typeof context !== 'string') return '';
8374
+ const version = opts.version || 'unknown';
8375
+ const timestamp = new Date().toISOString();
8376
+ const projectLine = opts.projectName
8377
+ ? `Project: ${opts.projectName}\n`
8378
+ : '';
8379
+
8380
+ const meta = _confidenceMeta(opts);
8381
+ return [
8382
+ `You are a coding assistant with full knowledge of this codebase.`,
8383
+ `Below are the code signatures extracted by SigMap v${version} on ${timestamp}.`,
8384
+ `<!-- ${meta} -->`,
8385
+ projectLine,
8386
+ `Use these signatures to answer questions about the code accurately.`,
8387
+ `When the user asks about a specific file or function, refer to the signatures below.`,
8388
+ `## Code Signatures`,
8389
+ ``,
8390
+ context,
8391
+ ].join('\n');
8392
+ }
8393
+
8394
+ /**
8395
+ * Return the output file path for this adapter.
8396
+ * Writes a .openai-context.md file that can be loaded at runtime.
8397
+ * @param {string} cwd - Project root
8398
+ * @returns {string}
8399
+ */
8400
+ function outputPath(cwd) {
8401
+ return path.join(cwd, '.github', 'openai-context.md');
8402
+ }
8403
+
8404
+ function _confidenceMeta(opts) {
8405
+ const parts = [`version=${opts.version || 'unknown'}`];
8406
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8407
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8408
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8409
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8410
+ return `sigmap: ${parts.join(' ')}`;
8411
+ }
8412
+
8413
+ module.exports = { name, format, outputPath };
8414
+
8415
+ };
8416
+ // ── ./packages/adapters/gemini (bundled) ──
8417
+ __factories["./packages/adapters/gemini"] = function(module, exports) {
8418
+ 'use strict';
8419
+
8420
+ /**
8421
+ * Gemini adapter — formats context as a Gemini system instruction.
8422
+ * Use the output as the `system_instruction` field in a Gemini API request.
8423
+ *
8424
+ * Example usage:
8425
+ * const { format } = require('sigmap/adapters/gemini');
8426
+ * const instruction = format(context);
8427
+ * // Pass to: genAI.getGenerativeModel({ model: 'gemini-pro', systemInstruction: instruction })
8428
+ *
8429
+ * Contract:
8430
+ * format(context, opts?) → string
8431
+ * outputPath(cwd) → string
8432
+ */
8433
+
8434
+ const path = require('path');
8435
+ const fs = require('fs');
8436
+
8437
+ const name = 'gemini';
8438
+ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8439
+
8440
+ /**
8441
+ * Format context as a Gemini system instruction.
8442
+ * @param {string} context - Raw signature context string
8443
+ * @param {object} [opts]
8444
+ * @param {string} [opts.version] - SigMap version string
8445
+ * @param {string} [opts.projectName] - Optional project name
8446
+ * @returns {string}
8447
+ */
8448
+ function format(context, opts = {}) {
8449
+ if (!context || typeof context !== 'string') return '';
8450
+ const version = opts.version || 'unknown';
8451
+ const timestamp = new Date().toISOString();
8452
+ const projectLine = opts.projectName
8453
+ ? `Project: ${opts.projectName}\n`
8454
+ : '';
8455
+
8456
+ const meta = _confidenceMeta(opts);
8457
+ return [
8458
+ `You are a coding assistant with complete knowledge of this codebase.`,
8459
+ `The following code signatures were extracted by SigMap v${version} on ${timestamp}.`,
8460
+ `<!-- ${meta} -->`,
8461
+ projectLine,
8462
+ `These signatures represent every public function, class, and type in the project.`,
8463
+ `Refer to them when answering questions about code structure, APIs, and implementation.`,
8464
+ `## Code Signatures`,
8465
+ ``,
8466
+ context,
8467
+ ].join('\n');
8468
+ }
8469
+
8470
+ /**
8471
+ * Return the output file path for this adapter.
8472
+ * @param {string} cwd - Project root
8473
+ * @returns {string}
8474
+ */
8475
+ function outputPath(cwd) {
8476
+ return path.join(cwd, '.github', 'gemini-context.md');
8477
+ }
8478
+
8479
+ /**
8480
+ * Write signatures into gemini-context.md using append-under-marker.
8481
+ * If marker exists, content above marker is preserved.
8482
+ * If legacy generated content exists without marker, replace it cleanly.
8483
+ * @param {string} context - Raw signature context string
8484
+ * @param {string} cwd - Project root
8485
+ * @param {object} [opts]
8486
+ */
8487
+ function write(context, cwd, opts = {}) {
8488
+ const filePath = outputPath(cwd);
8489
+ let existing = '';
8490
+ if (fs.existsSync(filePath)) {
8491
+ existing = fs.readFileSync(filePath, 'utf8');
8492
+ }
8493
+
8494
+ const formatted = format(context, opts);
8495
+ const markerIdx = existing.indexOf('## Auto-generated signatures');
8496
+
8497
+ let newContent;
8498
+ if (markerIdx !== -1) {
8499
+ newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
8500
+ } else {
8501
+ const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
8502
+ || existing.includes('## Code Signatures');
8503
+ newContent = isLegacyGenerated
8504
+ ? MARKER.trimStart() + formatted
8505
+ : existing + MARKER + formatted;
8506
+ }
8507
+
8508
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
8509
+ fs.writeFileSync(filePath, newContent, 'utf8');
8510
+ }
8511
+
8512
+ function _confidenceMeta(opts) {
8513
+ const parts = [`version=${opts.version || 'unknown'}`];
8514
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8515
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8516
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8517
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8518
+ return `sigmap: ${parts.join(' ')}`;
8519
+ }
8520
+
8521
+ module.exports = { name, format, outputPath, write };
8522
+
8523
+ };
8524
+ // ── ./packages/adapters/codex (bundled) ──
8525
+ __factories["./packages/adapters/codex"] = function(module, exports) {
8526
+ 'use strict';
8527
+
8528
+ /**
8529
+ * Codex adapter — writes OpenAI-style context to AGENTS.md.
8530
+ *
8531
+ * This adapter reuses the same prompt format as the OpenAI adapter,
8532
+ * but targets AGENTS.md so Codex-style agents can read repository guidance.
8533
+ *
8534
+ * Contract:
8535
+ * format(context, opts?) → string
8536
+ * outputPath(cwd) → string
8537
+ * write(context, cwd, opts?) → void
8538
+ */
8539
+
8540
+ const path = require('path');
8541
+ const fs = require('fs');
8542
+
8543
+ const name = 'codex';
8544
+ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8545
+
8546
+ /**
8547
+ * Format context for AGENTS.md — clean markdown, no LLM preamble.
8548
+ * @param {string} context - Raw signature context string
8549
+ * @param {object} [opts]
8550
+ * @returns {string}
8551
+ */
8552
+ function format(context, opts = {}) {
8553
+ if (!context || typeof context !== 'string' || !context.trim()) return '';
8554
+ return `# Code signatures\n\n${context}`;
8555
+ }
8556
+
8557
+ /**
8558
+ * Return the output file path for this adapter.
8559
+ * @param {string} cwd - Project root
8560
+ * @returns {string}
8561
+ */
8562
+ function outputPath(cwd) {
8563
+ return path.join(cwd, 'AGENTS.md');
8564
+ }
8565
+
8566
+ /**
8567
+ * Write signatures into AGENTS.md using append-under-marker.
8568
+ * If marker exists, content above marker is preserved.
8569
+ * If legacy generated content exists without marker, replace it cleanly.
8570
+ * @param {string} context - Raw signature context string
8571
+ * @param {string} cwd - Project root
8572
+ * @param {object} [opts]
8573
+ */
8574
+ function write(context, cwd, opts = {}) {
8575
+ const filePath = outputPath(cwd);
8576
+ let existing = '';
8577
+ if (fs.existsSync(filePath)) {
8578
+ existing = fs.readFileSync(filePath, 'utf8');
8579
+ }
8580
+
8581
+ const formatted = format(context, opts);
8582
+ const markerIdx = existing.indexOf('## Auto-generated signatures');
8583
+
8584
+ let newContent;
8585
+ if (markerIdx !== -1) {
8586
+ newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
8587
+ } else {
8588
+ const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
8589
+ || existing.includes('## Code Signatures')
8590
+ || existing.includes('# Code signatures');
8591
+ newContent = isLegacyGenerated
8592
+ ? MARKER.trimStart() + formatted
8593
+ : existing + MARKER + formatted;
8594
+ }
8595
+
8596
+ fs.writeFileSync(filePath, newContent, 'utf8');
8597
+ }
8598
+
8599
+ module.exports = { name, format, outputPath, write };
8600
+
8601
+ };
8602
+ // ── ./packages/adapters/index (bundled) ──
8603
+ __factories["./packages/adapters/index"] = function(module, exports) {
8604
+ const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
8605
+ const _adapterCache = {};
8606
+ function getAdapter(name) {
8607
+ if (!name || typeof name !== 'string') return null;
8608
+ const key = name.toLowerCase();
8609
+ if (!ADAPTER_NAMES.includes(key)) return null;
8610
+ if (!_adapterCache[key]) {
8611
+ try { _adapterCache[key] = __require('./packages/adapters/' + key); } catch (_) { return null; }
8612
+ }
8613
+ return _adapterCache[key];
8614
+ }
8615
+ function listAdapters() { return ADAPTER_NAMES.slice(); }
8616
+ function adapt(context, adapterName, opts = {}) {
8617
+ if (!context || typeof context !== 'string') return '';
8618
+ const adapter = getAdapter(adapterName);
8619
+ if (!adapter || typeof adapter.format !== 'function') return '';
8620
+ try { return adapter.format(context, opts); } catch (_) { return ''; }
8621
+ }
8622
+ function outputsToAdapters(outputs) {
8623
+ if (!Array.isArray(outputs)) return ['copilot'];
8624
+ return outputs.map((o) => ADAPTER_NAMES.includes(o) ? o : o);
8625
+ }
8626
+ module.exports = { getAdapter, listAdapters, adapt, outputsToAdapters };
8627
+ };
8628
+
8629
+ // ── ./src/extractors/generic ──
8630
+ __factories["./src/extractors/generic"] = function(module, exports) {
8631
+ 'use strict';
8632
+ const PATTERNS = [
8633
+ /^(?:pub\s+)?(?:async\s+)?function\s+\w+\s*\(/,
8319
8634
  /^(?:pub\s+)?(?:async\s+)?fn\s+\w+[\s(<]/,
8320
8635
  /^def\s+\w+[\s(|:]/,
8321
8636
  /^(?:pub\s+)?func\s+\w+\s*\(/,
@@ -8373,10 +8688,9 @@ __factories["./src/format/llms-txt"] = function(module, exports) {
8373
8688
  'use strict';
8374
8689
  const path = require('path');
8375
8690
  const fs = require('fs');
8376
- const { execSync } = require('child_process');
8377
8691
  function outputPath(cwd) { return path.join(cwd, 'llms.txt'); }
8378
8692
  function getShortCommit(cwd) {
8379
- try { return execSync('git rev-parse --short HEAD', { cwd, timeout: 2000 }).toString().trim(); }
8693
+ try { return __tryGit(['rev-parse', '--short', 'HEAD'], { cwd, timeout: 2000 }); }
8380
8694
  catch (_) { return ''; }
8381
8695
  }
8382
8696
  function detectVersion(cwd) {
@@ -8701,14 +9015,13 @@ __factories["./src/discovery/source-root-scorer"] = function(module, exports) {
8701
9015
  'use strict';
8702
9016
  const fs = require('fs');
8703
9017
  const path = require('path');
8704
- const { execSync } = require('child_process');
8705
9018
  const CODE_EXTS = new Set(['.js','.mjs','.cjs','.ts','.tsx','.jsx','.py','.rb','.go','.rs','.java','.kt','.cs','.cpp','.c','.h','.swift','.dart','.scala','.php']);
8706
9019
  const AUTO_SKIP = new Set(['node_modules','dist','build','.git','.next','.nuxt','vendor','DerivedData','Pods','target','coverage','__pycache__','.venv','venv','.build','Carthage','storybook-static','.gradle','bin','obj','.vs']);
8707
9020
  const PENALTY_DIRS = new Set(['test','tests','spec','__tests__','e2e','docs','doc','docs-vp','examples','example','fixtures','mocks','__mocks__','demo','samples','migrations','benchmarks','scripts']);
8708
9021
  const ROOT_ENTRYPOINTS = { go: ['main.go'], python: ['app.py','main.py','wsgi.py','asgi.py'], javascript: ['index.js','server.js','app.js'], typescript: ['index.ts','main.ts'], rust: [], php: ['index.php'] };
8709
9022
  function getRecentlyChangedDirs(cwd) {
8710
9023
  try {
8711
- const out = execSync('git log --name-only --format="" HEAD~10 2>/dev/null', { cwd, timeout: 3000 }).toString();
9024
+ const out = __git(['log', '--name-only', '--format=', 'HEAD~10'], { cwd, timeout: 3000 }).toString();
8712
9025
  return new Set(out.split('\n').filter(Boolean).map(f => f.split('/')[0]));
8713
9026
  } catch { return new Set(); }
8714
9027
  }
@@ -9180,6 +9493,605 @@ __factories["./src/workspace/detector"] = function(module, exports) {
9180
9493
  * No npm install required. Node 18+ built-ins only.
9181
9494
  */
9182
9495
 
9496
+ // ── ./src/format/usage-guidance ──
9497
+ __factories["./src/format/usage-guidance"] = function(module, exports) {
9498
+ 'use strict';
9499
+
9500
+ /**
9501
+ * Canonical "how to use SigMap" guidance block (v6.16/v7.0).
9502
+ *
9503
+ * Every adapter emits this one identical block so all generated context files
9504
+ * (CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, GEMINI.md, .cursorrules,
9505
+ * …) carry the same, single usage section — instead of each adapter inventing
9506
+ * its own wording (and codex emitting a redundant second JSON block).
9507
+ */
9508
+
9509
+ function usageBlock() {
9510
+ return [
9511
+ '## SigMap commands',
9512
+ '',
9513
+ '| When | Command |',
9514
+ '|------|---------|',
9515
+ '| Before answering a question about code | `sigmap ask "<your question>"` |',
9516
+ '| To rank files by topic | `sigmap --query "<topic>"` |',
9517
+ '| After changing config or source dirs | `sigmap validate` |',
9518
+ '| To verify an AI answer is grounded | `sigmap judge --response <file>` |',
9519
+ '',
9520
+ 'Always run `sigmap ask` (or `sigmap --query`) before searching for files relevant to a task.',
9521
+ '',
9522
+ ].join('\n');
9523
+ }
9524
+
9525
+ module.exports = { usageBlock };
9526
+
9527
+ };
9528
+
9529
+ // ── ./src/squeeze/classify ──
9530
+ __factories["./src/squeeze/classify"] = function(module, exports) {
9531
+ 'use strict';
9532
+
9533
+ /**
9534
+ * Squeeze input classifier (v7.0.0).
9535
+ *
9536
+ * Deterministic, zero-dep detector that labels a pasted blob as a
9537
+ * `stacktrace`, `cilog`, or `json` payload — or `null` (pass through). Pure
9538
+ * regex/heuristics; runs in well under 10ms even on large input.
9539
+ *
9540
+ * Order matters: stack traces are highest value and a CI log often *contains*
9541
+ * a trace, so stacktrace is checked first, then cilog, then json.
9542
+ */
9543
+
9544
+ const FRAME_RE = [
9545
+ /^\s*at\s+.+\(.+:\d+:\d+\)\s*$/, // JS: at fn (file:line:col)
9546
+ /^\s*at\s+.+:\d+:\d+\s*$/, // JS: at file:line:col
9547
+ /^\s*at\s+[\w$.<>]+\(.+\.\w+:\d+\)/, // Java/Kotlin: at pkg.Cls.m(File.java:42)
9548
+ /^\s*File\s+".+",\s+line\s+\d+/, // Python frame
9549
+ /^\s+\w+.*\([^)]*\.(go|rs):\d+\)/, // Go/Rust frame with file:line
9550
+ /^\s*#\d+\s+0x[0-9a-f]+/, // native/gdb frame
9551
+ ];
9552
+
9553
+ const STACK_HEADER_RE = [
9554
+ /Traceback \(most recent call last\)/,
9555
+ /Exception in thread/,
9556
+ /\bat Object\.<anonymous>\b/,
9557
+ /thread '.*' panicked at/,
9558
+ /^panic:/m,
9559
+ /goroutine \d+ \[/,
9560
+ ];
9561
+
9562
+ function countFrames(lines) {
9563
+ let n = 0;
9564
+ for (const line of lines) {
9565
+ if (FRAME_RE.some((re) => re.test(line))) n++;
9566
+ }
9567
+ return n;
9568
+ }
9569
+
9570
+ function matchesStackTrace(input, lines) {
9571
+ const frames = countFrames(lines);
9572
+ const header = STACK_HEADER_RE.some((re) => re.test(input));
9573
+ // A single explicit header (Traceback/panic) counts as strong evidence even
9574
+ // with few parsed frames; otherwise require 2+ frame-like lines.
9575
+ if (!header && frames < 2) return { match: false, confidence: 0, frames };
9576
+ // Confidence scales with frame count; a header alone floors it at 0.6.
9577
+ let confidence = Math.min(0.97, 0.15 * frames);
9578
+ if (header) confidence = Math.max(confidence, 0.6 + Math.min(0.3, 0.05 * frames));
9579
+ return { match: true, confidence: Number(confidence.toFixed(2)), frames };
9580
+ }
9581
+
9582
+ const TS_RE = /(\b\d{1,2}:\d{2}:\d{2}\b)|(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})/;
9583
+ const PROGRESS_RE = /(\d{1,3}%)|(\bDownloading\b)|(\bnpm (WARN|notice|http)\b)|(##\[(group|endgroup|command)\])|(\[\d+\/\d+\])|(▕|█|━|⠿)|(\bETA\b)|(\r$)/;
9584
+
9585
+ function matchesCiLog(input, lines) {
9586
+ if (lines.length < 8) return { match: false, confidence: 0 };
9587
+ let logish = 0;
9588
+ const seen = new Map();
9589
+ let repeats = 0;
9590
+ for (const line of lines) {
9591
+ if (TS_RE.test(line) || PROGRESS_RE.test(line)) logish++;
9592
+ const norm = line.replace(/\d+/g, '#').trim();
9593
+ if (norm) {
9594
+ const c = (seen.get(norm) || 0) + 1;
9595
+ seen.set(norm, c);
9596
+ if (c > 1) repeats++;
9597
+ }
9598
+ }
9599
+ const density = logish / lines.length;
9600
+ const repeatRatio = repeats / lines.length;
9601
+ if (density < 0.4 && repeatRatio < 0.4) return { match: false, confidence: 0 };
9602
+ const confidence = Math.min(0.95, Math.max(density, repeatRatio) + 0.15);
9603
+ return { match: true, confidence: Number(confidence.toFixed(2)) };
9604
+ }
9605
+
9606
+ function matchesJsonPayload(input) {
9607
+ const trimmed = input.trim();
9608
+ if (/^[[{]/.test(trimmed)) {
9609
+ try {
9610
+ JSON.parse(trimmed);
9611
+ return { match: true, confidence: 0.95 };
9612
+ } catch (_) { /* not strict JSON — fall through to heuristic */ }
9613
+ }
9614
+ const lines = trimmed.split('\n');
9615
+ if (lines.length < 4) return { match: false, confidence: 0 };
9616
+ let kv = 0;
9617
+ for (const line of lines) {
9618
+ if (/^\s*"[^"]+"\s*:\s*.+/.test(line) || /^\s*[}\]],?\s*$/.test(line)) kv++;
9619
+ }
9620
+ const ratio = kv / lines.length;
9621
+ if (ratio < 0.6) return { match: false, confidence: 0 };
9622
+ return { match: true, confidence: Number(Math.min(0.9, ratio).toFixed(2)) };
9623
+ }
9624
+
9625
+ /**
9626
+ * @param {string} input
9627
+ * @returns {{ category: 'stacktrace'|'cilog'|'json'|null, confidence: number }}
9628
+ */
9629
+ function classify(input) {
9630
+ if (typeof input !== 'string' || !input.trim()) return { category: null, confidence: 0 };
9631
+ const lines = input.split('\n');
9632
+
9633
+ const st = matchesStackTrace(input, lines);
9634
+ if (st.match) return { category: 'stacktrace', confidence: st.confidence };
9635
+
9636
+ const ci = matchesCiLog(input, lines);
9637
+ if (ci.match) return { category: 'cilog', confidence: ci.confidence };
9638
+
9639
+ const js = matchesJsonPayload(input);
9640
+ if (js.match) return { category: 'json', confidence: js.confidence };
9641
+
9642
+ return { category: null, confidence: 0 };
9643
+ }
9644
+
9645
+ module.exports = { classify, countFrames };
9646
+
9647
+ };
9648
+
9649
+ // ── ./src/squeeze/cilog ──
9650
+ __factories["./src/squeeze/cilog"] = function(module, exports) {
9651
+ 'use strict';
9652
+
9653
+ /**
9654
+ * CI / build-log squeeze (v7.0.0).
9655
+ *
9656
+ * Strips timestamps, progress bars, and repeated noise; keeps every error line
9657
+ * plus a small context window around it. Never returns empty — when there are
9658
+ * no errors it falls back to a head/tail summary. Also reused by the stacktrace
9659
+ * squeezer to clean noise surrounding a trace.
9660
+ */
9661
+
9662
+ const TS_PREFIX_RE = /^\s*(?:\[?\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:[.,]\d+)?Z?\]?\s*|\[?\d{1,2}:\d{2}:\d{2}(?:[.,]\d+)?\]?\s*)+/;
9663
+ const PROGRESS_LINE_RE = /(?:^|\s)(?:\d{1,3}%|Downloading|Receiving objects|Resolving deltas|Compressing objects|npm (?:WARN|notice|http|sill|verb)|##\[(?:group|endgroup|command|section)\]|\[\d+\/\d+\]|ETA[: ]|█|━|▕|⣿)/;
9664
+ const ERROR_RE = /\b(?:error|err!|fail(?:ed|ure)?|exception|panic|fatal|traceback|ERR_|E[A-Z]{3,})\b|✗|❌/i;
9665
+
9666
+ /** Remove a leading timestamp prefix from a single line. */
9667
+ function stripTimestamp(line) {
9668
+ return line.replace(TS_PREFIX_RE, '');
9669
+ }
9670
+
9671
+ /**
9672
+ * @param {string} input
9673
+ * @param {object} [opts]
9674
+ * @param {number} [opts.context=2] context lines kept around each error
9675
+ * @returns {{ squeezed: string, kept: string[], stripped: string[] }}
9676
+ */
9677
+ function squeezeCiLog(input, opts = {}) {
9678
+ const ctx = opts.context != null ? opts.context : 2;
9679
+ const lines = input.split('\n');
9680
+ const keep = new Set();
9681
+ const errorIdx = [];
9682
+
9683
+ for (let i = 0; i < lines.length; i++) {
9684
+ if (ERROR_RE.test(lines[i])) {
9685
+ errorIdx.push(i);
9686
+ for (let j = Math.max(0, i - ctx); j <= Math.min(lines.length - 1, i + ctx); j++) keep.add(j);
9687
+ }
9688
+ }
9689
+
9690
+ let body;
9691
+ let keptDesc;
9692
+ if (errorIdx.length === 0) {
9693
+ const head = lines.slice(0, 10).map(stripTimestamp);
9694
+ const tail = lines.length > 20 ? lines.slice(-10).map(stripTimestamp) : [];
9695
+ body = head.slice();
9696
+ if (tail.length) body.push(`… (${lines.length - 20} lines omitted) …`, ...tail);
9697
+ keptDesc = ['head/tail summary (no error lines found)'];
9698
+ } else {
9699
+ const sorted = [...keep].sort((a, b) => a - b);
9700
+ body = [];
9701
+ let prev = -2;
9702
+ for (const idx of sorted) {
9703
+ // Drop pure progress/noise lines unless they are themselves errors.
9704
+ const line = stripTimestamp(lines[idx]);
9705
+ if (PROGRESS_LINE_RE.test(line) && !ERROR_RE.test(line)) continue;
9706
+ if (idx > prev + 1) body.push(`… (${idx - prev - 1} lines) …`);
9707
+ body.push(line);
9708
+ prev = idx;
9709
+ }
9710
+ if (body.length === 0) body = errorIdx.map((i) => stripTimestamp(lines[i])); // safety net
9711
+ keptDesc = [`${errorIdx.length} error line(s) + ${ctx}-line context`];
9712
+ }
9713
+
9714
+ return {
9715
+ squeezed: body.join('\n'),
9716
+ kept: keptDesc,
9717
+ stripped: [`${Math.max(0, lines.length - body.length)} timestamp/progress/noise line(s)`],
9718
+ };
9719
+ }
9720
+
9721
+ module.exports = { squeezeCiLog, stripTimestamp, ERROR_RE };
9722
+
9723
+ };
9724
+
9725
+ // ── ./src/squeeze/jsonpayload ──
9726
+ __factories["./src/squeeze/jsonpayload"] = function(module, exports) {
9727
+ 'use strict';
9728
+
9729
+ /**
9730
+ * JSON-payload squeeze (v7.0.0).
9731
+ *
9732
+ * Collapses repeated array elements, truncates long string values, and
9733
+ * preserves the schema shape at every depth — so an LLM still sees the
9734
+ * structure of an API/GraphQL/validation error without the bulk.
9735
+ */
9736
+
9737
+ const MAX_STR = 500;
9738
+ const ARRAY_KEEP = 2;
9739
+
9740
+ function squeezeValue(v, opts) {
9741
+ const maxStr = opts.maxStr;
9742
+ const keep = opts.arrayKeep;
9743
+ if (Array.isArray(v)) {
9744
+ if (v.length <= keep + 1) return v.map((x) => squeezeValue(x, opts));
9745
+ const head = v.slice(0, keep).map((x) => squeezeValue(x, opts));
9746
+ head.push(`…${v.length - keep} more similar items`);
9747
+ return head;
9748
+ }
9749
+ if (v && typeof v === 'object') {
9750
+ const o = {};
9751
+ for (const k of Object.keys(v)) o[k] = squeezeValue(v[k], opts);
9752
+ return o;
9753
+ }
9754
+ if (typeof v === 'string' && v.length > maxStr) {
9755
+ return v.slice(0, maxStr) + `…(${v.length} chars)`;
9756
+ }
9757
+ return v;
9758
+ }
9759
+
9760
+ /**
9761
+ * @param {string} input
9762
+ * @param {object} [opts]
9763
+ * @param {number} [opts.maxStr=500] truncate strings longer than this
9764
+ * @param {number} [opts.arrayKeep=2] array items kept before collapsing
9765
+ * @returns {{ squeezed, kept, stripped }}
9766
+ */
9767
+ function squeezeJsonPayload(input, opts = {}) {
9768
+ let parsed;
9769
+ try { parsed = JSON.parse(input); }
9770
+ catch (_) { return { squeezed: input, kept: ['(not valid JSON — unchanged)'], stripped: [] }; }
9771
+ const cfg = { maxStr: opts.maxStr != null ? opts.maxStr : MAX_STR, arrayKeep: opts.arrayKeep != null ? opts.arrayKeep : ARRAY_KEEP };
9772
+ const squeezed = JSON.stringify(squeezeValue(parsed, cfg), null, 2);
9773
+ return {
9774
+ squeezed,
9775
+ kept: ['schema shape preserved at all depths'],
9776
+ stripped: ['collapsed repeated array items; truncated long string values'],
9777
+ };
9778
+ }
9779
+
9780
+ module.exports = { squeezeJsonPayload, squeezeValue };
9781
+
9782
+ };
9783
+
9784
+ // ── ./src/squeeze/stacktrace ──
9785
+ __factories["./src/squeeze/stacktrace"] = function(module, exports) {
9786
+ 'use strict';
9787
+
9788
+ /**
9789
+ * Stack-trace squeeze (v7.0.0) — the highest-value squeeze module.
9790
+ *
9791
+ * Dedupes repeated exceptions, strips vendor frames, keeps frames in the user's
9792
+ * own source dirs, and — the differentiator — **enriches the top kept frame**
9793
+ * with its real signature from the SigMap symbol index (`buildSigIndex`).
9794
+ * Generic log summarizers can't do this; SigMap has the repo's symbol map.
9795
+ *
9796
+ * Pure/deterministic. The symbol index is injected via `opts.symbolIndex` so
9797
+ * the module is unit-testable without touching the filesystem.
9798
+ */
9799
+
9800
+ const path = require('path');
9801
+
9802
+ const VENDOR_RE = /(?:^|[\\/])(?:node_modules|vendor|site-packages|dist|build|\.venv|venv|third_party|external|\.cargo|go\/pkg\/mod)[\\/]/;
9803
+
9804
+ /** Parse a frame line across JS/TS, Python, Java/Kotlin, Go, Rust, native. */
9805
+ function parseFrame(line) {
9806
+ let m;
9807
+ if ((m = line.match(/^\s*at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/))) return { fn: m[1], file: m[2], line: +m[3], raw: line };
9808
+ if ((m = line.match(/^\s*at\s+(.+?):(\d+):(\d+)\s*$/))) return { fn: '', file: m[1], line: +m[2], raw: line };
9809
+ if ((m = line.match(/^\s*at\s+([\w$.<>]+)\((.+?):(\d+)\)/))) return { fn: m[1], file: m[2], line: +m[3], raw: line };
9810
+ if ((m = line.match(/^\s*File\s+"(.+?)",\s+line\s+(\d+)(?:,\s+in\s+(.+))?/))) return { fn: (m[3] || '').trim(), file: m[1], line: +m[2], raw: line };
9811
+ if ((m = line.match(/^\s*(.+\.(?:go|rs)):(\d+)/))) return { fn: '', file: m[1], line: +m[2], raw: line };
9812
+ return null;
9813
+ }
9814
+
9815
+ function isVendor(file) { return VENDOR_RE.test(String(file).replace(/\\/g, '/')); }
9816
+
9817
+ function inSrcDirs(file, srcDirs) {
9818
+ const f = String(file).replace(/\\/g, '/');
9819
+ return srcDirs.some((d) => {
9820
+ const dd = String(d).replace(/^\.\//, '').replace(/\/$/, '');
9821
+ return dd && (f === dd || f.startsWith(dd + '/') || f.includes('/' + dd + '/'));
9822
+ });
9823
+ }
9824
+
9825
+ /** Look up the real signature for a frame in the SigMap symbol index. */
9826
+ function enrichFrame(frame, symbolIndex) {
9827
+ if (!symbolIndex || !frame) return null;
9828
+ const want = String(frame.file).replace(/\\/g, '/');
9829
+ const base = path.basename(want);
9830
+ let key = null;
9831
+ for (const k0 of symbolIndex.keys()) {
9832
+ const k = String(k0).replace(/\\/g, '/');
9833
+ if (k === want || want.endsWith('/' + k) || k.endsWith('/' + want)) { key = k0; break; }
9834
+ if (!key && path.basename(k) === base) key = k0;
9835
+ }
9836
+ if (!key) return null;
9837
+ const sigs = symbolIndex.get(key) || [];
9838
+ const wantFn = frame.fn ? frame.fn.split('.').pop() : '';
9839
+ let byLine = null, byName = null;
9840
+ for (const sig of sigs) {
9841
+ const s = String(sig);
9842
+ const mm = s.match(/:(\d+)(?:-(\d+))?\s*$/);
9843
+ if (mm) {
9844
+ const a = +mm[1], b = mm[2] ? +mm[2] : a;
9845
+ if (frame.line >= a && frame.line <= b) byLine = s;
9846
+ }
9847
+ if (wantFn && new RegExp('\\b' + wantFn.replace(/[^\w$]/g, '') + '\\b').test(s)) byName = byName || s;
9848
+ }
9849
+ const sig = byLine || byName;
9850
+ return sig ? { file: key, sig: sig.replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim() } : null;
9851
+ }
9852
+
9853
+ /**
9854
+ * @param {string} input
9855
+ * @param {object} [opts]
9856
+ * @param {string[]} [opts.srcDirs] user source dirs (default ['src'])
9857
+ * @param {Map} [opts.symbolIndex] SigMap signature index for enrichment
9858
+ * @param {number} [opts.maxFrames=8] cap on kept source frames
9859
+ * @returns {{ squeezed, kept, stripped, enriched }}
9860
+ */
9861
+ function squeezeStackTrace(input, opts = {}) {
9862
+ const srcDirs = (opts.srcDirs && opts.srcDirs.length) ? opts.srcDirs : ['src'];
9863
+ const maxFrames = opts.maxFrames != null ? opts.maxFrames : 8;
9864
+ const lines = input.split('\n');
9865
+
9866
+ const headerCount = new Map();
9867
+ const headerOrder = [];
9868
+ const frames = [];
9869
+ for (const line of lines) {
9870
+ const f = parseFrame(line);
9871
+ if (f) { frames.push(f); continue; }
9872
+ const t = line.trim();
9873
+ if (!t) continue;
9874
+ if (!headerCount.has(t)) headerOrder.push(t);
9875
+ headerCount.set(t, (headerCount.get(t) || 0) + 1);
9876
+ }
9877
+
9878
+ const seen = new Set();
9879
+ let dupFrames = 0;
9880
+ const nonVendor = [];
9881
+ const sourceFrames = [];
9882
+ let vendorCount = 0;
9883
+ for (const f of frames) {
9884
+ const k = f.file + ':' + f.line;
9885
+ if (seen.has(k)) { dupFrames++; continue; }
9886
+ seen.add(k);
9887
+ if (isVendor(f.file)) { vendorCount++; continue; }
9888
+ nonVendor.push(f);
9889
+ if (inSrcDirs(f.file, srcDirs)) sourceFrames.push(f);
9890
+ }
9891
+
9892
+ // Prefer source frames; never return empty (fall back to top non-vendor, then raw).
9893
+ const shown = sourceFrames.length ? sourceFrames.slice(0, maxFrames)
9894
+ : (nonVendor.length ? nonVendor.slice(0, 3) : frames.slice(0, 3));
9895
+
9896
+ const enrichment = shown.length ? enrichFrame(shown[0], opts.symbolIndex) : null;
9897
+
9898
+ const out = [];
9899
+ for (const h of headerOrder) {
9900
+ const n = headerCount.get(h);
9901
+ out.push(n > 1 ? `${h} (occurred ×${n})` : h);
9902
+ }
9903
+ for (let i = 0; i < shown.length; i++) {
9904
+ out.push(' ' + shown[i].raw.trim());
9905
+ if (i === 0 && enrichment) out.push(` ↳ ${enrichment.sig} [${enrichment.file}]`);
9906
+ }
9907
+
9908
+ return {
9909
+ squeezed: out.join('\n'),
9910
+ kept: [
9911
+ `${headerOrder.length} unique exception(s)`,
9912
+ `top ${shown.length} ${sourceFrames.length ? 'source ' : ''}frame(s)`,
9913
+ ...(enrichment ? [`enriched ${path.basename(shown[0].file)}:${shown[0].line}`] : []),
9914
+ ],
9915
+ stripped: [`${vendorCount} vendor frame(s)`, `${dupFrames} duplicate frame(s)`],
9916
+ enriched: !!enrichment,
9917
+ };
9918
+ }
9919
+
9920
+ module.exports = { squeezeStackTrace, parseFrame, isVendor, inSrcDirs, enrichFrame };
9921
+
9922
+ };
9923
+
9924
+ // ── ./src/squeeze/index ──
9925
+ __factories["./src/squeeze/index"] = function(module, exports) {
9926
+ 'use strict';
9927
+
9928
+ /**
9929
+ * Squeeze orchestrator (v7.0.0): classify → squeeze → reduction → decision.
9930
+ *
9931
+ * Always-on and silent: callers run `squeeze()` on pasted input, then use
9932
+ * `shouldPrompt()` to decide whether the reduction is worth interrupting for.
9933
+ * Everything is deterministic and offline; the symbol index for stack-trace
9934
+ * enrichment is passed through via `opts.symbolIndex`.
9935
+ */
9936
+
9937
+ const { classify } = __require('./src/squeeze/classify');
9938
+ const { squeezeStackTrace } = __require('./src/squeeze/stacktrace');
9939
+ const { squeezeCiLog } = __require('./src/squeeze/cilog');
9940
+ const { squeezeJsonPayload } = __require('./src/squeeze/jsonpayload');
9941
+
9942
+ function estimateTokens(s) { return Math.ceil(String(s || '').length / 4); }
9943
+
9944
+ /**
9945
+ * @param {string} input
9946
+ * @param {object} [opts] forwarded to the category squeezer (srcDirs, symbolIndex, …)
9947
+ * @returns {{ category, confidence, original, squeezed, rawTokens, squeezedTokens, reduction, kept, stripped, enriched, applies }}
9948
+ */
9949
+ function squeeze(input, opts = {}) {
9950
+ const { category, confidence } = classify(input);
9951
+ const rawTokens = estimateTokens(input);
9952
+ const base = {
9953
+ category, confidence, original: input, squeezed: input,
9954
+ rawTokens, squeezedTokens: rawTokens, reduction: 0,
9955
+ kept: [], stripped: [], enriched: false, applies: false,
9956
+ };
9957
+ if (!category) return base;
9958
+
9959
+ let r;
9960
+ if (category === 'stacktrace') r = squeezeStackTrace(input, opts);
9961
+ else if (category === 'cilog') r = squeezeCiLog(input, opts);
9962
+ else r = squeezeJsonPayload(input, opts);
9963
+
9964
+ const squeezedTokens = estimateTokens(r.squeezed);
9965
+ const reduction = rawTokens > 0 ? (rawTokens - squeezedTokens) / rawTokens : 0;
9966
+ return {
9967
+ category, confidence,
9968
+ original: input, squeezed: r.squeezed,
9969
+ rawTokens, squeezedTokens,
9970
+ reduction: Math.max(0, Number(reduction.toFixed(4))),
9971
+ kept: r.kept || [], stripped: r.stripped || [], enriched: !!r.enriched,
9972
+ applies: squeezedTokens < rawTokens,
9973
+ };
9974
+ }
9975
+
9976
+ /** True when the reduction clears the threshold (accepts 0–1 or 0–100). */
9977
+ function shouldPrompt(reduction, threshold) {
9978
+ const t = threshold > 1 ? threshold / 100 : threshold;
9979
+ return reduction >= t;
9980
+ }
9981
+
9982
+ /** A compact human summary of what squeeze would do (for the prompt). */
9983
+ function formatSummary(result) {
9984
+ const pct = Math.round(result.reduction * 100);
9985
+ const lines = [
9986
+ `Input: ${result.rawTokens.toLocaleString()} tokens`,
9987
+ `Can reduce to ${result.squeezedTokens.toLocaleString()} tokens (${pct}% smaller):`,
9988
+ ];
9989
+ for (const k of result.kept) lines.push(` ✓ Kept: ${k}`);
9990
+ for (const s of result.stripped) lines.push(` ✗ Stripped: ${s}`);
9991
+ return lines.join('\n');
9992
+ }
9993
+
9994
+ module.exports = { squeeze, shouldPrompt, formatSummary, estimateTokens };
9995
+
9996
+ };
9997
+
9998
+ // ── ./src/nudge ──
9999
+ __factories["./src/nudge"] = function(module, exports) {
10000
+ 'use strict';
10001
+
10002
+ /**
10003
+ * Star nudge + usage tracking (v7.0.0).
10004
+ *
10005
+ * Records run counts in `.context/usage.json` and shows a one-time GitHub-star
10006
+ * message after the tool has been genuinely useful (≥10 runs, ≥8 successes).
10007
+ * Shown exactly once per machine — even under concurrent runs (an `wx` lock
10008
+ * file makes the show race-safe). Wired into `ask` (and the `squeeze` path).
10009
+ */
10010
+
10011
+ const fs = require('fs');
10012
+ const path = require('path');
10013
+
10014
+ const RUN_THRESHOLD = 10;
10015
+ const SUCCESS_THRESHOLD = 8;
10016
+
10017
+ function usagePath(cwd) { return path.join(cwd, '.context', 'usage.json'); }
10018
+
10019
+ function defaultUsage() {
10020
+ return {
10021
+ totalRuns: 0, successfulRuns: 0, squeezeOffered: 0, squeezeAccepted: 0,
10022
+ starNudgeShown: false, firstRunDate: null, lastRunDate: null,
10023
+ };
10024
+ }
10025
+
10026
+ function readUsage(cwd) {
10027
+ try { return { ...defaultUsage(), ...JSON.parse(fs.readFileSync(usagePath(cwd), 'utf8')) }; }
10028
+ catch (_) { return defaultUsage(); }
10029
+ }
10030
+
10031
+ function writeUsageAtomic(cwd, usage) {
10032
+ const p = usagePath(cwd);
10033
+ fs.mkdirSync(path.dirname(p), { recursive: true });
10034
+ const tmp = `${p}.${process.pid}.tmp`;
10035
+ fs.writeFileSync(tmp, JSON.stringify(usage, null, 2));
10036
+ fs.renameSync(tmp, p); // atomic on POSIX
10037
+ }
10038
+
10039
+ const STAR_MESSAGE = [
10040
+ '─────────────────────────────────────────────────────────',
10041
+ ' SigMap has helped you 10 times now.',
10042
+ '',
10043
+ " If it's been useful, a GitHub star takes 5 seconds and",
10044
+ ' helps other developers find it:',
10045
+ ' → github.com/manojmallick/sigmap',
10046
+ '',
10047
+ " (Won't ask again. Press Enter to continue.)",
10048
+ '─────────────────────────────────────────────────────────',
10049
+ ].join('\n');
10050
+
10051
+ function showStarNudge(write) {
10052
+ (write || ((s) => process.stderr.write(s)))('\n' + STAR_MESSAGE + '\n');
10053
+ }
10054
+
10055
+ /**
10056
+ * Record one run and, when the thresholds are first met, show the star nudge.
10057
+ * @param {string} cwd
10058
+ * @param {boolean} runSuccess
10059
+ * @param {object} [opts]
10060
+ * @param {boolean} [opts.silent] record only — never print
10061
+ * @param {function} [opts.write] sink for the message (default stderr)
10062
+ * @param {string} [opts.today] override date (testing)
10063
+ * @param {object} [opts.bump] counter deltas to merge (e.g. { squeezeAccepted: 1 })
10064
+ * @returns {{ usage, nudged }}
10065
+ */
10066
+ function checkStarNudge(cwd, runSuccess, opts = {}) {
10067
+ const usage = readUsage(cwd);
10068
+ usage.totalRuns += 1;
10069
+ if (runSuccess) usage.successfulRuns += 1;
10070
+ if (opts.bump) for (const k of Object.keys(opts.bump)) usage[k] = (usage[k] || 0) + opts.bump[k];
10071
+
10072
+ const today = opts.today || new Date().toISOString().slice(0, 10);
10073
+ if (!usage.firstRunDate) usage.firstRunDate = today;
10074
+ usage.lastRunDate = today;
10075
+
10076
+ let nudged = false;
10077
+ if (!usage.starNudgeShown && usage.totalRuns >= RUN_THRESHOLD && usage.successfulRuns >= SUCCESS_THRESHOLD) {
10078
+ // Race-safe single-show: only the process that creates the lock prints.
10079
+ let won = false;
10080
+ try { fs.closeSync(fs.openSync(usagePath(cwd) + '.nudge.lock', 'wx')); won = true; }
10081
+ catch (_) { won = false; }
10082
+ if (won && !opts.silent) showStarNudge(opts.write);
10083
+ nudged = won;
10084
+ usage.starNudgeShown = true;
10085
+ }
10086
+
10087
+ writeUsageAtomic(cwd, usage);
10088
+ return { usage, nudged };
10089
+ }
10090
+
10091
+ module.exports = { checkStarNudge, readUsage, usagePath, showStarNudge, RUN_THRESHOLD, SUCCESS_THRESHOLD };
10092
+
10093
+ };
10094
+
9183
10095
  // ── ./src/verify/parsers ──
9184
10096
  __factories["./src/verify/parsers"] = function(module, exports) {
9185
10097
  'use strict';
@@ -10022,9 +10934,20 @@ module.exports = { verify, buildSymbolSet, loadDeps, loadScripts, isTestPath };
10022
10934
  const fs = require('fs');
10023
10935
  const path = require('path');
10024
10936
  const os = require('os');
10025
- const { execSync } = require('child_process');
10026
10937
 
10027
- const VERSION = '6.15.0';
10938
+ // Shell-free subprocess helpers. execFileSync runs the binary directly (never
10939
+ // /bin/sh), so there is no shell-injection surface and no "Shell access"
10940
+ // capability for supply-chain scanners. stderr is discarded by default.
10941
+ function __git(args, opts = {}) {
10942
+ const { execFileSync } = require('child_process');
10943
+ return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], ...opts });
10944
+ }
10945
+ function __tryGit(args, opts = {}) {
10946
+ try { return __git(args, opts).toString().trim(); }
10947
+ catch (_) { return ''; }
10948
+ }
10949
+
10950
+ const VERSION = '7.0.1';
10028
10951
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
10029
10952
 
10030
10953
  function requireSourceOrBundled(key) {
@@ -10324,29 +11247,17 @@ function applyTokenBudget(fileEntries, maxTokens) {
10324
11247
  let total = renderedTotal(fileEntries);
10325
11248
  if (total <= budgetForEntries) return fileEntries;
10326
11249
 
10327
- // v6.12 Surgical Context progressive disclosure: before dropping whole files,
10328
- // collapse signature BODIES to their line-anchor pointers (keep `symbol :start-end`).
10329
- // Only sigs that actually carry an anchor shrink; the agent can re-fetch bodies via
10330
- // the get_lines MCP tool. This degrades gracefully instead of losing files outright.
10331
- let working = fileEntries;
10332
- const collapsed = fileEntries.map((e) => {
10333
- const slim = (e.sigs || []).map((s) => {
10334
- const line = toIndexLine(s);
10335
- return line && /:\d+-\d+/.test(line) ? line : s; // replace only when it yields an anchor
10336
- });
10337
- return { ...e, sigs: slim };
10338
- });
10339
- const collapsedRendered = renderedTotal(collapsed);
10340
- if (collapsedRendered < total) {
10341
- console.warn(`[sigmap] budget: collapsed bodies to anchors, reclaimed ~${total - collapsedRendered} tokens`);
10342
- working = collapsed;
10343
- total = collapsedRendered;
10344
- // Collapsing keeps every file, so the section overhead must fit too — not just sigs.
10345
- if (total <= budgetForEntries) return working;
10346
- }
10347
-
10348
- // Sort by drop priority (drop first = index 0)
10349
- const withPriority = working.map((e) => {
11250
+ // Over budgetdegrade gracefully, best file first:
11251
+ // 1. keep FULL signatures (params + return type) while they fit;
11252
+ // 2. when a file no longer fits with full sigs, collapse just THAT file to
11253
+ // its line-anchor pointers (still discoverable via buildSigIndex / the
11254
+ // ranker it parses the context file, so an anchored file stays findable);
11255
+ // 3. drop a file only when even the anchor form won't fit.
11256
+ // The important, high-priority files therefore keep their real signatures —
11257
+ // the user-visible value — while only low-priority overflow degrades to
11258
+ // anchors instead of vanishing. (Earlier versions collapsed EVERY file to an
11259
+ // anchor the moment you went 1 token over budget, gutting all signatures.)
11260
+ const withPriority = fileEntries.map((e) => {
10350
11261
  let priority = 0;
10351
11262
  let dropReason = 'budget: low recency';
10352
11263
  if (isGeneratedFile(e.filePath)) { priority = 10; dropReason = 'budget: generated file'; }
@@ -10354,41 +11265,59 @@ function applyTokenBudget(fileEntries, maxTokens) {
10354
11265
  else if (isTestFile(e.filePath)) { priority = 8; dropReason = 'budget: test file'; }
10355
11266
  else if (isConfigFile(e.filePath)) { priority = 6; dropReason = 'budget: config file'; }
10356
11267
  else priority = 4;
10357
- // v4.0: signal quality = sigs per line-of-code (higher = more informative)
10358
11268
  const loc = e.content ? e.content.split('\n').length : 1;
10359
11269
  const signalQuality = loc > 0 ? (e.sigs ? e.sigs.length : 0) / loc : 0;
10360
11270
  return { ...e, priority, dropReason, signalQuality };
10361
11271
  });
10362
11272
 
10363
- // Within same priority: sort by mtime ascending (oldest first), then signalQuality ascending (least informative first)
10364
- withPriority.sort((a, b) => {
10365
- if (b.priority !== a.priority) return b.priority - a.priority;
10366
- if ((a.mtime || 0) !== (b.mtime || 0)) return (a.mtime || 0) - (b.mtime || 0);
10367
- return (a.signalQuality || 0) - (b.signalQuality || 0);
11273
+ // Best-first order: lowest drop-priority, then most-recent, then most-informative.
11274
+ const bestFirst = withPriority.slice().sort((a, b) => {
11275
+ if (a.priority !== b.priority) return a.priority - b.priority;
11276
+ if ((b.mtime || 0) !== (a.mtime || 0)) return (b.mtime || 0) - (a.mtime || 0);
11277
+ return (b.signalQuality || 0) - (a.signalQuality || 0);
10368
11278
  });
10369
11279
 
10370
- const kept = [];
11280
+ const entryCost = (e) => estimateTokens(e.sigs.join('\n')) + sectionOverhead(e);
11281
+ const collapseEntry = (e) => ({
11282
+ ...e,
11283
+ sigs: (e.sigs || []).map((s) => {
11284
+ const line = toIndexLine(s);
11285
+ return line && /:\d+-\d+/.test(line) ? line : s;
11286
+ }),
11287
+ });
11288
+ // Keep FULL signatures for the best files while they fit (signatures are the
11289
+ // value the user reads). When a file's full form no longer fits, fall back to
11290
+ // its anchor form (still discoverable via the ranker, which parses this file);
11291
+ // drop only when even the anchor won't fit. So the important files keep real
11292
+ // signatures and only the lowest-priority overflow degrades — instead of every
11293
+ // signature being gutted to an anchor the moment you go over budget.
11294
+ const finalByPath = new Map();
10371
11295
  const verboseDropped = [];
10372
- // Iterate forward: highest drop-priority files (generated=10, mock=9, test=8) are at index 0.
10373
- // Drop those first until the RENDERED total (sigs + section overhead) fits maxTokens,
10374
- // then keep everything else.
10375
- let rendered = renderedTotal(withPriority);
10376
- for (const entry of withPriority) {
10377
- if (rendered > budgetForEntries) {
10378
- rendered -= estimateTokens(entry.sigs.join('\n')) + sectionOverhead(entry);
10379
- verboseDropped.push({ filePath: entry.filePath, reason: entry.dropReason });
10380
- } else {
10381
- kept.push(entry);
11296
+ let collapsedCount = 0;
11297
+ let used = 0;
11298
+ for (const entry of bestFirst) { // best first
11299
+ if (used + entryCost(entry) <= budgetForEntries) {
11300
+ finalByPath.set(entry.filePath, entry); used += entryCost(entry); continue;
11301
+ }
11302
+ const slim = collapseEntry(entry);
11303
+ if (used + entryCost(slim) <= budgetForEntries) {
11304
+ finalByPath.set(entry.filePath, slim); used += entryCost(slim); collapsedCount++; continue;
10382
11305
  }
11306
+ verboseDropped.push({ filePath: entry.filePath, reason: entry.dropReason });
10383
11307
  }
10384
- if (verboseDropped.length > 0) {
10385
- console.warn(`[sigmap] budget: dropped ${verboseDropped.length} files to stay under ${maxTokens} tokens`);
10386
- // Feature 7: --verbose — print per-file drop reason
11308
+ // Restore the original file order for stable output.
11309
+ const kept = withPriority.filter((e) => finalByPath.has(e.filePath)).map((e) => finalByPath.get(e.filePath));
11310
+
11311
+ if (verboseDropped.length > 0 || collapsedCount > 0) {
11312
+ const parts = [];
11313
+ if (verboseDropped.length) parts.push(`dropped ${verboseDropped.length} file(s)`);
11314
+ if (collapsedCount) parts.push(`collapsed ${collapsedCount} low-priority file(s) to anchors`);
11315
+ console.warn(`[sigmap] budget: ${parts.join(', ')} to stay under ${maxTokens} tokens`);
10387
11316
  if (process.argv.includes('--verbose')) {
10388
11317
  for (const { filePath, reason } of verboseDropped) {
10389
11318
  console.warn(`[sigmap] dropped: ${path.relative(process.cwd(), filePath)} — ${reason}`);
10390
11319
  }
10391
- console.warn(`[sigmap] included: ${kept.length} files, dropped: ${verboseDropped.length}`);
11320
+ console.warn(`[sigmap] included: ${kept.length} files (${collapsedCount} collapsed), dropped: ${verboseDropped.length}`);
10392
11321
  }
10393
11322
  }
10394
11323
  return kept;
@@ -10400,9 +11329,7 @@ function applyTokenBudget(fileEntries, maxTokens) {
10400
11329
  function getRecentlyCommittedFiles(cwd, count) {
10401
11330
  const n = count || 10;
10402
11331
  try {
10403
- const out = execSync(`git log --name-only --format="" -n ${n}`, {
10404
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
10405
- });
11332
+ const out = __git(['log', '--name-only', '--format=', '-n', String(n)], { cwd });
10406
11333
  return new Set(out.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(cwd, f)));
10407
11334
  } catch (_) {
10408
11335
  return new Set();
@@ -10414,8 +11341,8 @@ function getRecentlyCommittedFiles(cwd, count) {
10414
11341
  // ---------------------------------------------------------------------------
10415
11342
  function getDiffFiles(cwd, stagedOnly) {
10416
11343
  try {
10417
- const cmd = stagedOnly ? 'git diff --cached --name-only' : 'git diff HEAD --name-only';
10418
- const out = execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
11344
+ const gitArgs = stagedOnly ? ['diff', '--cached', '--name-only'] : ['diff', 'HEAD', '--name-only'];
11345
+ const out = __git(gitArgs, { cwd });
10419
11346
  return new Set(
10420
11347
  out.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(cwd, f))
10421
11348
  );
@@ -10513,17 +11440,13 @@ function buildChangesSection(cwd, config, fileEntries) {
10513
11440
  try {
10514
11441
  let range = `HEAD~${n}..HEAD`;
10515
11442
  try {
10516
- execSync(`git rev-parse --verify HEAD~${n} 2>/dev/null`, {
10517
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
10518
- });
11443
+ __git(['rev-parse', '--verify', `HEAD~${n}`], { cwd });
10519
11444
  } catch (_) {
10520
11445
  // HEAD~n doesn't exist (shallow repo) — clamp to deepest valid ancestor
10521
11446
  let best = 1;
10522
11447
  for (let k = n - 1; k >= 2; k--) {
10523
11448
  try {
10524
- execSync(`git rev-parse --verify HEAD~${k} 2>/dev/null`, {
10525
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
10526
- });
11449
+ __git(['rev-parse', '--verify', `HEAD~${k}`], { cwd });
10527
11450
  best = k;
10528
11451
  break;
10529
11452
  } catch (_) {}
@@ -10531,15 +11454,11 @@ function buildChangesSection(cwd, config, fileEntries) {
10531
11454
  range = `HEAD~${best}..HEAD`;
10532
11455
  }
10533
11456
 
10534
- const namesOnly = execSync(`git diff ${range} --name-only 2>/dev/null`, {
10535
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
10536
- });
11457
+ const namesOnly = __git(['diff', range, '--name-only'], { cwd });
10537
11458
  const changed = new Set(namesOnly.split('\n').map((l) => l.trim()).filter(Boolean).map((f) => path.resolve(cwd, f)));
10538
11459
  if (changed.size === 0) return [];
10539
11460
 
10540
- const timeAgo = execSync('git log -1 --format="%cr" 2>/dev/null', {
10541
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
10542
- }).trim();
11461
+ const timeAgo = __git(['log', '-1', '--format=%cr'], { cwd }).trim();
10543
11462
 
10544
11463
  const lines = [`## changes (last ${n} commits — ${timeAgo})`, '```'];
10545
11464
  let hasDelta = false;
@@ -10548,9 +11467,7 @@ function buildChangesSection(cwd, config, fileEntries) {
10548
11467
  const rel = path.relative(cwd, entry.filePath);
10549
11468
  let diff = '';
10550
11469
  try {
10551
- diff = execSync(`git diff ${range} -- "${rel}" 2>/dev/null`, {
10552
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
10553
- });
11470
+ diff = __git(['diff', range, '--', rel], { cwd });
10554
11471
  } catch (_) {
10555
11472
  continue;
10556
11473
  }
@@ -10610,12 +11527,17 @@ function resolveImpactRadius(fileEntries, cwd, config) {
10610
11527
  }
10611
11528
 
10612
11529
  function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
11530
+ // One canonical usage block for every context file (CLAUDE.md, AGENTS.md,
11531
+ // copilot-instructions.md, …). Lives here — the single source all writers
11532
+ // consume — so adapters don't each invent their own (now-removed) variant.
11533
+ const { usageBlock } = requireSourceOrBundled('./src/format/usage-guidance');
10613
11534
  const lines = [
10614
11535
  '<!-- Generated by SigMap gen-context.js v' + VERSION + ' -->',
10615
11536
  '<!-- DO NOT EDIT below the marker line — run gen-context.js to regenerate -->',
10616
11537
  '',
10617
11538
  '# Code signatures',
10618
11539
  '',
11540
+ usageBlock(),
10619
11541
  ];
10620
11542
 
10621
11543
  // Compact dependency map section (shows import relationships, ~50-100 tokens)
@@ -11525,6 +12447,23 @@ function runGenerate(cwd, config, reportMode, reportJson = false) {
11525
12447
  }
11526
12448
  }
11527
12449
 
12450
+ // v7.0.1: count a plain context-generation run for the one-time star nudge.
12451
+ // Most users only ever run `sigmap` to build the context file — count that too,
12452
+ // not just ask/squeeze. Guard so a monorepo (many runGenerate calls per process)
12453
+ // counts as a single run. Interactive only — silent under --json/--report or non-TTY.
12454
+ if (!global.__sigmapGenCounted) {
12455
+ global.__sigmapGenCounted = true;
12456
+ try {
12457
+ const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
12458
+ const showNudge = !!process.stderr.isTTY
12459
+ && !reportJson
12460
+ && !process.argv.includes('--json')
12461
+ && !process.argv.includes('--report')
12462
+ && !process.argv.includes('--quiet');
12463
+ checkStarNudge(global.__sigmapRootCwd || cwd, true, { silent: !showNudge });
12464
+ } catch (_) {}
12465
+ }
12466
+
11528
12467
  return result;
11529
12468
  }
11530
12469
 
@@ -11706,11 +12645,7 @@ function suggestTool(description) {
11706
12645
 
11707
12646
  function resolveProjectRoot(startDir) {
11708
12647
  try {
11709
- const gitRoot = execSync('git rev-parse --show-toplevel', {
11710
- cwd: startDir,
11711
- encoding: 'utf8',
11712
- stdio: ['ignore', 'pipe', 'ignore'],
11713
- }).trim();
12648
+ const gitRoot = __git(['rev-parse', '--show-toplevel'], { cwd: startDir }).trim();
11714
12649
  if (gitRoot) return gitRoot;
11715
12650
  } catch (_) {}
11716
12651
  return startDir;
@@ -11786,6 +12721,10 @@ Usage:
11786
12721
  ${cmd} verify-ai-output <answer.md> Flag fake files/tests/imports/symbols/npm-scripts in an AI answer
11787
12722
  ${cmd} verify-ai-output <answer.md> --json Hallucination report as JSON (exits 1 if issues)
11788
12723
  ${cmd} verify-ai-output <answer.md> --report Write a standalone HTML report (red/amber/green)
12724
+ ${cmd} squeeze <file|-> Minimize a pasted stacktrace/CI-log/JSON blob (--json for stats)
12725
+ ${cmd} ask "<query>" --squeeze Auto-accept input minimization (no prompt; for scripts/CI)
12726
+ ${cmd} ask "<query>" --no-squeeze Disable input minimization entirely
12727
+ ${cmd} ask "<query>" --squeeze-threshold N Min reduction %% to prompt (default 30)
11789
12728
  ${cmd} note "<text>" Append a note to the cross-session decision log
11790
12729
  ${cmd} note List recent notes (also: note --list <N>)
11791
12730
  ${cmd} status Show repo state — branch, dirty files, index freshness, notes
@@ -11977,8 +12916,7 @@ function buildIndexContext(ranked, cwd) {
11977
12916
 
11978
12917
  function computeCurrentRisk(cwd) {
11979
12918
  try {
11980
- const { execSync } = require('child_process');
11981
- const out = execSync('git diff --name-only HEAD', { cwd, timeout: 3000, encoding: 'utf8' });
12919
+ const out = __git(['diff', '--name-only', 'HEAD'], { cwd, timeout: 3000 });
11982
12920
  const count = out.trim().split('\n').filter(Boolean).length;
11983
12921
  if (count === 0) return 'NONE';
11984
12922
  if (count <= 3) return 'LOW';
@@ -12062,6 +13000,9 @@ function main() {
12062
13000
  const cwd = cwdFlag
12063
13001
  ? path.resolve(invokedFrom, cwdFlag)
12064
13002
  : resolveProjectRoot(invokedFrom);
13003
+ // v7.0.1: remember the invocation root so the generation star-nudge always tracks
13004
+ // usage at the repo root, even when runGenerate is called per-package (monorepo).
13005
+ global.__sigmapRootCwd = cwd;
12065
13006
  const scriptPath = process.argv[1] || path.join(invokedFrom, 'gen-context.js');
12066
13007
 
12067
13008
  if (cwdFlag) {
@@ -12174,8 +13115,8 @@ function main() {
12174
13115
  const { loadSession, saveSession, mergeSessionContext } = requireSourceOrBundled('./src/session/memory');
12175
13116
  const { detectWorkspaces, inferPackage, scopeToPackage } = requireSourceOrBundled('./src/workspace/detector');
12176
13117
 
12177
- const intent = detectIntent(query);
12178
- const intentWeights = getIntentWeights(intent);
13118
+ let intent = detectIntent(query);
13119
+ let intentWeights = getIntentWeights(intent);
12179
13120
 
12180
13121
  const sigIndex = buildSigIndex(cwd);
12181
13122
  if (sigIndex.size === 0) {
@@ -12183,6 +13124,44 @@ function main() {
12183
13124
  process.exit(1);
12184
13125
  }
12185
13126
 
13127
+ // v7.0.0: Squeeze — classify and minimize pasted stacktrace/CI-log/JSON
13128
+ // input before ranking. Always runs silently; only prompts (interactive
13129
+ // TTY) when the reduction clears the threshold. Never blocks pipes/CI.
13130
+ let squeezeOffered = false;
13131
+ let squeezeAccepted = false;
13132
+ if (!args.includes('--no-squeeze')) {
13133
+ try {
13134
+ const { squeeze: runSqueeze, shouldPrompt, formatSummary } = requireSourceOrBundled('./src/squeeze/index');
13135
+ const sq = runSqueeze(query, { srcDirs: config.srcDirs, symbolIndex: sigIndex });
13136
+ if (sq.applies && sq.reduction > 0) {
13137
+ squeezeOffered = true;
13138
+ const thrIdx = args.indexOf('--squeeze-threshold');
13139
+ const threshold = thrIdx !== -1 ? parseFloat(args[thrIdx + 1]) : 30;
13140
+ const auto = args.includes('--squeeze');
13141
+ const interactive = !!(process.stdin.isTTY && process.stderr.isTTY) && !args.includes('--json');
13142
+ let accept = false;
13143
+ if (auto) {
13144
+ accept = true;
13145
+ } else if (interactive && shouldPrompt(sq.reduction, threshold)) {
13146
+ process.stderr.write('\n' + formatSummary(sq) + '\n\n');
13147
+ process.stderr.write('Proceed with minimized input? [Y/n] ');
13148
+ const buf = Buffer.alloc(8);
13149
+ try {
13150
+ const n = fs.readSync(0, buf, 0, 8, null);
13151
+ const ans = buf.toString('utf8', 0, n).trim().toLowerCase();
13152
+ accept = ans === '' || ans === 'y' || ans === 'yes';
13153
+ } catch (_) { accept = false; }
13154
+ }
13155
+ if (accept) {
13156
+ query = sq.squeezed;
13157
+ intent = detectIntent(query);
13158
+ intentWeights = getIntentWeights(intent);
13159
+ squeezeAccepted = true;
13160
+ }
13161
+ }
13162
+ } catch (_) { /* squeeze is best-effort — never break ask */ }
13163
+ }
13164
+
12186
13165
  let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });
12187
13166
 
12188
13167
  // v6.10: Workspace scoping — infer package from query and apply boost
@@ -12282,6 +13261,15 @@ function main() {
12282
13261
  bar,
12283
13262
  ].join('\n'));
12284
13263
  }
13264
+ // v7.0.0: record the run and show the one-time star nudge (interactive only).
13265
+ try {
13266
+ const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
13267
+ const showNudge = !!process.stderr.isTTY && !args.includes('--json');
13268
+ checkStarNudge(cwd, true, {
13269
+ silent: !showNudge,
13270
+ bump: { squeezeOffered: squeezeOffered ? 1 : 0, squeezeAccepted: squeezeAccepted ? 1 : 0 },
13271
+ });
13272
+ } catch (_) {}
12285
13273
  process.exit(0);
12286
13274
  }
12287
13275
 
@@ -12290,9 +13278,8 @@ function main() {
12290
13278
  const short = args.includes('--short');
12291
13279
  let msg = '', diff = '';
12292
13280
  try {
12293
- const { execSync } = require('child_process');
12294
- msg = execSync('git log -1 --format=%s', { cwd, timeout: 3000, encoding: 'utf8' }).trim();
12295
- diff = execSync('git diff --cached --name-only', { cwd, timeout: 3000, encoding: 'utf8' });
13281
+ msg = __git(['log', '-1', '--format=%s'], { cwd, timeout: 3000 }).trim();
13282
+ diff = __git(['diff', '--cached', '--name-only'], { cwd, timeout: 3000 });
12296
13283
  } catch (_) {}
12297
13284
 
12298
13285
  let profile = 'default';
@@ -12313,13 +13300,15 @@ function main() {
12313
13300
 
12314
13301
  // v4.2: `sigmap compare` — human-readable benchmark CLI
12315
13302
  if (args[0] === 'compare') {
12316
- const { execSync } = require('child_process');
13303
+ const { execFileSync } = require('child_process');
12317
13304
  console.log('[sigmap] Running comparison benchmark (this may take ~30s)...\n');
12318
13305
 
12319
13306
  let raw = '';
12320
13307
  try {
12321
- raw = execSync(
12322
- `node ${JSON.stringify(path.join(__dirname, 'scripts', 'run-retrieval-benchmark.mjs'))} --compare`,
13308
+ // Shell-free: run the node binary directly with the script path as an argv.
13309
+ raw = execFileSync(
13310
+ process.execPath,
13311
+ [path.join(__dirname, 'scripts', 'run-retrieval-benchmark.mjs'), '--compare'],
12323
13312
  { cwd, timeout: 90_000, encoding: 'utf8' }
12324
13313
  );
12325
13314
  } catch (e) { raw = (e && e.stdout) ? e.stdout : ''; }
@@ -12376,9 +13365,13 @@ function main() {
12376
13365
  console.log(shareText);
12377
13366
 
12378
13367
  try {
12379
- const { execSync } = require('child_process');
12380
- const clipCmd = process.platform === 'darwin' ? 'pbcopy' : 'xclip -selection clipboard';
12381
- execSync(`printf '%s' ${JSON.stringify(shareText)} | ${clipCmd}`, { timeout: 2000 });
13368
+ // Shell-free: spawn the clipboard binary directly and write the text to its
13369
+ // stdin no shell, no pipe, no string-built command.
13370
+ const { execFileSync } = require('child_process');
13371
+ const [clipBin, clipArgs] = process.platform === 'darwin'
13372
+ ? ['pbcopy', []]
13373
+ : ['xclip', ['-selection', 'clipboard']];
13374
+ execFileSync(clipBin, clipArgs, { input: shareText, timeout: 2000 });
12382
13375
  console.log('\n[sigmap] Copied to clipboard.');
12383
13376
  } catch (_) {}
12384
13377
 
@@ -12959,16 +13952,16 @@ function main() {
12959
13952
  // `sigmap status` — environment / repo state at a glance.
12960
13953
  if (args[0] === 'status') {
12961
13954
  const jsonOut = args.includes('--json');
12962
- const gitOpts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
13955
+ const gitOpts = { cwd };
12963
13956
  const st = { branch: null, dirty: 0, lastIndex: null, indexVersion: null, indexFiles: null, changedSinceIndex: null, notes: 0, lastNote: null };
12964
13957
 
12965
- try { st.branch = execSync('git rev-parse --abbrev-ref HEAD', gitOpts).trim() || null; } catch (_) {}
13958
+ st.branch = __tryGit(['rev-parse', '--abbrev-ref', 'HEAD'], gitOpts) || null;
12966
13959
  // Fallback for an unborn branch (fresh repo, no commits yet).
12967
13960
  if (!st.branch || st.branch === 'HEAD') {
12968
- try { st.branch = execSync('git symbolic-ref --short HEAD', gitOpts).trim() || st.branch; } catch (_) {}
13961
+ st.branch = __tryGit(['symbolic-ref', '--short', 'HEAD'], gitOpts) || st.branch;
12969
13962
  }
12970
13963
  try {
12971
- const porcelain = execSync('git status --porcelain', gitOpts).trim();
13964
+ const porcelain = __git(['status', '--porcelain'], gitOpts).trim();
12972
13965
  st.dirty = porcelain ? porcelain.split('\n').filter(Boolean).length : 0;
12973
13966
  } catch (_) {}
12974
13967
 
@@ -12987,7 +13980,7 @@ function main() {
12987
13980
  if (st.lastIndex) {
12988
13981
  try {
12989
13982
  const since = Date.parse(st.lastIndex);
12990
- const tracked = execSync('git ls-files', gitOpts).split('\n').filter(Boolean);
13983
+ const tracked = __git(['ls-files'], gitOpts).split('\n').filter(Boolean);
12991
13984
  let changed = 0;
12992
13985
  for (const f of tracked.slice(0, 5000)) {
12993
13986
  try { if (fs.statSync(path.join(cwd, f)).mtimeMs > since) changed++; } catch (_) {}
@@ -13037,6 +14030,49 @@ function main() {
13037
14030
  process.exit(0);
13038
14031
  }
13039
14032
 
14033
+ // v7.0.0: `sigmap squeeze <file|->` — minimize a pasted stacktrace / CI-log / JSON blob
14034
+ if (args[0] === 'squeeze') {
14035
+ const jsonOut = args.includes('--json');
14036
+ const target = args[1] && !args[1].startsWith('--') ? args[1] : '-';
14037
+ let input = '';
14038
+ try {
14039
+ input = fs.readFileSync(target === '-' ? 0 : path.resolve(cwd, target), 'utf8');
14040
+ } catch (e) {
14041
+ console.error(`[sigmap] cannot read input: ${e.message}`);
14042
+ process.exit(1);
14043
+ }
14044
+
14045
+ const { squeeze: runSqueeze, formatSummary } = requireSourceOrBundled('./src/squeeze/index');
14046
+ let symbolIndex = null;
14047
+ try {
14048
+ const { buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
14049
+ symbolIndex = buildSigIndex(cwd);
14050
+ } catch (_) {}
14051
+ const sq = runSqueeze(input, { srcDirs: config.srcDirs, symbolIndex });
14052
+
14053
+ try {
14054
+ const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
14055
+ checkStarNudge(cwd, true, { silent: !process.stderr.isTTY || jsonOut, bump: { squeezeOffered: sq.applies ? 1 : 0 } });
14056
+ } catch (_) {}
14057
+
14058
+ if (jsonOut) {
14059
+ process.stdout.write(JSON.stringify({
14060
+ category: sq.category, confidence: sq.confidence,
14061
+ rawTokens: sq.rawTokens, squeezedTokens: sq.squeezedTokens,
14062
+ reduction: sq.reduction, enriched: sq.enriched, squeezed: sq.squeezed,
14063
+ }) + '\n');
14064
+ process.exit(0);
14065
+ }
14066
+ if (!sq.category) {
14067
+ process.stderr.write('[sigmap] no squeezable structure detected — input unchanged\n');
14068
+ process.stdout.write(input);
14069
+ process.exit(0);
14070
+ }
14071
+ process.stderr.write(formatSummary(sq) + '\n\n');
14072
+ process.stdout.write(sq.squeezed + '\n');
14073
+ process.exit(0);
14074
+ }
14075
+
13040
14076
  // `sigmap verify-ai-output <answer.md>` — Hallucination Guard (deterministic core)
13041
14077
  if (args[0] === 'verify-ai-output') {
13042
14078
  const target = args[1];