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.
Files changed (44) hide show
  1. package/.contextignore.example +34 -0
  2. package/CHANGELOG.md +402 -0
  3. package/LICENSE +21 -0
  4. package/README.md +601 -0
  5. package/gen-context.config.json.example +40 -0
  6. package/gen-context.js +4316 -0
  7. package/gen-project-map.js +172 -0
  8. package/package.json +67 -0
  9. package/src/config/defaults.js +61 -0
  10. package/src/config/loader.js +60 -0
  11. package/src/extractors/cpp.js +60 -0
  12. package/src/extractors/csharp.js +48 -0
  13. package/src/extractors/css.js +51 -0
  14. package/src/extractors/dart.js +58 -0
  15. package/src/extractors/dockerfile.js +49 -0
  16. package/src/extractors/go.js +61 -0
  17. package/src/extractors/html.js +39 -0
  18. package/src/extractors/java.js +49 -0
  19. package/src/extractors/javascript.js +82 -0
  20. package/src/extractors/kotlin.js +62 -0
  21. package/src/extractors/php.js +62 -0
  22. package/src/extractors/python.js +69 -0
  23. package/src/extractors/ruby.js +43 -0
  24. package/src/extractors/rust.js +72 -0
  25. package/src/extractors/scala.js +67 -0
  26. package/src/extractors/shell.js +43 -0
  27. package/src/extractors/svelte.js +51 -0
  28. package/src/extractors/swift.js +63 -0
  29. package/src/extractors/typescript.js +109 -0
  30. package/src/extractors/vue.js +66 -0
  31. package/src/extractors/yaml.js +59 -0
  32. package/src/format/cache.js +53 -0
  33. package/src/health/scorer.js +123 -0
  34. package/src/map/class-hierarchy.js +117 -0
  35. package/src/map/import-graph.js +148 -0
  36. package/src/map/route-table.js +127 -0
  37. package/src/mcp/handlers.js +433 -0
  38. package/src/mcp/server.js +128 -0
  39. package/src/mcp/tools.js +125 -0
  40. package/src/routing/classifier.js +102 -0
  41. package/src/routing/hints.js +103 -0
  42. package/src/security/patterns.js +51 -0
  43. package/src/security/scanner.js +36 -0
  44. 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();