sigmap 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.contextignore.example +34 -0
- package/CHANGELOG.md +402 -0
- package/LICENSE +21 -0
- package/README.md +601 -0
- package/gen-context.config.json.example +40 -0
- package/gen-context.js +4316 -0
- package/gen-project-map.js +172 -0
- package/package.json +67 -0
- package/src/config/defaults.js +61 -0
- package/src/config/loader.js +60 -0
- package/src/extractors/cpp.js +60 -0
- package/src/extractors/csharp.js +48 -0
- package/src/extractors/css.js +51 -0
- package/src/extractors/dart.js +58 -0
- package/src/extractors/dockerfile.js +49 -0
- package/src/extractors/go.js +61 -0
- package/src/extractors/html.js +39 -0
- package/src/extractors/java.js +49 -0
- package/src/extractors/javascript.js +82 -0
- package/src/extractors/kotlin.js +62 -0
- package/src/extractors/php.js +62 -0
- package/src/extractors/python.js +69 -0
- package/src/extractors/ruby.js +43 -0
- package/src/extractors/rust.js +72 -0
- package/src/extractors/scala.js +67 -0
- package/src/extractors/shell.js +43 -0
- package/src/extractors/svelte.js +51 -0
- package/src/extractors/swift.js +63 -0
- package/src/extractors/typescript.js +109 -0
- package/src/extractors/vue.js +66 -0
- package/src/extractors/yaml.js +59 -0
- package/src/format/cache.js +53 -0
- package/src/health/scorer.js +123 -0
- package/src/map/class-hierarchy.js +117 -0
- package/src/map/import-graph.js +148 -0
- package/src/map/route-table.js +127 -0
- package/src/mcp/handlers.js +433 -0
- package/src/mcp/server.js +128 -0
- package/src/mcp/tools.js +125 -0
- package/src/routing/classifier.js +102 -0
- package/src/routing/hints.js +103 -0
- package/src/security/patterns.js +51 -0
- package/src/security/scanner.js +36 -0
- package/src/tracking/logger.js +115 -0
|
@@ -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 };
|