sigmap 1.5.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/.contextignore.example +34 -0
- package/CHANGELOG.md +402 -0
- package/LICENSE +21 -0
- package/README.md +601 -0
- package/gen-context.config.json.example +40 -0
- package/gen-context.js +4316 -0
- package/gen-project-map.js +172 -0
- package/package.json +67 -0
- package/src/config/defaults.js +61 -0
- package/src/config/loader.js +60 -0
- package/src/extractors/cpp.js +60 -0
- package/src/extractors/csharp.js +48 -0
- package/src/extractors/css.js +51 -0
- package/src/extractors/dart.js +58 -0
- package/src/extractors/dockerfile.js +49 -0
- package/src/extractors/go.js +61 -0
- package/src/extractors/html.js +39 -0
- package/src/extractors/java.js +49 -0
- package/src/extractors/javascript.js +82 -0
- package/src/extractors/kotlin.js +62 -0
- package/src/extractors/php.js +62 -0
- package/src/extractors/python.js +69 -0
- package/src/extractors/ruby.js +43 -0
- package/src/extractors/rust.js +72 -0
- package/src/extractors/scala.js +67 -0
- package/src/extractors/shell.js +43 -0
- package/src/extractors/svelte.js +51 -0
- package/src/extractors/swift.js +63 -0
- package/src/extractors/typescript.js +109 -0
- package/src/extractors/vue.js +66 -0
- package/src/extractors/yaml.js +59 -0
- package/src/format/cache.js +53 -0
- package/src/health/scorer.js +123 -0
- package/src/map/class-hierarchy.js +117 -0
- package/src/map/import-graph.js +148 -0
- package/src/map/route-table.js +127 -0
- package/src/mcp/handlers.js +433 -0
- package/src/mcp/server.js +128 -0
- package/src/mcp/tools.js +125 -0
- package/src/routing/classifier.js +102 -0
- package/src/routing/hints.js +103 -0
- package/src/security/patterns.js +51 -0
- package/src/security/scanner.js +36 -0
- package/src/tracking/logger.js +115 -0
package/gen-context.js
ADDED
|
@@ -0,0 +1,4316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// SigMap — standalone bundle (generated by scripts/bundle.js)
|
|
4
|
+
// Copy this single file to any project: node gen-context.js --setup
|
|
5
|
+
// No src/ directory required. Node.js 18+ only. Zero npm dependencies.
|
|
6
|
+
|
|
7
|
+
const __factories = {};
|
|
8
|
+
const __cache = {};
|
|
9
|
+
function __require(key) {
|
|
10
|
+
if (Object.prototype.hasOwnProperty.call(__cache, key)) {
|
|
11
|
+
return __cache[key].exports;
|
|
12
|
+
}
|
|
13
|
+
if (!Object.prototype.hasOwnProperty.call(__factories, key)) {
|
|
14
|
+
throw new Error('[sigmap] bundled module not found: ' + key);
|
|
15
|
+
}
|
|
16
|
+
const module = { exports: {} };
|
|
17
|
+
__cache[key] = module;
|
|
18
|
+
__factories[key](module, module.exports);
|
|
19
|
+
return module.exports;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
// ── ./src/config/defaults ──
|
|
24
|
+
__factories["./src/config/defaults"] = function(module, exports) {
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Default configuration values for SigMap.
|
|
28
|
+
* All keys documented here. Override via gen-context.config.json.
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULTS = {
|
|
31
|
+
// Primary output file (used when outputs includes 'copilot')
|
|
32
|
+
output: '.github/copilot-instructions.md',
|
|
33
|
+
|
|
34
|
+
// Output targets: 'copilot' | 'claude' | 'cursor' | 'windsurf'
|
|
35
|
+
outputs: ['copilot'],
|
|
36
|
+
|
|
37
|
+
// Directories to scan (relative to project root)
|
|
38
|
+
srcDirs: ['src', 'app', 'lib', 'packages', 'services', 'api'],
|
|
39
|
+
|
|
40
|
+
// Directory/file names to exclude entirely
|
|
41
|
+
exclude: [
|
|
42
|
+
'node_modules', '.git', 'dist', 'build', 'out',
|
|
43
|
+
'__pycache__', '.next', 'coverage', 'target', 'vendor',
|
|
44
|
+
'.context',
|
|
45
|
+
],
|
|
46
|
+
|
|
47
|
+
// Maximum directory depth to recurse
|
|
48
|
+
maxDepth: 6,
|
|
49
|
+
|
|
50
|
+
// Maximum signatures extracted per file
|
|
51
|
+
maxSigsPerFile: 25,
|
|
52
|
+
|
|
53
|
+
// Maximum tokens in final output before budget enforcement kicks in
|
|
54
|
+
maxTokens: 6000,
|
|
55
|
+
|
|
56
|
+
// Scan signatures for secrets and redact matches
|
|
57
|
+
secretScan: true,
|
|
58
|
+
|
|
59
|
+
// Auto-detect monorepo packages and write per-package output files
|
|
60
|
+
monorepo: false,
|
|
61
|
+
|
|
62
|
+
// Sort recently git-committed files higher in output
|
|
63
|
+
diffPriority: true,
|
|
64
|
+
|
|
65
|
+
// Debounce delay (ms) between file-system events and regeneration in watch mode
|
|
66
|
+
watchDebounce: 300,
|
|
67
|
+
|
|
68
|
+
// Append model routing hints section to the context output
|
|
69
|
+
// Routes files to fast/balanced/powerful model tiers based on complexity
|
|
70
|
+
routing: false,
|
|
71
|
+
|
|
72
|
+
// Output format: 'default' (markdown only) | 'cache' (also write Anthropic prompt-cache JSON)
|
|
73
|
+
format: 'default',
|
|
74
|
+
|
|
75
|
+
// Append run metrics to .context/usage.ndjson after each generate
|
|
76
|
+
tracking: false,
|
|
77
|
+
|
|
78
|
+
// MCP server configuration
|
|
79
|
+
mcp: {
|
|
80
|
+
autoRegister: true,
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Context strategy controls how the output is split and injected.
|
|
84
|
+
//
|
|
85
|
+
// 'full' — single file, all signatures (default, works everywhere)
|
|
86
|
+
//
|
|
87
|
+
// 'per-module' — one output file per top-level srcDir, e.g.
|
|
88
|
+
// .github/context-server.md, .github/context-web.md
|
|
89
|
+
// Best when IDEs can choose which file to inject per workspace.
|
|
90
|
+
// Zero context lost — all files are still covered.
|
|
91
|
+
// ~70% token reduction per question.
|
|
92
|
+
//
|
|
93
|
+
// 'hot-cold' — two files:
|
|
94
|
+
// hot: .github/copilot-instructions.md (recently changed files only)
|
|
95
|
+
// cold: .github/context-cold.md (all other files, MCP on-demand)
|
|
96
|
+
// Best when MCP is available (Claude Code, Cursor).
|
|
97
|
+
// ~90% token reduction but cold-file context requires MCP to retrieve.
|
|
98
|
+
strategy: 'full',
|
|
99
|
+
|
|
100
|
+
// For 'hot-cold' strategy: how many recent git commits count as "hot"
|
|
101
|
+
hotCommits: 10,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
module.exports = { DEFAULTS };
|
|
105
|
+
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// ── ./src/config/loader ──
|
|
109
|
+
__factories["./src/config/loader"] = function(module, exports) {
|
|
110
|
+
|
|
111
|
+
const fs = require('fs');
|
|
112
|
+
const path = require('path');
|
|
113
|
+
const { DEFAULTS } = __require('./src/config/defaults');
|
|
114
|
+
|
|
115
|
+
// Keys that are valid in gen-context.config.json
|
|
116
|
+
const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Load and merge configuration for a given working directory.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} cwd - Project root directory
|
|
122
|
+
* @returns {object} Merged config (DEFAULTS + user overrides)
|
|
123
|
+
*/
|
|
124
|
+
function loadConfig(cwd) {
|
|
125
|
+
const configPath = path.join(cwd, 'gen-context.config.json');
|
|
126
|
+
if (!fs.existsSync(configPath)) {
|
|
127
|
+
return deepClone(DEFAULTS);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let userConfig;
|
|
131
|
+
try {
|
|
132
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
133
|
+
userConfig = JSON.parse(raw);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.warn(`[sigmap] config parse error in ${configPath}: ${err.message}`);
|
|
136
|
+
return deepClone(DEFAULTS);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Warn on unknown keys (helps catch typos)
|
|
140
|
+
for (const key of Object.keys(userConfig)) {
|
|
141
|
+
if (key.startsWith('_')) continue; // allow _comment etc.
|
|
142
|
+
if (!KNOWN_KEYS.has(key)) {
|
|
143
|
+
console.warn(`[sigmap] unknown config key: "${key}" (ignored)`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Deep merge: top-level known keys from user override defaults
|
|
148
|
+
// For object values (e.g. mcp), merge one level deep
|
|
149
|
+
const merged = deepClone(DEFAULTS);
|
|
150
|
+
for (const key of Object.keys(userConfig)) {
|
|
151
|
+
if (key.startsWith('_')) continue;
|
|
152
|
+
if (!KNOWN_KEYS.has(key)) continue; // skip unknown keys
|
|
153
|
+
const val = userConfig[key];
|
|
154
|
+
if (val !== null && typeof val === 'object' && !Array.isArray(val) &&
|
|
155
|
+
typeof merged[key] === 'object' && !Array.isArray(merged[key])) {
|
|
156
|
+
merged[key] = Object.assign({}, merged[key], val);
|
|
157
|
+
} else {
|
|
158
|
+
merged[key] = val;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return merged;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function deepClone(obj) {
|
|
165
|
+
return JSON.parse(JSON.stringify(obj));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = { loadConfig };
|
|
169
|
+
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// ── ./src/extractors/cpp ──
|
|
173
|
+
__factories["./src/extractors/cpp"] = function(module, exports) {
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Extract signatures from C/C++ source code.
|
|
177
|
+
* @param {string} src - Raw file content
|
|
178
|
+
* @returns {string[]} Array of signature strings
|
|
179
|
+
*/
|
|
180
|
+
function extract(src) {
|
|
181
|
+
if (!src || typeof src !== 'string') return [];
|
|
182
|
+
const sigs = [];
|
|
183
|
+
|
|
184
|
+
const stripped = src
|
|
185
|
+
.replace(/\/\/.*$/gm, '')
|
|
186
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
187
|
+
|
|
188
|
+
// Classes and structs
|
|
189
|
+
const classRe = /^(?:class|struct)\s+(\w+)(?:\s*:\s*(?:public|protected|private)\s+[\w:]+)?\s*\{/gm;
|
|
190
|
+
for (const m of stripped.matchAll(classRe)) {
|
|
191
|
+
const kind = m[0].trimStart().startsWith('class') ? 'class' : 'struct';
|
|
192
|
+
sigs.push(`${kind} ${m[1]}`);
|
|
193
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
194
|
+
for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Top-level function declarations/definitions (not inside a class)
|
|
198
|
+
for (const m of stripped.matchAll(/^(?!class|struct|if|for|while|switch)[\w:*&<> ]+\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?\{/gm)) {
|
|
199
|
+
if (m[1].startsWith('_')) continue;
|
|
200
|
+
sigs.push(`${m[1]}(${normalizeParams(m[2])})`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return sigs.slice(0, 25);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function extractBlock(src, startIndex) {
|
|
207
|
+
let depth = 1, i = startIndex;
|
|
208
|
+
const end = Math.min(src.length, startIndex + 4000);
|
|
209
|
+
while (i < end && depth > 0) {
|
|
210
|
+
if (src[i] === '{') depth++;
|
|
211
|
+
else if (src[i] === '}') depth--;
|
|
212
|
+
i++;
|
|
213
|
+
}
|
|
214
|
+
return src.slice(startIndex, i - 1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function extractMembers(block) {
|
|
218
|
+
const members = [];
|
|
219
|
+
const methodRe = /^\s+(?:virtual\s+|static\s+|inline\s+)?(?!private:|protected:|public:)[\w:*&<> ]+\s+(\w+)\s*\(([^)]*)\)\s*(?:const\s*)?(?:override\s*)?(?:=\s*0\s*)?;/gm;
|
|
220
|
+
for (const m of block.matchAll(methodRe)) {
|
|
221
|
+
if (m[1].startsWith('_')) continue;
|
|
222
|
+
members.push(`${m[1]}(${normalizeParams(m[2])})`);
|
|
223
|
+
}
|
|
224
|
+
return members.slice(0, 8);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeParams(params) {
|
|
228
|
+
if (!params) return '';
|
|
229
|
+
return params.trim().replace(/\s+/g, ' ');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = { extract };
|
|
233
|
+
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// ── ./src/extractors/csharp ──
|
|
237
|
+
__factories["./src/extractors/csharp"] = function(module, exports) {
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Extract signatures from C# source code.
|
|
241
|
+
* @param {string} src - Raw file content
|
|
242
|
+
* @returns {string[]} Array of signature strings
|
|
243
|
+
*/
|
|
244
|
+
function extract(src) {
|
|
245
|
+
if (!src || typeof src !== 'string') return [];
|
|
246
|
+
const sigs = [];
|
|
247
|
+
|
|
248
|
+
const stripped = src
|
|
249
|
+
.replace(/\/\/.*$/gm, '')
|
|
250
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
251
|
+
|
|
252
|
+
// Classes and interfaces
|
|
253
|
+
const typeRe = /^\s*(?:public\s+|internal\s+|protected\s+)?(?:abstract\s+|sealed\s+|static\s+)?(class|interface|enum|record|struct)\s+(\w+)(?:<[^{]*>)?(?:\s*:\s*[\w<>, .]+)?\s*\{/gm;
|
|
254
|
+
for (const m of stripped.matchAll(typeRe)) {
|
|
255
|
+
sigs.push(`${m[1]} ${m[2]}`);
|
|
256
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
257
|
+
for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return sigs.slice(0, 25);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function extractBlock(src, startIndex) {
|
|
264
|
+
let depth = 1, i = startIndex;
|
|
265
|
+
const end = Math.min(src.length, startIndex + 5000);
|
|
266
|
+
while (i < end && depth > 0) {
|
|
267
|
+
if (src[i] === '{') depth++;
|
|
268
|
+
else if (src[i] === '}') depth--;
|
|
269
|
+
i++;
|
|
270
|
+
}
|
|
271
|
+
return src.slice(startIndex, i - 1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function extractMembers(block) {
|
|
275
|
+
const members = [];
|
|
276
|
+
const methodRe = /^\s+(?:public|internal|protected)\s+(?:static\s+|virtual\s+|override\s+|async\s+)*(?:[\w<>\[\]?]+\s+)+(\w+)\s*\(([^)]*)\)/gm;
|
|
277
|
+
for (const m of block.matchAll(methodRe)) {
|
|
278
|
+
const sig = m[0].trim().split('{')[0].trim();
|
|
279
|
+
members.push(sig);
|
|
280
|
+
}
|
|
281
|
+
return members.slice(0, 8);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = { extract };
|
|
285
|
+
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// ── ./src/extractors/css ──
|
|
289
|
+
__factories["./src/extractors/css"] = function(module, exports) {
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Extract signatures from CSS/SCSS/SASS/Less source code.
|
|
293
|
+
* @param {string} src - Raw file content
|
|
294
|
+
* @returns {string[]} Array of signature strings
|
|
295
|
+
*/
|
|
296
|
+
function extract(src) {
|
|
297
|
+
if (!src || typeof src !== 'string') return [];
|
|
298
|
+
const sigs = [];
|
|
299
|
+
|
|
300
|
+
const stripped = src
|
|
301
|
+
.replace(/\/\/.*$/gm, '')
|
|
302
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
303
|
+
|
|
304
|
+
// CSS custom properties (variables)
|
|
305
|
+
const rootMatch = stripped.match(/:root\s*\{([^}]*)\}/);
|
|
306
|
+
if (rootMatch) {
|
|
307
|
+
for (const m of rootMatch[1].matchAll(/(--[\w-]+)\s*:/g)) {
|
|
308
|
+
sigs.push(`var ${m[1]}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// SCSS/Less variables
|
|
313
|
+
for (const m of stripped.matchAll(/^(\$[\w-]+)\s*:/gm)) {
|
|
314
|
+
sigs.push(`$var ${m[1]}`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// SCSS mixins
|
|
318
|
+
for (const m of stripped.matchAll(/^@mixin\s+([\w-]+)(?:\s*\(([^)]*)\))?/gm)) {
|
|
319
|
+
const params = m[2] ? `(${m[2].trim()})` : '';
|
|
320
|
+
sigs.push(`@mixin ${m[1]}${params}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// SCSS functions
|
|
324
|
+
for (const m of stripped.matchAll(/^@function\s+([\w-]+)\s*\(([^)]*)\)/gm)) {
|
|
325
|
+
sigs.push(`@function ${m[1]}(${m[2].trim()})`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Key class names (top-level)
|
|
329
|
+
const classNames = new Set();
|
|
330
|
+
for (const m of stripped.matchAll(/^\.([\w-]+)(?=[^{]*\{)/gm)) {
|
|
331
|
+
classNames.add(m[1]);
|
|
332
|
+
if (classNames.size >= 10) break;
|
|
333
|
+
}
|
|
334
|
+
for (const name of classNames) sigs.push(`.${name}`);
|
|
335
|
+
|
|
336
|
+
return sigs.slice(0, 25);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = { extract };
|
|
340
|
+
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// ── ./src/extractors/dart ──
|
|
344
|
+
__factories["./src/extractors/dart"] = function(module, exports) {
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Extract signatures from Dart source code.
|
|
348
|
+
* @param {string} src - Raw file content
|
|
349
|
+
* @returns {string[]} Array of signature strings
|
|
350
|
+
*/
|
|
351
|
+
function extract(src) {
|
|
352
|
+
if (!src || typeof src !== 'string') return [];
|
|
353
|
+
const sigs = [];
|
|
354
|
+
|
|
355
|
+
const stripped = src
|
|
356
|
+
.replace(/\/\/.*$/gm, '')
|
|
357
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
358
|
+
|
|
359
|
+
// Classes and abstract classes
|
|
360
|
+
for (const m of stripped.matchAll(/^(?:abstract\s+)?class\s+(\w+)(?:<[^{]*>)?(?:\s+extends\s+[\w<>, ]+)?(?:\s+(?:implements|with|on)\s+[\w<>, ]+)?\s*\{/gm)) {
|
|
361
|
+
const abs = m[0].trimStart().startsWith('abstract') ? 'abstract ' : '';
|
|
362
|
+
sigs.push(`${abs}class ${m[1]}`);
|
|
363
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
364
|
+
for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Top-level functions
|
|
368
|
+
for (const m of stripped.matchAll(/^(?:Future|void|[\w<>?]+)\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
369
|
+
if (m[1].startsWith('_')) continue;
|
|
370
|
+
sigs.push(`${m[1]}(${normalizeParams(m[2])})`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return sigs.slice(0, 25);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function extractBlock(src, startIndex) {
|
|
377
|
+
let depth = 1, i = startIndex;
|
|
378
|
+
const end = Math.min(src.length, startIndex + 4000);
|
|
379
|
+
while (i < end && depth > 0) {
|
|
380
|
+
if (src[i] === '{') depth++;
|
|
381
|
+
else if (src[i] === '}') depth--;
|
|
382
|
+
i++;
|
|
383
|
+
}
|
|
384
|
+
return src.slice(startIndex, i - 1);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function extractMembers(block) {
|
|
388
|
+
const members = [];
|
|
389
|
+
for (const m of block.matchAll(/^\s+(?:@override\s+)?(?:Future|void|[\w<>?]+)\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
390
|
+
if (m[1].startsWith('_')) continue;
|
|
391
|
+
members.push(`${m[1]}(${normalizeParams(m[2])})`);
|
|
392
|
+
}
|
|
393
|
+
return members.slice(0, 8);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function normalizeParams(params) {
|
|
397
|
+
if (!params) return '';
|
|
398
|
+
return params.trim().replace(/\{[^}]*\}/g, '').replace(/\s+/g, ' ').trim();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
module.exports = { extract };
|
|
402
|
+
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// ── ./src/extractors/dockerfile ──
|
|
406
|
+
__factories["./src/extractors/dockerfile"] = function(module, exports) {
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Extract signatures from Dockerfiles.
|
|
410
|
+
* @param {string} src - Raw file content
|
|
411
|
+
* @returns {string[]} Array of signature strings
|
|
412
|
+
*/
|
|
413
|
+
function extract(src) {
|
|
414
|
+
if (!src || typeof src !== 'string') return [];
|
|
415
|
+
const sigs = [];
|
|
416
|
+
|
|
417
|
+
const lines = src.split('\n').filter((l) => l.trim() && !l.trimStart().startsWith('#'));
|
|
418
|
+
|
|
419
|
+
// FROM stages
|
|
420
|
+
for (const line of lines) {
|
|
421
|
+
const m = line.match(/^FROM\s+([^\s]+)(?:\s+AS\s+(\w+))?/i);
|
|
422
|
+
if (m) sigs.push(`FROM ${m[1]}${m[2] ? ` AS ${m[2]}` : ''}`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// EXPOSE ports
|
|
426
|
+
const exposePorts = [];
|
|
427
|
+
for (const line of lines) {
|
|
428
|
+
const m = line.match(/^EXPOSE\s+([\d\s/]+)/i);
|
|
429
|
+
if (m) exposePorts.push(...m[1].trim().split(/\s+/));
|
|
430
|
+
}
|
|
431
|
+
if (exposePorts.length > 0) sigs.push(`EXPOSE ${exposePorts.join(' ')}`);
|
|
432
|
+
|
|
433
|
+
// ENTRYPOINT and CMD
|
|
434
|
+
for (const line of lines) {
|
|
435
|
+
if (/^ENTRYPOINT\s+/i.test(line)) sigs.push(line.trim());
|
|
436
|
+
if (/^CMD\s+/i.test(line)) sigs.push(line.trim());
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ENV variables
|
|
440
|
+
for (const line of lines) {
|
|
441
|
+
const m = line.match(/^ENV\s+([\w]+)/i);
|
|
442
|
+
if (m) sigs.push(`ENV ${m[1]}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ARG variables
|
|
446
|
+
for (const line of lines) {
|
|
447
|
+
const m = line.match(/^ARG\s+([\w]+)/i);
|
|
448
|
+
if (m) sigs.push(`ARG ${m[1]}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return sigs.slice(0, 25);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
module.exports = { extract };
|
|
455
|
+
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// ── ./src/extractors/go ──
|
|
459
|
+
__factories["./src/extractors/go"] = function(module, exports) {
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Extract signatures from Go source code.
|
|
463
|
+
* @param {string} src - Raw file content
|
|
464
|
+
* @returns {string[]} Array of signature strings
|
|
465
|
+
*/
|
|
466
|
+
function extract(src) {
|
|
467
|
+
if (!src || typeof src !== 'string') return [];
|
|
468
|
+
const sigs = [];
|
|
469
|
+
|
|
470
|
+
const stripped = src
|
|
471
|
+
.replace(/\/\/.*$/gm, '')
|
|
472
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
473
|
+
|
|
474
|
+
// Structs
|
|
475
|
+
for (const m of stripped.matchAll(/^type\s+(\w+)\s+struct\s*\{/gm)) {
|
|
476
|
+
sigs.push(`type ${m[1]} struct`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Interfaces
|
|
480
|
+
for (const m of stripped.matchAll(/^type\s+(\w+)\s+interface\s*\{/gm)) {
|
|
481
|
+
sigs.push(`type ${m[1]} interface`);
|
|
482
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
483
|
+
for (const method of extractInterfaceMethods(block)) sigs.push(` ${method}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Functions and methods
|
|
487
|
+
for (const m of stripped.matchAll(/^func\s+(?:\((\w+)\s+[\w*]+\)\s+)?(\w+)\s*\(([^)]*)\)(?:\s*[\w*()\[\],\s]+)?\s*\{/gm)) {
|
|
488
|
+
const receiver = m[1] ? `(${m[1]}) ` : '';
|
|
489
|
+
sigs.push(`func ${receiver}${m[2]}(${normalizeParams(m[3])})`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return sigs.slice(0, 25);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function extractBlock(src, startIndex) {
|
|
496
|
+
let depth = 1, i = startIndex;
|
|
497
|
+
const end = Math.min(src.length, startIndex + 2000);
|
|
498
|
+
while (i < end && depth > 0) {
|
|
499
|
+
if (src[i] === '{') depth++;
|
|
500
|
+
else if (src[i] === '}') depth--;
|
|
501
|
+
i++;
|
|
502
|
+
}
|
|
503
|
+
return src.slice(startIndex, i - 1);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function extractInterfaceMethods(block) {
|
|
507
|
+
const methods = [];
|
|
508
|
+
for (const m of block.matchAll(/^\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
509
|
+
methods.push(`${m[1]}(${normalizeParams(m[2])})`);
|
|
510
|
+
}
|
|
511
|
+
return methods.slice(0, 8);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function normalizeParams(params) {
|
|
515
|
+
if (!params) return '';
|
|
516
|
+
return params.trim().replace(/\s+/g, ' ');
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
module.exports = { extract };
|
|
520
|
+
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// ── ./src/extractors/html ──
|
|
524
|
+
__factories["./src/extractors/html"] = function(module, exports) {
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Extract signatures from HTML files.
|
|
528
|
+
* Focuses on id/class attributes, forms, and script tags.
|
|
529
|
+
* @param {string} src - Raw file content
|
|
530
|
+
* @returns {string[]} Array of signature strings
|
|
531
|
+
*/
|
|
532
|
+
function extract(src) {
|
|
533
|
+
if (!src || typeof src !== 'string') return [];
|
|
534
|
+
const sigs = [];
|
|
535
|
+
|
|
536
|
+
// Page title
|
|
537
|
+
const titleMatch = src.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
538
|
+
if (titleMatch) sigs.push(`title: ${titleMatch[1].trim()}`);
|
|
539
|
+
|
|
540
|
+
// Forms with id/action
|
|
541
|
+
for (const m of src.matchAll(/<form\s+([^>]*)>/gi)) {
|
|
542
|
+
const attrs = m[1];
|
|
543
|
+
const id = attrs.match(/id=["']?(\w+)/i);
|
|
544
|
+
const action = attrs.match(/action=["']?([^"'\s>]+)/i);
|
|
545
|
+
if (id) sigs.push(`form#${id[1]}${action ? ` action="${action[1]}"` : ''}`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Elements with id
|
|
549
|
+
for (const m of src.matchAll(/<(\w+)\s+[^>]*id=["'](\w+)["'][^>]*>/gi)) {
|
|
550
|
+
if (['html', 'head', 'body', 'script', 'style', 'link', 'meta'].includes(m[1].toLowerCase())) continue;
|
|
551
|
+
sigs.push(`${m[1]}#${m[2]}`);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Data attributes (data-component, data-controller etc)
|
|
555
|
+
for (const m of src.matchAll(/data-(?:component|controller|view|page)=["'](\w[\w-]*)/gi)) {
|
|
556
|
+
sigs.push(`data-${m[0].match(/data-(\w[\w-]*)/i)[1]}: ${m[1]}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return sigs.slice(0, 25);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
module.exports = { extract };
|
|
563
|
+
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// ── ./src/extractors/java ──
|
|
567
|
+
__factories["./src/extractors/java"] = function(module, exports) {
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Extract signatures from Java source code.
|
|
571
|
+
* @param {string} src - Raw file content
|
|
572
|
+
* @returns {string[]} Array of signature strings
|
|
573
|
+
*/
|
|
574
|
+
function extract(src) {
|
|
575
|
+
if (!src || typeof src !== 'string') return [];
|
|
576
|
+
const sigs = [];
|
|
577
|
+
|
|
578
|
+
const stripped = src
|
|
579
|
+
.replace(/\/\/.*$/gm, '')
|
|
580
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
581
|
+
|
|
582
|
+
// Classes and interfaces
|
|
583
|
+
const typeRegex = /^(?:public\s+|protected\s+)?(?:abstract\s+|final\s+)?(class|interface|enum)\s+(\w+)(?:\s+extends\s+[\w<>, .]+)?(?:\s+implements\s+[\w<>, .]+)?\s*\{/gm;
|
|
584
|
+
for (const m of stripped.matchAll(typeRegex)) {
|
|
585
|
+
sigs.push(`${m[1]} ${m[2]}`);
|
|
586
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
587
|
+
for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return sigs.slice(0, 25);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function extractBlock(src, startIndex) {
|
|
594
|
+
let depth = 1;
|
|
595
|
+
let i = startIndex;
|
|
596
|
+
const end = Math.min(src.length, startIndex + 5000);
|
|
597
|
+
while (i < end && depth > 0) {
|
|
598
|
+
if (src[i] === '{') depth++;
|
|
599
|
+
else if (src[i] === '}') depth--;
|
|
600
|
+
i++;
|
|
601
|
+
}
|
|
602
|
+
return src.slice(startIndex, i - 1);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function extractMembers(block) {
|
|
606
|
+
const members = [];
|
|
607
|
+
const methodRe = /^\s+(?:public|protected)\s+(?:static\s+)?(?:final\s+)?(?:[\w<>\[\]]+\s+)+(\w+)\s*\(([^)]*)\)/gm;
|
|
608
|
+
for (const m of block.matchAll(methodRe)) {
|
|
609
|
+
const sig = m[0].trim().split('{')[0].trim();
|
|
610
|
+
members.push(sig);
|
|
611
|
+
}
|
|
612
|
+
return members.slice(0, 8);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
module.exports = { extract };
|
|
616
|
+
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// ── ./src/extractors/javascript ──
|
|
620
|
+
__factories["./src/extractors/javascript"] = function(module, exports) {
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Extract signatures from JavaScript source code.
|
|
624
|
+
* @param {string} src - Raw file content
|
|
625
|
+
* @returns {string[]} Array of signature strings
|
|
626
|
+
*/
|
|
627
|
+
function extract(src) {
|
|
628
|
+
if (!src || typeof src !== 'string') return [];
|
|
629
|
+
const sigs = [];
|
|
630
|
+
|
|
631
|
+
const stripped = src
|
|
632
|
+
.replace(/\/\/.*$/gm, '')
|
|
633
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
634
|
+
|
|
635
|
+
// Classes
|
|
636
|
+
const classRegex = /^(export\s+(?:default\s+)?)?class\s+(\w+)(?:\s+extends\s+[\w.]+)?\s*\{/gm;
|
|
637
|
+
for (const m of stripped.matchAll(classRegex)) {
|
|
638
|
+
const prefix = m[1] ? m[1].trim() + ' ' : '';
|
|
639
|
+
sigs.push(`${prefix}class ${m[2]}`);
|
|
640
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
641
|
+
for (const meth of extractClassMembers(block)) sigs.push(` ${meth}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Exported named functions
|
|
645
|
+
for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
646
|
+
const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
|
|
647
|
+
sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})`);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Exported arrow functions
|
|
651
|
+
for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/gm)) {
|
|
652
|
+
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
653
|
+
sigs.push(`export const ${m[1]} = ${asyncKw}(${normalizeParams(m[2])}) =>`);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// module.exports = { ... }
|
|
657
|
+
const moduleExports = stripped.match(/^module\.exports\s*=\s*\{([^}]+)\}/m);
|
|
658
|
+
if (moduleExports) {
|
|
659
|
+
const names = moduleExports[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
660
|
+
if (names.length > 0) sigs.push(`module.exports = { ${names.join(', ')} }`);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Top-level named functions (non-exported)
|
|
664
|
+
for (const m of stripped.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
665
|
+
const asyncKw = m[0].startsWith('async') ? 'async ' : '';
|
|
666
|
+
sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})`);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return sigs.slice(0, 25);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function extractBlock(src, startIndex) {
|
|
673
|
+
let depth = 1;
|
|
674
|
+
let i = startIndex;
|
|
675
|
+
const end = Math.min(src.length, startIndex + 4000);
|
|
676
|
+
while (i < end && depth > 0) {
|
|
677
|
+
if (src[i] === '{') depth++;
|
|
678
|
+
else if (src[i] === '}') depth--;
|
|
679
|
+
i++;
|
|
680
|
+
}
|
|
681
|
+
return src.slice(startIndex, i - 1);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function extractClassMembers(block) {
|
|
685
|
+
const members = [];
|
|
686
|
+
for (const m of block.matchAll(/^\s+(?:static\s+|async\s+|get\s+|set\s+)*(\w+)\s*\(([^)]*)\)\s*\{/gm)) {
|
|
687
|
+
if (/^_/.test(m[1])) continue;
|
|
688
|
+
if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
|
|
689
|
+
const isAsync = m[0].includes('async ') ? 'async ' : '';
|
|
690
|
+
const isStatic = m[0].includes('static ') ? 'static ' : '';
|
|
691
|
+
members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})`);
|
|
692
|
+
}
|
|
693
|
+
return members.slice(0, 8);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function normalizeParams(params) {
|
|
697
|
+
if (!params) return '';
|
|
698
|
+
return params.trim().replace(/\s+/g, ' ');
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
module.exports = { extract };
|
|
702
|
+
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
// ── ./src/extractors/kotlin ──
|
|
706
|
+
__factories["./src/extractors/kotlin"] = function(module, exports) {
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Extract signatures from Kotlin source code.
|
|
710
|
+
* @param {string} src - Raw file content
|
|
711
|
+
* @returns {string[]} Array of signature strings
|
|
712
|
+
*/
|
|
713
|
+
function extract(src) {
|
|
714
|
+
if (!src || typeof src !== 'string') return [];
|
|
715
|
+
const sigs = [];
|
|
716
|
+
|
|
717
|
+
const stripped = src
|
|
718
|
+
.replace(/\/\/.*$/gm, '')
|
|
719
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
720
|
+
|
|
721
|
+
// Classes, objects, interfaces
|
|
722
|
+
for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:data\s+|sealed\s+|abstract\s+|open\s+)?(class|object|interface)\s+(\w+)(?:[^{]*)\{/gm)) {
|
|
723
|
+
sigs.push(`${m[1]} ${m[2]}`);
|
|
724
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
725
|
+
for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Top-level functions
|
|
729
|
+
for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)/gm)) {
|
|
730
|
+
const suspend = m[0].includes('suspend') ? 'suspend ' : '';
|
|
731
|
+
sigs.push(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})`);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return sigs.slice(0, 25);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function extractBlock(src, startIndex) {
|
|
738
|
+
let depth = 1, i = startIndex;
|
|
739
|
+
const end = Math.min(src.length, startIndex + 4000);
|
|
740
|
+
while (i < end && depth > 0) {
|
|
741
|
+
if (src[i] === '{') depth++;
|
|
742
|
+
else if (src[i] === '}') depth--;
|
|
743
|
+
i++;
|
|
744
|
+
}
|
|
745
|
+
return src.slice(startIndex, i - 1);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function extractMembers(block) {
|
|
749
|
+
const members = [];
|
|
750
|
+
for (const m of block.matchAll(/^\s+(?:public\s+|internal\s+|override\s+)?(?:suspend\s+)?fun\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)/gm)) {
|
|
751
|
+
if (m[1].startsWith('_')) continue;
|
|
752
|
+
const suspend = m[0].includes('suspend') ? 'suspend ' : '';
|
|
753
|
+
members.push(`${suspend}fun ${m[1]}(${normalizeParams(m[2])})`);
|
|
754
|
+
}
|
|
755
|
+
return members.slice(0, 8);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function normalizeParams(params) {
|
|
759
|
+
if (!params) return '';
|
|
760
|
+
return params.trim()
|
|
761
|
+
.split(',')
|
|
762
|
+
.map((p) => p.trim().split(':')[0].trim())
|
|
763
|
+
.filter(Boolean)
|
|
764
|
+
.join(', ');
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
module.exports = { extract };
|
|
768
|
+
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
// ── ./src/extractors/php ──
|
|
772
|
+
__factories["./src/extractors/php"] = function(module, exports) {
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Extract signatures from PHP source code.
|
|
776
|
+
* @param {string} src - Raw file content
|
|
777
|
+
* @returns {string[]} Array of signature strings
|
|
778
|
+
*/
|
|
779
|
+
function extract(src) {
|
|
780
|
+
if (!src || typeof src !== 'string') return [];
|
|
781
|
+
const sigs = [];
|
|
782
|
+
|
|
783
|
+
const stripped = src
|
|
784
|
+
.replace(/\/\/.*$/gm, '')
|
|
785
|
+
.replace(/#.*$/gm, '')
|
|
786
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
787
|
+
|
|
788
|
+
// Classes and interfaces
|
|
789
|
+
const typeRe = /^(?:abstract\s+)?(?:class|interface|trait)\s+(\w+)(?:\s+extends\s+\w+)?(?:\s+implements\s+[\w, ]+)?\s*\{/gm;
|
|
790
|
+
for (const m of stripped.matchAll(typeRe)) {
|
|
791
|
+
const kind = m[0].trimStart().startsWith('interface') ? 'interface' :
|
|
792
|
+
m[0].trimStart().startsWith('trait') ? 'trait' : 'class';
|
|
793
|
+
sigs.push(`${kind} ${m[1]}`);
|
|
794
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
795
|
+
for (const meth of extractMembers(block)) sigs.push(` ${meth}`);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Top-level functions
|
|
799
|
+
for (const m of stripped.matchAll(/^function\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
800
|
+
sigs.push(`function ${m[1]}(${normalizeParams(m[2])})`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return sigs.slice(0, 25);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function extractBlock(src, startIndex) {
|
|
807
|
+
let depth = 1, i = startIndex;
|
|
808
|
+
const end = Math.min(src.length, startIndex + 4000);
|
|
809
|
+
while (i < end && depth > 0) {
|
|
810
|
+
if (src[i] === '{') depth++;
|
|
811
|
+
else if (src[i] === '}') depth--;
|
|
812
|
+
i++;
|
|
813
|
+
}
|
|
814
|
+
return src.slice(startIndex, i - 1);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function extractMembers(block) {
|
|
818
|
+
const members = [];
|
|
819
|
+
const methodRe = /^\s+(?:public|protected)\s+(?:static\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm;
|
|
820
|
+
for (const m of block.matchAll(methodRe)) {
|
|
821
|
+
if (m[1].startsWith('_')) continue;
|
|
822
|
+
const isStatic = m[0].includes('static ') ? 'static ' : '';
|
|
823
|
+
members.push(`${isStatic}function ${m[1]}(${normalizeParams(m[2])})`);
|
|
824
|
+
}
|
|
825
|
+
return members.slice(0, 8);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function normalizeParams(params) {
|
|
829
|
+
if (!params) return '';
|
|
830
|
+
return params.trim().replace(/\s+/g, ' ');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
module.exports = { extract };
|
|
834
|
+
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// ── ./src/extractors/python ──
|
|
838
|
+
__factories["./src/extractors/python"] = function(module, exports) {
|
|
839
|
+
|
|
840
|
+
/**
|
|
841
|
+
* Extract signatures from Python source code.
|
|
842
|
+
* @param {string} src - Raw file content
|
|
843
|
+
* @returns {string[]} Array of signature strings
|
|
844
|
+
*/
|
|
845
|
+
function extract(src) {
|
|
846
|
+
if (!src || typeof src !== 'string') return [];
|
|
847
|
+
const sigs = [];
|
|
848
|
+
|
|
849
|
+
// Strip comments and docstrings (simple approach)
|
|
850
|
+
const stripped = src
|
|
851
|
+
.replace(/#.*$/gm, '')
|
|
852
|
+
.replace(/"""[\s\S]*?"""/g, '')
|
|
853
|
+
.replace(/'''[\s\S]*?'''/g, '');
|
|
854
|
+
|
|
855
|
+
// Classes
|
|
856
|
+
for (const m of stripped.matchAll(/^class\s+(\w+)(?:\s*\(([^)]*)\))?\s*:/gm)) {
|
|
857
|
+
const base = m[2] ? `(${m[2].trim()})` : '';
|
|
858
|
+
sigs.push(`class ${m[1]}${base}`);
|
|
859
|
+
// Get class body methods
|
|
860
|
+
const bodyStart = m.index + m[0].length;
|
|
861
|
+
const methods = extractClassMethods(stripped, bodyStart);
|
|
862
|
+
for (const meth of methods) sigs.push(` ${meth}`);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Top-level functions
|
|
866
|
+
for (const m of stripped.matchAll(/^(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
867
|
+
if (/^_/.test(m[1])) continue; // skip private
|
|
868
|
+
const asyncKw = m[0].trimStart().startsWith('async') ? 'async ' : '';
|
|
869
|
+
const params = normalizeParams(m[2]);
|
|
870
|
+
sigs.push(`${asyncKw}def ${m[1]}(${params})`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return sigs.slice(0, 25);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function extractClassMethods(src, startIndex) {
|
|
877
|
+
const methods = [];
|
|
878
|
+
// Extract indented block
|
|
879
|
+
const lines = src.slice(startIndex).split('\n');
|
|
880
|
+
for (const line of lines) {
|
|
881
|
+
if (line.trim() === '') continue;
|
|
882
|
+
// End of class body: line with no leading indent that is not blank
|
|
883
|
+
const indent = line.match(/^(\s+)/);
|
|
884
|
+
if (!indent) break;
|
|
885
|
+
const m = line.match(/^\s+(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/);
|
|
886
|
+
if (m) {
|
|
887
|
+
if (m[1].startsWith('__') && m[1] !== '__init__') continue;
|
|
888
|
+
if (m[1].startsWith('_') && !m[1].startsWith('__')) continue;
|
|
889
|
+
const asyncKw = line.trimStart().startsWith('async') ? 'async ' : '';
|
|
890
|
+
const params = normalizeParams(m[2]).replace(/^self,?\s*/, '');
|
|
891
|
+
methods.push(`${asyncKw}def ${m[1]}(${params})`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return methods.slice(0, 8);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function normalizeParams(params) {
|
|
898
|
+
if (!params) return '';
|
|
899
|
+
return params.trim()
|
|
900
|
+
.split(',')
|
|
901
|
+
.map((p) => p.trim().split(':')[0].split('=')[0].trim())
|
|
902
|
+
.filter(Boolean)
|
|
903
|
+
.join(', ');
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
module.exports = { extract };
|
|
907
|
+
|
|
908
|
+
};
|
|
909
|
+
|
|
910
|
+
// ── ./src/extractors/ruby ──
|
|
911
|
+
__factories["./src/extractors/ruby"] = function(module, exports) {
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Extract signatures from Ruby source code.
|
|
915
|
+
* @param {string} src - Raw file content
|
|
916
|
+
* @returns {string[]} Array of signature strings
|
|
917
|
+
*/
|
|
918
|
+
function extract(src) {
|
|
919
|
+
if (!src || typeof src !== 'string') return [];
|
|
920
|
+
const sigs = [];
|
|
921
|
+
|
|
922
|
+
const stripped = src.replace(/#.*$/gm, '');
|
|
923
|
+
|
|
924
|
+
// Modules and classes
|
|
925
|
+
for (const m of stripped.matchAll(/^(?:module|class)\s+([\w:]+)(?:\s*<\s*[\w:]+)?\s*$/gm)) {
|
|
926
|
+
const kind = m[0].trimStart().startsWith('module') ? 'module' : 'class';
|
|
927
|
+
sigs.push(`${kind} ${m[1]}`);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Public methods (not private/protected)
|
|
931
|
+
for (const m of stripped.matchAll(/^\s+def\s+(?:self\.)?(\w+)(?:\s*\(([^)]*)\))?/gm)) {
|
|
932
|
+
if (m[1].startsWith('_')) continue;
|
|
933
|
+
const params = m[2] ? `(${normalizeParams(m[2])})` : '';
|
|
934
|
+
const selfPrefix = m[0].includes('self.') ? 'self.' : '';
|
|
935
|
+
sigs.push(` def ${selfPrefix}${m[1]}${params}`);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Top-level def
|
|
939
|
+
for (const m of stripped.matchAll(/^def\s+(\w+)(?:\s*\(([^)]*)\))?/gm)) {
|
|
940
|
+
if (m[1].startsWith('_')) continue;
|
|
941
|
+
const params = m[2] ? `(${normalizeParams(m[2])})` : '';
|
|
942
|
+
sigs.push(`def ${m[1]}${params}`);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return sigs.slice(0, 25);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function normalizeParams(params) {
|
|
949
|
+
if (!params) return '';
|
|
950
|
+
return params.trim().replace(/\s+/g, ' ');
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
module.exports = { extract };
|
|
954
|
+
|
|
955
|
+
};
|
|
956
|
+
|
|
957
|
+
// ── ./src/extractors/rust ──
|
|
958
|
+
__factories["./src/extractors/rust"] = function(module, exports) {
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Extract signatures from Rust source code.
|
|
962
|
+
* @param {string} src - Raw file content
|
|
963
|
+
* @returns {string[]} Array of signature strings
|
|
964
|
+
*/
|
|
965
|
+
function extract(src) {
|
|
966
|
+
if (!src || typeof src !== 'string') return [];
|
|
967
|
+
const sigs = [];
|
|
968
|
+
|
|
969
|
+
const stripped = src
|
|
970
|
+
.replace(/\/\/.*$/gm, '')
|
|
971
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
972
|
+
|
|
973
|
+
// Structs
|
|
974
|
+
for (const m of stripped.matchAll(/^pub\s+struct\s+(\w+)(?:<[^{]*>)?/gm)) {
|
|
975
|
+
sigs.push(`pub struct ${m[1]}`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Enums
|
|
979
|
+
for (const m of stripped.matchAll(/^pub\s+enum\s+(\w+)(?:<[^{]*>)?/gm)) {
|
|
980
|
+
sigs.push(`pub enum ${m[1]}`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Traits
|
|
984
|
+
for (const m of stripped.matchAll(/^pub\s+trait\s+(\w+)(?:<[^{]*>)?/gm)) {
|
|
985
|
+
sigs.push(`pub trait ${m[1]}`);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// impl blocks
|
|
989
|
+
for (const m of stripped.matchAll(/^impl(?:<[^>]*>)?\s+(?:[\w:]+\s+for\s+)?(\w+)(?:<[^{]*>)?\s*\{/gm)) {
|
|
990
|
+
sigs.push(`impl ${m[1]}`);
|
|
991
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
992
|
+
for (const fn of extractMethods(block)) sigs.push(` ${fn}`);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Top-level pub fns
|
|
996
|
+
for (const m of stripped.matchAll(/^pub(?:\s+async)?\s+fn\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)/gm)) {
|
|
997
|
+
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
998
|
+
sigs.push(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})`);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return sigs.slice(0, 25);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function extractBlock(src, startIndex) {
|
|
1005
|
+
let depth = 1, i = startIndex;
|
|
1006
|
+
const end = Math.min(src.length, startIndex + 5000);
|
|
1007
|
+
while (i < end && depth > 0) {
|
|
1008
|
+
if (src[i] === '{') depth++;
|
|
1009
|
+
else if (src[i] === '}') depth--;
|
|
1010
|
+
i++;
|
|
1011
|
+
}
|
|
1012
|
+
return src.slice(startIndex, i - 1);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function extractMethods(block) {
|
|
1016
|
+
const methods = [];
|
|
1017
|
+
for (const m of block.matchAll(/^\s+pub(?:\s+async)?\s+fn\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)/gm)) {
|
|
1018
|
+
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
1019
|
+
methods.push(`pub ${asyncKw}fn ${m[1]}(${normalizeParams(m[2])})`);
|
|
1020
|
+
}
|
|
1021
|
+
return methods.slice(0, 8);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function normalizeParams(params) {
|
|
1025
|
+
if (!params) return '';
|
|
1026
|
+
return params.trim().replace(/\s+/g, ' ');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
module.exports = { extract };
|
|
1030
|
+
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
// ── ./src/extractors/scala ──
|
|
1034
|
+
__factories["./src/extractors/scala"] = function(module, exports) {
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Extract signatures from Scala source code.
|
|
1038
|
+
* @param {string} src - Raw file content
|
|
1039
|
+
* @returns {string[]} Array of signature strings
|
|
1040
|
+
*/
|
|
1041
|
+
function extract(src) {
|
|
1042
|
+
if (!src || typeof src !== 'string') return [];
|
|
1043
|
+
const sigs = [];
|
|
1044
|
+
|
|
1045
|
+
const stripped = src
|
|
1046
|
+
.replace(/\/\/.*$/gm, '')
|
|
1047
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1048
|
+
|
|
1049
|
+
// Classes, traits, objects
|
|
1050
|
+
const typeRe = /^(?:case\s+)?(?:class|trait|object)\s+(\w+)(?:\[[\w, ]+\])?(?:[^{]*)\{/gm;
|
|
1051
|
+
for (const m of stripped.matchAll(typeRe)) {
|
|
1052
|
+
const kind = m[0].trimStart().startsWith('case class') ? 'case class' :
|
|
1053
|
+
m[0].trimStart().startsWith('trait') ? 'trait' :
|
|
1054
|
+
m[0].trimStart().startsWith('object') ? 'object' : 'class';
|
|
1055
|
+
sigs.push(`${kind} ${m[1]}`);
|
|
1056
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
1057
|
+
for (const fn of extractMembers(block)) sigs.push(` ${fn}`);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Top-level defs
|
|
1061
|
+
for (const m of stripped.matchAll(/^def\s+(\w+)(?:\[[\w, ]+\])?\s*(?:\(([^)]*)\))?/gm)) {
|
|
1062
|
+
if (m[1].startsWith('_')) continue;
|
|
1063
|
+
const params = m[2] ? `(${normalizeParams(m[2])})` : '';
|
|
1064
|
+
sigs.push(`def ${m[1]}${params}`);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
return sigs.slice(0, 25);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function extractBlock(src, startIndex) {
|
|
1071
|
+
let depth = 1, i = startIndex;
|
|
1072
|
+
const end = Math.min(src.length, startIndex + 4000);
|
|
1073
|
+
while (i < end && depth > 0) {
|
|
1074
|
+
if (src[i] === '{') depth++;
|
|
1075
|
+
else if (src[i] === '}') depth--;
|
|
1076
|
+
i++;
|
|
1077
|
+
}
|
|
1078
|
+
return src.slice(startIndex, i - 1);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function extractMembers(block) {
|
|
1082
|
+
const members = [];
|
|
1083
|
+
for (const m of block.matchAll(/^\s+def\s+(\w+)(?:\[[\w, ]+\])?\s*(?:\(([^)]*)\))?/gm)) {
|
|
1084
|
+
if (m[1].startsWith('_')) continue;
|
|
1085
|
+
const params = m[2] ? `(${normalizeParams(m[2])})` : '';
|
|
1086
|
+
members.push(`def ${m[1]}${params}`);
|
|
1087
|
+
}
|
|
1088
|
+
return members.slice(0, 8);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function normalizeParams(params) {
|
|
1092
|
+
if (!params) return '';
|
|
1093
|
+
return params.trim()
|
|
1094
|
+
.split(',')
|
|
1095
|
+
.map((p) => p.trim().split(':')[0].trim())
|
|
1096
|
+
.filter(Boolean)
|
|
1097
|
+
.join(', ');
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
module.exports = { extract };
|
|
1101
|
+
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
// ── ./src/extractors/shell ──
|
|
1105
|
+
__factories["./src/extractors/shell"] = function(module, exports) {
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Extract signatures from shell scripts (bash, zsh, fish).
|
|
1109
|
+
* @param {string} src - Raw file content
|
|
1110
|
+
* @returns {string[]} Array of signature strings
|
|
1111
|
+
*/
|
|
1112
|
+
function extract(src) {
|
|
1113
|
+
if (!src || typeof src !== 'string') return [];
|
|
1114
|
+
const sigs = [];
|
|
1115
|
+
|
|
1116
|
+
const stripped = src.replace(/#.*$/gm, '');
|
|
1117
|
+
|
|
1118
|
+
// Function definitions (bash: name() { and function name {)
|
|
1119
|
+
for (const m of stripped.matchAll(/^(?:function\s+)?([\w:-]+)\s*\(\s*\)\s*\{/gm)) {
|
|
1120
|
+
if (m[1].startsWith('_')) continue;
|
|
1121
|
+
sigs.push(`function ${m[1]}()`);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Main entry point patterns
|
|
1125
|
+
if (/^\s*main\s*\(/m.test(src) || /^\s*main\s+"?\$@/m.test(src)) {
|
|
1126
|
+
sigs.push('main "$@"');
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Exported variables
|
|
1130
|
+
for (const m of stripped.matchAll(/^export\s+([\w]+)=/gm)) {
|
|
1131
|
+
sigs.push(`export ${m[1]}`);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Script description (first non-comment line after shebang)
|
|
1135
|
+
const lines = src.split('\n');
|
|
1136
|
+
for (const line of lines.slice(0, 5)) {
|
|
1137
|
+
if (line.startsWith('#!')) continue;
|
|
1138
|
+
if (line.startsWith('#')) {
|
|
1139
|
+
const desc = line.replace(/^#+\s*/, '').trim();
|
|
1140
|
+
if (desc) { sigs.unshift(`# ${desc}`); break; }
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
return sigs.slice(0, 25);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
module.exports = { extract };
|
|
1148
|
+
|
|
1149
|
+
};
|
|
1150
|
+
|
|
1151
|
+
// ── ./src/extractors/svelte ──
|
|
1152
|
+
__factories["./src/extractors/svelte"] = function(module, exports) {
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* Extract signatures from Svelte components.
|
|
1156
|
+
* @param {string} src - Raw file content
|
|
1157
|
+
* @returns {string[]} Array of signature strings
|
|
1158
|
+
*/
|
|
1159
|
+
function extract(src) {
|
|
1160
|
+
if (!src || typeof src !== 'string') return [];
|
|
1161
|
+
const sigs = [];
|
|
1162
|
+
|
|
1163
|
+
// Extract <script> block
|
|
1164
|
+
const scriptMatch = src.match(/<script(?:\s[^>]*)?>(?:\s*)([\s\S]*?)<\/script>/i);
|
|
1165
|
+
if (!scriptMatch) return sigs;
|
|
1166
|
+
|
|
1167
|
+
const script = scriptMatch[1]
|
|
1168
|
+
.replace(/\/\/.*$/gm, '')
|
|
1169
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1170
|
+
|
|
1171
|
+
// Exported props (writable)
|
|
1172
|
+
for (const m of script.matchAll(/^\s+export\s+let\s+(\w+)(?:\s*=\s*[^;]+)?;/gm)) {
|
|
1173
|
+
sigs.push(`export let ${m[1]}`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Exported functions
|
|
1177
|
+
for (const m of script.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
1178
|
+
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
1179
|
+
sigs.push(`export ${asyncKw}function ${m[1]}(${normalizeParams(m[2])})`);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Top-level functions
|
|
1183
|
+
for (const m of script.matchAll(/^(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/gm)) {
|
|
1184
|
+
if (m[1].startsWith('_')) continue;
|
|
1185
|
+
const asyncKw = m[0].startsWith('async') ? 'async ' : '';
|
|
1186
|
+
sigs.push(`${asyncKw}function ${m[1]}(${normalizeParams(m[2])})`);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// Reactive declarations $:
|
|
1190
|
+
for (const m of script.matchAll(/^\s+\$:\s+(\w+)\s*=/gm)) {
|
|
1191
|
+
sigs.push(`$: ${m[1]}`);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return sigs.slice(0, 25);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function normalizeParams(params) {
|
|
1198
|
+
if (!params) return '';
|
|
1199
|
+
return params.trim().replace(/\s+/g, ' ');
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
module.exports = { extract };
|
|
1203
|
+
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
// ── ./src/extractors/swift ──
|
|
1207
|
+
__factories["./src/extractors/swift"] = function(module, exports) {
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Extract signatures from Swift source code.
|
|
1211
|
+
* @param {string} src - Raw file content
|
|
1212
|
+
* @returns {string[]} Array of signature strings
|
|
1213
|
+
*/
|
|
1214
|
+
function extract(src) {
|
|
1215
|
+
if (!src || typeof src !== 'string') return [];
|
|
1216
|
+
const sigs = [];
|
|
1217
|
+
|
|
1218
|
+
const stripped = src
|
|
1219
|
+
.replace(/\/\/.*$/gm, '')
|
|
1220
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1221
|
+
|
|
1222
|
+
// Classes, structs, protocols, enums
|
|
1223
|
+
const typeRe = /^(?:public\s+|internal\s+|open\s+)?(?:final\s+)?(class|struct|protocol|enum|actor)\s+(\w+)(?:<[^{]*>)?(?:\s*:\s*[\w, <>.]+)?\s*\{/gm;
|
|
1224
|
+
for (const m of stripped.matchAll(typeRe)) {
|
|
1225
|
+
sigs.push(`${m[1]} ${m[2]}`);
|
|
1226
|
+
const block = extractBlock(stripped, m.index + m[0].length);
|
|
1227
|
+
for (const fn of extractMembers(block)) sigs.push(` ${fn}`);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Top-level public functions
|
|
1231
|
+
for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:static\s+)?(?:async\s+)?func\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)/gm)) {
|
|
1232
|
+
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
1233
|
+
sigs.push(`${asyncKw}func ${m[1]}(${normalizeParams(m[2])})`);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
return sigs.slice(0, 25);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function extractBlock(src, startIndex) {
|
|
1240
|
+
let depth = 1, i = startIndex;
|
|
1241
|
+
const end = Math.min(src.length, startIndex + 4000);
|
|
1242
|
+
while (i < end && depth > 0) {
|
|
1243
|
+
if (src[i] === '{') depth++;
|
|
1244
|
+
else if (src[i] === '}') depth--;
|
|
1245
|
+
i++;
|
|
1246
|
+
}
|
|
1247
|
+
return src.slice(startIndex, i - 1);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function extractMembers(block) {
|
|
1251
|
+
const members = [];
|
|
1252
|
+
for (const m of block.matchAll(/^\s+(?:public\s+|internal\s+|open\s+)?(?:static\s+|class\s+)?(?:mutating\s+)?(?:async\s+)?func\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)/gm)) {
|
|
1253
|
+
if (m[1].startsWith('_')) continue;
|
|
1254
|
+
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
1255
|
+
members.push(`${asyncKw}func ${m[1]}(${normalizeParams(m[2])})`);
|
|
1256
|
+
}
|
|
1257
|
+
return members.slice(0, 8);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function normalizeParams(params) {
|
|
1261
|
+
if (!params) return '';
|
|
1262
|
+
return params.trim()
|
|
1263
|
+
.split(',')
|
|
1264
|
+
.map((p) => p.trim().split(':')[0].trim())
|
|
1265
|
+
.filter(Boolean)
|
|
1266
|
+
.join(', ');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
module.exports = { extract };
|
|
1270
|
+
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
// ── ./src/extractors/typescript ──
|
|
1274
|
+
__factories["./src/extractors/typescript"] = function(module, exports) {
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Extract signatures from TypeScript source code.
|
|
1278
|
+
* @param {string} src - Raw file content
|
|
1279
|
+
* @returns {string[]} Array of signature strings
|
|
1280
|
+
*/
|
|
1281
|
+
function extract(src) {
|
|
1282
|
+
if (!src || typeof src !== 'string') return [];
|
|
1283
|
+
const sigs = [];
|
|
1284
|
+
|
|
1285
|
+
// Strip single-line comments
|
|
1286
|
+
const stripped = src
|
|
1287
|
+
.replace(/\/\/.*$/gm, '')
|
|
1288
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1289
|
+
|
|
1290
|
+
// Exported interfaces
|
|
1291
|
+
for (const m of stripped.matchAll(/^export\s+interface\s+(\w+)(?:<[^{]*>)?\s*(?:extends\s+[^{]+)?\{/gm)) {
|
|
1292
|
+
sigs.push(`export interface ${m[1]}`);
|
|
1293
|
+
// Collect members
|
|
1294
|
+
const start = m.index + m[0].length;
|
|
1295
|
+
const block = extractBlock(stripped, start);
|
|
1296
|
+
const members = extractInterfaceMembers(block);
|
|
1297
|
+
for (const mem of members) sigs.push(` ${mem}`);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
// Exported type aliases
|
|
1301
|
+
for (const m of stripped.matchAll(/^export\s+type\s+(\w+)(?:<[^=]*>)?\s*=/gm)) {
|
|
1302
|
+
sigs.push(`export type ${m[1]}`);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// Exported enums
|
|
1306
|
+
for (const m of stripped.matchAll(/^export\s+(?:const\s+)?enum\s+(\w+)\s*\{/gm)) {
|
|
1307
|
+
sigs.push(`export enum ${m[1]}`);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Classes (exported and internal)
|
|
1311
|
+
const classRegex = /^(export\s+)?(abstract\s+)?class\s+(\w+)(?:<[^{]*>)?(?:\s+extends\s+[\w<>, .]+)?(?:\s+implements\s+[\w<> ,]+)?\s*\{/gm;
|
|
1312
|
+
for (const m of stripped.matchAll(classRegex)) {
|
|
1313
|
+
const prefix = m[1] ? 'export ' : '';
|
|
1314
|
+
const abs = m[2] ? 'abstract ' : '';
|
|
1315
|
+
sigs.push(`${prefix}${abs}class ${m[3]}`);
|
|
1316
|
+
const start = m.index + m[0].length;
|
|
1317
|
+
const block = extractBlock(stripped, start);
|
|
1318
|
+
const methods = extractClassMembers(block);
|
|
1319
|
+
for (const meth of methods) sigs.push(` ${meth}`);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Exported top-level functions (not methods)
|
|
1323
|
+
for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{]+)?\s*\{/gm)) {
|
|
1324
|
+
const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
|
|
1325
|
+
const params = normalizeParams(m[2]);
|
|
1326
|
+
sigs.push(`export ${asyncKw}function ${m[1]}(${params})`);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// Exported arrow functions / const functions
|
|
1330
|
+
for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=>{]+)?\s*=>/gm)) {
|
|
1331
|
+
const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
|
|
1332
|
+
const params = normalizeParams(m[2]);
|
|
1333
|
+
sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return sigs.slice(0, 25);
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
function extractBlock(src, startIndex) {
|
|
1340
|
+
let depth = 1;
|
|
1341
|
+
let i = startIndex;
|
|
1342
|
+
const end = Math.min(src.length, startIndex + 4000);
|
|
1343
|
+
while (i < end && depth > 0) {
|
|
1344
|
+
if (src[i] === '{') depth++;
|
|
1345
|
+
else if (src[i] === '}') depth--;
|
|
1346
|
+
i++;
|
|
1347
|
+
}
|
|
1348
|
+
return src.slice(startIndex, i - 1);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function extractInterfaceMembers(block) {
|
|
1352
|
+
const members = [];
|
|
1353
|
+
for (const m of block.matchAll(/^\s+(readonly\s+)?(\w+)\??:\s*[^;]+;/gm)) {
|
|
1354
|
+
const readonly = m[1] ? 'readonly ' : '';
|
|
1355
|
+
members.push(`${readonly}${m[2]}`);
|
|
1356
|
+
}
|
|
1357
|
+
for (const m of block.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)\s*:/gm)) {
|
|
1358
|
+
members.push(`${m[1]}(${normalizeParams(m[2])})`);
|
|
1359
|
+
}
|
|
1360
|
+
return members.slice(0, 8);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function extractClassMembers(block) {
|
|
1364
|
+
const members = [];
|
|
1365
|
+
// Public methods (skip private/protected/_ prefixed)
|
|
1366
|
+
const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{;]+)?\s*\{/gm;
|
|
1367
|
+
for (const m of block.matchAll(methodRe)) {
|
|
1368
|
+
if (/^(private|protected|_)/.test(m[1])) continue;
|
|
1369
|
+
if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
|
|
1370
|
+
const isAsync = m[0].includes('async ') ? 'async ' : '';
|
|
1371
|
+
const isStatic = m[0].includes('static ') ? 'static ' : '';
|
|
1372
|
+
members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})`);
|
|
1373
|
+
}
|
|
1374
|
+
return members.slice(0, 8);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function normalizeParams(params) {
|
|
1378
|
+
if (!params) return '';
|
|
1379
|
+
return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
module.exports = { extract };
|
|
1383
|
+
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
// ── ./src/extractors/vue ──
|
|
1387
|
+
__factories["./src/extractors/vue"] = function(module, exports) {
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Extract signatures from Vue single-file components.
|
|
1391
|
+
* @param {string} src - Raw file content
|
|
1392
|
+
* @returns {string[]} Array of signature strings
|
|
1393
|
+
*/
|
|
1394
|
+
function extract(src) {
|
|
1395
|
+
if (!src || typeof src !== 'string') return [];
|
|
1396
|
+
const sigs = [];
|
|
1397
|
+
|
|
1398
|
+
// Extract component name from filename hint if present or defineComponent
|
|
1399
|
+
const nameMatch = src.match(/name\s*:\s*['"](\w+)['"]/);
|
|
1400
|
+
if (nameMatch) sigs.push(`component ${nameMatch[1]}`);
|
|
1401
|
+
|
|
1402
|
+
// Extract <script> block
|
|
1403
|
+
const scriptMatch = src.match(/<script(?:\s[^>]*)?>(?:\s*)([\s\S]*?)<\/script>/i);
|
|
1404
|
+
if (!scriptMatch) return sigs;
|
|
1405
|
+
|
|
1406
|
+
const script = scriptMatch[1]
|
|
1407
|
+
.replace(/\/\/.*$/gm, '')
|
|
1408
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1409
|
+
|
|
1410
|
+
// Props
|
|
1411
|
+
const propsMatch = script.match(/props\s*:\s*(\{[\s\S]*?\})/);
|
|
1412
|
+
if (propsMatch) {
|
|
1413
|
+
const propNames = [];
|
|
1414
|
+
for (const m of propsMatch[1].matchAll(/^\s+(\w+)\s*:/gm)) {
|
|
1415
|
+
propNames.push(m[1]);
|
|
1416
|
+
}
|
|
1417
|
+
if (propNames.length > 0) sigs.push(`props: [${propNames.join(', ')}]`);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
// Methods in options API
|
|
1421
|
+
const methodsMatch = script.match(/methods\s*:\s*\{([\s\S]*?)\},?\s*(?:computed|watch|mounted|created|data|\})/);
|
|
1422
|
+
if (methodsMatch) {
|
|
1423
|
+
for (const m of methodsMatch[1].matchAll(/^\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)/gm)) {
|
|
1424
|
+
if (m[1].startsWith('_')) continue;
|
|
1425
|
+
const asyncKw = m[0].includes('async') ? 'async ' : '';
|
|
1426
|
+
sigs.push(` ${asyncKw}${m[1]}(${normalizeParams(m[2])})`);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// defineProps (Composition API)
|
|
1431
|
+
const definePropsMatch = script.match(/defineProps(?:<[^>]*>)?\s*\(\s*(\{[\s\S]*?\})\s*\)/);
|
|
1432
|
+
if (definePropsMatch) {
|
|
1433
|
+
const propNames = [];
|
|
1434
|
+
for (const m of definePropsMatch[1].matchAll(/^\s+(\w+)\s*:/gm)) {
|
|
1435
|
+
propNames.push(m[1]);
|
|
1436
|
+
}
|
|
1437
|
+
if (propNames.length > 0) sigs.push(`defineProps: [${propNames.join(', ')}]`);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Emits
|
|
1441
|
+
const emitsMatch = script.match(/(?:defineEmits|emits)\s*(?::\s*|\(\s*)(\[[\s\S]*?\])/);
|
|
1442
|
+
if (emitsMatch) sigs.push(`emits: ${emitsMatch[1].replace(/\s+/g, ' ')}`);
|
|
1443
|
+
|
|
1444
|
+
return sigs.slice(0, 25);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function normalizeParams(params) {
|
|
1448
|
+
if (!params) return '';
|
|
1449
|
+
return params.trim().replace(/\s+/g, ' ');
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
module.exports = { extract };
|
|
1453
|
+
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
// ── ./src/extractors/yaml ──
|
|
1457
|
+
__factories["./src/extractors/yaml"] = function(module, exports) {
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Extract signatures from YAML configuration files.
|
|
1461
|
+
* @param {string} src - Raw file content
|
|
1462
|
+
* @returns {string[]} Array of signature strings
|
|
1463
|
+
*/
|
|
1464
|
+
function extract(src) {
|
|
1465
|
+
if (!src || typeof src !== 'string') return [];
|
|
1466
|
+
const sigs = [];
|
|
1467
|
+
|
|
1468
|
+
const lines = src.split('\n');
|
|
1469
|
+
|
|
1470
|
+
// Top-level keys (no leading whitespace)
|
|
1471
|
+
const topKeys = [];
|
|
1472
|
+
for (const line of lines) {
|
|
1473
|
+
if (/^#/.test(line)) continue;
|
|
1474
|
+
const m = line.match(/^([\w-]+)\s*:/);
|
|
1475
|
+
if (m) topKeys.push(m[1]);
|
|
1476
|
+
}
|
|
1477
|
+
if (topKeys.length > 0) sigs.push(`keys: [${topKeys.slice(0, 12).join(', ')}]`);
|
|
1478
|
+
|
|
1479
|
+
// GitHub Actions: jobs
|
|
1480
|
+
let inJobs = false;
|
|
1481
|
+
for (const line of lines) {
|
|
1482
|
+
if (/^jobs\s*:/.test(line)) { inJobs = true; continue; }
|
|
1483
|
+
if (inJobs && /^[a-z]/.test(line) && !line.startsWith('jobs')) inJobs = false;
|
|
1484
|
+
if (inJobs) {
|
|
1485
|
+
const m = line.match(/^ ([\w-]+)\s*:/);
|
|
1486
|
+
if (m) sigs.push(`job: ${m[1]}`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// Docker Compose: services
|
|
1491
|
+
let inServices = false;
|
|
1492
|
+
for (const line of lines) {
|
|
1493
|
+
if (/^services\s*:/.test(line)) { inServices = true; continue; }
|
|
1494
|
+
if (inServices && /^[a-z]/.test(line) && !line.startsWith('services')) inServices = false;
|
|
1495
|
+
if (inServices) {
|
|
1496
|
+
const m = line.match(/^ ([\w-]+)\s*:/);
|
|
1497
|
+
if (m) sigs.push(`service: ${m[1]}`);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// OpenAPI paths
|
|
1502
|
+
let inPaths = false;
|
|
1503
|
+
for (const line of lines) {
|
|
1504
|
+
if (/^paths\s*:/.test(line)) { inPaths = true; continue; }
|
|
1505
|
+
if (inPaths && /^[a-z]/.test(line) && !line.startsWith('paths')) inPaths = false;
|
|
1506
|
+
if (inPaths) {
|
|
1507
|
+
const m = line.match(/^ (\/[\w/{}-]*)\s*:/);
|
|
1508
|
+
if (m) sigs.push(`path: ${m[1]}`);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
return sigs.slice(0, 25);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
module.exports = { extract };
|
|
1516
|
+
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
// ── ./src/format/cache ──
|
|
1520
|
+
__factories["./src/format/cache"] = function(module, exports) {
|
|
1521
|
+
|
|
1522
|
+
/**
|
|
1523
|
+
* Format context output for Anthropic prompt cache API.
|
|
1524
|
+
*
|
|
1525
|
+
* Usage:
|
|
1526
|
+
* const { formatCache } = require('./src/format/cache');
|
|
1527
|
+
* const json = formatCache(markdownContent);
|
|
1528
|
+
* // json is a ready-to-use Anthropic system block with cache_control
|
|
1529
|
+
*
|
|
1530
|
+
* Writes: .github/copilot-instructions.cache.json
|
|
1531
|
+
*/
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* Wrap markdown context in an Anthropic cache-control system block.
|
|
1535
|
+
* @param {string} content - Markdown content from formatOutput()
|
|
1536
|
+
* @returns {string} - JSON string: a single Anthropic system content block
|
|
1537
|
+
*/
|
|
1538
|
+
function formatCache(content) {
|
|
1539
|
+
if (!content || typeof content !== 'string') content = '';
|
|
1540
|
+
const block = {
|
|
1541
|
+
type: 'text',
|
|
1542
|
+
text: content,
|
|
1543
|
+
cache_control: { type: 'ephemeral' },
|
|
1544
|
+
};
|
|
1545
|
+
return JSON.stringify(block, null, 2);
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Wrap markdown context in a full Anthropic messages API payload.
|
|
1550
|
+
* Includes the system array with cache_control so it can be copy-pasted
|
|
1551
|
+
* directly into an API call.
|
|
1552
|
+
* @param {string} content - Markdown content from formatOutput()
|
|
1553
|
+
* @param {string} [model] - Anthropic model ID (default: claude-opus-4-5)
|
|
1554
|
+
* @returns {string} - JSON string: { model, system: [...] }
|
|
1555
|
+
*/
|
|
1556
|
+
function formatCachePayload(content, model) {
|
|
1557
|
+
if (!content || typeof content !== 'string') content = '';
|
|
1558
|
+
const payload = {
|
|
1559
|
+
model: model || 'claude-opus-4-5',
|
|
1560
|
+
system: [
|
|
1561
|
+
{
|
|
1562
|
+
type: 'text',
|
|
1563
|
+
text: content,
|
|
1564
|
+
cache_control: { type: 'ephemeral' },
|
|
1565
|
+
},
|
|
1566
|
+
],
|
|
1567
|
+
messages: [],
|
|
1568
|
+
};
|
|
1569
|
+
return JSON.stringify(payload, null, 2);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
module.exports = { formatCache, formatCachePayload };
|
|
1573
|
+
|
|
1574
|
+
};
|
|
1575
|
+
|
|
1576
|
+
// ── ./src/health/scorer ──
|
|
1577
|
+
__factories["./src/health/scorer"] = function(module, exports) {
|
|
1578
|
+
|
|
1579
|
+
/**
|
|
1580
|
+
* SigMap health scorer.
|
|
1581
|
+
*
|
|
1582
|
+
* Computes a composite 0-100 health score for the current project by combining:
|
|
1583
|
+
* 1. Days since context file was last regenerated (staleness penalty ≤ 30 pts)
|
|
1584
|
+
* 2. Average token reduction percentage (low-reduction penalty 20 pts)
|
|
1585
|
+
* 3. Over-budget run rate (budget penalty 20 pts)
|
|
1586
|
+
*
|
|
1587
|
+
* Strategy-aware: thresholds adjust based on the active strategy so that
|
|
1588
|
+
* hot-cold (90% reduction intentional) is not penalized as 'low reduction'.
|
|
1589
|
+
*
|
|
1590
|
+
* Grade scale: A ≥ 90 | B ≥ 75 | C ≥ 60 | D < 60
|
|
1591
|
+
*
|
|
1592
|
+
* Never throws — returns graceful result with nulls for unavailable metrics.
|
|
1593
|
+
*
|
|
1594
|
+
* @param {string} cwd - Working directory (root of the project)
|
|
1595
|
+
* @returns {{
|
|
1596
|
+
* score: number,
|
|
1597
|
+
* grade: 'A'|'B'|'C'|'D',
|
|
1598
|
+
* strategy: string,
|
|
1599
|
+
* tokenReductionPct: number|null,
|
|
1600
|
+
* daysSinceRegen: number|null,
|
|
1601
|
+
* strategyFreshnessDays: number|null,
|
|
1602
|
+
* totalRuns: number,
|
|
1603
|
+
* overBudgetRuns: number,
|
|
1604
|
+
* }}
|
|
1605
|
+
*/
|
|
1606
|
+
function score(cwd) {
|
|
1607
|
+
const fs = require('fs');
|
|
1608
|
+
const path = require('path');
|
|
1609
|
+
|
|
1610
|
+
let tokenReductionPct = null;
|
|
1611
|
+
let daysSinceRegen = null;
|
|
1612
|
+
let strategyFreshnessDays = null;
|
|
1613
|
+
let overBudgetRuns = 0;
|
|
1614
|
+
let totalRuns = 0;
|
|
1615
|
+
|
|
1616
|
+
// ── Detect active strategy ──────────────────────────────────────────────
|
|
1617
|
+
let strategy = 'full';
|
|
1618
|
+
try {
|
|
1619
|
+
const cfgPath = path.join(cwd, 'gen-context.config.json');
|
|
1620
|
+
if (fs.existsSync(cfgPath)) {
|
|
1621
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
1622
|
+
strategy = cfg.strategy || 'full';
|
|
1623
|
+
}
|
|
1624
|
+
} catch (_) {}
|
|
1625
|
+
|
|
1626
|
+
// ── Read usage log via tracking logger ──────────────────────────────────
|
|
1627
|
+
try {
|
|
1628
|
+
const { readLog, summarize } = __require('./src/tracking/logger');
|
|
1629
|
+
const entries = readLog(cwd);
|
|
1630
|
+
const s = summarize(entries);
|
|
1631
|
+
// Only set tokenReductionPct when there is actual history; a brand-new/
|
|
1632
|
+
// untracked project should not be penalised for "0% reduction".
|
|
1633
|
+
if (s.totalRuns > 0) tokenReductionPct = s.avgReductionPct;
|
|
1634
|
+
overBudgetRuns = s.overBudgetRuns;
|
|
1635
|
+
totalRuns = s.totalRuns;
|
|
1636
|
+
} catch (_) {
|
|
1637
|
+
// No usage log yet — proceed with nulls
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
// ── Days since primary context file was last regenerated ─────────────────
|
|
1641
|
+
try {
|
|
1642
|
+
const ctxFile = path.join(cwd, '.github', 'copilot-instructions.md');
|
|
1643
|
+
if (fs.existsSync(ctxFile)) {
|
|
1644
|
+
const mtime = fs.statSync(ctxFile).mtimeMs;
|
|
1645
|
+
daysSinceRegen = parseFloat(((Date.now() - mtime) / (1000 * 60 * 60 * 24)).toFixed(1));
|
|
1646
|
+
}
|
|
1647
|
+
} catch (_) {}
|
|
1648
|
+
|
|
1649
|
+
// ── Strategy freshness: context-cold.md age (hot-cold only) ─────────────
|
|
1650
|
+
if (strategy === 'hot-cold') {
|
|
1651
|
+
try {
|
|
1652
|
+
const coldFile = path.join(cwd, '.github', 'context-cold.md');
|
|
1653
|
+
if (fs.existsSync(coldFile)) {
|
|
1654
|
+
const mtime = fs.statSync(coldFile).mtimeMs;
|
|
1655
|
+
strategyFreshnessDays = parseFloat(((Date.now() - mtime) / (1000 * 60 * 60 * 24)).toFixed(1));
|
|
1656
|
+
}
|
|
1657
|
+
} catch (_) {}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// ── Compute composite score ───────────────────────────────────────────────
|
|
1661
|
+
let points = 100;
|
|
1662
|
+
|
|
1663
|
+
// Staleness penalty: -4 pts per day over the 7-day freshness window (max -30)
|
|
1664
|
+
if (daysSinceRegen !== null && daysSinceRegen > 7) {
|
|
1665
|
+
points -= Math.min(30, Math.floor((daysSinceRegen - 7) * 4));
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Low-reduction penalty — threshold depends on strategy:
|
|
1669
|
+
// - hot-cold: primary output tiny by design; use cold freshness instead
|
|
1670
|
+
// - per-module: per-file budgets; global < 60% is expected, no penalty
|
|
1671
|
+
// - full: standard 60% threshold
|
|
1672
|
+
const reductionThreshold = (strategy === 'full') ? 60 : 0;
|
|
1673
|
+
if (tokenReductionPct !== null && tokenReductionPct < reductionThreshold) {
|
|
1674
|
+
points -= 20;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// hot-cold strategy freshness penalty: context-cold.md older than 1 day (-10 pts)
|
|
1678
|
+
if (strategy === 'hot-cold' && strategyFreshnessDays !== null && strategyFreshnessDays > 1) {
|
|
1679
|
+
points -= Math.min(10, Math.floor(strategyFreshnessDays - 1) * 3);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// Over-budget penalty: more than 20% of runs exceeded the token budget (-20)
|
|
1683
|
+
if (overBudgetRuns > 0 && totalRuns > 0) {
|
|
1684
|
+
const overBudgetRate = (overBudgetRuns / totalRuns) * 100;
|
|
1685
|
+
if (overBudgetRate > 20) points -= 20;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
points = Math.max(0, Math.min(100, Math.round(points)));
|
|
1689
|
+
|
|
1690
|
+
let grade;
|
|
1691
|
+
if (points >= 90) grade = 'A';
|
|
1692
|
+
else if (points >= 75) grade = 'B';
|
|
1693
|
+
else if (points >= 60) grade = 'C';
|
|
1694
|
+
else grade = 'D';
|
|
1695
|
+
|
|
1696
|
+
return { score: points, grade, strategy, tokenReductionPct, daysSinceRegen, strategyFreshnessDays, totalRuns, overBudgetRuns };
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
module.exports = { score };
|
|
1700
|
+
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
// ── ./src/map/class-hierarchy ──
|
|
1704
|
+
__factories["./src/map/class-hierarchy"] = function(module, exports) {
|
|
1705
|
+
|
|
1706
|
+
/**
|
|
1707
|
+
* Class hierarchy analyzer.
|
|
1708
|
+
* Extracts class declarations with extends/implements across
|
|
1709
|
+
* TypeScript, JavaScript, Python, Java, Kotlin, C# files.
|
|
1710
|
+
*
|
|
1711
|
+
* @param {string[]} files — absolute file paths to analyze
|
|
1712
|
+
* @param {string} cwd — project root for relative path display
|
|
1713
|
+
* @returns {string} formatted section content (empty string if nothing found)
|
|
1714
|
+
*/
|
|
1715
|
+
|
|
1716
|
+
const fs = require('fs');
|
|
1717
|
+
const path = require('path');
|
|
1718
|
+
|
|
1719
|
+
function analyze(files, cwd) {
|
|
1720
|
+
const entries = [];
|
|
1721
|
+
|
|
1722
|
+
for (const filePath of files) {
|
|
1723
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1724
|
+
const rel = path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
1725
|
+
let content;
|
|
1726
|
+
try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
|
|
1727
|
+
|
|
1728
|
+
// TS / JS
|
|
1729
|
+
if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
|
|
1730
|
+
const re = /^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+([\w<>.]+?))?(?:\s+implements\s+([\w<>.,\s]+?))?\s*\{/gm;
|
|
1731
|
+
let m;
|
|
1732
|
+
while ((m = re.exec(content)) !== null) {
|
|
1733
|
+
const parent = m[2] ? m[2].split('<')[0].trim() : null;
|
|
1734
|
+
const ifaces = m[3]
|
|
1735
|
+
? m[3].split(',').map((s) => s.split('<')[0].trim()).filter(Boolean)
|
|
1736
|
+
: [];
|
|
1737
|
+
entries.push({ name: m[1], parent, interfaces: ifaces, file: rel });
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// Python
|
|
1742
|
+
if (['.py', '.pyw'].includes(ext)) {
|
|
1743
|
+
const re = /^\s*class\s+(\w+)\s*\(([^)]*)\)\s*:/gm;
|
|
1744
|
+
let m;
|
|
1745
|
+
while ((m = re.exec(content)) !== null) {
|
|
1746
|
+
const parents = m[2]
|
|
1747
|
+
.split(',')
|
|
1748
|
+
.map((s) => s.trim())
|
|
1749
|
+
.filter((s) => s && s !== 'object');
|
|
1750
|
+
entries.push({
|
|
1751
|
+
name: m[1],
|
|
1752
|
+
parent: parents[0] || null,
|
|
1753
|
+
interfaces: parents.slice(1),
|
|
1754
|
+
file: rel,
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// Java
|
|
1760
|
+
if (ext === '.java') {
|
|
1761
|
+
const re = /^\s*(?:(?:public|protected|private|static|abstract|final)\s+)*class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s<>]+?))?\s*\{/gm;
|
|
1762
|
+
let m;
|
|
1763
|
+
while ((m = re.exec(content)) !== null) {
|
|
1764
|
+
const ifaces = m[3]
|
|
1765
|
+
? m[3].split(',').map((s) => s.split('<')[0].trim()).filter(Boolean)
|
|
1766
|
+
: [];
|
|
1767
|
+
entries.push({ name: m[1], parent: m[2] || null, interfaces: ifaces, file: rel });
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// Kotlin
|
|
1772
|
+
if (['.kt', '.kts'].includes(ext)) {
|
|
1773
|
+
const re = /^\s*(?:(?:data|sealed|abstract|open|inner)\s+)?class\s+(\w+)(?:\s*[^:\r\n]*)?\s*:\s*([\w<>(),.\s]+?)(?:\s*\{|$)/gm;
|
|
1774
|
+
let m;
|
|
1775
|
+
while ((m = re.exec(content)) !== null) {
|
|
1776
|
+
const parents = m[2]
|
|
1777
|
+
.split(',')
|
|
1778
|
+
.map((s) => s.replace(/\(.*?\)/, '').split('<')[0].trim())
|
|
1779
|
+
.filter(Boolean);
|
|
1780
|
+
entries.push({
|
|
1781
|
+
name: m[1],
|
|
1782
|
+
parent: parents[0] || null,
|
|
1783
|
+
interfaces: parents.slice(1),
|
|
1784
|
+
file: rel,
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
// C#
|
|
1790
|
+
if (ext === '.cs') {
|
|
1791
|
+
const re = /^\s*(?:(?:public|internal|protected|private|static|abstract|sealed|partial)\s+)*class\s+(\w+)(?:\s*:\s*([\w<>.,\s]+?))?\s*\{/gm;
|
|
1792
|
+
let m;
|
|
1793
|
+
while ((m = re.exec(content)) !== null) {
|
|
1794
|
+
const parents = m[2]
|
|
1795
|
+
? m[2].split(',').map((s) => s.split('<')[0].trim()).filter(Boolean)
|
|
1796
|
+
: [];
|
|
1797
|
+
entries.push({
|
|
1798
|
+
name: m[1],
|
|
1799
|
+
parent: parents[0] || null,
|
|
1800
|
+
interfaces: parents.slice(1),
|
|
1801
|
+
file: rel,
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
if (entries.length === 0) return '';
|
|
1808
|
+
|
|
1809
|
+
return entries
|
|
1810
|
+
.map((e) => {
|
|
1811
|
+
let line = e.name;
|
|
1812
|
+
if (e.parent) line += ` extends ${e.parent}`;
|
|
1813
|
+
if (e.interfaces.length > 0) line += ` implements ${e.interfaces.join(', ')}`;
|
|
1814
|
+
line += ` (${e.file})`;
|
|
1815
|
+
return line;
|
|
1816
|
+
})
|
|
1817
|
+
.join('\n');
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
module.exports = { analyze };
|
|
1821
|
+
|
|
1822
|
+
};
|
|
1823
|
+
|
|
1824
|
+
// ── ./src/map/import-graph ──
|
|
1825
|
+
__factories["./src/map/import-graph"] = function(module, exports) {
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* Import graph analyzer.
|
|
1829
|
+
* Extracts relative import relationships from JS/TS/Python files
|
|
1830
|
+
* and detects circular dependencies.
|
|
1831
|
+
*
|
|
1832
|
+
* @param {string[]} files — absolute file paths to analyze
|
|
1833
|
+
* @param {string} cwd — project root for relative path display
|
|
1834
|
+
* @returns {string} formatted section content (empty string if nothing found)
|
|
1835
|
+
*/
|
|
1836
|
+
|
|
1837
|
+
const fs = require('fs');
|
|
1838
|
+
const path = require('path');
|
|
1839
|
+
|
|
1840
|
+
const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
1841
|
+
const PY_EXTS = new Set(['.py', '.pyw']);
|
|
1842
|
+
|
|
1843
|
+
// ---------------------------------------------------------------------------
|
|
1844
|
+
// Import extraction per language
|
|
1845
|
+
// ---------------------------------------------------------------------------
|
|
1846
|
+
function extractImports(filePath, content, fileSet) {
|
|
1847
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1848
|
+
const dir = path.dirname(filePath);
|
|
1849
|
+
const found = [];
|
|
1850
|
+
|
|
1851
|
+
if (JS_EXTS.has(ext)) {
|
|
1852
|
+
// ES: import ... from './foo' or import './side-effect'
|
|
1853
|
+
const re1 = /(?:^|[\r\n])\s*import\s+(?:[^'";\r\n]*?\s+from\s+)?['"](\.[^'"]+)['"]/g;
|
|
1854
|
+
let m;
|
|
1855
|
+
while ((m = re1.exec(content)) !== null) {
|
|
1856
|
+
const resolved = resolveJsPath(dir, m[1], fileSet);
|
|
1857
|
+
if (resolved) found.push(resolved);
|
|
1858
|
+
}
|
|
1859
|
+
// CommonJS: require('./foo')
|
|
1860
|
+
const re2 = /\brequire\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
|
|
1861
|
+
while ((m = re2.exec(content)) !== null) {
|
|
1862
|
+
const resolved = resolveJsPath(dir, m[1], fileSet);
|
|
1863
|
+
if (resolved) found.push(resolved);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
|
|
1867
|
+
if (PY_EXTS.has(ext)) {
|
|
1868
|
+
// from .module import ... / from ..pkg import ...
|
|
1869
|
+
const re = /^[ \t]*from\s+(\.+[\w.]*)\s+import/gm;
|
|
1870
|
+
let m;
|
|
1871
|
+
while ((m = re.exec(content)) !== null) {
|
|
1872
|
+
const dotCount = (m[1].match(/^\.+/) || [''])[0].length;
|
|
1873
|
+
const modPart = m[1].slice(dotCount).replace(/\./g, '/');
|
|
1874
|
+
let base = dir;
|
|
1875
|
+
for (let i = 1; i < dotCount; i++) base = path.dirname(base);
|
|
1876
|
+
const candidate = modPart ? path.join(base, modPart + '.py') : null;
|
|
1877
|
+
if (candidate && fileSet.has(candidate)) found.push(candidate);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
return [...new Set(found)];
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function resolveJsPath(dir, importStr, fileSet) {
|
|
1885
|
+
const base = path.resolve(dir, importStr);
|
|
1886
|
+
const candidates = [
|
|
1887
|
+
base,
|
|
1888
|
+
base + '.ts', base + '.tsx',
|
|
1889
|
+
base + '.js', base + '.jsx',
|
|
1890
|
+
base + '/index.ts', base + '/index.js',
|
|
1891
|
+
];
|
|
1892
|
+
for (const c of candidates) {
|
|
1893
|
+
if (fileSet.has(c)) return c;
|
|
1894
|
+
}
|
|
1895
|
+
return null;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// ---------------------------------------------------------------------------
|
|
1899
|
+
// Cycle detection (DFS with path tracking)
|
|
1900
|
+
// ---------------------------------------------------------------------------
|
|
1901
|
+
function detectCycles(graph) {
|
|
1902
|
+
const cycles = [];
|
|
1903
|
+
const visited = new Set();
|
|
1904
|
+
const onStack = new Set();
|
|
1905
|
+
const stackArr = [];
|
|
1906
|
+
|
|
1907
|
+
function dfs(node) {
|
|
1908
|
+
if (onStack.has(node)) {
|
|
1909
|
+
const start = stackArr.indexOf(node);
|
|
1910
|
+
if (start !== -1) cycles.push([...stackArr.slice(start), node]);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
if (visited.has(node)) return;
|
|
1914
|
+
|
|
1915
|
+
onStack.add(node);
|
|
1916
|
+
stackArr.push(node);
|
|
1917
|
+
for (const dep of (graph.get(node) || [])) dfs(dep);
|
|
1918
|
+
stackArr.pop();
|
|
1919
|
+
onStack.delete(node);
|
|
1920
|
+
visited.add(node);
|
|
1921
|
+
}
|
|
1922
|
+
|
|
1923
|
+
for (const node of graph.keys()) {
|
|
1924
|
+
if (!visited.has(node)) dfs(node);
|
|
1925
|
+
}
|
|
1926
|
+
return cycles;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// ---------------------------------------------------------------------------
|
|
1930
|
+
// Public API
|
|
1931
|
+
// ---------------------------------------------------------------------------
|
|
1932
|
+
function analyze(files, cwd) {
|
|
1933
|
+
const fileSet = new Set(files.map((f) => path.resolve(f)));
|
|
1934
|
+
const graph = new Map();
|
|
1935
|
+
|
|
1936
|
+
for (const filePath of files) {
|
|
1937
|
+
let content;
|
|
1938
|
+
try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
|
|
1939
|
+
const deps = extractImports(path.resolve(filePath), content, fileSet);
|
|
1940
|
+
if (deps.length > 0) graph.set(path.resolve(filePath), deps);
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
if (graph.size === 0) return '';
|
|
1944
|
+
|
|
1945
|
+
const cycles = detectCycles(graph);
|
|
1946
|
+
const cycleNodeSet = new Set(cycles.flatMap((c) => c));
|
|
1947
|
+
|
|
1948
|
+
const lines = [];
|
|
1949
|
+
const sorted = [...graph.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1950
|
+
|
|
1951
|
+
for (const [fp, deps] of sorted) {
|
|
1952
|
+
const rel = path.relative(cwd, fp).replace(/\\/g, '/');
|
|
1953
|
+
const depList = deps.map((d) => {
|
|
1954
|
+
const drel = path.relative(cwd, d).replace(/\\/g, '/');
|
|
1955
|
+
return cycleNodeSet.has(d) ? `${drel} ⚠` : drel;
|
|
1956
|
+
});
|
|
1957
|
+
lines.push(`${rel} → ${depList.join(', ')}`);
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
if (cycles.length > 0) {
|
|
1961
|
+
lines.push('');
|
|
1962
|
+
lines.push('Circular dependencies detected:');
|
|
1963
|
+
for (const cycle of cycles) {
|
|
1964
|
+
const relPath = cycle.map((n) => path.relative(cwd, n).replace(/\\/g, '/')).join(' → ');
|
|
1965
|
+
lines.push(` ⚠ ${relPath}`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
return lines.join('\n');
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
module.exports = { analyze };
|
|
1973
|
+
|
|
1974
|
+
};
|
|
1975
|
+
|
|
1976
|
+
// ── ./src/map/route-table ──
|
|
1977
|
+
__factories["./src/map/route-table"] = function(module, exports) {
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* HTTP route table extractor.
|
|
1981
|
+
* Detects routes in Express, Fastify, NestJS, Flask, FastAPI, Gin, Spring.
|
|
1982
|
+
*
|
|
1983
|
+
* @param {string[]} files — absolute file paths to analyze
|
|
1984
|
+
* @param {string} cwd — project root for relative path display
|
|
1985
|
+
* @returns {string} formatted markdown table (empty string if no routes found)
|
|
1986
|
+
*/
|
|
1987
|
+
|
|
1988
|
+
const fs = require('fs');
|
|
1989
|
+
const path = require('path');
|
|
1990
|
+
|
|
1991
|
+
const JS_EXTS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
|
1992
|
+
const PY_EXTS = new Set(['.py', '.pyw']);
|
|
1993
|
+
|
|
1994
|
+
function analyze(files, cwd) {
|
|
1995
|
+
const routes = [];
|
|
1996
|
+
|
|
1997
|
+
for (const filePath of files) {
|
|
1998
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1999
|
+
const rel = path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
2000
|
+
let content;
|
|
2001
|
+
try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
|
|
2002
|
+
|
|
2003
|
+
// -----------------------------------------------------------------------
|
|
2004
|
+
// Express / Fastify / Koa (JS/TS)
|
|
2005
|
+
// -----------------------------------------------------------------------
|
|
2006
|
+
if (JS_EXTS.has(ext)) {
|
|
2007
|
+
// app.get('/path', ...) / router.post('/path') / fastify.put('/path')
|
|
2008
|
+
const re1 = /\b(?:app|router|fastify|server|koa|instance)\.(get|post|put|patch|delete|head|options|all)\s*\(\s*['"`]([^'"`]+)['"`]/gi;
|
|
2009
|
+
let m;
|
|
2010
|
+
while ((m = re1.exec(content)) !== null) {
|
|
2011
|
+
routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// NestJS decorators: @Get('/path') @Post('/path')
|
|
2015
|
+
const re2 = /@(Get|Post|Put|Patch|Delete|Head|Options|All)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
2016
|
+
while ((m = re2.exec(content)) !== null) {
|
|
2017
|
+
routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// NestJS: @Get() with no path
|
|
2021
|
+
const re3 = /@(Get|Post|Put|Patch|Delete)\s*\(\s*\)/g;
|
|
2022
|
+
while ((m = re3.exec(content)) !== null) {
|
|
2023
|
+
routes.push({ method: m[1].toUpperCase(), path: '/', file: rel });
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
// -----------------------------------------------------------------------
|
|
2028
|
+
// Flask / FastAPI (Python)
|
|
2029
|
+
// -----------------------------------------------------------------------
|
|
2030
|
+
if (PY_EXTS.has(ext)) {
|
|
2031
|
+
// @app.route('/path', methods=['GET', 'POST'])
|
|
2032
|
+
const re1 = /@[\w.]+\.route\s*\(\s*['"]([^'"]+)['"]([\s\S]{0,150}?)\)/g;
|
|
2033
|
+
let m;
|
|
2034
|
+
while ((m = re1.exec(content)) !== null) {
|
|
2035
|
+
const routePath = m[1];
|
|
2036
|
+
const methodsMatch = m[2].match(/methods\s*=\s*\[([^\]]+)\]/);
|
|
2037
|
+
if (methodsMatch) {
|
|
2038
|
+
const methods = methodsMatch[1].match(/['"]([A-Z]+)['"]/g) || [];
|
|
2039
|
+
for (const meth of methods) {
|
|
2040
|
+
routes.push({ method: meth.replace(/['"]/g, ''), path: routePath, file: rel });
|
|
2041
|
+
}
|
|
2042
|
+
} else {
|
|
2043
|
+
routes.push({ method: 'GET', path: routePath, file: rel });
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
// @app.get('/path') @router.post('/path') FastAPI style
|
|
2048
|
+
const re2 = /@[\w.]+\.(get|post|put|patch|delete|head|options)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
2049
|
+
while ((m = re2.exec(content)) !== null) {
|
|
2050
|
+
routes.push({ method: m[1].toUpperCase(), path: m[2], file: rel });
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
// -----------------------------------------------------------------------
|
|
2055
|
+
// Go — Gin / Echo / chi / net/http
|
|
2056
|
+
// -----------------------------------------------------------------------
|
|
2057
|
+
if (ext === '.go') {
|
|
2058
|
+
// r.GET("/path", handler)
|
|
2059
|
+
const re1 = /\b\w+\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*["']([^"']+)["']/g;
|
|
2060
|
+
let m;
|
|
2061
|
+
while ((m = re1.exec(content)) !== null) {
|
|
2062
|
+
routes.push({ method: m[1], path: m[2], file: rel });
|
|
2063
|
+
}
|
|
2064
|
+
// http.HandleFunc("/path", handler)
|
|
2065
|
+
const re2 = /http\.HandleFunc\s*\(\s*["']([^"']+)["']/g;
|
|
2066
|
+
while ((m = re2.exec(content)) !== null) {
|
|
2067
|
+
routes.push({ method: 'ANY', path: m[1], file: rel });
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
// -----------------------------------------------------------------------
|
|
2072
|
+
// Spring (Java)
|
|
2073
|
+
// -----------------------------------------------------------------------
|
|
2074
|
+
if (ext === '.java') {
|
|
2075
|
+
// @GetMapping("/path") @PostMapping @RequestMapping
|
|
2076
|
+
const re1 = /@(Get|Post|Put|Patch|Delete|Request)Mapping\s*\(\s*(?:value\s*=\s*)?["']([^"']+)["']/g;
|
|
2077
|
+
let m;
|
|
2078
|
+
while ((m = re1.exec(content)) !== null) {
|
|
2079
|
+
const method = m[1] === 'Request' ? 'ANY' : m[1].toUpperCase();
|
|
2080
|
+
routes.push({ method, path: m[2], file: rel });
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
if (routes.length === 0) return '';
|
|
2086
|
+
|
|
2087
|
+
const lines = [
|
|
2088
|
+
'| Method | Path | File |',
|
|
2089
|
+
'|--------|------|------|',
|
|
2090
|
+
];
|
|
2091
|
+
for (const r of routes) {
|
|
2092
|
+
lines.push(`| ${r.method} | ${r.path} | ${r.file} |`);
|
|
2093
|
+
}
|
|
2094
|
+
return lines.join('\n');
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
module.exports = { analyze };
|
|
2098
|
+
|
|
2099
|
+
};
|
|
2100
|
+
|
|
2101
|
+
// ── ./src/mcp/handlers ──
|
|
2102
|
+
__factories["./src/mcp/handlers"] = function(module, exports) {
|
|
2103
|
+
|
|
2104
|
+
const fs = require('fs');
|
|
2105
|
+
const path = require('path');
|
|
2106
|
+
const { execSync } = require('child_process');
|
|
2107
|
+
|
|
2108
|
+
const CONTEXT_FILE = path.join('.github', 'copilot-instructions.md');
|
|
2109
|
+
|
|
2110
|
+
// Section header keywords in PROJECT_MAP.md
|
|
2111
|
+
const MAP_SECTIONS = {
|
|
2112
|
+
imports: '### Import graph',
|
|
2113
|
+
classes: '### Class hierarchy',
|
|
2114
|
+
routes: '### Route table',
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
/**
|
|
2118
|
+
* read_context({ module? }) → string
|
|
2119
|
+
*
|
|
2120
|
+
* Returns the full context file, or just the sections whose file paths
|
|
2121
|
+
* contain the given module substring.
|
|
2122
|
+
*/
|
|
2123
|
+
function readContext(args, cwd) {
|
|
2124
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
2125
|
+
if (!fs.existsSync(contextPath)) {
|
|
2126
|
+
return 'No context file found. Run: node gen-context.js';
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
2130
|
+
|
|
2131
|
+
if (!args || !args.module) return content;
|
|
2132
|
+
|
|
2133
|
+
const mod = args.module.replace(/\\/g, '/').replace(/\/$/, '');
|
|
2134
|
+
const lines = content.split('\n');
|
|
2135
|
+
const result = [];
|
|
2136
|
+
let capturing = false;
|
|
2137
|
+
|
|
2138
|
+
for (const line of lines) {
|
|
2139
|
+
if (line.startsWith('### ')) {
|
|
2140
|
+
const filePath = line.slice(4).trim().replace(/\\/g, '/');
|
|
2141
|
+
// Match if file path starts with mod or contains /mod/ or /mod
|
|
2142
|
+
capturing =
|
|
2143
|
+
filePath === mod ||
|
|
2144
|
+
filePath.startsWith(mod + '/') ||
|
|
2145
|
+
filePath.includes('/' + mod + '/') ||
|
|
2146
|
+
filePath.includes('/' + mod);
|
|
2147
|
+
if (capturing) result.push(line);
|
|
2148
|
+
continue;
|
|
2149
|
+
}
|
|
2150
|
+
if (capturing) result.push(line);
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
if (result.length === 0) return `No signatures found for module: ${mod}`;
|
|
2154
|
+
return result.join('\n');
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* search_signatures({ query }) → string
|
|
2159
|
+
*
|
|
2160
|
+
* Case-insensitive search through all signature lines.
|
|
2161
|
+
* Returns matching lines grouped by file path.
|
|
2162
|
+
*/
|
|
2163
|
+
function searchSignatures(args, cwd) {
|
|
2164
|
+
if (!args || !args.query) return 'Missing required argument: query';
|
|
2165
|
+
|
|
2166
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
2167
|
+
if (!fs.existsSync(contextPath)) {
|
|
2168
|
+
return 'No context file found. Run: node gen-context.js';
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
2172
|
+
const query = args.query.toLowerCase();
|
|
2173
|
+
const lines = content.split('\n');
|
|
2174
|
+
|
|
2175
|
+
const result = [];
|
|
2176
|
+
let currentFile = '';
|
|
2177
|
+
let fileHeaderAdded = false;
|
|
2178
|
+
|
|
2179
|
+
for (const line of lines) {
|
|
2180
|
+
if (line.startsWith('### ')) {
|
|
2181
|
+
currentFile = line.slice(4).trim();
|
|
2182
|
+
fileHeaderAdded = false;
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
// Skip markdown fences and top-level headers
|
|
2186
|
+
if (line.startsWith('```') || line.startsWith('## ') || line.startsWith('# ') || line.startsWith('<!--')) {
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
if (line.toLowerCase().includes(query)) {
|
|
2190
|
+
if (currentFile && !fileHeaderAdded) {
|
|
2191
|
+
if (result.length > 0) result.push('');
|
|
2192
|
+
result.push(`### ${currentFile}`);
|
|
2193
|
+
fileHeaderAdded = true;
|
|
2194
|
+
}
|
|
2195
|
+
result.push(line);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
if (result.length === 0) return `No signatures found matching: ${args.query}`;
|
|
2200
|
+
return result.join('\n');
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
/**
|
|
2204
|
+
* get_map({ type }) → string
|
|
2205
|
+
*
|
|
2206
|
+
* Returns a section from PROJECT_MAP.md.
|
|
2207
|
+
* type: 'imports' | 'classes' | 'routes'
|
|
2208
|
+
*/
|
|
2209
|
+
function getMap(args, cwd) {
|
|
2210
|
+
if (!args || !args.type) return 'Missing required argument: type';
|
|
2211
|
+
|
|
2212
|
+
const header = MAP_SECTIONS[args.type];
|
|
2213
|
+
if (!header) {
|
|
2214
|
+
return `Unknown map type: "${args.type}". Use: imports, classes, routes`;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
2218
|
+
if (!fs.existsSync(mapPath)) {
|
|
2219
|
+
return 'PROJECT_MAP.md not found. Run: node gen-project-map.js';
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
const content = fs.readFileSync(mapPath, 'utf8');
|
|
2223
|
+
const idx = content.indexOf(header);
|
|
2224
|
+
if (idx === -1) {
|
|
2225
|
+
return `Section "${header}" not found in PROJECT_MAP.md`;
|
|
2226
|
+
}
|
|
2227
|
+
|
|
2228
|
+
// Extract from this header to the next ### header
|
|
2229
|
+
const after = content.slice(idx);
|
|
2230
|
+
const nextMatch = after.slice(header.length).search(/\n###\s/);
|
|
2231
|
+
return nextMatch === -1 ? after : after.slice(0, header.length + nextMatch);
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
/**
|
|
2235
|
+
* create_checkpoint({ note? }) → string
|
|
2236
|
+
*
|
|
2237
|
+
* Returns a markdown checkpoint summarising current project state:
|
|
2238
|
+
* - Timestamp and optional user note
|
|
2239
|
+
* - Active git branch + last 5 commit messages
|
|
2240
|
+
* - Token count of current context file
|
|
2241
|
+
* - List of modules present in the context
|
|
2242
|
+
* - Route count (if PROJECT_MAP.md exists)
|
|
2243
|
+
*/
|
|
2244
|
+
function createCheckpoint(args, cwd) {
|
|
2245
|
+
const note = (args && args.note) ? args.note.trim() : '';
|
|
2246
|
+
const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
|
2247
|
+
const lines = [
|
|
2248
|
+
'# SigMap Checkpoint',
|
|
2249
|
+
`**Created:** ${now}`,
|
|
2250
|
+
];
|
|
2251
|
+
|
|
2252
|
+
if (note) lines.push(`**Note:** ${note}`);
|
|
2253
|
+
lines.push('');
|
|
2254
|
+
|
|
2255
|
+
// ── Git info ────────────────────────────────────────────────────────────
|
|
2256
|
+
lines.push('## Git state');
|
|
2257
|
+
try {
|
|
2258
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
2259
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
2260
|
+
}).trim();
|
|
2261
|
+
lines.push(`**Branch:** ${branch}`);
|
|
2262
|
+
} catch (_) {
|
|
2263
|
+
lines.push('**Branch:** (not a git repo)');
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
try {
|
|
2267
|
+
const log = execSync(
|
|
2268
|
+
'git log --oneline -5 --no-decorate 2>/dev/null',
|
|
2269
|
+
{ cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
|
2270
|
+
).trim();
|
|
2271
|
+
if (log) {
|
|
2272
|
+
lines.push('');
|
|
2273
|
+
lines.push('**Recent commits:**');
|
|
2274
|
+
for (const l of log.split('\n')) lines.push(`- ${l}`);
|
|
2275
|
+
}
|
|
2276
|
+
} catch (_) {} // ignore — not every project uses git
|
|
2277
|
+
lines.push('');
|
|
2278
|
+
|
|
2279
|
+
// ── Context stats ────────────────────────────────────────────────────────
|
|
2280
|
+
lines.push('## Context snapshot');
|
|
2281
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
2282
|
+
if (fs.existsSync(contextPath)) {
|
|
2283
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
2284
|
+
const tokens = Math.ceil(content.length / 4);
|
|
2285
|
+
|
|
2286
|
+
// Count modules (### headers are file paths)
|
|
2287
|
+
const modules = content.split('\n').filter((l) => l.startsWith('### ')).map((l) => l.slice(4).trim());
|
|
2288
|
+
lines.push(`**Token count:** ~${tokens}`);
|
|
2289
|
+
lines.push(`**Modules in context:** ${modules.length}`);
|
|
2290
|
+
|
|
2291
|
+
if (modules.length > 0) {
|
|
2292
|
+
lines.push('');
|
|
2293
|
+
lines.push('**Modules:**');
|
|
2294
|
+
for (const m of modules.slice(0, 20)) lines.push(`- ${m}`);
|
|
2295
|
+
if (modules.length > 20) lines.push(`- … and ${modules.length - 20} more`);
|
|
2296
|
+
}
|
|
2297
|
+
} else {
|
|
2298
|
+
lines.push('_No context file found. Run: node gen-context.js_');
|
|
2299
|
+
}
|
|
2300
|
+
lines.push('');
|
|
2301
|
+
|
|
2302
|
+
// ── Route summary ────────────────────────────────────────────────────────
|
|
2303
|
+
const mapPath = path.join(cwd, 'PROJECT_MAP.md');
|
|
2304
|
+
if (fs.existsSync(mapPath)) {
|
|
2305
|
+
const mapContent = fs.readFileSync(mapPath, 'utf8');
|
|
2306
|
+
const routeLines = mapContent.split('\n').filter((l) => l.startsWith('| ') && !l.startsWith('| Method') && !l.startsWith('|---'));
|
|
2307
|
+
if (routeLines.length > 0) {
|
|
2308
|
+
lines.push('## Routes');
|
|
2309
|
+
lines.push(`**Total routes detected:** ${routeLines.length}`);
|
|
2310
|
+
lines.push('');
|
|
2311
|
+
for (const r of routeLines.slice(0, 10)) lines.push(r);
|
|
2312
|
+
if (routeLines.length > 10) lines.push(`| … | +${routeLines.length - 10} more | |`);
|
|
2313
|
+
lines.push('');
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
lines.push('---');
|
|
2318
|
+
lines.push('_Generated by SigMap `create_checkpoint`_');
|
|
2319
|
+
|
|
2320
|
+
return lines.join('\n');
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
/**
|
|
2324
|
+
* get_routing({}) → string
|
|
2325
|
+
*
|
|
2326
|
+
* Reads the current context file, classifies all indexed files by complexity,
|
|
2327
|
+
* and returns a formatted markdown routing guide showing which files belong
|
|
2328
|
+
* to the fast/balanced/powerful model tier.
|
|
2329
|
+
*/
|
|
2330
|
+
function getRouting(args, cwd) {
|
|
2331
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
2332
|
+
if (!fs.existsSync(contextPath)) {
|
|
2333
|
+
return (
|
|
2334
|
+
'_No context file found. Run `node gen-context.js --routing` first._\n\n' +
|
|
2335
|
+
'This generates routing hints that map each file to a model tier:\n' +
|
|
2336
|
+
'- **fast** (haiku/gpt-4o-mini) — config, markup, trivial utilities\n' +
|
|
2337
|
+
'- **balanced** (sonnet/gpt-4o) — standard application code\n' +
|
|
2338
|
+
'- **powerful** (opus/gpt-4-turbo) — complex, security-critical, or large modules'
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// Parse file list from context (### headings are file paths)
|
|
2343
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
2344
|
+
const fileRels = content.split('\n')
|
|
2345
|
+
.filter((l) => l.startsWith('### '))
|
|
2346
|
+
.map((l) => l.slice(4).trim());
|
|
2347
|
+
|
|
2348
|
+
// Build synthetic fileEntries for the classifier
|
|
2349
|
+
// We don't have live sig arrays here, so rebuild from the context blocks
|
|
2350
|
+
const entries = [];
|
|
2351
|
+
const blocks = content.split(/^### /m).slice(1); // slice past the header
|
|
2352
|
+
for (const block of blocks) {
|
|
2353
|
+
const firstLine = block.split('\n')[0].trim();
|
|
2354
|
+
const codeBlock = block.match(/```\n([\s\S]*?)```/);
|
|
2355
|
+
const sigs = codeBlock ? codeBlock[1].trim().split('\n').filter(Boolean) : [];
|
|
2356
|
+
entries.push({ filePath: path.join(cwd, firstLine), sigs });
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
try {
|
|
2360
|
+
const { classifyAll } = __require('./src/routing/classifier');
|
|
2361
|
+
const { formatRoutingSection } = __require('./src/routing/hints');
|
|
2362
|
+
const groups = classifyAll(entries, cwd);
|
|
2363
|
+
return formatRoutingSection(groups);
|
|
2364
|
+
} catch (err) {
|
|
2365
|
+
return `_Routing classification failed: ${err.message}_`;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
function explainFile(args, cwd) {
|
|
2370
|
+
if (!args || !args.path) return 'Missing required argument: path';
|
|
2371
|
+
|
|
2372
|
+
const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
|
|
2373
|
+
const targetAbs = path.resolve(cwd, targetRel);
|
|
2374
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
2375
|
+
|
|
2376
|
+
const lines = ['# explain_file: ' + targetRel, ''];
|
|
2377
|
+
|
|
2378
|
+
lines.push('## Signatures');
|
|
2379
|
+
let indexedFiles = [];
|
|
2380
|
+
|
|
2381
|
+
if (fs.existsSync(contextPath)) {
|
|
2382
|
+
const ctxContent = fs.readFileSync(contextPath, 'utf8');
|
|
2383
|
+
const ctxLines = ctxContent.split('\n');
|
|
2384
|
+
let capturing = false;
|
|
2385
|
+
const sigLines = [];
|
|
2386
|
+
|
|
2387
|
+
for (const line of ctxLines) {
|
|
2388
|
+
if (line.startsWith('### ')) {
|
|
2389
|
+
if (capturing) break;
|
|
2390
|
+
const rel = line.slice(4).trim().replace(/\\/g, '/');
|
|
2391
|
+
capturing = rel === targetRel || rel.endsWith('/' + targetRel) || targetRel.endsWith('/' + rel);
|
|
2392
|
+
if (capturing) continue;
|
|
2393
|
+
} else if (capturing) {
|
|
2394
|
+
sigLines.push(line);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
const sigs = sigLines.filter((l) => l !== '```' && l.trim() !== '');
|
|
2399
|
+
if (sigs.length > 0) {
|
|
2400
|
+
lines.push(...sigs);
|
|
2401
|
+
} else {
|
|
2402
|
+
lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
indexedFiles = ctxContent
|
|
2406
|
+
.split('\n')
|
|
2407
|
+
.filter((l) => l.startsWith('### '))
|
|
2408
|
+
.map((l) => path.resolve(cwd, l.slice(4).trim()));
|
|
2409
|
+
} else {
|
|
2410
|
+
lines.push('_No context file found. Run: node gen-context.js_');
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (!fs.existsSync(targetAbs)) {
|
|
2414
|
+
lines.push('');
|
|
2415
|
+
lines.push('> File not found on disk: ' + targetRel);
|
|
2416
|
+
return lines.join('\n');
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
lines.push('');
|
|
2420
|
+
|
|
2421
|
+
lines.push('## Imports (direct dependencies)');
|
|
2422
|
+
try {
|
|
2423
|
+
const { extractImports } = __require('./src/map/import-graph');
|
|
2424
|
+
const fileContent = fs.readFileSync(targetAbs, 'utf8');
|
|
2425
|
+
const fileSet = new Set(indexedFiles);
|
|
2426
|
+
fileSet.add(targetAbs);
|
|
2427
|
+
const imports = extractImports(targetAbs, fileContent, fileSet);
|
|
2428
|
+
if (imports.length > 0) {
|
|
2429
|
+
for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
|
|
2430
|
+
} else {
|
|
2431
|
+
lines.push('_No resolvable relative imports found._');
|
|
2432
|
+
}
|
|
2433
|
+
} catch (err) {
|
|
2434
|
+
lines.push('_Could not analyze imports: ' + err.message + '_');
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
lines.push('');
|
|
2438
|
+
|
|
2439
|
+
lines.push('## Callers (files that import this file)');
|
|
2440
|
+
try {
|
|
2441
|
+
const { extractImports } = __require('./src/map/import-graph');
|
|
2442
|
+
const fileSet = new Set(indexedFiles);
|
|
2443
|
+
fileSet.add(targetAbs);
|
|
2444
|
+
const callers = [];
|
|
2445
|
+
for (const f of indexedFiles) {
|
|
2446
|
+
if (f === targetAbs || !fs.existsSync(f)) continue;
|
|
2447
|
+
try {
|
|
2448
|
+
const fc = fs.readFileSync(f, 'utf8');
|
|
2449
|
+
const imps = extractImports(f, fc, fileSet);
|
|
2450
|
+
if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
|
|
2451
|
+
} catch (_) {}
|
|
2452
|
+
}
|
|
2453
|
+
if (callers.length > 0) {
|
|
2454
|
+
for (const c of callers) lines.push('- ' + c);
|
|
2455
|
+
} else {
|
|
2456
|
+
lines.push('_No indexed files import this file._');
|
|
2457
|
+
}
|
|
2458
|
+
} catch (err) {
|
|
2459
|
+
lines.push('_Could not analyze callers: ' + err.message + '_');
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
return lines.join('\n');
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
function listModules(args, cwd) {
|
|
2466
|
+
const contextPath = path.join(cwd, CONTEXT_FILE);
|
|
2467
|
+
if (!fs.existsSync(contextPath)) {
|
|
2468
|
+
return 'No context file found. Run: node gen-context.js';
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
const content = fs.readFileSync(contextPath, 'utf8');
|
|
2472
|
+
const ctxLines = content.split('\n');
|
|
2473
|
+
const groups = {};
|
|
2474
|
+
let currentGroup = null;
|
|
2475
|
+
let blockBuf = [];
|
|
2476
|
+
|
|
2477
|
+
function flushBlock() {
|
|
2478
|
+
if (currentGroup === null || blockBuf.length === 0) return;
|
|
2479
|
+
if (!groups[currentGroup]) groups[currentGroup] = { fileCount: 0, tokenCount: 0 };
|
|
2480
|
+
groups[currentGroup].fileCount++;
|
|
2481
|
+
groups[currentGroup].tokenCount += Math.ceil(blockBuf.join('\n').length / 4);
|
|
2482
|
+
blockBuf = [];
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
for (const line of ctxLines) {
|
|
2486
|
+
if (line.startsWith('### ')) {
|
|
2487
|
+
flushBlock();
|
|
2488
|
+
const rel = line.slice(4).trim().replace(/\\/g, '/');
|
|
2489
|
+
const parts = rel.split('/');
|
|
2490
|
+
currentGroup = parts.length > 1 ? parts[0] : '.';
|
|
2491
|
+
} else if (currentGroup !== null) {
|
|
2492
|
+
blockBuf.push(line);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
flushBlock();
|
|
2496
|
+
|
|
2497
|
+
const sorted = Object.entries(groups)
|
|
2498
|
+
.map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
|
|
2499
|
+
.sort((a, b) => b.tokenCount - a.tokenCount);
|
|
2500
|
+
|
|
2501
|
+
if (sorted.length === 0) return 'No modules found in context file.';
|
|
2502
|
+
|
|
2503
|
+
const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
|
|
2504
|
+
|
|
2505
|
+
return [
|
|
2506
|
+
'# Modules',
|
|
2507
|
+
'',
|
|
2508
|
+
'| Module | Files | Tokens |',
|
|
2509
|
+
'|--------|-------|--------|',
|
|
2510
|
+
...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
|
|
2511
|
+
'',
|
|
2512
|
+
`**Total context tokens: ~${total}**`,
|
|
2513
|
+
'',
|
|
2514
|
+
'_Use `read_context({ module: "name" })` to get signatures for a specific module._',
|
|
2515
|
+
].join('\n');
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules };
|
|
2519
|
+
};
|
|
2520
|
+
|
|
2521
|
+
// ── ./src/mcp/server ──
|
|
2522
|
+
__factories["./src/mcp/server"] = function(module, exports) {
|
|
2523
|
+
|
|
2524
|
+
/**
|
|
2525
|
+
* SigMap MCP server — zero npm dependencies.
|
|
2526
|
+
*
|
|
2527
|
+
* Wire protocol: JSON-RPC 2.0 over stdio.
|
|
2528
|
+
* One JSON object per line on both stdin and stdout.
|
|
2529
|
+
*
|
|
2530
|
+
* Supported methods:
|
|
2531
|
+
* initialize → serverInfo + capabilities
|
|
2532
|
+
* tools/list → 3 tool definitions
|
|
2533
|
+
* tools/call → dispatch to handler, return result
|
|
2534
|
+
*/
|
|
2535
|
+
|
|
2536
|
+
const readline = require('readline');
|
|
2537
|
+
const { TOOLS } = __require('./src/mcp/tools');
|
|
2538
|
+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules } = __require('./src/mcp/handlers');
|
|
2539
|
+
|
|
2540
|
+
const SERVER_INFO = {
|
|
2541
|
+
name: 'sigmap',
|
|
2542
|
+
version: '1.4.0',
|
|
2543
|
+
description: 'SigMap MCP server — code signatures on demand',
|
|
2544
|
+
};
|
|
2545
|
+
|
|
2546
|
+
// ---------------------------------------------------------------------------
|
|
2547
|
+
// JSON-RPC helpers
|
|
2548
|
+
// ---------------------------------------------------------------------------
|
|
2549
|
+
function respond(id, result) {
|
|
2550
|
+
process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
function respondError(id, code, message) {
|
|
2554
|
+
process.stdout.write(
|
|
2555
|
+
JSON.stringify({ jsonrpc: '2.0', id, error: { code, message } }) + '\n'
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// ---------------------------------------------------------------------------
|
|
2560
|
+
// Method dispatcher
|
|
2561
|
+
// ---------------------------------------------------------------------------
|
|
2562
|
+
function dispatch(msg, cwd) {
|
|
2563
|
+
const { method, id, params } = msg;
|
|
2564
|
+
|
|
2565
|
+
// Notifications (no id) need no response
|
|
2566
|
+
if (method === 'notifications/initialized' || method === 'notifications/cancelled') {
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
if (method === 'initialize') {
|
|
2571
|
+
respond(id, {
|
|
2572
|
+
protocolVersion: (params && params.protocolVersion) || '2024-11-05',
|
|
2573
|
+
serverInfo: SERVER_INFO,
|
|
2574
|
+
capabilities: { tools: {} },
|
|
2575
|
+
});
|
|
2576
|
+
return;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
if (method === 'tools/list') {
|
|
2580
|
+
respond(id, { tools: TOOLS });
|
|
2581
|
+
return;
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
if (method === 'tools/call') {
|
|
2585
|
+
const name = params && params.name;
|
|
2586
|
+
const args = (params && params.arguments) || {};
|
|
2587
|
+
|
|
2588
|
+
let text;
|
|
2589
|
+
try {
|
|
2590
|
+
if (name === 'read_context') text = readContext(args, cwd);
|
|
2591
|
+
else if (name === 'search_signatures') text = searchSignatures(args, cwd);
|
|
2592
|
+
else if (name === 'get_map') text = getMap(args, cwd);
|
|
2593
|
+
else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
|
|
2594
|
+
else if (name === 'get_routing') text = getRouting(args, cwd);
|
|
2595
|
+
else if (name === 'explain_file') text = explainFile(args, cwd);
|
|
2596
|
+
else if (name === 'list_modules') text = listModules(args, cwd);
|
|
2597
|
+
else {
|
|
2598
|
+
respondError(id, -32601, `Unknown tool: ${name}`);
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
} catch (err) {
|
|
2602
|
+
respondError(id, -32603, `Tool error: ${err.message}`);
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
respond(id, {
|
|
2607
|
+
content: [{ type: 'text', text: String(text) }],
|
|
2608
|
+
});
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
// Unknown method
|
|
2613
|
+
if (id !== undefined && id !== null) {
|
|
2614
|
+
respondError(id, -32601, `Method not found: ${method}`);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
// ---------------------------------------------------------------------------
|
|
2619
|
+
// Server entry point
|
|
2620
|
+
// ---------------------------------------------------------------------------
|
|
2621
|
+
function start(cwd) {
|
|
2622
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
2623
|
+
|
|
2624
|
+
rl.on('line', (line) => {
|
|
2625
|
+
const trimmed = line.trim();
|
|
2626
|
+
if (!trimmed) return;
|
|
2627
|
+
|
|
2628
|
+
let msg;
|
|
2629
|
+
try {
|
|
2630
|
+
msg = JSON.parse(trimmed);
|
|
2631
|
+
} catch (_) {
|
|
2632
|
+
// Cannot respond without a valid id — ignore malformed input
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
try {
|
|
2637
|
+
dispatch(msg, cwd);
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
const id = (msg && msg.id) != null ? msg.id : null;
|
|
2640
|
+
respondError(id, -32603, `Internal error: ${err.message}`);
|
|
2641
|
+
}
|
|
2642
|
+
});
|
|
2643
|
+
|
|
2644
|
+
rl.on('close', () => {
|
|
2645
|
+
process.exit(0);
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
|
|
2649
|
+
module.exports = { start };
|
|
2650
|
+
|
|
2651
|
+
};
|
|
2652
|
+
|
|
2653
|
+
// ── ./src/mcp/tools ──
|
|
2654
|
+
__factories["./src/mcp/tools"] = function(module, exports) {
|
|
2655
|
+
|
|
2656
|
+
/**
|
|
2657
|
+
* MCP tool definitions for SigMap.
|
|
2658
|
+
* Three tools: read_context, search_signatures, get_map.
|
|
2659
|
+
*/
|
|
2660
|
+
|
|
2661
|
+
const TOOLS = [
|
|
2662
|
+
{
|
|
2663
|
+
name: 'read_context',
|
|
2664
|
+
description:
|
|
2665
|
+
'Read extracted code signatures for the project or a specific module path. ' +
|
|
2666
|
+
'Returns the full copilot-instructions.md content (~500–4K tokens) or a ' +
|
|
2667
|
+
'filtered subset when a module path is provided (~50–500 tokens).',
|
|
2668
|
+
inputSchema: {
|
|
2669
|
+
type: 'object',
|
|
2670
|
+
properties: {
|
|
2671
|
+
module: {
|
|
2672
|
+
type: 'string',
|
|
2673
|
+
description:
|
|
2674
|
+
'Optional subdirectory path to scope results (e.g. "src/services"). ' +
|
|
2675
|
+
'Omit to get the full codebase context.',
|
|
2676
|
+
},
|
|
2677
|
+
},
|
|
2678
|
+
required: [],
|
|
2679
|
+
},
|
|
2680
|
+
},
|
|
2681
|
+
{
|
|
2682
|
+
name: 'search_signatures',
|
|
2683
|
+
description:
|
|
2684
|
+
'Search extracted code signatures for a keyword, function name, or class name. ' +
|
|
2685
|
+
'Returns matching signature lines with their file paths.',
|
|
2686
|
+
inputSchema: {
|
|
2687
|
+
type: 'object',
|
|
2688
|
+
properties: {
|
|
2689
|
+
query: {
|
|
2690
|
+
type: 'string',
|
|
2691
|
+
description: 'Keyword to search for in signatures (case-insensitive).',
|
|
2692
|
+
},
|
|
2693
|
+
},
|
|
2694
|
+
required: ['query'],
|
|
2695
|
+
},
|
|
2696
|
+
},
|
|
2697
|
+
{
|
|
2698
|
+
name: 'get_map',
|
|
2699
|
+
description:
|
|
2700
|
+
'Read a section from PROJECT_MAP.md — import graph, class hierarchy, or route table. ' +
|
|
2701
|
+
'Requires gen-project-map.js to have been run first.',
|
|
2702
|
+
inputSchema: {
|
|
2703
|
+
type: 'object',
|
|
2704
|
+
properties: {
|
|
2705
|
+
type: {
|
|
2706
|
+
type: 'string',
|
|
2707
|
+
enum: ['imports', 'classes', 'routes'],
|
|
2708
|
+
description: 'Which section to retrieve: imports, classes, or routes.',
|
|
2709
|
+
},
|
|
2710
|
+
},
|
|
2711
|
+
required: ['type'],
|
|
2712
|
+
},
|
|
2713
|
+
},
|
|
2714
|
+
{
|
|
2715
|
+
name: 'create_checkpoint',
|
|
2716
|
+
description:
|
|
2717
|
+
'Create a session checkpoint summarising current project state. ' +
|
|
2718
|
+
'Returns recent git commits, active branch, token count, and a ' +
|
|
2719
|
+
'compact snapshot of the codebase context — ideal for session handoffs ' +
|
|
2720
|
+
'or periodic saves during long coding sessions.',
|
|
2721
|
+
inputSchema: {
|
|
2722
|
+
type: 'object',
|
|
2723
|
+
properties: {
|
|
2724
|
+
note: {
|
|
2725
|
+
type: 'string',
|
|
2726
|
+
description: 'Optional free-text note to include in the checkpoint (e.g. what you were working on).',
|
|
2727
|
+
},
|
|
2728
|
+
},
|
|
2729
|
+
required: [],
|
|
2730
|
+
},
|
|
2731
|
+
},
|
|
2732
|
+
{
|
|
2733
|
+
name: 'get_routing',
|
|
2734
|
+
description:
|
|
2735
|
+
'Get model routing hints for this project — which files belong to which complexity ' +
|
|
2736
|
+
'tier (fast/balanced/powerful) and which AI model to use for each type of task. ' +
|
|
2737
|
+
'Helps reduce API costs by 40–80% by routing simple tasks to cheaper models.',
|
|
2738
|
+
inputSchema: {
|
|
2739
|
+
type: 'object',
|
|
2740
|
+
properties: {},
|
|
2741
|
+
required: [],
|
|
2742
|
+
},
|
|
2743
|
+
},
|
|
2744
|
+
{
|
|
2745
|
+
name: 'explain_file',
|
|
2746
|
+
description:
|
|
2747
|
+
'Explain a specific file: returns its extracted signatures, direct imports ' +
|
|
2748
|
+
'(files it depends on), and callers (files that import it). ' +
|
|
2749
|
+
'Ideal for understanding a file in isolation without reading raw source. ' +
|
|
2750
|
+
'Requires the context file to have been generated first.',
|
|
2751
|
+
inputSchema: {
|
|
2752
|
+
type: 'object',
|
|
2753
|
+
properties: {
|
|
2754
|
+
path: {
|
|
2755
|
+
type: 'string',
|
|
2756
|
+
description:
|
|
2757
|
+
'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
|
|
2758
|
+
'Use the paths shown in read_context output.',
|
|
2759
|
+
},
|
|
2760
|
+
},
|
|
2761
|
+
required: ['path'],
|
|
2762
|
+
},
|
|
2763
|
+
},
|
|
2764
|
+
{
|
|
2765
|
+
name: 'list_modules',
|
|
2766
|
+
description:
|
|
2767
|
+
'List all top-level modules (srcDirs) present in the context file, ' +
|
|
2768
|
+
'sorted by token count descending. Use this to decide which module to ' +
|
|
2769
|
+
'pass to read_context before querying a specific area of the codebase.',
|
|
2770
|
+
inputSchema: {
|
|
2771
|
+
type: 'object',
|
|
2772
|
+
properties: {},
|
|
2773
|
+
required: [],
|
|
2774
|
+
},
|
|
2775
|
+
},
|
|
2776
|
+
];
|
|
2777
|
+
|
|
2778
|
+
module.exports = { TOOLS };
|
|
2779
|
+
|
|
2780
|
+
};
|
|
2781
|
+
|
|
2782
|
+
// ── ./src/routing/classifier ──
|
|
2783
|
+
__factories["./src/routing/classifier"] = function(module, exports) {
|
|
2784
|
+
|
|
2785
|
+
/**
|
|
2786
|
+
* Classify files by complexity tier for model routing hints.
|
|
2787
|
+
*
|
|
2788
|
+
* Tiers:
|
|
2789
|
+
* 'fast' — simple files: config, markup, templates, trivial utilities
|
|
2790
|
+
* 'balanced' — standard application code: moderate functions and classes
|
|
2791
|
+
* 'powerful' — complex files: large classes, many exports, security-critical paths
|
|
2792
|
+
*
|
|
2793
|
+
* @param {string} filePath - absolute path to the file
|
|
2794
|
+
* @param {string[]} sigs - extracted signatures for the file
|
|
2795
|
+
* @returns {'fast'|'balanced'|'powerful'}
|
|
2796
|
+
*/
|
|
2797
|
+
function classify(filePath, sigs) {
|
|
2798
|
+
const lower = filePath.toLowerCase();
|
|
2799
|
+
const sigCount = sigs.length;
|
|
2800
|
+
|
|
2801
|
+
// ── Fast tier heuristics ────────────────────────────────────────────────
|
|
2802
|
+
// Configuration, markup, templates, and trivial utilities are small tasks
|
|
2803
|
+
if (
|
|
2804
|
+
lower.endsWith('.json') ||
|
|
2805
|
+
lower.endsWith('.yml') ||
|
|
2806
|
+
lower.endsWith('.yaml') ||
|
|
2807
|
+
lower.endsWith('.toml') ||
|
|
2808
|
+
lower.endsWith('.env') ||
|
|
2809
|
+
lower.endsWith('.html') ||
|
|
2810
|
+
lower.endsWith('.htm') ||
|
|
2811
|
+
lower.endsWith('.css') ||
|
|
2812
|
+
lower.endsWith('.scss') ||
|
|
2813
|
+
lower.endsWith('.sass') ||
|
|
2814
|
+
lower.endsWith('.less') ||
|
|
2815
|
+
/dockerfile/i.test(lower) ||
|
|
2816
|
+
lower.endsWith('.sh') ||
|
|
2817
|
+
lower.endsWith('.bash') ||
|
|
2818
|
+
lower.endsWith('.zsh') ||
|
|
2819
|
+
lower.endsWith('.fish')
|
|
2820
|
+
) {
|
|
2821
|
+
return 'fast';
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// Config-like directories
|
|
2825
|
+
if (
|
|
2826
|
+
lower.includes('/config/') ||
|
|
2827
|
+
lower.includes('/configs/') ||
|
|
2828
|
+
lower.includes('/fixtures/') ||
|
|
2829
|
+
lower.includes('/migrations/') ||
|
|
2830
|
+
lower.includes('/seeds/')
|
|
2831
|
+
) {
|
|
2832
|
+
return sigCount > 4 ? 'balanced' : 'fast';
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// Test files — usually standard complexity
|
|
2836
|
+
if (/\.(test|spec)\.[a-z]+$/.test(lower) || /_test\.[a-z]+$/.test(lower)) {
|
|
2837
|
+
return 'balanced';
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// ── Powerful tier heuristics — high-signal keywords in the path ─────────
|
|
2841
|
+
if (
|
|
2842
|
+
lower.includes('/security/') ||
|
|
2843
|
+
lower.includes('/auth/') ||
|
|
2844
|
+
lower.includes('/crypto/') ||
|
|
2845
|
+
lower.includes('/core/') ||
|
|
2846
|
+
lower.includes('/engine/') ||
|
|
2847
|
+
lower.includes('/compiler/') ||
|
|
2848
|
+
lower.includes('/parser/') ||
|
|
2849
|
+
lower.includes('/scheduler/') ||
|
|
2850
|
+
lower.includes('/orchestrat')
|
|
2851
|
+
) {
|
|
2852
|
+
return 'powerful';
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
// Many exports → complex file
|
|
2856
|
+
if (sigCount >= 12) return 'powerful';
|
|
2857
|
+
|
|
2858
|
+
// Large number of class-level methods (indented 2-space sigs)
|
|
2859
|
+
const methodCount = sigs.filter((s) => s.startsWith(' ')).length;
|
|
2860
|
+
if (methodCount >= 8) return 'powerful';
|
|
2861
|
+
|
|
2862
|
+
// ── Balanced covers everything else ────────────────────────────────────
|
|
2863
|
+
if (sigCount <= 2) return 'fast';
|
|
2864
|
+
return 'balanced';
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
/**
|
|
2868
|
+
* Classify all file entries and group them by tier.
|
|
2869
|
+
*
|
|
2870
|
+
* @param {Array<{filePath: string, sigs: string[]}>} fileEntries
|
|
2871
|
+
* @returns {{ fast: string[], balanced: string[], powerful: string[] }}
|
|
2872
|
+
* Each array contains relative paths (relative to cwd).
|
|
2873
|
+
*/
|
|
2874
|
+
function classifyAll(fileEntries, cwd) {
|
|
2875
|
+
const path = require('path');
|
|
2876
|
+
const result = { fast: [], balanced: [], powerful: [] };
|
|
2877
|
+
for (const { filePath, sigs } of fileEntries) {
|
|
2878
|
+
const tier = classify(filePath, sigs);
|
|
2879
|
+
result[tier].push(path.relative(cwd, filePath));
|
|
2880
|
+
}
|
|
2881
|
+
return result;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
module.exports = { classify, classifyAll };
|
|
2885
|
+
|
|
2886
|
+
};
|
|
2887
|
+
|
|
2888
|
+
// ── ./src/routing/hints ──
|
|
2889
|
+
__factories["./src/routing/hints"] = function(module, exports) {
|
|
2890
|
+
|
|
2891
|
+
/**
|
|
2892
|
+
* Model routing hint definitions for SigMap.
|
|
2893
|
+
*
|
|
2894
|
+
* Maps complexity tiers to:
|
|
2895
|
+
* - example model names (vendor-agnostic labels used as hints)
|
|
2896
|
+
* - task types that belong to each tier
|
|
2897
|
+
* - estimated per-1K-token API cost (USD, illustrative — update as needed)
|
|
2898
|
+
*
|
|
2899
|
+
* These are embedded in the generated context file when `routing: true`
|
|
2900
|
+
* so that AI agents and developers know which model tier to invoke.
|
|
2901
|
+
*/
|
|
2902
|
+
|
|
2903
|
+
const TIERS = {
|
|
2904
|
+
fast: {
|
|
2905
|
+
label: 'Fast (low-cost)',
|
|
2906
|
+
examples: 'claude-haiku-4-5, gpt-5-1-codex-mini, gemini-3-flash',
|
|
2907
|
+
tasks: [
|
|
2908
|
+
'Autocomplete and inline suggestions',
|
|
2909
|
+
'Edit config or markup files',
|
|
2910
|
+
'Fix typos and rename symbols',
|
|
2911
|
+
'Format, lint, and trivial style changes',
|
|
2912
|
+
'Explain a short utility function',
|
|
2913
|
+
'Generate simple shell scripts or Dockerfiles',
|
|
2914
|
+
],
|
|
2915
|
+
costHint: '~$0.0008 / 1K tokens',
|
|
2916
|
+
},
|
|
2917
|
+
|
|
2918
|
+
balanced: {
|
|
2919
|
+
label: 'Balanced (mid-tier)',
|
|
2920
|
+
examples: 'claude-sonnet-4-6, gpt-5-2, gemini-3-1-pro',
|
|
2921
|
+
tasks: [
|
|
2922
|
+
'Write unit or integration tests',
|
|
2923
|
+
'Implement a well-scoped feature function',
|
|
2924
|
+
'Debug a runtime error with stack trace',
|
|
2925
|
+
'Refactor a module (< 200 lines)',
|
|
2926
|
+
'Generate a PR description',
|
|
2927
|
+
'Explain a multi-function module',
|
|
2928
|
+
],
|
|
2929
|
+
costHint: '~$0.003 / 1K tokens',
|
|
2930
|
+
},
|
|
2931
|
+
|
|
2932
|
+
powerful: {
|
|
2933
|
+
label: 'Powerful (high-cost)',
|
|
2934
|
+
examples: 'claude-opus-4-6, gpt-5-4, gemini-2-5-pro',
|
|
2935
|
+
tasks: [
|
|
2936
|
+
'Cross-cutting architecture decisions',
|
|
2937
|
+
'Multi-file refactor spanning 5+ files',
|
|
2938
|
+
'Security audit (OWASP Top 10)',
|
|
2939
|
+
'Complex debugging across async boundaries',
|
|
2940
|
+
'Migration plan for a library/framework upgrade',
|
|
2941
|
+
'Designing a new module from requirements',
|
|
2942
|
+
],
|
|
2943
|
+
costHint: '~$0.015 / 1K tokens',
|
|
2944
|
+
},
|
|
2945
|
+
};
|
|
2946
|
+
|
|
2947
|
+
/**
|
|
2948
|
+
* Format the routing section as markdown to append to the context file.
|
|
2949
|
+
*
|
|
2950
|
+
* @param {{ fast: string[], balanced: string[], powerful: string[] }} groups
|
|
2951
|
+
* Relative file paths grouped by tier (from classifier.classifyAll).
|
|
2952
|
+
* @returns {string} Markdown block to embed in the context output.
|
|
2953
|
+
*/
|
|
2954
|
+
function formatRoutingSection(groups) {
|
|
2955
|
+
const lines = [
|
|
2956
|
+
'',
|
|
2957
|
+
'---',
|
|
2958
|
+
'',
|
|
2959
|
+
'## Model routing hints',
|
|
2960
|
+
'<!-- Generated by SigMap routing module — update gen-context.config.json to disable -->',
|
|
2961
|
+
'',
|
|
2962
|
+
'Select the model tier based on the task complexity and the files involved.',
|
|
2963
|
+
'',
|
|
2964
|
+
];
|
|
2965
|
+
|
|
2966
|
+
for (const [tier, info] of Object.entries(TIERS)) {
|
|
2967
|
+
const files = groups[tier] || [];
|
|
2968
|
+
lines.push(`### ${info.label}`);
|
|
2969
|
+
lines.push(`**Examples:** ${info.examples} `);
|
|
2970
|
+
lines.push(`**Cost:** ${info.costHint}`);
|
|
2971
|
+
lines.push('');
|
|
2972
|
+
lines.push('**Use for tasks like:**');
|
|
2973
|
+
for (const task of info.tasks) lines.push(`- ${task}`);
|
|
2974
|
+
lines.push('');
|
|
2975
|
+
if (files.length > 0) {
|
|
2976
|
+
lines.push('**Files in this tier:**');
|
|
2977
|
+
for (const f of files.slice(0, 15)) lines.push(`- \`${f}\``);
|
|
2978
|
+
if (files.length > 15) lines.push(`- … and ${files.length - 15} more`);
|
|
2979
|
+
} else {
|
|
2980
|
+
lines.push('**Files in this tier:** _(none detected)_');
|
|
2981
|
+
}
|
|
2982
|
+
lines.push('');
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
lines.push('> **Tip:** Run `node gen-context.js --routing` to regenerate routing hints.');
|
|
2986
|
+
lines.push('> See `docs/MODEL_ROUTING.md` for full routing guide and cost optimisation tips.');
|
|
2987
|
+
|
|
2988
|
+
return lines.join('\n');
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
module.exports = { TIERS, formatRoutingSection };
|
|
2992
|
+
|
|
2993
|
+
};
|
|
2994
|
+
|
|
2995
|
+
// ── ./src/security/patterns ──
|
|
2996
|
+
__factories["./src/security/patterns"] = function(module, exports) {
|
|
2997
|
+
|
|
2998
|
+
/**
|
|
2999
|
+
* Secret detection patterns for SigMap scanner.
|
|
3000
|
+
* Each pattern has a name and a regex tested against signature strings.
|
|
3001
|
+
*/
|
|
3002
|
+
const PATTERNS = [
|
|
3003
|
+
{
|
|
3004
|
+
name: 'AWS Access Key',
|
|
3005
|
+
regex: /AKIA[0-9A-Z]{16}/,
|
|
3006
|
+
},
|
|
3007
|
+
{
|
|
3008
|
+
name: 'AWS Secret Key',
|
|
3009
|
+
// 40-char base64-like string following common AWS secret key assignment patterns
|
|
3010
|
+
regex: /(?:aws_secret|secret_access_key|SecretAccessKey)\s*[:=]\s*['"]?[0-9a-zA-Z/+]{40}/i,
|
|
3011
|
+
},
|
|
3012
|
+
{
|
|
3013
|
+
name: 'GCP API Key',
|
|
3014
|
+
regex: /AIza[0-9A-Za-z\-_]{35}/,
|
|
3015
|
+
},
|
|
3016
|
+
{
|
|
3017
|
+
name: 'GitHub Token',
|
|
3018
|
+
regex: /gh[pousr]_[A-Za-z0-9_]{36,}/,
|
|
3019
|
+
},
|
|
3020
|
+
{
|
|
3021
|
+
name: 'JWT Token',
|
|
3022
|
+
regex: /eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/,
|
|
3023
|
+
},
|
|
3024
|
+
{
|
|
3025
|
+
name: 'DB Connection String',
|
|
3026
|
+
regex: /(mongodb|postgres|postgresql|mysql|redis):\/\/[^:]+:[^@]+@/i,
|
|
3027
|
+
},
|
|
3028
|
+
{
|
|
3029
|
+
name: 'SSH Private Key',
|
|
3030
|
+
regex: /-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/,
|
|
3031
|
+
},
|
|
3032
|
+
{
|
|
3033
|
+
name: 'Stripe Key',
|
|
3034
|
+
regex: /sk_(live|test)_[0-9a-zA-Z]{24,}/,
|
|
3035
|
+
},
|
|
3036
|
+
{
|
|
3037
|
+
name: 'Twilio Key',
|
|
3038
|
+
regex: /SK[0-9a-fA-F]{32}/,
|
|
3039
|
+
},
|
|
3040
|
+
{
|
|
3041
|
+
name: 'Generic Secret',
|
|
3042
|
+
regex: /(secret|password|passwd|api_key|apikey|auth_token|access_token)\s*[:=]\s*['"][^'"]{8,}['"]/i,
|
|
3043
|
+
},
|
|
3044
|
+
];
|
|
3045
|
+
|
|
3046
|
+
module.exports = { PATTERNS };
|
|
3047
|
+
|
|
3048
|
+
};
|
|
3049
|
+
|
|
3050
|
+
// ── ./src/security/scanner ──
|
|
3051
|
+
__factories["./src/security/scanner"] = function(module, exports) {
|
|
3052
|
+
|
|
3053
|
+
const { PATTERNS } = __require('./src/security/patterns');
|
|
3054
|
+
|
|
3055
|
+
/**
|
|
3056
|
+
* Scan an array of signature strings for secrets.
|
|
3057
|
+
*
|
|
3058
|
+
* @param {string[]} signatures - Array of extracted signature strings
|
|
3059
|
+
* @param {string} filePath - Source file path (used in redaction message)
|
|
3060
|
+
* @returns {{ safe: string[], redacted: boolean }}
|
|
3061
|
+
* safe — signatures with any secret-containing entries replaced
|
|
3062
|
+
* redacted — true if at least one signature was redacted
|
|
3063
|
+
*/
|
|
3064
|
+
function scan(signatures, filePath) {
|
|
3065
|
+
if (!Array.isArray(signatures)) return { safe: [], redacted: false };
|
|
3066
|
+
|
|
3067
|
+
try {
|
|
3068
|
+
let redacted = false;
|
|
3069
|
+
const safe = signatures.map((sig) => {
|
|
3070
|
+
if (typeof sig !== 'string') return sig;
|
|
3071
|
+
for (const pattern of PATTERNS) {
|
|
3072
|
+
if (pattern.regex.test(sig)) {
|
|
3073
|
+
redacted = true;
|
|
3074
|
+
return `[REDACTED — ${pattern.name} detected in ${filePath}]`;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
return sig;
|
|
3078
|
+
});
|
|
3079
|
+
return { safe, redacted };
|
|
3080
|
+
} catch (_) {
|
|
3081
|
+
// Never throw — return original signatures on any error
|
|
3082
|
+
return { safe: signatures, redacted: false };
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
module.exports = { scan };
|
|
3087
|
+
|
|
3088
|
+
};
|
|
3089
|
+
|
|
3090
|
+
// ── ./src/tracking/logger ──
|
|
3091
|
+
__factories["./src/tracking/logger"] = function(module, exports) {
|
|
3092
|
+
|
|
3093
|
+
/**
|
|
3094
|
+
* SigMap usage logger (v0.9)
|
|
3095
|
+
*
|
|
3096
|
+
* Writes an append-only newline-delimited JSON (NDJSON) log at
|
|
3097
|
+
* .context/usage.ndjson
|
|
3098
|
+
*
|
|
3099
|
+
* Each line is one JSON object describing a gen-context run.
|
|
3100
|
+
* Zero npm dependencies — pure Node.js fs.
|
|
3101
|
+
*
|
|
3102
|
+
* Enabled by:
|
|
3103
|
+
* config.tracking: true (gen-context.config.json)
|
|
3104
|
+
* --track CLI flag
|
|
3105
|
+
*/
|
|
3106
|
+
|
|
3107
|
+
const fs = require('fs');
|
|
3108
|
+
const path = require('path');
|
|
3109
|
+
|
|
3110
|
+
const LOG_FILE = path.join('.context', 'usage.ndjson');
|
|
3111
|
+
|
|
3112
|
+
/**
|
|
3113
|
+
* Append one run entry to the usage log.
|
|
3114
|
+
* @param {object} entry - Run metrics from runGenerate()
|
|
3115
|
+
* @param {string} cwd - Project root (absolute path)
|
|
3116
|
+
*/
|
|
3117
|
+
function logRun(entry, cwd) {
|
|
3118
|
+
try {
|
|
3119
|
+
const logPath = path.join(cwd, LOG_FILE);
|
|
3120
|
+
const dir = path.dirname(logPath);
|
|
3121
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
3122
|
+
|
|
3123
|
+
const record = {
|
|
3124
|
+
ts: new Date().toISOString(),
|
|
3125
|
+
version: entry.version || '0.9.0',
|
|
3126
|
+
fileCount: entry.fileCount || 0,
|
|
3127
|
+
droppedCount: entry.droppedCount || 0,
|
|
3128
|
+
rawTokens: entry.rawTokens || 0,
|
|
3129
|
+
finalTokens: entry.finalTokens || 0,
|
|
3130
|
+
reductionPct: entry.rawTokens > 0
|
|
3131
|
+
? parseFloat((100 - (entry.finalTokens / entry.rawTokens) * 100).toFixed(1))
|
|
3132
|
+
: 0,
|
|
3133
|
+
overBudget: entry.overBudget || false,
|
|
3134
|
+
budgetLimit: entry.budgetLimit || 6000,
|
|
3135
|
+
};
|
|
3136
|
+
|
|
3137
|
+
fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
|
|
3138
|
+
} catch (err) {
|
|
3139
|
+
// Never crash the main process — tracking is optional
|
|
3140
|
+
process.stderr.write(`[sigmap] tracking: could not write log: ${err.message}\n`);
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
/**
|
|
3145
|
+
* Read and parse all usage log entries.
|
|
3146
|
+
* @param {string} cwd - Project root (absolute path)
|
|
3147
|
+
* @returns {object[]} Array of parsed log records (oldest first)
|
|
3148
|
+
*/
|
|
3149
|
+
function readLog(cwd) {
|
|
3150
|
+
try {
|
|
3151
|
+
const logPath = path.join(cwd, LOG_FILE);
|
|
3152
|
+
if (!fs.existsSync(logPath)) return [];
|
|
3153
|
+
const raw = fs.readFileSync(logPath, 'utf8');
|
|
3154
|
+
return raw
|
|
3155
|
+
.split('\n')
|
|
3156
|
+
.filter(Boolean)
|
|
3157
|
+
.map((line) => {
|
|
3158
|
+
try { return JSON.parse(line); } catch (_) { return null; }
|
|
3159
|
+
})
|
|
3160
|
+
.filter(Boolean);
|
|
3161
|
+
} catch (_) {
|
|
3162
|
+
return [];
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
/**
|
|
3167
|
+
* Compute summary statistics from an array of log records.
|
|
3168
|
+
* @param {object[]} entries
|
|
3169
|
+
* @returns {object} Summary stats
|
|
3170
|
+
*/
|
|
3171
|
+
function summarize(entries) {
|
|
3172
|
+
if (!entries || entries.length === 0) {
|
|
3173
|
+
return {
|
|
3174
|
+
totalRuns: 0,
|
|
3175
|
+
avgReductionPct: 0,
|
|
3176
|
+
avgFinalTokens: 0,
|
|
3177
|
+
avgRawTokens: 0,
|
|
3178
|
+
minFinalTokens: 0,
|
|
3179
|
+
maxFinalTokens: 0,
|
|
3180
|
+
firstRun: null,
|
|
3181
|
+
lastRun: null,
|
|
3182
|
+
overBudgetRuns: 0,
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
const reductions = entries.map((e) => e.reductionPct || 0);
|
|
3187
|
+
const finals = entries.map((e) => e.finalTokens || 0);
|
|
3188
|
+
const raws = entries.map((e) => e.rawTokens || 0);
|
|
3189
|
+
|
|
3190
|
+
const avg = (arr) => arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
3191
|
+
|
|
3192
|
+
return {
|
|
3193
|
+
totalRuns: entries.length,
|
|
3194
|
+
avgReductionPct: parseFloat(avg(reductions).toFixed(1)),
|
|
3195
|
+
avgFinalTokens: Math.round(avg(finals)),
|
|
3196
|
+
avgRawTokens: Math.round(avg(raws)),
|
|
3197
|
+
minFinalTokens: Math.min(...finals),
|
|
3198
|
+
maxFinalTokens: Math.max(...finals),
|
|
3199
|
+
firstRun: entries[0].ts || null,
|
|
3200
|
+
lastRun: entries[entries.length - 1].ts || null,
|
|
3201
|
+
overBudgetRuns: entries.filter((e) => e.overBudget).length,
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
module.exports = { logRun, readLog, summarize };
|
|
3206
|
+
|
|
3207
|
+
};
|
|
3208
|
+
|
|
3209
|
+
|
|
3210
|
+
/**
|
|
3211
|
+
* SigMap — gen-context.js v1.2.0
|
|
3212
|
+
* Zero-dependency AI context engine.
|
|
3213
|
+
* Runs with: node gen-context.js
|
|
3214
|
+
* No npm install required. Node 18+ built-ins only.
|
|
3215
|
+
*/
|
|
3216
|
+
|
|
3217
|
+
const fs = require('fs');
|
|
3218
|
+
const path = require('path');
|
|
3219
|
+
const os = require('os');
|
|
3220
|
+
const { execSync } = require('child_process');
|
|
3221
|
+
|
|
3222
|
+
const VERSION = '1.4.0';
|
|
3223
|
+
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
3224
|
+
|
|
3225
|
+
// ---------------------------------------------------------------------------
|
|
3226
|
+
// Config — delegate to src/config/loader.js
|
|
3227
|
+
// ---------------------------------------------------------------------------
|
|
3228
|
+
const { loadConfig } = __require('./src/config/loader');
|
|
3229
|
+
const { DEFAULTS } = __require('./src/config/defaults');
|
|
3230
|
+
|
|
3231
|
+
// ---------------------------------------------------------------------------
|
|
3232
|
+
// Language → extractor mapping (by file extension)
|
|
3233
|
+
// ---------------------------------------------------------------------------
|
|
3234
|
+
const EXT_MAP = {
|
|
3235
|
+
'.ts': 'typescript', '.tsx': 'typescript',
|
|
3236
|
+
'.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
|
|
3237
|
+
'.py': 'python', '.pyw': 'python',
|
|
3238
|
+
'.java': 'java',
|
|
3239
|
+
'.kt': 'kotlin', '.kts': 'kotlin',
|
|
3240
|
+
'.go': 'go',
|
|
3241
|
+
'.rs': 'rust',
|
|
3242
|
+
'.cs': 'csharp',
|
|
3243
|
+
'.cpp': 'cpp', '.c': 'cpp', '.h': 'cpp', '.hpp': 'cpp', '.cc': 'cpp',
|
|
3244
|
+
'.rb': 'ruby', '.rake': 'ruby',
|
|
3245
|
+
'.php': 'php',
|
|
3246
|
+
'.swift': 'swift',
|
|
3247
|
+
'.dart': 'dart',
|
|
3248
|
+
'.scala': 'scala', '.sc': 'scala',
|
|
3249
|
+
'.vue': 'vue',
|
|
3250
|
+
'.svelte': 'svelte',
|
|
3251
|
+
'.html': 'html', '.htm': 'html',
|
|
3252
|
+
'.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
|
|
3253
|
+
'.yml': 'yaml', '.yaml': 'yaml',
|
|
3254
|
+
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
|
|
3255
|
+
};
|
|
3256
|
+
|
|
3257
|
+
// Dockerfile handled separately (no extension)
|
|
3258
|
+
function isDockerfile(filename) {
|
|
3259
|
+
return filename === 'Dockerfile' || filename.startsWith('Dockerfile.');
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
// ---------------------------------------------------------------------------
|
|
3263
|
+
// .contextignore parser (gitignore-style subset)
|
|
3264
|
+
// ---------------------------------------------------------------------------
|
|
3265
|
+
function loadIgnorePatterns(cwd) {
|
|
3266
|
+
const patterns = [];
|
|
3267
|
+
for (const name of ['.contextignore', '.repomixignore']) {
|
|
3268
|
+
const p = path.join(cwd, name);
|
|
3269
|
+
if (fs.existsSync(p)) {
|
|
3270
|
+
const lines = fs.readFileSync(p, 'utf8').split('\n');
|
|
3271
|
+
for (const line of lines) {
|
|
3272
|
+
const trimmed = line.trim();
|
|
3273
|
+
if (trimmed && !trimmed.startsWith('#')) patterns.push(trimmed);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
return patterns;
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
function matchesIgnore(relPath, patterns) {
|
|
3281
|
+
for (const pat of patterns) {
|
|
3282
|
+
const normalized = pat.replace(/\\/g, '/');
|
|
3283
|
+
// Simple glob: support * and ** and trailing /
|
|
3284
|
+
const regexStr = normalized
|
|
3285
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
3286
|
+
.replace(/\*\*/g, '___DOUBLE___')
|
|
3287
|
+
.replace(/\*/g, '[^/]*')
|
|
3288
|
+
.replace(/___DOUBLE___/g, '.*');
|
|
3289
|
+
const regex = new RegExp(`(^|/)${regexStr}($|/)`);
|
|
3290
|
+
if (regex.test(relPath)) return true;
|
|
3291
|
+
}
|
|
3292
|
+
return false;
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
// ---------------------------------------------------------------------------
|
|
3296
|
+
// File walker
|
|
3297
|
+
// ---------------------------------------------------------------------------
|
|
3298
|
+
function walkDir(dir, exclude, maxDepth, depth = 0) {
|
|
3299
|
+
if (depth > maxDepth) return [];
|
|
3300
|
+
let results = [];
|
|
3301
|
+
let entries;
|
|
3302
|
+
try {
|
|
3303
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
3304
|
+
} catch (_) {
|
|
3305
|
+
return [];
|
|
3306
|
+
}
|
|
3307
|
+
for (const entry of entries) {
|
|
3308
|
+
if (exclude.includes(entry.name)) continue;
|
|
3309
|
+
const full = path.join(dir, entry.name);
|
|
3310
|
+
if (entry.isDirectory()) {
|
|
3311
|
+
results = results.concat(walkDir(full, exclude, maxDepth, depth + 1));
|
|
3312
|
+
} else if (entry.isFile()) {
|
|
3313
|
+
results.push(full);
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
return results;
|
|
3317
|
+
}
|
|
3318
|
+
|
|
3319
|
+
function buildFileList(cwd, config) {
|
|
3320
|
+
const files = [];
|
|
3321
|
+
for (const srcDir of config.srcDirs) {
|
|
3322
|
+
const abs = path.join(cwd, srcDir);
|
|
3323
|
+
if (!fs.existsSync(abs)) continue;
|
|
3324
|
+
const found = walkDir(abs, config.exclude, config.maxDepth);
|
|
3325
|
+
files.push(...found);
|
|
3326
|
+
}
|
|
3327
|
+
// Deduplicate
|
|
3328
|
+
return [...new Set(files)];
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
// ---------------------------------------------------------------------------
|
|
3332
|
+
// Extractor loader (lazy, cached)
|
|
3333
|
+
// ---------------------------------------------------------------------------
|
|
3334
|
+
const _extractorCache = {};
|
|
3335
|
+
function getExtractor(name) {
|
|
3336
|
+
if (_extractorCache[name]) return _extractorCache[name];
|
|
3337
|
+
try {
|
|
3338
|
+
const mod = __require(`./src/extractors/${name}`);
|
|
3339
|
+
_extractorCache[name] = mod;
|
|
3340
|
+
return mod;
|
|
3341
|
+
} catch (err) {
|
|
3342
|
+
console.warn(`[sigmap] failed to load extractor ${name}: ${err.message}`);
|
|
3343
|
+
return null;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
function detectAndExtract(filePath, content, maxSigsPerFile) {
|
|
3348
|
+
const base = path.basename(filePath);
|
|
3349
|
+
const ext = path.extname(base).toLowerCase();
|
|
3350
|
+
let extractorName = EXT_MAP[ext] || null;
|
|
3351
|
+
if (!extractorName && isDockerfile(base)) extractorName = 'dockerfile';
|
|
3352
|
+
if (!extractorName) return [];
|
|
3353
|
+
|
|
3354
|
+
const extractor = getExtractor(extractorName);
|
|
3355
|
+
if (!extractor) return [];
|
|
3356
|
+
|
|
3357
|
+
try {
|
|
3358
|
+
const sigs = extractor.extract(content);
|
|
3359
|
+
return Array.isArray(sigs) ? sigs.slice(0, maxSigsPerFile) : [];
|
|
3360
|
+
} catch (err) {
|
|
3361
|
+
console.warn(`[sigmap] extractor failed for ${filePath}: ${err.message}`);
|
|
3362
|
+
return [];
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
|
|
3366
|
+
// ---------------------------------------------------------------------------
|
|
3367
|
+
// Token budget enforcement
|
|
3368
|
+
// ---------------------------------------------------------------------------
|
|
3369
|
+
function estimateTokens(str) {
|
|
3370
|
+
return Math.ceil(str.length / 4);
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
function isTestFile(filePath) {
|
|
3374
|
+
return /\.(test|spec)\.[a-z]+$/.test(filePath) || /_test\.[a-z]+$/.test(filePath);
|
|
3375
|
+
}
|
|
3376
|
+
|
|
3377
|
+
function isConfigFile(filePath) {
|
|
3378
|
+
return /\.(config|conf)\.[a-z]+$/.test(filePath) ||
|
|
3379
|
+
path.extname(filePath) === '.json';
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
function isGeneratedFile(filePath) {
|
|
3383
|
+
return /(\.generated\.|\.pb\.|_pb\.)/.test(filePath);
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
function applyTokenBudget(fileEntries, maxTokens) {
|
|
3387
|
+
// fileEntries: [{ filePath, sigs, mtime }]
|
|
3388
|
+
// Reserve ~10% for formatting overhead (section headers, code fences, top-level header)
|
|
3389
|
+
const effectiveBudget = Math.floor(maxTokens * 0.90);
|
|
3390
|
+
let total = fileEntries.reduce((s, e) => s + estimateTokens(e.sigs.join('\n')), 0);
|
|
3391
|
+
if (total <= effectiveBudget) return fileEntries;
|
|
3392
|
+
|
|
3393
|
+
// Sort by drop priority (drop first = index 0)
|
|
3394
|
+
const withPriority = fileEntries.map((e) => {
|
|
3395
|
+
let priority = 0;
|
|
3396
|
+
if (isGeneratedFile(e.filePath)) priority = 10;
|
|
3397
|
+
else if (isTestFile(e.filePath)) priority = 8;
|
|
3398
|
+
else if (isConfigFile(e.filePath)) priority = 6;
|
|
3399
|
+
else priority = 4;
|
|
3400
|
+
return { ...e, priority };
|
|
3401
|
+
});
|
|
3402
|
+
|
|
3403
|
+
// Within same priority, sort by mtime ascending (oldest first = drop first)
|
|
3404
|
+
withPriority.sort((a, b) => {
|
|
3405
|
+
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
3406
|
+
return (a.mtime || 0) - (b.mtime || 0);
|
|
3407
|
+
});
|
|
3408
|
+
|
|
3409
|
+
const kept = [];
|
|
3410
|
+
let dropped = 0;
|
|
3411
|
+
for (let i = withPriority.length - 1; i >= 0; i--) {
|
|
3412
|
+
const entry = withPriority[i];
|
|
3413
|
+
const entryTokens = estimateTokens(entry.sigs.join('\n'));
|
|
3414
|
+
if (total <= effectiveBudget) {
|
|
3415
|
+
kept.unshift(entry);
|
|
3416
|
+
} else {
|
|
3417
|
+
total -= entryTokens;
|
|
3418
|
+
dropped++;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
if (dropped > 0) {
|
|
3422
|
+
console.warn(`[sigmap] budget: dropped ${dropped} files to stay under ${maxTokens} tokens`);
|
|
3423
|
+
}
|
|
3424
|
+
return kept;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
// ---------------------------------------------------------------------------
|
|
3428
|
+
// Recently committed files (git, optional)
|
|
3429
|
+
// ---------------------------------------------------------------------------
|
|
3430
|
+
function getRecentlyCommittedFiles(cwd, count) {
|
|
3431
|
+
const n = count || 10;
|
|
3432
|
+
try {
|
|
3433
|
+
const out = execSync(`git log --name-only --format="" -n ${n}`, {
|
|
3434
|
+
cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
3435
|
+
});
|
|
3436
|
+
return new Set(out.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(cwd, f)));
|
|
3437
|
+
} catch (_) {
|
|
3438
|
+
return new Set();
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// ---------------------------------------------------------------------------
|
|
3443
|
+
// Diff mode: files changed in working tree or staging area
|
|
3444
|
+
// ---------------------------------------------------------------------------
|
|
3445
|
+
function getDiffFiles(cwd, stagedOnly) {
|
|
3446
|
+
try {
|
|
3447
|
+
const cmd = stagedOnly ? 'git diff --cached --name-only' : 'git diff HEAD --name-only';
|
|
3448
|
+
const out = execSync(cmd, { cwd, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
3449
|
+
return new Set(
|
|
3450
|
+
out.split('\n').map((f) => f.trim()).filter(Boolean).map((f) => path.resolve(cwd, f))
|
|
3451
|
+
);
|
|
3452
|
+
} catch (_) {
|
|
3453
|
+
return new Set();
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
// ---------------------------------------------------------------------------
|
|
3458
|
+
// Output formatter
|
|
3459
|
+
// ---------------------------------------------------------------------------
|
|
3460
|
+
function formatOutput(fileEntries, cwd, routingEnabled) {
|
|
3461
|
+
const lines = [
|
|
3462
|
+
'<!-- Generated by SigMap gen-context.js v' + VERSION + ' -->',
|
|
3463
|
+
'<!-- DO NOT EDIT below the marker line — run gen-context.js to regenerate -->',
|
|
3464
|
+
'',
|
|
3465
|
+
'# Code signatures',
|
|
3466
|
+
'',
|
|
3467
|
+
];
|
|
3468
|
+
|
|
3469
|
+
// Group by top-level src dir
|
|
3470
|
+
const groups = {};
|
|
3471
|
+
for (const entry of fileEntries) {
|
|
3472
|
+
const rel = path.relative(cwd, entry.filePath);
|
|
3473
|
+
const parts = rel.split(path.sep);
|
|
3474
|
+
const group = parts.length > 1 ? parts[0] : '.';
|
|
3475
|
+
if (!groups[group]) groups[group] = [];
|
|
3476
|
+
groups[group].push({ rel, sigs: entry.sigs });
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
for (const [group, entries] of Object.entries(groups).sort()) {
|
|
3480
|
+
lines.push(`## ${group}`);
|
|
3481
|
+
lines.push('');
|
|
3482
|
+
for (const { rel, sigs } of entries) {
|
|
3483
|
+
if (sigs.length === 0) continue;
|
|
3484
|
+
lines.push(`### ${rel}`);
|
|
3485
|
+
lines.push('```');
|
|
3486
|
+
lines.push(...sigs);
|
|
3487
|
+
lines.push('```');
|
|
3488
|
+
lines.push('');
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
if (routingEnabled) {
|
|
3493
|
+
try {
|
|
3494
|
+
const { classifyAll } = __require('./src/routing/classifier');
|
|
3495
|
+
const { formatRoutingSection } = __require('./src/routing/hints');
|
|
3496
|
+
const groups = classifyAll(fileEntries, cwd);
|
|
3497
|
+
lines.push(formatRoutingSection(groups));
|
|
3498
|
+
} catch (err) {
|
|
3499
|
+
console.warn(`[sigmap] routing hints skipped: ${err.message}`);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
return lines.join('\n');
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
// ---------------------------------------------------------------------------
|
|
3507
|
+
// Output writers
|
|
3508
|
+
// ---------------------------------------------------------------------------
|
|
3509
|
+
function ensureDir(filePath) {
|
|
3510
|
+
const dir = path.dirname(filePath);
|
|
3511
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// ---------------------------------------------------------------------------
|
|
3515
|
+
// Cache output writer (v0.8)
|
|
3516
|
+
// ---------------------------------------------------------------------------
|
|
3517
|
+
function writeCacheOutput(content, cwd) {
|
|
3518
|
+
try {
|
|
3519
|
+
const { formatCache } = __require('./src/format/cache');
|
|
3520
|
+
const cachePath = path.join(cwd, '.github', 'copilot-instructions.cache.json');
|
|
3521
|
+
ensureDir(cachePath);
|
|
3522
|
+
fs.writeFileSync(cachePath, formatCache(content), 'utf8');
|
|
3523
|
+
console.warn(`[sigmap] cache: wrote ${path.relative(cwd, cachePath)}`);
|
|
3524
|
+
} catch (err) {
|
|
3525
|
+
console.warn(`[sigmap] cache: failed to write cache output: ${err.message}`);
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
function writeOutputs(content, targets, cwd) {
|
|
3530
|
+
const targetMap = {
|
|
3531
|
+
copilot: path.join(cwd, '.github', 'copilot-instructions.md'),
|
|
3532
|
+
cursor: path.join(cwd, '.cursorrules'),
|
|
3533
|
+
windsurf: path.join(cwd, '.windsurfrules'),
|
|
3534
|
+
};
|
|
3535
|
+
|
|
3536
|
+
for (const target of targets) {
|
|
3537
|
+
if (target === 'claude') {
|
|
3538
|
+
writeClaude(content, cwd);
|
|
3539
|
+
continue;
|
|
3540
|
+
}
|
|
3541
|
+
const outPath = targetMap[target];
|
|
3542
|
+
if (!outPath) {
|
|
3543
|
+
console.warn(`[sigmap] unknown output target: ${target}`);
|
|
3544
|
+
continue;
|
|
3545
|
+
}
|
|
3546
|
+
ensureDir(outPath);
|
|
3547
|
+
fs.writeFileSync(outPath, content, 'utf8');
|
|
3548
|
+
console.warn(`[sigmap] wrote ${path.relative(cwd, outPath)}`);
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
function writeClaude(content, cwd) {
|
|
3553
|
+
const claudePath = path.join(cwd, 'CLAUDE.md');
|
|
3554
|
+
let existing = '';
|
|
3555
|
+
if (fs.existsSync(claudePath)) {
|
|
3556
|
+
existing = fs.readFileSync(claudePath, 'utf8');
|
|
3557
|
+
}
|
|
3558
|
+
const markerIdx = existing.indexOf('## Auto-generated signatures');
|
|
3559
|
+
let newContent;
|
|
3560
|
+
if (markerIdx !== -1) {
|
|
3561
|
+
newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + content;
|
|
3562
|
+
} else {
|
|
3563
|
+
newContent = existing + MARKER + content;
|
|
3564
|
+
}
|
|
3565
|
+
fs.writeFileSync(claudePath, newContent, 'utf8');
|
|
3566
|
+
console.warn(`[sigmap] wrote CLAUDE.md (appended signatures)`);
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
// ---------------------------------------------------------------------------
|
|
3570
|
+
// Report
|
|
3571
|
+
// ---------------------------------------------------------------------------
|
|
3572
|
+
function printReport(inputTokens, finalTokens, fileCount, droppedCount, asJson, budgetLimit) {
|
|
3573
|
+
const reduction = inputTokens > 0 ? (100 - (finalTokens / inputTokens) * 100).toFixed(1) : 0;
|
|
3574
|
+
const overBudget = finalTokens > (budgetLimit || 6000);
|
|
3575
|
+
if (asJson) {
|
|
3576
|
+
process.stdout.write(JSON.stringify({
|
|
3577
|
+
version: VERSION,
|
|
3578
|
+
timestamp: new Date().toISOString(),
|
|
3579
|
+
rawTokens: inputTokens,
|
|
3580
|
+
inputTokens,
|
|
3581
|
+
finalTokens,
|
|
3582
|
+
fileCount,
|
|
3583
|
+
droppedCount,
|
|
3584
|
+
reductionPct: parseFloat(reduction),
|
|
3585
|
+
overBudget,
|
|
3586
|
+
budgetLimit: budgetLimit || 6000,
|
|
3587
|
+
}) + '\n');
|
|
3588
|
+
// Exit 1 in CI if over budget — lets pipelines fail fast
|
|
3589
|
+
if (overBudget) process.exitCode = 1;
|
|
3590
|
+
} else {
|
|
3591
|
+
console.log(`[sigmap] report:`);
|
|
3592
|
+
console.log(` version : ${VERSION}`);
|
|
3593
|
+
console.log(` files processed : ${fileCount}`);
|
|
3594
|
+
console.log(` files dropped : ${droppedCount}`);
|
|
3595
|
+
console.log(` input tokens : ~${inputTokens}`);
|
|
3596
|
+
console.log(` output tokens : ~${finalTokens}`);
|
|
3597
|
+
console.log(` budget limit : ${budgetLimit || 6000}`);
|
|
3598
|
+
console.log(` reduction : ${reduction}%`);
|
|
3599
|
+
if (overBudget) console.warn(`[sigmap] WARNING: output (${finalTokens} tokens) exceeds budget (${budgetLimit || 6000})`);
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
// ---------------------------------------------------------------------------
|
|
3604
|
+
// Watch mode
|
|
3605
|
+
// ---------------------------------------------------------------------------
|
|
3606
|
+
function watchMode(cwd, config) {
|
|
3607
|
+
console.warn('[sigmap] watching for changes (Ctrl+C to stop)…');
|
|
3608
|
+
let debounce = null;
|
|
3609
|
+
for (const srcDir of config.srcDirs) {
|
|
3610
|
+
const abs = path.join(cwd, srcDir);
|
|
3611
|
+
if (!fs.existsSync(abs)) continue;
|
|
3612
|
+
fs.watch(abs, { recursive: true }, () => {
|
|
3613
|
+
if (debounce) clearTimeout(debounce);
|
|
3614
|
+
debounce = setTimeout(() => {
|
|
3615
|
+
console.warn('[sigmap] change detected, regenerating…');
|
|
3616
|
+
runGenerate(cwd, config, false);
|
|
3617
|
+
}, config.watchDebounce || 300);
|
|
3618
|
+
});
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
|
|
3622
|
+
// ---------------------------------------------------------------------------
|
|
3623
|
+
// Git hook installer
|
|
3624
|
+
// ---------------------------------------------------------------------------
|
|
3625
|
+
function installHook(cwd, scriptPath) {
|
|
3626
|
+
const hookDir = path.join(cwd, '.git', 'hooks');
|
|
3627
|
+
if (!fs.existsSync(hookDir)) {
|
|
3628
|
+
console.warn('[sigmap] .git/hooks not found — skipping hook install');
|
|
3629
|
+
return;
|
|
3630
|
+
}
|
|
3631
|
+
const hookPath = path.join(hookDir, 'post-commit');
|
|
3632
|
+
const resolvedScript = path.resolve(scriptPath);
|
|
3633
|
+
const hookLine = `\nnode ${JSON.stringify(resolvedScript)} --generate 2>/dev/null || true\n`;
|
|
3634
|
+
|
|
3635
|
+
if (fs.existsSync(hookPath)) {
|
|
3636
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
3637
|
+
const existingHookLines = existing.split('\n').filter((line) => line.includes('gen-context.js'));
|
|
3638
|
+
if (existing.includes(hookLine.trim()) && existingHookLines.length === 1) {
|
|
3639
|
+
console.warn('[sigmap] post-commit hook already installed');
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
if (existing.includes('gen-context.js')) {
|
|
3643
|
+
const updated = existing
|
|
3644
|
+
.split('\n')
|
|
3645
|
+
.filter((line) => !line.includes('gen-context.js'))
|
|
3646
|
+
.join('\n')
|
|
3647
|
+
.replace(/\n+$/g, '\n');
|
|
3648
|
+
fs.writeFileSync(hookPath, updated.replace(/\n?$/g, '') + hookLine);
|
|
3649
|
+
console.warn('[sigmap] updated post-commit hook');
|
|
3650
|
+
return;
|
|
3651
|
+
}
|
|
3652
|
+
fs.appendFileSync(hookPath, hookLine);
|
|
3653
|
+
} else {
|
|
3654
|
+
fs.writeFileSync(hookPath, `#!/bin/sh${hookLine}`);
|
|
3655
|
+
fs.chmodSync(hookPath, '755');
|
|
3656
|
+
}
|
|
3657
|
+
console.warn('[sigmap] installed post-commit hook');
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3660
|
+
// ---------------------------------------------------------------------------
|
|
3661
|
+
// Example config writer
|
|
3662
|
+
// ---------------------------------------------------------------------------
|
|
3663
|
+
function writeInitConfig(cwd) {
|
|
3664
|
+
const dest = path.join(cwd, 'gen-context.config.json');
|
|
3665
|
+
if (fs.existsSync(dest)) {
|
|
3666
|
+
console.warn('[sigmap] gen-context.config.json already exists — skipping');
|
|
3667
|
+
} else {
|
|
3668
|
+
const example = path.join(__dirname, 'gen-context.config.json.example');
|
|
3669
|
+
if (fs.existsSync(example)) {
|
|
3670
|
+
fs.copyFileSync(example, dest);
|
|
3671
|
+
} else {
|
|
3672
|
+
fs.writeFileSync(dest, JSON.stringify(DEFAULTS, null, 2) + '\n');
|
|
3673
|
+
}
|
|
3674
|
+
console.warn('[sigmap] wrote gen-context.config.json');
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// Also scaffold .contextignore if it does not already exist
|
|
3678
|
+
const ignoreDest = path.join(cwd, '.contextignore');
|
|
3679
|
+
if (fs.existsSync(ignoreDest)) {
|
|
3680
|
+
console.warn('[sigmap] .contextignore already exists — skipping');
|
|
3681
|
+
} else {
|
|
3682
|
+
const ignoreContent = [
|
|
3683
|
+
'# SigMap ignore file — gitignore syntax',
|
|
3684
|
+
'# Files and directories listed here are excluded from signature extraction.',
|
|
3685
|
+
'# This file is union-merged with .repomixignore if present.',
|
|
3686
|
+
'',
|
|
3687
|
+
'node_modules/',
|
|
3688
|
+
'dist/',
|
|
3689
|
+
'build/',
|
|
3690
|
+
'out/',
|
|
3691
|
+
'.next/',
|
|
3692
|
+
'coverage/',
|
|
3693
|
+
'target/',
|
|
3694
|
+
'vendor/',
|
|
3695
|
+
'*.generated.*',
|
|
3696
|
+
'*.pb.*',
|
|
3697
|
+
'*_pb.*',
|
|
3698
|
+
'*.min.js',
|
|
3699
|
+
'*.min.css',
|
|
3700
|
+
].join('\n') + '\n';
|
|
3701
|
+
fs.writeFileSync(ignoreDest, ignoreContent);
|
|
3702
|
+
console.warn('[sigmap] wrote .contextignore');
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
|
|
3706
|
+
// ---------------------------------------------------------------------------
|
|
3707
|
+
// Core generate pipeline
|
|
3708
|
+
// ---------------------------------------------------------------------------
|
|
3709
|
+
// ---------------------------------------------------------------------------
|
|
3710
|
+
// Strategy: per-module — one output file per top-level srcDir
|
|
3711
|
+
// ---------------------------------------------------------------------------
|
|
3712
|
+
function runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal) {
|
|
3713
|
+
// Group entries by their top-level srcDir
|
|
3714
|
+
const modules = {};
|
|
3715
|
+
for (const entry of fileEntries) {
|
|
3716
|
+
const rel = path.relative(cwd, entry.filePath);
|
|
3717
|
+
const topDir = rel.split(path.sep)[0];
|
|
3718
|
+
if (!modules[topDir]) modules[topDir] = [];
|
|
3719
|
+
modules[topDir].push(entry);
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
const moduleNames = Object.keys(modules).sort();
|
|
3723
|
+
console.warn(`[sigmap] per-module: ${moduleNames.length} modules — ${moduleNames.join(', ')}`);
|
|
3724
|
+
|
|
3725
|
+
// Thin overview → primary output (always-on, tiny)
|
|
3726
|
+
const overviewLines = [
|
|
3727
|
+
'<!-- Generated by SigMap gen-context.js v' + VERSION + ' — per-module overview -->',
|
|
3728
|
+
'<!-- Load the full module context file for detailed signatures -->',
|
|
3729
|
+
'',
|
|
3730
|
+
'# Codebase overview',
|
|
3731
|
+
'',
|
|
3732
|
+
'| Module | Full context file |',
|
|
3733
|
+
'|--------|-------------------|',
|
|
3734
|
+
];
|
|
3735
|
+
|
|
3736
|
+
let totalOut = 0;
|
|
3737
|
+
for (const mod of moduleNames) {
|
|
3738
|
+
const outName = `context-${mod}.md`;
|
|
3739
|
+
const outPath = path.join(cwd, '.github', outName);
|
|
3740
|
+
const modEntries = modules[mod];
|
|
3741
|
+
|
|
3742
|
+
// Per-module budget: proportional share of maxTokens
|
|
3743
|
+
const modBudget = Math.max(1000, Math.floor(config.maxTokens / moduleNames.length));
|
|
3744
|
+
const budgeted = applyTokenBudget(modEntries, modBudget);
|
|
3745
|
+
|
|
3746
|
+
const content = formatOutput(budgeted, cwd, false);
|
|
3747
|
+
ensureDir(outPath);
|
|
3748
|
+
fs.writeFileSync(outPath, content, 'utf8');
|
|
3749
|
+
const modTokens = estimateTokens(content);
|
|
3750
|
+
totalOut += modTokens;
|
|
3751
|
+
console.warn(`[sigmap] per-module: wrote .github/${outName} (~${modTokens} tokens, ${budgeted.length} files)`);
|
|
3752
|
+
|
|
3753
|
+
overviewLines.push(`| \`${mod}\` | \`.github/${outName}\` |`);
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
overviewLines.push('');
|
|
3757
|
+
overviewLines.push('> Inject the relevant module file into your IDE context window.');
|
|
3758
|
+
overviewLines.push('> For cross-module questions load both files.');
|
|
3759
|
+
const overviewContent = overviewLines.join('\n') + '\n';
|
|
3760
|
+
const primaryTargets = (config.outputs || ['copilot']).filter((t) => t !== 'claude');
|
|
3761
|
+
writeOutputs(overviewContent, primaryTargets, cwd);
|
|
3762
|
+
|
|
3763
|
+
const overviewTokens = estimateTokens(overviewContent);
|
|
3764
|
+
console.warn(`[sigmap] per-module: overview ~${overviewTokens} tokens (always-on), modules total ~${totalOut} tokens (on-demand)`);
|
|
3765
|
+
return { inputTokenTotal, finalTokens: overviewTokens, fileCount: fileEntries.length, droppedCount: 0 };
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
// ---------------------------------------------------------------------------
|
|
3769
|
+
// Strategy: hot/cold — hot injected always, cold available via MCP only
|
|
3770
|
+
// ---------------------------------------------------------------------------
|
|
3771
|
+
function runHotColdStrategy(cwd, config, fileEntries, recentFiles, inputTokenTotal) {
|
|
3772
|
+
const hotEntries = fileEntries.filter((e) => recentFiles.has(e.filePath));
|
|
3773
|
+
const coldEntries = fileEntries.filter((e) => !recentFiles.has(e.filePath));
|
|
3774
|
+
|
|
3775
|
+
// Hot → primary output (auto-injected by IDE)
|
|
3776
|
+
const hotContent = hotEntries.length > 0
|
|
3777
|
+
? formatOutput(hotEntries, cwd, false)
|
|
3778
|
+
: '<!-- Generated by SigMap — no recently changed files -->\n';
|
|
3779
|
+
const primaryTargets = (config.outputs || ['copilot']).filter((t) => t !== 'claude');
|
|
3780
|
+
writeOutputs(hotContent, primaryTargets, cwd);
|
|
3781
|
+
const hotTokens = estimateTokens(hotContent);
|
|
3782
|
+
|
|
3783
|
+
// Cold → .github/context-cold.md (MCP reads this on demand)
|
|
3784
|
+
const coldPath = path.join(cwd, '.github', 'context-cold.md');
|
|
3785
|
+
const coldHeader = [
|
|
3786
|
+
'<!-- Generated by SigMap gen-context.js v' + VERSION + ' — cold context (MCP only) -->',
|
|
3787
|
+
'<!-- NOT auto-injected. Retrieve via MCP: read_context({ module: "..." }) -->',
|
|
3788
|
+
'',
|
|
3789
|
+
].join('\n');
|
|
3790
|
+
const coldContent = coldHeader + formatOutput(coldEntries, cwd, false);
|
|
3791
|
+
ensureDir(coldPath);
|
|
3792
|
+
fs.writeFileSync(coldPath, coldContent, 'utf8');
|
|
3793
|
+
const coldTokens = estimateTokens(coldContent);
|
|
3794
|
+
|
|
3795
|
+
console.warn('[sigmap] hot-cold:');
|
|
3796
|
+
console.warn(` hot (auto-injected) : ${hotEntries.length} files ~${hotTokens} tokens → primary output`);
|
|
3797
|
+
console.warn(` cold (MCP on-demand) : ${coldEntries.length} files ~${coldTokens} tokens → .github/context-cold.md`);
|
|
3798
|
+
if (coldEntries.length > 0) {
|
|
3799
|
+
console.warn(' retrieve cold: read_context({ module: "<dir>" }) via MCP');
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
return { inputTokenTotal, finalTokens: hotTokens, fileCount: fileEntries.length, droppedCount: 0 };
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
// ---------------------------------------------------------------------------
|
|
3806
|
+
// Diff-mode pipeline — context for changed files only
|
|
3807
|
+
// ---------------------------------------------------------------------------
|
|
3808
|
+
function runDiff(cwd, config, stagedOnly) {
|
|
3809
|
+
const diffFiles = getDiffFiles(cwd, stagedOnly);
|
|
3810
|
+
|
|
3811
|
+
if (diffFiles.size === 0) {
|
|
3812
|
+
const scope = stagedOnly ? 'staged' : 'working tree';
|
|
3813
|
+
console.warn(`[sigmap] --diff: no changed files found in ${scope} — running full generate`);
|
|
3814
|
+
runGenerate(cwd, config, false);
|
|
3815
|
+
return;
|
|
3816
|
+
}
|
|
3817
|
+
|
|
3818
|
+
const ignorePatterns = loadIgnorePatterns(cwd);
|
|
3819
|
+
let allFiles = buildFileList(cwd, config);
|
|
3820
|
+
allFiles = allFiles.filter((f) => {
|
|
3821
|
+
const rel = path.relative(cwd, f).replace(/\\/g, '/');
|
|
3822
|
+
return !matchesIgnore(rel, ignorePatterns);
|
|
3823
|
+
});
|
|
3824
|
+
|
|
3825
|
+
// Restrict to diff files, keeping only those already in srcDirs
|
|
3826
|
+
const diffFiltered = allFiles.filter((f) => diffFiles.has(f));
|
|
3827
|
+
|
|
3828
|
+
if (diffFiltered.length === 0) {
|
|
3829
|
+
console.warn('[sigmap] --diff: changed files are outside tracked srcDirs — running full generate');
|
|
3830
|
+
runGenerate(cwd, config, false);
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
|
|
3834
|
+
let inputTokenTotal = 0;
|
|
3835
|
+
let fileEntries = [];
|
|
3836
|
+
|
|
3837
|
+
for (const filePath of diffFiltered) {
|
|
3838
|
+
let content = '';
|
|
3839
|
+
try {
|
|
3840
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
3841
|
+
} catch (_) {
|
|
3842
|
+
continue;
|
|
3843
|
+
}
|
|
3844
|
+
|
|
3845
|
+
let sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
|
|
3846
|
+
if (sigs.length === 0) continue;
|
|
3847
|
+
|
|
3848
|
+
inputTokenTotal += estimateTokens(content);
|
|
3849
|
+
|
|
3850
|
+
if (config.secretScan) {
|
|
3851
|
+
const { scan } = __require('./src/security/scanner');
|
|
3852
|
+
const result = scan(sigs, filePath);
|
|
3853
|
+
if (result.redacted) {
|
|
3854
|
+
console.warn(`[sigmap] secrets redacted in ${path.relative(cwd, filePath)}`);
|
|
3855
|
+
}
|
|
3856
|
+
sigs = result.safe;
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
fileEntries.push({ filePath, sigs, mtime: Date.now() });
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
if (fileEntries.length === 0) {
|
|
3863
|
+
console.warn('[sigmap] --diff: no extractable signatures in changed files — running full generate');
|
|
3864
|
+
runGenerate(cwd, config, false);
|
|
3865
|
+
return;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
const routingEnabled = !!(config.routing || process.argv.includes('--routing'));
|
|
3869
|
+
const content = formatOutput(fileEntries, cwd, routingEnabled);
|
|
3870
|
+
const finalTokens = estimateTokens(content);
|
|
3871
|
+
writeOutputs(content, config.outputs, cwd);
|
|
3872
|
+
|
|
3873
|
+
const scope = stagedOnly ? 'staged' : 'diff';
|
|
3874
|
+
console.warn(`[sigmap] ${scope} files: ${fileEntries.length}, diff tokens: ~${finalTokens}`);
|
|
3875
|
+
|
|
3876
|
+
if (process.argv.includes('--report')) {
|
|
3877
|
+
// Also show what the full run would cost for comparison
|
|
3878
|
+
const fullResult = runGenerate(cwd, config, true);
|
|
3879
|
+
console.log(`[sigmap] diff report:`);
|
|
3880
|
+
console.log(` mode : ${scope}`);
|
|
3881
|
+
console.log(` diff files : ${fileEntries.length}`);
|
|
3882
|
+
console.log(` diff tokens : ~${finalTokens}`);
|
|
3883
|
+
console.log(` full tokens : ~${fullResult.finalTokens}`);
|
|
3884
|
+
console.log(` savings : ~${fullResult.finalTokens - finalTokens} tokens`);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// ---------------------------------------------------------------------------
|
|
3889
|
+
// Core generate pipeline
|
|
3890
|
+
// ---------------------------------------------------------------------------
|
|
3891
|
+
function runGenerate(cwd, config, reportMode, reportJson = false) {
|
|
3892
|
+
const ignorePatterns = loadIgnorePatterns(cwd);
|
|
3893
|
+
let allFiles = buildFileList(cwd, config);
|
|
3894
|
+
|
|
3895
|
+
// Apply .contextignore
|
|
3896
|
+
allFiles = allFiles.filter((f) => {
|
|
3897
|
+
const rel = path.relative(cwd, f).replace(/\\/g, '/');
|
|
3898
|
+
return !matchesIgnore(rel, ignorePatterns);
|
|
3899
|
+
});
|
|
3900
|
+
|
|
3901
|
+
// Gather mtime and git-committed info
|
|
3902
|
+
const hotCommits = config.hotCommits || 10;
|
|
3903
|
+
const recentFiles = config.diffPriority ? getRecentlyCommittedFiles(cwd, hotCommits) : new Set();
|
|
3904
|
+
|
|
3905
|
+
let inputTokenTotal = 0;
|
|
3906
|
+
let fileEntries = [];
|
|
3907
|
+
|
|
3908
|
+
for (const filePath of allFiles) {
|
|
3909
|
+
let content = '';
|
|
3910
|
+
try {
|
|
3911
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
3912
|
+
} catch (_) {
|
|
3913
|
+
continue;
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
let sigs = detectAndExtract(filePath, content, config.maxSigsPerFile);
|
|
3917
|
+
if (sigs.length === 0) continue;
|
|
3918
|
+
|
|
3919
|
+
// Baseline = estimated tokens of original source content for intuitive reduction stats.
|
|
3920
|
+
inputTokenTotal += estimateTokens(content);
|
|
3921
|
+
|
|
3922
|
+
if (config.secretScan) {
|
|
3923
|
+
const { scan } = __require('./src/security/scanner');
|
|
3924
|
+
const result = scan(sigs, filePath);
|
|
3925
|
+
if (result.redacted) {
|
|
3926
|
+
console.warn(`[sigmap] secrets redacted in ${path.relative(cwd, filePath)}`);
|
|
3927
|
+
}
|
|
3928
|
+
sigs = result.safe;
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
let mtime = 0;
|
|
3932
|
+
try {
|
|
3933
|
+
mtime = fs.statSync(filePath).mtimeMs;
|
|
3934
|
+
} catch (_) {}
|
|
3935
|
+
|
|
3936
|
+
// Boost recently committed files (give them max mtime so they aren't dropped first)
|
|
3937
|
+
if (recentFiles.has(filePath)) mtime = Date.now();
|
|
3938
|
+
|
|
3939
|
+
fileEntries.push({ filePath, sigs, mtime });
|
|
3940
|
+
}
|
|
3941
|
+
|
|
3942
|
+
const strategy = config.strategy || 'full';
|
|
3943
|
+
const beforeCount = fileEntries.length;
|
|
3944
|
+
|
|
3945
|
+
// Sort for output ordering: recently committed files appear first
|
|
3946
|
+
if (config.diffPriority && recentFiles.size > 0) {
|
|
3947
|
+
fileEntries.sort((a, b) => {
|
|
3948
|
+
const aRecent = recentFiles.has(a.filePath) ? 0 : 1;
|
|
3949
|
+
const bRecent = recentFiles.has(b.filePath) ? 0 : 1;
|
|
3950
|
+
return aRecent - bRecent;
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3953
|
+
|
|
3954
|
+
let result;
|
|
3955
|
+
if (!reportMode) {
|
|
3956
|
+
if (strategy === 'per-module') {
|
|
3957
|
+
result = runPerModuleStrategy(cwd, config, fileEntries, inputTokenTotal);
|
|
3958
|
+
} else if (strategy === 'hot-cold') {
|
|
3959
|
+
result = runHotColdStrategy(cwd, config, fileEntries, recentFiles, inputTokenTotal);
|
|
3960
|
+
} else {
|
|
3961
|
+
// 'full' — original behaviour
|
|
3962
|
+
fileEntries = applyTokenBudget(fileEntries, config.maxTokens);
|
|
3963
|
+
const droppedCount = beforeCount - fileEntries.length;
|
|
3964
|
+
const routingEnabled = !!(config.routing || process.argv.includes('--routing'));
|
|
3965
|
+
const content = formatOutput(fileEntries, cwd, routingEnabled);
|
|
3966
|
+
const finalTokens = estimateTokens(content);
|
|
3967
|
+
const formatIdx = process.argv.indexOf('--format');
|
|
3968
|
+
const formatValue = formatIdx >= 0 ? process.argv[formatIdx + 1] : (config.format || 'default');
|
|
3969
|
+
writeOutputs(content, config.outputs, cwd);
|
|
3970
|
+
if (formatValue === 'cache') writeCacheOutput(content, cwd);
|
|
3971
|
+
result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount };
|
|
3972
|
+
}
|
|
3973
|
+
} else {
|
|
3974
|
+
// report mode: always run full pipeline for accurate stats
|
|
3975
|
+
const budgeted = applyTokenBudget([...fileEntries], config.maxTokens);
|
|
3976
|
+
const droppedCount = beforeCount - budgeted.length;
|
|
3977
|
+
const content = formatOutput(budgeted, cwd, false);
|
|
3978
|
+
const finalTokens = estimateTokens(content);
|
|
3979
|
+
result = { inputTokenTotal, finalTokens, fileCount: beforeCount, droppedCount };
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
if (reportMode || process.argv.includes('--report')) {
|
|
3983
|
+
printReport(result.inputTokenTotal, result.finalTokens, result.fileCount, result.droppedCount, reportJson, config.maxTokens);
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
// Usage tracking (v0.9) — optional append-only NDJSON log
|
|
3987
|
+
const trackingEnabled = !!(config.tracking || process.argv.includes('--track'));
|
|
3988
|
+
if (trackingEnabled && !reportMode) {
|
|
3989
|
+
try {
|
|
3990
|
+
const { logRun } = __require('./src/tracking/logger');
|
|
3991
|
+
logRun({
|
|
3992
|
+
version: VERSION,
|
|
3993
|
+
fileCount: result.fileCount,
|
|
3994
|
+
droppedCount: result.droppedCount,
|
|
3995
|
+
rawTokens: result.inputTokenTotal,
|
|
3996
|
+
finalTokens: result.finalTokens,
|
|
3997
|
+
overBudget: result.finalTokens > config.maxTokens,
|
|
3998
|
+
budgetLimit: config.maxTokens,
|
|
3999
|
+
}, cwd);
|
|
4000
|
+
} catch (err) {
|
|
4001
|
+
console.warn(`[sigmap] tracking: ${err.message}`);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
return result;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
// ---------------------------------------------------------------------------
|
|
4009
|
+
// Monorepo support
|
|
4010
|
+
// ---------------------------------------------------------------------------
|
|
4011
|
+
const MONO_ROOTS = ['packages', 'apps', 'services', 'libs'];
|
|
4012
|
+
const PKG_MANIFESTS = ['package.json', 'pyproject.toml', 'Cargo.toml', 'go.mod', 'build.gradle', 'pom.xml'];
|
|
4013
|
+
|
|
4014
|
+
function detectMonorepoPackages(cwd) {
|
|
4015
|
+
const packages = [];
|
|
4016
|
+
for (const monoDir of MONO_ROOTS) {
|
|
4017
|
+
const abs = path.join(cwd, monoDir);
|
|
4018
|
+
if (!fs.existsSync(abs)) continue;
|
|
4019
|
+
let entries;
|
|
4020
|
+
try { entries = fs.readdirSync(abs, { withFileTypes: true }); } catch (_) { continue; }
|
|
4021
|
+
for (const entry of entries) {
|
|
4022
|
+
if (!entry.isDirectory()) continue;
|
|
4023
|
+
const pkgPath = path.join(abs, entry.name);
|
|
4024
|
+
for (const manifest of PKG_MANIFESTS) {
|
|
4025
|
+
if (fs.existsSync(path.join(pkgPath, manifest))) {
|
|
4026
|
+
packages.push(pkgPath);
|
|
4027
|
+
break;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
return packages;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
function runMonorepo(cwd, config) {
|
|
4036
|
+
const packages = detectMonorepoPackages(cwd);
|
|
4037
|
+
if (packages.length === 0) {
|
|
4038
|
+
console.warn('[sigmap] no monorepo packages found — checked packages/, apps/, services/, libs/');
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
console.warn(`[sigmap] monorepo: found ${packages.length} packages`);
|
|
4042
|
+
|
|
4043
|
+
for (const pkgPath of packages) {
|
|
4044
|
+
const pkgName = path.relative(cwd, pkgPath);
|
|
4045
|
+
console.warn(`[sigmap] monorepo: processing ${pkgName}`);
|
|
4046
|
+
// Per-package config: scan src/ and package root, write CLAUDE.md per package
|
|
4047
|
+
const pkgConfig = {
|
|
4048
|
+
...config,
|
|
4049
|
+
srcDirs: ['src', 'lib', 'app', '.'],
|
|
4050
|
+
outputs: ['claude'],
|
|
4051
|
+
};
|
|
4052
|
+
try {
|
|
4053
|
+
runGenerate(pkgPath, pkgConfig, false);
|
|
4054
|
+
} catch (err) {
|
|
4055
|
+
console.warn(`[sigmap] monorepo: failed for ${pkgName}: ${err.message}`);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
console.warn(`[sigmap] monorepo: wrote CLAUDE.md for ${packages.length} packages`);
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
// ---------------------------------------------------------------------------
|
|
4062
|
+
// CLI entry point
|
|
4063
|
+
// ---------------------------------------------------------------------------
|
|
4064
|
+
/**
|
|
4065
|
+
* Classify a task description and return the recommended model tier.
|
|
4066
|
+
*
|
|
4067
|
+
* Rules (ordered):
|
|
4068
|
+
* 1. Match any 'powerful' keyword → 'powerful'
|
|
4069
|
+
* 2. Match any 'fast' keyword → 'fast'
|
|
4070
|
+
* 3. Default → 'balanced'
|
|
4071
|
+
*
|
|
4072
|
+
* @param {string} description - Natural-language task description
|
|
4073
|
+
* @returns {{ tier: string, label: string, models: string, costHint: string }}
|
|
4074
|
+
*/
|
|
4075
|
+
function suggestTool(description) {
|
|
4076
|
+
const lower = description.toLowerCase();
|
|
4077
|
+
const { TIERS } = __require('./src/routing/hints');
|
|
4078
|
+
|
|
4079
|
+
const powerfulKeywords = [
|
|
4080
|
+
'architecture', 'cross-cutting', 'multi-file', 'security audit', 'owasp',
|
|
4081
|
+
'migration plan', 'framework upgrade', 'complex debug', 'async boundar',
|
|
4082
|
+
'multi-service', 'distributed system', 'performance audit', 'new module from',
|
|
4083
|
+
'redesign', 'from requirements',
|
|
4084
|
+
];
|
|
4085
|
+
const fastKeywords = [
|
|
4086
|
+
'typo', 'rename symbol', 'format', 'lint', 'style change', 'trivial',
|
|
4087
|
+
'quick fix', 'inline suggest', 'autocomplete', 'config file',
|
|
4088
|
+
'dockerfile', 'shell script', '.yaml', '.yml', '.json', '.css',
|
|
4089
|
+
'html template', 'markup',
|
|
4090
|
+
];
|
|
4091
|
+
|
|
4092
|
+
let tier = 'balanced';
|
|
4093
|
+
if (powerfulKeywords.some((kw) => lower.includes(kw))) tier = 'powerful';
|
|
4094
|
+
else if (fastKeywords.some((kw) => lower.includes(kw))) tier = 'fast';
|
|
4095
|
+
|
|
4096
|
+
const info = TIERS[tier];
|
|
4097
|
+
return { tier, label: info.label, models: info.examples, costHint: info.costHint };
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
function resolveProjectRoot(startDir) {
|
|
4101
|
+
try {
|
|
4102
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', {
|
|
4103
|
+
cwd: startDir,
|
|
4104
|
+
encoding: 'utf8',
|
|
4105
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
4106
|
+
}).trim();
|
|
4107
|
+
if (gitRoot) return gitRoot;
|
|
4108
|
+
} catch (_) {}
|
|
4109
|
+
return startDir;
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
function printHelp() {
|
|
4113
|
+
console.log(`
|
|
4114
|
+
SigMap — gen-context.js v${VERSION}
|
|
4115
|
+
Zero-dependency AI context engine
|
|
4116
|
+
|
|
4117
|
+
Usage:
|
|
4118
|
+
node gen-context.js Generate context once and exit
|
|
4119
|
+
node gen-context.js --monorepo Generate per-package context (monorepo)
|
|
4120
|
+
node gen-context.js --routing Include model routing hints in output
|
|
4121
|
+
node gen-context.js --format cache Also write Anthropic prompt-cache JSON
|
|
4122
|
+
node gen-context.js --track Append run metrics to .context/usage.ndjson
|
|
4123
|
+
node gen-context.js --watch Generate + watch for file changes
|
|
4124
|
+
node gen-context.js --setup Generate + install git hook + watch
|
|
4125
|
+
node gen-context.js --mcp Start MCP server on stdio
|
|
4126
|
+
node gen-context.js --report Token reduction stats to stdout
|
|
4127
|
+
node gen-context.js --report --json Token report as JSON (for CI; exits 1 if over budget)
|
|
4128
|
+
node gen-context.js --report --history Print usage log summary from .context/usage.ndjson
|
|
4129
|
+
node gen-context.js --suggest-tool "<task>" Recommend model tier for a task description
|
|
4130
|
+
node gen-context.js --suggest-tool "<task>" --json Machine-readable tier recommendation
|
|
4131
|
+
node gen-context.js --health Print composite health score
|
|
4132
|
+
node gen-context.js --health --json Machine-readable health score
|
|
4133
|
+
node gen-context.js --diff Generate context for git-changed files only
|
|
4134
|
+
node gen-context.js --diff --staged Generate context for staged files only
|
|
4135
|
+
node gen-context.js --init Write example config + .contextignore scaffold
|
|
4136
|
+
node gen-context.js --help Show this message
|
|
4137
|
+
node gen-context.js --version Show version
|
|
4138
|
+
|
|
4139
|
+
Strategies (set via config "strategy" key):
|
|
4140
|
+
"full" Single file, all signatures. Works everywhere. (default)
|
|
4141
|
+
"per-module" One .github/context-<module>.md per srcDir + thin overview.
|
|
4142
|
+
~70% fewer tokens per question. No MCP needed.
|
|
4143
|
+
"hot-cold" Hot (recently changed) auto-injected; cold in .github/context-cold.md
|
|
4144
|
+
~90% fewer tokens. Best with MCP (Claude Code, Cursor).
|
|
4145
|
+
Set "hotCommits": N to control how many commits count as hot (default 10).
|
|
4146
|
+
|
|
4147
|
+
Config: gen-context.config.json
|
|
4148
|
+
Ignore: .contextignore, .repomixignore
|
|
4149
|
+
Output: .github/copilot-instructions.md (default)
|
|
4150
|
+
`);
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
// ---------------------------------------------------------------------------
|
|
4154
|
+
// MCP auto-registration
|
|
4155
|
+
// ---------------------------------------------------------------------------
|
|
4156
|
+
function registerMcp(cwd, scriptPath) {
|
|
4157
|
+
const serverEntry = {
|
|
4158
|
+
command: 'node',
|
|
4159
|
+
args: [path.resolve(scriptPath), '--mcp'],
|
|
4160
|
+
};
|
|
4161
|
+
|
|
4162
|
+
const targets = [
|
|
4163
|
+
path.join(cwd, '.claude', 'settings.json'),
|
|
4164
|
+
path.join(cwd, '.cursor', 'mcp.json'),
|
|
4165
|
+
];
|
|
4166
|
+
|
|
4167
|
+
for (const settingsPath of targets) {
|
|
4168
|
+
if (!fs.existsSync(settingsPath)) continue;
|
|
4169
|
+
try {
|
|
4170
|
+
const raw = fs.readFileSync(settingsPath, 'utf8');
|
|
4171
|
+
const settings = JSON.parse(raw);
|
|
4172
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
4173
|
+
if (settings.mcpServers['sigmap']) continue; // already registered
|
|
4174
|
+
settings.mcpServers['sigmap'] = serverEntry;
|
|
4175
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
4176
|
+
console.warn(`[sigmap] registered MCP server in ${path.relative(cwd, settingsPath)}`);
|
|
4177
|
+
} catch (err) {
|
|
4178
|
+
console.warn(`[sigmap] could not update ${path.relative(cwd, settingsPath)}: ${err.message}`);
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
|
|
4182
|
+
// Always print the manual snippet so users can configure other tools
|
|
4183
|
+
console.warn('[sigmap] MCP server config snippet:');
|
|
4184
|
+
console.warn(JSON.stringify({ mcpServers: { 'sigmap': serverEntry } }, null, 2));
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
function main() {
|
|
4188
|
+
const args = process.argv.slice(2);
|
|
4189
|
+
const invokedFrom = process.cwd();
|
|
4190
|
+
const cwd = resolveProjectRoot(invokedFrom);
|
|
4191
|
+
const scriptPath = process.argv[1] || path.join(invokedFrom, 'gen-context.js');
|
|
4192
|
+
|
|
4193
|
+
if (cwd !== invokedFrom) {
|
|
4194
|
+
console.warn(`[sigmap] using project root: ${cwd}`);
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
4198
|
+
printHelp();
|
|
4199
|
+
process.exit(0);
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
if (args.includes('--version') || args.includes('-v')) {
|
|
4203
|
+
console.log(VERSION);
|
|
4204
|
+
process.exit(0);
|
|
4205
|
+
}
|
|
4206
|
+
|
|
4207
|
+
// MCP server — start before loading config (reads files on demand)
|
|
4208
|
+
if (args.includes('--mcp')) {
|
|
4209
|
+
const { start } = __require('./src/mcp/server');
|
|
4210
|
+
start(cwd);
|
|
4211
|
+
return; // keep process alive — server drives lifecycle via stdin
|
|
4212
|
+
}
|
|
4213
|
+
|
|
4214
|
+
const config = loadConfig(cwd);
|
|
4215
|
+
|
|
4216
|
+
if (args.includes('--init')) {
|
|
4217
|
+
writeInitConfig(cwd);
|
|
4218
|
+
process.exit(0);
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
if (args.includes('--health')) {
|
|
4222
|
+
const { score } = __require('./src/health/scorer');
|
|
4223
|
+
const result = score(cwd);
|
|
4224
|
+
if (args.includes('--json')) {
|
|
4225
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
4226
|
+
} else {
|
|
4227
|
+
console.log('[sigmap] health:');
|
|
4228
|
+
console.log(` score : ${result.score}/100 (grade ${result.grade})`);
|
|
4229
|
+
console.log(` strategy : ${result.strategy}`);
|
|
4230
|
+
console.log(` token reduction : ${result.tokenReductionPct !== null ? result.tokenReductionPct + '%' : 'no history'}`);
|
|
4231
|
+
console.log(` days since regen: ${result.daysSinceRegen !== null ? result.daysSinceRegen : 'context file not found'}`);
|
|
4232
|
+
if (result.strategyFreshnessDays !== null) {
|
|
4233
|
+
console.log(` cold freshness : ${result.strategyFreshnessDays} day(s)`);
|
|
4234
|
+
}
|
|
4235
|
+
console.log(` total runs : ${result.totalRuns}`);
|
|
4236
|
+
console.log(` over-budget runs: ${result.overBudgetRuns}`);
|
|
4237
|
+
}
|
|
4238
|
+
process.exit(0);
|
|
4239
|
+
}
|
|
4240
|
+
|
|
4241
|
+
if (args.includes('--suggest-tool')) {
|
|
4242
|
+
const idx = args.indexOf('--suggest-tool');
|
|
4243
|
+
const taskDesc = (args[idx + 1] || '').trim();
|
|
4244
|
+
if (!taskDesc || taskDesc.startsWith('--')) {
|
|
4245
|
+
console.error('[sigmap] --suggest-tool requires a task description');
|
|
4246
|
+
console.error(' Example: node gen-context.js --suggest-tool "refactor the auth module"');
|
|
4247
|
+
process.exit(1);
|
|
4248
|
+
}
|
|
4249
|
+
const result = suggestTool(taskDesc);
|
|
4250
|
+
if (args.includes('--json')) {
|
|
4251
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
4252
|
+
} else {
|
|
4253
|
+
console.log('[sigmap] suggest-tool:');
|
|
4254
|
+
console.log(` tier : ${result.tier}`);
|
|
4255
|
+
console.log(` label : ${result.label}`);
|
|
4256
|
+
console.log(` models : ${result.models}`);
|
|
4257
|
+
console.log(` cost : ${result.costHint}`);
|
|
4258
|
+
}
|
|
4259
|
+
process.exit(0);
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
if (args.includes('--report')) {
|
|
4263
|
+
if (args.includes('--history')) {
|
|
4264
|
+
try {
|
|
4265
|
+
const { readLog, summarize } = __require('./src/tracking/logger');
|
|
4266
|
+
const entries = readLog(cwd);
|
|
4267
|
+
const summary = summarize(entries);
|
|
4268
|
+
if (args.includes('--json')) {
|
|
4269
|
+
process.stdout.write(JSON.stringify(summary) + '\n');
|
|
4270
|
+
} else {
|
|
4271
|
+
console.log('[sigmap] usage history:');
|
|
4272
|
+
console.log(` total runs : ${summary.totalRuns}`);
|
|
4273
|
+
console.log(` avg reduction : ${summary.avgReductionPct}%`);
|
|
4274
|
+
console.log(` avg tokens out : ~${summary.avgFinalTokens}`);
|
|
4275
|
+
console.log(` over-budget runs: ${summary.overBudgetRuns}`);
|
|
4276
|
+
if (summary.firstRun) console.log(` first run : ${summary.firstRun}`);
|
|
4277
|
+
if (summary.lastRun) console.log(` last run : ${summary.lastRun}`);
|
|
4278
|
+
}
|
|
4279
|
+
} catch (err) {
|
|
4280
|
+
console.warn(`[sigmap] tracking: ${err.message}`);
|
|
4281
|
+
}
|
|
4282
|
+
process.exit(0);
|
|
4283
|
+
}
|
|
4284
|
+
runGenerate(cwd, config, true, args.includes('--json'));
|
|
4285
|
+
process.exit(0);
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
if (args.includes('--monorepo') || config.monorepo) {
|
|
4289
|
+
runMonorepo(cwd, config);
|
|
4290
|
+
process.exit(0);
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
if (args.includes('--setup')) {
|
|
4294
|
+
runGenerate(cwd, config, false);
|
|
4295
|
+
installHook(cwd, scriptPath);
|
|
4296
|
+
registerMcp(cwd, scriptPath);
|
|
4297
|
+
watchMode(cwd, config);
|
|
4298
|
+
return; // keep process alive for watch
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
if (args.includes('--watch')) {
|
|
4302
|
+
runGenerate(cwd, config, false);
|
|
4303
|
+
watchMode(cwd, config);
|
|
4304
|
+
return; // keep process alive
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
if (args.includes('--diff')) {
|
|
4308
|
+
runDiff(cwd, config, args.includes('--staged'));
|
|
4309
|
+
process.exit(0);
|
|
4310
|
+
}
|
|
4311
|
+
|
|
4312
|
+
// Default: generate once
|
|
4313
|
+
runGenerate(cwd, config, false);
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
main();
|