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/CHANGELOG.md +21 -0
- package/gen-context.js +1479 -802
- package/llms-full.txt +37 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/src/extractors/dispatch.js +112 -0
- package/src/mcp/handlers.js +130 -1
- package/src/mcp/server.js +6 -2
- package/src/mcp/tools.js +65 -2
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
|
-
|
|
5532
|
-
|
|
5533
|
-
const
|
|
5534
|
-
const
|
|
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
|
|
5570
|
-
const
|
|
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
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
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
|
-
|
|
5661
|
+
return chunks.join('\n');
|
|
5587
5662
|
}
|
|
5588
5663
|
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
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
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
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
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
if (
|
|
5615
|
-
|
|
5616
|
-
|
|
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
|
|
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
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
function
|
|
5633
|
-
|
|
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
|
-
|
|
5636
|
-
|
|
5637
|
-
|
|
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
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
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
|
-
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
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
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
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
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
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
|
-
|
|
5676
|
-
|
|
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
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
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
|
-
|
|
5688
|
-
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
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
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
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
|
-
//
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
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
|
-
|
|
5710
|
-
|
|
5711
|
-
lines.push(
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
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
|
-
|
|
5736
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
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
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
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
|
-
|
|
5778
|
-
|
|
5779
|
-
const
|
|
5780
|
-
const
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
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
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
function explainFile(args, cwd) {
|
|
5794
|
-
|
|
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
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
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
|
-
|
|
5916
|
+
const lines = ['# explain_file: ' + targetRel, ''];
|
|
5801
5917
|
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5918
|
+
// ── Signatures (hot + cold + cache via buildSigIndex) ───────────────────
|
|
5919
|
+
lines.push('## Signatures');
|
|
5920
|
+
let indexedFiles = [];
|
|
5805
5921
|
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
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
|
-
|
|
5819
|
-
|
|
5820
|
-
|
|
5821
|
-
lines.push('
|
|
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
|
-
|
|
5835
|
-
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
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
|
-
|
|
5969
|
+
lines.push('');
|
|
5854
5970
|
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
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
|
-
|
|
5880
|
-
}
|
|
5995
|
+
return lines.join('\n');
|
|
5996
|
+
}
|
|
5881
5997
|
|
|
5882
|
-
/**
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
|
|
5886
|
-
|
|
5887
|
-
|
|
5888
|
-
function listModules(args, cwd) {
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
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
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
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
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
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
|
-
|
|
6025
|
+
if (sorted.length === 0) return 'No modules found in context file.';
|
|
5910
6026
|
|
|
5911
|
-
|
|
6027
|
+
const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
|
|
5912
6028
|
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
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
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
try {
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
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
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
function getImpact(args, cwd) {
|
|
5962
|
-
|
|
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
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
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
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
function getLines(args, cwd) {
|
|
5983
|
-
|
|
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
|
-
|
|
5986
|
-
|
|
6101
|
+
const rel = String(args.file).replace(/\\/g, '/').replace(/^\//, '');
|
|
6102
|
+
const abs = path.resolve(cwd, rel);
|
|
5987
6103
|
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
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
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
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
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
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
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
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
|
-
|
|
6159
|
+
const out = ['# SigMap memory'];
|
|
6016
6160
|
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
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
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
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
|
-
//
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
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
|
-
}
|
|
6278
|
+
}
|
|
6071
6279
|
|
|
6072
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
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/list → 11 tool definitions
|
|
6492
|
+
* tools/call → dispatch to handler, return result
|
|
6493
|
+
*/
|
|
6273
6494
|
|
|
6274
|
-
|
|
6275
|
-
|
|
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
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
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
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
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
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
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
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6518
|
+
// ---------------------------------------------------------------------------
|
|
6519
|
+
// Method dispatcher
|
|
6520
|
+
// ---------------------------------------------------------------------------
|
|
6521
|
+
function dispatch(msg, cwd) {
|
|
6522
|
+
const { method, id, params } = msg;
|
|
6302
6523
|
|
|
6303
|
-
|
|
6304
|
-
|
|
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
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
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
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
}
|
|
6538
|
+
if (method === 'tools/list') {
|
|
6539
|
+
respond(id, { tools: TOOLS });
|
|
6540
|
+
return;
|
|
6541
|
+
}
|
|
6336
6542
|
|
|
6337
|
-
|
|
6338
|
-
|
|
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
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
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
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
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
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
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
|
-
|
|
6364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
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
|
-
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
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
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
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
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
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
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
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
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
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
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
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
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
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
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
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
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
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
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
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
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
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
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
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
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
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
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
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
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
7926
|
-
|
|
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)) {
|
|
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
|
-
|
|
8363
|
+
|
|
8364
|
+
// Path token match
|
|
8365
|
+
if (pathTokenSet.has(qt)) {
|
|
8366
|
+
score += w.pathMatch;
|
|
8367
|
+
signals.pathMatch += w.pathMatch;
|
|
8368
|
+
}
|
|
7934
8369
|
}
|
|
7935
|
-
|
|
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
|
|
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())
|
|
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
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
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;
|
|
8561
|
+
let currentFile = null;
|
|
8562
|
+
let inBlock = false;
|
|
8563
|
+
let sigs = [];
|
|
8564
|
+
|
|
7997
8565
|
for (const line of lines) {
|
|
7998
|
-
const
|
|
7999
|
-
if (
|
|
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 } =
|
|
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
|
|
8049
|
-
|
|
8050
|
-
|
|
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
|
|
8057
|
-
|
|
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
|
-
|
|
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)
|
|
8070
|
-
|
|
8071
|
-
|
|
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}
|
|
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
|
-
|
|
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.
|
|
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) {
|