sigmap 6.14.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/gen-context.js CHANGED
@@ -3164,55 +3164,78 @@ __factories["./src/extractors/coverage"] = function(module, exports) {
3164
3164
 
3165
3165
  // ── ./src/extractors/prdiff ──
3166
3166
  __factories["./src/extractors/prdiff"] = function(module, exports) {
3167
+ 'use strict';
3167
3168
 
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 = [];
3169
+ /**
3170
+ * Compare signature arrays and produce compact diff markers.
3171
+ * @param {string[]} baseSigs
3172
+ * @param {string[]} currentSigs
3173
+ * @returns {{added:string[], removed:string[], modified:string[]}}
3174
+ */
3175
+ function diffSignatures(baseSigs, currentSigs) {
3176
+ const base = new Set(baseSigs || []);
3177
+ const curr = new Set(currentSigs || []);
3178
+
3179
+ const added = [...curr].filter((s) => !base.has(s));
3180
+ const removed = [...base].filter((s) => !curr.has(s));
3181
+
3182
+ const byName = (arr) => {
3183
+ const m = new Map();
3184
+ for (const s of arr) {
3185
+ const n = extractName(s);
3186
+ if (!n) continue;
3187
+ if (!m.has(n)) m.set(n, []);
3188
+ m.get(n).push(s);
3189
+ }
3190
+ return m;
3191
+ };
3197
3192
 
3198
- for (const [name] of aBy) {
3199
- if (rBy.has(name)) modified.push(name);
3200
- }
3193
+ const aBy = byName(added);
3194
+ const rBy = byName(removed);
3195
+ const modified = [];
3201
3196
 
3202
- return { added, removed, modified };
3197
+ for (const [name] of aBy) {
3198
+ if (rBy.has(name)) modified.push(name);
3203
3199
  }
3204
3200
 
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
- }
3201
+ return { added, removed, modified };
3202
+ }
3203
+
3204
+ /**
3205
+ * Extract the declared symbol name from a signature line.
3206
+ *
3207
+ * Robust to the real forms SigMap emits — `export class X`, `const x = () =>`,
3208
+ * `async function x`, members, a trailing `:start-end` anchor, and `→ return`
3209
+ * suffixes. Anchored so it never returns a mid-string fragment (the old regex
3210
+ * could turn a signature into a 2-char name like `is`), and returns '' for
3211
+ * non-symbol lines (`module.exports = {…}`, markdown headers) instead of guessing.
3212
+ */
3213
+ function extractName(sig) {
3214
+ if (!sig) return '';
3215
+ // Drop a trailing `:start-end` (or `:line`) line anchor.
3216
+ let t = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim();
3217
+ // Re-export / barrel lines carry no single declared name.
3218
+ if (/^(?:module\.)?exports\b/.test(t) || /^export\s*\{/.test(t) || /^export\s+\*/.test(t)) return '';
3219
+ // Strip leading modifiers so the keyword/name is at the start.
3220
+ t = t.replace(/^export\s+/, '')
3221
+ .replace(/^default\s+/, '')
3222
+ .replace(/^(?:public|private|protected|static|abstract|final|override|readonly)\s+/g, '')
3223
+ .replace(/^async\s+/, '');
3224
+ let m;
3225
+ // Declared forms: function/def/func/fn/class/interface/trait/struct/enum/record/type <name>
3226
+ if ((m = t.match(/^(?:def|function|func|fn|class|interface|trait|struct|enum|record|type)\s+([A-Za-z_$][\w$]*)/))) return m[1];
3227
+ // const/let/var/val <name> = … (arrow functions, assigned values)
3228
+ if ((m = t.match(/^(?:const|let|var|val)\s+([A-Za-z_$][\w$]*)/))) return m[1];
3229
+ // Call / method form: <name>(…)
3230
+ if ((m = t.match(/^([A-Za-z_$][\w$]*)\s*\(/))) return m[1];
3231
+ // Lone identifier (e.g. a collapsed `symbol` after the anchor was stripped).
3232
+ if ((m = t.match(/^([A-Za-z_$][\w$]*)$/))) return m[1];
3233
+ return '';
3234
+ }
3211
3235
 
3212
- module.exports = { diffSignatures, extractName };
3236
+ module.exports = { diffSignatures, extractName };
3213
3237
 
3214
3238
  };
3215
-
3216
3239
  // ── ./src/extractors/r ──
3217
3240
  __factories["./src/extractors/r"] = function(module, exports) {
3218
3241
 
@@ -5506,487 +5529,570 @@ __factories["./src/graph/impact"] = function(module, exports) {
5506
5529
 
5507
5530
  // ── ./src/mcp/handlers ──
5508
5531
  __factories["./src/mcp/handlers"] = function(module, exports) {
5509
-
5510
- const fs = require('fs');
5511
- const path = require('path');
5512
- const { execSync } = require('child_process');
5513
-
5514
- const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
5515
-
5516
- // Section header keywords in PROJECT_MAP.md
5517
- const MAP_SECTIONS = {
5518
- imports: '### Import graph',
5519
- classes: '### Class hierarchy',
5520
- routes: '### Route table',
5521
- };
5522
-
5523
- /**
5524
- * read_context({ module? }) → string
5525
- *
5526
- * Returns the full context file, or just the sections whose file paths
5527
- * contain the given module substring.
5528
- */
5529
- function readContext(args, cwd) {
5530
- const contextPath = path.join(cwd, CONTEXT_FILE);
5531
- if (!fs.existsSync(contextPath)) {
5532
- return 'No context file found. Run: node gen-context.js';
5533
- }
5534
-
5535
- const content = fs.readFileSync(contextPath, 'utf8');
5536
-
5537
- if (!args || !args.module) return content;
5538
-
5539
- const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
5540
- const lines = content.split('\n');
5541
- const result = [];
5542
- let capturing = false;
5543
-
5544
- for (const line of lines) {
5545
- if (line.startsWith('### ')) {
5546
- const filePath = line.slice(4).trim().replace(/\\/g, '/');
5547
- // Match if file path starts with mod or contains /mod/ or /mod
5548
- capturing =
5549
- filePath === mod ||
5550
- filePath.startsWith(mod + '/') ||
5551
- filePath.includes('/' + mod + '/') ||
5552
- filePath.includes('/' + mod);
5553
- if (capturing) result.push(line);
5554
- continue;
5555
- }
5556
- if (capturing) result.push(line);
5557
- }
5558
-
5559
- if (result.length === 0) return `No signatures found for module: ${mod}`;
5560
- return result.join('\n');
5561
- }
5562
-
5563
- /**
5564
- * search_signatures({ query }) → string
5565
- *
5566
- * Case-insensitive search through all signature lines.
5567
- * Returns matching lines grouped by file path.
5568
- */
5569
- function searchSignatures(args, cwd) {
5570
- if (!args || !args.query) return 'Missing required argument: query';
5571
- const query = args.query.toLowerCase();
5532
+ 'use strict';
5572
5533
 
5573
- try {
5574
- const { buildSigIndex } = __require('./src/retrieval/ranker');
5575
- const index = buildSigIndex(cwd);
5576
- if (index.size === 0) {
5577
- return 'No context file found. Run: node gen-context.js';
5578
- }
5534
+ const fs = require('fs');
5535
+ const path = require('path');
5536
+ const { execSync } = require('child_process');
5579
5537
 
5580
- const result = [];
5581
- for (const [file, sigs] of index.entries()) {
5582
- const hits = sigs.filter((s) => s.toLowerCase().includes(query));
5583
- if (hits.length === 0) continue;
5584
- if (result.length > 0) result.push('');
5585
- result.push(`### ${file}`);
5586
- result.push(...hits);
5587
- }
5538
+ const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
5539
+ const CONTEXT_COLD_FILE = path.join('.github', 'context-cold.md');
5588
5540
 
5589
- if (result.length === 0) return `No signatures found matching: ${args.query}`;
5590
- return result.join('\n');
5591
- } catch (err) {
5592
- return `_search_signatures failed: ${err.message}_`;
5593
- }
5541
+ function _readContextFiles(cwd) {
5542
+ const paths = [path.join(cwd, CONTEXT_FILE), path.join(cwd, CONTEXT_COLD_FILE)];
5543
+ const chunks = [];
5544
+ for (const p of paths) {
5545
+ if (fs.existsSync(p)) chunks.push(fs.readFileSync(p, 'utf8'));
5594
5546
  }
5595
-
5596
- /**
5597
- * get_map({ type }) → string
5598
- *
5599
- * Returns a section from PROJECT_MAP.md.
5600
- * type: 'imports' | 'classes' | 'routes'
5601
- */
5602
- function getMap(args, cwd) {
5603
- if (!args || !args.type) return 'Missing required argument: type';
5604
-
5605
- const header = MAP_SECTIONS[args.type];
5606
- if (!header) {
5607
- return `Unknown map type: "${args.type}". Use: imports, classes, routes`;
5608
- }
5609
-
5610
- const mapPath = path.join(cwd, 'PROJECT_MAP.md');
5611
- if (!fs.existsSync(mapPath)) {
5612
- return 'PROJECT_MAP.md not found. Run: node gen-project-map.js';
5613
- }
5614
-
5615
- const content = fs.readFileSync(mapPath, 'utf8');
5616
- const idx = content.indexOf(header);
5617
- if (idx === -1) {
5618
- return `Section "${header}" not found in PROJECT_MAP.md`;
5547
+ return chunks.join('\n');
5548
+ }
5549
+
5550
+ // Section header keywords in PROJECT_MAP.md
5551
+ const MAP_SECTIONS = {
5552
+ imports: '### Import graph',
5553
+ classes: '### Class hierarchy',
5554
+ routes: '### Route table',
5555
+ };
5556
+
5557
+ /**
5558
+ * read_context({ module? }) → string
5559
+ *
5560
+ * Returns the full context file, or just the sections whose file paths
5561
+ * contain the given module substring.
5562
+ */
5563
+ function readContext(args, cwd) {
5564
+ const content = _readContextFiles(cwd);
5565
+ if (!content) {
5566
+ return 'No context file found. Run: node gen-context.js';
5567
+ }
5568
+
5569
+ if (!args || !args.module) return content;
5570
+
5571
+ const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
5572
+ const lines = content.split('\n');
5573
+ const result = [];
5574
+ let capturing = false;
5575
+
5576
+ for (const line of lines) {
5577
+ if (line.startsWith('### ')) {
5578
+ const filePath = line.slice(4).trim().replace(/\\/g, '/');
5579
+ // Match if file path starts with mod or contains /mod/ or /mod
5580
+ capturing =
5581
+ filePath === mod ||
5582
+ filePath.startsWith(mod + '/') ||
5583
+ filePath.includes('/' + mod + '/') ||
5584
+ filePath.includes('/' + mod);
5585
+ if (capturing) result.push(line);
5586
+ continue;
5619
5587
  }
5620
-
5621
- // Extract from this header to the next ### header
5622
- const after = content.slice(idx);
5623
- const nextMatch = after.slice(header.length).search(/\n###\s/);
5624
- return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
5588
+ if (capturing) result.push(line);
5625
5589
  }
5626
-
5627
- /**
5628
- * create_checkpoint({ note? }) → string
5629
- *
5630
- * Returns a markdown checkpoint summarising current project state:
5631
- * - Timestamp and optional user note
5632
- * - Active git branch + last 5 commit messages
5633
- * - Token count of current context file
5634
- * - List of modules present in the context
5635
- * - Route count (if PROJECT_MAP.md exists)
5636
- */
5637
- function createCheckpoint(args, cwd) {
5638
- const note = (args && args.note) ? args.note.trim() : '';
5639
- const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
5640
- const lines = [
5641
- '# SigMap Checkpoint',
5642
- `**Created:** ${now}`,
5643
- ];
5644
-
5645
- if (note) lines.push(`**Note:** ${note}`);
5646
- lines.push('');
5647
-
5648
- // ── Git info ────────────────────────────────────────────────────────────
5649
- lines.push('## Git state');
5650
- try {
5651
- const branch = execSync('git rev-parse --abbrev-ref HEAD', {
5652
- cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
5653
- }).trim();
5654
- lines.push(`**Branch:** ${branch}`);
5655
- } catch (_) {
5656
- lines.push('**Branch:** (not a git repo)');
5657
- }
5658
-
5659
- try {
5660
- const log = execSync(
5661
- 'git log --oneline -5 --no-decorate 2>/dev/null',
5662
- { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
5663
- ).trim();
5664
- if (log) {
5665
- lines.push('');
5666
- lines.push('**Recent commits:**');
5667
- for (const l of log.split('\n')) lines.push(`- ${l}`);
5668
- }
5669
- } catch (_) {} // ignore — not every project uses git
5670
- lines.push('');
5671
-
5672
- // ── Context stats ────────────────────────────────────────────────────────
5673
- lines.push('## Context snapshot');
5674
- const contextPath = path.join(cwd, CONTEXT_FILE);
5675
- if (fs.existsSync(contextPath)) {
5676
- const content = fs.readFileSync(contextPath, 'utf8');
5677
- const tokens = Math.ceil(content.length / 4);
5678
-
5679
- // Count modules (### headers are file paths)
5680
- const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
5681
- lines.push(`**Token count:** ~${tokens}`);
5682
- lines.push(`**Modules in context:** ${modules.length}`);
5683
-
5684
- if (modules.length > 0) {
5685
- lines.push('');
5686
- lines.push('**Modules:**');
5687
- for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
5688
- if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
5689
- }
5690
- } else {
5691
- lines.push('_No context file found. Run: node gen-context.js_');
5590
+
5591
+ if (result.length === 0) return `No signatures found for module: ${mod}`;
5592
+ return result.join('\n');
5593
+ }
5594
+
5595
+ /**
5596
+ * search_signatures({ query }) string
5597
+ *
5598
+ * Case-insensitive search through all signature lines.
5599
+ * Returns matching lines grouped by file path.
5600
+ */
5601
+ function searchSignatures(args, cwd) {
5602
+ if (!args || !args.query) return 'Missing required argument: query';
5603
+
5604
+ const query = args.query.toLowerCase();
5605
+ try {
5606
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
5607
+ const index = buildSigIndex(cwd);
5608
+ if (index.size === 0) {
5609
+ return 'No context file found. Run: node gen-context.js';
5692
5610
  }
5693
- lines.push('');
5694
-
5695
- // ── Route summary ────────────────────────────────────────────────────────
5696
- const mapPath = path.join(cwd, 'PROJECT_MAP.md');
5697
- if (fs.existsSync(mapPath)) {
5698
- const mapContent = fs.readFileSync(mapPath, 'utf8');
5699
- const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
5700
- if (routeLines.length > 0) {
5701
- lines.push('## Routes');
5702
- lines.push(`**Total routes detected:** ${routeLines.length}`);
5703
- lines.push('');
5704
- for (const r of routeLines.slice(0, 10)) lines.push(r);
5705
- if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
5706
- lines.push('');
5707
- }
5611
+
5612
+ const result = [];
5613
+ for (const [file, sigs] of index.entries()) {
5614
+ const hits = sigs.filter((s) => s.toLowerCase().includes(query));
5615
+ if (hits.length === 0) continue;
5616
+ if (result.length > 0) result.push('');
5617
+ result.push(`### ${file}`);
5618
+ result.push(...hits);
5708
5619
  }
5709
-
5710
- lines.push('---');
5711
- lines.push('_Generated by SigMap `create_checkpoint`_');
5712
-
5713
- return lines.join('\n');
5620
+
5621
+ if (result.length === 0) return `No signatures found matching: ${args.query}`;
5622
+ return result.join('\n');
5623
+ } catch (err) {
5624
+ return `_search_signatures failed: ${err.message}_`;
5714
5625
  }
5715
-
5716
- /**
5717
- * get_routing({}) → string
5718
- *
5719
- * Reads the current context file, classifies all indexed files by complexity,
5720
- * and returns a formatted markdown routing guide showing which files belong
5721
- * to the fast/balanced/powerful model tier.
5722
- */
5723
- function getRouting(args, cwd) {
5724
- const contextPath = path.join(cwd, CONTEXT_FILE);
5725
- if (!fs.existsSync(contextPath)) {
5726
- return (
5727
- '_No context file found. Run `node gen-context.js --routing` first._\n\n' +
5728
- 'This generates routing hints that map each file to a model tier:\n' +
5729
- '- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
5730
- '- **balanced** (sonnet/gpt-4o) — standard application code\n' +
5731
- '- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
5732
- );
5733
- }
5734
-
5735
- // Parse file list from context (### headings are file paths)
5736
- const content = fs.readFileSync(contextPath, 'utf8');
5737
- const fileRels = content.split('\n')
5738
- .filter((l) => l.startsWith('### '))
5739
- .map((l) => l.slice(4).trim());
5740
-
5741
- // Build synthetic fileEntries for the classifier
5742
- // We don't have live sig arrays here, so rebuild from the context blocks
5743
- const entries = [];
5744
- const blocks = content.split(/^### /m).slice(1); // slice past the header
5745
- for (const block of blocks) {
5746
- const firstLine = block.split('\n')[0].trim();
5747
- const codeBlock = block.match(/```\n([\s\S]*?)```/);
5748
- const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
5749
- entries.push({ filePath: path.join(cwd, firstLine), sigs });
5750
- }
5751
-
5752
- try {
5753
- const { classifyAll } = __require('./src/routing/classifier');
5754
- const { formatRoutingSection } = __require('./src/routing/hints');
5755
- const groups = classifyAll(entries, cwd);
5756
- return formatRoutingSection(groups);
5757
- } catch (err) {
5758
- return `_Routing classification failed: ${err.message}_`;
5759
- }
5626
+ }
5627
+
5628
+ /**
5629
+ * get_map({ type }) → string
5630
+ *
5631
+ * Returns a section from PROJECT_MAP.md.
5632
+ * type: 'imports' | 'classes' | 'routes'
5633
+ */
5634
+ function getMap(args, cwd) {
5635
+ if (!args || !args.type) return 'Missing required argument: type';
5636
+
5637
+ const header = MAP_SECTIONS[args.type];
5638
+ if (!header) {
5639
+ return `Unknown map type: "${args.type}". Use: imports, classes, routes`;
5760
5640
  }
5761
-
5762
- function explainFile(args, cwd) {
5763
- if (!args || !args.path) return 'Missing required argument: path';
5764
-
5765
- const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
5766
- const targetAbs = path.resolve(cwd, targetRel);
5767
- const contextPath = path.join(cwd, CONTEXT_FILE);
5768
-
5769
- const lines = ['# explain_file: ' + targetRel, ''];
5770
-
5771
- lines.push('## Signatures');
5772
- let indexedFiles = [];
5773
-
5774
- if (fs.existsSync(contextPath)) {
5775
- const ctxContent = fs.readFileSync(contextPath, 'utf8');
5776
- const ctxLines = ctxContent.split('\n');
5777
- let capturing = false;
5778
- const sigLines = [];
5779
-
5780
- for (const line of ctxLines) {
5781
- if (line.startsWith('### ')) {
5782
- if (capturing) break;
5783
- const rel = line.slice(4).trim().replace(/\\/g, '/');
5784
- capturing = rel === targetRel || rel.endsWith('/' + targetRel) || targetRel.endsWith('/' + rel);
5785
- if (capturing) continue;
5786
- } else if (capturing) {
5787
- sigLines.push(line);
5788
- }
5789
- }
5790
-
5791
- const sigs = sigLines.filter((l) => l !== '```' && l.trim() !== '');
5792
- if (sigs.length > 0) {
5793
- lines.push(...sigs);
5794
- } else {
5795
- lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
5796
- }
5797
-
5798
- indexedFiles = ctxContent
5799
- .split('\n')
5800
- .filter((l) => l.startsWith('### '))
5801
- .map((l) => path.resolve(cwd, l.slice(4).trim()));
5802
- } else {
5803
- lines.push('_No context file found. Run: node gen-context.js_');
5804
- }
5805
-
5806
- if (!fs.existsSync(targetAbs)) {
5807
- lines.push('');
5808
- lines.push('> File not found on disk: ' + targetRel);
5809
- return lines.join('\n');
5810
- }
5811
-
5812
- lines.push('');
5813
-
5814
- lines.push('## Imports (direct dependencies)');
5815
- try {
5816
- const { extractImports } = __require('./src/map/import-graph');
5817
- const fileContent = fs.readFileSync(targetAbs, 'utf8');
5818
- const fileSet = new Set(indexedFiles);
5819
- fileSet.add(targetAbs);
5820
- const imports = extractImports(targetAbs, fileContent, fileSet);
5821
- if (imports.length > 0) {
5822
- for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
5823
- } else {
5824
- lines.push('_No resolvable relative imports found._');
5825
- }
5826
- } catch (err) {
5827
- lines.push('_Could not analyze imports: ' + err.message + '_');
5828
- }
5829
-
5830
- lines.push('');
5831
-
5832
- lines.push('## Callers (files that import this file)');
5833
- try {
5834
- const { extractImports } = __require('./src/map/import-graph');
5835
- const fileSet = new Set(indexedFiles);
5836
- fileSet.add(targetAbs);
5837
- const callers = [];
5838
- for (const f of indexedFiles) {
5839
- if (f === targetAbs || !fs.existsSync(f)) continue;
5840
- try {
5841
- const fc = fs.readFileSync(f, 'utf8');
5842
- const imps = extractImports(f, fc, fileSet);
5843
- if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
5844
- } catch (_) {}
5845
- }
5846
- if (callers.length > 0) {
5847
- for (const c of callers) lines.push('- ' + c);
5848
- } else {
5849
- lines.push('_No indexed files import this file._');
5850
- }
5851
- } catch (err) {
5852
- lines.push('_Could not analyze callers: ' + err.message + '_');
5853
- }
5854
-
5855
- return lines.join('\n');
5641
+
5642
+ const mapPath = path.join(cwd, 'PROJECT_MAP.md');
5643
+ if (!fs.existsSync(mapPath)) {
5644
+ return 'PROJECT_MAP.md not found. Run: node gen-project-map.js';
5856
5645
  }
5857
-
5858
- function listModules(args, cwd) {
5859
- try {
5860
- const { buildSigIndex } = __require('./src/retrieval/ranker');
5861
- const index = buildSigIndex(cwd);
5862
- if (index.size === 0) {
5863
- return 'No context file found. Run: node gen-context.js';
5864
- }
5865
5646
 
5866
- const groups = {};
5867
- for (const [rel, sigs] of index.entries()) {
5868
- const parts = rel.split('/');
5869
- const mod = parts.length > 1 ? parts[0] : '.';
5870
- if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
5871
- groups[mod].fileCount++;
5872
- groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
5873
- }
5647
+ const content = fs.readFileSync(mapPath, 'utf8');
5648
+ const idx = content.indexOf(header);
5649
+ if (idx === -1) {
5650
+ return `Section "${header}" not found in PROJECT_MAP.md`;
5651
+ }
5874
5652
 
5875
- const sorted = Object.entries(groups)
5876
- .map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
5877
- .sort((a, b) => b.tokenCount - a.tokenCount);
5653
+ // Extract from this header to the next ### header
5654
+ const after = content.slice(idx);
5655
+ const nextMatch = after.slice(header.length).search(/\n###\s/);
5656
+ return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
5657
+ }
5878
5658
 
5879
- if (sorted.length === 0) return 'No modules found in context file.';
5659
+ /**
5660
+ * create_checkpoint({ note? }) → string
5661
+ *
5662
+ * Returns a markdown checkpoint summarising current project state:
5663
+ * - Timestamp and optional user note
5664
+ * - Active git branch + last 5 commit messages
5665
+ * - Token count of current context file
5666
+ * - List of modules present in the context
5667
+ * - Route count (if PROJECT_MAP.md exists)
5668
+ */
5669
+ function createCheckpoint(args, cwd) {
5670
+ const note = (args && args.note) ? args.note.trim() : '';
5671
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
5672
+ const lines = [
5673
+ '# SigMap Checkpoint',
5674
+ `**Created:** ${now}`,
5675
+ ];
5880
5676
 
5881
- const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
5677
+ if (note) lines.push(`**Note:** ${note}`);
5678
+ lines.push('');
5882
5679
 
5883
- return [
5884
- '# Modules',
5885
- '',
5886
- '| Module | Files | Tokens |',
5887
- '|--------|-------|--------|',
5888
- ...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
5889
- '',
5890
- `**Total context tokens: ~${total}**`,
5891
- '',
5892
- '_Use `read_context({ module: "name" })` to get signatures for a specific module._',
5893
- ].join('\n');
5894
- } catch (err) {
5895
- return `_list_modules failed: ${err.message}_`;
5896
- }
5897
- }
5898
-
5899
- function queryContext(args, cwd) {
5900
- if (!args || !args.query) return 'Missing required argument: query';
5901
- const contextPath = path.join(cwd, CONTEXT_FILE);
5902
- if (!fs.existsSync(contextPath)) return 'No context file found. Run: node gen-context.js';
5903
- try {
5904
- const { rank, buildSigIndex, formatRankTable } = __require('./src/retrieval/ranker');
5905
- const index = buildSigIndex(cwd);
5906
- if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
5907
- const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
5908
- const results = rank(args.query, index, { topK, cwd });
5909
- return formatRankTable(results, args.query);
5910
- } catch (err) {
5911
- return `_query_context failed: ${err.message}_`;
5912
- }
5680
+ // ── Git info ────────────────────────────────────────────────────────────
5681
+ lines.push('## Git state');
5682
+ try {
5683
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
5684
+ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
5685
+ }).trim();
5686
+ lines.push(`**Branch:** ${branch}`);
5687
+ } catch (_) {
5688
+ lines.push('**Branch:** (not a git repo)');
5913
5689
  }
5914
-
5915
- function getImpact(args, cwd) {
5916
- if (!args || !args.file) return 'Missing required argument: file';
5917
- try {
5918
- const { analyzeImpact, formatImpact } = __require('./src/graph/impact');
5919
- const depth = Math.max(0, parseInt(args.depth, 10) || 3);
5920
- const results = analyzeImpact(args.file, cwd, { depth });
5921
- return results.map((r) => formatImpact(r.impact)).join('\n\n---\n\n');
5922
- } catch (err) {
5923
- return `_get_impact failed: ${err.message}_`;
5690
+
5691
+ try {
5692
+ const log = execSync(
5693
+ 'git log --oneline -5 --no-decorate 2>/dev/null',
5694
+ { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
5695
+ ).trim();
5696
+ if (log) {
5697
+ lines.push('');
5698
+ lines.push('**Recent commits:**');
5699
+ for (const l of log.split('\n')) lines.push(`- ${l}`);
5924
5700
  }
5925
- }
5701
+ } catch (_) {} // ignore — not every project uses git
5702
+ lines.push('');
5926
5703
 
5927
- function getLines(args, cwd) {
5928
- if (!args || !args.file) return 'Missing required argument: file';
5704
+ // ── Context stats ────────────────────────────────────────────────────────
5705
+ lines.push('## Context snapshot');
5706
+ const contextPath = path.join(cwd, CONTEXT_FILE);
5707
+ if (fs.existsSync(contextPath)) {
5708
+ const content = fs.readFileSync(contextPath, 'utf8');
5709
+ const tokens = Math.ceil(content.length / 4);
5929
5710
 
5930
- const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
5931
- const abs = path.resolve(cwd, rel);
5711
+ // Count modules (### headers are file paths)
5712
+ const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
5713
+ lines.push(`**Token count:** ~${tokens}`);
5714
+ lines.push(`**Modules in context:** ${modules.length}`);
5932
5715
 
5933
- // Sandbox: refuse paths that resolve outside the project root.
5934
- const root = path.resolve(cwd);
5935
- if (abs !== root && !abs.startsWith(root + path.sep)) {
5936
- return `Refused: ${rel} resolves outside the project root`;
5716
+ if (modules.length > 0) {
5717
+ lines.push('');
5718
+ lines.push('**Modules:**');
5719
+ for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
5720
+ if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
5937
5721
  }
5938
- if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
5939
- return `File not found: ${rel}`;
5722
+ } else {
5723
+ lines.push('_No context file found. Run: node gen-context.js_');
5724
+ }
5725
+ lines.push('');
5726
+
5727
+ // ── Route summary ────────────────────────────────────────────────────────
5728
+ const mapPath = path.join(cwd, 'PROJECT_MAP.md');
5729
+ if (fs.existsSync(mapPath)) {
5730
+ const mapContent = fs.readFileSync(mapPath, 'utf8');
5731
+ const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
5732
+ if (routeLines.length > 0) {
5733
+ lines.push('## Routes');
5734
+ lines.push(`**Total routes detected:** ${routeLines.length}`);
5735
+ lines.push('');
5736
+ for (const r of routeLines.slice(0, 10)) lines.push(r);
5737
+ if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
5738
+ lines.push('');
5940
5739
  }
5740
+ }
5941
5741
 
5942
- const start = parseInt(args.start, 10);
5943
- const end = parseInt(args.end, 10);
5944
- if (!Number.isFinite(start) || !Number.isFinite(end)) {
5945
- return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
5946
- }
5742
+ lines.push('---');
5743
+ lines.push('_Generated by SigMap `create_checkpoint`_');
5947
5744
 
5948
- let lines;
5949
- try {
5950
- lines = fs.readFileSync(abs, 'utf8').split('\n');
5951
- } catch (err) {
5952
- return `Could not read ${rel}: ${err.message}`;
5953
- }
5745
+ return lines.join('\n');
5746
+ }
5954
5747
 
5955
- const total = lines.length;
5956
- const from = Math.max(1, Math.min(start, end));
5957
- const to = Math.min(total, Math.max(start, end));
5958
- if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
5748
+ /**
5749
+ * get_routing({}) string
5750
+ *
5751
+ * Reads the current context file, classifies all indexed files by complexity,
5752
+ * and returns a formatted markdown routing guide showing which files belong
5753
+ * to the fast/balanced/powerful model tier.
5754
+ */
5755
+ function getRouting(args, cwd) {
5756
+ const contextPath = path.join(cwd, CONTEXT_FILE);
5757
+ if (!fs.existsSync(contextPath)) {
5758
+ return (
5759
+ '_No context file found. Run `node gen-context.js --routing` first._\n\n' +
5760
+ 'This generates routing hints that map each file to a model tier:\n' +
5761
+ '- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
5762
+ '- **balanced** (sonnet/gpt-4o) — standard application code\n' +
5763
+ '- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
5764
+ );
5765
+ }
5959
5766
 
5960
- const slice = lines.slice(from - 1, to);
5767
+ // Parse file list from context (### headings are file paths)
5768
+ const content = fs.readFileSync(contextPath, 'utf8');
5769
+ const fileRels = content.split('\n')
5770
+ .filter((l) => l.startsWith('### '))
5771
+ .map((l) => l.slice(4).trim());
5961
5772
 
5962
- let safeLines = slice;
5963
- try {
5964
- const { scan } = __require('./src/security/scanner');
5965
- safeLines = scan(slice, rel).safe;
5966
- } catch (_) {}
5773
+ // Build synthetic fileEntries for the classifier
5774
+ // We don't have live sig arrays here, so rebuild from the context blocks
5775
+ const entries = [];
5776
+ const blocks = content.split(/^### /m).slice(1); // slice past the header
5777
+ for (const block of blocks) {
5778
+ const firstLine = block.split('\n')[0].trim();
5779
+ const codeBlock = block.match(/```\n([\s\S]*?)```/);
5780
+ const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
5781
+ entries.push({ filePath: path.join(cwd, firstLine), sigs });
5782
+ }
5967
5783
 
5968
- return [
5969
- `# ${rel}:${from}-${to}`,
5970
- '```',
5971
- ...safeLines,
5972
- '```',
5973
- ].join('\n');
5784
+ try {
5785
+ const { classifyAll } = __require('./src/routing/classifier');
5786
+ const { formatRoutingSection } = __require('./src/routing/hints');
5787
+ const groups = classifyAll(entries, cwd);
5788
+ return formatRoutingSection(groups);
5789
+ } catch (err) {
5790
+ return `_Routing classification failed: ${err.message}_`;
5974
5791
  }
5792
+ }
5975
5793
 
5976
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines };
5977
- };
5794
+ /**
5795
+ * explain_file({ path }) → string
5796
+ *
5797
+ * Returns a file's signatures, its direct imports, and files that import it.
5798
+ * path: relative path from project root (e.g. 'src/services/auth.ts')
5799
+ */
5800
+ function explainFile(args, cwd) {
5801
+ if (!args || !args.path) return 'Missing required argument: path';
5978
5802
 
5979
- // ── ./src/learning/weights ──
5980
- __factories["./src/learning/weights"] = function(module, exports) {
5981
- 'use strict';
5803
+ const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
5804
+ const targetAbs = path.resolve(cwd, targetRel);
5805
+ const contextPath = path.join(cwd, CONTEXT_FILE);
5982
5806
 
5983
- const fs = require('fs');
5984
- const path = require('path');
5807
+ const lines = ['# explain_file: ' + targetRel, ''];
5985
5808
 
5986
- const DECAY = 0.95;
5987
- const MAX_MULT = 3.0;
5988
- const MIN_MULT = 0.30;
5989
- const BASELINE = 1.0;
5809
+ // ── Signatures (hot + cold + cache via buildSigIndex) ───────────────────
5810
+ lines.push('## Signatures');
5811
+ let indexedFiles = [];
5812
+
5813
+ try {
5814
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
5815
+ const index = buildSigIndex(cwd);
5816
+ let sigs = index.get(targetRel);
5817
+ if (!sigs) {
5818
+ for (const [file, fileSigs] of index.entries()) {
5819
+ if (file === targetRel || file.endsWith('/' + targetRel) || targetRel.endsWith('/' + file)) {
5820
+ sigs = fileSigs;
5821
+ break;
5822
+ }
5823
+ }
5824
+ }
5825
+ if (sigs && sigs.length > 0) {
5826
+ lines.push(...sigs);
5827
+ } else {
5828
+ lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
5829
+ }
5830
+ indexedFiles = [...index.keys()].map((rel) => path.resolve(cwd, rel));
5831
+ } catch (_) {
5832
+ lines.push('_No context file found. Run: node gen-context.js_');
5833
+ }
5834
+
5835
+ if (!fs.existsSync(targetAbs)) {
5836
+ lines.push('');
5837
+ lines.push('> File not found on disk: ' + targetRel);
5838
+ return lines.join('\n');
5839
+ }
5840
+
5841
+ lines.push('');
5842
+
5843
+ // ── Direct imports ────────────────────────────────────────────────────────
5844
+ lines.push('## Imports (direct dependencies)');
5845
+ try {
5846
+ const { extractImports } = __require('./src/map/import-graph');
5847
+ const fileContent = fs.readFileSync(targetAbs, 'utf8');
5848
+ const fileSet = new Set(indexedFiles);
5849
+ fileSet.add(targetAbs);
5850
+ const imports = extractImports(targetAbs, fileContent, fileSet);
5851
+ if (imports.length > 0) {
5852
+ for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
5853
+ } else {
5854
+ lines.push('_No resolvable relative imports found._');
5855
+ }
5856
+ } catch (err) {
5857
+ lines.push('_Could not analyze imports: ' + err.message + '_');
5858
+ }
5859
+
5860
+ lines.push('');
5861
+
5862
+ // ── Callers (reverse-import lookup) ──────────────────────────────────────
5863
+ lines.push('## Callers (files that import this file)');
5864
+ try {
5865
+ const { extractImports } = __require('./src/map/import-graph');
5866
+ const fileSet = new Set(indexedFiles);
5867
+ fileSet.add(targetAbs);
5868
+ const callers = [];
5869
+ for (const f of indexedFiles) {
5870
+ if (f === targetAbs || !fs.existsSync(f)) continue;
5871
+ try {
5872
+ const fc = fs.readFileSync(f, 'utf8');
5873
+ const imps = extractImports(f, fc, fileSet);
5874
+ if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
5875
+ } catch (_) {}
5876
+ }
5877
+ if (callers.length > 0) {
5878
+ for (const c of callers) lines.push('- ' + c);
5879
+ } else {
5880
+ lines.push('_No indexed files import this file._');
5881
+ }
5882
+ } catch (err) {
5883
+ lines.push('_Could not analyze callers: ' + err.message + '_');
5884
+ }
5885
+
5886
+ return lines.join('\n');
5887
+ }
5888
+
5889
+ /**
5890
+ * list_modules({}) → string
5891
+ *
5892
+ * Lists all srcDir modules present in the context file, sorted by token count
5893
+ * descending. Helps agents decide which module to query with read_context.
5894
+ */
5895
+ function listModules(args, cwd) {
5896
+ try {
5897
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
5898
+ const index = buildSigIndex(cwd);
5899
+ if (index.size === 0) {
5900
+ return 'No context file found. Run: node gen-context.js';
5901
+ }
5902
+
5903
+ const groups = {};
5904
+ for (const [rel, sigs] of index.entries()) {
5905
+ const parts = rel.replace(/\\/g, '/').split('/');
5906
+ const mod = parts.length > 1 ? parts[0] : '.';
5907
+ if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
5908
+ groups[mod].fileCount++;
5909
+ groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
5910
+ }
5911
+
5912
+ const sorted = Object.entries(groups)
5913
+ .map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
5914
+ .sort((a, b) => b.tokenCount - a.tokenCount);
5915
+
5916
+ if (sorted.length === 0) return 'No modules found in context file.';
5917
+
5918
+ const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
5919
+
5920
+ return [
5921
+ '# Modules',
5922
+ '',
5923
+ '| Module | Files | Tokens |',
5924
+ '|--------|-------|--------|',
5925
+ ...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
5926
+ '',
5927
+ `**Total context tokens: ~${total}**`,
5928
+ '',
5929
+ '_Use `read_context({ module: "name" })` to get signatures for a specific module._',
5930
+ ].join('\n');
5931
+ } catch (err) {
5932
+ return `_list_modules failed: ${err.message}_`;
5933
+ }
5934
+ }
5935
+
5936
+ /**
5937
+ * query_context({ query, topK? }) → string
5938
+ *
5939
+ * Ranks context-file entries by relevance to the query and returns the
5940
+ * top-K most relevant files with their signatures and scores.
5941
+ */
5942
+ function queryContext(args, cwd) {
5943
+ if (!args || !args.query) return 'Missing required argument: query';
5944
+
5945
+ try {
5946
+ const { rank, buildSigIndex, formatRankTable } = __require('./src/retrieval/ranker');
5947
+ const { buildFromCwd } = __require('./src/graph/builder');
5948
+ const index = buildSigIndex(cwd);
5949
+ if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
5950
+
5951
+ const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
5952
+ // Build dependency graph for neighbor boost — non-fatal if it fails
5953
+ let graph = null;
5954
+ try { graph = buildFromCwd(cwd); } catch (_) {}
5955
+ const results = rank(args.query, index, { topK, cwd, graph });
5956
+ return formatRankTable(results, args.query);
5957
+ } catch (err) {
5958
+ return `_query_context failed: ${err.message}_`;
5959
+ }
5960
+ }
5961
+
5962
+ /**
5963
+ * get_impact({ file, depth? }) → string
5964
+ *
5965
+ * Returns a formatted markdown impact report for the given file:
5966
+ * direct importers, transitive importers, affected tests, affected routes.
5967
+ */
5968
+ function getImpact(args, cwd) {
5969
+ if (!args || !args.file) return 'Missing required argument: file';
5970
+
5971
+ try {
5972
+ const { analyzeImpact, formatImpact } = __require('./src/graph/impact');
5973
+ const depth = Math.max(0, parseInt(args.depth, 10) || 3);
5974
+ const results = analyzeImpact(args.file, cwd, { depth });
5975
+ if (results.length === 0) return `No impact data for: ${args.file}`;
5976
+ return results.map((r) => formatImpact(r.impact)).join('\n\n---\n\n');
5977
+ } catch (err) {
5978
+ return `_get_impact failed: ${err.message}_`;
5979
+ }
5980
+ }
5981
+
5982
+ /**
5983
+ * get_lines({ file, start, end }) → string
5984
+ *
5985
+ * Surgical Context demand-driven fetch: returns an exact, clamped line range from a
5986
+ * source file. The path is resolved inside the project root (no traversal escape) and
5987
+ * the returned lines are secret-scanned via the same redactor used for signatures.
5988
+ */
5989
+ function getLines(args, cwd) {
5990
+ if (!args || !args.file) return 'Missing required argument: file';
5991
+
5992
+ const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
5993
+ const abs = path.resolve(cwd, rel);
5994
+
5995
+ // Sandbox: refuse paths that resolve outside the project root.
5996
+ const root = path.resolve(cwd);
5997
+ if (abs !== root && !abs.startsWith(root + path.sep)) {
5998
+ return `Refused: ${rel} resolves outside the project root`;
5999
+ }
6000
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
6001
+ return `File not found: ${rel}`;
6002
+ }
6003
+
6004
+ const start = parseInt(args.start, 10);
6005
+ const end = parseInt(args.end, 10);
6006
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
6007
+ return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
6008
+ }
6009
+
6010
+ let lines;
6011
+ try {
6012
+ lines = fs.readFileSync(abs, 'utf8').split('\n');
6013
+ } catch (err) {
6014
+ return `Could not read ${rel}: ${err.message}`;
6015
+ }
6016
+
6017
+ const total = lines.length;
6018
+ const from = Math.max(1, Math.min(start, end));
6019
+ const to = Math.min(total, Math.max(start, end));
6020
+ if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
6021
+
6022
+ const slice = lines.slice(from - 1, to);
6023
+
6024
+ // Redaction scan: reuse the signature secret scanner line-by-line.
6025
+ let safeLines = slice;
6026
+ try {
6027
+ const { scan } = __require('./src/security/scanner');
6028
+ safeLines = scan(slice, rel).safe;
6029
+ } catch (_) {} // non-fatal: fall back to raw slice
6030
+
6031
+ return [
6032
+ `# ${rel}:${from}-${to}`,
6033
+ '```',
6034
+ ...safeLines,
6035
+ '```',
6036
+ ].join('\n');
6037
+ }
6038
+
6039
+ /**
6040
+ * read_memory({ limit? }) → string
6041
+ *
6042
+ * Recall the cross-session decision log (notes) plus the last ranking-session
6043
+ * focus, formatted for an agent to consume at the start of a task.
6044
+ */
6045
+ function readMemory(args, cwd) {
6046
+ let limit = parseInt(args && args.limit, 10);
6047
+ if (!Number.isFinite(limit) || limit <= 0) limit = 10;
6048
+ limit = Math.min(limit, 50);
6049
+
6050
+ const out = ['# SigMap memory'];
6051
+
6052
+ let notes = [];
6053
+ try {
6054
+ const { readNotes, formatNotes } = __require('./src/session/notes');
6055
+ notes = readNotes(cwd, limit);
6056
+ out.push('');
6057
+ out.push(`## Recent notes (${notes.length})`);
6058
+ // Most recent first for quick scanning.
6059
+ out.push(formatNotes(notes.slice().reverse()));
6060
+ } catch (_) {
6061
+ out.push('');
6062
+ out.push('_No notes available._');
6063
+ }
6064
+
6065
+ // Last ranking-session focus (if any) — extends src/session/memory.js.
6066
+ try {
6067
+ const { loadSession } = __require('./src/session/memory');
6068
+ const s = loadSession(cwd);
6069
+ if (s && (s.lastQuery || (s.topFiles && s.topFiles.length))) {
6070
+ out.push('');
6071
+ out.push('## Last session');
6072
+ if (s.lastQuery) out.push(`**Last query:** ${s.lastQuery}`);
6073
+ if (s.topFiles && s.topFiles.length) {
6074
+ out.push(`**Focus files:** ${s.topFiles.map((f) => f.file).slice(0, 5).join(', ')}`);
6075
+ }
6076
+ }
6077
+ } catch (_) { /* session optional */ }
6078
+
6079
+ return out.join('\n');
6080
+ }
6081
+
6082
+ module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory };
6083
+
6084
+ };
6085
+ // ── ./src/learning/weights ──
6086
+ __factories["./src/learning/weights"] = function(module, exports) {
6087
+ 'use strict';
6088
+
6089
+ const fs = require('fs');
6090
+ const path = require('path');
6091
+
6092
+ const DECAY = 0.95;
6093
+ const MAX_MULT = 3.0;
6094
+ const MIN_MULT = 0.30;
6095
+ const BASELINE = 1.0;
5990
6096
 
5991
6097
  function weightsPath(cwd) {
5992
6098
  return path.join(cwd, '.context', 'weights.json');
@@ -6135,344 +6241,364 @@ __factories["./src/learning/weights"] = function(module, exports) {
6135
6241
 
6136
6242
  // ── ./src/mcp/server ──
6137
6243
  __factories["./src/mcp/server"] = function(module, exports) {
6138
-
6139
- /**
6140
- * SigMap MCP server — zero npm dependencies.
6141
- *
6142
- * Wire protocol: JSON-RPC 2.0 over stdio.
6143
- * One JSON object per line on both stdin and stdout.
6144
- *
6145
- * Supported methods:
6146
- * initialize → serverInfo + capabilities
6147
- * tools/list10 tool definitions
6148
- * tools/calldispatch to handler, return result
6149
- */
6244
+ 'use strict';
6245
+
6246
+ /**
6247
+ * SigMap MCP server — zero npm dependencies.
6248
+ *
6249
+ * Wire protocol: JSON-RPC 2.0 over stdio.
6250
+ * One JSON object per line on both stdin and stdout.
6251
+ *
6252
+ * Supported methods:
6253
+ * initializeserverInfo + capabilities
6254
+ * tools/list11 tool definitions
6255
+ * tools/call → dispatch to handler, return result
6256
+ */
6150
6257
 
6151
- const readline = require('readline');
6152
- const { TOOLS } = __require('./src/mcp/tools');
6153
- const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines } = __require('./src/mcp/handlers');
6258
+ const readline = require('readline');
6259
+ const { TOOLS } = __require('./src/mcp/tools');
6260
+ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory } = __require('./src/mcp/handlers');
6154
6261
 
6155
- const SERVER_INFO = {
6156
- name: 'sigmap',
6157
- version: '6.14.0',
6158
- description: 'SigMap MCP server — code signatures on demand',
6159
- };
6160
-
6161
- // ---------------------------------------------------------------------------
6162
- // JSON-RPC helpers
6163
- // ---------------------------------------------------------------------------
6164
- function respond(id, result) {
6165
- process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
6262
+ const SERVER_INFO = {
6263
+ name: 'sigmap',
6264
+ version: '7.0.0',
6265
+ description: 'SigMap MCP server — code signatures on demand',
6266
+ };
6267
+
6268
+ // ---------------------------------------------------------------------------
6269
+ // JSON-RPC helpers
6270
+ // ---------------------------------------------------------------------------
6271
+ function respond(id, result) {
6272
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
6273
+ }
6274
+
6275
+ function respondError(id, code, message) {
6276
+ process.stdout.write(
6277
+ JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
6278
+ );
6279
+ }
6280
+
6281
+ // ---------------------------------------------------------------------------
6282
+ // Method dispatcher
6283
+ // ---------------------------------------------------------------------------
6284
+ function dispatch(msg, cwd) {
6285
+ const { method, id, params } = msg;
6286
+
6287
+ // Notifications (no id) need no response
6288
+ if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
6289
+ return;
6166
6290
  }
6167
-
6168
- function respondError(id, code, message) {
6169
- process.stdout.write(
6170
- JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
6171
- );
6291
+
6292
+ if (method === 'initialize') {
6293
+ respond(id, {
6294
+ protocolVersion: (params && params.protocolVersion) || '2024-11-05',
6295
+ serverInfo: SERVER_INFO,
6296
+ capabilities: { tools: {} },
6297
+ });
6298
+ return;
6172
6299
  }
6173
-
6174
- // ---------------------------------------------------------------------------
6175
- // Method dispatcher
6176
- // ---------------------------------------------------------------------------
6177
- function dispatch(msg, cwd) {
6178
- const { method, id, params } = msg;
6179
-
6180
- // Notifications (no id) need no response
6181
- if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
6182
- return;
6183
- }
6184
-
6185
- if (method === 'initialize') {
6186
- respond(id, {
6187
- protocolVersion: (params && params.protocolVersion) || '2024-11-05',
6188
- serverInfo: SERVER_INFO,
6189
- capabilities: { tools: {} },
6190
- });
6191
- return;
6192
- }
6193
-
6194
- if (method === 'tools/list') {
6195
- respond(id, { tools: TOOLS });
6196
- return;
6197
- }
6198
-
6199
- if (method === 'tools/call') {
6200
- const name = params && params.name;
6201
- const args = (params && params.arguments) || {};
6202
-
6203
- let text;
6204
- try {
6205
- if (name === 'read_context') text = readContext(args, cwd);
6206
- else if (name === 'search_signatures') text = searchSignatures(args, cwd);
6207
- else if (name === 'get_map') text = getMap(args, cwd);
6208
- else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
6209
- else if (name === 'get_routing') text = getRouting(args, cwd);
6210
- else if (name === 'explain_file') text = explainFile(args, cwd);
6211
- else if (name === 'list_modules') text = listModules(args, cwd);
6212
- else if (name === 'query_context') text = queryContext(args, cwd);
6213
- else if (name === 'get_impact') text = getImpact(args, cwd);
6214
- else if (name === 'get_lines') text = getLines(args, cwd);
6215
- else {
6216
- respondError(id, -32601, `Unknown tool: ${name}`);
6217
- return;
6218
- }
6219
- } catch (err) {
6220
- respondError(id, -32603, `Tool error: ${err.message}`);
6300
+
6301
+ if (method === 'tools/list') {
6302
+ respond(id, { tools: TOOLS });
6303
+ return;
6304
+ }
6305
+
6306
+ if (method === 'tools/call') {
6307
+ const name = params && params.name;
6308
+ const args = (params && params.arguments) || {};
6309
+
6310
+ let text;
6311
+ try {
6312
+ if (name === 'read_context') text = readContext(args, cwd);
6313
+ else if (name === 'search_signatures') text = searchSignatures(args, cwd);
6314
+ else if (name === 'get_map') text = getMap(args, cwd);
6315
+ else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
6316
+ else if (name === 'get_routing') text = getRouting(args, cwd);
6317
+ else if (name === 'explain_file') text = explainFile(args, cwd);
6318
+ else if (name === 'list_modules') text = listModules(args, cwd);
6319
+ else if (name === 'query_context') text = queryContext(args, cwd);
6320
+ else if (name === 'get_impact') text = getImpact(args, cwd);
6321
+ else if (name === 'get_lines') text = getLines(args, cwd);
6322
+ else if (name === 'read_memory') text = readMemory(args, cwd);
6323
+ else {
6324
+ respondError(id, -32601, `Unknown tool: ${name}`);
6221
6325
  return;
6222
6326
  }
6223
-
6224
- respond(id, {
6225
- content: [{ type: 'text', text: String(text) }],
6226
- });
6327
+ } catch (err) {
6328
+ respondError(id, -32603, `Tool error: ${err.message}`);
6227
6329
  return;
6228
6330
  }
6229
-
6230
- // Unknown method
6231
- if (id !== undefined && id !== null) {
6232
- respondError(id, -32601, `Method not found: ${method}`);
6233
- }
6234
- }
6235
-
6236
- // ---------------------------------------------------------------------------
6237
- // Server entry point
6238
- // ---------------------------------------------------------------------------
6239
- function start(cwd) {
6240
- const rl = readline.createInterface({ input: process.stdin, terminal: false });
6241
-
6242
- rl.on('line', (line) => {
6243
- const trimmed = line.trim();
6244
- if (!trimmed) return;
6245
-
6246
- let msg;
6247
- try {
6248
- msg = JSON.parse(trimmed);
6249
- } catch (_) {
6250
- // Cannot respond without a valid id — ignore malformed input
6251
- return;
6252
- }
6253
-
6254
- try {
6255
- dispatch(msg, cwd);
6256
- } catch (err) {
6257
- const id = (msg && msg.id) != null ? msg.id : null;
6258
- respondError(id, -32603, `Internal error: ${err.message}`);
6259
- }
6260
- });
6261
-
6262
- rl.on('close', () => {
6263
- process.exit(0);
6331
+
6332
+ respond(id, {
6333
+ content: [{ type: 'text', text: String(text) }],
6264
6334
  });
6335
+ return;
6265
6336
  }
6266
-
6267
- module.exports = { start };
6268
-
6269
- };
6270
6337
 
6338
+ // Unknown method
6339
+ if (id !== undefined && id !== null) {
6340
+ respondError(id, -32601, `Method not found: ${method}`);
6341
+ }
6342
+ }
6343
+
6344
+ // ---------------------------------------------------------------------------
6345
+ // Server entry point
6346
+ // ---------------------------------------------------------------------------
6347
+ function start(cwd) {
6348
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
6349
+
6350
+ rl.on('line', (line) => {
6351
+ const trimmed = line.trim();
6352
+ if (!trimmed) return;
6353
+
6354
+ let msg;
6355
+ try {
6356
+ msg = JSON.parse(trimmed);
6357
+ } catch (_) {
6358
+ // Cannot respond without a valid id — ignore malformed input
6359
+ return;
6360
+ }
6361
+
6362
+ try {
6363
+ dispatch(msg, cwd);
6364
+ } catch (err) {
6365
+ const id = (msg && msg.id) != null ? msg.id : null;
6366
+ respondError(id, -32603, `Internal error: ${err.message}`);
6367
+ }
6368
+ });
6369
+
6370
+ rl.on('close', () => {
6371
+ process.exit(0);
6372
+ });
6373
+ }
6374
+
6375
+ module.exports = { start };
6376
+
6377
+ };
6271
6378
  // ── ./src/mcp/tools ──
6272
6379
  __factories["./src/mcp/tools"] = function(module, exports) {
6273
-
6274
- /**
6275
- * MCP tool definitions for SigMap.
6276
- * Three tools: read_context, search_signatures, get_map.
6277
- */
6278
-
6279
- const TOOLS = [
6280
- {
6281
- name: 'read_context',
6282
- description:
6283
- 'Read extracted code signatures for the project or a specific module path. ' +
6284
- 'Returns the full copilot-instructions.md content (~500–4K tokens) or a ' +
6285
- 'filtered subset when a module path is provided (~50–500 tokens).',
6286
- inputSchema: {
6287
- type: 'object',
6288
- properties: {
6289
- module: {
6290
- type: 'string',
6291
- description:
6292
- 'Optional subdirectory path to scope results (e.g. "src/services"). ' +
6293
- 'Omit to get the full codebase context.',
6294
- },
6380
+ 'use strict';
6381
+
6382
+ /**
6383
+ * MCP tool definitions for SigMap (11 tools).
6384
+ * read_context, search_signatures, get_map, create_checkpoint, get_routing,
6385
+ * explain_file, list_modules, query_context, get_impact, get_lines, read_memory.
6386
+ */
6387
+
6388
+ const TOOLS = [
6389
+ {
6390
+ name: 'read_context',
6391
+ description:
6392
+ 'Read extracted code signatures for the project or a specific module path. ' +
6393
+ 'Returns the full copilot-instructions.md content (~500–4K tokens) or a ' +
6394
+ 'filtered subset when a module path is provided (~50–500 tokens).',
6395
+ inputSchema: {
6396
+ type: 'object',
6397
+ properties: {
6398
+ module: {
6399
+ type: 'string',
6400
+ description:
6401
+ 'Optional subdirectory path to scope results (e.g. "src/services"). ' +
6402
+ 'Omit to get the full codebase context.',
6295
6403
  },
6296
- required: [],
6297
6404
  },
6405
+ required: [],
6298
6406
  },
6299
- {
6300
- name: 'search_signatures',
6301
- description:
6302
- 'Search extracted code signatures for a keyword, function name, or class name. ' +
6303
- 'Returns matching signature lines with their file paths.',
6304
- inputSchema: {
6305
- type: 'object',
6306
- properties: {
6307
- query: {
6308
- type: 'string',
6309
- description: 'Keyword to search for in signatures (case-insensitive).',
6310
- },
6407
+ },
6408
+ {
6409
+ name: 'search_signatures',
6410
+ description:
6411
+ 'Search extracted code signatures for a keyword, function name, or class name. ' +
6412
+ 'Returns matching signature lines with their file paths.',
6413
+ inputSchema: {
6414
+ type: 'object',
6415
+ properties: {
6416
+ query: {
6417
+ type: 'string',
6418
+ description: 'Keyword to search for in signatures (case-insensitive).',
6311
6419
  },
6312
- required: ['query'],
6313
6420
  },
6421
+ required: ['query'],
6314
6422
  },
6315
- {
6316
- name: 'get_map',
6317
- description:
6318
- 'Read a section from PROJECT_MAP.md — import graph, class hierarchy, or route table. ' +
6319
- 'Requires gen-project-map.js to have been run first.',
6320
- inputSchema: {
6321
- type: 'object',
6322
- properties: {
6323
- type: {
6324
- type: 'string',
6325
- enum: ['imports', 'classes', 'routes'],
6326
- description: 'Which section to retrieve: imports, classes, or routes.',
6327
- },
6423
+ },
6424
+ {
6425
+ name: 'get_map',
6426
+ description:
6427
+ 'Read a section from PROJECT_MAP.md import graph, class hierarchy, or route table. ' +
6428
+ 'Requires gen-project-map.js to have been run first.',
6429
+ inputSchema: {
6430
+ type: 'object',
6431
+ properties: {
6432
+ type: {
6433
+ type: 'string',
6434
+ enum: ['imports', 'classes', 'routes'],
6435
+ description: 'Which section to retrieve: imports, classes, or routes.',
6328
6436
  },
6329
- required: ['type'],
6330
6437
  },
6438
+ required: ['type'],
6331
6439
  },
6332
- {
6333
- name: 'create_checkpoint',
6334
- description:
6335
- 'Create a session checkpoint summarising current project state. ' +
6336
- 'Returns recent git commits, active branch, token count, and a ' +
6337
- 'compact snapshot of the codebase context ideal for session handoffs ' +
6338
- 'or periodic saves during long coding sessions.',
6339
- inputSchema: {
6340
- type: 'object',
6341
- properties: {
6342
- note: {
6343
- type: 'string',
6344
- description: 'Optional free-text note to include in the checkpoint (e.g. what you were working on).',
6345
- },
6440
+ },
6441
+ {
6442
+ name: 'create_checkpoint',
6443
+ description:
6444
+ 'Create a session checkpoint summarising current project state. ' +
6445
+ 'Returns recent git commits, active branch, token count, and a ' +
6446
+ 'compact snapshot of the codebase context — ideal for session handoffs ' +
6447
+ 'or periodic saves during long coding sessions.',
6448
+ inputSchema: {
6449
+ type: 'object',
6450
+ properties: {
6451
+ note: {
6452
+ type: 'string',
6453
+ description: 'Optional free-text note to include in the checkpoint (e.g. what you were working on).',
6346
6454
  },
6347
- required: [],
6348
6455
  },
6456
+ required: [],
6349
6457
  },
6350
- {
6351
- name: 'get_routing',
6352
- description:
6353
- 'Get model routing hints for this project — which files belong to which complexity ' +
6354
- 'tier (fast/balanced/powerful) and which AI model to use for each type of task. ' +
6355
- 'Helps reduce API costs by 40–80% by routing simple tasks to cheaper models.',
6356
- inputSchema: {
6357
- type: 'object',
6358
- properties: {},
6359
- required: [],
6360
- },
6458
+ },
6459
+ {
6460
+ name: 'get_routing',
6461
+ description:
6462
+ 'Get model routing hints for this project which files belong to which complexity ' +
6463
+ 'tier (fast/balanced/powerful) and which AI model to use for each type of task. ' +
6464
+ 'Helps reduce API costs by 40–80% by routing simple tasks to cheaper models.',
6465
+ inputSchema: {
6466
+ type: 'object',
6467
+ properties: {},
6468
+ required: [],
6361
6469
  },
6362
- {
6363
- name: 'explain_file',
6364
- description:
6365
- 'Explain a specific file: returns its extracted signatures, direct imports ' +
6366
- '(files it depends on), and callers (files that import it). ' +
6367
- 'Ideal for understanding a file in isolation without reading raw source. ' +
6368
- 'Requires the context file to have been generated first.',
6369
- inputSchema: {
6370
- type: 'object',
6371
- properties: {
6372
- path: {
6373
- type: 'string',
6374
- description:
6375
- 'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
6376
- 'Use the paths shown in read_context output.',
6377
- },
6470
+ },
6471
+ {
6472
+ name: 'explain_file',
6473
+ description:
6474
+ 'Explain a specific file: returns its extracted signatures, direct imports ' +
6475
+ '(files it depends on), and callers (files that import it). ' +
6476
+ 'Ideal for understanding a file in isolation without reading raw source. ' +
6477
+ 'Requires the context file to have been generated first.',
6478
+ inputSchema: {
6479
+ type: 'object',
6480
+ properties: {
6481
+ path: {
6482
+ type: 'string',
6483
+ description:
6484
+ 'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
6485
+ 'Use the paths shown in read_context output.',
6378
6486
  },
6379
- required: ['path'],
6380
6487
  },
6488
+ required: ['path'],
6381
6489
  },
6382
- {
6383
- name: 'list_modules',
6384
- description:
6385
- 'List all top-level modules (srcDirs) present in the context file, ' +
6386
- 'sorted by token count descending. Use this to decide which module to ' +
6387
- 'pass to read_context before querying a specific area of the codebase.',
6388
- inputSchema: {
6389
- type: 'object',
6390
- properties: {},
6391
- required: [],
6392
- },
6490
+ },
6491
+ {
6492
+ name: 'list_modules',
6493
+ description:
6494
+ 'List all top-level modules (srcDirs) present in the context file, ' +
6495
+ 'sorted by token count descending. Use this to decide which module to ' +
6496
+ 'pass to read_context before querying a specific area of the codebase.',
6497
+ inputSchema: {
6498
+ type: 'object',
6499
+ properties: {},
6500
+ required: [],
6393
6501
  },
6394
- {
6395
- name: 'query_context',
6396
- description:
6397
- 'Rank and return the most relevant files for a specific task or question. ' +
6398
- 'Uses keyword + symbol + path scoring to surface only the top-K files relevant ' +
6399
- 'to the query much cheaper than reading all context. ' +
6400
- 'Returns ranked file list with signatures and relevance scores.',
6401
- inputSchema: {
6402
- type: 'object',
6403
- properties: {
6404
- query: {
6405
- type: 'string',
6406
- description:
6407
- 'Natural language task description or keyword(s) to rank files against. ' +
6408
- 'E.g. "add a new language extractor", "fix secret scanning", "auth module".',
6409
- },
6410
- topK: {
6411
- type: 'number',
6412
- description: 'Maximum number of files to return (default: 10, max: 25).',
6413
- },
6502
+ },
6503
+ {
6504
+ name: 'query_context',
6505
+ description:
6506
+ 'Rank and return the most relevant files for a specific task or question. ' +
6507
+ 'Uses keyword + symbol + path scoring to surface only the top-K files relevant ' +
6508
+ 'to the query much cheaper than reading all context. ' +
6509
+ 'Returns ranked file list with signatures and relevance scores.',
6510
+ inputSchema: {
6511
+ type: 'object',
6512
+ properties: {
6513
+ query: {
6514
+ type: 'string',
6515
+ description:
6516
+ 'Natural language task description or keyword(s) to rank files against. ' +
6517
+ 'E.g. "add a new language extractor", "fix secret scanning", "auth module".',
6518
+ },
6519
+ topK: {
6520
+ type: 'number',
6521
+ description: 'Maximum number of files to return (default: 10, max: 25).',
6414
6522
  },
6415
- required: ['query'],
6416
6523
  },
6524
+ required: ['query'],
6417
6525
  },
6418
- {
6419
- name: 'get_impact',
6420
- description:
6421
- 'Show every file that is impacted when a given file changes — direct importers, ' +
6422
- 'transitive importers, affected tests, and affected routes/controllers. ' +
6423
- 'Gives agents instant blast-radius awareness before making a change. ' +
6424
- 'Handles circular dependencies safely (no infinite loops).',
6425
- inputSchema: {
6426
- type: 'object',
6427
- properties: {
6428
- file: {
6429
- type: 'string',
6430
- description:
6431
- 'Relative path from the project root of the file that changed ' +
6432
- '(e.g. "src/extractors/python.js"). Use forward slashes.',
6433
- },
6434
- depth: {
6435
- type: 'number',
6436
- description: 'BFS traversal depth limit (default: 3). Use 0 for unlimited.',
6437
- },
6526
+ },
6527
+ {
6528
+ name: 'get_impact',
6529
+ description:
6530
+ 'Show every file that is impacted when a given file changes — direct importers, ' +
6531
+ 'transitive importers, affected tests, and affected routes/controllers. ' +
6532
+ 'Gives agents instant blast-radius awareness before making a change. ' +
6533
+ 'Handles circular dependencies safely (no infinite loops).',
6534
+ inputSchema: {
6535
+ type: 'object',
6536
+ properties: {
6537
+ file: {
6538
+ type: 'string',
6539
+ description:
6540
+ 'Relative path from the project root of the file that changed ' +
6541
+ '(e.g. "src/extractors/python.js"). Use forward slashes.',
6542
+ },
6543
+ depth: {
6544
+ type: 'number',
6545
+ description: 'BFS traversal depth limit (default: 3). Use 0 for unlimited.',
6438
6546
  },
6439
- required: ['file'],
6440
6547
  },
6548
+ required: ['file'],
6441
6549
  },
6442
- {
6443
- name: 'get_lines',
6444
- description:
6445
- 'Fetch an exact line range from a source file on demand — the Surgical Context ' +
6446
- 'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
6447
- 'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
6448
- 'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
6449
- inputSchema: {
6450
- type: 'object',
6451
- properties: {
6452
- file: {
6453
- type: 'string',
6454
- description:
6455
- 'Relative path from the project root (e.g. "src/config/loader.js"). ' +
6456
- 'Use the path shown in a signature anchor. Use forward slashes.',
6457
- },
6458
- start: {
6459
- type: 'number',
6460
- description: '1-based start line (inclusive). Clamped to the file bounds.',
6461
- },
6462
- end: {
6463
- type: 'number',
6464
- description: '1-based end line (inclusive). Clamped to the file bounds.',
6465
- },
6550
+ },
6551
+ {
6552
+ name: 'get_lines',
6553
+ description:
6554
+ 'Fetch an exact line range from a source file on demand — the Surgical Context ' +
6555
+ 'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
6556
+ 'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
6557
+ 'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
6558
+ inputSchema: {
6559
+ type: 'object',
6560
+ properties: {
6561
+ file: {
6562
+ type: 'string',
6563
+ description:
6564
+ 'Relative path from the project root (e.g. "src/config/loader.js"). ' +
6565
+ 'Use the path shown in a signature anchor. Use forward slashes.',
6566
+ },
6567
+ start: {
6568
+ type: 'number',
6569
+ description: '1-based start line (inclusive). Clamped to the file bounds.',
6570
+ },
6571
+ end: {
6572
+ type: 'number',
6573
+ description: '1-based end line (inclusive). Clamped to the file bounds.',
6466
6574
  },
6467
- required: ['file', 'start', 'end'],
6468
6575
  },
6576
+ required: ['file', 'start', 'end'],
6469
6577
  },
6470
- ];
6578
+ },
6579
+ {
6580
+ name: 'read_memory',
6581
+ description:
6582
+ 'Recall the project decision log — recent notes left by humans or agents ' +
6583
+ 'across sessions (via `sigmap note`), plus the last ranking-session focus. ' +
6584
+ 'Call this at the start of a task to kill cold-start: it answers ' +
6585
+ '"what were we doing and why" without re-reading the whole codebase.',
6586
+ inputSchema: {
6587
+ type: 'object',
6588
+ properties: {
6589
+ limit: {
6590
+ type: 'number',
6591
+ description: 'How many of the most recent notes to return (default: 10, max: 50).',
6592
+ },
6593
+ },
6594
+ required: [],
6595
+ },
6596
+ },
6597
+ ];
6471
6598
 
6472
- module.exports = { TOOLS };
6473
-
6474
- };
6599
+ module.exports = { TOOLS };
6475
6600
 
6601
+ };
6476
6602
  // ── ./src/routing/classifier ──
6477
6603
  __factories["./src/routing/classifier"] = function(module, exports) {
6478
6604
 
@@ -7007,6 +7133,110 @@ __factories["./src/tracking/logger"] = function(module, exports) {
7007
7133
 
7008
7134
  };
7009
7135
 
7136
+ // ── ./src/session/notes ──
7137
+ __factories["./src/session/notes"] = function(module, exports) {
7138
+ 'use strict';
7139
+
7140
+ /**
7141
+ * Decision-log notes (Memory, v6.15.0).
7142
+ *
7143
+ * A tiny append-only log of human/agent notes that survives across sessions,
7144
+ * so an agent starting cold can recall "what were we doing / why". Stored as
7145
+ * NDJSON under `.context/` to match the project's other state logs
7146
+ * (usage.ndjson, benchmark-history.ndjson). Zero dependencies.
7147
+ *
7148
+ * Consumed by the `read_memory` MCP tool and the `sigmap note` / `sigmap status`
7149
+ * commands. Complements `src/session/memory.js` (ranking session) rather than
7150
+ * duplicating it.
7151
+ */
7152
+
7153
+ const fs = require('fs');
7154
+ const path = require('path');
7155
+ const { execSync } = require('child_process');
7156
+
7157
+ const NOTES_FILE = path.join('.context', 'notes.ndjson');
7158
+ const MAX_TEXT = 2000;
7159
+
7160
+ function notesPath(cwd) {
7161
+ return path.join(cwd, NOTES_FILE);
7162
+ }
7163
+
7164
+ function _currentBranch(cwd) {
7165
+ try {
7166
+ return execSync('git rev-parse --abbrev-ref HEAD', {
7167
+ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
7168
+ }).trim() || null;
7169
+ } catch (_) {
7170
+ return null;
7171
+ }
7172
+ }
7173
+
7174
+ /**
7175
+ * Append a note. Returns the stored entry.
7176
+ * @param {string} cwd
7177
+ * @param {string} text
7178
+ * @param {object} [opts]
7179
+ * @param {string|null} [opts.branch] override branch (default: current git branch)
7180
+ * @param {string} [opts.tag] optional category tag
7181
+ */
7182
+ function addNote(cwd, text, opts = {}) {
7183
+ const clean = String(text == null ? '' : text).trim().slice(0, MAX_TEXT);
7184
+ if (!clean) throw new Error('note text is empty');
7185
+ const entry = {
7186
+ ts: new Date().toISOString(),
7187
+ text: clean,
7188
+ branch: opts.branch !== undefined ? opts.branch : _currentBranch(cwd),
7189
+ };
7190
+ if (opts.tag) entry.tag = String(opts.tag).trim().slice(0, 40);
7191
+ const p = notesPath(cwd);
7192
+ fs.mkdirSync(path.dirname(p), { recursive: true });
7193
+ fs.appendFileSync(p, JSON.stringify(entry) + '\n');
7194
+ return entry;
7195
+ }
7196
+
7197
+ /**
7198
+ * Read notes in chronological order (oldest first).
7199
+ * @param {string} cwd
7200
+ * @param {number} [limit=0] keep only the most recent N (0 = all)
7201
+ * @returns {object[]}
7202
+ */
7203
+ function readNotes(cwd, limit = 0) {
7204
+ let raw;
7205
+ try { raw = fs.readFileSync(notesPath(cwd), 'utf8'); }
7206
+ catch (_) { return []; }
7207
+ const out = [];
7208
+ for (const line of raw.split('\n')) {
7209
+ const t = line.trim();
7210
+ if (!t) continue;
7211
+ try { out.push(JSON.parse(t)); } catch (_) { /* skip corrupt line */ }
7212
+ }
7213
+ if (limit > 0 && out.length > limit) return out.slice(out.length - limit);
7214
+ return out;
7215
+ }
7216
+
7217
+ /** Format notes as a Markdown list (pass already-ordered notes). */
7218
+ function formatNotes(notes) {
7219
+ if (!notes || !notes.length) {
7220
+ return 'No notes recorded yet. Add one with: sigmap note "<text>"';
7221
+ }
7222
+ return notes.map((n) => {
7223
+ const when = String(n.ts || '').replace('T', ' ').slice(0, 16);
7224
+ const br = n.branch ? ` (${n.branch})` : '';
7225
+ const tag = n.tag ? ` #${n.tag}` : '';
7226
+ return `- [${when}${br}]${tag} ${n.text}`;
7227
+ }).join('\n');
7228
+ }
7229
+
7230
+ /** Delete the notes log. Returns true if a file was removed. */
7231
+ function clearNotes(cwd) {
7232
+ try { fs.unlinkSync(notesPath(cwd)); return true; }
7233
+ catch (_) { return false; }
7234
+ }
7235
+
7236
+ module.exports = { notesPath, addNote, readNotes, formatNotes, clearNotes };
7237
+
7238
+ };
7239
+
7010
7240
  // ── ./src/session/memory ──
7011
7241
  __factories["./src/session/memory"] = function(module, exports) {
7012
7242
  'use strict';
@@ -7781,314 +8011,621 @@ __factories["./src/eval/usefulness-scorer"] = function(module, exports) {
7781
8011
 
7782
8012
  // ── ./packages/adapters/copilot (bundled) ──
7783
8013
  __factories["./packages/adapters/copilot"] = function(module, exports) {
7784
- const path = require('path');
7785
- const fs = require('fs');
7786
- const name = 'copilot';
7787
- const COPILOT_MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
7788
- function format(context, opts = {}) {
7789
- if (!context || typeof context !== 'string') return '';
7790
- const version = (opts && opts.version) || 'unknown';
7791
- const timestamp = new Date().toISOString();
7792
- return [
7793
- `<!-- Generated by SigMap gen-context.js v${version} -->`,
7794
- `<!-- Updated: ${timestamp} -->`,
7795
- `<!-- Do not edit below — regenerate with: node gen-context.js -->`,
7796
- '',
7797
- '## SigMap commands',
7798
- '',
7799
- '| When | Command |',
7800
- '|------|---------|',
7801
- '| Before answering a question about code | `sigmap ask "<your question>"` |',
7802
- '| After changing config or source dirs | `sigmap validate` |',
7803
- '| To verify an AI answer is grounded | `sigmap judge --response <file>` |',
7804
- '',
7805
- '# Code signatures',
7806
- '',
7807
- context,
7808
- ].join('\n');
8014
+ 'use strict';
8015
+
8016
+ /**
8017
+ * Copilot adapter writes to .github/copilot-instructions.md
8018
+ * GitHub Copilot reads this file automatically in every workspace.
8019
+ *
8020
+ * Contract:
8021
+ * format(context, opts?) string
8022
+ * outputPath(cwd) → string
8023
+ */
8024
+
8025
+ const path = require('path');
8026
+ const fs = require('fs');
8027
+
8028
+ const name = 'copilot';
8029
+ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8030
+
8031
+ /**
8032
+ * Format context for GitHub Copilot instructions.
8033
+ * @param {string} context - Raw signature context string
8034
+ * @param {object} [opts]
8035
+ * @param {string} [opts.version] - SigMap version string
8036
+ * @returns {string}
8037
+ */
8038
+ function format(context, opts = {}) {
8039
+ if (!context || typeof context !== 'string') return '';
8040
+ const version = opts.version || 'unknown';
8041
+ const timestamp = new Date().toISOString();
8042
+ const meta = _confidenceMeta(opts);
8043
+ const header = [
8044
+ `<!-- Generated by SigMap gen-context.js v${version} -->`,
8045
+ `<!-- Updated: ${timestamp} -->`,
8046
+ meta,
8047
+ `<!-- Do not edit below — regenerate with: node gen-context.js -->`,
8048
+ '',
8049
+ '# Code signatures',
8050
+ '',
8051
+ ].join('\n');
8052
+ return header + context;
8053
+ }
8054
+
8055
+ function _confidenceMeta(opts) {
8056
+ const parts = [`version=${opts.version || 'unknown'}`];
8057
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8058
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8059
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8060
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8061
+ return `<!-- sigmap: ${parts.join(' ')} -->`;
8062
+ }
8063
+
8064
+ /**
8065
+ * Return the output file path for this adapter.
8066
+ * @param {string} cwd - Project root
8067
+ * @returns {string}
8068
+ */
8069
+ function outputPath(cwd) {
8070
+ return path.join(cwd, '.github', 'copilot-instructions.md');
8071
+ }
8072
+
8073
+ /**
8074
+ * Write signatures into copilot-instructions.md using append-under-marker.
8075
+ * If marker exists, content above marker is preserved.
8076
+ * If legacy generated content exists without marker, replace it cleanly.
8077
+ * @param {string} context - Raw signature context string
8078
+ * @param {string} cwd - Project root
8079
+ * @param {object} [opts]
8080
+ */
8081
+ function write(context, cwd, opts = {}) {
8082
+ const filePath = outputPath(cwd);
8083
+ let existing = '';
8084
+ if (fs.existsSync(filePath)) {
8085
+ existing = fs.readFileSync(filePath, 'utf8');
7809
8086
  }
7810
- function outputPath(cwd) { return path.join(cwd, '.github', 'copilot-instructions.md'); }
7811
- function write(context, cwd, opts = {}) {
7812
- const filePath = outputPath(cwd);
7813
- let existing = '';
7814
- if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
7815
- const formatted = format(context, opts);
7816
- const markerIdx = existing.indexOf('## Auto-generated signatures');
7817
- const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js') || existing.includes('# Code signatures');
7818
- const newContent = markerIdx !== -1
7819
- ? existing.slice(0, markerIdx) + COPILOT_MARKER.trimStart() + formatted
7820
- : (isLegacyGenerated ? COPILOT_MARKER.trimStart() + formatted : existing + COPILOT_MARKER + formatted);
7821
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
7822
- fs.writeFileSync(filePath, newContent, 'utf8');
8087
+
8088
+ const formatted = format(context, opts);
8089
+ const markerIdx = existing.indexOf('## Auto-generated signatures');
8090
+
8091
+ let newContent;
8092
+ if (markerIdx !== -1) {
8093
+ newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
8094
+ } else {
8095
+ const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
8096
+ || existing.includes('# Code signatures');
8097
+ newContent = isLegacyGenerated
8098
+ ? MARKER.trimStart() + formatted
8099
+ : existing + MARKER + formatted;
7823
8100
  }
7824
- module.exports = { name, format, outputPath, write };
7825
- };
7826
8101
 
8102
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
8103
+ fs.writeFileSync(filePath, newContent, 'utf8');
8104
+ }
8105
+
8106
+ module.exports = { name, format, outputPath, write };
8107
+
8108
+ };
7827
8109
  // ── ./packages/adapters/claude (bundled) ──
7828
8110
  __factories["./packages/adapters/claude"] = function(module, exports) {
7829
- const path = require('path');
7830
- const fs = require('fs');
7831
- const name = 'claude';
7832
- const CLAUDE_MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
7833
- const CLAUDE_ALLOWLIST_MARKER = '<!-- sigmap-bash-allowlist -->';
7834
- const CLAUDE_ALLOWLIST_BLOCK = [
7835
- '## Bash allowlist',
7836
- '',
7837
- CLAUDE_ALLOWLIST_MARKER,
7838
- '',
7839
- 'The following sigmap commands are pre-approved — Claude Code will not prompt for confirmation:',
7840
- '',
7841
- '```json',
7842
- JSON.stringify({
7843
- permissions: {
7844
- allow: [
7845
- 'Bash(sigmap ask*)',
7846
- 'Bash(sigmap validate*)',
7847
- 'Bash(sigmap judge*)',
7848
- 'Bash(sigmap weights*)',
7849
- 'Bash(sigmap history*)',
7850
- 'Bash(sigmap --query*)',
7851
- 'Bash(sigmap --diff*)',
7852
- 'Bash(sigmap --health*)',
7853
- 'Bash(sigmap --coverage*)',
7854
- 'Bash(node gen-context.js*)',
7855
- ],
7856
- },
7857
- }, null, 2),
7858
- '```',
7859
- '',
7860
- 'Add the `permissions.allow` array above to `.claude/settings.json` to activate.',
8111
+ 'use strict';
8112
+
8113
+ /**
8114
+ * Claude adapter appends to CLAUDE.md under a marker line.
8115
+ * Never overwrites human-written content above the marker.
8116
+ *
8117
+ * Contract:
8118
+ * format(context, opts?) → string
8119
+ * outputPath(cwd) → string
8120
+ * write(context, cwd, opts?) → void (handles append logic)
8121
+ */
8122
+
8123
+ const path = require('path');
8124
+ const fs = require('fs');
8125
+
8126
+ const name = 'claude';
8127
+
8128
+ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8129
+ const ALLOWLIST_MARKER = '<!-- sigmap-bash-allowlist -->';
8130
+
8131
+ const ALLOWLIST_BLOCK = [
8132
+ '## Bash allowlist',
8133
+ '',
8134
+ ALLOWLIST_MARKER,
8135
+ '',
8136
+ 'The following sigmap commands are pre-approved — Claude Code will not prompt for confirmation:',
8137
+ '',
8138
+ '```json',
8139
+ JSON.stringify({
8140
+ permissions: {
8141
+ allow: [
8142
+ 'Bash(sigmap ask*)',
8143
+ 'Bash(sigmap validate*)',
8144
+ 'Bash(sigmap judge*)',
8145
+ 'Bash(sigmap weights*)',
8146
+ 'Bash(sigmap history*)',
8147
+ 'Bash(sigmap --query*)',
8148
+ 'Bash(sigmap --diff*)',
8149
+ 'Bash(sigmap --health*)',
8150
+ 'Bash(sigmap --coverage*)',
8151
+ 'Bash(node gen-context.js*)',
8152
+ ],
8153
+ },
8154
+ }, null, 2),
8155
+ '```',
8156
+ '',
8157
+ 'Add the `permissions.allow` array above to `.claude/settings.json` to activate.',
8158
+ '',
8159
+ ].join('\n');
8160
+
8161
+ /**
8162
+ * Format context suited for CLAUDE.md.
8163
+ * @param {string} context - Raw signature context string
8164
+ * @param {object} [opts]
8165
+ * @param {string} [opts.version] - SigMap version string
8166
+ * @returns {string}
8167
+ */
8168
+ function format(context, opts = {}) {
8169
+ if (!context || typeof context !== 'string') return '';
8170
+ const version = opts.version || 'unknown';
8171
+ const timestamp = new Date().toISOString();
8172
+ const meta = _confidenceMeta(opts);
8173
+ return [
8174
+ `<!-- Generated by SigMap v${version} — ${timestamp} -->`,
8175
+ meta,
7861
8176
  '',
8177
+ context,
7862
8178
  ].join('\n');
7863
- function format(context, opts = {}) {
7864
- if (!context || typeof context !== 'string') return '';
7865
- const version = (opts && opts.version) || 'unknown';
7866
- const timestamp = new Date().toISOString();
7867
- return [
7868
- '## SigMap',
7869
- '- Before searching for files, run `sigmap ask "<query>"` — ranks by TF-IDF relevance',
7870
- '- After config changes, run `sigmap validate` to confirm coverage',
7871
- '- To verify answer groundedness, run `sigmap judge --response <file> --context CLAUDE.md`',
7872
- '- For PR-scoped context, run `sigmap --diff`',
7873
- '',
7874
- `<!-- Generated by SigMap v${version} — ${timestamp} -->`,
7875
- '',
7876
- context,
7877
- ].join('\n');
8179
+ }
8180
+
8181
+ function _confidenceMeta(opts) {
8182
+ const parts = [`version=${opts.version || 'unknown'}`];
8183
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8184
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8185
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8186
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8187
+ return `<!-- sigmap: ${parts.join(' ')} -->`;
8188
+ }
8189
+
8190
+ /**
8191
+ * Return the output file path for this adapter.
8192
+ * @param {string} cwd - Project root
8193
+ * @returns {string}
8194
+ */
8195
+ function outputPath(cwd) {
8196
+ return path.join(cwd, 'CLAUDE.md');
8197
+ }
8198
+
8199
+ /**
8200
+ * Write signatures into CLAUDE.md using the append-under-marker strategy.
8201
+ * Human content above the marker is never touched.
8202
+ * @param {string} context - Raw signature context string
8203
+ * @param {string} cwd - Project root
8204
+ * @param {object} [opts]
8205
+ */
8206
+ function write(context, cwd, opts = {}) {
8207
+ const filePath = outputPath(cwd);
8208
+ let existing = '';
8209
+ if (fs.existsSync(filePath)) {
8210
+ existing = fs.readFileSync(filePath, 'utf8');
7878
8211
  }
7879
- function outputPath(cwd) { return path.join(cwd, 'CLAUDE.md'); }
7880
- function write(context, cwd, opts = {}) {
7881
- const filePath = outputPath(cwd);
7882
- let existing = '';
7883
- if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
7884
- const formatted = format(context, opts);
7885
- const markerIdx = existing.indexOf('## Auto-generated signatures');
7886
- let newContent = markerIdx !== -1
7887
- ? existing.slice(0, markerIdx) + CLAUDE_MARKER.trimStart() + formatted
7888
- : existing + CLAUDE_MARKER + formatted;
7889
- if (!newContent.includes(CLAUDE_ALLOWLIST_MARKER)) {
7890
- const sigPos = newContent.indexOf('## Auto-generated signatures');
7891
- if (sigPos !== -1) {
7892
- newContent = newContent.slice(0, sigPos) + CLAUDE_ALLOWLIST_BLOCK + '\n' + newContent.slice(sigPos);
7893
- } else {
7894
- newContent = CLAUDE_ALLOWLIST_BLOCK + '\n' + newContent;
7895
- }
8212
+ const formatted = format(context, opts);
8213
+ const markerIdx = existing.indexOf('## Auto-generated signatures');
8214
+ let newContent;
8215
+ if (markerIdx !== -1) {
8216
+ newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
8217
+ } else {
8218
+ newContent = existing + MARKER + formatted;
8219
+ }
8220
+
8221
+ // Inject ## Bash allowlist above the sig marker if not already present
8222
+ if (!newContent.includes(ALLOWLIST_MARKER)) {
8223
+ const sigMarkerPos = newContent.indexOf('## Auto-generated signatures');
8224
+ if (sigMarkerPos !== -1) {
8225
+ newContent = newContent.slice(0, sigMarkerPos) + ALLOWLIST_BLOCK + '\n' + newContent.slice(sigMarkerPos);
8226
+ } else {
8227
+ newContent = ALLOWLIST_BLOCK + '\n' + newContent;
7896
8228
  }
7897
- fs.writeFileSync(filePath, newContent, 'utf8');
7898
8229
  }
7899
- module.exports = { name, format, outputPath, write };
7900
- };
7901
8230
 
8231
+ fs.writeFileSync(filePath, newContent, 'utf8');
8232
+ }
8233
+
8234
+ module.exports = { name, format, outputPath, write };
8235
+
8236
+ };
7902
8237
  // ── ./packages/adapters/cursor (bundled) ──
7903
8238
  __factories["./packages/adapters/cursor"] = function(module, exports) {
7904
- const path = require('path');
7905
- const name = 'cursor';
7906
- function format(context, opts = {}) {
7907
- if (!context || typeof context !== 'string') return '';
7908
- const version = (opts && opts.version) || 'unknown';
7909
- const timestamp = new Date().toISOString();
7910
- return [
7911
- `# Code signatures generated by SigMap v${version}`,
7912
- `# Updated: ${timestamp}`,
7913
- `# Regenerate: node gen-context.js`,
7914
- '#',
7915
- '# SigMap: before answering, run: sigmap ask "<query>" — finds relevant files',
7916
- '# SigMap: after config changes, run: sigmap validate',
7917
- '# SigMap: to verify answers, run: sigmap judge --response <file>',
7918
- '',
7919
- context,
7920
- ].join('\n');
7921
- }
7922
- function outputPath(cwd) { return path.join(cwd, '.cursorrules'); }
7923
- module.exports = { name, format, outputPath };
7924
- };
8239
+ 'use strict';
8240
+
8241
+ /**
8242
+ * Cursor adapter writes to .cursorrules
8243
+ * Cursor reads .cursorrules automatically in every workspace.
8244
+ *
8245
+ * Contract:
8246
+ * format(context, opts?) string
8247
+ * outputPath(cwd) → string
8248
+ */
8249
+
8250
+ const path = require('path');
8251
+
8252
+ const name = 'cursor';
8253
+
8254
+ /**
8255
+ * Format context for Cursor rules file.
8256
+ * @param {string} context - Raw signature context string
8257
+ * @param {object} [opts]
8258
+ * @param {string} [opts.version] - SigMap version string
8259
+ * @returns {string}
8260
+ */
8261
+ function format(context, opts = {}) {
8262
+ if (!context || typeof context !== 'string') return '';
8263
+ const version = opts.version || 'unknown';
8264
+ const timestamp = new Date().toISOString();
8265
+ const meta = _confidenceMeta(opts);
8266
+ const header = [
8267
+ `# Code signatures — generated by SigMap v${version}`,
8268
+ `# Updated: ${timestamp}`,
8269
+ `# ${meta}`,
8270
+ `# Regenerate: node gen-context.js`,
8271
+ '',
8272
+ ].join('\n');
8273
+ return header + context;
8274
+ }
8275
+
8276
+ function _confidenceMeta(opts) {
8277
+ const parts = [`version=${opts.version || 'unknown'}`];
8278
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8279
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8280
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8281
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8282
+ return `sigmap: ${parts.join(' ')}`;
8283
+ }
8284
+
8285
+ /**
8286
+ * Return the output file path for this adapter.
8287
+ * @param {string} cwd - Project root
8288
+ * @returns {string}
8289
+ */
8290
+ function outputPath(cwd) {
8291
+ return path.join(cwd, '.cursorrules');
8292
+ }
8293
+
8294
+ module.exports = { name, format, outputPath };
7925
8295
 
8296
+ };
7926
8297
  // ── ./packages/adapters/windsurf (bundled) ──
7927
8298
  __factories["./packages/adapters/windsurf"] = function(module, exports) {
7928
- const path = require('path');
7929
- const name = 'windsurf';
7930
- function format(context, opts = {}) {
7931
- if (!context || typeof context !== 'string') return '';
7932
- const version = (opts && opts.version) || 'unknown';
7933
- const timestamp = new Date().toISOString();
7934
- return [
7935
- `# Code signatures — generated by SigMap v${version}`,
7936
- `# Updated: ${timestamp}`,
7937
- `# Regenerate: node gen-context.js`,
7938
- '#',
7939
- '# SigMap: before answering, run: sigmap ask "<query>" — finds relevant files',
7940
- '# SigMap: after config changes, run: sigmap validate',
7941
- '# SigMap: to verify answers, run: sigmap judge --response <file>',
7942
- '',
7943
- context,
7944
- ].join('\n');
7945
- }
7946
- function outputPath(cwd) { return path.join(cwd, '.windsurfrules'); }
7947
- module.exports = { name, format, outputPath };
7948
- };
8299
+ 'use strict';
7949
8300
 
7950
- // ── ./packages/adapters/openai (bundled) ──
7951
- __factories["./packages/adapters/openai"] = function(module, exports) {
7952
- const path = require('path');
7953
- const name = 'openai';
7954
- function format(context, opts = {}) {
7955
- if (!context || typeof context !== 'string') return '';
7956
- const version = (opts && opts.version) || 'unknown';
7957
- const timestamp = new Date().toISOString();
7958
- const projectLine = (opts && opts.projectName) ? `Project: ${opts.projectName}\n` : '';
7959
- return [
7960
- `You are a coding assistant with full knowledge of this codebase.`,
7961
- `Below are the code signatures extracted by SigMap v${version} on ${timestamp}.`,
7962
- projectLine,
7963
- `Use these signatures to answer questions about the code accurately.`,
7964
- `When the user asks about a specific file or function, refer to the signatures below.`,
7965
- `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.`,
7966
- ``,
7967
- `## Code Signatures`,
7968
- ``,
7969
- context,
7970
- ].join('\n');
7971
- }
7972
- function outputPath(cwd) { return path.join(cwd, '.github', 'openai-context.md'); }
7973
- module.exports = { name, format, outputPath };
7974
- };
8301
+ /**
8302
+ * Windsurf adapter — writes to .windsurfrules
8303
+ * Windsurf reads .windsurfrules automatically in every workspace.
8304
+ *
8305
+ * Contract:
8306
+ * format(context, opts?) string
8307
+ * outputPath(cwd) string
8308
+ */
7975
8309
 
7976
- // ── ./packages/adapters/gemini (bundled) ──
7977
- __factories["./packages/adapters/gemini"] = function(module, exports) {
7978
- const path = require('path');
7979
- const fs = require('fs');
7980
- const name = 'gemini';
7981
- const GEMINI_MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
7982
- function format(context, opts = {}) {
7983
- if (!context || typeof context !== 'string') return '';
7984
- const version = (opts && opts.version) || 'unknown';
7985
- const timestamp = new Date().toISOString();
7986
- const projectLine = (opts && opts.projectName) ? `Project: ${opts.projectName}\n` : '';
7987
- return [
7988
- `You are a coding assistant with complete knowledge of this codebase.`,
7989
- `The following code signatures were extracted by SigMap v${version} on ${timestamp}.`,
7990
- projectLine,
7991
- `These signatures represent every public function, class, and type in the project.`,
7992
- `Refer to them when answering questions about code structure, APIs, and implementation.`,
7993
- `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.`,
7994
- ``,
7995
- `## Code Signatures`,
7996
- ``,
7997
- context,
7998
- ].join('\n');
7999
- }
8000
- function outputPath(cwd) { return path.join(cwd, '.github', 'gemini-context.md'); }
8001
- function write(context, cwd, opts = {}) {
8002
- const filePath = outputPath(cwd);
8003
- let existing = '';
8004
- if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
8005
- const formatted = format(context, opts);
8006
- const markerIdx = existing.indexOf('## Auto-generated signatures');
8007
- const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js') || existing.includes('## Code Signatures');
8008
- const newContent = markerIdx !== -1
8009
- ? existing.slice(0, markerIdx) + GEMINI_MARKER.trimStart() + formatted
8010
- : (isLegacyGenerated ? GEMINI_MARKER.trimStart() + formatted : existing + GEMINI_MARKER + formatted);
8011
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
8012
- fs.writeFileSync(filePath, newContent, 'utf8');
8013
- }
8014
- module.exports = { name, format, outputPath, write };
8015
- };
8310
+ const path = require('path');
8016
8311
 
8017
- // ── ./packages/adapters/codex (bundled) ──
8018
- __factories["./packages/adapters/codex"] = function(module, exports) {
8019
- const path = require('path');
8020
- const fs = require('fs');
8021
- const name = 'codex';
8022
- const CODEX_MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8023
- const CODEX_TOOLS_MARKER = '<!-- sigmap-tools -->';
8024
- const CODEX_TOOLS_BLOCK = [
8025
- '## Tools',
8026
- '',
8027
- CODEX_TOOLS_MARKER,
8028
- '',
8029
- '```json',
8030
- JSON.stringify([
8031
- { name: 'sigmap_ask', description: 'Rank source files by relevance to a natural-language query. Run before exploring the codebase.', command: 'sigmap ask "$QUERY"' },
8032
- { name: 'sigmap_validate', description: 'Validate SigMap config and measure context coverage. Run after changing config or source dirs.', command: 'sigmap validate' },
8033
- { 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"' },
8034
- { name: 'sigmap_query', description: 'Rank all files by relevance using TF-IDF and write a focused mini-context.', command: 'sigmap --query "$QUERY" --context' },
8035
- { name: 'sigmap_weights', description: 'Show learned file-ranking multipliers accumulated from past sessions.', command: 'sigmap weights' },
8036
- ], null, 2),
8037
- '```',
8312
+ const name = 'windsurf';
8313
+
8314
+ /**
8315
+ * Format context for Windsurf rules file.
8316
+ * @param {string} context - Raw signature context string
8317
+ * @param {object} [opts]
8318
+ * @param {string} [opts.version] - SigMap version string
8319
+ * @returns {string}
8320
+ */
8321
+ function format(context, opts = {}) {
8322
+ if (!context || typeof context !== 'string') return '';
8323
+ const version = opts.version || 'unknown';
8324
+ const timestamp = new Date().toISOString();
8325
+ const meta = _confidenceMeta(opts);
8326
+ const header = [
8327
+ `# Code signatures generated by SigMap v${version}`,
8328
+ `# Updated: ${timestamp}`,
8329
+ `# ${meta}`,
8330
+ `# Regenerate: node gen-context.js`,
8038
8331
  '',
8039
8332
  ].join('\n');
8040
- function format(context, opts = {}) {
8041
- if (!context || typeof context !== 'string' || !context.trim()) return '';
8042
- const toolBlock = [
8043
- '## SigMap commands',
8044
- '',
8045
- '| When | Command |',
8046
- '|------|---------|',
8047
- '| Before answering a question | `sigmap ask "<your question>"` |',
8048
- '| After code changes | `sigmap validate` |',
8049
- '| To query by topic | `sigmap --query "<topic>"` |',
8050
- '',
8051
- 'Always run `sigmap ask` or `sigmap --query` before searching for files relevant to a task.',
8052
- '',
8053
- ].join('\n');
8054
- return '# Code signatures\n\n' + toolBlock + context;
8055
- }
8056
- function outputPath(cwd) { return path.join(cwd, 'AGENTS.md'); }
8057
- function write(context, cwd, opts = {}) {
8058
- const filePath = outputPath(cwd);
8059
- let existing = '';
8060
- if (fs.existsSync(filePath)) existing = fs.readFileSync(filePath, 'utf8');
8061
- const formatted = format(context, opts);
8062
- const markerIdx = existing.indexOf('## Auto-generated signatures');
8063
- const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js') || existing.includes('## Code Signatures') || existing.includes('# Code signatures');
8064
- let newContent = markerIdx !== -1
8065
- ? existing.slice(0, markerIdx) + CODEX_MARKER.trimStart() + formatted
8066
- : (isLegacyGenerated ? CODEX_MARKER.trimStart() + formatted : existing + CODEX_MARKER + formatted);
8067
- if (!newContent.includes(CODEX_TOOLS_MARKER)) {
8068
- const sigPos = newContent.indexOf('## Auto-generated signatures');
8069
- if (sigPos !== -1) {
8070
- newContent = newContent.slice(0, sigPos) + CODEX_TOOLS_BLOCK + '\n' + newContent.slice(sigPos);
8071
- } else {
8072
- newContent = CODEX_TOOLS_BLOCK + '\n' + newContent;
8073
- }
8074
- }
8075
- fs.writeFileSync(filePath, newContent, 'utf8');
8076
- }
8077
- module.exports = { name, format, outputPath, write };
8333
+ return header + context;
8334
+ }
8335
+
8336
+ function _confidenceMeta(opts) {
8337
+ const parts = [`version=${opts.version || 'unknown'}`];
8338
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8339
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8340
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8341
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8342
+ return `sigmap: ${parts.join(' ')}`;
8343
+ }
8344
+
8345
+ /**
8346
+ * Return the output file path for this adapter.
8347
+ * @param {string} cwd - Project root
8348
+ * @returns {string}
8349
+ */
8350
+ function outputPath(cwd) {
8351
+ return path.join(cwd, '.windsurfrules');
8352
+ }
8353
+
8354
+ module.exports = { name, format, outputPath };
8355
+
8078
8356
  };
8357
+ // ── ./packages/adapters/openai (bundled) ──
8358
+ __factories["./packages/adapters/openai"] = function(module, exports) {
8359
+ 'use strict';
8079
8360
 
8080
- // ── ./packages/adapters/index (bundled) ──
8081
- __factories["./packages/adapters/index"] = function(module, exports) {
8082
- const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
8083
- const _adapterCache = {};
8084
- function getAdapter(name) {
8085
- if (!name || typeof name !== 'string') return null;
8086
- const key = name.toLowerCase();
8087
- if (!ADAPTER_NAMES.includes(key)) return null;
8088
- if (!_adapterCache[key]) {
8089
- try { _adapterCache[key] = __require('./packages/adapters/' + key); } catch (_) { return null; }
8090
- }
8091
- return _adapterCache[key];
8361
+ /**
8362
+ * OpenAI adapter — formats context as an OpenAI system message.
8363
+ * Use the output as the `content` field of a system role message.
8364
+ *
8365
+ * Example usage in code:
8366
+ * const { format } = require('sigmap/adapters/openai');
8367
+ * const systemPrompt = format(context);
8368
+ * // Pass to: openai.chat.completions.create({ messages: [{ role: 'system', content: systemPrompt }] })
8369
+ *
8370
+ * Contract:
8371
+ * format(context, opts?) → string
8372
+ * outputPath(cwd) → string
8373
+ */
8374
+
8375
+ const path = require('path');
8376
+
8377
+ const name = 'openai';
8378
+
8379
+ /**
8380
+ * Format context as an OpenAI system prompt.
8381
+ * @param {string} context - Raw signature context string
8382
+ * @param {object} [opts]
8383
+ * @param {string} [opts.version] - SigMap version string
8384
+ * @param {string} [opts.projectName] - Optional project name
8385
+ * @returns {string}
8386
+ */
8387
+ function format(context, opts = {}) {
8388
+ if (!context || typeof context !== 'string') return '';
8389
+ const version = opts.version || 'unknown';
8390
+ const timestamp = new Date().toISOString();
8391
+ const projectLine = opts.projectName
8392
+ ? `Project: ${opts.projectName}\n`
8393
+ : '';
8394
+
8395
+ const meta = _confidenceMeta(opts);
8396
+ return [
8397
+ `You are a coding assistant with full knowledge of this codebase.`,
8398
+ `Below are the code signatures extracted by SigMap v${version} on ${timestamp}.`,
8399
+ `<!-- ${meta} -->`,
8400
+ projectLine,
8401
+ `Use these signatures to answer questions about the code accurately.`,
8402
+ `When the user asks about a specific file or function, refer to the signatures below.`,
8403
+ `## Code Signatures`,
8404
+ ``,
8405
+ context,
8406
+ ].join('\n');
8407
+ }
8408
+
8409
+ /**
8410
+ * Return the output file path for this adapter.
8411
+ * Writes a .openai-context.md file that can be loaded at runtime.
8412
+ * @param {string} cwd - Project root
8413
+ * @returns {string}
8414
+ */
8415
+ function outputPath(cwd) {
8416
+ return path.join(cwd, '.github', 'openai-context.md');
8417
+ }
8418
+
8419
+ function _confidenceMeta(opts) {
8420
+ const parts = [`version=${opts.version || 'unknown'}`];
8421
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8422
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8423
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8424
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8425
+ return `sigmap: ${parts.join(' ')}`;
8426
+ }
8427
+
8428
+ module.exports = { name, format, outputPath };
8429
+
8430
+ };
8431
+ // ── ./packages/adapters/gemini (bundled) ──
8432
+ __factories["./packages/adapters/gemini"] = function(module, exports) {
8433
+ 'use strict';
8434
+
8435
+ /**
8436
+ * Gemini adapter — formats context as a Gemini system instruction.
8437
+ * Use the output as the `system_instruction` field in a Gemini API request.
8438
+ *
8439
+ * Example usage:
8440
+ * const { format } = require('sigmap/adapters/gemini');
8441
+ * const instruction = format(context);
8442
+ * // Pass to: genAI.getGenerativeModel({ model: 'gemini-pro', systemInstruction: instruction })
8443
+ *
8444
+ * Contract:
8445
+ * format(context, opts?) → string
8446
+ * outputPath(cwd) → string
8447
+ */
8448
+
8449
+ const path = require('path');
8450
+ const fs = require('fs');
8451
+
8452
+ const name = 'gemini';
8453
+ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8454
+
8455
+ /**
8456
+ * Format context as a Gemini system instruction.
8457
+ * @param {string} context - Raw signature context string
8458
+ * @param {object} [opts]
8459
+ * @param {string} [opts.version] - SigMap version string
8460
+ * @param {string} [opts.projectName] - Optional project name
8461
+ * @returns {string}
8462
+ */
8463
+ function format(context, opts = {}) {
8464
+ if (!context || typeof context !== 'string') return '';
8465
+ const version = opts.version || 'unknown';
8466
+ const timestamp = new Date().toISOString();
8467
+ const projectLine = opts.projectName
8468
+ ? `Project: ${opts.projectName}\n`
8469
+ : '';
8470
+
8471
+ const meta = _confidenceMeta(opts);
8472
+ return [
8473
+ `You are a coding assistant with complete knowledge of this codebase.`,
8474
+ `The following code signatures were extracted by SigMap v${version} on ${timestamp}.`,
8475
+ `<!-- ${meta} -->`,
8476
+ projectLine,
8477
+ `These signatures represent every public function, class, and type in the project.`,
8478
+ `Refer to them when answering questions about code structure, APIs, and implementation.`,
8479
+ `## Code Signatures`,
8480
+ ``,
8481
+ context,
8482
+ ].join('\n');
8483
+ }
8484
+
8485
+ /**
8486
+ * Return the output file path for this adapter.
8487
+ * @param {string} cwd - Project root
8488
+ * @returns {string}
8489
+ */
8490
+ function outputPath(cwd) {
8491
+ return path.join(cwd, '.github', 'gemini-context.md');
8492
+ }
8493
+
8494
+ /**
8495
+ * Write signatures into gemini-context.md using append-under-marker.
8496
+ * If marker exists, content above marker is preserved.
8497
+ * If legacy generated content exists without marker, replace it cleanly.
8498
+ * @param {string} context - Raw signature context string
8499
+ * @param {string} cwd - Project root
8500
+ * @param {object} [opts]
8501
+ */
8502
+ function write(context, cwd, opts = {}) {
8503
+ const filePath = outputPath(cwd);
8504
+ let existing = '';
8505
+ if (fs.existsSync(filePath)) {
8506
+ existing = fs.readFileSync(filePath, 'utf8');
8507
+ }
8508
+
8509
+ const formatted = format(context, opts);
8510
+ const markerIdx = existing.indexOf('## Auto-generated signatures');
8511
+
8512
+ let newContent;
8513
+ if (markerIdx !== -1) {
8514
+ newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
8515
+ } else {
8516
+ const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
8517
+ || existing.includes('## Code Signatures');
8518
+ newContent = isLegacyGenerated
8519
+ ? MARKER.trimStart() + formatted
8520
+ : existing + MARKER + formatted;
8521
+ }
8522
+
8523
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
8524
+ fs.writeFileSync(filePath, newContent, 'utf8');
8525
+ }
8526
+
8527
+ function _confidenceMeta(opts) {
8528
+ const parts = [`version=${opts.version || 'unknown'}`];
8529
+ if (opts.confidence) parts.push(`confidence=${opts.confidence}`);
8530
+ if (opts.coverage != null) parts.push(`coverage=${opts.coverage}%`);
8531
+ if (opts.dropped != null) parts.push(`dropped=${opts.dropped}`);
8532
+ if (opts.commit) parts.push(`commit=${opts.commit}`);
8533
+ return `sigmap: ${parts.join(' ')}`;
8534
+ }
8535
+
8536
+ module.exports = { name, format, outputPath, write };
8537
+
8538
+ };
8539
+ // ── ./packages/adapters/codex (bundled) ──
8540
+ __factories["./packages/adapters/codex"] = function(module, exports) {
8541
+ 'use strict';
8542
+
8543
+ /**
8544
+ * Codex adapter — writes OpenAI-style context to AGENTS.md.
8545
+ *
8546
+ * This adapter reuses the same prompt format as the OpenAI adapter,
8547
+ * but targets AGENTS.md so Codex-style agents can read repository guidance.
8548
+ *
8549
+ * Contract:
8550
+ * format(context, opts?) → string
8551
+ * outputPath(cwd) → string
8552
+ * write(context, cwd, opts?) → void
8553
+ */
8554
+
8555
+ const path = require('path');
8556
+ const fs = require('fs');
8557
+
8558
+ const name = 'codex';
8559
+ const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8560
+
8561
+ /**
8562
+ * Format context for AGENTS.md — clean markdown, no LLM preamble.
8563
+ * @param {string} context - Raw signature context string
8564
+ * @param {object} [opts]
8565
+ * @returns {string}
8566
+ */
8567
+ function format(context, opts = {}) {
8568
+ if (!context || typeof context !== 'string' || !context.trim()) return '';
8569
+ return `# Code signatures\n\n${context}`;
8570
+ }
8571
+
8572
+ /**
8573
+ * Return the output file path for this adapter.
8574
+ * @param {string} cwd - Project root
8575
+ * @returns {string}
8576
+ */
8577
+ function outputPath(cwd) {
8578
+ return path.join(cwd, 'AGENTS.md');
8579
+ }
8580
+
8581
+ /**
8582
+ * Write signatures into AGENTS.md using append-under-marker.
8583
+ * If marker exists, content above marker is preserved.
8584
+ * If legacy generated content exists without marker, replace it cleanly.
8585
+ * @param {string} context - Raw signature context string
8586
+ * @param {string} cwd - Project root
8587
+ * @param {object} [opts]
8588
+ */
8589
+ function write(context, cwd, opts = {}) {
8590
+ const filePath = outputPath(cwd);
8591
+ let existing = '';
8592
+ if (fs.existsSync(filePath)) {
8593
+ existing = fs.readFileSync(filePath, 'utf8');
8594
+ }
8595
+
8596
+ const formatted = format(context, opts);
8597
+ const markerIdx = existing.indexOf('## Auto-generated signatures');
8598
+
8599
+ let newContent;
8600
+ if (markerIdx !== -1) {
8601
+ newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
8602
+ } else {
8603
+ const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
8604
+ || existing.includes('## Code Signatures')
8605
+ || existing.includes('# Code signatures');
8606
+ newContent = isLegacyGenerated
8607
+ ? MARKER.trimStart() + formatted
8608
+ : existing + MARKER + formatted;
8609
+ }
8610
+
8611
+ fs.writeFileSync(filePath, newContent, 'utf8');
8612
+ }
8613
+
8614
+ module.exports = { name, format, outputPath, write };
8615
+
8616
+ };
8617
+ // ── ./packages/adapters/index (bundled) ──
8618
+ __factories["./packages/adapters/index"] = function(module, exports) {
8619
+ const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
8620
+ const _adapterCache = {};
8621
+ function getAdapter(name) {
8622
+ if (!name || typeof name !== 'string') return null;
8623
+ const key = name.toLowerCase();
8624
+ if (!ADAPTER_NAMES.includes(key)) return null;
8625
+ if (!_adapterCache[key]) {
8626
+ try { _adapterCache[key] = __require('./packages/adapters/' + key); } catch (_) { return null; }
8627
+ }
8628
+ return _adapterCache[key];
8092
8629
  }
8093
8630
  function listAdapters() { return ADAPTER_NAMES.slice(); }
8094
8631
  function adapt(context, adapterName, opts = {}) {
@@ -8877,108 +9414,1551 @@ __factories["./src/discovery/r-manifest"] = function(module, exports) {
8877
9414
  }
8878
9415
  };
8879
9416
 
8880
- // ── ./src/workspace/detector ──
8881
- __factories["./src/workspace/detector"] = function(module, exports) {
8882
- 'use strict';
8883
- const fs = require('fs');
8884
- const path = require('path');
8885
- module.exports = { detectWorkspaces, inferPackage, scopeToPackage };
9417
+ // ── ./src/workspace/detector ──
9418
+ __factories["./src/workspace/detector"] = function(module, exports) {
9419
+ 'use strict';
9420
+ const fs = require('fs');
9421
+ const path = require('path');
9422
+ module.exports = { detectWorkspaces, inferPackage, scopeToPackage };
9423
+
9424
+ function detectWorkspaces(cwd) {
9425
+ const pkgPath = path.join(cwd, 'package.json');
9426
+ if (!fs.existsSync(pkgPath)) return [];
9427
+
9428
+ let pkg;
9429
+ try {
9430
+ pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
9431
+ } catch {
9432
+ return [];
9433
+ }
9434
+
9435
+ const patterns = pkg.workspaces || [];
9436
+ const dirs = [];
9437
+
9438
+ // Handle both flat array and object with packages field (Yarn v2 format)
9439
+ const patternArray = Array.isArray(patterns) ? patterns : (patterns.packages || []);
9440
+
9441
+ for (const p of patternArray) {
9442
+ const base = p.replace(/\/\*\*?$/, '');
9443
+ const resolved = path.join(cwd, base);
9444
+ if (fs.existsSync(resolved)) {
9445
+ try {
9446
+ for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
9447
+ if (entry.isDirectory()) dirs.push(path.join(resolved, entry.name));
9448
+ }
9449
+ } catch (_) {}
9450
+ }
9451
+ }
9452
+
9453
+ return dirs;
9454
+ }
9455
+
9456
+ // Infer package from query tokens: "add rate limiting to payments" → "packages/payments"
9457
+ function inferPackage(query, workspaceDirs, cwd) {
9458
+ const tokens = query.toLowerCase().split(/\W+/).filter(t => t.length > 2);
9459
+
9460
+ // Find longest matching package name
9461
+ let bestMatch = null;
9462
+ let bestLen = 0;
9463
+ let bestMatchLen = 0;
9464
+
9465
+ for (const dir of workspaceDirs) {
9466
+ const name = path.basename(dir).toLowerCase();
9467
+ for (const token of tokens) {
9468
+ const matchLen = _getMatchLength(name, token);
9469
+ // Only consider matches; use longest match, and break ties by longest package name
9470
+ if (matchLen > 0 && (matchLen > bestLen || (matchLen === bestLen && name.length > bestMatchLen))) {
9471
+ bestMatch = dir;
9472
+ bestLen = matchLen;
9473
+ bestMatchLen = name.length;
9474
+ }
9475
+ }
9476
+ }
9477
+
9478
+ return bestMatch;
9479
+ }
9480
+
9481
+ function _getMatchLength(name, token) {
9482
+ if (name === token) return 1000 + name.length; // Exact match is best
9483
+ if (name.startsWith(token) && token.length >= 3) return 100 + token.length;
9484
+ if (token.startsWith(name) && name.length >= 3) return name.length;
9485
+ return 0;
9486
+ }
9487
+
9488
+ // Return boost multiplier for files inside the inferred package
9489
+ function scopeToPackage(filePath, packageDir) {
9490
+ const normalized = filePath.replace(/\\/g, '/');
9491
+ const normalizedPkg = packageDir.replace(/\\/g, '/');
9492
+
9493
+ // Ensure we match the directory boundary, not just a prefix
9494
+ // e.g., packages/payment should not match packages/payment-old
9495
+ if (normalized.startsWith(normalizedPkg)) {
9496
+ const afterPrefix = normalized.slice(normalizedPkg.length);
9497
+ // Check if next char is / or if it's the exact match
9498
+ if (afterPrefix === '' || afterPrefix[0] === '/') {
9499
+ return 0.30;
9500
+ }
9501
+ }
9502
+ return 0;
9503
+ }
9504
+ };
9505
+
9506
+ /**
9507
+ * SigMap — gen-context.js v1.2.0
9508
+ * Zero-dependency AI context engine.
9509
+ * Runs with: node gen-context.js
9510
+ * No npm install required. Node 18+ built-ins only.
9511
+ */
9512
+
9513
+ // ── ./src/format/usage-guidance ──
9514
+ __factories["./src/format/usage-guidance"] = function(module, exports) {
9515
+ 'use strict';
9516
+
9517
+ /**
9518
+ * Canonical "how to use SigMap" guidance block (v6.16/v7.0).
9519
+ *
9520
+ * Every adapter emits this one identical block so all generated context files
9521
+ * (CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, GEMINI.md, .cursorrules,
9522
+ * …) carry the same, single usage section — instead of each adapter inventing
9523
+ * its own wording (and codex emitting a redundant second JSON block).
9524
+ */
9525
+
9526
+ function usageBlock() {
9527
+ return [
9528
+ '## SigMap commands',
9529
+ '',
9530
+ '| When | Command |',
9531
+ '|------|---------|',
9532
+ '| Before answering a question about code | `sigmap ask "<your question>"` |',
9533
+ '| To rank files by topic | `sigmap --query "<topic>"` |',
9534
+ '| After changing config or source dirs | `sigmap validate` |',
9535
+ '| To verify an AI answer is grounded | `sigmap judge --response <file>` |',
9536
+ '',
9537
+ 'Always run `sigmap ask` (or `sigmap --query`) before searching for files relevant to a task.',
9538
+ '',
9539
+ ].join('\n');
9540
+ }
9541
+
9542
+ module.exports = { usageBlock };
9543
+
9544
+ };
9545
+
9546
+ // ── ./src/squeeze/classify ──
9547
+ __factories["./src/squeeze/classify"] = function(module, exports) {
9548
+ 'use strict';
9549
+
9550
+ /**
9551
+ * Squeeze input classifier (v7.0.0).
9552
+ *
9553
+ * Deterministic, zero-dep detector that labels a pasted blob as a
9554
+ * `stacktrace`, `cilog`, or `json` payload — or `null` (pass through). Pure
9555
+ * regex/heuristics; runs in well under 10ms even on large input.
9556
+ *
9557
+ * Order matters: stack traces are highest value and a CI log often *contains*
9558
+ * a trace, so stacktrace is checked first, then cilog, then json.
9559
+ */
9560
+
9561
+ const FRAME_RE = [
9562
+ /^\s*at\s+.+\(.+:\d+:\d+\)\s*$/, // JS: at fn (file:line:col)
9563
+ /^\s*at\s+.+:\d+:\d+\s*$/, // JS: at file:line:col
9564
+ /^\s*at\s+[\w$.<>]+\(.+\.\w+:\d+\)/, // Java/Kotlin: at pkg.Cls.m(File.java:42)
9565
+ /^\s*File\s+".+",\s+line\s+\d+/, // Python frame
9566
+ /^\s+\w+.*\([^)]*\.(go|rs):\d+\)/, // Go/Rust frame with file:line
9567
+ /^\s*#\d+\s+0x[0-9a-f]+/, // native/gdb frame
9568
+ ];
9569
+
9570
+ const STACK_HEADER_RE = [
9571
+ /Traceback \(most recent call last\)/,
9572
+ /Exception in thread/,
9573
+ /\bat Object\.<anonymous>\b/,
9574
+ /thread '.*' panicked at/,
9575
+ /^panic:/m,
9576
+ /goroutine \d+ \[/,
9577
+ ];
9578
+
9579
+ function countFrames(lines) {
9580
+ let n = 0;
9581
+ for (const line of lines) {
9582
+ if (FRAME_RE.some((re) => re.test(line))) n++;
9583
+ }
9584
+ return n;
9585
+ }
9586
+
9587
+ function matchesStackTrace(input, lines) {
9588
+ const frames = countFrames(lines);
9589
+ const header = STACK_HEADER_RE.some((re) => re.test(input));
9590
+ // A single explicit header (Traceback/panic) counts as strong evidence even
9591
+ // with few parsed frames; otherwise require 2+ frame-like lines.
9592
+ if (!header && frames < 2) return { match: false, confidence: 0, frames };
9593
+ // Confidence scales with frame count; a header alone floors it at 0.6.
9594
+ let confidence = Math.min(0.97, 0.15 * frames);
9595
+ if (header) confidence = Math.max(confidence, 0.6 + Math.min(0.3, 0.05 * frames));
9596
+ return { match: true, confidence: Number(confidence.toFixed(2)), frames };
9597
+ }
9598
+
9599
+ 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})/;
9600
+ const PROGRESS_RE = /(\d{1,3}%)|(\bDownloading\b)|(\bnpm (WARN|notice|http)\b)|(##\[(group|endgroup|command)\])|(\[\d+\/\d+\])|(▕|█|━|⠿)|(\bETA\b)|(\r$)/;
9601
+
9602
+ function matchesCiLog(input, lines) {
9603
+ if (lines.length < 8) return { match: false, confidence: 0 };
9604
+ let logish = 0;
9605
+ const seen = new Map();
9606
+ let repeats = 0;
9607
+ for (const line of lines) {
9608
+ if (TS_RE.test(line) || PROGRESS_RE.test(line)) logish++;
9609
+ const norm = line.replace(/\d+/g, '#').trim();
9610
+ if (norm) {
9611
+ const c = (seen.get(norm) || 0) + 1;
9612
+ seen.set(norm, c);
9613
+ if (c > 1) repeats++;
9614
+ }
9615
+ }
9616
+ const density = logish / lines.length;
9617
+ const repeatRatio = repeats / lines.length;
9618
+ if (density < 0.4 && repeatRatio < 0.4) return { match: false, confidence: 0 };
9619
+ const confidence = Math.min(0.95, Math.max(density, repeatRatio) + 0.15);
9620
+ return { match: true, confidence: Number(confidence.toFixed(2)) };
9621
+ }
9622
+
9623
+ function matchesJsonPayload(input) {
9624
+ const trimmed = input.trim();
9625
+ if (/^[[{]/.test(trimmed)) {
9626
+ try {
9627
+ JSON.parse(trimmed);
9628
+ return { match: true, confidence: 0.95 };
9629
+ } catch (_) { /* not strict JSON — fall through to heuristic */ }
9630
+ }
9631
+ const lines = trimmed.split('\n');
9632
+ if (lines.length < 4) return { match: false, confidence: 0 };
9633
+ let kv = 0;
9634
+ for (const line of lines) {
9635
+ if (/^\s*"[^"]+"\s*:\s*.+/.test(line) || /^\s*[}\]],?\s*$/.test(line)) kv++;
9636
+ }
9637
+ const ratio = kv / lines.length;
9638
+ if (ratio < 0.6) return { match: false, confidence: 0 };
9639
+ return { match: true, confidence: Number(Math.min(0.9, ratio).toFixed(2)) };
9640
+ }
9641
+
9642
+ /**
9643
+ * @param {string} input
9644
+ * @returns {{ category: 'stacktrace'|'cilog'|'json'|null, confidence: number }}
9645
+ */
9646
+ function classify(input) {
9647
+ if (typeof input !== 'string' || !input.trim()) return { category: null, confidence: 0 };
9648
+ const lines = input.split('\n');
9649
+
9650
+ const st = matchesStackTrace(input, lines);
9651
+ if (st.match) return { category: 'stacktrace', confidence: st.confidence };
9652
+
9653
+ const ci = matchesCiLog(input, lines);
9654
+ if (ci.match) return { category: 'cilog', confidence: ci.confidence };
9655
+
9656
+ const js = matchesJsonPayload(input);
9657
+ if (js.match) return { category: 'json', confidence: js.confidence };
9658
+
9659
+ return { category: null, confidence: 0 };
9660
+ }
9661
+
9662
+ module.exports = { classify, countFrames };
9663
+
9664
+ };
9665
+
9666
+ // ── ./src/squeeze/cilog ──
9667
+ __factories["./src/squeeze/cilog"] = function(module, exports) {
9668
+ 'use strict';
9669
+
9670
+ /**
9671
+ * CI / build-log squeeze (v7.0.0).
9672
+ *
9673
+ * Strips timestamps, progress bars, and repeated noise; keeps every error line
9674
+ * plus a small context window around it. Never returns empty — when there are
9675
+ * no errors it falls back to a head/tail summary. Also reused by the stacktrace
9676
+ * squeezer to clean noise surrounding a trace.
9677
+ */
9678
+
9679
+ 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*)+/;
9680
+ 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[: ]|█|━|▕|⣿)/;
9681
+ const ERROR_RE = /\b(?:error|err!|fail(?:ed|ure)?|exception|panic|fatal|traceback|ERR_|E[A-Z]{3,})\b|✗|❌/i;
9682
+
9683
+ /** Remove a leading timestamp prefix from a single line. */
9684
+ function stripTimestamp(line) {
9685
+ return line.replace(TS_PREFIX_RE, '');
9686
+ }
9687
+
9688
+ /**
9689
+ * @param {string} input
9690
+ * @param {object} [opts]
9691
+ * @param {number} [opts.context=2] context lines kept around each error
9692
+ * @returns {{ squeezed: string, kept: string[], stripped: string[] }}
9693
+ */
9694
+ function squeezeCiLog(input, opts = {}) {
9695
+ const ctx = opts.context != null ? opts.context : 2;
9696
+ const lines = input.split('\n');
9697
+ const keep = new Set();
9698
+ const errorIdx = [];
9699
+
9700
+ for (let i = 0; i < lines.length; i++) {
9701
+ if (ERROR_RE.test(lines[i])) {
9702
+ errorIdx.push(i);
9703
+ for (let j = Math.max(0, i - ctx); j <= Math.min(lines.length - 1, i + ctx); j++) keep.add(j);
9704
+ }
9705
+ }
9706
+
9707
+ let body;
9708
+ let keptDesc;
9709
+ if (errorIdx.length === 0) {
9710
+ const head = lines.slice(0, 10).map(stripTimestamp);
9711
+ const tail = lines.length > 20 ? lines.slice(-10).map(stripTimestamp) : [];
9712
+ body = head.slice();
9713
+ if (tail.length) body.push(`… (${lines.length - 20} lines omitted) …`, ...tail);
9714
+ keptDesc = ['head/tail summary (no error lines found)'];
9715
+ } else {
9716
+ const sorted = [...keep].sort((a, b) => a - b);
9717
+ body = [];
9718
+ let prev = -2;
9719
+ for (const idx of sorted) {
9720
+ // Drop pure progress/noise lines unless they are themselves errors.
9721
+ const line = stripTimestamp(lines[idx]);
9722
+ if (PROGRESS_LINE_RE.test(line) && !ERROR_RE.test(line)) continue;
9723
+ if (idx > prev + 1) body.push(`… (${idx - prev - 1} lines) …`);
9724
+ body.push(line);
9725
+ prev = idx;
9726
+ }
9727
+ if (body.length === 0) body = errorIdx.map((i) => stripTimestamp(lines[i])); // safety net
9728
+ keptDesc = [`${errorIdx.length} error line(s) + ${ctx}-line context`];
9729
+ }
9730
+
9731
+ return {
9732
+ squeezed: body.join('\n'),
9733
+ kept: keptDesc,
9734
+ stripped: [`${Math.max(0, lines.length - body.length)} timestamp/progress/noise line(s)`],
9735
+ };
9736
+ }
9737
+
9738
+ module.exports = { squeezeCiLog, stripTimestamp, ERROR_RE };
9739
+
9740
+ };
9741
+
9742
+ // ── ./src/squeeze/jsonpayload ──
9743
+ __factories["./src/squeeze/jsonpayload"] = function(module, exports) {
9744
+ 'use strict';
9745
+
9746
+ /**
9747
+ * JSON-payload squeeze (v7.0.0).
9748
+ *
9749
+ * Collapses repeated array elements, truncates long string values, and
9750
+ * preserves the schema shape at every depth — so an LLM still sees the
9751
+ * structure of an API/GraphQL/validation error without the bulk.
9752
+ */
9753
+
9754
+ const MAX_STR = 500;
9755
+ const ARRAY_KEEP = 2;
9756
+
9757
+ function squeezeValue(v, opts) {
9758
+ const maxStr = opts.maxStr;
9759
+ const keep = opts.arrayKeep;
9760
+ if (Array.isArray(v)) {
9761
+ if (v.length <= keep + 1) return v.map((x) => squeezeValue(x, opts));
9762
+ const head = v.slice(0, keep).map((x) => squeezeValue(x, opts));
9763
+ head.push(`…${v.length - keep} more similar items`);
9764
+ return head;
9765
+ }
9766
+ if (v && typeof v === 'object') {
9767
+ const o = {};
9768
+ for (const k of Object.keys(v)) o[k] = squeezeValue(v[k], opts);
9769
+ return o;
9770
+ }
9771
+ if (typeof v === 'string' && v.length > maxStr) {
9772
+ return v.slice(0, maxStr) + `…(${v.length} chars)`;
9773
+ }
9774
+ return v;
9775
+ }
9776
+
9777
+ /**
9778
+ * @param {string} input
9779
+ * @param {object} [opts]
9780
+ * @param {number} [opts.maxStr=500] truncate strings longer than this
9781
+ * @param {number} [opts.arrayKeep=2] array items kept before collapsing
9782
+ * @returns {{ squeezed, kept, stripped }}
9783
+ */
9784
+ function squeezeJsonPayload(input, opts = {}) {
9785
+ let parsed;
9786
+ try { parsed = JSON.parse(input); }
9787
+ catch (_) { return { squeezed: input, kept: ['(not valid JSON — unchanged)'], stripped: [] }; }
9788
+ const cfg = { maxStr: opts.maxStr != null ? opts.maxStr : MAX_STR, arrayKeep: opts.arrayKeep != null ? opts.arrayKeep : ARRAY_KEEP };
9789
+ const squeezed = JSON.stringify(squeezeValue(parsed, cfg), null, 2);
9790
+ return {
9791
+ squeezed,
9792
+ kept: ['schema shape preserved at all depths'],
9793
+ stripped: ['collapsed repeated array items; truncated long string values'],
9794
+ };
9795
+ }
9796
+
9797
+ module.exports = { squeezeJsonPayload, squeezeValue };
9798
+
9799
+ };
9800
+
9801
+ // ── ./src/squeeze/stacktrace ──
9802
+ __factories["./src/squeeze/stacktrace"] = function(module, exports) {
9803
+ 'use strict';
9804
+
9805
+ /**
9806
+ * Stack-trace squeeze (v7.0.0) — the highest-value squeeze module.
9807
+ *
9808
+ * Dedupes repeated exceptions, strips vendor frames, keeps frames in the user's
9809
+ * own source dirs, and — the differentiator — **enriches the top kept frame**
9810
+ * with its real signature from the SigMap symbol index (`buildSigIndex`).
9811
+ * Generic log summarizers can't do this; SigMap has the repo's symbol map.
9812
+ *
9813
+ * Pure/deterministic. The symbol index is injected via `opts.symbolIndex` so
9814
+ * the module is unit-testable without touching the filesystem.
9815
+ */
9816
+
9817
+ const path = require('path');
9818
+
9819
+ const VENDOR_RE = /(?:^|[\\/])(?:node_modules|vendor|site-packages|dist|build|\.venv|venv|third_party|external|\.cargo|go\/pkg\/mod)[\\/]/;
9820
+
9821
+ /** Parse a frame line across JS/TS, Python, Java/Kotlin, Go, Rust, native. */
9822
+ function parseFrame(line) {
9823
+ let m;
9824
+ if ((m = line.match(/^\s*at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/))) return { fn: m[1], file: m[2], line: +m[3], raw: line };
9825
+ if ((m = line.match(/^\s*at\s+(.+?):(\d+):(\d+)\s*$/))) return { fn: '', file: m[1], line: +m[2], raw: line };
9826
+ if ((m = line.match(/^\s*at\s+([\w$.<>]+)\((.+?):(\d+)\)/))) return { fn: m[1], file: m[2], line: +m[3], raw: line };
9827
+ 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 };
9828
+ if ((m = line.match(/^\s*(.+\.(?:go|rs)):(\d+)/))) return { fn: '', file: m[1], line: +m[2], raw: line };
9829
+ return null;
9830
+ }
9831
+
9832
+ function isVendor(file) { return VENDOR_RE.test(String(file).replace(/\\/g, '/')); }
9833
+
9834
+ function inSrcDirs(file, srcDirs) {
9835
+ const f = String(file).replace(/\\/g, '/');
9836
+ return srcDirs.some((d) => {
9837
+ const dd = String(d).replace(/^\.\//, '').replace(/\/$/, '');
9838
+ return dd && (f === dd || f.startsWith(dd + '/') || f.includes('/' + dd + '/'));
9839
+ });
9840
+ }
9841
+
9842
+ /** Look up the real signature for a frame in the SigMap symbol index. */
9843
+ function enrichFrame(frame, symbolIndex) {
9844
+ if (!symbolIndex || !frame) return null;
9845
+ const want = String(frame.file).replace(/\\/g, '/');
9846
+ const base = path.basename(want);
9847
+ let key = null;
9848
+ for (const k0 of symbolIndex.keys()) {
9849
+ const k = String(k0).replace(/\\/g, '/');
9850
+ if (k === want || want.endsWith('/' + k) || k.endsWith('/' + want)) { key = k0; break; }
9851
+ if (!key && path.basename(k) === base) key = k0;
9852
+ }
9853
+ if (!key) return null;
9854
+ const sigs = symbolIndex.get(key) || [];
9855
+ const wantFn = frame.fn ? frame.fn.split('.').pop() : '';
9856
+ let byLine = null, byName = null;
9857
+ for (const sig of sigs) {
9858
+ const s = String(sig);
9859
+ const mm = s.match(/:(\d+)(?:-(\d+))?\s*$/);
9860
+ if (mm) {
9861
+ const a = +mm[1], b = mm[2] ? +mm[2] : a;
9862
+ if (frame.line >= a && frame.line <= b) byLine = s;
9863
+ }
9864
+ if (wantFn && new RegExp('\\b' + wantFn.replace(/[^\w$]/g, '') + '\\b').test(s)) byName = byName || s;
9865
+ }
9866
+ const sig = byLine || byName;
9867
+ return sig ? { file: key, sig: sig.replace(/\s*:\d+(?:-\d+)?\s*$/, '').trim() } : null;
9868
+ }
9869
+
9870
+ /**
9871
+ * @param {string} input
9872
+ * @param {object} [opts]
9873
+ * @param {string[]} [opts.srcDirs] user source dirs (default ['src'])
9874
+ * @param {Map} [opts.symbolIndex] SigMap signature index for enrichment
9875
+ * @param {number} [opts.maxFrames=8] cap on kept source frames
9876
+ * @returns {{ squeezed, kept, stripped, enriched }}
9877
+ */
9878
+ function squeezeStackTrace(input, opts = {}) {
9879
+ const srcDirs = (opts.srcDirs && opts.srcDirs.length) ? opts.srcDirs : ['src'];
9880
+ const maxFrames = opts.maxFrames != null ? opts.maxFrames : 8;
9881
+ const lines = input.split('\n');
9882
+
9883
+ const headerCount = new Map();
9884
+ const headerOrder = [];
9885
+ const frames = [];
9886
+ for (const line of lines) {
9887
+ const f = parseFrame(line);
9888
+ if (f) { frames.push(f); continue; }
9889
+ const t = line.trim();
9890
+ if (!t) continue;
9891
+ if (!headerCount.has(t)) headerOrder.push(t);
9892
+ headerCount.set(t, (headerCount.get(t) || 0) + 1);
9893
+ }
9894
+
9895
+ const seen = new Set();
9896
+ let dupFrames = 0;
9897
+ const nonVendor = [];
9898
+ const sourceFrames = [];
9899
+ let vendorCount = 0;
9900
+ for (const f of frames) {
9901
+ const k = f.file + ':' + f.line;
9902
+ if (seen.has(k)) { dupFrames++; continue; }
9903
+ seen.add(k);
9904
+ if (isVendor(f.file)) { vendorCount++; continue; }
9905
+ nonVendor.push(f);
9906
+ if (inSrcDirs(f.file, srcDirs)) sourceFrames.push(f);
9907
+ }
9908
+
9909
+ // Prefer source frames; never return empty (fall back to top non-vendor, then raw).
9910
+ const shown = sourceFrames.length ? sourceFrames.slice(0, maxFrames)
9911
+ : (nonVendor.length ? nonVendor.slice(0, 3) : frames.slice(0, 3));
9912
+
9913
+ const enrichment = shown.length ? enrichFrame(shown[0], opts.symbolIndex) : null;
9914
+
9915
+ const out = [];
9916
+ for (const h of headerOrder) {
9917
+ const n = headerCount.get(h);
9918
+ out.push(n > 1 ? `${h} (occurred ×${n})` : h);
9919
+ }
9920
+ for (let i = 0; i < shown.length; i++) {
9921
+ out.push(' ' + shown[i].raw.trim());
9922
+ if (i === 0 && enrichment) out.push(` ↳ ${enrichment.sig} [${enrichment.file}]`);
9923
+ }
9924
+
9925
+ return {
9926
+ squeezed: out.join('\n'),
9927
+ kept: [
9928
+ `${headerOrder.length} unique exception(s)`,
9929
+ `top ${shown.length} ${sourceFrames.length ? 'source ' : ''}frame(s)`,
9930
+ ...(enrichment ? [`enriched ${path.basename(shown[0].file)}:${shown[0].line}`] : []),
9931
+ ],
9932
+ stripped: [`${vendorCount} vendor frame(s)`, `${dupFrames} duplicate frame(s)`],
9933
+ enriched: !!enrichment,
9934
+ };
9935
+ }
9936
+
9937
+ module.exports = { squeezeStackTrace, parseFrame, isVendor, inSrcDirs, enrichFrame };
9938
+
9939
+ };
9940
+
9941
+ // ── ./src/squeeze/index ──
9942
+ __factories["./src/squeeze/index"] = function(module, exports) {
9943
+ 'use strict';
9944
+
9945
+ /**
9946
+ * Squeeze orchestrator (v7.0.0): classify → squeeze → reduction → decision.
9947
+ *
9948
+ * Always-on and silent: callers run `squeeze()` on pasted input, then use
9949
+ * `shouldPrompt()` to decide whether the reduction is worth interrupting for.
9950
+ * Everything is deterministic and offline; the symbol index for stack-trace
9951
+ * enrichment is passed through via `opts.symbolIndex`.
9952
+ */
9953
+
9954
+ const { classify } = __require('./src/squeeze/classify');
9955
+ const { squeezeStackTrace } = __require('./src/squeeze/stacktrace');
9956
+ const { squeezeCiLog } = __require('./src/squeeze/cilog');
9957
+ const { squeezeJsonPayload } = __require('./src/squeeze/jsonpayload');
9958
+
9959
+ function estimateTokens(s) { return Math.ceil(String(s || '').length / 4); }
9960
+
9961
+ /**
9962
+ * @param {string} input
9963
+ * @param {object} [opts] forwarded to the category squeezer (srcDirs, symbolIndex, …)
9964
+ * @returns {{ category, confidence, original, squeezed, rawTokens, squeezedTokens, reduction, kept, stripped, enriched, applies }}
9965
+ */
9966
+ function squeeze(input, opts = {}) {
9967
+ const { category, confidence } = classify(input);
9968
+ const rawTokens = estimateTokens(input);
9969
+ const base = {
9970
+ category, confidence, original: input, squeezed: input,
9971
+ rawTokens, squeezedTokens: rawTokens, reduction: 0,
9972
+ kept: [], stripped: [], enriched: false, applies: false,
9973
+ };
9974
+ if (!category) return base;
9975
+
9976
+ let r;
9977
+ if (category === 'stacktrace') r = squeezeStackTrace(input, opts);
9978
+ else if (category === 'cilog') r = squeezeCiLog(input, opts);
9979
+ else r = squeezeJsonPayload(input, opts);
9980
+
9981
+ const squeezedTokens = estimateTokens(r.squeezed);
9982
+ const reduction = rawTokens > 0 ? (rawTokens - squeezedTokens) / rawTokens : 0;
9983
+ return {
9984
+ category, confidence,
9985
+ original: input, squeezed: r.squeezed,
9986
+ rawTokens, squeezedTokens,
9987
+ reduction: Math.max(0, Number(reduction.toFixed(4))),
9988
+ kept: r.kept || [], stripped: r.stripped || [], enriched: !!r.enriched,
9989
+ applies: squeezedTokens < rawTokens,
9990
+ };
9991
+ }
9992
+
9993
+ /** True when the reduction clears the threshold (accepts 0–1 or 0–100). */
9994
+ function shouldPrompt(reduction, threshold) {
9995
+ const t = threshold > 1 ? threshold / 100 : threshold;
9996
+ return reduction >= t;
9997
+ }
9998
+
9999
+ /** A compact human summary of what squeeze would do (for the prompt). */
10000
+ function formatSummary(result) {
10001
+ const pct = Math.round(result.reduction * 100);
10002
+ const lines = [
10003
+ `Input: ${result.rawTokens.toLocaleString()} tokens`,
10004
+ `Can reduce to ${result.squeezedTokens.toLocaleString()} tokens (${pct}% smaller):`,
10005
+ ];
10006
+ for (const k of result.kept) lines.push(` ✓ Kept: ${k}`);
10007
+ for (const s of result.stripped) lines.push(` ✗ Stripped: ${s}`);
10008
+ return lines.join('\n');
10009
+ }
10010
+
10011
+ module.exports = { squeeze, shouldPrompt, formatSummary, estimateTokens };
10012
+
10013
+ };
10014
+
10015
+ // ── ./src/nudge ──
10016
+ __factories["./src/nudge"] = function(module, exports) {
10017
+ 'use strict';
10018
+
10019
+ /**
10020
+ * Star nudge + usage tracking (v7.0.0).
10021
+ *
10022
+ * Records run counts in `.context/usage.json` and shows a one-time GitHub-star
10023
+ * message after the tool has been genuinely useful (≥10 runs, ≥8 successes).
10024
+ * Shown exactly once per machine — even under concurrent runs (an `wx` lock
10025
+ * file makes the show race-safe). Wired into `ask` (and the `squeeze` path).
10026
+ */
10027
+
10028
+ const fs = require('fs');
10029
+ const path = require('path');
10030
+ const os = require('os');
10031
+ const crypto = require('crypto');
10032
+
10033
+ const RUN_THRESHOLD = 10;
10034
+ const SUCCESS_THRESHOLD = 8;
10035
+
10036
+ function usagePath(cwd) { return path.join(cwd, '.context', 'usage.json'); }
10037
+
10038
+ function defaultUsage() {
10039
+ return {
10040
+ totalRuns: 0, successfulRuns: 0, squeezeOffered: 0, squeezeAccepted: 0,
10041
+ starNudgeShown: false, machineId: '', firstRunDate: null, lastRunDate: null,
10042
+ };
10043
+ }
10044
+
10045
+ function readUsage(cwd) {
10046
+ try { return { ...defaultUsage(), ...JSON.parse(fs.readFileSync(usagePath(cwd), 'utf8')) }; }
10047
+ catch (_) { return defaultUsage(); }
10048
+ }
10049
+
10050
+ function writeUsageAtomic(cwd, usage) {
10051
+ const p = usagePath(cwd);
10052
+ fs.mkdirSync(path.dirname(p), { recursive: true });
10053
+ const tmp = `${p}.${process.pid}.tmp`;
10054
+ fs.writeFileSync(tmp, JSON.stringify(usage, null, 2));
10055
+ fs.renameSync(tmp, p); // atomic on POSIX
10056
+ }
10057
+
10058
+ const STAR_MESSAGE = [
10059
+ '─────────────────────────────────────────────────────────',
10060
+ ' SigMap has helped you 10 times now.',
10061
+ '',
10062
+ " If it's been useful, a GitHub star takes 5 seconds and",
10063
+ ' helps other developers find it:',
10064
+ ' → github.com/manojmallick/sigmap',
10065
+ '',
10066
+ " (Won't ask again. Press Enter to continue.)",
10067
+ '─────────────────────────────────────────────────────────',
10068
+ ].join('\n');
10069
+
10070
+ function showStarNudge(write) {
10071
+ (write || ((s) => process.stderr.write(s)))('\n' + STAR_MESSAGE + '\n');
10072
+ }
10073
+
10074
+ /**
10075
+ * Record one run and, when the thresholds are first met, show the star nudge.
10076
+ * @param {string} cwd
10077
+ * @param {boolean} runSuccess
10078
+ * @param {object} [opts]
10079
+ * @param {boolean} [opts.silent] record only — never print
10080
+ * @param {function} [opts.write] sink for the message (default stderr)
10081
+ * @param {string} [opts.today] override date (testing)
10082
+ * @param {object} [opts.bump] counter deltas to merge (e.g. { squeezeAccepted: 1 })
10083
+ * @returns {{ usage, nudged }}
10084
+ */
10085
+ function checkStarNudge(cwd, runSuccess, opts = {}) {
10086
+ const usage = readUsage(cwd);
10087
+ usage.totalRuns += 1;
10088
+ if (runSuccess) usage.successfulRuns += 1;
10089
+ if (opts.bump) for (const k of Object.keys(opts.bump)) usage[k] = (usage[k] || 0) + opts.bump[k];
10090
+
10091
+ const today = opts.today || new Date().toISOString().slice(0, 10);
10092
+ if (!usage.firstRunDate) usage.firstRunDate = today;
10093
+ usage.lastRunDate = today;
10094
+ if (!usage.machineId) {
10095
+ try { usage.machineId = 'sha256-' + crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 16); } catch (_) {}
10096
+ }
10097
+
10098
+ let nudged = false;
10099
+ if (!usage.starNudgeShown && usage.totalRuns >= RUN_THRESHOLD && usage.successfulRuns >= SUCCESS_THRESHOLD) {
10100
+ // Race-safe single-show: only the process that creates the lock prints.
10101
+ let won = false;
10102
+ try { fs.closeSync(fs.openSync(usagePath(cwd) + '.nudge.lock', 'wx')); won = true; }
10103
+ catch (_) { won = false; }
10104
+ if (won && !opts.silent) showStarNudge(opts.write);
10105
+ nudged = won;
10106
+ usage.starNudgeShown = true;
10107
+ }
10108
+
10109
+ writeUsageAtomic(cwd, usage);
10110
+ return { usage, nudged };
10111
+ }
10112
+
10113
+ module.exports = { checkStarNudge, readUsage, usagePath, showStarNudge, RUN_THRESHOLD, SUCCESS_THRESHOLD };
10114
+
10115
+ };
10116
+
10117
+ // ── ./src/verify/parsers ──
10118
+ __factories["./src/verify/parsers"] = function(module, exports) {
10119
+ 'use strict';
10120
+
10121
+ /**
10122
+ * Parsers for the Hallucination Guard (verify-ai-output).
10123
+ *
10124
+ * Extract the verifiable claims an AI answer makes about a codebase:
10125
+ * - file paths it references
10126
+ * - import / require statements it shows
10127
+ * - function / class symbols it calls
10128
+ * - fenced code blocks (so callers can scope checks to code vs prose)
10129
+ *
10130
+ * Everything here is deterministic and offline — pure string analysis.
10131
+ */
10132
+
10133
+ // Extensions we are confident name a source/code/config file (no slash required).
10134
+ const KNOWN_CODE_EXT = new Set([
10135
+ 'js', 'jsx', 'mjs', 'cjs', 'ts', 'tsx', 'py', 'pyw', 'rb', 'go', 'rs',
10136
+ 'java', 'kt', 'swift', 'c', 'h', 'cpp', 'hpp', 'cs', 'php', 'r',
10137
+ 'vue', 'svelte', 'css', 'scss', 'less', 'html', 'json', 'yml', 'yaml',
10138
+ 'toml', 'xml', 'sql', 'graphql', 'gql', 'proto', 'tf', 'md', 'sh',
10139
+ 'gd', 'gdscript',
10140
+ ]);
10141
+
10142
+ /**
10143
+ * Extract fenced code blocks.
10144
+ * @param {string} text
10145
+ * @returns {{ lang: string, content: string, line: number }[]}
10146
+ */
10147
+ function extractCodeBlocks(text) {
10148
+ const blocks = [];
10149
+ const lines = text.split('\n');
10150
+ let inBlock = false;
10151
+ let lang = '';
10152
+ let buf = [];
10153
+ let startLine = 0;
10154
+ for (let i = 0; i < lines.length; i++) {
10155
+ const m = lines[i].match(/^```(\w*)/);
10156
+ if (m) {
10157
+ if (!inBlock) {
10158
+ inBlock = true;
10159
+ lang = m[1] || '';
10160
+ buf = [];
10161
+ startLine = i + 2; // first content line (1-based)
10162
+ } else {
10163
+ blocks.push({ lang, content: buf.join('\n'), line: startLine });
10164
+ inBlock = false;
10165
+ }
10166
+ continue;
10167
+ }
10168
+ if (inBlock) buf.push(lines[i]);
10169
+ }
10170
+ return blocks;
10171
+ }
10172
+
10173
+ /**
10174
+ * Extract file-path references (deduped, first-seen line kept).
10175
+ * A token counts as a path when it has a `.<letter…>` extension AND
10176
+ * either contains a `/` or carries a known code/config extension.
10177
+ * @param {string} text
10178
+ * @returns {{ path: string, line: number }[]}
10179
+ */
10180
+ function extractFilePaths(text) {
10181
+ const lines = text.split('\n');
10182
+ const seen = new Map();
10183
+ const re = /(?:^|[\s`"'(\[<])([A-Za-z0-9_][\w./-]*\.[A-Za-z][A-Za-z0-9]*)/g;
10184
+ for (let i = 0; i < lines.length; i++) {
10185
+ const line = lines[i];
10186
+ let m;
10187
+ re.lastIndex = 0;
10188
+ while ((m = re.exec(line)) !== null) {
10189
+ const p = m[1];
10190
+ if (/^https?:/i.test(p)) continue;
10191
+ const ext = (p.split('.').pop() || '').toLowerCase();
10192
+ const hasSlash = p.includes('/');
10193
+ if (!hasSlash && !KNOWN_CODE_EXT.has(ext)) continue;
10194
+ if (!seen.has(p)) seen.set(p, i + 1);
10195
+ }
10196
+ }
10197
+ return [...seen.entries()].map(([p, line]) => ({ path: p, line }));
10198
+ }
10199
+
10200
+ /**
10201
+ * Extract import / require statements.
10202
+ * @param {string} text
10203
+ * @returns {{ module: string, kind: 'js'|'py', relative: boolean, line: number, raw: string }[]}
10204
+ */
10205
+ function extractImports(text) {
10206
+ const lines = text.split('\n');
10207
+ const out = [];
10208
+ const push = (module, kind, line, raw) => {
10209
+ if (!module) return;
10210
+ out.push({ module, kind, relative: /^[./]/.test(module), line, raw: raw.trim() });
10211
+ };
10212
+ for (let i = 0; i < lines.length; i++) {
10213
+ const line = lines[i];
10214
+ let m;
10215
+ // JS/TS: import ... from 'x' | export ... from 'x'
10216
+ if ((m = line.match(/\b(?:import|export)\b[^'"]*\bfrom\s*['"]([^'"]+)['"]/))) {
10217
+ push(m[1], 'js', i + 1, line);
10218
+ } else if ((m = line.match(/\bimport\s*['"]([^'"]+)['"]/))) {
10219
+ // side-effect import 'x'
10220
+ push(m[1], 'js', i + 1, line);
10221
+ }
10222
+ // require('x') / dynamic import('x') — may co-occur, scan separately
10223
+ const reqRe = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
10224
+ let r;
10225
+ while ((r = reqRe.exec(line)) !== null) push(r[1], 'js', i + 1, line);
10226
+
10227
+ // TS: import X = require('mod')
10228
+ if ((m = line.match(/\bimport\s+[A-Za-z_$][\w$]*\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/))) {
10229
+ push(m[1], 'js', i + 1, line);
10230
+ }
10231
+
10232
+ // Python: from x import y | import x
10233
+ if ((m = line.match(/^\s*from\s+([.\w]+)\s+import\b/))) {
10234
+ push(m[1], 'py', i + 1, line);
10235
+ } else if ((m = line.match(/^\s*import\s+([A-Za-z_][\w.]*)/))) {
10236
+ push(m[1], 'py', i + 1, line);
10237
+ }
10238
+ }
10239
+
10240
+ // Multi-line JS/TS imports, e.g.
10241
+ // import {
10242
+ // A as B,
10243
+ // } from './mod';
10244
+ // The per-line pass above misses these because `from '…'` sits on a later
10245
+ // line. Trigger only when the opening line has no quote and no `from` yet,
10246
+ // then gather forward until the source string appears.
10247
+ for (let i = 0; i < lines.length; i++) {
10248
+ const start = lines[i];
10249
+ if (!/^\s*(?:import|export)\b/.test(start)) continue;
10250
+ if (/['"]/.test(start) || /\bfrom\b/.test(start)) continue; // single-line, already handled
10251
+ let joined = start;
10252
+ for (let j = i + 1; j < Math.min(lines.length, i + 12); j++) {
10253
+ joined += ' ' + lines[j];
10254
+ const fm = joined.match(/\bfrom\s*['"]([^'"]+)['"]/);
10255
+ if (fm) { push(fm[1], 'js', i + 1, start.trim()); break; }
10256
+ if (/['"]/.test(lines[j]) && !/\bfrom\b/.test(joined)) break; // a string that isn't a source — bail
10257
+ }
10258
+ }
10259
+ return out;
10260
+ }
10261
+
10262
+ /**
10263
+ * Extract npm/pnpm/yarn script invocations (`npm run <name>`).
10264
+ * Only the explicit `run` form is matched, to avoid confusing package-manager
10265
+ * subcommands (`yarn add`, `pnpm install`) with script names.
10266
+ * @param {string} text
10267
+ * @returns {{ name: string, line: number }[]}
10268
+ */
10269
+ function extractNpmScripts(text) {
10270
+ const lines = text.split('\n');
10271
+ const out = [];
10272
+ const seen = new Set();
10273
+ const re = /\b(?:npm|pnpm|yarn)\s+run(?:-script)?\s+([A-Za-z0-9:_-]+)/g;
10274
+ for (let i = 0; i < lines.length; i++) {
10275
+ let m;
10276
+ re.lastIndex = 0;
10277
+ while ((m = re.exec(lines[i])) !== null) {
10278
+ const name = m[1];
10279
+ if (seen.has(name)) continue;
10280
+ seen.add(name);
10281
+ out.push({ name, line: i + 1 });
10282
+ }
10283
+ }
10284
+ return out;
10285
+ }
10286
+
10287
+ /**
10288
+ * Extract function/class symbol references that look like calls.
10289
+ * Restricted to backtick-wrapped calls (`foo(...)`) for high precision.
10290
+ * @param {string} text
10291
+ * @returns {{ name: string, line: number }[]}
10292
+ */
10293
+ function extractSymbols(text) {
10294
+ const lines = text.split('\n');
10295
+ const out = [];
10296
+ const seen = new Set();
10297
+ const re = /`([A-Za-z_$][\w$]*)\s*\([^`]*\)`/g;
10298
+ for (let i = 0; i < lines.length; i++) {
10299
+ let m;
10300
+ re.lastIndex = 0;
10301
+ while ((m = re.exec(lines[i])) !== null) {
10302
+ const name = m[1];
10303
+ const key = name + '@' + (i + 1);
10304
+ if (seen.has(key)) continue;
10305
+ seen.add(key);
10306
+ out.push({ name, line: i + 1 });
10307
+ }
10308
+ }
10309
+ return out;
10310
+ }
10311
+
10312
+ module.exports = {
10313
+ extractCodeBlocks,
10314
+ extractFilePaths,
10315
+ extractImports,
10316
+ extractSymbols,
10317
+ extractNpmScripts,
10318
+ };
10319
+
10320
+ };
10321
+
10322
+ // ── ./src/verify/closest-match ──
10323
+ __factories["./src/verify/closest-match"] = function(module, exports) {
10324
+ 'use strict';
10325
+
10326
+ /**
10327
+ * Closest-match suggestions for the Hallucination Guard (v6.15.0).
10328
+ *
10329
+ * Heuristic layer (plan §5 labels this "Medium" confidence): given a name the
10330
+ * detectors flagged as fake, find the nearest *real* candidate by Levenshtein
10331
+ * distance so the report can say "Did you mean `loadConfig()` in
10332
+ * src/config/loader.js:42?". Pure, deterministic, offline — no network, no LLM.
10333
+ *
10334
+ * All inputs are passed in (symbol/file/script candidate lists) so this module
10335
+ * stays unit-testable without touching the filesystem or the SigMap index.
10336
+ */
10337
+
10338
+ /**
10339
+ * Levenshtein edit distance with an early-exit ceiling.
10340
+ * Returns `max + 1` as soon as the best achievable distance exceeds `max`,
10341
+ * so callers can cheaply reject far-apart strings.
10342
+ */
10343
+ function levenshtein(a, b, max = Infinity) {
10344
+ if (a === b) return 0;
10345
+ const al = a.length;
10346
+ const bl = b.length;
10347
+ if (al === 0) return bl;
10348
+ if (bl === 0) return al;
10349
+ if (Math.abs(al - bl) > max) return max + 1;
10350
+
10351
+ let prev = new Array(bl + 1);
10352
+ let curr = new Array(bl + 1);
10353
+ for (let j = 0; j <= bl; j++) prev[j] = j;
10354
+
10355
+ for (let i = 1; i <= al; i++) {
10356
+ curr[0] = i;
10357
+ let rowMin = curr[0];
10358
+ const ca = a.charCodeAt(i - 1);
10359
+ for (let j = 1; j <= bl; j++) {
10360
+ const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
10361
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
10362
+ if (curr[j] < rowMin) rowMin = curr[j];
10363
+ }
10364
+ if (rowMin > max) return max + 1;
10365
+ const tmp = prev;
10366
+ prev = curr;
10367
+ curr = tmp;
10368
+ }
10369
+ return prev[bl];
10370
+ }
10371
+
10372
+ /** Bucket a normalized edit distance into a confidence label (plan §5). */
10373
+ function suggestionConfidence(distance, targetLen) {
10374
+ const ratio = distance / Math.max(targetLen, 1);
10375
+ if (distance === 0 || ratio <= 0.2) return 'high';
10376
+ if (ratio <= 0.4) return 'medium';
10377
+ return 'low';
10378
+ }
10379
+
10380
+ /**
10381
+ * Find the nearest candidate name to `target`.
10382
+ *
10383
+ * @param {string} target
10384
+ * @param {Array<string | { name: string, file?: string, line?: number }>} candidates
10385
+ * @param {object} [opts]
10386
+ * @param {number} [opts.maxRatio=0.5] reject matches farther than ratio·len edits
10387
+ * @param {number} [opts.minLen=3] skip very short targets (too noisy)
10388
+ * @returns {{ name, file, line, distance, confidence } | null}
10389
+ */
10390
+ function closestMatch(target, candidates, opts = {}) {
10391
+ const maxRatio = opts.maxRatio != null ? opts.maxRatio : 0.5;
10392
+ const minLen = opts.minLen != null ? opts.minLen : 3;
10393
+ if (!target || target.length < minLen) return null;
10394
+ if (!candidates || candidates.length === 0) return null;
10395
+
10396
+ const lower = target.toLowerCase();
10397
+ const cap = Math.max(1, Math.ceil(target.length * maxRatio));
10398
+ let best = null;
10399
+
10400
+ for (const c of candidates) {
10401
+ const name = typeof c === 'string' ? c : c && c.name;
10402
+ if (!name || name === target) continue;
10403
+ if (Math.abs(name.length - target.length) > cap) continue;
10404
+ const d = levenshtein(lower, name.toLowerCase(), cap);
10405
+ if (d > cap) continue;
10406
+ if (!best || d < best.distance ||
10407
+ (d === best.distance && name.length < best.name.length)) {
10408
+ best = {
10409
+ name,
10410
+ file: typeof c === 'object' ? c.file : undefined,
10411
+ line: typeof c === 'object' ? c.line : undefined,
10412
+ distance: d,
10413
+ };
10414
+ if (d === 0) break; // case-only difference — can't beat it
10415
+ }
10416
+ }
10417
+
10418
+ if (!best) return null;
10419
+ best.confidence = suggestionConfidence(best.distance, target.length);
10420
+ return best;
10421
+ }
10422
+
10423
+ /**
10424
+ * Build `[{ name, file, line }]` symbol candidates from a SigMap signature
10425
+ * index (`Map<file, string[]>` whose entries may carry a `:start-end` anchor).
10426
+ */
10427
+ function buildSymbolCandidates(sigIndex) {
10428
+ const out = [];
10429
+ const seen = new Set();
10430
+ if (!sigIndex) return out;
10431
+ for (const [file, sigs] of sigIndex) {
10432
+ for (const sig of sigs) {
10433
+ const s = String(sig);
10434
+ const lineM = s.match(/:(\d+)(?:-\d+)?\s*$/);
10435
+ const line = lineM ? parseInt(lineM[1], 10) : null;
10436
+ const cleaned = s.replace(/\s*:\d+(?:-\d+)?\s*$/, '');
10437
+ const m = cleaned.match(/\b(?:async\s+function|function|class|def|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/)
10438
+ || cleaned.match(/([A-Za-z_$][\w$]*)\s*\(/)
10439
+ || cleaned.match(/([A-Za-z_$][\w$]*)/);
10440
+ if (!m) continue;
10441
+ const name = m[1];
10442
+ const key = name + '@' + file;
10443
+ if (seen.has(key)) continue;
10444
+ seen.add(key);
10445
+ out.push({ name, file, line });
10446
+ }
10447
+ }
10448
+ return out;
10449
+ }
10450
+
10451
+ /** Format a suggestion object into a human one-liner for reports/CLI. */
10452
+ function formatSuggestion(match, asCall) {
10453
+ if (!match) return null;
10454
+ const sym = asCall ? `${match.name}()` : match.name;
10455
+ let where = '';
10456
+ if (match.file) {
10457
+ where = match.line ? ` in ${match.file}:${match.line}` : ` in ${match.file}`;
10458
+ }
10459
+ return `Did you mean \`${sym}\`${where}?`;
10460
+ }
10461
+
10462
+ module.exports = {
10463
+ levenshtein,
10464
+ closestMatch,
10465
+ buildSymbolCandidates,
10466
+ suggestionConfidence,
10467
+ formatSuggestion,
10468
+ };
10469
+
10470
+ };
10471
+
10472
+ // ── ./src/format/verify-report ──
10473
+ __factories["./src/format/verify-report"] = function(module, exports) {
10474
+ 'use strict';
10475
+
10476
+ /**
10477
+ * Hallucination Guard report view (Surface A, v6.15.0).
10478
+ *
10479
+ * Turns the `verify-ai-output --json` result into a standalone, self-contained
10480
+ * HTML report — red/amber/green per issue, with closest-match suggestions
10481
+ * inline. The visual language deliberately mirrors the planned PR-comment
10482
+ * styling so a single screenshot is reusable across docs and CI (plan proof #5).
10483
+ *
10484
+ * Zero dependencies, inline CSS/SVG, no external assets. Also exports a compact
10485
+ * Markdown renderer for CI / PR comments that shares the same structure.
10486
+ */
8886
10487
 
8887
- function detectWorkspaces(cwd) {
8888
- const pkgPath = path.join(cwd, 'package.json');
8889
- if (!fs.existsSync(pkgPath)) return [];
10488
+ const TYPE_META = {
10489
+ 'fake-file': { label: 'Fake file', tone: 'red', icon: '✕' },
10490
+ 'fake-test-file': { label: 'Fake test file', tone: 'red', icon: '✕' },
10491
+ 'fake-import': { label: 'Fake import', tone: 'red', icon: '✕' },
10492
+ 'fake-npm-script': { label: 'Fake npm script', tone: 'red', icon: '✕' },
10493
+ 'fake-symbol': { label: 'Fake symbol', tone: 'amber', icon: '!' },
10494
+ };
8890
10495
 
8891
- let pkg;
8892
- try {
8893
- pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
8894
- } catch {
8895
- return [];
8896
- }
10496
+ function escapeHtml(value) {
10497
+ return String(value == null ? '' : value)
10498
+ .replace(/&/g, '&amp;')
10499
+ .replace(/</g, '&lt;')
10500
+ .replace(/>/g, '&gt;')
10501
+ .replace(/"/g, '&quot;');
10502
+ }
8897
10503
 
8898
- const patterns = pkg.workspaces || [];
8899
- const dirs = [];
10504
+ function toneFor(issue) {
10505
+ const meta = TYPE_META[issue.type];
10506
+ if (meta) return meta.tone;
10507
+ return issue.confidence === 'high' ? 'red' : 'amber';
10508
+ }
8900
10509
 
8901
- // Handle both flat array and object with packages field (Yarn v2 format)
8902
- const patternArray = Array.isArray(patterns) ? patterns : (patterns.packages || []);
10510
+ function labelFor(issue) {
10511
+ return (TYPE_META[issue.type] && TYPE_META[issue.type].label) || issue.type;
10512
+ }
8903
10513
 
8904
- for (const p of patternArray) {
8905
- const base = p.replace(/\/\*\*?$/, '');
8906
- const resolved = path.join(cwd, base);
8907
- if (fs.existsSync(resolved)) {
8908
- try {
8909
- for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
8910
- if (entry.isDirectory()) dirs.push(path.join(resolved, entry.name));
8911
- }
8912
- } catch (_) {}
8913
- }
8914
- }
10514
+ /**
10515
+ * Render the verify result to a full HTML document.
10516
+ * @param {{ file?: string, issues: object[], summary: object }} result
10517
+ * @param {object} [opts]
10518
+ * @param {string} [opts.title]
10519
+ * @returns {string} HTML
10520
+ */
10521
+ function renderReportHtml(result, opts = {}) {
10522
+ const issues = Array.isArray(result.issues) ? result.issues : [];
10523
+ const summary = result.summary || { total: issues.length, byType: {}, clean: issues.length === 0 };
10524
+ const file = result.file || opts.file || 'AI answer';
10525
+ const title = opts.title || 'SigMap — Hallucination Guard report';
10526
+ const clean = summary.clean || issues.length === 0;
10527
+ const byType = summary.byType || {};
10528
+
10529
+ const chips = Object.keys(TYPE_META)
10530
+ .filter((t) => byType[t])
10531
+ .map((t) => `<span class="chip chip-${TYPE_META[t].tone}">${escapeHtml(TYPE_META[t].label)}: ${byType[t]}</span>`)
10532
+ .join('');
10533
+
10534
+ const banner = clean
10535
+ ? `<div class="banner banner-green"><span class="dot"></span> No hallucinations detected — ${escapeHtml(String(summary.symbolsIndexed || 0))} symbols indexed</div>`
10536
+ : `<div class="banner banner-red"><span class="dot"></span> ${issues.length} issue${issues.length === 1 ? '' : 's'} found in <code>${escapeHtml(file)}</code></div>`;
10537
+
10538
+ const rows = issues.map((issue) => {
10539
+ const tone = toneFor(issue);
10540
+ const sugg = issue.suggestion
10541
+ ? `<div class="suggestion">↳ ${escapeHtml(issue.suggestion)} <span class="conf">heuristic</span></div>`
10542
+ : '';
10543
+ return [
10544
+ `<li class="issue issue-${tone}">`,
10545
+ ` <div class="issue-head">`,
10546
+ ` <span class="badge badge-${tone}">${escapeHtml(labelFor(issue))}</span>`,
10547
+ ` <span class="loc">${escapeHtml(issue.location || ('L' + issue.line))}</span>`,
10548
+ ` <span class="conf conf-${escapeHtml(issue.confidence || 'high')}">${escapeHtml(issue.confidence || 'high')} confidence</span>`,
10549
+ ` </div>`,
10550
+ ` <div class="msg">${escapeHtml(issue.message || issue.value)}</div>`,
10551
+ ` ${sugg}`,
10552
+ `</li>`,
10553
+ ].join('\n');
10554
+ }).join('\n');
10555
+
10556
+ const list = clean
10557
+ ? '<p class="empty">Nothing to report — every file, import, symbol, and script in the answer resolves against the repository.</p>'
10558
+ : `<ul class="issues">${rows}</ul>`;
10559
+
10560
+ return `<!DOCTYPE html>
10561
+ <html lang="en">
10562
+ <head>
10563
+ <meta charset="utf-8">
10564
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10565
+ <title>${escapeHtml(title)}</title>
10566
+ <style>
10567
+ :root { color-scheme: light dark; }
10568
+ * { box-sizing: border-box; }
10569
+ body { font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #0d1117; color: #e6edf3; }
10570
+ .wrap { max-width: 880px; margin: 0 auto; padding: 32px 20px 64px; }
10571
+ h1 { font-size: 20px; margin: 0 0 4px; }
10572
+ .sub { color: #8b949e; margin: 0 0 20px; font-size: 13px; }
10573
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; background: #161b22; padding: 1px 5px; border-radius: 4px; font-size: 12.5px; }
10574
+ .banner { display: flex; align-items: center; gap: 8px; padding: 12px 16px; border-radius: 8px; font-weight: 600; margin-bottom: 16px; }
10575
+ .banner-green { background: rgba(46,160,67,.15); border: 1px solid rgba(46,160,67,.4); color: #3fb950; }
10576
+ .banner-red { background: rgba(248,81,73,.12); border: 1px solid rgba(248,81,73,.4); color: #f85149; }
10577
+ .dot { width: 9px; height: 9px; border-radius: 50%; background: currentColor; }
10578
+ .chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 20px; }
10579
+ .chip { font-size: 12px; padding: 3px 9px; border-radius: 999px; font-weight: 600; }
10580
+ .chip-red { background: rgba(248,81,73,.15); color: #f85149; }
10581
+ .chip-amber { background: rgba(210,153,34,.18); color: #d29922; }
10582
+ ul.issues { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
10583
+ .issue { border: 1px solid #30363d; border-left-width: 4px; border-radius: 8px; padding: 12px 14px; background: #0f141a; }
10584
+ .issue-red { border-left-color: #f85149; }
10585
+ .issue-amber { border-left-color: #d29922; }
10586
+ .issue-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
10587
+ .badge { font-size: 11.5px; font-weight: 700; padding: 2px 8px; border-radius: 5px; text-transform: uppercase; letter-spacing: .03em; }
10588
+ .badge-red { background: rgba(248,81,73,.18); color: #f85149; }
10589
+ .badge-amber { background: rgba(210,153,34,.2); color: #d29922; }
10590
+ .loc { font-family: ui-monospace, monospace; color: #8b949e; font-size: 12px; }
10591
+ .conf { font-size: 11px; color: #8b949e; }
10592
+ .conf-high { color: #f85149; }
10593
+ .conf-medium { color: #d29922; }
10594
+ .msg { margin-top: 7px; font-family: ui-monospace, monospace; font-size: 12.5px; color: #e6edf3; }
10595
+ .suggestion { margin-top: 6px; font-size: 12.5px; color: #3fb950; }
10596
+ .suggestion .conf { margin-left: 6px; }
10597
+ .empty { color: #8b949e; }
10598
+ footer { margin-top: 28px; color: #6e7681; font-size: 12px; }
10599
+ a { color: #58a6ff; }
10600
+ </style>
10601
+ </head>
10602
+ <body>
10603
+ <div class="wrap">
10604
+ <h1>Hallucination Guard report</h1>
10605
+ <p class="sub">Deterministic verification of an AI answer against the real repository — <code>sigmap verify-ai-output</code></p>
10606
+ ${banner}
10607
+ ${chips ? `<div class="chips">${chips}</div>` : ''}
10608
+ ${list}
10609
+ <footer>Generated by <a href="https://github.com/manojmallick/sigmap">SigMap</a> · offline, no LLM · suggestions are heuristic (closest-match)</footer>
10610
+ </div>
10611
+ </body>
10612
+ </html>
10613
+ `;
10614
+ }
8915
10615
 
8916
- return dirs;
10616
+ /** Compact Markdown rendering of the same result (CI / PR comments). */
10617
+ function renderReportMarkdown(result) {
10618
+ const issues = Array.isArray(result.issues) ? result.issues : [];
10619
+ const summary = result.summary || {};
10620
+ const file = result.file || 'AI answer';
10621
+ if (summary.clean || issues.length === 0) {
10622
+ return `### ✅ Hallucination Guard — clean\n\nNo fabricated files, imports, symbols, or scripts in \`${file}\`.`;
10623
+ }
10624
+ const lines = [
10625
+ `### ❌ Hallucination Guard — ${issues.length} issue${issues.length === 1 ? '' : 's'} in \`${file}\``,
10626
+ '',
10627
+ '| Type | Location | Detail | Suggestion |',
10628
+ '| --- | --- | --- | --- |',
10629
+ ];
10630
+ for (const i of issues) {
10631
+ const sugg = i.suggestion ? i.suggestion.replace(/\|/g, '\\|') : '—';
10632
+ lines.push(`| ${labelFor(i)} | ${i.location || ('L' + i.line)} | \`${String(i.value).replace(/\|/g, '\\|')}\` | ${sugg} |`);
8917
10633
  }
10634
+ return lines.join('\n');
10635
+ }
8918
10636
 
8919
- // Infer package from query tokens: "add rate limiting to payments" → "packages/payments"
8920
- function inferPackage(query, workspaceDirs, cwd) {
8921
- const tokens = query.toLowerCase().split(/\W+/).filter(t => t.length > 2);
10637
+ module.exports = { renderReportHtml, renderReportMarkdown, escapeHtml };
8922
10638
 
8923
- // Find longest matching package name
8924
- let bestMatch = null;
8925
- let bestLen = 0;
8926
- let bestMatchLen = 0;
10639
+ };
8927
10640
 
8928
- for (const dir of workspaceDirs) {
8929
- const name = path.basename(dir).toLowerCase();
8930
- for (const token of tokens) {
8931
- const matchLen = _getMatchLength(name, token);
8932
- // Only consider matches; use longest match, and break ties by longest package name
8933
- if (matchLen > 0 && (matchLen > bestLen || (matchLen === bestLen && name.length > bestMatchLen))) {
8934
- bestMatch = dir;
8935
- bestLen = matchLen;
8936
- bestMatchLen = name.length;
8937
- }
10641
+ // ── ./src/verify/hallucination-guard ──
10642
+ __factories["./src/verify/hallucination-guard"] = function(module, exports) {
10643
+ 'use strict';
10644
+
10645
+ /**
10646
+ * Hallucination Guard deterministic core (Reliable MVP, v6.15.0).
10647
+ *
10648
+ * Given the text of an AI answer, flag claims that do not match the repo:
10649
+ * - fake-file : a referenced path is not on disk
10650
+ * - fake-test-file : a referenced *test* path is not on disk (sub-type)
10651
+ * - fake-import : a relative import does not resolve; a bare import is
10652
+ * absent from package.json deps (builtins allow-listed)
10653
+ * - fake-symbol : a called function/class is absent from the symbol index
10654
+ * - fake-npm-script: `npm run X` where X is not a package.json script
10655
+ *
10656
+ * Each issue carries a `confidence` (detection certainty) and, where a near
10657
+ * match exists, a heuristic `suggestion` ("Did you mean …?"). No network, no
10658
+ * LLM. Reuses SigMap primitives (buildSigIndex) but every external dependency
10659
+ * is injectable via `opts` so the core stays unit-testable.
10660
+ */
10661
+
10662
+ const fs = require('fs');
10663
+ const path = require('path');
10664
+ const parsers = __require('./src/verify/parsers');
10665
+ const { closestMatch, buildSymbolCandidates, formatSuggestion } = __require('./src/verify/closest-match');
10666
+
10667
+ // A path that looks like a test file (JS/TS spec/test, Python test_/_test, or
10668
+ // a tests/__tests__ directory). Used to flag fake-test-file separately.
10669
+ const TEST_PATH_RE = /(?:\.(?:test|spec)\.[mc]?[jt]sx?$)|(?:(?:^|\/)__tests__\/)|(?:(?:^|\/)test_[^/]+\.py$)|(?:_test\.py$)|(?:(?:^|\/)tests?\/)/i;
10670
+ function isTestPath(p) { return TEST_PATH_RE.test(p); }
10671
+
10672
+ const NODE_BUILTINS = new Set([
10673
+ 'fs', 'path', 'os', 'util', 'events', 'stream', 'http', 'https', 'crypto',
10674
+ 'child_process', 'url', 'querystring', 'assert', 'zlib', 'readline', 'net',
10675
+ 'tls', 'dns', 'buffer', 'process', 'vm', 'module', 'console', 'timers',
10676
+ 'string_decoder', 'perf_hooks', 'worker_threads', 'cluster', 'dgram', 'v8',
10677
+ 'tty', 'repl', 'async_hooks', 'inspector', 'fs/promises', 'path/posix',
10678
+ ]);
10679
+
10680
+ const PY_BUILTINS = new Set([
10681
+ 'os', 'sys', 're', 'json', 'math', 'typing', 'collections', 'itertools',
10682
+ 'functools', 'datetime', 'pathlib', 'subprocess', 'abc', 'dataclasses',
10683
+ 'enum', 'io', 'time', 'random', 'logging', 'argparse', 'unittest', 'asyncio',
10684
+ 'copy', 'hashlib', 'threading', 'string', 'csv', 'glob', 'shutil', 'tempfile',
10685
+ ]);
10686
+
10687
+ const LANG_GLOBALS = new Set([
10688
+ // JS
10689
+ 'console', 'require', 'module', 'exports', 'process', 'Object', 'Array',
10690
+ 'String', 'Number', 'Boolean', 'Math', 'JSON', 'Date', 'Promise', 'Map',
10691
+ 'Set', 'WeakMap', 'WeakSet', 'RegExp', 'Error', 'Symbol', 'parseInt',
10692
+ 'parseFloat', 'isNaN', 'setTimeout', 'setInterval', 'clearTimeout', 'fetch',
10693
+ 'Buffer', 'Function', 'eval', 'encodeURIComponent', 'decodeURIComponent',
10694
+ // Python
10695
+ 'print', 'len', 'range', 'str', 'int', 'float', 'dict', 'list', 'tuple',
10696
+ 'set', 'bool', 'open', 'enumerate', 'zip', 'map', 'filter', 'sorted',
10697
+ 'sum', 'min', 'max', 'abs', 'isinstance', 'super', 'type', 'getattr',
10698
+ 'setattr', 'hasattr',
10699
+ ]);
10700
+
10701
+ const REL_EXTS = ['', '.js', '.ts', '.tsx', '.jsx', '.mjs', '.cjs', '.json', '.py', '.r', '.R', '.vue'];
10702
+ const REL_INDEX = ['index.js', 'index.ts', 'index.tsx', 'index.jsx', '__init__.py'];
10703
+
10704
+ /**
10705
+ * Build the set of known symbol identifiers from the SigMap signature index,
10706
+ * plus `{ name, file, line }` candidates (for closest-match suggestions).
10707
+ */
10708
+ function buildSymbolSet(cwd) {
10709
+ const set = new Set();
10710
+ let fileKeys = [];
10711
+ let symbolCandidates = [];
10712
+ try {
10713
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
10714
+ const idx = buildSigIndex(cwd);
10715
+ fileKeys = [...idx.keys()];
10716
+ for (const sigs of idx.values()) {
10717
+ for (const sig of sigs) {
10718
+ const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
10719
+ const ids = cleaned.match(/[A-Za-z_$][\w$]*/g) || [];
10720
+ for (const id of ids) set.add(id);
8938
10721
  }
8939
10722
  }
10723
+ symbolCandidates = buildSymbolCandidates(idx);
10724
+ } catch (_) {}
10725
+ return { set, fileKeys, symbolCandidates };
10726
+ }
8940
10727
 
8941
- return bestMatch;
10728
+ /** Load declared dependency names from package.json. */
10729
+ function loadDeps(cwd) {
10730
+ const deps = new Set();
10731
+ let hasPkg = false;
10732
+ try {
10733
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
10734
+ hasPkg = true;
10735
+ for (const k of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
10736
+ if (pkg[k] && typeof pkg[k] === 'object') {
10737
+ for (const name of Object.keys(pkg[k])) deps.add(name);
10738
+ }
10739
+ }
10740
+ } catch (_) {}
10741
+ return { deps, hasPkg };
10742
+ }
10743
+
10744
+ /** Load the set of npm script names declared in package.json. */
10745
+ function loadScripts(cwd) {
10746
+ const scripts = new Set();
10747
+ try {
10748
+ const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
10749
+ if (pkg.scripts && typeof pkg.scripts === 'object') {
10750
+ for (const name of Object.keys(pkg.scripts)) scripts.add(name);
10751
+ }
10752
+ } catch (_) {}
10753
+ return scripts;
10754
+ }
10755
+
10756
+ /** Default file-existence check: resolve a referenced path against cwd. */
10757
+ function defaultFileExists(cwd, ref) {
10758
+ const clean = ref.replace(/^\.\//, '');
10759
+ for (const c of [path.resolve(cwd, clean), path.resolve(cwd, ref)]) {
10760
+ try {
10761
+ if (fs.existsSync(c)) return true;
10762
+ } catch (_) {}
8942
10763
  }
10764
+ return false;
10765
+ }
8943
10766
 
8944
- function _getMatchLength(name, token) {
8945
- if (name === token) return 1000 + name.length; // Exact match is best
8946
- if (name.startsWith(token) && token.length >= 3) return 100 + token.length;
8947
- if (token.startsWith(name) && name.length >= 3) return name.length;
8948
- return 0;
10767
+ /** Default relative-import resolver: fs candidates + basename match in index. */
10768
+ function defaultRelativeResolvable(cwd, mod, fileBasenames) {
10769
+ const base = path.resolve(cwd, mod);
10770
+ for (const e of REL_EXTS) {
10771
+ try {
10772
+ if (fs.existsSync(base + e)) return true;
10773
+ } catch (_) {}
8949
10774
  }
10775
+ for (const idx of REL_INDEX) {
10776
+ try {
10777
+ if (fs.existsSync(path.join(base, idx))) return true;
10778
+ } catch (_) {}
10779
+ }
10780
+ // Fall back to basename match against the indexed file set (the answer's
10781
+ // import is relative to a file we cannot know, so a name match is enough
10782
+ // to avoid false positives).
10783
+ const wantBase = path.basename(mod).replace(/\.[^.]+$/, '').toLowerCase();
10784
+ return fileBasenames.has(wantBase);
10785
+ }
8950
10786
 
8951
- // Return boost multiplier for files inside the inferred package
8952
- function scopeToPackage(filePath, packageDir) {
8953
- const normalized = filePath.replace(/\\/g, '/');
8954
- const normalizedPkg = packageDir.replace(/\\/g, '/');
10787
+ /**
10788
+ * Verify an AI answer against the repository.
10789
+ *
10790
+ * Each issue has the shape:
10791
+ * { type, value, line, location, message, confidence, suggestion }
10792
+ * where `confidence` is the *detection* certainty ('high' for path/dep/script
10793
+ * checks, 'medium' for symbol checks) and `suggestion` is a heuristic
10794
+ * closest-match hint (or null).
10795
+ *
10796
+ * @param {string} answerText
10797
+ * @param {string} cwd
10798
+ * @param {object} [opts]
10799
+ * @param {Set<string>} [opts.symbolSet] override known symbols
10800
+ * @param {Array} [opts.symbolCandidates] override { name, file, line } list
10801
+ * @param {Array<string>} [opts.fileCandidates] override repo file paths (suggestions)
10802
+ * @param {Set<string>} [opts.deps] override package deps
10803
+ * @param {Set<string>} [opts.scripts] override package.json script names
10804
+ * @param {boolean} [opts.hasPkg] whether a package.json exists
10805
+ * @param {(ref: string) => boolean} [opts.fileExists] override file check
10806
+ * @param {(mod: string) => boolean} [opts.relativeResolvable] override rel-import check
10807
+ * @returns {{ issues: object[], summary: object }}
10808
+ */
10809
+ function verify(answerText, cwd, opts = {}) {
10810
+ let symbolSet = opts.symbolSet;
10811
+ let fileBasenames = opts.fileBasenames;
10812
+ let symbolCandidates = opts.symbolCandidates || [];
10813
+ let fileCandidates = opts.fileCandidates || [];
10814
+ if (!symbolSet) {
10815
+ const built = buildSymbolSet(cwd);
10816
+ symbolSet = built.set;
10817
+ fileBasenames = new Set(built.fileKeys.map(
10818
+ (k) => path.basename(k).replace(/\.[^.]+$/, '').toLowerCase()
10819
+ ));
10820
+ symbolCandidates = built.symbolCandidates;
10821
+ fileCandidates = built.fileKeys;
10822
+ }
10823
+ if (!fileBasenames) fileBasenames = new Set();
10824
+
10825
+ let deps = opts.deps;
10826
+ let hasPkg = opts.hasPkg;
10827
+ if (!deps) {
10828
+ const loaded = loadDeps(cwd);
10829
+ deps = loaded.deps;
10830
+ if (hasPkg === undefined) hasPkg = loaded.hasPkg;
10831
+ }
10832
+ const scripts = opts.scripts || (hasPkg ? loadScripts(cwd) : new Set());
10833
+
10834
+ const fileExists = opts.fileExists || ((ref) => defaultFileExists(cwd, ref));
10835
+ const relativeResolvable = opts.relativeResolvable
10836
+ || ((mod) => defaultRelativeResolvable(cwd, mod, fileBasenames));
10837
+
10838
+ // Pre-derive basename candidates for file suggestions (compare on basename so
10839
+ // a wrong directory still surfaces the right file).
10840
+ const fileBasenameCandidates = fileCandidates.map((f) => ({ name: path.basename(f), file: f }));
10841
+
10842
+ const issues = [];
10843
+ const dedupe = new Set();
10844
+ const add = (issue) => {
10845
+ const key = `${issue.type}::${issue.value}`;
10846
+ if (dedupe.has(key)) return;
10847
+ dedupe.add(key);
10848
+ if (!('suggestion' in issue)) issue.suggestion = null;
10849
+ issue.location = `L${issue.line}`;
10850
+ issues.push(issue);
10851
+ };
8955
10852
 
8956
- // Ensure we match the directory boundary, not just a prefix
8957
- // e.g., packages/payment should not match packages/payment-old
8958
- if (normalized.startsWith(normalizedPkg)) {
8959
- const afterPrefix = normalized.slice(normalizedPkg.length);
8960
- // Check if next char is / or if it's the exact match
8961
- if (afterPrefix === '' || afterPrefix[0] === '/') {
8962
- return 0.30;
10853
+ // 1. fake-file / fake-test-file
10854
+ for (const { path: p, line } of parsers.extractFilePaths(answerText)) {
10855
+ if (fileExists(p)) continue;
10856
+ const isTest = isTestPath(p);
10857
+ const match = closestMatch(path.basename(p), fileBasenameCandidates, { minLen: 4 });
10858
+ add({
10859
+ type: isTest ? 'fake-test-file' : 'fake-file',
10860
+ value: p,
10861
+ line,
10862
+ message: `${isTest ? 'Test file' : 'File'} not found on disk: ${p}`,
10863
+ confidence: 'high',
10864
+ suggestion: match ? formatSuggestion(match, false) : null,
10865
+ });
10866
+ }
10867
+
10868
+ // 2. fake-import
10869
+ for (const imp of parsers.extractImports(answerText)) {
10870
+ if (imp.relative) {
10871
+ if (!relativeResolvable(imp.module)) {
10872
+ add({ type: 'fake-import', value: imp.module, line: imp.line, message: `Import does not resolve: ${imp.module}`, confidence: 'high' });
8963
10873
  }
10874
+ continue;
10875
+ }
10876
+ // Bare module — only verifiable for JS when a package.json exists.
10877
+ const top = imp.module.split('/')[0];
10878
+ if (imp.kind === 'js') {
10879
+ if (!hasPkg) continue;
10880
+ if (NODE_BUILTINS.has(imp.module) || NODE_BUILTINS.has(top)) continue;
10881
+ if (top.startsWith('@')) {
10882
+ const scoped = imp.module.split('/').slice(0, 2).join('/');
10883
+ if (deps.has(scoped) || deps.has(imp.module)) continue;
10884
+ } else if (deps.has(top) || deps.has(imp.module)) {
10885
+ continue;
10886
+ }
10887
+ const match = closestMatch(top, [...deps], { minLen: 3 });
10888
+ add({
10889
+ type: 'fake-import',
10890
+ value: imp.module,
10891
+ line: imp.line,
10892
+ message: `Package not in dependencies: ${imp.module}`,
10893
+ confidence: 'high',
10894
+ suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
10895
+ });
10896
+ }
10897
+ // Python bare imports: stdlib is unbounded offline — skip to keep precision.
10898
+ }
10899
+
10900
+ // 3. fake-symbol
10901
+ if (symbolSet.size > 0) {
10902
+ for (const { name, line } of parsers.extractSymbols(answerText)) {
10903
+ if (symbolSet.has(name)) continue;
10904
+ if (LANG_GLOBALS.has(name) || NODE_BUILTINS.has(name) || PY_BUILTINS.has(name)) continue;
10905
+ const match = closestMatch(name, symbolCandidates, { minLen: 4 });
10906
+ add({
10907
+ type: 'fake-symbol',
10908
+ value: name,
10909
+ line,
10910
+ message: `Symbol not found in repo index: ${name}()`,
10911
+ confidence: 'medium',
10912
+ suggestion: match ? formatSuggestion(match, true) : null,
10913
+ });
8964
10914
  }
8965
- return 0;
8966
10915
  }
8967
- };
8968
10916
 
8969
- /**
8970
- * SigMap — gen-context.js v1.2.0
8971
- * Zero-dependency AI context engine.
8972
- * Runs with: node gen-context.js
8973
- * No npm install required. Node 18+ built-ins only.
8974
- */
10917
+ // 4. fake-npm-script
10918
+ if (hasPkg && scripts.size > 0) {
10919
+ for (const { name, line } of parsers.extractNpmScripts(answerText)) {
10920
+ if (scripts.has(name)) continue;
10921
+ const match = closestMatch(name, [...scripts], { minLen: 2 });
10922
+ add({
10923
+ type: 'fake-npm-script',
10924
+ value: name,
10925
+ line,
10926
+ message: `npm script not in package.json: ${name}`,
10927
+ confidence: 'high',
10928
+ suggestion: match ? formatSuggestion({ name: match.name }, false) : null,
10929
+ });
10930
+ }
10931
+ }
10932
+
10933
+ issues.sort((a, b) => a.line - b.line);
10934
+
10935
+ const byType = {
10936
+ 'fake-file': 0, 'fake-test-file': 0, 'fake-import': 0,
10937
+ 'fake-symbol': 0, 'fake-npm-script': 0,
10938
+ };
10939
+ for (const i of issues) byType[i.type] = (byType[i.type] || 0) + 1;
10940
+
10941
+ const summary = {
10942
+ total: issues.length,
10943
+ byType,
10944
+ clean: issues.length === 0,
10945
+ symbolsIndexed: symbolSet.size,
10946
+ withSuggestion: issues.filter((i) => i.suggestion).length,
10947
+ };
10948
+
10949
+ return { issues, summary };
10950
+ }
10951
+
10952
+ module.exports = { verify, buildSymbolSet, loadDeps, loadScripts, isTestPath };
10953
+
10954
+ };
8975
10955
 
8976
10956
  const fs = require('fs');
8977
10957
  const path = require('path');
8978
10958
  const os = require('os');
8979
10959
  const { execSync } = require('child_process');
8980
10960
 
8981
- const VERSION = '6.14.0';
10961
+ const VERSION = '7.0.0';
8982
10962
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
8983
10963
 
8984
10964
  function requireSourceOrBundled(key) {
@@ -9278,29 +11258,17 @@ function applyTokenBudget(fileEntries, maxTokens) {
9278
11258
  let total = renderedTotal(fileEntries);
9279
11259
  if (total <= budgetForEntries) return fileEntries;
9280
11260
 
9281
- // v6.12 Surgical Context progressive disclosure: before dropping whole files,
9282
- // collapse signature BODIES to their line-anchor pointers (keep `symbol :start-end`).
9283
- // Only sigs that actually carry an anchor shrink; the agent can re-fetch bodies via
9284
- // the get_lines MCP tool. This degrades gracefully instead of losing files outright.
9285
- let working = fileEntries;
9286
- const collapsed = fileEntries.map((e) => {
9287
- const slim = (e.sigs || []).map((s) => {
9288
- const line = toIndexLine(s);
9289
- return line && /:\d+-\d+/.test(line) ? line : s; // replace only when it yields an anchor
9290
- });
9291
- return { ...e, sigs: slim };
9292
- });
9293
- const collapsedRendered = renderedTotal(collapsed);
9294
- if (collapsedRendered < total) {
9295
- console.warn(`[sigmap] budget: collapsed bodies to anchors, reclaimed ~${total - collapsedRendered} tokens`);
9296
- working = collapsed;
9297
- total = collapsedRendered;
9298
- // Collapsing keeps every file, so the section overhead must fit too — not just sigs.
9299
- if (total <= budgetForEntries) return working;
9300
- }
9301
-
9302
- // Sort by drop priority (drop first = index 0)
9303
- const withPriority = working.map((e) => {
11261
+ // Over budgetdegrade gracefully, best file first:
11262
+ // 1. keep FULL signatures (params + return type) while they fit;
11263
+ // 2. when a file no longer fits with full sigs, collapse just THAT file to
11264
+ // its line-anchor pointers (still discoverable via buildSigIndex / the
11265
+ // ranker it parses the context file, so an anchored file stays findable);
11266
+ // 3. drop a file only when even the anchor form won't fit.
11267
+ // The important, high-priority files therefore keep their real signatures —
11268
+ // the user-visible value — while only low-priority overflow degrades to
11269
+ // anchors instead of vanishing. (Earlier versions collapsed EVERY file to an
11270
+ // anchor the moment you went 1 token over budget, gutting all signatures.)
11271
+ const withPriority = fileEntries.map((e) => {
9304
11272
  let priority = 0;
9305
11273
  let dropReason = 'budget: low recency';
9306
11274
  if (isGeneratedFile(e.filePath)) { priority = 10; dropReason = 'budget: generated file'; }
@@ -9308,41 +11276,59 @@ function applyTokenBudget(fileEntries, maxTokens) {
9308
11276
  else if (isTestFile(e.filePath)) { priority = 8; dropReason = 'budget: test file'; }
9309
11277
  else if (isConfigFile(e.filePath)) { priority = 6; dropReason = 'budget: config file'; }
9310
11278
  else priority = 4;
9311
- // v4.0: signal quality = sigs per line-of-code (higher = more informative)
9312
11279
  const loc = e.content ? e.content.split('\n').length : 1;
9313
11280
  const signalQuality = loc > 0 ? (e.sigs ? e.sigs.length : 0) / loc : 0;
9314
11281
  return { ...e, priority, dropReason, signalQuality };
9315
11282
  });
9316
11283
 
9317
- // Within same priority: sort by mtime ascending (oldest first), then signalQuality ascending (least informative first)
9318
- withPriority.sort((a, b) => {
9319
- if (b.priority !== a.priority) return b.priority - a.priority;
9320
- if ((a.mtime || 0) !== (b.mtime || 0)) return (a.mtime || 0) - (b.mtime || 0);
9321
- return (a.signalQuality || 0) - (b.signalQuality || 0);
11284
+ // Best-first order: lowest drop-priority, then most-recent, then most-informative.
11285
+ const bestFirst = withPriority.slice().sort((a, b) => {
11286
+ if (a.priority !== b.priority) return a.priority - b.priority;
11287
+ if ((b.mtime || 0) !== (a.mtime || 0)) return (b.mtime || 0) - (a.mtime || 0);
11288
+ return (b.signalQuality || 0) - (a.signalQuality || 0);
9322
11289
  });
9323
11290
 
9324
- const kept = [];
11291
+ const entryCost = (e) => estimateTokens(e.sigs.join('\n')) + sectionOverhead(e);
11292
+ const collapseEntry = (e) => ({
11293
+ ...e,
11294
+ sigs: (e.sigs || []).map((s) => {
11295
+ const line = toIndexLine(s);
11296
+ return line && /:\d+-\d+/.test(line) ? line : s;
11297
+ }),
11298
+ });
11299
+ // Keep FULL signatures for the best files while they fit (signatures are the
11300
+ // value the user reads). When a file's full form no longer fits, fall back to
11301
+ // its anchor form (still discoverable via the ranker, which parses this file);
11302
+ // drop only when even the anchor won't fit. So the important files keep real
11303
+ // signatures and only the lowest-priority overflow degrades — instead of every
11304
+ // signature being gutted to an anchor the moment you go over budget.
11305
+ const finalByPath = new Map();
9325
11306
  const verboseDropped = [];
9326
- // Iterate forward: highest drop-priority files (generated=10, mock=9, test=8) are at index 0.
9327
- // Drop those first until the RENDERED total (sigs + section overhead) fits maxTokens,
9328
- // then keep everything else.
9329
- let rendered = renderedTotal(withPriority);
9330
- for (const entry of withPriority) {
9331
- if (rendered > budgetForEntries) {
9332
- rendered -= estimateTokens(entry.sigs.join('\n')) + sectionOverhead(entry);
9333
- verboseDropped.push({ filePath: entry.filePath, reason: entry.dropReason });
9334
- } else {
9335
- kept.push(entry);
11307
+ let collapsedCount = 0;
11308
+ let used = 0;
11309
+ for (const entry of bestFirst) { // best first
11310
+ if (used + entryCost(entry) <= budgetForEntries) {
11311
+ finalByPath.set(entry.filePath, entry); used += entryCost(entry); continue;
11312
+ }
11313
+ const slim = collapseEntry(entry);
11314
+ if (used + entryCost(slim) <= budgetForEntries) {
11315
+ finalByPath.set(entry.filePath, slim); used += entryCost(slim); collapsedCount++; continue;
9336
11316
  }
11317
+ verboseDropped.push({ filePath: entry.filePath, reason: entry.dropReason });
9337
11318
  }
9338
- if (verboseDropped.length > 0) {
9339
- console.warn(`[sigmap] budget: dropped ${verboseDropped.length} files to stay under ${maxTokens} tokens`);
9340
- // Feature 7: --verbose — print per-file drop reason
11319
+ // Restore the original file order for stable output.
11320
+ const kept = withPriority.filter((e) => finalByPath.has(e.filePath)).map((e) => finalByPath.get(e.filePath));
11321
+
11322
+ if (verboseDropped.length > 0 || collapsedCount > 0) {
11323
+ const parts = [];
11324
+ if (verboseDropped.length) parts.push(`dropped ${verboseDropped.length} file(s)`);
11325
+ if (collapsedCount) parts.push(`collapsed ${collapsedCount} low-priority file(s) to anchors`);
11326
+ console.warn(`[sigmap] budget: ${parts.join(', ')} to stay under ${maxTokens} tokens`);
9341
11327
  if (process.argv.includes('--verbose')) {
9342
11328
  for (const { filePath, reason } of verboseDropped) {
9343
11329
  console.warn(`[sigmap] dropped: ${path.relative(process.cwd(), filePath)} — ${reason}`);
9344
11330
  }
9345
- console.warn(`[sigmap] included: ${kept.length} files, dropped: ${verboseDropped.length}`);
11331
+ console.warn(`[sigmap] included: ${kept.length} files (${collapsedCount} collapsed), dropped: ${verboseDropped.length}`);
9346
11332
  }
9347
11333
  }
9348
11334
  return kept;
@@ -9564,12 +11550,17 @@ function resolveImpactRadius(fileEntries, cwd, config) {
9564
11550
  }
9565
11551
 
9566
11552
  function formatOutput(fileEntries, cwd, routingEnabled, config, extras) {
11553
+ // One canonical usage block for every context file (CLAUDE.md, AGENTS.md,
11554
+ // copilot-instructions.md, …). Lives here — the single source all writers
11555
+ // consume — so adapters don't each invent their own (now-removed) variant.
11556
+ const { usageBlock } = requireSourceOrBundled('./src/format/usage-guidance');
9567
11557
  const lines = [
9568
11558
  '<!-- Generated by SigMap gen-context.js v' + VERSION + ' -->',
9569
11559
  '<!-- DO NOT EDIT below the marker line — run gen-context.js to regenerate -->',
9570
11560
  '',
9571
11561
  '# Code signatures',
9572
11562
  '',
11563
+ usageBlock(),
9573
11564
  ];
9574
11565
 
9575
11566
  // Compact dependency map section (shows import relationships, ~50-100 tokens)
@@ -10737,8 +12728,16 @@ Usage:
10737
12728
  ${cmd} --impact <file> Show every file impacted by changing <file>
10738
12729
  ${cmd} --impact <file> --json Impact as JSON {changed, direct, transitive, tests, routes}
10739
12730
  ${cmd} --impact <file> --depth <n> BFS depth limit (default 3, 0=unlimited)
10740
- ${cmd} verify-ai-output <answer.md> Flag fake files/imports/symbols in an AI answer
10741
- ${cmd} verify-ai-output <answer.md> --json Hallucination report as JSON (exits 1 if issues)
12731
+ ${cmd} verify-ai-output <answer.md> Flag fake files/tests/imports/symbols/npm-scripts in an AI answer
12732
+ ${cmd} verify-ai-output <answer.md> --json Hallucination report as JSON (exits 1 if issues)
12733
+ ${cmd} verify-ai-output <answer.md> --report Write a standalone HTML report (red/amber/green)
12734
+ ${cmd} squeeze <file|-> Minimize a pasted stacktrace/CI-log/JSON blob (--json for stats)
12735
+ ${cmd} ask "<query>" --squeeze Auto-accept input minimization (no prompt; for scripts/CI)
12736
+ ${cmd} ask "<query>" --no-squeeze Disable input minimization entirely
12737
+ ${cmd} ask "<query>" --squeeze-threshold N Min reduction %% to prompt (default 30)
12738
+ ${cmd} note "<text>" Append a note to the cross-session decision log
12739
+ ${cmd} note List recent notes (also: note --list <N>)
12740
+ ${cmd} status Show repo state — branch, dirty files, index freshness, notes
10742
12741
  ${cmd} --init Write example config + .contextignore scaffold
10743
12742
  ${cmd} --help Show this message
10744
12743
  ${cmd} --version Show version
@@ -11124,8 +13123,8 @@ function main() {
11124
13123
  const { loadSession, saveSession, mergeSessionContext } = requireSourceOrBundled('./src/session/memory');
11125
13124
  const { detectWorkspaces, inferPackage, scopeToPackage } = requireSourceOrBundled('./src/workspace/detector');
11126
13125
 
11127
- const intent = detectIntent(query);
11128
- const intentWeights = getIntentWeights(intent);
13126
+ let intent = detectIntent(query);
13127
+ let intentWeights = getIntentWeights(intent);
11129
13128
 
11130
13129
  const sigIndex = buildSigIndex(cwd);
11131
13130
  if (sigIndex.size === 0) {
@@ -11133,6 +13132,44 @@ function main() {
11133
13132
  process.exit(1);
11134
13133
  }
11135
13134
 
13135
+ // v7.0.0: Squeeze — classify and minimize pasted stacktrace/CI-log/JSON
13136
+ // input before ranking. Always runs silently; only prompts (interactive
13137
+ // TTY) when the reduction clears the threshold. Never blocks pipes/CI.
13138
+ let squeezeOffered = false;
13139
+ let squeezeAccepted = false;
13140
+ if (!args.includes('--no-squeeze')) {
13141
+ try {
13142
+ const { squeeze: runSqueeze, shouldPrompt, formatSummary } = requireSourceOrBundled('./src/squeeze/index');
13143
+ const sq = runSqueeze(query, { srcDirs: config.srcDirs, symbolIndex: sigIndex });
13144
+ if (sq.applies && sq.reduction > 0) {
13145
+ squeezeOffered = true;
13146
+ const thrIdx = args.indexOf('--squeeze-threshold');
13147
+ const threshold = thrIdx !== -1 ? parseFloat(args[thrIdx + 1]) : 30;
13148
+ const auto = args.includes('--squeeze');
13149
+ const interactive = !!(process.stdin.isTTY && process.stderr.isTTY) && !args.includes('--json');
13150
+ let accept = false;
13151
+ if (auto) {
13152
+ accept = true;
13153
+ } else if (interactive && shouldPrompt(sq.reduction, threshold)) {
13154
+ process.stderr.write('\n' + formatSummary(sq) + '\n\n');
13155
+ process.stderr.write('Proceed with minimized input? [Y/n] ');
13156
+ const buf = Buffer.alloc(8);
13157
+ try {
13158
+ const n = fs.readSync(0, buf, 0, 8, null);
13159
+ const ans = buf.toString('utf8', 0, n).trim().toLowerCase();
13160
+ accept = ans === '' || ans === 'y' || ans === 'yes';
13161
+ } catch (_) { accept = false; }
13162
+ }
13163
+ if (accept) {
13164
+ query = sq.squeezed;
13165
+ intent = detectIntent(query);
13166
+ intentWeights = getIntentWeights(intent);
13167
+ squeezeAccepted = true;
13168
+ }
13169
+ }
13170
+ } catch (_) { /* squeeze is best-effort — never break ask */ }
13171
+ }
13172
+
11136
13173
  let ranked = rank(query, sigIndex, { topK: 5, weights: intentWeights, cwd });
11137
13174
 
11138
13175
  // v6.10: Workspace scoping — infer package from query and apply boost
@@ -11232,6 +13269,15 @@ function main() {
11232
13269
  bar,
11233
13270
  ].join('\n'));
11234
13271
  }
13272
+ // v7.0.0: record the run and show the one-time star nudge (interactive only).
13273
+ try {
13274
+ const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
13275
+ const showNudge = !!process.stderr.isTTY && !args.includes('--json');
13276
+ checkStarNudge(cwd, true, {
13277
+ silent: !showNudge,
13278
+ bump: { squeezeOffered: squeezeOffered ? 1 : 0, squeezeAccepted: squeezeAccepted ? 1 : 0 },
13279
+ });
13280
+ } catch (_) {}
11235
13281
  process.exit(0);
11236
13282
  }
11237
13283
 
@@ -11860,12 +13906,186 @@ function main() {
11860
13906
  process.exit(0);
11861
13907
  }
11862
13908
 
13909
+ // `sigmap note "<text>"` — append to the cross-session decision log.
13910
+ // With no text, lists recent notes (also `note --list [N]`).
13911
+ if (args[0] === 'note') {
13912
+ const jsonOut = args.includes('--json');
13913
+ const { addNote, readNotes, formatNotes } = requireSourceOrBundled('./src/session/notes');
13914
+ const listIdx = args.indexOf('--list');
13915
+ // Build positionals, skipping value-taking flags and their values
13916
+ // (e.g. `--cwd <dir>`, `--list <N>`) so they never leak into the note text.
13917
+ const VALUE_FLAGS = new Set(['--cwd', '--list']);
13918
+ const positional = [];
13919
+ for (let i = 1; i < args.length; i++) {
13920
+ const a = args[i];
13921
+ if (a.startsWith('--')) { if (VALUE_FLAGS.has(a)) i++; continue; }
13922
+ positional.push(a);
13923
+ }
13924
+ const wantsList = listIdx !== -1 || positional.length === 0;
13925
+
13926
+ if (wantsList) {
13927
+ const limArg = listIdx !== -1 ? parseInt(args[listIdx + 1], 10) : parseInt(positional[0], 10);
13928
+ const limit = Number.isFinite(limArg) && limArg > 0 ? limArg : 10;
13929
+ const notes = readNotes(cwd, limit);
13930
+ if (jsonOut) {
13931
+ process.stdout.write(JSON.stringify({ notes }) + '\n');
13932
+ } else {
13933
+ console.log(`[sigmap] ${notes.length} recent note${notes.length === 1 ? '' : 's'}`);
13934
+ console.log(formatNotes(notes.slice().reverse()));
13935
+ }
13936
+ process.exit(0);
13937
+ }
13938
+
13939
+ const text = positional.join(' ');
13940
+ let entry;
13941
+ try {
13942
+ entry = addNote(cwd, text);
13943
+ } catch (err) {
13944
+ console.error(`[sigmap] ${err.message}`);
13945
+ process.exit(1);
13946
+ }
13947
+ if (jsonOut) {
13948
+ process.stdout.write(JSON.stringify({ added: entry }) + '\n');
13949
+ } else {
13950
+ console.log(`[sigmap] noted${entry.branch ? ` (${entry.branch})` : ''}: ${entry.text}`);
13951
+ }
13952
+ process.exit(0);
13953
+ }
13954
+
13955
+ // `sigmap status` — environment / repo state at a glance.
13956
+ if (args[0] === 'status') {
13957
+ const jsonOut = args.includes('--json');
13958
+ const gitOpts = { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
13959
+ const st = { branch: null, dirty: 0, lastIndex: null, indexVersion: null, indexFiles: null, changedSinceIndex: null, notes: 0, lastNote: null };
13960
+
13961
+ try { st.branch = execSync('git rev-parse --abbrev-ref HEAD', gitOpts).trim() || null; } catch (_) {}
13962
+ // Fallback for an unborn branch (fresh repo, no commits yet).
13963
+ if (!st.branch || st.branch === 'HEAD') {
13964
+ try { st.branch = execSync('git symbolic-ref --short HEAD', gitOpts).trim() || st.branch; } catch (_) {}
13965
+ }
13966
+ try {
13967
+ const porcelain = execSync('git status --porcelain', gitOpts).trim();
13968
+ st.dirty = porcelain ? porcelain.split('\n').filter(Boolean).length : 0;
13969
+ } catch (_) {}
13970
+
13971
+ try {
13972
+ const { readLog } = requireSourceOrBundled('./src/tracking/logger');
13973
+ const log = readLog(cwd);
13974
+ if (log.length) {
13975
+ const last = log[log.length - 1];
13976
+ st.lastIndex = last.ts || null;
13977
+ st.indexVersion = last.version || null;
13978
+ st.indexFiles = last.fileCount != null ? last.fileCount : null;
13979
+ }
13980
+ } catch (_) {}
13981
+
13982
+ // Index freshness: count tracked files modified after the last index run.
13983
+ if (st.lastIndex) {
13984
+ try {
13985
+ const since = Date.parse(st.lastIndex);
13986
+ const tracked = execSync('git ls-files', gitOpts).split('\n').filter(Boolean);
13987
+ let changed = 0;
13988
+ for (const f of tracked.slice(0, 5000)) {
13989
+ try { if (fs.statSync(path.join(cwd, f)).mtimeMs > since) changed++; } catch (_) {}
13990
+ }
13991
+ st.changedSinceIndex = changed;
13992
+ } catch (_) {}
13993
+ }
13994
+
13995
+ try {
13996
+ const { readNotes } = requireSourceOrBundled('./src/session/notes');
13997
+ const notes = readNotes(cwd);
13998
+ st.notes = notes.length;
13999
+ if (notes.length) st.lastNote = notes[notes.length - 1];
14000
+ } catch (_) {}
14001
+
14002
+ if (jsonOut) {
14003
+ process.stdout.write(JSON.stringify(st) + '\n');
14004
+ process.exit(0);
14005
+ }
14006
+
14007
+ const fmtAgo = (iso) => {
14008
+ if (!iso) return 'never';
14009
+ const ms = Date.now() - Date.parse(iso);
14010
+ if (!Number.isFinite(ms)) return iso;
14011
+ const m = Math.floor(ms / 60000), h = Math.floor(m / 60), d = Math.floor(h / 24);
14012
+ if (d > 0) return `${d}d ago`;
14013
+ if (h > 0) return `${h}h ago`;
14014
+ if (m > 0) return `${m}m ago`;
14015
+ return 'just now';
14016
+ };
14017
+
14018
+ console.log('[sigmap] status');
14019
+ console.log(` Branch: ${st.branch || '(not a git repo)'}`);
14020
+ console.log(` Working tree: ${st.dirty === 0 ? 'clean' : `${st.dirty} file${st.dirty === 1 ? '' : 's'} changed`}`);
14021
+ if (st.lastIndex) {
14022
+ let fresh = `${fmtAgo(st.lastIndex)} (v${st.indexVersion || '?'}, ${st.indexFiles != null ? st.indexFiles + ' files' : 'n/a'})`;
14023
+ if (st.changedSinceIndex != null && st.changedSinceIndex > 0) fresh += ` — STALE: ${st.changedSinceIndex} file${st.changedSinceIndex === 1 ? '' : 's'} changed since`;
14024
+ console.log(` Last index: ${fresh}`);
14025
+ } else {
14026
+ console.log(' Last index: never — run: sigmap');
14027
+ }
14028
+ if (st.notes > 0) {
14029
+ console.log(` Notes: ${st.notes} (latest: ${st.lastNote.text.slice(0, 60)}${st.lastNote.text.length > 60 ? '…' : ''})`);
14030
+ } else {
14031
+ console.log(' Notes: none — add with: sigmap note "<text>"');
14032
+ }
14033
+ process.exit(0);
14034
+ }
14035
+
14036
+ // v7.0.0: `sigmap squeeze <file|->` — minimize a pasted stacktrace / CI-log / JSON blob
14037
+ if (args[0] === 'squeeze') {
14038
+ const jsonOut = args.includes('--json');
14039
+ const target = args[1] && !args[1].startsWith('--') ? args[1] : '-';
14040
+ let input = '';
14041
+ try {
14042
+ input = fs.readFileSync(target === '-' ? 0 : path.resolve(cwd, target), 'utf8');
14043
+ } catch (e) {
14044
+ console.error(`[sigmap] cannot read input: ${e.message}`);
14045
+ process.exit(1);
14046
+ }
14047
+
14048
+ const { squeeze: runSqueeze, formatSummary } = requireSourceOrBundled('./src/squeeze/index');
14049
+ let symbolIndex = null;
14050
+ try {
14051
+ const { buildSigIndex } = requireSourceOrBundled('./src/retrieval/ranker');
14052
+ symbolIndex = buildSigIndex(cwd);
14053
+ } catch (_) {}
14054
+ const sq = runSqueeze(input, { srcDirs: config.srcDirs, symbolIndex });
14055
+
14056
+ try {
14057
+ const { checkStarNudge } = requireSourceOrBundled('./src/nudge');
14058
+ checkStarNudge(cwd, true, { silent: !process.stderr.isTTY || jsonOut, bump: { squeezeOffered: sq.applies ? 1 : 0 } });
14059
+ } catch (_) {}
14060
+
14061
+ if (jsonOut) {
14062
+ process.stdout.write(JSON.stringify({
14063
+ category: sq.category, confidence: sq.confidence,
14064
+ rawTokens: sq.rawTokens, squeezedTokens: sq.squeezedTokens,
14065
+ reduction: sq.reduction, enriched: sq.enriched, squeezed: sq.squeezed,
14066
+ }) + '\n');
14067
+ process.exit(0);
14068
+ }
14069
+ if (!sq.category) {
14070
+ process.stderr.write('[sigmap] no squeezable structure detected — input unchanged\n');
14071
+ process.stdout.write(input);
14072
+ process.exit(0);
14073
+ }
14074
+ process.stderr.write(formatSummary(sq) + '\n\n');
14075
+ process.stdout.write(sq.squeezed + '\n');
14076
+ process.exit(0);
14077
+ }
14078
+
11863
14079
  // `sigmap verify-ai-output <answer.md>` — Hallucination Guard (deterministic core)
11864
14080
  if (args[0] === 'verify-ai-output') {
11865
14081
  const target = args[1];
11866
14082
  const jsonOut = args.includes('--json');
14083
+ const reportIdx = args.indexOf('--report');
14084
+ const reportOut = reportIdx !== -1
14085
+ ? (args[reportIdx + 1] && !args[reportIdx + 1].startsWith('--') ? args[reportIdx + 1] : 'sigmap-verify-report.html')
14086
+ : null;
11867
14087
  if (!target || target.startsWith('--')) {
11868
- console.error('[sigmap] Usage: sigmap verify-ai-output <answer.md> [--json]');
14088
+ console.error('[sigmap] Usage: sigmap verify-ai-output <answer.md> [--json] [--report [out.html]]');
11869
14089
  process.exit(1);
11870
14090
  }
11871
14091
  const absTarget = path.resolve(cwd, target);
@@ -11877,24 +14097,39 @@ function main() {
11877
14097
  const answerText = fs.readFileSync(absTarget, 'utf8');
11878
14098
  const { verify } = requireSourceOrBundled('./src/verify/hallucination-guard');
11879
14099
  const { issues, summary } = verify(answerText, cwd);
14100
+ const rel = path.relative(cwd, absTarget) || target;
14101
+ const result = { file: rel, issues, summary };
14102
+
14103
+ // Optional HTML report (Surface A) — written alongside any other output.
14104
+ if (reportOut) {
14105
+ const { renderReportHtml } = requireSourceOrBundled('./src/format/verify-report');
14106
+ const outAbs = path.resolve(cwd, reportOut);
14107
+ fs.writeFileSync(outAbs, renderReportHtml(result));
14108
+ if (!jsonOut) console.log(`[sigmap] report written: ${path.relative(cwd, outAbs) || reportOut}`);
14109
+ }
11880
14110
 
11881
14111
  if (jsonOut) {
11882
- process.stdout.write(JSON.stringify({ file: path.relative(cwd, absTarget) || target, issues, summary }) + '\n');
14112
+ process.stdout.write(JSON.stringify(result) + '\n');
11883
14113
  process.exit(summary.total > 0 ? 1 : 0);
11884
14114
  }
11885
14115
 
11886
- const rel = path.relative(cwd, absTarget) || target;
11887
14116
  if (summary.total === 0) {
11888
14117
  console.log(`[sigmap] ✓ ${rel} — no hallucinations detected (${summary.symbolsIndexed} symbols indexed)`);
11889
14118
  process.exit(0);
11890
14119
  }
11891
14120
 
11892
- const labels = { 'fake-file': 'Fake file', 'fake-import': 'Fake import', 'fake-symbol': 'Fake symbol' };
14121
+ const labels = {
14122
+ 'fake-file': 'Fake file', 'fake-test-file': 'Fake test file',
14123
+ 'fake-import': 'Fake import', 'fake-symbol': 'Fake symbol',
14124
+ 'fake-npm-script': 'Fake npm script',
14125
+ };
14126
+ const bt = summary.byType;
11893
14127
  console.log(`[sigmap] ✗ ${rel} — ${summary.total} issue${summary.total === 1 ? '' : 's'} found`);
11894
- console.log(` fake-file: ${summary.byType['fake-file']} fake-import: ${summary.byType['fake-import']} fake-symbol: ${summary.byType['fake-symbol']}`);
14128
+ console.log(` fake-file: ${bt['fake-file']} fake-test-file: ${bt['fake-test-file']} fake-import: ${bt['fake-import']} fake-symbol: ${bt['fake-symbol']} fake-npm-script: ${bt['fake-npm-script']}`);
11895
14129
  console.log('');
11896
14130
  for (const issue of issues) {
11897
14131
  console.log(` L${issue.line} [${labels[issue.type] || issue.type}] ${issue.message}`);
14132
+ if (issue.suggestion) console.log(` ↳ ${issue.suggestion}`);
11898
14133
  }
11899
14134
  process.exit(1);
11900
14135
  }