sigmap 3.3.1 → 3.3.3
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/AGENTS.md +42 -0
- package/CHANGELOG.md +18 -0
- package/README.md +108 -46
- package/gen-context.config.json.example +1 -1
- package/gen-context.js +91 -18
- package/package.json +3 -1
- package/packages/adapters/codex.js +74 -0
- package/packages/adapters/copilot.js +36 -1
- package/packages/adapters/gemini.js +36 -1
- package/packages/adapters/index.js +2 -2
- package/packages/cli/package.json +1 -1
- package/packages/core/index.js +5 -0
- package/packages/core/package.json +1 -1
- package/src/config/loader.js +151 -2
- package/src/extractors/graphql.js +66 -0
- package/src/extractors/protobuf.js +63 -0
- package/src/extractors/sql.js +93 -0
- package/src/extractors/terraform.js +74 -0
- package/src/mcp/server.js +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Codex adapter — writes OpenAI-style context to AGENTS.md.
|
|
5
|
+
*
|
|
6
|
+
* This adapter reuses the same prompt format as the OpenAI adapter,
|
|
7
|
+
* but targets AGENTS.md so Codex-style agents can read repository guidance.
|
|
8
|
+
*
|
|
9
|
+
* Contract:
|
|
10
|
+
* format(context, opts?) → string
|
|
11
|
+
* outputPath(cwd) → string
|
|
12
|
+
* write(context, cwd, opts?) → void
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const openai = require('./openai');
|
|
18
|
+
|
|
19
|
+
const name = 'codex';
|
|
20
|
+
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Format context using the OpenAI adapter format.
|
|
24
|
+
* @param {string} context - Raw signature context string
|
|
25
|
+
* @param {object} [opts]
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function format(context, opts = {}) {
|
|
29
|
+
return openai.format(context, opts);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Return the output file path for this adapter.
|
|
34
|
+
* @param {string} cwd - Project root
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function outputPath(cwd) {
|
|
38
|
+
return path.join(cwd, 'AGENTS.md');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Write signatures into AGENTS.md using append-under-marker.
|
|
43
|
+
* If marker exists, content above marker is preserved.
|
|
44
|
+
* If legacy generated content exists without marker, replace it cleanly.
|
|
45
|
+
* @param {string} context - Raw signature context string
|
|
46
|
+
* @param {string} cwd - Project root
|
|
47
|
+
* @param {object} [opts]
|
|
48
|
+
*/
|
|
49
|
+
function write(context, cwd, opts = {}) {
|
|
50
|
+
const filePath = outputPath(cwd);
|
|
51
|
+
let existing = '';
|
|
52
|
+
if (fs.existsSync(filePath)) {
|
|
53
|
+
existing = fs.readFileSync(filePath, 'utf8');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const formatted = format(context, opts);
|
|
57
|
+
const markerIdx = existing.indexOf('## Auto-generated signatures');
|
|
58
|
+
|
|
59
|
+
let newContent;
|
|
60
|
+
if (markerIdx !== -1) {
|
|
61
|
+
newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
|
|
62
|
+
} else {
|
|
63
|
+
const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
|
|
64
|
+
|| existing.includes('## Code Signatures')
|
|
65
|
+
|| existing.includes('# Code signatures');
|
|
66
|
+
newContent = isLegacyGenerated
|
|
67
|
+
? MARKER.trimStart() + formatted
|
|
68
|
+
: existing + MARKER + formatted;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { name, format, outputPath, write };
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const path = require('path');
|
|
13
|
+
const fs = require('fs');
|
|
13
14
|
|
|
14
15
|
const name = 'copilot';
|
|
16
|
+
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* Format context for GitHub Copilot instructions.
|
|
@@ -44,4 +46,37 @@ function outputPath(cwd) {
|
|
|
44
46
|
return path.join(cwd, '.github', 'copilot-instructions.md');
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Write signatures into copilot-instructions.md using append-under-marker.
|
|
51
|
+
* If marker exists, content above marker is preserved.
|
|
52
|
+
* If legacy generated content exists without marker, replace it cleanly.
|
|
53
|
+
* @param {string} context - Raw signature context string
|
|
54
|
+
* @param {string} cwd - Project root
|
|
55
|
+
* @param {object} [opts]
|
|
56
|
+
*/
|
|
57
|
+
function write(context, cwd, opts = {}) {
|
|
58
|
+
const filePath = outputPath(cwd);
|
|
59
|
+
let existing = '';
|
|
60
|
+
if (fs.existsSync(filePath)) {
|
|
61
|
+
existing = fs.readFileSync(filePath, 'utf8');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const formatted = format(context, opts);
|
|
65
|
+
const markerIdx = existing.indexOf('## Auto-generated signatures');
|
|
66
|
+
|
|
67
|
+
let newContent;
|
|
68
|
+
if (markerIdx !== -1) {
|
|
69
|
+
newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
|
|
70
|
+
} else {
|
|
71
|
+
const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
|
|
72
|
+
|| existing.includes('# Code signatures');
|
|
73
|
+
newContent = isLegacyGenerated
|
|
74
|
+
? MARKER.trimStart() + formatted
|
|
75
|
+
: existing + MARKER + formatted;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { name, format, outputPath, write };
|
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const path = require('path');
|
|
18
|
+
const fs = require('fs');
|
|
18
19
|
|
|
19
20
|
const name = 'gemini';
|
|
21
|
+
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Format context as a Gemini system instruction.
|
|
@@ -56,4 +58,37 @@ function outputPath(cwd) {
|
|
|
56
58
|
return path.join(cwd, '.github', 'gemini-context.md');
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Write signatures into gemini-context.md using append-under-marker.
|
|
63
|
+
* If marker exists, content above marker is preserved.
|
|
64
|
+
* If legacy generated content exists without marker, replace it cleanly.
|
|
65
|
+
* @param {string} context - Raw signature context string
|
|
66
|
+
* @param {string} cwd - Project root
|
|
67
|
+
* @param {object} [opts]
|
|
68
|
+
*/
|
|
69
|
+
function write(context, cwd, opts = {}) {
|
|
70
|
+
const filePath = outputPath(cwd);
|
|
71
|
+
let existing = '';
|
|
72
|
+
if (fs.existsSync(filePath)) {
|
|
73
|
+
existing = fs.readFileSync(filePath, 'utf8');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const formatted = format(context, opts);
|
|
77
|
+
const markerIdx = existing.indexOf('## Auto-generated signatures');
|
|
78
|
+
|
|
79
|
+
let newContent;
|
|
80
|
+
if (markerIdx !== -1) {
|
|
81
|
+
newContent = existing.slice(0, markerIdx) + MARKER.trimStart() + formatted;
|
|
82
|
+
} else {
|
|
83
|
+
const isLegacyGenerated = existing.includes('<!-- Generated by SigMap gen-context.js')
|
|
84
|
+
|| existing.includes('## Code Signatures');
|
|
85
|
+
newContent = isLegacyGenerated
|
|
86
|
+
? MARKER.trimStart() + formatted
|
|
87
|
+
: existing + MARKER + formatted;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
91
|
+
fs.writeFileSync(filePath, newContent, 'utf8');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { name, format, outputPath, write };
|
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
|
|
14
|
-
const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini'];
|
|
14
|
+
const ADAPTER_NAMES = ['copilot', 'claude', 'cursor', 'windsurf', 'openai', 'gemini', 'codex'];
|
|
15
15
|
|
|
16
16
|
// Lazy-load adapters so unused ones don't pay any require() cost
|
|
17
17
|
const _cache = {};
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Load and return an adapter module by name.
|
|
21
|
-
* @param {string} name - Adapter name (copilot|claude|cursor|windsurf|openai|gemini)
|
|
21
|
+
* @param {string} name - Adapter name (copilot|claude|cursor|windsurf|openai|gemini|codex)
|
|
22
22
|
* @returns {{ name: string, format: Function, outputPath: Function }|null}
|
|
23
23
|
*/
|
|
24
24
|
function getAdapter(name) {
|
package/packages/core/index.js
CHANGED
|
@@ -35,6 +35,11 @@ const EXT_MAP = {
|
|
|
35
35
|
'.css': 'css', '.scss': 'css', '.sass': 'css', '.less': 'css',
|
|
36
36
|
'.yml': 'yaml', '.yaml': 'yaml',
|
|
37
37
|
'.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell',
|
|
38
|
+
// P1 languages
|
|
39
|
+
'.sql': 'sql',
|
|
40
|
+
'.graphql': 'graphql', '.gql': 'graphql',
|
|
41
|
+
'.tf': 'terraform', '.tfvars': 'terraform',
|
|
42
|
+
'.proto': 'protobuf',
|
|
38
43
|
};
|
|
39
44
|
|
|
40
45
|
const SRC_ROOT = path.resolve(__dirname, '..', '..', 'src');
|
package/src/config/loader.js
CHANGED
|
@@ -7,6 +7,142 @@ const { DEFAULTS } = require('./defaults');
|
|
|
7
7
|
// Keys that are valid in gen-context.config.json
|
|
8
8
|
const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
|
|
9
9
|
|
|
10
|
+
// Common top-level folder names that reliably hold source code
|
|
11
|
+
const COMMON_CODE_DIRS = new Set([
|
|
12
|
+
'src', 'app', 'lib', 'packages', 'services', 'api', 'core', 'cmd',
|
|
13
|
+
'internal', 'pkg', 'handlers', 'controllers', 'models', 'views',
|
|
14
|
+
'components', 'pages', 'routes', 'middleware', 'utils', 'helpers',
|
|
15
|
+
'modules', 'plugins', 'extensions', 'adapters', 'drivers',
|
|
16
|
+
'examples', 'sample', 'demo', 'tests', 'test', 'spec', '__tests__',
|
|
17
|
+
'hooks', 'composables', 'stores', 'features', 'domain', 'infra',
|
|
18
|
+
'infrastructure', 'application', 'data', 'Sources', 'Tests',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const SUPPORTED_CODE_EXTS = new Set([
|
|
22
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
23
|
+
'.py', '.pyw', '.java', '.kt', '.kts', '.go', '.rs', '.cs',
|
|
24
|
+
'.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.rake', '.php',
|
|
25
|
+
'.swift', '.dart', '.scala', '.sc', '.vue', '.svelte',
|
|
26
|
+
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
|
27
|
+
'.yml', '.yaml', '.sh', '.bash', '.zsh', '.fish',
|
|
28
|
+
'.sql', '.graphql', '.gql', '.tf', '.tfvars', '.proto',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Detect source directories for the given project root by reading manifest
|
|
33
|
+
* files and scanning top-level directories for code files.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} cwd - Project root
|
|
36
|
+
* @param {string[]} excludeList - Folders to skip
|
|
37
|
+
* @returns {string[]}
|
|
38
|
+
*/
|
|
39
|
+
function detectAutoSrcDirs(cwd, excludeList) {
|
|
40
|
+
const excludeSet = new Set(excludeList || []);
|
|
41
|
+
const candidates = new Set(DEFAULTS.srcDirs);
|
|
42
|
+
|
|
43
|
+
// ── Manifest-based detection ──────────────────────────────────────────────
|
|
44
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
45
|
+
if (fs.existsSync(pkgPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
48
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies };
|
|
49
|
+
if (allDeps.react || allDeps.next) {
|
|
50
|
+
for (const d of ['src', 'app', 'pages', 'components', 'hooks', 'lib', 'utils']) candidates.add(d);
|
|
51
|
+
}
|
|
52
|
+
if (allDeps['@angular/core']) {
|
|
53
|
+
for (const d of ['src', 'projects', 'apps', 'libs']) candidates.add(d);
|
|
54
|
+
}
|
|
55
|
+
if (allDeps['@nestjs/core']) {
|
|
56
|
+
for (const d of ['src', 'libs', 'apps']) candidates.add(d);
|
|
57
|
+
}
|
|
58
|
+
if (allDeps.vue) {
|
|
59
|
+
for (const d of ['src', 'components', 'views', 'stores', 'composables', 'plugins']) candidates.add(d);
|
|
60
|
+
}
|
|
61
|
+
if (allDeps.svelte || allDeps['@sveltejs/kit']) {
|
|
62
|
+
for (const d of ['src', 'lib', 'routes']) candidates.add(d);
|
|
63
|
+
}
|
|
64
|
+
if (allDeps.nx || allDeps.turbo || allDeps.lerna || pkg.workspaces) {
|
|
65
|
+
for (const d of ['packages', 'apps', 'libs', 'services']) candidates.add(d);
|
|
66
|
+
}
|
|
67
|
+
} catch (_) {}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const hasPyproject = fs.existsSync(path.join(cwd, 'pyproject.toml'));
|
|
71
|
+
const hasRequirements = fs.existsSync(path.join(cwd, 'requirements.txt'));
|
|
72
|
+
const hasSetupPy = fs.existsSync(path.join(cwd, 'setup.py'));
|
|
73
|
+
if (hasPyproject || hasRequirements || hasSetupPy) {
|
|
74
|
+
for (const d of ['src', 'app', 'tests', 'examples']) candidates.add(d);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (fs.existsSync(path.join(cwd, 'Gemfile'))) {
|
|
78
|
+
for (const d of ['app', 'lib', 'config', 'db', 'spec', 'test']) candidates.add(d);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (fs.existsSync(path.join(cwd, 'composer.json'))) {
|
|
82
|
+
for (const d of ['app', 'resources', 'routes', 'database', 'tests']) candidates.add(d);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (fs.existsSync(path.join(cwd, 'go.mod'))) {
|
|
86
|
+
for (const d of ['cmd', 'internal', 'pkg', 'api', 'handler', 'handlers', 'middleware', 'service']) candidates.add(d);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) {
|
|
90
|
+
for (const d of ['src', 'crates', 'examples', 'tests', 'benches']) candidates.add(d);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const hasGradle = fs.existsSync(path.join(cwd, 'build.gradle')) ||
|
|
94
|
+
fs.existsSync(path.join(cwd, 'build.gradle.kts'));
|
|
95
|
+
const hasMaven = fs.existsSync(path.join(cwd, 'pom.xml'));
|
|
96
|
+
if (hasGradle || hasMaven) {
|
|
97
|
+
for (const d of [
|
|
98
|
+
'src/main/java', 'src/main/kotlin', 'src/main/scala',
|
|
99
|
+
'src/main/resources', 'src/test/java', 'src/test/kotlin',
|
|
100
|
+
]) candidates.add(d);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (fs.existsSync(path.join(cwd, 'pubspec.yaml'))) {
|
|
104
|
+
for (const d of ['lib', 'test', 'integration_test', 'example', 'bin']) candidates.add(d);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (fs.existsSync(path.join(cwd, 'Package.swift'))) {
|
|
108
|
+
for (const d of ['Sources', 'Tests']) candidates.add(d);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Top-level directory scan ──────────────────────────────────────────────
|
|
112
|
+
try {
|
|
113
|
+
const entries = fs.readdirSync(cwd, { withFileTypes: true });
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
if (!entry.isDirectory()) continue;
|
|
116
|
+
if (entry.name.startsWith('.')) continue;
|
|
117
|
+
if (excludeSet.has(entry.name)) continue;
|
|
118
|
+
|
|
119
|
+
const lname = entry.name.toLowerCase();
|
|
120
|
+
if (COMMON_CODE_DIRS.has(entry.name) || COMMON_CODE_DIRS.has(lname)) {
|
|
121
|
+
candidates.add(entry.name);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
// Unknown dir: add if it directly contains source files
|
|
125
|
+
const dirPath = path.join(cwd, entry.name);
|
|
126
|
+
try {
|
|
127
|
+
const subs = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
128
|
+
const hasSrc = subs.some((s) => {
|
|
129
|
+
if (!s.isFile()) return false;
|
|
130
|
+
return SUPPORTED_CODE_EXTS.has(path.extname(s.name).toLowerCase()) || s.name === 'Dockerfile';
|
|
131
|
+
});
|
|
132
|
+
if (hasSrc) { candidates.add(entry.name); continue; }
|
|
133
|
+
const hasSrcSub = subs.some((s) =>
|
|
134
|
+
s.isDirectory() && ['src', 'lib', 'main', 'java', 'kotlin', 'scala', 'python'].includes(s.name));
|
|
135
|
+
if (hasSrcSub) candidates.add(entry.name);
|
|
136
|
+
} catch (_) {}
|
|
137
|
+
}
|
|
138
|
+
} catch (_) {}
|
|
139
|
+
|
|
140
|
+
// Only return those that exist
|
|
141
|
+
return Array.from(candidates).filter((d) => {
|
|
142
|
+
try { return fs.statSync(path.join(cwd, d)).isDirectory(); } catch (_) { return false; }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
10
146
|
/**
|
|
11
147
|
* Load and merge configuration for a given working directory.
|
|
12
148
|
*
|
|
@@ -16,7 +152,10 @@ const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
|
|
|
16
152
|
function loadConfig(cwd) {
|
|
17
153
|
const configPath = path.join(cwd, 'gen-context.config.json');
|
|
18
154
|
if (!fs.existsSync(configPath)) {
|
|
19
|
-
|
|
155
|
+
const cfg = deepClone(DEFAULTS);
|
|
156
|
+
const detected = detectAutoSrcDirs(cwd, cfg.exclude);
|
|
157
|
+
if (detected.length > 0) cfg.srcDirs = detected;
|
|
158
|
+
return cfg;
|
|
20
159
|
}
|
|
21
160
|
|
|
22
161
|
let userConfig;
|
|
@@ -25,7 +164,10 @@ function loadConfig(cwd) {
|
|
|
25
164
|
userConfig = JSON.parse(raw);
|
|
26
165
|
} catch (err) {
|
|
27
166
|
console.warn(`[sigmap] config parse error in ${configPath}: ${err.message}`);
|
|
28
|
-
|
|
167
|
+
const cfg = deepClone(DEFAULTS);
|
|
168
|
+
const detected = detectAutoSrcDirs(cwd, cfg.exclude);
|
|
169
|
+
if (detected.length > 0) cfg.srcDirs = detected;
|
|
170
|
+
return cfg;
|
|
29
171
|
}
|
|
30
172
|
|
|
31
173
|
// Warn on unknown keys (helps catch typos)
|
|
@@ -50,6 +192,13 @@ function loadConfig(cwd) {
|
|
|
50
192
|
merged[key] = val;
|
|
51
193
|
}
|
|
52
194
|
}
|
|
195
|
+
|
|
196
|
+
// If user didn't specify srcDirs, auto-detect; fall back to DEFAULTS if nothing found
|
|
197
|
+
if (!Array.isArray(userConfig.srcDirs)) {
|
|
198
|
+
const detected = detectAutoSrcDirs(cwd, merged.exclude);
|
|
199
|
+
merged.srcDirs = detected.length > 0 ? detected : deepClone(DEFAULTS.srcDirs);
|
|
200
|
+
}
|
|
201
|
+
|
|
53
202
|
// Backward compat (v3.0+): mirror outputs ↔ adapters
|
|
54
203
|
if (merged.adapters && !Array.isArray(merged.adapters)) merged.adapters = null;
|
|
55
204
|
if (!merged.adapters && Array.isArray(merged.outputs)) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract signatures from GraphQL schema / operation files.
|
|
5
|
+
* Captures type, interface, enum, input, union, scalar, query, mutation,
|
|
6
|
+
* subscription, fragment definitions.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} src - Raw GraphQL content
|
|
9
|
+
* @returns {string[]} Array of signature strings
|
|
10
|
+
*/
|
|
11
|
+
function extract(src) {
|
|
12
|
+
if (!src || typeof src !== 'string') return [];
|
|
13
|
+
const sigs = [];
|
|
14
|
+
|
|
15
|
+
// Strip comments (# style)
|
|
16
|
+
const stripped = src.replace(/#[^\n]*/g, '');
|
|
17
|
+
|
|
18
|
+
// Schema type definitions: type Foo [implements Bar] { ... }
|
|
19
|
+
for (const m of stripped.matchAll(
|
|
20
|
+
/\b(type|interface|input)\s+(\w+)(?:\s+implements\s+([\w\s&]+))?\s*\{/g
|
|
21
|
+
)) {
|
|
22
|
+
const implements_ = m[3] ? ` implements ${m[3].trim().replace(/\s+/g, ' ')}` : '';
|
|
23
|
+
sigs.push(`${m[1]} ${m[2]}${implements_}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// enum
|
|
27
|
+
for (const m of stripped.matchAll(/\benum\s+(\w+)\s*\{/g)) {
|
|
28
|
+
sigs.push(`enum ${m[1]}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// union
|
|
32
|
+
for (const m of stripped.matchAll(/\bunion\s+(\w+)\s*=/g)) {
|
|
33
|
+
sigs.push(`union ${m[1]}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// scalar
|
|
37
|
+
for (const m of stripped.matchAll(/\bscalar\s+(\w+)/g)) {
|
|
38
|
+
sigs.push(`scalar ${m[1]}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// extend type / extend interface
|
|
42
|
+
for (const m of stripped.matchAll(/\bextend\s+(type|interface)\s+(\w+)/g)) {
|
|
43
|
+
sigs.push(`extend ${m[1]} ${m[2]}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Query / Mutation / Subscription operations
|
|
47
|
+
for (const m of stripped.matchAll(
|
|
48
|
+
/\b(query|mutation|subscription)\s+(\w+)\s*(?:\([^)]*\))?\s*\{/g
|
|
49
|
+
)) {
|
|
50
|
+
sigs.push(`${m[1]} ${m[2]}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Named fragments
|
|
54
|
+
for (const m of stripped.matchAll(/\bfragment\s+(\w+)\s+on\s+(\w+)/g)) {
|
|
55
|
+
sigs.push(`fragment ${m[1]} on ${m[2]}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Top-level schema { query: ... }
|
|
59
|
+
if (/\bschema\s*\{/.test(stripped)) {
|
|
60
|
+
sigs.push('schema { ... }');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return sigs;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { extract };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract signatures from Protocol Buffer (.proto) files.
|
|
5
|
+
* Captures message, enum, service, rpc, oneof, extend definitions.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} src - Raw .proto content
|
|
8
|
+
* @returns {string[]} Array of signature strings
|
|
9
|
+
*/
|
|
10
|
+
function extract(src) {
|
|
11
|
+
if (!src || typeof src !== 'string') return [];
|
|
12
|
+
const sigs = [];
|
|
13
|
+
|
|
14
|
+
// Strip single-line and block comments
|
|
15
|
+
const stripped = src
|
|
16
|
+
.replace(/\/\/[^\n]*/g, '')
|
|
17
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
18
|
+
|
|
19
|
+
// syntax / package / option (top-level metadata)
|
|
20
|
+
const syntaxM = stripped.match(/\bsyntax\s*=\s*"([^"]+)"/);
|
|
21
|
+
if (syntaxM) sigs.push(`syntax = "${syntaxM[1]}"`);
|
|
22
|
+
|
|
23
|
+
const pkgM = stripped.match(/\bpackage\s+([\w.]+)\s*;/);
|
|
24
|
+
if (pkgM) sigs.push(`package ${pkgM[1]}`);
|
|
25
|
+
|
|
26
|
+
// message <Name> { ... }
|
|
27
|
+
for (const m of stripped.matchAll(/\bmessage\s+(\w+)\s*\{/g)) {
|
|
28
|
+
sigs.push(`message ${m[1]}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// enum <Name> { ... }
|
|
32
|
+
for (const m of stripped.matchAll(/\benum\s+(\w+)\s*\{/g)) {
|
|
33
|
+
sigs.push(`enum ${m[1]}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// service <Name> { ... }
|
|
37
|
+
for (const m of stripped.matchAll(/\bservice\s+(\w+)\s*\{/g)) {
|
|
38
|
+
sigs.push(`service ${m[1]}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// rpc <Name>(<Request>) returns (<Response>)
|
|
42
|
+
for (const m of stripped.matchAll(
|
|
43
|
+
/\brpc\s+(\w+)\s*\(\s*(stream\s+)?(\w+)\s*\)\s+returns\s*\(\s*(stream\s+)?(\w+)\s*\)/g
|
|
44
|
+
)) {
|
|
45
|
+
const req = `${m[2] || ''}${m[3]}`.trim();
|
|
46
|
+
const res = `${m[4] || ''}${m[5]}`.trim();
|
|
47
|
+
sigs.push(`rpc ${m[1]}(${req}) returns (${res})`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// oneof <name>
|
|
51
|
+
for (const m of stripped.matchAll(/\boneof\s+(\w+)\s*\{/g)) {
|
|
52
|
+
sigs.push(`oneof ${m[1]}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// extend <TypeName>
|
|
56
|
+
for (const m of stripped.matchAll(/\bextend\s+([\w.]+)\s*\{/g)) {
|
|
57
|
+
sigs.push(`extend ${m[1]}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return sigs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { extract };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract signatures from SQL source files.
|
|
5
|
+
* Captures CREATE TABLE, VIEW, INDEX, FUNCTION, PROCEDURE, TRIGGER, TYPE, SEQUENCE.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} src - Raw SQL content
|
|
8
|
+
* @returns {string[]} Array of signature strings
|
|
9
|
+
*/
|
|
10
|
+
function extract(src) {
|
|
11
|
+
if (!src || typeof src !== 'string') return [];
|
|
12
|
+
const sigs = [];
|
|
13
|
+
|
|
14
|
+
// Strip single-line comments and block comments
|
|
15
|
+
const stripped = src
|
|
16
|
+
.replace(/--[^\n]*/g, '')
|
|
17
|
+
.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
18
|
+
|
|
19
|
+
// CREATE TABLE [IF NOT EXISTS] <name> / CREATE [TEMP] TABLE ...
|
|
20
|
+
for (const m of stripped.matchAll(
|
|
21
|
+
/CREATE\s+(?:OR\s+REPLACE\s+)?(?:TEMP(?:ORARY)?\s+)?TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"[\w.]+)/gi
|
|
22
|
+
)) {
|
|
23
|
+
sigs.push(`TABLE ${_cleanName(m[1])}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// CREATE VIEW / MATERIALIZED VIEW
|
|
27
|
+
for (const m of stripped.matchAll(
|
|
28
|
+
/CREATE\s+(?:OR\s+REPLACE\s+)?(?:MATERIALIZED\s+)?VIEW\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"[\w.]+)/gi
|
|
29
|
+
)) {
|
|
30
|
+
sigs.push(`VIEW ${_cleanName(m[1])}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// CREATE INDEX / UNIQUE INDEX
|
|
34
|
+
for (const m of stripped.matchAll(
|
|
35
|
+
/CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:CONCURRENTLY\s+)?(?:IF\s+NOT\s+EXISTS\s+)?([`"[\w.]+)\s+ON\s+([`"[\w.]+)/gi
|
|
36
|
+
)) {
|
|
37
|
+
sigs.push(`INDEX ${_cleanName(m[1])} ON ${_cleanName(m[2])}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// CREATE FUNCTION / CREATE OR REPLACE FUNCTION
|
|
41
|
+
for (const m of stripped.matchAll(
|
|
42
|
+
/CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+([`"[\w.]+)\s*\(([^)]*)\)/gi
|
|
43
|
+
)) {
|
|
44
|
+
const params = _normalizeParams(m[2]);
|
|
45
|
+
sigs.push(`FUNCTION ${_cleanName(m[1])}(${params})`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// CREATE PROCEDURE
|
|
49
|
+
for (const m of stripped.matchAll(
|
|
50
|
+
/CREATE\s+(?:OR\s+REPLACE\s+)?PROCEDURE\s+([`"[\w.]+)\s*\(([^)]*)\)/gi
|
|
51
|
+
)) {
|
|
52
|
+
const params = _normalizeParams(m[2]);
|
|
53
|
+
sigs.push(`PROCEDURE ${_cleanName(m[1])}(${params})`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// CREATE TRIGGER
|
|
57
|
+
for (const m of stripped.matchAll(
|
|
58
|
+
/CREATE\s+(?:OR\s+REPLACE\s+)?(?:CONSTRAINT\s+)?TRIGGER\s+([`"[\w.]+)/gi
|
|
59
|
+
)) {
|
|
60
|
+
sigs.push(`TRIGGER ${_cleanName(m[1])}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// CREATE TYPE (composite, enum, domain)
|
|
64
|
+
for (const m of stripped.matchAll(
|
|
65
|
+
/CREATE\s+(?:OR\s+REPLACE\s+)?TYPE\s+([`"[\w.]+)/gi
|
|
66
|
+
)) {
|
|
67
|
+
sigs.push(`TYPE ${_cleanName(m[1])}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// CREATE SEQUENCE
|
|
71
|
+
for (const m of stripped.matchAll(
|
|
72
|
+
/CREATE\s+(?:OR\s+REPLACE\s+)?SEQUENCE\s+(?:IF\s+NOT\s+EXISTS\s+)?([`"[\w.]+)/gi
|
|
73
|
+
)) {
|
|
74
|
+
sigs.push(`SEQUENCE ${_cleanName(m[1])}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return sigs;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _cleanName(raw) {
|
|
81
|
+
return raw.replace(/^[`"[]|[`"\]]+$/g, '').trim();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _normalizeParams(raw) {
|
|
85
|
+
if (!raw || !raw.trim()) return '';
|
|
86
|
+
return raw.trim()
|
|
87
|
+
.split(',')
|
|
88
|
+
.map((p) => p.trim().replace(/\s+/g, ' ').split(' ').slice(0, 2).join(' '))
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.join(', ');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { extract };
|