sigmap 7.2.1 → 7.4.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
@@ -20,6 +20,122 @@ function __require(key) {
20
20
  }
21
21
 
22
22
 
23
+ // ── ./src/extractors/dispatch ──
24
+ __factories["./src/extractors/dispatch"] = function(module, exports) {
25
+
26
+ /**
27
+ * Bundle-safe extractor dispatch.
28
+ *
29
+ * `packages/core`'s extract() resolves extractors via dynamic `require(path.join(…))`,
30
+ * which cannot run inside the standalone bundle (no filesystem `src/`). This module
31
+ * uses STATIC requires so the bundler rewrites them to `__require` and the extractors
32
+ * resolve from the bundled factories. Used by the live-index MCP write hooks.
33
+ */
34
+
35
+ const path = require('path');
36
+
37
+ // Static language → extractor map (every entry is a bundled factory).
38
+ const EXTRACTORS = {
39
+ typescript: __require('./src/extractors/typescript'),
40
+ typescript_react: __require('./src/extractors/typescript_react'),
41
+ javascript: __require('./src/extractors/javascript'),
42
+ python: __require('./src/extractors/python'),
43
+ java: __require('./src/extractors/java'),
44
+ kotlin: __require('./src/extractors/kotlin'),
45
+ go: __require('./src/extractors/go'),
46
+ rust: __require('./src/extractors/rust'),
47
+ csharp: __require('./src/extractors/csharp'),
48
+ cpp: __require('./src/extractors/cpp'),
49
+ ruby: __require('./src/extractors/ruby'),
50
+ php: __require('./src/extractors/php'),
51
+ swift: __require('./src/extractors/swift'),
52
+ dart: __require('./src/extractors/dart'),
53
+ scala: __require('./src/extractors/scala'),
54
+ gdscript: __require('./src/extractors/gdscript'),
55
+ r: __require('./src/extractors/r'),
56
+ vue: __require('./src/extractors/vue'),
57
+ vue_sfc: __require('./src/extractors/vue_sfc'),
58
+ svelte: __require('./src/extractors/svelte'),
59
+ html: __require('./src/extractors/html'),
60
+ css: __require('./src/extractors/css'),
61
+ yaml: __require('./src/extractors/yaml'),
62
+ shell: __require('./src/extractors/shell'),
63
+ sql: __require('./src/extractors/sql'),
64
+ graphql: __require('./src/extractors/graphql'),
65
+ terraform: __require('./src/extractors/terraform'),
66
+ protobuf: __require('./src/extractors/protobuf'),
67
+ toml: __require('./src/extractors/toml'),
68
+ properties: __require('./src/extractors/properties'),
69
+ xml: __require('./src/extractors/xml'),
70
+ markdown: __require('./src/extractors/markdown'),
71
+ dockerfile: __require('./src/extractors/dockerfile'),
72
+ generic: __require('./src/extractors/generic'),
73
+ };
74
+
75
+ const EXT_MAP = {
76
+ '.ts': 'typescript', '.tsx': 'typescript_react',
77
+ '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
78
+ '.py': 'python', '.pyw': 'python',
79
+ '.java': 'java',
80
+ '.kt': 'kotlin', '.kts': 'kotlin',
81
+ '.go': 'go',
82
+ '.rs': 'rust',
83
+ '.cs': 'csharp',
84
+ '.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
85
+ '.rb': 'ruby', '.rake': 'ruby',
86
+ '.php': 'php',
87
+ '.swift': 'swift',
88
+ '.dart': 'dart',
89
+ '.scala': 'scala', '.sc': 'scala',
90
+ '.gd': 'gdscript',
91
+ '.r': 'r', '.R': 'r',
92
+ '.vue': 'vue_sfc',
93
+ '.svelte': 'svelte',
94
+ '.html': 'html', '.htm': 'html',
95
+ '.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
96
+ '.yml': 'yaml', '.yaml': 'yaml',
97
+ '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
98
+ '.sql': 'sql',
99
+ '.graphql': 'graphql', '.gql': 'graphql',
100
+ '.tf': 'terraform', '.tfvars': 'terraform',
101
+ '.proto': 'protobuf',
102
+ '.toml': 'toml',
103
+ '.properties': 'properties',
104
+ '.xml': 'xml',
105
+ '.md': 'markdown',
106
+ };
107
+
108
+ /** Resolve a language key from a file path/name. */
109
+ function langFor(filePathOrName) {
110
+ const base = path.basename(String(filePathOrName || ''));
111
+ if (base === 'Dockerfile' || base.startsWith('Dockerfile.')) return 'dockerfile';
112
+ const ext = path.extname(base).toLowerCase();
113
+ return EXT_MAP[ext] || null;
114
+ }
115
+
116
+ /**
117
+ * Extract signatures from a file's content using the right extractor.
118
+ * @param {string} filePathOrName - path or name (extension drives the extractor)
119
+ * @param {string} src - file content
120
+ * @returns {string[]}
121
+ */
122
+ function extractFile(filePathOrName, src) {
123
+ if (!src || typeof src !== 'string') return [];
124
+ const lang = langFor(filePathOrName);
125
+ const mod = lang ? EXTRACTORS[lang] : null;
126
+ if (!mod || typeof mod.extract !== 'function') return [];
127
+ try {
128
+ const out = mod.extract(src);
129
+ return Array.isArray(out) ? out : [];
130
+ } catch (_) {
131
+ return [];
132
+ }
133
+ }
134
+
135
+ module.exports = { extractFile, langFor };
136
+
137
+ };
138
+
23
139
  // ── ./src/config/defaults ──
24
140
  __factories["./src/config/defaults"] = function(module, exports) {
25
141
 
@@ -5528,552 +5644,681 @@ __factories["./src/graph/impact"] = function(module, exports) {
5528
5644
 
5529
5645
  // ── ./src/mcp/handlers ──
5530
5646
  __factories["./src/mcp/handlers"] = function(module, exports) {
5531
- 'use strict';
5532
-
5533
- const fs = require('fs');
5534
- const path = require('path');
5535
-
5536
- const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
5537
- const CONTEXT_COLD_FILE = path.join('.github', 'context-cold.md');
5538
-
5539
- function _readContextFiles(cwd) {
5540
- const paths = [path.join(cwd, CONTEXT_FILE), path.join(cwd, CONTEXT_COLD_FILE)];
5541
- const chunks = [];
5542
- for (const p of paths) {
5543
- if (fs.existsSync(p)) chunks.push(fs.readFileSync(p, 'utf8'));
5544
- }
5545
- return chunks.join('\n');
5546
- }
5547
-
5548
- // Section header keywords in PROJECT_MAP.md
5549
- const MAP_SECTIONS = {
5550
- imports: '### Import graph',
5551
- classes: '### Class hierarchy',
5552
- routes: '### Route table',
5553
- };
5554
-
5555
- /**
5556
- * read_context({ module? }) → string
5557
- *
5558
- * Returns the full context file, or just the sections whose file paths
5559
- * contain the given module substring.
5560
- */
5561
- function readContext(args, cwd) {
5562
- const content = _readContextFiles(cwd);
5563
- if (!content) {
5564
- return 'No context file found. Run: node gen-context.js';
5565
- }
5566
-
5567
- if (!args || !args.module) return content;
5647
+
5648
+ const fs = require('fs');
5649
+ const path = require('path');
5650
+ const { git } = __require('./src/util/git');
5568
5651
 
5569
- const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
5570
- const lines = content.split('\n');
5571
- const result = [];
5572
- let capturing = false;
5652
+ const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
5653
+ const CONTEXT_COLD_FILE = path.join('.github', 'context-cold.md');
5573
5654
 
5574
- for (const line of lines) {
5575
- if (line.startsWith('### ')) {
5576
- const filePath = line.slice(4).trim().replace(/\\/g, '/');
5577
- // Match if file path starts with mod or contains /mod/ or /mod
5578
- capturing =
5579
- filePath === mod ||
5580
- filePath.startsWith(mod + '/') ||
5581
- filePath.includes('/' + mod + '/') ||
5582
- filePath.includes('/' + mod);
5583
- if (capturing) result.push(line);
5584
- continue;
5655
+ function _readContextFiles(cwd) {
5656
+ const paths = [path.join(cwd, CONTEXT_FILE), path.join(cwd, CONTEXT_COLD_FILE)];
5657
+ const chunks = [];
5658
+ for (const p of paths) {
5659
+ if (fs.existsSync(p)) chunks.push(fs.readFileSync(p, 'utf8'));
5585
5660
  }
5586
- if (capturing) result.push(line);
5661
+ return chunks.join('\n');
5587
5662
  }
5588
5663
 
5589
- if (result.length === 0) return `No signatures found for module: ${mod}`;
5590
- return result.join('\n');
5591
- }
5592
-
5593
- /**
5594
- * search_signatures({ query }) → string
5595
- *
5596
- * Case-insensitive search through all signature lines.
5597
- * Returns matching lines grouped by file path.
5598
- */
5599
- function searchSignatures(args, cwd) {
5600
- if (!args || !args.query) return 'Missing required argument: query';
5664
+ // Section header keywords in PROJECT_MAP.md
5665
+ const MAP_SECTIONS = {
5666
+ imports: '### Import graph',
5667
+ classes: '### Class hierarchy',
5668
+ routes: '### Route table',
5669
+ };
5601
5670
 
5602
- const query = args.query.toLowerCase();
5603
- try {
5604
- const { buildSigIndex } = __require('./src/retrieval/ranker');
5605
- const index = buildSigIndex(cwd);
5606
- if (index.size === 0) {
5671
+ /**
5672
+ * read_context({ module? }) → string
5673
+ *
5674
+ * Returns the full context file, or just the sections whose file paths
5675
+ * contain the given module substring.
5676
+ */
5677
+ function readContext(args, cwd) {
5678
+ const content = _readContextFiles(cwd);
5679
+ if (!content) {
5607
5680
  return 'No context file found. Run: node gen-context.js';
5608
5681
  }
5609
5682
 
5683
+ if (!args || !args.module) return content;
5684
+
5685
+ const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
5686
+ const lines = content.split('\n');
5610
5687
  const result = [];
5611
- for (const [file, sigs] of index.entries()) {
5612
- const hits = sigs.filter((s) => s.toLowerCase().includes(query));
5613
- if (hits.length === 0) continue;
5614
- if (result.length > 0) result.push('');
5615
- result.push(`### ${file}`);
5616
- result.push(...hits);
5688
+ let capturing = false;
5689
+
5690
+ for (const line of lines) {
5691
+ if (line.startsWith('### ')) {
5692
+ const filePath = line.slice(4).trim().replace(/\\/g, '/');
5693
+ // Match if file path starts with mod or contains /mod/ or /mod
5694
+ capturing =
5695
+ filePath === mod ||
5696
+ filePath.startsWith(mod + '/') ||
5697
+ filePath.includes('/' + mod + '/') ||
5698
+ filePath.includes('/' + mod);
5699
+ if (capturing) result.push(line);
5700
+ continue;
5701
+ }
5702
+ if (capturing) result.push(line);
5617
5703
  }
5618
5704
 
5619
- if (result.length === 0) return `No signatures found matching: ${args.query}`;
5705
+ if (result.length === 0) return `No signatures found for module: ${mod}`;
5620
5706
  return result.join('\n');
5621
- } catch (err) {
5622
- return `_search_signatures failed: ${err.message}_`;
5623
5707
  }
5624
- }
5625
5708
 
5626
- /**
5627
- * get_map({ type }) → string
5628
- *
5629
- * Returns a section from PROJECT_MAP.md.
5630
- * type: 'imports' | 'classes' | 'routes'
5631
- */
5632
- function getMap(args, cwd) {
5633
- if (!args || !args.type) return 'Missing required argument: type';
5709
+ /**
5710
+ * search_signatures({ query }) → string
5711
+ *
5712
+ * Case-insensitive search through all signature lines.
5713
+ * Returns matching lines grouped by file path.
5714
+ */
5715
+ function searchSignatures(args, cwd) {
5716
+ if (!args || !args.query) return 'Missing required argument: query';
5634
5717
 
5635
- const header = MAP_SECTIONS[args.type];
5636
- if (!header) {
5637
- return `Unknown map type: "${args.type}". Use: imports, classes, routes`;
5638
- }
5718
+ const query = args.query.toLowerCase();
5719
+ try {
5720
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
5721
+ const index = buildSigIndex(cwd);
5722
+ if (index.size === 0) {
5723
+ return 'No context file found. Run: node gen-context.js';
5724
+ }
5639
5725
 
5640
- const mapPath = path.join(cwd, 'PROJECT_MAP.md');
5641
- if (!fs.existsSync(mapPath)) {
5642
- return 'PROJECT_MAP.md not found. Run: node gen-project-map.js';
5643
- }
5726
+ const result = [];
5727
+ for (const [file, sigs] of index.entries()) {
5728
+ const hits = sigs.filter((s) => s.toLowerCase().includes(query));
5729
+ if (hits.length === 0) continue;
5730
+ if (result.length > 0) result.push('');
5731
+ result.push(`### ${file}`);
5732
+ result.push(...hits);
5733
+ }
5644
5734
 
5645
- const content = fs.readFileSync(mapPath, 'utf8');
5646
- const idx = content.indexOf(header);
5647
- if (idx === -1) {
5648
- return `Section "${header}" not found in PROJECT_MAP.md`;
5735
+ if (result.length === 0) return `No signatures found matching: ${args.query}`;
5736
+ return result.join('\n');
5737
+ } catch (err) {
5738
+ return `_search_signatures failed: ${err.message}_`;
5739
+ }
5649
5740
  }
5650
5741
 
5651
- // Extract from this header to the next ### header
5652
- const after = content.slice(idx);
5653
- const nextMatch = after.slice(header.length).search(/\n###\s/);
5654
- return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
5655
- }
5742
+ /**
5743
+ * get_map({ type }) → string
5744
+ *
5745
+ * Returns a section from PROJECT_MAP.md.
5746
+ * type: 'imports' | 'classes' | 'routes'
5747
+ */
5748
+ function getMap(args, cwd) {
5749
+ if (!args || !args.type) return 'Missing required argument: type';
5656
5750
 
5657
- /**
5658
- * create_checkpoint({ note? }) → string
5659
- *
5660
- * Returns a markdown checkpoint summarising current project state:
5661
- * - Timestamp and optional user note
5662
- * - Active git branch + last 5 commit messages
5663
- * - Token count of current context file
5664
- * - List of modules present in the context
5665
- * - Route count (if PROJECT_MAP.md exists)
5666
- */
5667
- function createCheckpoint(args, cwd) {
5668
- const note = (args && args.note) ? args.note.trim() : '';
5669
- const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
5670
- const lines = [
5671
- '# SigMap Checkpoint',
5672
- `**Created:** ${now}`,
5673
- ];
5751
+ const header = MAP_SECTIONS[args.type];
5752
+ if (!header) {
5753
+ return `Unknown map type: "${args.type}". Use: imports, classes, routes`;
5754
+ }
5674
5755
 
5675
- if (note) lines.push(`**Note:** ${note}`);
5676
- lines.push('');
5756
+ const mapPath = path.join(cwd, 'PROJECT_MAP.md');
5757
+ if (!fs.existsSync(mapPath)) {
5758
+ return 'PROJECT_MAP.md not found. Run: node gen-project-map.js';
5759
+ }
5677
5760
 
5678
- // ── Git info ────────────────────────────────────────────────────────────
5679
- lines.push('## Git state');
5680
- try {
5681
- const branch = __git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }).trim();
5682
- lines.push(`**Branch:** ${branch}`);
5683
- } catch (_) {
5684
- lines.push('**Branch:** (not a git repo)');
5761
+ const content = fs.readFileSync(mapPath, 'utf8');
5762
+ const idx = content.indexOf(header);
5763
+ if (idx === -1) {
5764
+ return `Section "${header}" not found in PROJECT_MAP.md`;
5765
+ }
5766
+
5767
+ // Extract from this header to the next ### header
5768
+ const after = content.slice(idx);
5769
+ const nextMatch = after.slice(header.length).search(/\n###\s/);
5770
+ return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
5685
5771
  }
5686
5772
 
5687
- try {
5688
- const log = __git(['log', '--oneline', '-5', '--no-decorate'], { cwd }).trim();
5689
- if (log) {
5690
- lines.push('');
5691
- lines.push('**Recent commits:**');
5692
- for (const l of log.split('\n')) lines.push(`- ${l}`);
5773
+ /**
5774
+ * create_checkpoint({ note? }) → string
5775
+ *
5776
+ * Returns a markdown checkpoint summarising current project state:
5777
+ * - Timestamp and optional user note
5778
+ * - Active git branch + last 5 commit messages
5779
+ * - Token count of current context file
5780
+ * - List of modules present in the context
5781
+ * - Route count (if PROJECT_MAP.md exists)
5782
+ */
5783
+ function createCheckpoint(args, cwd) {
5784
+ const note = (args && args.note) ? args.note.trim() : '';
5785
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
5786
+ const lines = [
5787
+ '# SigMap Checkpoint',
5788
+ `**Created:** ${now}`,
5789
+ ];
5790
+
5791
+ if (note) lines.push(`**Note:** ${note}`);
5792
+ lines.push('');
5793
+
5794
+ // ── Git info ────────────────────────────────────────────────────────────
5795
+ lines.push('## Git state');
5796
+ try {
5797
+ const branch = git(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd }).trim();
5798
+ lines.push(`**Branch:** ${branch}`);
5799
+ } catch (_) {
5800
+ lines.push('**Branch:** (not a git repo)');
5693
5801
  }
5694
- } catch (_) {} // ignore — not every project uses git
5695
- lines.push('');
5696
5802
 
5697
- // ── Context stats ────────────────────────────────────────────────────────
5698
- lines.push('## Context snapshot');
5699
- const contextPath = path.join(cwd, CONTEXT_FILE);
5700
- if (fs.existsSync(contextPath)) {
5701
- const content = fs.readFileSync(contextPath, 'utf8');
5702
- const tokens = Math.ceil(content.length / 4);
5803
+ try {
5804
+ const log = git(['log', '--oneline', '-5', '--no-decorate'], { cwd }).trim();
5805
+ if (log) {
5806
+ lines.push('');
5807
+ lines.push('**Recent commits:**');
5808
+ for (const l of log.split('\n')) lines.push(`- ${l}`);
5809
+ }
5810
+ } catch (_) {} // ignore — not every project uses git
5811
+ lines.push('');
5703
5812
 
5704
- // Count modules (### headers are file paths)
5705
- const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
5706
- lines.push(`**Token count:** ~${tokens}`);
5707
- lines.push(`**Modules in context:** ${modules.length}`);
5813
+ // ── Context stats ────────────────────────────────────────────────────────
5814
+ lines.push('## Context snapshot');
5815
+ const contextPath = path.join(cwd, CONTEXT_FILE);
5816
+ if (fs.existsSync(contextPath)) {
5817
+ const content = fs.readFileSync(contextPath, 'utf8');
5818
+ const tokens = Math.ceil(content.length / 4);
5708
5819
 
5709
- if (modules.length > 0) {
5710
- lines.push('');
5711
- lines.push('**Modules:**');
5712
- for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
5713
- if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
5714
- }
5715
- } else {
5716
- lines.push('_No context file found. Run: node gen-context.js_');
5717
- }
5718
- lines.push('');
5719
-
5720
- // ── Route summary ────────────────────────────────────────────────────────
5721
- const mapPath = path.join(cwd, 'PROJECT_MAP.md');
5722
- if (fs.existsSync(mapPath)) {
5723
- const mapContent = fs.readFileSync(mapPath, 'utf8');
5724
- const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
5725
- if (routeLines.length > 0) {
5726
- lines.push('## Routes');
5727
- lines.push(`**Total routes detected:** ${routeLines.length}`);
5728
- lines.push('');
5729
- for (const r of routeLines.slice(0, 10)) lines.push(r);
5730
- if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
5731
- lines.push('');
5820
+ // Count modules (### headers are file paths)
5821
+ const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
5822
+ lines.push(`**Token count:** ~${tokens}`);
5823
+ lines.push(`**Modules in context:** ${modules.length}`);
5824
+
5825
+ if (modules.length > 0) {
5826
+ lines.push('');
5827
+ lines.push('**Modules:**');
5828
+ for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
5829
+ if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
5830
+ }
5831
+ } else {
5832
+ lines.push('_No context file found. Run: node gen-context.js_');
5732
5833
  }
5733
- }
5834
+ lines.push('');
5734
5835
 
5735
- lines.push('---');
5736
- lines.push('_Generated by SigMap `create_checkpoint`_');
5836
+ // ── Route summary ────────────────────────────────────────────────────────
5837
+ const mapPath = path.join(cwd, 'PROJECT_MAP.md');
5838
+ if (fs.existsSync(mapPath)) {
5839
+ const mapContent = fs.readFileSync(mapPath, 'utf8');
5840
+ const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
5841
+ if (routeLines.length > 0) {
5842
+ lines.push('## Routes');
5843
+ lines.push(`**Total routes detected:** ${routeLines.length}`);
5844
+ lines.push('');
5845
+ for (const r of routeLines.slice(0, 10)) lines.push(r);
5846
+ if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
5847
+ lines.push('');
5848
+ }
5849
+ }
5737
5850
 
5738
- return lines.join('\n');
5739
- }
5851
+ lines.push('---');
5852
+ lines.push('_Generated by SigMap `create_checkpoint`_');
5740
5853
 
5741
- /**
5742
- * get_routing({}) → string
5743
- *
5744
- * Reads the current context file, classifies all indexed files by complexity,
5745
- * and returns a formatted markdown routing guide showing which files belong
5746
- * to the fast/balanced/powerful model tier.
5747
- */
5748
- function getRouting(args, cwd) {
5749
- const contextPath = path.join(cwd, CONTEXT_FILE);
5750
- if (!fs.existsSync(contextPath)) {
5751
- return (
5752
- '_No context file found. Run `node gen-context.js --routing` first._\n\n' +
5753
- 'This generates routing hints that map each file to a model tier:\n' +
5754
- '- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
5755
- '- **balanced** (sonnet/gpt-4o) — standard application code\n' +
5756
- '- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
5757
- );
5854
+ return lines.join('\n');
5758
5855
  }
5759
5856
 
5760
- // Parse file list from context (### headings are file paths)
5761
- const content = fs.readFileSync(contextPath, 'utf8');
5762
- const fileRels = content.split('\n')
5763
- .filter((l) => l.startsWith('### '))
5764
- .map((l) => l.slice(4).trim());
5857
+ /**
5858
+ * get_routing({}) string
5859
+ *
5860
+ * Reads the current context file, classifies all indexed files by complexity,
5861
+ * and returns a formatted markdown routing guide showing which files belong
5862
+ * to the fast/balanced/powerful model tier.
5863
+ */
5864
+ function getRouting(args, cwd) {
5865
+ const contextPath = path.join(cwd, CONTEXT_FILE);
5866
+ if (!fs.existsSync(contextPath)) {
5867
+ return (
5868
+ '_No context file found. Run `node gen-context.js --routing` first._\n\n' +
5869
+ 'This generates routing hints that map each file to a model tier:\n' +
5870
+ '- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
5871
+ '- **balanced** (sonnet/gpt-4o) — standard application code\n' +
5872
+ '- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
5873
+ );
5874
+ }
5765
5875
 
5766
- // Build synthetic fileEntries for the classifier
5767
- // We don't have live sig arrays here, so rebuild from the context blocks
5768
- const entries = [];
5769
- const blocks = content.split(/^### /m).slice(1); // slice past the header
5770
- for (const block of blocks) {
5771
- const firstLine = block.split('\n')[0].trim();
5772
- const codeBlock = block.match(/```\n([\s\S]*?)```/);
5773
- const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
5774
- entries.push({ filePath: path.join(cwd, firstLine), sigs });
5775
- }
5876
+ // Parse file list from context (### headings are file paths)
5877
+ const content = fs.readFileSync(contextPath, 'utf8');
5878
+ const fileRels = content.split('\n')
5879
+ .filter((l) => l.startsWith('### '))
5880
+ .map((l) => l.slice(4).trim());
5776
5881
 
5777
- try {
5778
- const { classifyAll } = __require('./src/routing/classifier');
5779
- const { formatRoutingSection } = __require('./src/routing/hints');
5780
- const groups = classifyAll(entries, cwd);
5781
- return formatRoutingSection(groups);
5782
- } catch (err) {
5783
- return `_Routing classification failed: ${err.message}_`;
5882
+ // Build synthetic fileEntries for the classifier
5883
+ // We don't have live sig arrays here, so rebuild from the context blocks
5884
+ const entries = [];
5885
+ const blocks = content.split(/^### /m).slice(1); // slice past the header
5886
+ for (const block of blocks) {
5887
+ const firstLine = block.split('\n')[0].trim();
5888
+ const codeBlock = block.match(/```\n([\s\S]*?)```/);
5889
+ const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
5890
+ entries.push({ filePath: path.join(cwd, firstLine), sigs });
5891
+ }
5892
+
5893
+ try {
5894
+ const { classifyAll } = __require('./src/routing/classifier');
5895
+ const { formatRoutingSection } = __require('./src/routing/hints');
5896
+ const groups = classifyAll(entries, cwd);
5897
+ return formatRoutingSection(groups);
5898
+ } catch (err) {
5899
+ return `_Routing classification failed: ${err.message}_`;
5900
+ }
5784
5901
  }
5785
- }
5786
5902
 
5787
- /**
5788
- * explain_file({ path }) → string
5789
- *
5790
- * Returns a file's signatures, its direct imports, and files that import it.
5791
- * path: relative path from project root (e.g. 'src/services/auth.ts')
5792
- */
5793
- function explainFile(args, cwd) {
5794
- if (!args || !args.path) return 'Missing required argument: path';
5903
+ /**
5904
+ * explain_file({ path }) → string
5905
+ *
5906
+ * Returns a file's signatures, its direct imports, and files that import it.
5907
+ * path: relative path from project root (e.g. 'src/services/auth.ts')
5908
+ */
5909
+ function explainFile(args, cwd) {
5910
+ if (!args || !args.path) return 'Missing required argument: path';
5795
5911
 
5796
- const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
5797
- const targetAbs = path.resolve(cwd, targetRel);
5798
- const contextPath = path.join(cwd, CONTEXT_FILE);
5912
+ const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
5913
+ const targetAbs = path.resolve(cwd, targetRel);
5914
+ const contextPath = path.join(cwd, CONTEXT_FILE);
5799
5915
 
5800
- const lines = ['# explain_file: ' + targetRel, ''];
5916
+ const lines = ['# explain_file: ' + targetRel, ''];
5801
5917
 
5802
- // ── Signatures (hot + cold + cache via buildSigIndex) ───────────────────
5803
- lines.push('## Signatures');
5804
- let indexedFiles = [];
5918
+ // ── Signatures (hot + cold + cache via buildSigIndex) ───────────────────
5919
+ lines.push('## Signatures');
5920
+ let indexedFiles = [];
5805
5921
 
5806
- try {
5807
- const { buildSigIndex } = __require('./src/retrieval/ranker');
5808
- const index = buildSigIndex(cwd);
5809
- let sigs = index.get(targetRel);
5810
- if (!sigs) {
5811
- for (const [file, fileSigs] of index.entries()) {
5812
- if (file === targetRel || file.endsWith('/' + targetRel) || targetRel.endsWith('/' + file)) {
5813
- sigs = fileSigs;
5814
- break;
5922
+ try {
5923
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
5924
+ const index = buildSigIndex(cwd);
5925
+ let sigs = index.get(targetRel);
5926
+ if (!sigs) {
5927
+ for (const [file, fileSigs] of index.entries()) {
5928
+ if (file === targetRel || file.endsWith('/' + targetRel) || targetRel.endsWith('/' + file)) {
5929
+ sigs = fileSigs;
5930
+ break;
5931
+ }
5815
5932
  }
5816
5933
  }
5934
+ if (sigs && sigs.length > 0) {
5935
+ lines.push(...sigs);
5936
+ } else {
5937
+ lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
5938
+ }
5939
+ indexedFiles = [...index.keys()].map((rel) => path.resolve(cwd, rel));
5940
+ } catch (_) {
5941
+ lines.push('_No context file found. Run: node gen-context.js_');
5817
5942
  }
5818
- if (sigs && sigs.length > 0) {
5819
- lines.push(...sigs);
5820
- } else {
5821
- lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
5943
+
5944
+ if (!fs.existsSync(targetAbs)) {
5945
+ lines.push('');
5946
+ lines.push('> File not found on disk: ' + targetRel);
5947
+ return lines.join('\n');
5822
5948
  }
5823
- indexedFiles = [...index.keys()].map((rel) => path.resolve(cwd, rel));
5824
- } catch (_) {
5825
- lines.push('_No context file found. Run: node gen-context.js_');
5826
- }
5827
5949
 
5828
- if (!fs.existsSync(targetAbs)) {
5829
5950
  lines.push('');
5830
- lines.push('> File not found on disk: ' + targetRel);
5831
- return lines.join('\n');
5832
- }
5833
5951
 
5834
- lines.push('');
5835
-
5836
- // ── Direct imports ────────────────────────────────────────────────────────
5837
- lines.push('## Imports (direct dependencies)');
5838
- try {
5839
- const { extractImports } = __require('./src/map/import-graph');
5840
- const fileContent = fs.readFileSync(targetAbs, 'utf8');
5841
- const fileSet = new Set(indexedFiles);
5842
- fileSet.add(targetAbs);
5843
- const imports = extractImports(targetAbs, fileContent, fileSet);
5844
- if (imports.length > 0) {
5845
- for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
5846
- } else {
5847
- lines.push('_No resolvable relative imports found._');
5952
+ // ── Direct imports ────────────────────────────────────────────────────────
5953
+ lines.push('## Imports (direct dependencies)');
5954
+ try {
5955
+ const { extractImports } = __require('./src/map/import-graph');
5956
+ const fileContent = fs.readFileSync(targetAbs, 'utf8');
5957
+ const fileSet = new Set(indexedFiles);
5958
+ fileSet.add(targetAbs);
5959
+ const imports = extractImports(targetAbs, fileContent, fileSet);
5960
+ if (imports.length > 0) {
5961
+ for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
5962
+ } else {
5963
+ lines.push('_No resolvable relative imports found._');
5964
+ }
5965
+ } catch (err) {
5966
+ lines.push('_Could not analyze imports: ' + err.message + '_');
5848
5967
  }
5849
- } catch (err) {
5850
- lines.push('_Could not analyze imports: ' + err.message + '_');
5851
- }
5852
5968
 
5853
- lines.push('');
5969
+ lines.push('');
5854
5970
 
5855
- // ── Callers (reverse-import lookup) ──────────────────────────────────────
5856
- lines.push('## Callers (files that import this file)');
5857
- try {
5858
- const { extractImports } = __require('./src/map/import-graph');
5859
- const fileSet = new Set(indexedFiles);
5860
- fileSet.add(targetAbs);
5861
- const callers = [];
5862
- for (const f of indexedFiles) {
5863
- if (f === targetAbs || !fs.existsSync(f)) continue;
5864
- try {
5865
- const fc = fs.readFileSync(f, 'utf8');
5866
- const imps = extractImports(f, fc, fileSet);
5867
- if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
5868
- } catch (_) {}
5869
- }
5870
- if (callers.length > 0) {
5871
- for (const c of callers) lines.push('- ' + c);
5872
- } else {
5873
- lines.push('_No indexed files import this file._');
5971
+ // ── Callers (reverse-import lookup) ──────────────────────────────────────
5972
+ lines.push('## Callers (files that import this file)');
5973
+ try {
5974
+ const { extractImports } = __require('./src/map/import-graph');
5975
+ const fileSet = new Set(indexedFiles);
5976
+ fileSet.add(targetAbs);
5977
+ const callers = [];
5978
+ for (const f of indexedFiles) {
5979
+ if (f === targetAbs || !fs.existsSync(f)) continue;
5980
+ try {
5981
+ const fc = fs.readFileSync(f, 'utf8');
5982
+ const imps = extractImports(f, fc, fileSet);
5983
+ if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
5984
+ } catch (_) {}
5985
+ }
5986
+ if (callers.length > 0) {
5987
+ for (const c of callers) lines.push('- ' + c);
5988
+ } else {
5989
+ lines.push('_No indexed files import this file._');
5990
+ }
5991
+ } catch (err) {
5992
+ lines.push('_Could not analyze callers: ' + err.message + '_');
5874
5993
  }
5875
- } catch (err) {
5876
- lines.push('_Could not analyze callers: ' + err.message + '_');
5877
- }
5878
5994
 
5879
- return lines.join('\n');
5880
- }
5995
+ return lines.join('\n');
5996
+ }
5881
5997
 
5882
- /**
5883
- * list_modules({}) → string
5884
- *
5885
- * Lists all srcDir modules present in the context file, sorted by token count
5886
- * descending. Helps agents decide which module to query with read_context.
5887
- */
5888
- function listModules(args, cwd) {
5889
- try {
5890
- const { buildSigIndex } = __require('./src/retrieval/ranker');
5891
- const index = buildSigIndex(cwd);
5892
- if (index.size === 0) {
5893
- return 'No context file found. Run: node gen-context.js';
5894
- }
5998
+ /**
5999
+ * list_modules({}) → string
6000
+ *
6001
+ * Lists all srcDir modules present in the context file, sorted by token count
6002
+ * descending. Helps agents decide which module to query with read_context.
6003
+ */
6004
+ function listModules(args, cwd) {
6005
+ try {
6006
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
6007
+ const index = buildSigIndex(cwd);
6008
+ if (index.size === 0) {
6009
+ return 'No context file found. Run: node gen-context.js';
6010
+ }
5895
6011
 
5896
- const groups = {};
5897
- for (const [rel, sigs] of index.entries()) {
5898
- const parts = rel.replace(/\\/g, '/').split('/');
5899
- const mod = parts.length > 1 ? parts[0] : '.';
5900
- if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
5901
- groups[mod].fileCount++;
5902
- groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
5903
- }
6012
+ const groups = {};
6013
+ for (const [rel, sigs] of index.entries()) {
6014
+ const parts = rel.replace(/\\/g, '/').split('/');
6015
+ const mod = parts.length > 1 ? parts[0] : '.';
6016
+ if (!groups[mod]) groups[mod] = { fileCount: 0, tokenCount: 0 };
6017
+ groups[mod].fileCount++;
6018
+ groups[mod].tokenCount += Math.ceil(sigs.join('\n').length / 4);
6019
+ }
5904
6020
 
5905
- const sorted = Object.entries(groups)
5906
- .map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
5907
- .sort((a, b) => b.tokenCount - a.tokenCount);
6021
+ const sorted = Object.entries(groups)
6022
+ .map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
6023
+ .sort((a, b) => b.tokenCount - a.tokenCount);
5908
6024
 
5909
- if (sorted.length === 0) return 'No modules found in context file.';
6025
+ if (sorted.length === 0) return 'No modules found in context file.';
5910
6026
 
5911
- const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
6027
+ const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
5912
6028
 
5913
- return [
5914
- '# Modules',
5915
- '',
5916
- '| Module | Files | Tokens |',
5917
- '|--------|-------|--------|',
5918
- ...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
5919
- '',
5920
- `**Total context tokens: ~${total}**`,
5921
- '',
5922
- '_Use `read_context({ module: "name" })` to get signatures for a specific module._',
5923
- ].join('\n');
5924
- } catch (err) {
5925
- return `_list_modules failed: ${err.message}_`;
6029
+ return [
6030
+ '# Modules',
6031
+ '',
6032
+ '| Module | Files | Tokens |',
6033
+ '|--------|-------|--------|',
6034
+ ...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
6035
+ '',
6036
+ `**Total context tokens: ~${total}**`,
6037
+ '',
6038
+ '_Use `read_context({ module: "name" })` to get signatures for a specific module._',
6039
+ ].join('\n');
6040
+ } catch (err) {
6041
+ return `_list_modules failed: ${err.message}_`;
6042
+ }
5926
6043
  }
5927
- }
5928
-
5929
- /**
5930
- * query_context({ query, topK? }) → string
5931
- *
5932
- * Ranks context-file entries by relevance to the query and returns the
5933
- * top-K most relevant files with their signatures and scores.
5934
- */
5935
- function queryContext(args, cwd) {
5936
- if (!args || !args.query) return 'Missing required argument: query';
5937
6044
 
5938
- try {
5939
- const { rank, buildSigIndex, formatRankTable } = __require('./src/retrieval/ranker');
5940
- const { buildFromCwd } = __require('./src/graph/builder');
5941
- const index = buildSigIndex(cwd);
5942
- if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
5943
-
5944
- const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
5945
- // Build dependency graph for neighbor boost non-fatal if it fails
5946
- let graph = null;
5947
- try { graph = buildFromCwd(cwd); } catch (_) {}
5948
- const results = rank(args.query, index, { topK, cwd, graph });
5949
- return formatRankTable(results, args.query);
5950
- } catch (err) {
5951
- return `_query_context failed: ${err.message}_`;
6045
+ /**
6046
+ * query_context({ query, topK? }) string
6047
+ *
6048
+ * Ranks context-file entries by relevance to the query and returns the
6049
+ * top-K most relevant files with their signatures and scores.
6050
+ */
6051
+ function queryContext(args, cwd) {
6052
+ if (!args || !args.query) return 'Missing required argument: query';
6053
+
6054
+ try {
6055
+ const { rank, buildSigIndex, formatRankTable } = __require('./src/retrieval/ranker');
6056
+ const { buildFromCwd } = __require('./src/graph/builder');
6057
+ const index = buildSigIndex(cwd);
6058
+ if (index.size === 0) return 'No signatures indexed. Run: node gen-context.js';
6059
+
6060
+ const topK = Math.min(Math.max(1, parseInt(args.topK, 10) || 10), 25);
6061
+ // Build dependency graph for neighbor boost — non-fatal if it fails
6062
+ let graph = null;
6063
+ try { graph = buildFromCwd(cwd); } catch (_) {}
6064
+ const results = rank(args.query, index, { topK, cwd, graph });
6065
+ return formatRankTable(results, args.query);
6066
+ } catch (err) {
6067
+ return `_query_context failed: ${err.message}_`;
6068
+ }
5952
6069
  }
5953
- }
5954
6070
 
5955
- /**
5956
- * get_impact({ file, depth? }) → string
5957
- *
5958
- * Returns a formatted markdown impact report for the given file:
5959
- * direct importers, transitive importers, affected tests, affected routes.
5960
- */
5961
- function getImpact(args, cwd) {
5962
- if (!args || !args.file) return 'Missing required argument: file';
6071
+ /**
6072
+ * get_impact({ file, depth? }) → string
6073
+ *
6074
+ * Returns a formatted markdown impact report for the given file:
6075
+ * direct importers, transitive importers, affected tests, affected routes.
6076
+ */
6077
+ function getImpact(args, cwd) {
6078
+ if (!args || !args.file) return 'Missing required argument: file';
5963
6079
 
5964
- try {
5965
- const { analyzeImpact, formatImpact } = __require('./src/graph/impact');
5966
- const depth = Math.max(0, parseInt(args.depth, 10) || 3);
5967
- const results = analyzeImpact(args.file, cwd, { depth });
5968
- if (results.length === 0) return `No impact data for: ${args.file}`;
5969
- return results.map((r) => formatImpact(r.impact)).join('\n\n---\n\n');
5970
- } catch (err) {
5971
- return `_get_impact failed: ${err.message}_`;
6080
+ try {
6081
+ const { analyzeImpact, formatImpact } = __require('./src/graph/impact');
6082
+ const depth = Math.max(0, parseInt(args.depth, 10) || 3);
6083
+ const results = analyzeImpact(args.file, cwd, { depth });
6084
+ if (results.length === 0) return `No impact data for: ${args.file}`;
6085
+ return results.map((r) => formatImpact(r.impact)).join('\n\n---\n\n');
6086
+ } catch (err) {
6087
+ return `_get_impact failed: ${err.message}_`;
6088
+ }
5972
6089
  }
5973
- }
5974
6090
 
5975
- /**
5976
- * get_lines({ file, start, end }) → string
5977
- *
5978
- * Surgical Context demand-driven fetch: returns an exact, clamped line range from a
5979
- * source file. The path is resolved inside the project root (no traversal escape) and
5980
- * the returned lines are secret-scanned via the same redactor used for signatures.
5981
- */
5982
- function getLines(args, cwd) {
5983
- if (!args || !args.file) return 'Missing required argument: file';
6091
+ /**
6092
+ * get_lines({ file, start, end }) → string
6093
+ *
6094
+ * Surgical Context demand-driven fetch: returns an exact, clamped line range from a
6095
+ * source file. The path is resolved inside the project root (no traversal escape) and
6096
+ * the returned lines are secret-scanned via the same redactor used for signatures.
6097
+ */
6098
+ function getLines(args, cwd) {
6099
+ if (!args || !args.file) return 'Missing required argument: file';
5984
6100
 
5985
- const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
5986
- const abs = path.resolve(cwd, rel);
6101
+ const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
6102
+ const abs = path.resolve(cwd, rel);
5987
6103
 
5988
- // Sandbox: refuse paths that resolve outside the project root.
5989
- const root = path.resolve(cwd);
5990
- if (abs !== root && !abs.startsWith(root + path.sep)) {
5991
- return `Refused: ${rel} resolves outside the project root`;
5992
- }
5993
- if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
5994
- return `File not found: ${rel}`;
5995
- }
6104
+ // Sandbox: refuse paths that resolve outside the project root.
6105
+ const root = path.resolve(cwd);
6106
+ if (abs !== root && !abs.startsWith(root + path.sep)) {
6107
+ return `Refused: ${rel} resolves outside the project root`;
6108
+ }
6109
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) {
6110
+ return `File not found: ${rel}`;
6111
+ }
5996
6112
 
5997
- const start = parseInt(args.start, 10);
5998
- const end = parseInt(args.end, 10);
5999
- if (!Number.isFinite(start) || !Number.isFinite(end)) {
6000
- return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
6001
- }
6113
+ const start = parseInt(args.start, 10);
6114
+ const end = parseInt(args.end, 10);
6115
+ if (!Number.isFinite(start) || !Number.isFinite(end)) {
6116
+ return 'Arguments "start" and "end" must be numbers (1-based line numbers).';
6117
+ }
6002
6118
 
6003
- let lines;
6004
- try {
6005
- lines = fs.readFileSync(abs, 'utf8').split('\n');
6006
- } catch (err) {
6007
- return `Could not read ${rel}: ${err.message}`;
6119
+ let lines;
6120
+ try {
6121
+ lines = fs.readFileSync(abs, 'utf8').split('\n');
6122
+ } catch (err) {
6123
+ return `Could not read ${rel}: ${err.message}`;
6124
+ }
6125
+
6126
+ const total = lines.length;
6127
+ const from = Math.max(1, Math.min(start, end));
6128
+ const to = Math.min(total, Math.max(start, end));
6129
+ if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
6130
+
6131
+ const slice = lines.slice(from - 1, to);
6132
+
6133
+ // Redaction scan: reuse the signature secret scanner line-by-line.
6134
+ let safeLines = slice;
6135
+ try {
6136
+ const { scan } = __require('./src/security/scanner');
6137
+ safeLines = scan(slice, rel).safe;
6138
+ } catch (_) {} // non-fatal: fall back to raw slice
6139
+
6140
+ return [
6141
+ `# ${rel}:${from}-${to}`,
6142
+ '```',
6143
+ ...safeLines,
6144
+ '```',
6145
+ ].join('\n');
6008
6146
  }
6009
6147
 
6010
- const total = lines.length;
6011
- const from = Math.max(1, Math.min(start, end));
6012
- const to = Math.min(total, Math.max(start, end));
6013
- if (from > total) return `${rel} has only ${total} lines; requested ${start}-${end}`;
6148
+ /**
6149
+ * read_memory({ limit? }) string
6150
+ *
6151
+ * Recall the cross-session decision log (notes) plus the last ranking-session
6152
+ * focus, formatted for an agent to consume at the start of a task.
6153
+ */
6154
+ function readMemory(args, cwd) {
6155
+ let limit = parseInt(args && args.limit, 10);
6156
+ if (!Number.isFinite(limit) || limit <= 0) limit = 10;
6157
+ limit = Math.min(limit, 50);
6014
6158
 
6015
- const slice = lines.slice(from - 1, to);
6159
+ const out = ['# SigMap memory'];
6016
6160
 
6017
- // Redaction scan: reuse the signature secret scanner line-by-line.
6018
- let safeLines = slice;
6019
- try {
6020
- const { scan } = __require('./src/security/scanner');
6021
- safeLines = scan(slice, rel).safe;
6022
- } catch (_) {} // non-fatal: fall back to raw slice
6161
+ let notes = [];
6162
+ try {
6163
+ const { readNotes, formatNotes } = __require('./src/session/notes');
6164
+ notes = readNotes(cwd, limit);
6165
+ out.push('');
6166
+ out.push(`## Recent notes (${notes.length})`);
6167
+ // Most recent first for quick scanning.
6168
+ out.push(formatNotes(notes.slice().reverse()));
6169
+ } catch (_) {
6170
+ out.push('');
6171
+ out.push('_No notes available._');
6172
+ }
6023
6173
 
6024
- return [
6025
- `# ${rel}:${from}-${to}`,
6026
- '```',
6027
- ...safeLines,
6028
- '```',
6029
- ].join('\n');
6030
- }
6174
+ // Last ranking-session focus (if any) — extends src/session/memory.js.
6175
+ try {
6176
+ const { loadSession } = __require('./src/session/memory');
6177
+ const s = loadSession(cwd);
6178
+ if (s && (s.lastQuery || (s.topFiles && s.topFiles.length))) {
6179
+ out.push('');
6180
+ out.push('## Last session');
6181
+ if (s.lastQuery) out.push(`**Last query:** ${s.lastQuery}`);
6182
+ if (s.topFiles && s.topFiles.length) {
6183
+ out.push(`**Focus files:** ${s.topFiles.map((f) => f.file).slice(0, 5).join(', ')}`);
6184
+ }
6185
+ }
6186
+ } catch (_) { /* session optional */ }
6031
6187
 
6032
- /**
6033
- * read_memory({ limit? }) → string
6034
- *
6035
- * Recall the cross-session decision log (notes) plus the last ranking-session
6036
- * focus, formatted for an agent to consume at the start of a task.
6037
- */
6038
- function readMemory(args, cwd) {
6039
- let limit = parseInt(args && args.limit, 10);
6040
- if (!Number.isFinite(limit) || limit <= 0) limit = 10;
6041
- limit = Math.min(limit, 50);
6188
+ return out.join('\n');
6189
+ }
6042
6190
 
6043
- const out = ['# SigMap memory'];
6191
+ /**
6192
+ * get_callee_signatures — return the exact defining signature(s) of named
6193
+ * symbols from the index, so an agent never guesses a callee's parameter types.
6194
+ * Unknown names get a closest-match suggestion.
6195
+ * @param {{symbols:string[]}} args
6196
+ * @param {string} cwd
6197
+ */
6198
+ function getCalleeSignatures(args, cwd) {
6199
+ const symbols = args && Array.isArray(args.symbols)
6200
+ ? args.symbols.map((s) => String(s).trim()).filter(Boolean)
6201
+ : null;
6202
+ if (!symbols || symbols.length === 0) {
6203
+ return 'Missing required argument: symbols (non-empty string[])';
6204
+ }
6044
6205
 
6045
- let notes = [];
6046
- try {
6047
- const { readNotes, formatNotes } = __require('./src/session/notes');
6048
- notes = readNotes(cwd, limit);
6049
- out.push('');
6050
- out.push(`## Recent notes (${notes.length})`);
6051
- // Most recent first for quick scanning.
6052
- out.push(formatNotes(notes.slice().reverse()));
6053
- } catch (_) {
6054
- out.push('');
6055
- out.push('_No notes available._');
6206
+ try {
6207
+ const { buildSigIndex } = __require('./src/retrieval/ranker');
6208
+ const { buildSymbolCandidates, closestMatch, formatSuggestion } = __require('./src/verify/closest-match');
6209
+ const index = buildSigIndex(cwd);
6210
+ if (index.size === 0) return 'No context file found. Run: node gen-context.js';
6211
+
6212
+ // Extract the defining symbol name from a signature line (same rules as
6213
+ // buildSymbolCandidates) so we match definitions, not param occurrences.
6214
+ const defName = (sig) => {
6215
+ const cleaned = String(sig).replace(/\s*:\d+(?:-\d+)?\s*$/, '');
6216
+ const m = cleaned.match(/\b(?:async\s+function|function|class|def|interface|type|enum|const|let|var)\s+([A-Za-z_$][\w$]*)/)
6217
+ || cleaned.match(/([A-Za-z_$][\w$]*)\s*\(/);
6218
+ return m ? m[1] : null;
6219
+ };
6220
+
6221
+ const candidates = buildSymbolCandidates(index);
6222
+ const blocks = [];
6223
+ for (const symbol of symbols) {
6224
+ const matches = [];
6225
+ for (const [file, sigs] of index.entries()) {
6226
+ for (const sig of sigs) {
6227
+ if (defName(sig) === symbol) matches.push(`${sig} (${file})`);
6228
+ }
6229
+ }
6230
+ if (matches.length === 0) {
6231
+ const cm = closestMatch(symbol, candidates);
6232
+ const hint = cm ? ' — ' + formatSuggestion(cm) : '';
6233
+ blocks.push(`### ${symbol}\n_not found in index${hint}_`);
6234
+ } else {
6235
+ blocks.push(`### ${symbol}\n${matches.join('\n')}`);
6236
+ }
6237
+ }
6238
+ return blocks.join('\n\n');
6239
+ } catch (err) {
6240
+ return `_get_callee_signatures failed: ${err.message}_`;
6241
+ }
6056
6242
  }
6057
6243
 
6058
- // Last ranking-session focus (if any) extends src/session/memory.js.
6059
- try {
6060
- const { loadSession } = __require('./src/session/memory');
6061
- const s = loadSession(cwd);
6062
- if (s && (s.lastQuery || (s.topFiles && s.topFiles.length))) {
6063
- out.push('');
6064
- out.push('## Last session');
6065
- if (s.lastQuery) out.push(`**Last query:** ${s.lastQuery}`);
6066
- if (s.topFiles && s.topFiles.length) {
6067
- out.push(`**Focus files:** ${s.topFiles.map((f) => f.file).slice(0, 5).join(', ')}`);
6244
+ // ── Layer 1: live-index write hooks ────────────────────────────────────────
6245
+ // Keep the sig-cache fresh while an agent creates/modifies/deletes files, so
6246
+ // new code is discoverable in the same session. buildSigIndex already merges
6247
+ // the cache (_buildSigIndexFromCache), so updates are live on the next read.
6248
+
6249
+ function _pkgVersion(cwd) {
6250
+ try { return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')).version || '0.0.0'; }
6251
+ catch (_) { return '0.0.0'; }
6252
+ }
6253
+
6254
+
6255
+ /** notify_file_created — extract a file's signatures and index it live. */
6256
+ function notifyFileCreated(args, cwd) {
6257
+ const rel = args && args.path;
6258
+ if (!rel) return 'Missing required argument: path';
6259
+ try {
6260
+ const { extractFile } = __require('./src/extractors/dispatch');
6261
+ const { loadCache, saveCache } = __require('./src/cache/sig-cache');
6262
+ const abs = path.resolve(cwd, rel);
6263
+ let content = args.content;
6264
+ if (typeof content !== 'string') {
6265
+ try { content = fs.readFileSync(abs, 'utf8'); } catch (_) { content = ''; }
6266
+ }
6267
+ const sigs = extractFile(abs, content);
6268
+ const version = _pkgVersion(cwd);
6269
+ const cache = loadCache(cwd, version);
6270
+ if (sigs.length > 0) {
6271
+ cache.set(abs, { mtime: Date.now(), sigs });
6068
6272
  }
6273
+ saveCache(cwd, version, cache);
6274
+ return `Indexed ${rel}: ${sigs.length} signature(s) now live.`;
6275
+ } catch (err) {
6276
+ return `_notify_file_created failed: ${err.message}_`;
6069
6277
  }
6070
- } catch (_) { /* session optional */ }
6278
+ }
6071
6279
 
6072
- return out.join('\n');
6073
- }
6280
+ /** notify_symbol_added — append one signature to a file's live cache entry. */
6281
+ function notifySymbolAdded(args, cwd) {
6282
+ if (!args || !args.signature || !args.file) {
6283
+ return 'Missing required arguments: signature, file';
6284
+ }
6285
+ try {
6286
+ const { loadCache, saveCache } = __require('./src/cache/sig-cache');
6287
+ const abs = path.resolve(cwd, args.file);
6288
+ const version = _pkgVersion(cwd);
6289
+ const cache = loadCache(cwd, version);
6290
+ const entry = cache.get(abs) || { mtime: Date.now(), sigs: [] };
6291
+ const line = Number.isFinite(Number(args.line)) ? ` :${args.line}` : '';
6292
+ const sig = String(args.signature) + line;
6293
+ if (!entry.sigs.includes(sig)) entry.sigs.push(sig);
6294
+ entry.mtime = Date.now();
6295
+ cache.set(abs, entry);
6296
+ saveCache(cwd, version, cache);
6297
+ return `Added signature to ${args.file} (${entry.sigs.length} total).`;
6298
+ } catch (err) {
6299
+ return `_notify_symbol_added failed: ${err.message}_`;
6300
+ }
6301
+ }
6074
6302
 
6075
- module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory };
6303
+ /** notify_file_deleted drop a file's cache-overlay entry. */
6304
+ function notifyFileDeleted(args, cwd) {
6305
+ const rel = args && args.path;
6306
+ if (!rel) return 'Missing required argument: path';
6307
+ try {
6308
+ const { loadCache, saveCache } = __require('./src/cache/sig-cache');
6309
+ const abs = path.resolve(cwd, rel);
6310
+ const version = _pkgVersion(cwd);
6311
+ const cache = loadCache(cwd, version);
6312
+ const had = cache.delete(abs);
6313
+ saveCache(cwd, version, cache);
6314
+ return had ? `Removed ${rel} from the live index.` : `${rel} was not in the live cache.`;
6315
+ } catch (err) {
6316
+ return `_notify_file_deleted failed: ${err.message}_`;
6317
+ }
6318
+ }
6076
6319
 
6320
+ module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted };
6321
+
6077
6322
  };
6078
6323
  // ── ./src/learning/weights ──
6079
6324
  __factories["./src/learning/weights"] = function(module, exports) {
@@ -6234,363 +6479,428 @@ __factories["./src/learning/weights"] = function(module, exports) {
6234
6479
 
6235
6480
  // ── ./src/mcp/server ──
6236
6481
  __factories["./src/mcp/server"] = function(module, exports) {
6237
- 'use strict';
6238
-
6239
- /**
6240
- * SigMap MCP server — zero npm dependencies.
6241
- *
6242
- * Wire protocol: JSON-RPC 2.0 over stdio.
6243
- * One JSON object per line on both stdin and stdout.
6244
- *
6245
- * Supported methods:
6246
- * initializeserverInfo + capabilities
6247
- * tools/list11 tool definitions
6248
- * tools/call → dispatch to handler, return result
6249
- */
6250
-
6251
- const readline = require('readline');
6252
- const { TOOLS } = __require('./src/mcp/tools');
6253
- const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory } = __require('./src/mcp/handlers');
6254
-
6255
- const SERVER_INFO = {
6256
- name: 'sigmap',
6257
- version: '7.2.1',
6258
- description: 'SigMap MCP server — code signatures on demand',
6259
- };
6260
-
6261
- // ---------------------------------------------------------------------------
6262
- // JSON-RPC helpers
6263
- // ---------------------------------------------------------------------------
6264
- function respond(id, result) {
6265
- process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
6266
- }
6267
-
6268
- function respondError(id, code, message) {
6269
- process.stdout.write(
6270
- JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
6271
- );
6272
- }
6482
+
6483
+ /**
6484
+ * SigMap MCP server — zero npm dependencies.
6485
+ *
6486
+ * Wire protocol: JSON-RPC 2.0 over stdio.
6487
+ * One JSON object per line on both stdin and stdout.
6488
+ *
6489
+ * Supported methods:
6490
+ * initialize → serverInfo + capabilities
6491
+ * tools/list11 tool definitions
6492
+ * tools/calldispatch to handler, return result
6493
+ */
6273
6494
 
6274
- // ---------------------------------------------------------------------------
6275
- // Method dispatcher
6276
- // ---------------------------------------------------------------------------
6277
- function dispatch(msg, cwd) {
6278
- const { method, id, params } = msg;
6495
+ const readline = require('readline');
6496
+ const { TOOLS } = __require('./src/mcp/tools');
6497
+ const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules, queryContext, getImpact, getLines, readMemory, getCalleeSignatures, notifyFileCreated, notifySymbolAdded, notifyFileDeleted } = __require('./src/mcp/handlers');
6279
6498
 
6280
- // Notifications (no id) need no response
6281
- if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
6282
- return;
6283
- }
6499
+ const SERVER_INFO = {
6500
+ name: 'sigmap',
6501
+ version: '7.4.0',
6502
+ description: 'SigMap MCP server — code signatures on demand',
6503
+ };
6284
6504
 
6285
- if (method === 'initialize') {
6286
- respond(id, {
6287
- protocolVersion: (params && params.protocolVersion) || '2024-11-05',
6288
- serverInfo: SERVER_INFO,
6289
- capabilities: { tools: {} },
6290
- });
6291
- return;
6505
+ // ---------------------------------------------------------------------------
6506
+ // JSON-RPC helpers
6507
+ // ---------------------------------------------------------------------------
6508
+ function respond(id, result) {
6509
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
6292
6510
  }
6293
6511
 
6294
- if (method === 'tools/list') {
6295
- respond(id, { tools: TOOLS });
6296
- return;
6512
+ function respondError(id, code, message) {
6513
+ process.stdout.write(
6514
+ JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
6515
+ );
6297
6516
  }
6298
6517
 
6299
- if (method === 'tools/call') {
6300
- const name = params && params.name;
6301
- const args = (params && params.arguments) || {};
6518
+ // ---------------------------------------------------------------------------
6519
+ // Method dispatcher
6520
+ // ---------------------------------------------------------------------------
6521
+ function dispatch(msg, cwd) {
6522
+ const { method, id, params } = msg;
6302
6523
 
6303
- let text;
6304
- try {
6305
- if (name === 'read_context') text = readContext(args, cwd);
6306
- else if (name === 'search_signatures') text = searchSignatures(args, cwd);
6307
- else if (name === 'get_map') text = getMap(args, cwd);
6308
- else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
6309
- else if (name === 'get_routing') text = getRouting(args, cwd);
6310
- else if (name === 'explain_file') text = explainFile(args, cwd);
6311
- else if (name === 'list_modules') text = listModules(args, cwd);
6312
- else if (name === 'query_context') text = queryContext(args, cwd);
6313
- else if (name === 'get_impact') text = getImpact(args, cwd);
6314
- else if (name === 'get_lines') text = getLines(args, cwd);
6315
- else if (name === 'read_memory') text = readMemory(args, cwd);
6316
- else {
6317
- respondError(id, -32601, `Unknown tool: ${name}`);
6318
- return;
6319
- }
6320
- } catch (err) {
6321
- respondError(id, -32603, `Tool error: ${err.message}`);
6524
+ // Notifications (no id) need no response
6525
+ if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
6322
6526
  return;
6323
6527
  }
6324
6528
 
6325
- respond(id, {
6326
- content: [{ type: 'text', text: String(text) }],
6327
- });
6328
- return;
6329
- }
6529
+ if (method === 'initialize') {
6530
+ respond(id, {
6531
+ protocolVersion: (params && params.protocolVersion) || '2024-11-05',
6532
+ serverInfo: SERVER_INFO,
6533
+ capabilities: { tools: {} },
6534
+ });
6535
+ return;
6536
+ }
6330
6537
 
6331
- // Unknown method
6332
- if (id !== undefined && id !== null) {
6333
- respondError(id, -32601, `Method not found: ${method}`);
6334
- }
6335
- }
6538
+ if (method === 'tools/list') {
6539
+ respond(id, { tools: TOOLS });
6540
+ return;
6541
+ }
6336
6542
 
6337
- // ---------------------------------------------------------------------------
6338
- // Server entry point
6339
- // ---------------------------------------------------------------------------
6340
- function start(cwd) {
6341
- const rl = readline.createInterface({ input: process.stdin, terminal: false });
6543
+ if (method === 'tools/call') {
6544
+ const name = params && params.name;
6545
+ const args = (params && params.arguments) || {};
6342
6546
 
6343
- rl.on('line', (line) => {
6344
- const trimmed = line.trim();
6345
- if (!trimmed) return;
6547
+ let text;
6548
+ try {
6549
+ if (name === 'read_context') text = readContext(args, cwd);
6550
+ else if (name === 'search_signatures') text = searchSignatures(args, cwd);
6551
+ else if (name === 'get_map') text = getMap(args, cwd);
6552
+ else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
6553
+ else if (name === 'get_routing') text = getRouting(args, cwd);
6554
+ else if (name === 'explain_file') text = explainFile(args, cwd);
6555
+ else if (name === 'list_modules') text = listModules(args, cwd);
6556
+ else if (name === 'query_context') text = queryContext(args, cwd);
6557
+ else if (name === 'get_impact') text = getImpact(args, cwd);
6558
+ else if (name === 'get_lines') text = getLines(args, cwd);
6559
+ else if (name === 'read_memory') text = readMemory(args, cwd);
6560
+ else if (name === 'get_callee_signatures') text = getCalleeSignatures(args, cwd);
6561
+ else if (name === 'sigmap_notify_file_created') text = notifyFileCreated(args, cwd);
6562
+ else if (name === 'sigmap_notify_symbol_added') text = notifySymbolAdded(args, cwd);
6563
+ else if (name === 'sigmap_notify_file_deleted') text = notifyFileDeleted(args, cwd);
6564
+ else {
6565
+ respondError(id, -32601, `Unknown tool: ${name}`);
6566
+ return;
6567
+ }
6568
+ } catch (err) {
6569
+ respondError(id, -32603, `Tool error: ${err.message}`);
6570
+ return;
6571
+ }
6346
6572
 
6347
- let msg;
6348
- try {
6349
- msg = JSON.parse(trimmed);
6350
- } catch (_) {
6351
- // Cannot respond without a valid id — ignore malformed input
6573
+ respond(id, {
6574
+ content: [{ type: 'text', text: String(text) }],
6575
+ });
6352
6576
  return;
6353
6577
  }
6354
6578
 
6355
- try {
6356
- dispatch(msg, cwd);
6357
- } catch (err) {
6358
- const id = (msg && msg.id) != null ? msg.id : null;
6359
- respondError(id, -32603, `Internal error: ${err.message}`);
6579
+ // Unknown method
6580
+ if (id !== undefined && id !== null) {
6581
+ respondError(id, -32601, `Method not found: ${method}`);
6360
6582
  }
6361
- });
6583
+ }
6362
6584
 
6363
- rl.on('close', () => {
6364
- process.exit(0);
6365
- });
6366
- }
6585
+ // ---------------------------------------------------------------------------
6586
+ // Server entry point
6587
+ // ---------------------------------------------------------------------------
6588
+ function start(cwd) {
6589
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
6367
6590
 
6368
- module.exports = { start };
6591
+ rl.on('line', (line) => {
6592
+ const trimmed = line.trim();
6593
+ if (!trimmed) return;
6369
6594
 
6595
+ let msg;
6596
+ try {
6597
+ msg = JSON.parse(trimmed);
6598
+ } catch (_) {
6599
+ // Cannot respond without a valid id — ignore malformed input
6600
+ return;
6601
+ }
6602
+
6603
+ try {
6604
+ dispatch(msg, cwd);
6605
+ } catch (err) {
6606
+ const id = (msg && msg.id) != null ? msg.id : null;
6607
+ respondError(id, -32603, `Internal error: ${err.message}`);
6608
+ }
6609
+ });
6610
+
6611
+ rl.on('close', () => {
6612
+ process.exit(0);
6613
+ });
6614
+ }
6615
+
6616
+ module.exports = { start };
6617
+
6370
6618
  };
6371
6619
  // ── ./src/mcp/tools ──
6372
6620
  __factories["./src/mcp/tools"] = function(module, exports) {
6373
- 'use strict';
6374
-
6375
- /**
6376
- * MCP tool definitions for SigMap (11 tools).
6377
- * read_context, search_signatures, get_map, create_checkpoint, get_routing,
6378
- * explain_file, list_modules, query_context, get_impact, get_lines, read_memory.
6379
- */
6621
+
6622
+ /**
6623
+ * MCP tool definitions for SigMap (15 tools).
6624
+ * read_context, search_signatures, get_map, create_checkpoint, get_routing,
6625
+ * explain_file, list_modules, query_context, get_impact, get_lines, read_memory,
6626
+ * get_callee_signatures, sigmap_notify_file_created, sigmap_notify_symbol_added,
6627
+ * sigmap_notify_file_deleted.
6628
+ */
6380
6629
 
6381
- const TOOLS = [
6382
- {
6383
- name: 'read_context',
6384
- description:
6385
- 'Read extracted code signatures for the project or a specific module path. ' +
6386
- 'Returns the full copilot-instructions.md content (~500–4K tokens) or a ' +
6387
- 'filtered subset when a module path is provided (~50–500 tokens).',
6388
- inputSchema: {
6389
- type: 'object',
6390
- properties: {
6391
- module: {
6392
- type: 'string',
6393
- description:
6394
- 'Optional subdirectory path to scope results (e.g. "src/services"). ' +
6395
- 'Omit to get the full codebase context.',
6630
+ const TOOLS = [
6631
+ {
6632
+ name: 'read_context',
6633
+ description:
6634
+ 'Read extracted code signatures for the project or a specific module path. ' +
6635
+ 'Returns the full copilot-instructions.md content (~500–4K tokens) or a ' +
6636
+ 'filtered subset when a module path is provided (~50–500 tokens).',
6637
+ inputSchema: {
6638
+ type: 'object',
6639
+ properties: {
6640
+ module: {
6641
+ type: 'string',
6642
+ description:
6643
+ 'Optional subdirectory path to scope results (e.g. "src/services"). ' +
6644
+ 'Omit to get the full codebase context.',
6645
+ },
6396
6646
  },
6647
+ required: [],
6397
6648
  },
6398
- required: [],
6399
6649
  },
6400
- },
6401
- {
6402
- name: 'search_signatures',
6403
- description:
6404
- 'Search extracted code signatures for a keyword, function name, or class name. ' +
6405
- 'Returns matching signature lines with their file paths.',
6406
- inputSchema: {
6407
- type: 'object',
6408
- properties: {
6409
- query: {
6410
- type: 'string',
6411
- description: 'Keyword to search for in signatures (case-insensitive).',
6650
+ {
6651
+ name: 'search_signatures',
6652
+ description:
6653
+ 'Search extracted code signatures for a keyword, function name, or class name. ' +
6654
+ 'Returns matching signature lines with their file paths.',
6655
+ inputSchema: {
6656
+ type: 'object',
6657
+ properties: {
6658
+ query: {
6659
+ type: 'string',
6660
+ description: 'Keyword to search for in signatures (case-insensitive).',
6661
+ },
6412
6662
  },
6663
+ required: ['query'],
6413
6664
  },
6414
- required: ['query'],
6415
6665
  },
6416
- },
6417
- {
6418
- name: 'get_map',
6419
- description:
6420
- 'Read a section from PROJECT_MAP.md import graph, class hierarchy, or route table. ' +
6421
- 'Requires gen-project-map.js to have been run first.',
6422
- inputSchema: {
6423
- type: 'object',
6424
- properties: {
6425
- type: {
6426
- type: 'string',
6427
- enum: ['imports', 'classes', 'routes'],
6428
- description: 'Which section to retrieve: imports, classes, or routes.',
6666
+ {
6667
+ name: 'get_map',
6668
+ description:
6669
+ 'Read a section from PROJECT_MAP.md — import graph, class hierarchy, or route table. ' +
6670
+ 'Requires gen-project-map.js to have been run first.',
6671
+ inputSchema: {
6672
+ type: 'object',
6673
+ properties: {
6674
+ type: {
6675
+ type: 'string',
6676
+ enum: ['imports', 'classes', 'routes'],
6677
+ description: 'Which section to retrieve: imports, classes, or routes.',
6678
+ },
6429
6679
  },
6680
+ required: ['type'],
6430
6681
  },
6431
- required: ['type'],
6432
6682
  },
6433
- },
6434
- {
6435
- name: 'create_checkpoint',
6436
- description:
6437
- 'Create a session checkpoint summarising current project state. ' +
6438
- 'Returns recent git commits, active branch, token count, and a ' +
6439
- 'compact snapshot of the codebase context — ideal for session handoffs ' +
6440
- 'or periodic saves during long coding sessions.',
6441
- inputSchema: {
6442
- type: 'object',
6443
- properties: {
6444
- note: {
6445
- type: 'string',
6446
- description: 'Optional free-text note to include in the checkpoint (e.g. what you were working on).',
6683
+ {
6684
+ name: 'create_checkpoint',
6685
+ description:
6686
+ 'Create a session checkpoint summarising current project state. ' +
6687
+ 'Returns recent git commits, active branch, token count, and a ' +
6688
+ 'compact snapshot of the codebase context ideal for session handoffs ' +
6689
+ 'or periodic saves during long coding sessions.',
6690
+ inputSchema: {
6691
+ type: 'object',
6692
+ properties: {
6693
+ note: {
6694
+ type: 'string',
6695
+ description: 'Optional free-text note to include in the checkpoint (e.g. what you were working on).',
6696
+ },
6447
6697
  },
6698
+ required: [],
6448
6699
  },
6449
- required: [],
6450
6700
  },
6451
- },
6452
- {
6453
- name: 'get_routing',
6454
- description:
6455
- 'Get model routing hints for this project which files belong to which complexity ' +
6456
- 'tier (fast/balanced/powerful) and which AI model to use for each type of task. ' +
6457
- 'Helps reduce API costs by 40–80% by routing simple tasks to cheaper models.',
6458
- inputSchema: {
6459
- type: 'object',
6460
- properties: {},
6461
- required: [],
6701
+ {
6702
+ name: 'get_routing',
6703
+ description:
6704
+ 'Get model routing hints for this project — which files belong to which complexity ' +
6705
+ 'tier (fast/balanced/powerful) and which AI model to use for each type of task. ' +
6706
+ 'Helps reduce API costs by 40–80% by routing simple tasks to cheaper models.',
6707
+ inputSchema: {
6708
+ type: 'object',
6709
+ properties: {},
6710
+ required: [],
6711
+ },
6462
6712
  },
6463
- },
6464
- {
6465
- name: 'explain_file',
6466
- description:
6467
- 'Explain a specific file: returns its extracted signatures, direct imports ' +
6468
- '(files it depends on), and callers (files that import it). ' +
6469
- 'Ideal for understanding a file in isolation without reading raw source. ' +
6470
- 'Requires the context file to have been generated first.',
6471
- inputSchema: {
6472
- type: 'object',
6473
- properties: {
6474
- path: {
6475
- type: 'string',
6476
- description:
6477
- 'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
6478
- 'Use the paths shown in read_context output.',
6713
+ {
6714
+ name: 'explain_file',
6715
+ description:
6716
+ 'Explain a specific file: returns its extracted signatures, direct imports ' +
6717
+ '(files it depends on), and callers (files that import it). ' +
6718
+ 'Ideal for understanding a file in isolation without reading raw source. ' +
6719
+ 'Requires the context file to have been generated first.',
6720
+ inputSchema: {
6721
+ type: 'object',
6722
+ properties: {
6723
+ path: {
6724
+ type: 'string',
6725
+ description:
6726
+ 'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
6727
+ 'Use the paths shown in read_context output.',
6728
+ },
6479
6729
  },
6730
+ required: ['path'],
6480
6731
  },
6481
- required: ['path'],
6482
6732
  },
6483
- },
6484
- {
6485
- name: 'list_modules',
6486
- description:
6487
- 'List all top-level modules (srcDirs) present in the context file, ' +
6488
- 'sorted by token count descending. Use this to decide which module to ' +
6489
- 'pass to read_context before querying a specific area of the codebase.',
6490
- inputSchema: {
6491
- type: 'object',
6492
- properties: {},
6493
- required: [],
6733
+ {
6734
+ name: 'list_modules',
6735
+ description:
6736
+ 'List all top-level modules (srcDirs) present in the context file, ' +
6737
+ 'sorted by token count descending. Use this to decide which module to ' +
6738
+ 'pass to read_context before querying a specific area of the codebase.',
6739
+ inputSchema: {
6740
+ type: 'object',
6741
+ properties: {},
6742
+ required: [],
6743
+ },
6494
6744
  },
6495
- },
6496
- {
6497
- name: 'query_context',
6498
- description:
6499
- 'Rank and return the most relevant files for a specific task or question. ' +
6500
- 'Uses keyword + symbol + path scoring to surface only the top-K files relevant ' +
6501
- 'to the query much cheaper than reading all context. ' +
6502
- 'Returns ranked file list with signatures and relevance scores.',
6503
- inputSchema: {
6504
- type: 'object',
6505
- properties: {
6506
- query: {
6507
- type: 'string',
6508
- description:
6509
- 'Natural language task description or keyword(s) to rank files against. ' +
6510
- 'E.g. "add a new language extractor", "fix secret scanning", "auth module".',
6745
+ {
6746
+ name: 'query_context',
6747
+ description:
6748
+ 'Rank and return the most relevant files for a specific task or question. ' +
6749
+ 'Uses keyword + symbol + path scoring to surface only the top-K files relevant ' +
6750
+ 'to the query much cheaper than reading all context. ' +
6751
+ 'Returns ranked file list with signatures and relevance scores.',
6752
+ inputSchema: {
6753
+ type: 'object',
6754
+ properties: {
6755
+ query: {
6756
+ type: 'string',
6757
+ description:
6758
+ 'Natural language task description or keyword(s) to rank files against. ' +
6759
+ 'E.g. "add a new language extractor", "fix secret scanning", "auth module".',
6760
+ },
6761
+ topK: {
6762
+ type: 'number',
6763
+ description: 'Maximum number of files to return (default: 10, max: 25).',
6764
+ },
6511
6765
  },
6512
- topK: {
6513
- type: 'number',
6514
- description: 'Maximum number of files to return (default: 10, max: 25).',
6766
+ required: ['query'],
6767
+ },
6768
+ },
6769
+ {
6770
+ name: 'get_impact',
6771
+ description:
6772
+ 'Show every file that is impacted when a given file changes — direct importers, ' +
6773
+ 'transitive importers, affected tests, and affected routes/controllers. ' +
6774
+ 'Gives agents instant blast-radius awareness before making a change. ' +
6775
+ 'Handles circular dependencies safely (no infinite loops).',
6776
+ inputSchema: {
6777
+ type: 'object',
6778
+ properties: {
6779
+ file: {
6780
+ type: 'string',
6781
+ description:
6782
+ 'Relative path from the project root of the file that changed ' +
6783
+ '(e.g. "src/extractors/python.js"). Use forward slashes.',
6784
+ },
6785
+ depth: {
6786
+ type: 'number',
6787
+ description: 'BFS traversal depth limit (default: 3). Use 0 for unlimited.',
6788
+ },
6515
6789
  },
6790
+ required: ['file'],
6516
6791
  },
6517
- required: ['query'],
6518
6792
  },
6519
- },
6520
- {
6521
- name: 'get_impact',
6522
- description:
6523
- 'Show every file that is impacted when a given file changes — direct importers, ' +
6524
- 'transitive importers, affected tests, and affected routes/controllers. ' +
6525
- 'Gives agents instant blast-radius awareness before making a change. ' +
6526
- 'Handles circular dependencies safely (no infinite loops).',
6527
- inputSchema: {
6528
- type: 'object',
6529
- properties: {
6530
- file: {
6531
- type: 'string',
6532
- description:
6533
- 'Relative path from the project root of the file that changed ' +
6534
- '(e.g. "src/extractors/python.js"). Use forward slashes.',
6793
+ {
6794
+ name: 'get_lines',
6795
+ description:
6796
+ 'Fetch an exact line range from a source file on demand — the Surgical Context ' +
6797
+ 'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
6798
+ 'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
6799
+ 'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
6800
+ inputSchema: {
6801
+ type: 'object',
6802
+ properties: {
6803
+ file: {
6804
+ type: 'string',
6805
+ description:
6806
+ 'Relative path from the project root (e.g. "src/config/loader.js"). ' +
6807
+ 'Use the path shown in a signature anchor. Use forward slashes.',
6808
+ },
6809
+ start: {
6810
+ type: 'number',
6811
+ description: '1-based start line (inclusive). Clamped to the file bounds.',
6812
+ },
6813
+ end: {
6814
+ type: 'number',
6815
+ description: '1-based end line (inclusive). Clamped to the file bounds.',
6816
+ },
6535
6817
  },
6536
- depth: {
6537
- type: 'number',
6538
- description: 'BFS traversal depth limit (default: 3). Use 0 for unlimited.',
6818
+ required: ['file', 'start', 'end'],
6819
+ },
6820
+ },
6821
+ {
6822
+ name: 'read_memory',
6823
+ description:
6824
+ 'Recall the project decision log — recent notes left by humans or agents ' +
6825
+ 'across sessions (via `sigmap note`), plus the last ranking-session focus. ' +
6826
+ 'Call this at the start of a task to kill cold-start: it answers ' +
6827
+ '"what were we doing and why" without re-reading the whole codebase.',
6828
+ inputSchema: {
6829
+ type: 'object',
6830
+ properties: {
6831
+ limit: {
6832
+ type: 'number',
6833
+ description: 'How many of the most recent notes to return (default: 10, max: 50).',
6834
+ },
6539
6835
  },
6836
+ required: [],
6540
6837
  },
6541
- required: ['file'],
6542
6838
  },
6543
- },
6544
- {
6545
- name: 'get_lines',
6546
- description:
6547
- 'Fetch an exact line range from a source file on demand the Surgical Context ' +
6548
- 'workhorse. Signatures carry `path:start-end` anchors; call this to read just those ' +
6549
- 'lines instead of re-opening the whole file. Lines are clamped to the file bounds and ' +
6550
- 'secret-scanned (redacted) before return. Path is sandboxed to the project root.',
6551
- inputSchema: {
6552
- type: 'object',
6553
- properties: {
6554
- file: {
6555
- type: 'string',
6556
- description:
6557
- 'Relative path from the project root (e.g. "src/config/loader.js"). ' +
6558
- 'Use the path shown in a signature anchor. Use forward slashes.',
6839
+ {
6840
+ name: 'get_callee_signatures',
6841
+ description:
6842
+ 'Return the EXACT current signature(s) of named symbols (functions, classes, ' +
6843
+ "methods) from the index so an agent never guesses a callee's parameter types " +
6844
+ 'from training memory. Call this before writing code that uses a symbol. ' +
6845
+ 'Unknown names get a closest-match suggestion.',
6846
+ inputSchema: {
6847
+ type: 'object',
6848
+ properties: {
6849
+ symbols: {
6850
+ type: 'array',
6851
+ items: { type: 'string' },
6852
+ description: 'Symbol names to resolve (e.g. ["validateToken", "UserService"]).',
6853
+ },
6559
6854
  },
6560
- start: {
6561
- type: 'number',
6562
- description: '1-based start line (inclusive). Clamped to the file bounds.',
6855
+ required: ['symbols'],
6856
+ },
6857
+ },
6858
+ {
6859
+ name: 'sigmap_notify_file_created',
6860
+ description:
6861
+ 'Tell SigMap a file was created or modified so its signatures are indexed ' +
6862
+ 'live for the rest of the session. Call this after writing a file — the new ' +
6863
+ 'symbols become resolvable by search_signatures / get_callee_signatures.',
6864
+ inputSchema: {
6865
+ type: 'object',
6866
+ properties: {
6867
+ path: { type: 'string', description: 'File path relative to the project root.' },
6868
+ content: { type: 'string', description: 'Optional file content; read from disk if omitted.' },
6563
6869
  },
6564
- end: {
6565
- type: 'number',
6566
- description: '1-based end line (inclusive). Clamped to the file bounds.',
6870
+ required: ['path'],
6871
+ },
6872
+ },
6873
+ {
6874
+ name: 'sigmap_notify_symbol_added',
6875
+ description:
6876
+ 'Fast path: register a single new symbol signature directly in the live ' +
6877
+ 'index without re-reading the whole file.',
6878
+ inputSchema: {
6879
+ type: 'object',
6880
+ properties: {
6881
+ signature: { type: 'string', description: 'The signature line (e.g. "function check(key)").' },
6882
+ file: { type: 'string', description: 'File path the symbol belongs to (relative to root).' },
6883
+ line: { type: 'number', description: 'Optional 1-based line number.' },
6567
6884
  },
6885
+ required: ['signature', 'file'],
6568
6886
  },
6569
- required: ['file', 'start', 'end'],
6570
6887
  },
6571
- },
6572
- {
6573
- name: 'read_memory',
6574
- description:
6575
- 'Recall the project decision log — recent notes left by humans or agents ' +
6576
- 'across sessions (via `sigmap note`), plus the last ranking-session focus. ' +
6577
- 'Call this at the start of a task to kill cold-start: it answers ' +
6578
- '"what were we doing and why" without re-reading the whole codebase.',
6579
- inputSchema: {
6580
- type: 'object',
6581
- properties: {
6582
- limit: {
6583
- type: 'number',
6584
- description: 'How many of the most recent notes to return (default: 10, max: 50).',
6888
+ {
6889
+ name: 'sigmap_notify_file_deleted',
6890
+ description:
6891
+ 'Tell SigMap a file was deleted so its symbols are dropped from the live index.',
6892
+ inputSchema: {
6893
+ type: 'object',
6894
+ properties: {
6895
+ path: { type: 'string', description: 'Deleted file path relative to the project root.' },
6585
6896
  },
6897
+ required: ['path'],
6586
6898
  },
6587
- required: [],
6588
6899
  },
6589
- },
6590
- ];
6591
-
6592
- module.exports = { TOOLS };
6900
+ ];
6593
6901
 
6902
+ module.exports = { TOOLS };
6903
+
6594
6904
  };
6595
6905
  // ── ./src/routing/classifier ──
6596
6906
  __factories["./src/routing/classifier"] = function(module, exports) {
@@ -7907,61 +8217,293 @@ __factories["./src/retrieval/tokenizer"] = function(module, exports) {
7907
8217
 
7908
8218
  // ── ./src/retrieval/ranker ──
7909
8219
  __factories["./src/retrieval/ranker"] = function(module, exports) {
7910
- 'use strict';
8220
+
8221
+ /**
8222
+ * SigMap zero-dependency relevance ranker.
8223
+ *
8224
+ * Ranks all files in a signature index against a natural-language query.
8225
+ * Scoring weights:
8226
+ * - keyword overlap (exact token match against sigs)
8227
+ * - symbol match (token appears in a top-level identifier / function name)
8228
+ * - partial prefix match (token is prefix of a sig token, length ≥ 4)
8229
+ * - path relevance (query token appears in the file path)
8230
+ * - recency boost (applied externally via recency map)
8231
+ *
8232
+ * Usage:
8233
+ * const { rank } = __require('./src/retrieval/src/retrieval/ranker');
8234
+ * const results = rank(query, sigIndex, { topK: 10 });
8235
+ * // results: [{ file, score, sigs, tokens }]
8236
+ */
8237
+
7911
8238
  const { loadWeights } = __require('./src/learning/weights');
7912
8239
  const { tokenize, STOP_WORDS } = __require('./src/retrieval/tokenizer');
8240
+
8241
+ // ---------------------------------------------------------------------------
8242
+ // Default weights
8243
+ // ---------------------------------------------------------------------------
7913
8244
  const DEFAULT_WEIGHTS = {
7914
- exactToken: 1.0, symbolMatch: 0.5, prefixMatch: 0.3, pathMatch: 0.8, recencyBoost: 1.5,
8245
+ exactToken: 1.0, // query token exactly in sig tokens
8246
+ symbolMatch: 0.5, // bonus if token appears in a function/class name line
8247
+ prefixMatch: 0.3, // partial prefix hit (query token ≥ 4 chars)
8248
+ pathMatch: 0.8, // query token appears in the file path
8249
+ recencyBoost: 1.5, // multiplier applied when file is in recencySet
8250
+ graphBoost: 0.4, // additive bonus for 1-hop import neighbors of matching files
8251
+ };
8252
+
8253
+ // Graph boost amounts for 2-hop traversal with decay (v6.7)
8254
+ const GRAPH_BOOST_AMOUNTS = {
8255
+ hop1: 0.40, // direct import neighbor of a file with score > 0
8256
+ hop2: 0.15, // 2 hops away (transitive), with decay
8257
+ };
8258
+
8259
+ // Intent-specific weight adjustments
8260
+ const INTENT_WEIGHTS = {
8261
+ search: DEFAULT_WEIGHTS,
8262
+ debug: { ...DEFAULT_WEIGHTS, exactToken: 1.2, pathMatch: 0.6 },
8263
+ explain: { ...DEFAULT_WEIGHTS, symbolMatch: 0.8, pathMatch: 0.9 },
8264
+ refactor: { ...DEFAULT_WEIGHTS, symbolMatch: 0.9, exactToken: 0.8 },
8265
+ review: { ...DEFAULT_WEIGHTS, pathMatch: 1.0, exactToken: 0.9 },
8266
+ test: { ...DEFAULT_WEIGHTS, exactToken: 0.7, symbolMatch: 0.4 },
8267
+ integrate: { ...DEFAULT_WEIGHTS, graphBoost: 0.7, pathMatch: 1.1 },
8268
+ navigate: { ...DEFAULT_WEIGHTS, pathMatch: 1.2, exactToken: 0.9 },
8269
+ };
8270
+
8271
+ // Penalty multipliers for negative signals
8272
+ const PENALTY_SIGNALS = {
8273
+ testFile: 0.4, // test/spec/__tests__ in path
8274
+ generatedCode: 0.3, // dist/build/.next in path
8275
+ docsFile: 0.2, // docs/doc/README in path
8276
+ nodeModules: 0.0, // node_modules (zero score)
7915
8277
  };
8278
+
8279
+ function _computePenalty(filePath) {
8280
+ const pathLower = filePath.toLowerCase();
8281
+ if (pathLower.includes('node_modules')) return PENALTY_SIGNALS.nodeModules;
8282
+ if (/(^|\/)(test|tests|spec|__tests__|e2e)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.testFile;
8283
+ if (/(^|\/)(dist|build|\.next|\.nuxt|out|\.venv|venv)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.generatedCode;
8284
+ if (/(^|\/)(docs|doc|readme|changelog)($|\/)/.test(pathLower)) return PENALTY_SIGNALS.docsFile;
8285
+ return 1.0;
8286
+ }
8287
+
8288
+ // Detect hub files: those with fanout > 20% of all files in the graph
8289
+ function _computeHubs(graph) {
8290
+ if (!graph || !graph.reverse) return new Set();
8291
+ const fileCount = Math.max(1, graph.reverse.size);
8292
+ const threshold = Math.ceil(fileCount * 0.2);
8293
+ const hubs = new Set();
8294
+ for (const [file, deps] of graph.reverse) {
8295
+ if ((deps && deps.size >= threshold) || (Array.isArray(deps) && deps.length >= threshold)) {
8296
+ hubs.add(file);
8297
+ }
8298
+ }
8299
+ return hubs;
8300
+ }
8301
+
8302
+ // Common utility paths that should be treated as hubs regardless of fanout
8303
+ function _isHub(filePath) {
8304
+ return /\/(utils|helpers|shared|common|constants|types|interfaces|index|zzz|globals)\.(ts|tsx|js|jsx|r|R)$/.test(filePath)
8305
+ || filePath.endsWith('/index.ts') || filePath.endsWith('/index.js')
8306
+ || filePath.endsWith('/R/utils.R') || filePath.endsWith('/R/zzz.R') || filePath.endsWith('/R/globals.R');
8307
+ }
8308
+
8309
+ /**
8310
+ * Score a single file against a query, returning detailed signal breakdown.
8311
+ *
8312
+ * @param {string} filePath - relative file path (e.g. 'src/extractors/python.js')
8313
+ * @param {string[]} sigs - signature strings for this file
8314
+ * @param {string[]} queryTokens - pre-tokenized query
8315
+ * @param {object} weights
8316
+ * @returns {{ score: number, signals: { exactToken: number, symbolMatch: number, prefixMatch: number, pathMatch: number, penalty: number } }}
8317
+ */
7916
8318
  function scoreFile(filePath, sigs, queryTokens, weights) {
7917
- if (!sigs || sigs.length === 0) return 0;
8319
+ if (!sigs || sigs.length === 0) return { score: 0, signals: { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: 1.0 } };
8320
+
7918
8321
  const w = weights || DEFAULT_WEIGHTS;
7919
- const sigTokenSet = new Set(tokenize(sigs.join(' ')));
8322
+ const signals = { exactToken: 0, symbolMatch: 0, prefixMatch: 0, pathMatch: 0, penalty: _computePenalty(filePath) };
8323
+
8324
+ // Build token set from all signatures
8325
+ const sigText = sigs.join(' ');
8326
+ const sigTokenSet = new Set(tokenize(sigText));
8327
+
8328
+ // Build token set from the file path
7920
8329
  const pathTokenSet = new Set(tokenize(filePath));
8330
+
7921
8331
  let score = 0;
8332
+
7922
8333
  for (const qt of queryTokens) {
7923
8334
  if (STOP_WORDS.has(qt)) continue;
8335
+
8336
+ // Exact token match in sigs
7924
8337
  if (sigTokenSet.has(qt)) {
7925
- score += w.exactToken;
7926
- if (sigs.some((sig) => tokenize(sig.replace(/[^a-zA-Z0-9_\s]/g, ' ')).includes(qt))) score += w.symbolMatch;
8338
+ const bonus = w.exactToken;
8339
+ score += bonus;
8340
+ signals.exactToken += bonus;
8341
+
8342
+ // Bonus: appears directly in a function/class/method name line
8343
+ const nameLineMatch = sigs.some((sig) => {
8344
+ const nt = tokenize(sig.replace(/[^a-zA-Z0-9_\s]/g, ' '));
8345
+ return nt.includes(qt);
8346
+ });
8347
+ if (nameLineMatch) {
8348
+ score += w.symbolMatch;
8349
+ signals.symbolMatch += w.symbolMatch;
8350
+ }
7927
8351
  }
8352
+
8353
+ // Prefix match (e.g. query "python" matches "pythonDeps")
7928
8354
  if (qt.length >= 4) {
7929
8355
  for (const st of sigTokenSet) {
7930
- if (st !== qt && st.startsWith(qt)) { score += w.prefixMatch; break; }
8356
+ if (st !== qt && st.startsWith(qt)) {
8357
+ score += w.prefixMatch;
8358
+ signals.prefixMatch += w.prefixMatch;
8359
+ break; // one bonus per query token
8360
+ }
7931
8361
  }
7932
8362
  }
7933
- if (pathTokenSet.has(qt)) score += w.pathMatch;
8363
+
8364
+ // Path token match
8365
+ if (pathTokenSet.has(qt)) {
8366
+ score += w.pathMatch;
8367
+ signals.pathMatch += w.pathMatch;
8368
+ }
7934
8369
  }
7935
- return score;
8370
+
8371
+ // Apply penalty multiplier
8372
+ score *= signals.penalty;
8373
+
8374
+ return { score, signals };
7936
8375
  }
8376
+
8377
+ /**
8378
+ * Rank all files in a signature index against a query.
8379
+ *
8380
+ * @param {string} query - natural language query
8381
+ * @param {Map<string,string[]>} sigIndex - Map<file, sigs[]>
8382
+ * @param {object} [opts]
8383
+ * @param {number} [opts.topK=10] - max results to return
8384
+ * @param {number} [opts.recencyBoost=1.5] - multiplier for recent files
8385
+ * @param {Set<string>} [opts.recencySet] - set of recently-changed file paths
8386
+ * @param {object} [opts.weights] - override scoring weights
8387
+ * @param {string} [opts.cwd] - project root for learned ranking weights
8388
+ * @param {{ forward: Map<string,string[]> }} [opts.graph] - dependency graph for neighbor boost
8389
+ * @returns {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]}
8390
+ */
7937
8391
  function rank(query, sigIndex, opts) {
7938
8392
  if (!query || typeof query !== 'string') return [];
7939
8393
  if (!sigIndex || !(sigIndex instanceof Map) || sigIndex.size === 0) return [];
8394
+
7940
8395
  const topK = (opts && opts.topK) || 10;
7941
8396
  const recencyMultiplier = (opts && opts.recencyBoost) || DEFAULT_WEIGHTS.recencyBoost;
7942
8397
  const recencySet = (opts && opts.recencySet) || null;
7943
- const weights = (opts && opts.weights) ? Object.assign({}, DEFAULT_WEIGHTS, opts.weights) : DEFAULT_WEIGHTS;
8398
+ const graph = (opts && opts.graph && opts.graph.forward instanceof Map) ? opts.graph : null;
8399
+ const cwd = (opts && opts.cwd) || null;
8400
+
8401
+ // Detect query intent and get appropriate weights
8402
+ const intent = detectIntent(query);
8403
+ const intentWeights = INTENT_WEIGHTS[intent] || DEFAULT_WEIGHTS;
8404
+ const weights = (opts && opts.weights) ? Object.assign({}, intentWeights, opts.weights) : intentWeights;
7944
8405
  const learnedWeights = opts && opts.cwd ? loadWeights(opts.cwd) : null;
8406
+
7945
8407
  const queryTokens = tokenize(query);
7946
8408
  if (queryTokens.length === 0) {
8409
+ // Empty query: return top-K by file count (most signatures = most useful)
7947
8410
  const all = [];
7948
- for (const [file, sigs] of sigIndex.entries()) all.push({ file, score: sigs.length, sigs, tokens: Math.ceil(sigs.join('\n').length / 4) });
8411
+ for (const [file, sigs] of sigIndex.entries()) {
8412
+ all.push({ file, score: sigs.length, sigs, tokens: Math.ceil(sigs.join('\n').length / 4), intent, signals: {} });
8413
+ }
7949
8414
  all.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
7950
8415
  return all.slice(0, topK);
7951
8416
  }
8417
+
7952
8418
  const scored = [];
7953
8419
  for (const [file, sigs] of sigIndex.entries()) {
7954
- let score = scoreFile(file, sigs, queryTokens, weights);
7955
- if (recencySet && recencySet.has(file) && score > 0) score *= recencyMultiplier;
7956
- if (learnedWeights && score > 0) score *= learnedWeights[file] || 1.0;
7957
- scored.push({ file, score, sigs, tokens: Math.ceil(sigs.join('\n').length / 4) });
8420
+ const result = scoreFile(file, sigs, queryTokens, weights);
8421
+ let score = result.score;
8422
+ const signals = result.signals;
8423
+
8424
+ // Recency boost
8425
+ if (recencySet && recencySet.has(file) && score > 0) {
8426
+ score *= recencyMultiplier;
8427
+ signals.recencyBoost = recencyMultiplier;
8428
+ }
8429
+
8430
+ if (learnedWeights && score > 0) {
8431
+ const multiplier = learnedWeights[file] || 1.0;
8432
+ score *= multiplier;
8433
+ signals.learnedWeights = multiplier;
8434
+ }
8435
+
8436
+ scored.push({
8437
+ file,
8438
+ score,
8439
+ sigs,
8440
+ tokens: Math.ceil(sigs.join('\n').length / 4),
8441
+ intent,
8442
+ signals,
8443
+ });
8444
+ }
8445
+
8446
+ // Graph neighbor boost: 2-hop traversal with decay (v6.7)
8447
+ // Hop 1: add hop1 amount to direct import neighbors (score > 0)
8448
+ // Hop 2: add hop2 amount to neighbors of hop1 files (with decay)
8449
+ // Hub suppression: files with high fanout (>20%) are not boosted
8450
+ if (graph && cwd) {
8451
+ const path = require('path');
8452
+ // Build maps for relative ↔ absolute path conversion and index lookup
8453
+ const relToIdx = new Map();
8454
+ const absToRel = new Map();
8455
+ for (let i = 0; i < scored.length; i++) {
8456
+ relToIdx.set(scored[i].file, i);
8457
+ const abs = path.resolve(cwd, scored[i].file);
8458
+ absToRel.set(abs, scored[i].file);
8459
+ }
8460
+
8461
+ const hubs = _computeHubs(graph);
8462
+ const hop1Files = new Set(); // track which files received hop1 boost
8463
+
8464
+ // Hop 1: direct neighbors of scored files
8465
+ for (const entry of scored) {
8466
+ if (entry.score <= 0) continue;
8467
+ const abs = path.resolve(cwd, entry.file);
8468
+ const neighbors = graph.forward.get(abs) || [];
8469
+ for (const neighborAbs of neighbors) {
8470
+ if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
8471
+ const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
8472
+ const idx = relToIdx.get(neighborRel);
8473
+ if (idx !== undefined) {
8474
+ scored[idx].score += GRAPH_BOOST_AMOUNTS.hop1;
8475
+ scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop1;
8476
+ hop1Files.add(neighborAbs);
8477
+ }
8478
+ }
8479
+ }
8480
+
8481
+ // Hop 2: neighbors of hop1 files (only if they didn't get a direct score)
8482
+ for (const hop1File of hop1Files) {
8483
+ if (!absToRel.has(hop1File)) continue; // skip files not in index
8484
+ const neighbors = graph.forward.get(hop1File) || [];
8485
+ for (const neighborAbs of neighbors) {
8486
+ if (_isHub(neighborAbs) || hubs.has(neighborAbs)) continue;
8487
+ if (hop1Files.has(neighborAbs)) continue; // skip already hop1-boosted
8488
+ const neighborRel = path.relative(cwd, neighborAbs).replace(/\\/g, '/');
8489
+ const idx = relToIdx.get(neighborRel);
8490
+ if (idx !== undefined && scored[idx].score > 0) {
8491
+ // Only boost files that have some baseline score (not noise)
8492
+ scored[idx].score += GRAPH_BOOST_AMOUNTS.hop2;
8493
+ scored[idx].signals.graphBoost = (scored[idx].signals.graphBoost || 0) + GRAPH_BOOST_AMOUNTS.hop2;
8494
+ }
8495
+ }
8496
+ }
7958
8497
  }
8498
+
7959
8499
  // Compute confidence levels based on score distribution
7960
8500
  if (scored.length > 0) {
7961
8501
  const scores = scored.map(s => s.score);
7962
8502
  const maxScore = Math.max(...scores);
7963
8503
  const minScore = Math.min(...scores);
7964
8504
  const scoreRange = maxScore - minScore || 1;
8505
+
8506
+ // Confidence tiers: top 33% = high, next 33% = medium, rest = low
7965
8507
  for (const entry of scored) {
7966
8508
  if (entry.score <= 0) {
7967
8509
  entry.confidence = 'low';
@@ -7971,39 +8513,73 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
7971
8513
  }
7972
8514
  }
7973
8515
  }
8516
+
7974
8517
  scored.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
7975
8518
  return scored.slice(0, topK);
7976
8519
  }
8520
+
8521
+ /**
8522
+ * All paths where sigmap adapters write their context files, in probe order.
8523
+ * The first existing file with a non-empty index wins when no explicit path
8524
+ * is supplied.
8525
+ */
7977
8526
  const ADAPTER_OUTPUT_PATHS = [
7978
- ['.github', 'copilot-instructions.md'],
7979
- ['CLAUDE.md'],
7980
- ['AGENTS.md'],
7981
- ['.cursorrules'],
7982
- ['.windsurfrules'],
7983
- ['.github', 'openai-context.md'],
7984
- ['.github', 'gemini-context.md'],
7985
- ['llm-full.txt'],
7986
- ['llm.txt'],
8527
+ ['.github', 'copilot-instructions.md'], // copilot (default)
8528
+ ['CLAUDE.md'], // claude
8529
+ ['AGENTS.md'], // codex
8530
+ ['.cursorrules'], // cursor
8531
+ ['.windsurfrules'], // windsurf
8532
+ ['.github', 'openai-context.md'], // openai
8533
+ ['.github', 'gemini-context.md'], // gemini
8534
+ ['llm-full.txt'], // llm-full
8535
+ ['llm.txt'], // llm
7987
8536
  ];
8537
+
8538
+ /**
8539
+ * Parse a single context file into a Map<filePath, string[]>.
8540
+ *
8541
+ * Files that contain human-written content before an
8542
+ * "## Auto-generated signatures" marker (e.g. CLAUDE.md) are handled
8543
+ * by skipping everything above the marker before scanning for ### headers.
8544
+ *
8545
+ * @param {string} contextPath - absolute path to the context file
8546
+ * @returns {Map<string, string[]>}
8547
+ */
7988
8548
  function _parseContextFile(contextPath) {
7989
8549
  const fs = require('fs');
7990
8550
  const index = new Map();
8551
+
7991
8552
  if (!fs.existsSync(contextPath)) return index;
8553
+
7992
8554
  let content = fs.readFileSync(contextPath, 'utf8');
8555
+
8556
+ // Skip any human-written preamble that sits above the auto-generated block.
7993
8557
  const markerIdx = content.indexOf('## Auto-generated signatures');
7994
8558
  if (markerIdx !== -1) content = content.slice(markerIdx);
8559
+
7995
8560
  const lines = content.split('\n');
7996
- let currentFile = null; let inBlock = false; let sigs = [];
8561
+ let currentFile = null;
8562
+ let inBlock = false;
8563
+ let sigs = [];
8564
+
7997
8565
  for (const line of lines) {
7998
- const hm = line.match(/^###\s+(\S+)\s*$/);
7999
- if (hm) { if (currentFile !== null) index.set(currentFile, sigs); currentFile = hm[1]; sigs = []; inBlock = false; continue; }
8566
+ const headerMatch = line.match(/^###\s+(\S+)\s*$/);
8567
+ if (headerMatch) {
8568
+ if (currentFile !== null) index.set(currentFile, sigs);
8569
+ currentFile = headerMatch[1];
8570
+ sigs = [];
8571
+ inBlock = false;
8572
+ continue;
8573
+ }
8000
8574
  if (line.startsWith('```')) { inBlock = !inBlock; continue; }
8001
8575
  if (inBlock && currentFile && line.trim()) sigs.push(line.trim());
8002
8576
  }
8003
8577
  if (currentFile !== null) index.set(currentFile, sigs);
8578
+
8004
8579
  return index;
8005
8580
  }
8006
8581
 
8582
+ /** Merge source index into target; prefer non-empty sig lists. */
8007
8583
  function _mergeSigIndex(target, source) {
8008
8584
  for (const [file, sigs] of source.entries()) {
8009
8585
  if (!sigs || sigs.length === 0) continue;
@@ -8014,12 +8590,17 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
8014
8590
  return target;
8015
8591
  }
8016
8592
 
8593
+ /**
8594
+ * Load signatures from .sigmap-cache.json (absolute paths → repo-relative keys).
8595
+ * @param {string} cwd
8596
+ * @returns {Map<string, string[]>}
8597
+ */
8017
8598
  function _buildSigIndexFromCache(cwd) {
8018
8599
  const fs = require('fs');
8019
8600
  const path = require('path');
8020
8601
  const index = new Map();
8021
8602
  try {
8022
- const { loadCache } = require('../cache/sig-cache');
8603
+ const { loadCache } = __require('./src/cache/sig-cache');
8023
8604
  const pkgPath = path.join(cwd, 'package.json');
8024
8605
  let version = '0.0.0';
8025
8606
  if (fs.existsSync(pkgPath)) {
@@ -8036,6 +8617,12 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
8036
8617
  return index;
8037
8618
  }
8038
8619
 
8620
+ /**
8621
+ * Hot-cold and per-module strategies store most signatures outside the primary
8622
+ * copilot-instructions.md file. MCP tools must merge all sources.
8623
+ * @param {string} cwd
8624
+ * @returns {Map<string, string[]>}
8625
+ */
8039
8626
  function _enrichSigIndexFromStrategy(cwd, index) {
8040
8627
  const path = require('path');
8041
8628
  const coldPath = path.join(cwd, '.github', 'context-cold.md');
@@ -8044,49 +8631,138 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
8044
8631
  return index;
8045
8632
  }
8046
8633
 
8634
+ /**
8635
+ * Build a signature index from the generated context file.
8636
+ * Returns Map<filePath, string[]> where filePath is the relative path
8637
+ * as it appears in the ### headers of the context file.
8638
+ *
8639
+ * Resolution priority:
8640
+ * 1. `opts.contextPath` — explicit path from --output or --adapter flag
8641
+ * 2. `customOutput` key in gen-context.config.json — persisted from a
8642
+ * previous `--output <file>` generation run
8643
+ * 3. All known adapter output paths probed in order (first non-empty wins)
8644
+ *
8645
+ * @param {string} cwd
8646
+ * @param {{ contextPath?: string }} [opts]
8647
+ * @returns {Map<string, string[]>}
8648
+ */
8047
8649
  function buildSigIndex(cwd, opts) {
8048
- const fs = require('fs'); const path = require('path');
8049
- if (opts && opts.contextPath) return _enrichSigIndexFromStrategy(cwd, _parseContextFile(opts.contextPath));
8050
- // Check gen-context.config.json for a persisted customOutput path.
8650
+ const fs = require('fs');
8651
+ const path = require('path');
8652
+
8653
+ // 1. Caller supplied an explicit path — use it directly.
8654
+ if (opts && opts.contextPath) {
8655
+ const index = _parseContextFile(opts.contextPath);
8656
+ return _enrichSigIndexFromStrategy(cwd, index);
8657
+ }
8658
+
8659
+ // 2. Check gen-context.config.json for a persisted customOutput path.
8051
8660
  try {
8052
8661
  const cfgPath = path.join(cwd, 'gen-context.config.json');
8053
8662
  if (fs.existsSync(cfgPath)) {
8054
8663
  const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
8055
8664
  if (cfg.customOutput) {
8056
- const idx = _parseContextFile(path.resolve(cwd, cfg.customOutput));
8057
- if (idx.size > 0) return _enrichSigIndexFromStrategy(cwd, idx);
8665
+ const customPath = path.resolve(cwd, cfg.customOutput);
8666
+ const index = _parseContextFile(customPath);
8667
+ if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
8058
8668
  }
8059
8669
  }
8060
8670
  } catch (_) {}
8671
+
8672
+ // 3. Probe all known adapter output paths; return first non-empty index.
8061
8673
  for (const parts of ADAPTER_OUTPUT_PATHS) {
8062
8674
  const contextPath = path.join(cwd, ...parts);
8063
8675
  const index = _parseContextFile(contextPath);
8064
8676
  if (index.size > 0) return _enrichSigIndexFromStrategy(cwd, index);
8065
8677
  }
8066
- return _enrichSigIndexFromStrategy(cwd, new Map());
8678
+
8679
+ // 4. Primary file empty/missing (hot-cold) — still serve cold + cache.
8680
+ const fallback = new Map();
8681
+ return _enrichSigIndexFromStrategy(cwd, fallback);
8067
8682
  }
8683
+
8684
+ /**
8685
+ * Format ranked results as a markdown table string.
8686
+ *
8687
+ * @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
8688
+ * @param {string} query
8689
+ * @returns {string}
8690
+ */
8068
8691
  function formatRankTable(results, query) {
8069
- if (!results || results.length === 0) return `No matching files found for query: "${query}"\n`;
8070
- const lines = [`## Query: ${query}`, '', '| Rank | File | Score | Sigs | Tokens |', '|------|------|-------|------|--------|',
8071
- ...results.map((r, i) => `| ${i + 1} | ${r.file} | ${r.score.toFixed(2)} | ${r.sigs.length} | ${r.tokens} |`), ''];
8692
+ if (!results || results.length === 0) {
8693
+ return `No matching files found for query: "${query}"\n`;
8694
+ }
8695
+
8696
+ const intent = (results[0] && results[0].intent) || 'search';
8697
+ const lines = [
8698
+ `## Query: ${query}`,
8699
+ `Intent: ${intent}`,
8700
+ '',
8701
+ '| Rank | File | Score | Sigs | Penalty |',
8702
+ '|------|------|-------|------|---------|',
8703
+ ...results.map((r, i) => {
8704
+ const penalty = r.signals && r.signals.penalty ? r.signals.penalty.toFixed(2) : '1.00';
8705
+ return `| ${i + 1} | ${r.file} | ${r.score.toFixed(2)} | ${r.sigs.length} | ${penalty} |`;
8706
+ }),
8707
+ '',
8708
+ ];
8709
+
8710
+ // Add signature details for top results
8072
8711
  for (const r of results.slice(0, 3)) {
8073
8712
  if (r.sigs.length > 0) {
8074
- lines.push(`### ${r.file}`, '```', ...r.sigs.slice(0, 10));
8713
+ lines.push(`### ${r.file}`);
8714
+ if (r.signals) {
8715
+ const sig = r.signals;
8716
+ lines.push(`Signals: exactToken=${(sig.exactToken || 0).toFixed(2)} symbolMatch=${(sig.symbolMatch || 0).toFixed(2)} prefixMatch=${(sig.prefixMatch || 0).toFixed(2)} pathMatch=${(sig.pathMatch || 0).toFixed(2)} penalty=${(sig.penalty || 1).toFixed(2)}`);
8717
+ }
8718
+ lines.push('```');
8719
+ lines.push(...r.sigs.slice(0, 10));
8075
8720
  if (r.sigs.length > 10) lines.push(`... (${r.sigs.length - 10} more)`);
8076
- lines.push('```', '');
8721
+ lines.push('```');
8722
+ lines.push('');
8077
8723
  }
8078
8724
  }
8725
+
8079
8726
  return lines.join('\n');
8080
8727
  }
8728
+
8729
+ /**
8730
+ * Format ranked results as a structured JSON-serialisable object.
8731
+ *
8732
+ * @param {{ file: string, score: number, sigs: string[], tokens: number, intent: string, signals: object }[]} results
8733
+ * @param {string} query
8734
+ * @returns {object}
8735
+ */
8081
8736
  function formatRankJSON(results, query) {
8082
- return { query, results: (results || []).map((r, i) => ({ rank: i + 1, file: r.file, score: r.score, sigs: r.sigs, tokens: r.tokens })), totalResults: (results || []).length };
8737
+ const intent = (results && results[0] && results[0].intent) || 'search';
8738
+ return {
8739
+ query,
8740
+ intent,
8741
+ results: (results || []).map((r, i) => ({
8742
+ rank: i + 1,
8743
+ file: r.file,
8744
+ score: r.score,
8745
+ sigs: r.sigs,
8746
+ tokens: r.tokens,
8747
+ signals: r.signals || {},
8748
+ })),
8749
+ totalResults: (results || []).length,
8750
+ };
8083
8751
  }
8752
+
8753
+ // ---------------------------------------------------------------------------
8754
+ // Intent detection — 7 intents
8755
+ // ---------------------------------------------------------------------------
8084
8756
  const INTENT_PATTERNS = {
8085
8757
  debug: /\b(bug|fix|error|crash|exception|broken|failing|issue|problem|regression)\b/i,
8086
- explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me)\b/i,
8087
- refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify)\b/i,
8088
- review: /\b(review|check|audit|security|pr|pull request|assess)\b/i,
8758
+ explain: /\b(explain|how does|what is|understand|overview|architecture|describe|walk me|teach)\b/i,
8759
+ refactor: /\b(refactor|restructure|redesign|clean up|extract|move|rename|simplify|optimize)\b/i,
8760
+ review: /\b(review|check|audit|security|pr|pull request|assess|validate)\b/i,
8761
+ test: /\b(test|unit test|integration test|testing|spec|assert|mock)\b/i,
8762
+ integrate:/\b(import|integrate|connect|wire|bind|require|export|depend|graph)\b|require[ds]\b/i,
8763
+ navigate: /\b(find|locate|where|search|look for|show me|navigate|browse|list)\b/i,
8089
8764
  };
8765
+
8090
8766
  function detectIntent(query) {
8091
8767
  if (!query || typeof query !== 'string') return 'search';
8092
8768
  for (const [intent, re] of Object.entries(INTENT_PATTERNS)) {
@@ -8094,9 +8770,10 @@ __factories["./src/retrieval/ranker"] = function(module, exports) {
8094
8770
  }
8095
8771
  return 'search';
8096
8772
  }
8097
- module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, detectIntent };
8098
- };
8099
8773
 
8774
+ module.exports = { rank, buildSigIndex, scoreFile, formatRankTable, formatRankJSON, DEFAULT_WEIGHTS, GRAPH_BOOST_AMOUNTS, detectIntent };
8775
+
8776
+ };
8100
8777
  // ── ./src/eval/scorer ──
8101
8778
  __factories["./src/eval/scorer"] = function(module, exports) {
8102
8779
  'use strict';
@@ -11499,7 +12176,7 @@ function __tryGit(args, opts = {}) {
11499
12176
  catch (_) { return ''; }
11500
12177
  }
11501
12178
 
11502
- const VERSION = '7.2.1';
12179
+ const VERSION = '7.4.0';
11503
12180
  const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
11504
12181
 
11505
12182
  function requireSourceOrBundled(key) {