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.
@@ -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
- module.exports = { name, format, outputPath };
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
- module.exports = { name, format, outputPath };
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) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-cli",
3
- "version": "3.3.1",
3
+ "version": "3.3.3",
4
4
  "description": "SigMap CLI wrapper — thin adapter for programmatic CLI invocation",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sigmap-core",
3
- "version": "3.3.1",
3
+ "version": "3.3.3",
4
4
  "description": "SigMap core library — zero-dependency code signature extraction, retrieval, and security scanning",
5
5
  "main": "index.js",
6
6
  "keywords": [
@@ -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
- return deepClone(DEFAULTS);
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
- return deepClone(DEFAULTS);
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 };