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
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract signatures from Swift source code.
5
+ * @param {string} src - Raw file content
6
+ * @returns {string[]} Array of signature strings
7
+ */
8
+ function extract(src) {
9
+ if (!src || typeof src !== 'string') return [];
10
+ const sigs = [];
11
+
12
+ const stripped = src
13
+ .replace(/\/\/.*$/gm, '')
14
+ .replace(/\/\*[\s\S]*?\*\//g, '');
15
+
16
+ // Classes, structs, protocols, enums
17
+ const typeRe = /^(?:public\s+|internal\s+|open\s+)?(?:final\s+)?(class|struct|protocol|enum|actor)\s+(\w+)(?:<[^{]*>)?(?:\s*:\s*[\w, <>.]+)?\s*\{/gm;
18
+ for (const m of stripped.matchAll(typeRe)) {
19
+ sigs.push(`${m[1]} ${m[2]}`);
20
+ const block = extractBlock(stripped, m.index + m[0].length);
21
+ for (const fn of extractMembers(block)) sigs.push(` ${fn}`);
22
+ }
23
+
24
+ // Top-level public functions
25
+ for (const m of stripped.matchAll(/^(?:public\s+|internal\s+)?(?:static\s+)?(?:async\s+)?func\s+(\w+)(?:<[^(]*>)?\s*\(([^)]*)\)/gm)) {
26
+ const asyncKw = m[0].includes('async') ? 'async ' : '';
27
+ sigs.push(`${asyncKw}func ${m[1]}(${normalizeParams(m[2])})`);
28
+ }
29
+
30
+ return sigs.slice(0, 25);
31
+ }
32
+
33
+ function extractBlock(src, startIndex) {
34
+ let depth = 1, i = startIndex;
35
+ const end = Math.min(src.length, startIndex + 4000);
36
+ while (i < end && depth > 0) {
37
+ if (src[i] === '{') depth++;
38
+ else if (src[i] === '}') depth--;
39
+ i++;
40
+ }
41
+ return src.slice(startIndex, i - 1);
42
+ }
43
+
44
+ function extractMembers(block) {
45
+ const members = [];
46
+ 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)) {
47
+ if (m[1].startsWith('_')) continue;
48
+ const asyncKw = m[0].includes('async') ? 'async ' : '';
49
+ members.push(`${asyncKw}func ${m[1]}(${normalizeParams(m[2])})`);
50
+ }
51
+ return members.slice(0, 8);
52
+ }
53
+
54
+ function normalizeParams(params) {
55
+ if (!params) return '';
56
+ return params.trim()
57
+ .split(',')
58
+ .map((p) => p.trim().split(':')[0].trim())
59
+ .filter(Boolean)
60
+ .join(', ');
61
+ }
62
+
63
+ module.exports = { extract };
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract signatures from TypeScript source code.
5
+ * @param {string} src - Raw file content
6
+ * @returns {string[]} Array of signature strings
7
+ */
8
+ function extract(src) {
9
+ if (!src || typeof src !== 'string') return [];
10
+ const sigs = [];
11
+
12
+ // Strip single-line comments
13
+ const stripped = src
14
+ .replace(/\/\/.*$/gm, '')
15
+ .replace(/\/\*[\s\S]*?\*\//g, '');
16
+
17
+ // Exported interfaces
18
+ for (const m of stripped.matchAll(/^export\s+interface\s+(\w+)(?:<[^{]*>)?\s*(?:extends\s+[^{]+)?\{/gm)) {
19
+ sigs.push(`export interface ${m[1]}`);
20
+ // Collect members
21
+ const start = m.index + m[0].length;
22
+ const block = extractBlock(stripped, start);
23
+ const members = extractInterfaceMembers(block);
24
+ for (const mem of members) sigs.push(` ${mem}`);
25
+ }
26
+
27
+ // Exported type aliases
28
+ for (const m of stripped.matchAll(/^export\s+type\s+(\w+)(?:<[^=]*>)?\s*=/gm)) {
29
+ sigs.push(`export type ${m[1]}`);
30
+ }
31
+
32
+ // Exported enums
33
+ for (const m of stripped.matchAll(/^export\s+(?:const\s+)?enum\s+(\w+)\s*\{/gm)) {
34
+ sigs.push(`export enum ${m[1]}`);
35
+ }
36
+
37
+ // Classes (exported and internal)
38
+ const classRegex = /^(export\s+)?(abstract\s+)?class\s+(\w+)(?:<[^{]*>)?(?:\s+extends\s+[\w<>, .]+)?(?:\s+implements\s+[\w<> ,]+)?\s*\{/gm;
39
+ for (const m of stripped.matchAll(classRegex)) {
40
+ const prefix = m[1] ? 'export ' : '';
41
+ const abs = m[2] ? 'abstract ' : '';
42
+ sigs.push(`${prefix}${abs}class ${m[3]}`);
43
+ const start = m.index + m[0].length;
44
+ const block = extractBlock(stripped, start);
45
+ const methods = extractClassMembers(block);
46
+ for (const meth of methods) sigs.push(` ${meth}`);
47
+ }
48
+
49
+ // Exported top-level functions (not methods)
50
+ for (const m of stripped.matchAll(/^export\s+(?:async\s+)?function\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{]+)?\s*\{/gm)) {
51
+ const asyncKw = /export\s+async/.test(m[0]) ? 'async ' : '';
52
+ const params = normalizeParams(m[2]);
53
+ sigs.push(`export ${asyncKw}function ${m[1]}(${params})`);
54
+ }
55
+
56
+ // Exported arrow functions / const functions
57
+ for (const m of stripped.matchAll(/^export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=>{]+)?\s*=>/gm)) {
58
+ const asyncKw = /=\s*async\s+/.test(m[0]) ? 'async ' : '';
59
+ const params = normalizeParams(m[2]);
60
+ sigs.push(`export const ${m[1]} = ${asyncKw}(${params}) =>`);
61
+ }
62
+
63
+ return sigs.slice(0, 25);
64
+ }
65
+
66
+ function extractBlock(src, startIndex) {
67
+ let depth = 1;
68
+ let i = startIndex;
69
+ const end = Math.min(src.length, startIndex + 4000);
70
+ while (i < end && depth > 0) {
71
+ if (src[i] === '{') depth++;
72
+ else if (src[i] === '}') depth--;
73
+ i++;
74
+ }
75
+ return src.slice(startIndex, i - 1);
76
+ }
77
+
78
+ function extractInterfaceMembers(block) {
79
+ const members = [];
80
+ for (const m of block.matchAll(/^\s+(readonly\s+)?(\w+)\??:\s*[^;]+;/gm)) {
81
+ const readonly = m[1] ? 'readonly ' : '';
82
+ members.push(`${readonly}${m[2]}`);
83
+ }
84
+ for (const m of block.matchAll(/^\s+(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)\s*:/gm)) {
85
+ members.push(`${m[1]}(${normalizeParams(m[2])})`);
86
+ }
87
+ return members.slice(0, 8);
88
+ }
89
+
90
+ function extractClassMembers(block) {
91
+ const members = [];
92
+ // Public methods (skip private/protected/_ prefixed)
93
+ const methodRe = /^\s+(?:public\s+|static\s+|async\s+|override\s+)*(\w+)\s*(?:<[^(]*>)?\s*\(([^)]*)\)(?:\s*:\s*[^{;]+)?\s*\{/gm;
94
+ for (const m of block.matchAll(methodRe)) {
95
+ if (/^(private|protected|_)/.test(m[1])) continue;
96
+ if (m[1] === 'constructor') { members.push(`constructor(${normalizeParams(m[2])})`); continue; }
97
+ const isAsync = m[0].includes('async ') ? 'async ' : '';
98
+ const isStatic = m[0].includes('static ') ? 'static ' : '';
99
+ members.push(`${isStatic}${isAsync}${m[1]}(${normalizeParams(m[2])})`);
100
+ }
101
+ return members.slice(0, 8);
102
+ }
103
+
104
+ function normalizeParams(params) {
105
+ if (!params) return '';
106
+ return params.trim().replace(/\s+/g, ' ').replace(/:[^,)]+/g, '').trim();
107
+ }
108
+
109
+ module.exports = { extract };
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract signatures from Vue single-file components.
5
+ * @param {string} src - Raw file content
6
+ * @returns {string[]} Array of signature strings
7
+ */
8
+ function extract(src) {
9
+ if (!src || typeof src !== 'string') return [];
10
+ const sigs = [];
11
+
12
+ // Extract component name from filename hint if present or defineComponent
13
+ const nameMatch = src.match(/name\s*:\s*['"](\w+)['"]/);
14
+ if (nameMatch) sigs.push(`component ${nameMatch[1]}`);
15
+
16
+ // Extract <script> block
17
+ const scriptMatch = src.match(/<script(?:\s[^>]*)?>(?:\s*)([\s\S]*?)<\/script>/i);
18
+ if (!scriptMatch) return sigs;
19
+
20
+ const script = scriptMatch[1]
21
+ .replace(/\/\/.*$/gm, '')
22
+ .replace(/\/\*[\s\S]*?\*\//g, '');
23
+
24
+ // Props
25
+ const propsMatch = script.match(/props\s*:\s*(\{[\s\S]*?\})/);
26
+ if (propsMatch) {
27
+ const propNames = [];
28
+ for (const m of propsMatch[1].matchAll(/^\s+(\w+)\s*:/gm)) {
29
+ propNames.push(m[1]);
30
+ }
31
+ if (propNames.length > 0) sigs.push(`props: [${propNames.join(', ')}]`);
32
+ }
33
+
34
+ // Methods in options API
35
+ const methodsMatch = script.match(/methods\s*:\s*\{([\s\S]*?)\},?\s*(?:computed|watch|mounted|created|data|\})/);
36
+ if (methodsMatch) {
37
+ for (const m of methodsMatch[1].matchAll(/^\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)/gm)) {
38
+ if (m[1].startsWith('_')) continue;
39
+ const asyncKw = m[0].includes('async') ? 'async ' : '';
40
+ sigs.push(` ${asyncKw}${m[1]}(${normalizeParams(m[2])})`);
41
+ }
42
+ }
43
+
44
+ // defineProps (Composition API)
45
+ const definePropsMatch = script.match(/defineProps(?:<[^>]*>)?\s*\(\s*(\{[\s\S]*?\})\s*\)/);
46
+ if (definePropsMatch) {
47
+ const propNames = [];
48
+ for (const m of definePropsMatch[1].matchAll(/^\s+(\w+)\s*:/gm)) {
49
+ propNames.push(m[1]);
50
+ }
51
+ if (propNames.length > 0) sigs.push(`defineProps: [${propNames.join(', ')}]`);
52
+ }
53
+
54
+ // Emits
55
+ const emitsMatch = script.match(/(?:defineEmits|emits)\s*(?::\s*|\(\s*)(\[[\s\S]*?\])/);
56
+ if (emitsMatch) sigs.push(`emits: ${emitsMatch[1].replace(/\s+/g, ' ')}`);
57
+
58
+ return sigs.slice(0, 25);
59
+ }
60
+
61
+ function normalizeParams(params) {
62
+ if (!params) return '';
63
+ return params.trim().replace(/\s+/g, ' ');
64
+ }
65
+
66
+ module.exports = { extract };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract signatures from YAML configuration files.
5
+ * @param {string} src - Raw file content
6
+ * @returns {string[]} Array of signature strings
7
+ */
8
+ function extract(src) {
9
+ if (!src || typeof src !== 'string') return [];
10
+ const sigs = [];
11
+
12
+ const lines = src.split('\n');
13
+
14
+ // Top-level keys (no leading whitespace)
15
+ const topKeys = [];
16
+ for (const line of lines) {
17
+ if (/^#/.test(line)) continue;
18
+ const m = line.match(/^([\w-]+)\s*:/);
19
+ if (m) topKeys.push(m[1]);
20
+ }
21
+ if (topKeys.length > 0) sigs.push(`keys: [${topKeys.slice(0, 12).join(', ')}]`);
22
+
23
+ // GitHub Actions: jobs
24
+ let inJobs = false;
25
+ for (const line of lines) {
26
+ if (/^jobs\s*:/.test(line)) { inJobs = true; continue; }
27
+ if (inJobs && /^[a-z]/.test(line) && !line.startsWith('jobs')) inJobs = false;
28
+ if (inJobs) {
29
+ const m = line.match(/^ ([\w-]+)\s*:/);
30
+ if (m) sigs.push(`job: ${m[1]}`);
31
+ }
32
+ }
33
+
34
+ // Docker Compose: services
35
+ let inServices = false;
36
+ for (const line of lines) {
37
+ if (/^services\s*:/.test(line)) { inServices = true; continue; }
38
+ if (inServices && /^[a-z]/.test(line) && !line.startsWith('services')) inServices = false;
39
+ if (inServices) {
40
+ const m = line.match(/^ ([\w-]+)\s*:/);
41
+ if (m) sigs.push(`service: ${m[1]}`);
42
+ }
43
+ }
44
+
45
+ // OpenAPI paths
46
+ let inPaths = false;
47
+ for (const line of lines) {
48
+ if (/^paths\s*:/.test(line)) { inPaths = true; continue; }
49
+ if (inPaths && /^[a-z]/.test(line) && !line.startsWith('paths')) inPaths = false;
50
+ if (inPaths) {
51
+ const m = line.match(/^ (\/[\w/{}-]*)\s*:/);
52
+ if (m) sigs.push(`path: ${m[1]}`);
53
+ }
54
+ }
55
+
56
+ return sigs.slice(0, 25);
57
+ }
58
+
59
+ module.exports = { extract };
@@ -0,0 +1,53 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Format context output for Anthropic prompt cache API.
5
+ *
6
+ * Usage:
7
+ * const { formatCache } = require('./src/format/cache');
8
+ * const json = formatCache(markdownContent);
9
+ * // json is a ready-to-use Anthropic system block with cache_control
10
+ *
11
+ * Writes: .github/copilot-instructions.cache.json
12
+ */
13
+
14
+ /**
15
+ * Wrap markdown context in an Anthropic cache-control system block.
16
+ * @param {string} content - Markdown content from formatOutput()
17
+ * @returns {string} - JSON string: a single Anthropic system content block
18
+ */
19
+ function formatCache(content) {
20
+ if (!content || typeof content !== 'string') content = '';
21
+ const block = {
22
+ type: 'text',
23
+ text: content,
24
+ cache_control: { type: 'ephemeral' },
25
+ };
26
+ return JSON.stringify(block, null, 2);
27
+ }
28
+
29
+ /**
30
+ * Wrap markdown context in a full Anthropic messages API payload.
31
+ * Includes the system array with cache_control so it can be copy-pasted
32
+ * directly into an API call.
33
+ * @param {string} content - Markdown content from formatOutput()
34
+ * @param {string} [model] - Anthropic model ID (default: claude-opus-4-5)
35
+ * @returns {string} - JSON string: { model, system: [...] }
36
+ */
37
+ function formatCachePayload(content, model) {
38
+ if (!content || typeof content !== 'string') content = '';
39
+ const payload = {
40
+ model: model || 'claude-opus-4-5',
41
+ system: [
42
+ {
43
+ type: 'text',
44
+ text: content,
45
+ cache_control: { type: 'ephemeral' },
46
+ },
47
+ ],
48
+ messages: [],
49
+ };
50
+ return JSON.stringify(payload, null, 2);
51
+ }
52
+
53
+ module.exports = { formatCache, formatCachePayload };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * SigMap health scorer.
5
+ *
6
+ * Computes a composite 0-100 health score for the current project by combining:
7
+ * 1. Days since context file was last regenerated (staleness penalty ≤ 30 pts)
8
+ * 2. Average token reduction percentage (low-reduction penalty 20 pts)
9
+ * 3. Over-budget run rate (budget penalty 20 pts)
10
+ *
11
+ * Strategy-aware: thresholds adjust based on the active strategy so that
12
+ * hot-cold (90% reduction intentional) is not penalized as 'low reduction'.
13
+ *
14
+ * Grade scale: A ≥ 90 | B ≥ 75 | C ≥ 60 | D < 60
15
+ *
16
+ * Never throws — returns graceful result with nulls for unavailable metrics.
17
+ *
18
+ * @param {string} cwd - Working directory (root of the project)
19
+ * @returns {{
20
+ * score: number,
21
+ * grade: 'A'|'B'|'C'|'D',
22
+ * strategy: string,
23
+ * tokenReductionPct: number|null,
24
+ * daysSinceRegen: number|null,
25
+ * strategyFreshnessDays: number|null,
26
+ * totalRuns: number,
27
+ * overBudgetRuns: number,
28
+ * }}
29
+ */
30
+ function score(cwd) {
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+
34
+ let tokenReductionPct = null;
35
+ let daysSinceRegen = null;
36
+ let strategyFreshnessDays = null;
37
+ let overBudgetRuns = 0;
38
+ let totalRuns = 0;
39
+
40
+ // ── Detect active strategy ────────────────────────────────────────────────
41
+ let strategy = 'full';
42
+ try {
43
+ const cfgPath = path.join(cwd, 'gen-context.config.json');
44
+ if (fs.existsSync(cfgPath)) {
45
+ const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
46
+ strategy = cfg.strategy || 'full';
47
+ }
48
+ } catch (_) {}
49
+
50
+ // ── Read usage log via tracking logger ──────────────────────────────────
51
+ try {
52
+ const { readLog, summarize } = require('../tracking/logger');
53
+ const entries = readLog(cwd);
54
+ const s = summarize(entries);
55
+ // Only set tokenReductionPct when there is actual history; a brand-new/
56
+ // untracked project should not be penalised for "0% reduction".
57
+ if (s.totalRuns > 0) tokenReductionPct = s.avgReductionPct;
58
+ overBudgetRuns = s.overBudgetRuns;
59
+ totalRuns = s.totalRuns;
60
+ } catch (_) {
61
+ // No usage log yet — proceed with nulls
62
+ }
63
+
64
+ // ── Days since primary context file was last regenerated ─────────────────
65
+ try {
66
+ const ctxFile = path.join(cwd, '.github', 'copilot-instructions.md');
67
+ if (fs.existsSync(ctxFile)) {
68
+ const mtime = fs.statSync(ctxFile).mtimeMs;
69
+ daysSinceRegen = parseFloat(((Date.now() - mtime) / (1000 * 60 * 60 * 24)).toFixed(1));
70
+ }
71
+ } catch (_) {}
72
+
73
+ // ── Strategy freshness: context-cold.md age (hot-cold only) ─────────────
74
+ if (strategy === 'hot-cold') {
75
+ try {
76
+ const coldFile = path.join(cwd, '.github', 'context-cold.md');
77
+ if (fs.existsSync(coldFile)) {
78
+ const mtime = fs.statSync(coldFile).mtimeMs;
79
+ strategyFreshnessDays = parseFloat(((Date.now() - mtime) / (1000 * 60 * 60 * 24)).toFixed(1));
80
+ }
81
+ } catch (_) {}
82
+ }
83
+
84
+ // ── Compute composite score ───────────────────────────────────────────────
85
+ let points = 100;
86
+
87
+ // Staleness penalty: -4 pts per day over the 7-day freshness window (max -30)
88
+ if (daysSinceRegen !== null && daysSinceRegen > 7) {
89
+ points -= Math.min(30, Math.floor((daysSinceRegen - 7) * 4));
90
+ }
91
+
92
+ // Low-reduction penalty — threshold depends on strategy:
93
+ // - hot-cold: primary output is intentionally tiny; measure cold freshness instead
94
+ // - per-module: per-file budgets; global reduction < 60% expected, no penalty
95
+ // - full: standard 60% threshold
96
+ const reductionThreshold = (strategy === 'full') ? 60 : 0; // disable for hot-cold/per-module
97
+ if (tokenReductionPct !== null && tokenReductionPct < reductionThreshold) {
98
+ points -= 20;
99
+ }
100
+
101
+ // hot-cold strategy freshness penalty: context-cold.md older than 1 day (-10 pts)
102
+ if (strategy === 'hot-cold' && strategyFreshnessDays !== null && strategyFreshnessDays > 1) {
103
+ points -= Math.min(10, Math.floor(strategyFreshnessDays - 1) * 3);
104
+ }
105
+
106
+ // Over-budget penalty: more than 20% of runs exceeded the token budget (-20)
107
+ if (overBudgetRuns > 0 && totalRuns > 0) {
108
+ const overBudgetRate = (overBudgetRuns / totalRuns) * 100;
109
+ if (overBudgetRate > 20) points -= 20;
110
+ }
111
+
112
+ points = Math.max(0, Math.min(100, Math.round(points)));
113
+
114
+ let grade;
115
+ if (points >= 90) grade = 'A';
116
+ else if (points >= 75) grade = 'B';
117
+ else if (points >= 60) grade = 'C';
118
+ else grade = 'D';
119
+
120
+ return { score: points, grade, strategy, tokenReductionPct, daysSinceRegen, strategyFreshnessDays, totalRuns, overBudgetRuns };
121
+ }
122
+
123
+ module.exports = { score };
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Class hierarchy analyzer.
5
+ * Extracts class declarations with extends/implements across
6
+ * TypeScript, JavaScript, Python, Java, Kotlin, C# files.
7
+ *
8
+ * @param {string[]} files — absolute file paths to analyze
9
+ * @param {string} cwd — project root for relative path display
10
+ * @returns {string} formatted section content (empty string if nothing found)
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ function analyze(files, cwd) {
17
+ const entries = [];
18
+
19
+ for (const filePath of files) {
20
+ const ext = path.extname(filePath).toLowerCase();
21
+ const rel = path.relative(cwd, filePath).replace(/\\/g, '/');
22
+ let content;
23
+ try { content = fs.readFileSync(filePath, 'utf8'); } catch (_) { continue; }
24
+
25
+ // TS / JS
26
+ if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
27
+ const re = /^\s*(?:export\s+)?(?:abstract\s+)?class\s+(\w+)(?:\s+extends\s+([\w<>.]+?))?(?:\s+implements\s+([\w<>.,\s]+?))?\s*\{/gm;
28
+ let m;
29
+ while ((m = re.exec(content)) !== null) {
30
+ const parent = m[2] ? m[2].split('<')[0].trim() : null;
31
+ const ifaces = m[3]
32
+ ? m[3].split(',').map((s) => s.split('<')[0].trim()).filter(Boolean)
33
+ : [];
34
+ entries.push({ name: m[1], parent, interfaces: ifaces, file: rel });
35
+ }
36
+ }
37
+
38
+ // Python
39
+ if (['.py', '.pyw'].includes(ext)) {
40
+ const re = /^\s*class\s+(\w+)\s*\(([^)]*)\)\s*:/gm;
41
+ let m;
42
+ while ((m = re.exec(content)) !== null) {
43
+ const parents = m[2]
44
+ .split(',')
45
+ .map((s) => s.trim())
46
+ .filter((s) => s && s !== 'object');
47
+ entries.push({
48
+ name: m[1],
49
+ parent: parents[0] || null,
50
+ interfaces: parents.slice(1),
51
+ file: rel,
52
+ });
53
+ }
54
+ }
55
+
56
+ // Java
57
+ if (ext === '.java') {
58
+ const re = /^\s*(?:(?:public|protected|private|static|abstract|final)\s+)*class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s<>]+?))?\s*\{/gm;
59
+ let m;
60
+ while ((m = re.exec(content)) !== null) {
61
+ const ifaces = m[3]
62
+ ? m[3].split(',').map((s) => s.split('<')[0].trim()).filter(Boolean)
63
+ : [];
64
+ entries.push({ name: m[1], parent: m[2] || null, interfaces: ifaces, file: rel });
65
+ }
66
+ }
67
+
68
+ // Kotlin
69
+ if (['.kt', '.kts'].includes(ext)) {
70
+ const re = /^\s*(?:(?:data|sealed|abstract|open|inner)\s+)?class\s+(\w+)(?:\s*[^:\r\n]*)?\s*:\s*([\w<>(),.\s]+?)(?:\s*\{|$)/gm;
71
+ let m;
72
+ while ((m = re.exec(content)) !== null) {
73
+ const parents = m[2]
74
+ .split(',')
75
+ .map((s) => s.replace(/\(.*?\)/, '').split('<')[0].trim())
76
+ .filter(Boolean);
77
+ entries.push({
78
+ name: m[1],
79
+ parent: parents[0] || null,
80
+ interfaces: parents.slice(1),
81
+ file: rel,
82
+ });
83
+ }
84
+ }
85
+
86
+ // C#
87
+ if (ext === '.cs') {
88
+ const re = /^\s*(?:(?:public|internal|protected|private|static|abstract|sealed|partial)\s+)*class\s+(\w+)(?:\s*:\s*([\w<>.,\s]+?))?\s*\{/gm;
89
+ let m;
90
+ while ((m = re.exec(content)) !== null) {
91
+ const parents = m[2]
92
+ ? m[2].split(',').map((s) => s.split('<')[0].trim()).filter(Boolean)
93
+ : [];
94
+ entries.push({
95
+ name: m[1],
96
+ parent: parents[0] || null,
97
+ interfaces: parents.slice(1),
98
+ file: rel,
99
+ });
100
+ }
101
+ }
102
+ }
103
+
104
+ if (entries.length === 0) return '';
105
+
106
+ return entries
107
+ .map((e) => {
108
+ let line = e.name;
109
+ if (e.parent) line += ` extends ${e.parent}`;
110
+ if (e.interfaces.length > 0) line += ` implements ${e.interfaces.join(', ')}`;
111
+ line += ` (${e.file})`;
112
+ return line;
113
+ })
114
+ .join('\n');
115
+ }
116
+
117
+ module.exports = { analyze };