claude-code-workflow 6.3.13 → 6.3.15
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/.claude/agents/issue-plan-agent.md +57 -103
- package/.claude/agents/issue-queue-agent.md +69 -120
- package/.claude/commands/issue/new.md +217 -473
- package/.claude/commands/issue/plan.md +76 -154
- package/.claude/commands/issue/queue.md +208 -259
- package/.claude/skills/issue-manage/SKILL.md +63 -22
- package/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json +3 -3
- package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +3 -3
- package/.claude/workflows/cli-templates/schemas/queue-schema.json +0 -5
- package/.codex/prompts/issue-plan.md +16 -19
- package/.codex/prompts/issue-queue.md +0 -1
- package/README.md +1 -0
- package/ccw/dist/cli.d.ts.map +1 -1
- package/ccw/dist/cli.js +3 -1
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +45 -3
- package/ccw/dist/commands/cli.js.map +1 -1
- package/ccw/dist/commands/issue.d.ts +3 -1
- package/ccw/dist/commands/issue.d.ts.map +1 -1
- package/ccw/dist/commands/issue.js +383 -30
- package/ccw/dist/commands/issue.js.map +1 -1
- package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -1
- package/ccw/dist/core/routes/issue-routes.js +77 -16
- package/ccw/dist/core/routes/issue-routes.js.map +1 -1
- package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
- package/ccw/dist/tools/cli-executor.js +117 -4
- package/ccw/dist/tools/cli-executor.js.map +1 -1
- package/ccw/dist/tools/litellm-executor.d.ts +4 -0
- package/ccw/dist/tools/litellm-executor.d.ts.map +1 -1
- package/ccw/dist/tools/litellm-executor.js +54 -1
- package/ccw/dist/tools/litellm-executor.js.map +1 -1
- package/ccw/dist/tools/ui-generate-preview.d.ts +18 -0
- package/ccw/dist/tools/ui-generate-preview.d.ts.map +1 -1
- package/ccw/dist/tools/ui-generate-preview.js +26 -10
- package/ccw/dist/tools/ui-generate-preview.js.map +1 -1
- package/ccw/src/cli.ts +3 -1
- package/ccw/src/commands/cli.ts +47 -3
- package/ccw/src/commands/issue.ts +442 -34
- package/ccw/src/core/routes/issue-routes.ts +82 -16
- package/ccw/src/tools/cli-executor.ts +125 -4
- package/ccw/src/tools/litellm-executor.ts +107 -24
- package/ccw/src/tools/ui-generate-preview.js +60 -37
- package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/__pycache__/entities.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/config.py +25 -2
- package/codex-lens/src/codexlens/entities.py +5 -1
- package/codex-lens/src/codexlens/indexing/__pycache__/symbol_extractor.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/indexing/symbol_extractor.py +243 -243
- package/codex-lens/src/codexlens/parsers/__pycache__/factory.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/parsers/__pycache__/treesitter_parser.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/parsers/factory.py +256 -256
- package/codex-lens/src/codexlens/parsers/treesitter_parser.py +335 -335
- package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/chain_search.py +30 -1
- package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/reranker.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/embedder.py +6 -9
- package/codex-lens/src/codexlens/semantic/vector_store.py +271 -200
- package/codex-lens/src/codexlens/storage/__pycache__/dir_index.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/storage/__pycache__/index_tree.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/storage/__pycache__/sqlite_store.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/storage/sqlite_store.py +184 -108
- package/package.json +6 -1
- package/.claude/commands/issue/manage.md +0 -113
|
@@ -1,243 +1,243 @@
|
|
|
1
|
-
"""Symbol and relationship extraction from source code."""
|
|
2
|
-
import re
|
|
3
|
-
import sqlite3
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any, Dict, List, Optional, Tuple
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class SymbolExtractor:
|
|
9
|
-
"""Extract symbols and relationships from source code using regex patterns."""
|
|
10
|
-
|
|
11
|
-
# Pattern definitions for different languages
|
|
12
|
-
PATTERNS = {
|
|
13
|
-
'python': {
|
|
14
|
-
'function': r'^(?:async\s+)?def\s+(\w+)\s*\(',
|
|
15
|
-
'class': r'^class\s+(\w+)\s*[:\(]',
|
|
16
|
-
'import': r'^(?:from\s+([\w.]+)\s+)?import\s+([\w.,\s]+)',
|
|
17
|
-
'call': r'(?<![.\w])(\w+)\s*\(',
|
|
18
|
-
},
|
|
19
|
-
'typescript': {
|
|
20
|
-
'function': r'(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*[<\(]',
|
|
21
|
-
'class': r'(?:export\s+)?class\s+(\w+)',
|
|
22
|
-
'import': r"import\s+.*\s+from\s+['\"]([^'\"]+)['\"]",
|
|
23
|
-
'call': r'(?<![.\w])(\w+)\s*[<\(]',
|
|
24
|
-
},
|
|
25
|
-
'javascript': {
|
|
26
|
-
'function': r'(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(',
|
|
27
|
-
'class': r'(?:export\s+)?class\s+(\w+)',
|
|
28
|
-
'import': r"(?:import|require)\s*\(?['\"]([^'\"]+)['\"]",
|
|
29
|
-
'call': r'(?<![.\w])(\w+)\s*\(',
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
LANGUAGE_MAP = {
|
|
34
|
-
'.py': 'python',
|
|
35
|
-
'.ts': 'typescript',
|
|
36
|
-
'.tsx': 'typescript',
|
|
37
|
-
'.js': 'javascript',
|
|
38
|
-
'.jsx': 'javascript',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
def __init__(self, db_path: Path):
|
|
42
|
-
self.db_path = db_path
|
|
43
|
-
self.db_conn: Optional[sqlite3.Connection] = None
|
|
44
|
-
|
|
45
|
-
def connect(self) -> None:
|
|
46
|
-
"""Connect to database and ensure schema exists."""
|
|
47
|
-
self.db_conn = sqlite3.connect(str(self.db_path))
|
|
48
|
-
self._ensure_tables()
|
|
49
|
-
|
|
50
|
-
def __enter__(self) -> "SymbolExtractor":
|
|
51
|
-
"""Context manager entry: connect to database."""
|
|
52
|
-
self.connect()
|
|
53
|
-
return self
|
|
54
|
-
|
|
55
|
-
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
56
|
-
"""Context manager exit: close database connection."""
|
|
57
|
-
self.close()
|
|
58
|
-
|
|
59
|
-
def _ensure_tables(self) -> None:
|
|
60
|
-
"""Create symbols and relationships tables if they don't exist."""
|
|
61
|
-
if not self.db_conn:
|
|
62
|
-
return
|
|
63
|
-
cursor = self.db_conn.cursor()
|
|
64
|
-
|
|
65
|
-
# Create symbols table with qualified_name
|
|
66
|
-
cursor.execute('''
|
|
67
|
-
CREATE TABLE IF NOT EXISTS symbols (
|
|
68
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
-
qualified_name TEXT NOT NULL,
|
|
70
|
-
name TEXT NOT NULL,
|
|
71
|
-
kind TEXT NOT NULL,
|
|
72
|
-
file_path TEXT NOT NULL,
|
|
73
|
-
start_line INTEGER NOT NULL,
|
|
74
|
-
end_line INTEGER NOT NULL,
|
|
75
|
-
UNIQUE(file_path, name, start_line)
|
|
76
|
-
)
|
|
77
|
-
''')
|
|
78
|
-
|
|
79
|
-
# Create relationships table with target_symbol_fqn
|
|
80
|
-
cursor.execute('''
|
|
81
|
-
CREATE TABLE IF NOT EXISTS symbol_relationships (
|
|
82
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
-
source_symbol_id INTEGER NOT NULL,
|
|
84
|
-
target_symbol_fqn TEXT NOT NULL,
|
|
85
|
-
relationship_type TEXT NOT NULL,
|
|
86
|
-
file_path TEXT NOT NULL,
|
|
87
|
-
line INTEGER,
|
|
88
|
-
FOREIGN KEY (source_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE
|
|
89
|
-
)
|
|
90
|
-
''')
|
|
91
|
-
|
|
92
|
-
# Create performance indexes
|
|
93
|
-
cursor.execute('CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)')
|
|
94
|
-
cursor.execute('CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path)')
|
|
95
|
-
cursor.execute('CREATE INDEX IF NOT EXISTS idx_rel_source ON symbol_relationships(source_symbol_id)')
|
|
96
|
-
cursor.execute('CREATE INDEX IF NOT EXISTS idx_rel_target ON symbol_relationships(target_symbol_fqn)')
|
|
97
|
-
cursor.execute('CREATE INDEX IF NOT EXISTS idx_rel_type ON symbol_relationships(relationship_type)')
|
|
98
|
-
|
|
99
|
-
self.db_conn.commit()
|
|
100
|
-
|
|
101
|
-
def extract_from_file(self, file_path: Path, content: str) -> Tuple[List[Dict], List[Dict]]:
|
|
102
|
-
"""Extract symbols and relationships from file content.
|
|
103
|
-
|
|
104
|
-
Args:
|
|
105
|
-
file_path: Path to the source file
|
|
106
|
-
content: File content as string
|
|
107
|
-
|
|
108
|
-
Returns:
|
|
109
|
-
Tuple of (symbols, relationships) where:
|
|
110
|
-
- symbols: List of symbol dicts with qualified_name, name, kind, file_path, start_line, end_line
|
|
111
|
-
- relationships: List of relationship dicts with source_scope, target, type, file_path, line
|
|
112
|
-
"""
|
|
113
|
-
ext = file_path.suffix.lower()
|
|
114
|
-
lang = self.LANGUAGE_MAP.get(ext)
|
|
115
|
-
|
|
116
|
-
if not lang or lang not in self.PATTERNS:
|
|
117
|
-
return [], []
|
|
118
|
-
|
|
119
|
-
patterns = self.PATTERNS[lang]
|
|
120
|
-
symbols = []
|
|
121
|
-
relationships = []
|
|
122
|
-
lines = content.split('\n')
|
|
123
|
-
|
|
124
|
-
current_scope = None
|
|
125
|
-
|
|
126
|
-
for line_num, line in enumerate(lines, 1):
|
|
127
|
-
# Extract function/class definitions
|
|
128
|
-
for kind in ['function', 'class']:
|
|
129
|
-
if kind in patterns:
|
|
130
|
-
match = re.search(patterns[kind], line)
|
|
131
|
-
if match:
|
|
132
|
-
name = match.group(1)
|
|
133
|
-
qualified_name = f"{file_path.stem}.{name}"
|
|
134
|
-
symbols.append({
|
|
135
|
-
'qualified_name': qualified_name,
|
|
136
|
-
'name': name,
|
|
137
|
-
'kind': kind,
|
|
138
|
-
'file_path': str(file_path),
|
|
139
|
-
'start_line': line_num,
|
|
140
|
-
'end_line': line_num, # Simplified - would need proper parsing for actual end
|
|
141
|
-
})
|
|
142
|
-
current_scope = name
|
|
143
|
-
|
|
144
|
-
# Extract imports
|
|
145
|
-
if 'import' in patterns:
|
|
146
|
-
match = re.search(patterns['import'], line)
|
|
147
|
-
if match:
|
|
148
|
-
import_target = match.group(1) or match.group(2) if match.lastindex >= 2 else match.group(1)
|
|
149
|
-
if import_target and current_scope:
|
|
150
|
-
relationships.append({
|
|
151
|
-
'source_scope': current_scope,
|
|
152
|
-
'target': import_target.strip(),
|
|
153
|
-
'type': 'imports',
|
|
154
|
-
'file_path': str(file_path),
|
|
155
|
-
'line': line_num,
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
# Extract function calls (simplified)
|
|
159
|
-
if 'call' in patterns and current_scope:
|
|
160
|
-
for match in re.finditer(patterns['call'], line):
|
|
161
|
-
call_name = match.group(1)
|
|
162
|
-
# Skip common keywords and the current function
|
|
163
|
-
if call_name not in ['if', 'for', 'while', 'return', 'print', 'len', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple', current_scope]:
|
|
164
|
-
relationships.append({
|
|
165
|
-
'source_scope': current_scope,
|
|
166
|
-
'target': call_name,
|
|
167
|
-
'type': 'calls',
|
|
168
|
-
'file_path': str(file_path),
|
|
169
|
-
'line': line_num,
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
return symbols, relationships
|
|
173
|
-
|
|
174
|
-
def save_symbols(self, symbols: List[Dict]) -> Dict[str, int]:
|
|
175
|
-
"""Save symbols to database and return name->id mapping.
|
|
176
|
-
|
|
177
|
-
Args:
|
|
178
|
-
symbols: List of symbol dicts with qualified_name, name, kind, file_path, start_line, end_line
|
|
179
|
-
|
|
180
|
-
Returns:
|
|
181
|
-
Dictionary mapping symbol name to database id
|
|
182
|
-
"""
|
|
183
|
-
if not self.db_conn or not symbols:
|
|
184
|
-
return {}
|
|
185
|
-
|
|
186
|
-
cursor = self.db_conn.cursor()
|
|
187
|
-
name_to_id = {}
|
|
188
|
-
|
|
189
|
-
for sym in symbols:
|
|
190
|
-
try:
|
|
191
|
-
cursor.execute('''
|
|
192
|
-
INSERT OR IGNORE INTO symbols
|
|
193
|
-
(qualified_name, name, kind, file_path, start_line, end_line)
|
|
194
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
195
|
-
''', (sym['qualified_name'], sym['name'], sym['kind'],
|
|
196
|
-
sym['file_path'], sym['start_line'], sym['end_line']))
|
|
197
|
-
|
|
198
|
-
# Get the id
|
|
199
|
-
cursor.execute('''
|
|
200
|
-
SELECT id FROM symbols
|
|
201
|
-
WHERE file_path = ? AND name = ? AND start_line = ?
|
|
202
|
-
''', (sym['file_path'], sym['name'], sym['start_line']))
|
|
203
|
-
|
|
204
|
-
row = cursor.fetchone()
|
|
205
|
-
if row:
|
|
206
|
-
name_to_id[sym['name']] = row[0]
|
|
207
|
-
except sqlite3.Error:
|
|
208
|
-
continue
|
|
209
|
-
|
|
210
|
-
self.db_conn.commit()
|
|
211
|
-
return name_to_id
|
|
212
|
-
|
|
213
|
-
def save_relationships(self, relationships: List[Dict], name_to_id: Dict[str, int]) -> None:
|
|
214
|
-
"""Save relationships to database.
|
|
215
|
-
|
|
216
|
-
Args:
|
|
217
|
-
relationships: List of relationship dicts with source_scope, target, type, file_path, line
|
|
218
|
-
name_to_id: Dictionary mapping symbol names to database ids
|
|
219
|
-
"""
|
|
220
|
-
if not self.db_conn or not relationships:
|
|
221
|
-
return
|
|
222
|
-
|
|
223
|
-
cursor = self.db_conn.cursor()
|
|
224
|
-
|
|
225
|
-
for rel in relationships:
|
|
226
|
-
source_id = name_to_id.get(rel['source_scope'])
|
|
227
|
-
if source_id:
|
|
228
|
-
try:
|
|
229
|
-
cursor.execute('''
|
|
230
|
-
INSERT INTO symbol_relationships
|
|
231
|
-
(source_symbol_id, target_symbol_fqn, relationship_type, file_path, line)
|
|
232
|
-
VALUES (?, ?, ?, ?, ?)
|
|
233
|
-
''', (source_id, rel['target'], rel['type'], rel['file_path'], rel['line']))
|
|
234
|
-
except sqlite3.Error:
|
|
235
|
-
continue
|
|
236
|
-
|
|
237
|
-
self.db_conn.commit()
|
|
238
|
-
|
|
239
|
-
def close(self) -> None:
|
|
240
|
-
"""Close database connection."""
|
|
241
|
-
if self.db_conn:
|
|
242
|
-
self.db_conn.close()
|
|
243
|
-
self.db_conn = None
|
|
1
|
+
"""Symbol and relationship extraction from source code."""
|
|
2
|
+
import re
|
|
3
|
+
import sqlite3
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SymbolExtractor:
|
|
9
|
+
"""Extract symbols and relationships from source code using regex patterns."""
|
|
10
|
+
|
|
11
|
+
# Pattern definitions for different languages
|
|
12
|
+
PATTERNS = {
|
|
13
|
+
'python': {
|
|
14
|
+
'function': r'^(?:async\s+)?def\s+(\w+)\s*\(',
|
|
15
|
+
'class': r'^class\s+(\w+)\s*[:\(]',
|
|
16
|
+
'import': r'^(?:from\s+([\w.]+)\s+)?import\s+([\w.,\s]+)',
|
|
17
|
+
'call': r'(?<![.\w])(\w+)\s*\(',
|
|
18
|
+
},
|
|
19
|
+
'typescript': {
|
|
20
|
+
'function': r'(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*[<\(]',
|
|
21
|
+
'class': r'(?:export\s+)?class\s+(\w+)',
|
|
22
|
+
'import': r"import\s+.*\s+from\s+['\"]([^'\"]+)['\"]",
|
|
23
|
+
'call': r'(?<![.\w])(\w+)\s*[<\(]',
|
|
24
|
+
},
|
|
25
|
+
'javascript': {
|
|
26
|
+
'function': r'(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(',
|
|
27
|
+
'class': r'(?:export\s+)?class\s+(\w+)',
|
|
28
|
+
'import': r"(?:import|require)\s*\(?['\"]([^'\"]+)['\"]",
|
|
29
|
+
'call': r'(?<![.\w])(\w+)\s*\(',
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
LANGUAGE_MAP = {
|
|
34
|
+
'.py': 'python',
|
|
35
|
+
'.ts': 'typescript',
|
|
36
|
+
'.tsx': 'typescript',
|
|
37
|
+
'.js': 'javascript',
|
|
38
|
+
'.jsx': 'javascript',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
def __init__(self, db_path: Path):
|
|
42
|
+
self.db_path = db_path
|
|
43
|
+
self.db_conn: Optional[sqlite3.Connection] = None
|
|
44
|
+
|
|
45
|
+
def connect(self) -> None:
|
|
46
|
+
"""Connect to database and ensure schema exists."""
|
|
47
|
+
self.db_conn = sqlite3.connect(str(self.db_path))
|
|
48
|
+
self._ensure_tables()
|
|
49
|
+
|
|
50
|
+
def __enter__(self) -> "SymbolExtractor":
|
|
51
|
+
"""Context manager entry: connect to database."""
|
|
52
|
+
self.connect()
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
56
|
+
"""Context manager exit: close database connection."""
|
|
57
|
+
self.close()
|
|
58
|
+
|
|
59
|
+
def _ensure_tables(self) -> None:
|
|
60
|
+
"""Create symbols and relationships tables if they don't exist."""
|
|
61
|
+
if not self.db_conn:
|
|
62
|
+
return
|
|
63
|
+
cursor = self.db_conn.cursor()
|
|
64
|
+
|
|
65
|
+
# Create symbols table with qualified_name
|
|
66
|
+
cursor.execute('''
|
|
67
|
+
CREATE TABLE IF NOT EXISTS symbols (
|
|
68
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
69
|
+
qualified_name TEXT NOT NULL,
|
|
70
|
+
name TEXT NOT NULL,
|
|
71
|
+
kind TEXT NOT NULL,
|
|
72
|
+
file_path TEXT NOT NULL,
|
|
73
|
+
start_line INTEGER NOT NULL,
|
|
74
|
+
end_line INTEGER NOT NULL,
|
|
75
|
+
UNIQUE(file_path, name, start_line)
|
|
76
|
+
)
|
|
77
|
+
''')
|
|
78
|
+
|
|
79
|
+
# Create relationships table with target_symbol_fqn
|
|
80
|
+
cursor.execute('''
|
|
81
|
+
CREATE TABLE IF NOT EXISTS symbol_relationships (
|
|
82
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
83
|
+
source_symbol_id INTEGER NOT NULL,
|
|
84
|
+
target_symbol_fqn TEXT NOT NULL,
|
|
85
|
+
relationship_type TEXT NOT NULL,
|
|
86
|
+
file_path TEXT NOT NULL,
|
|
87
|
+
line INTEGER,
|
|
88
|
+
FOREIGN KEY (source_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE
|
|
89
|
+
)
|
|
90
|
+
''')
|
|
91
|
+
|
|
92
|
+
# Create performance indexes
|
|
93
|
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)')
|
|
94
|
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path)')
|
|
95
|
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_rel_source ON symbol_relationships(source_symbol_id)')
|
|
96
|
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_rel_target ON symbol_relationships(target_symbol_fqn)')
|
|
97
|
+
cursor.execute('CREATE INDEX IF NOT EXISTS idx_rel_type ON symbol_relationships(relationship_type)')
|
|
98
|
+
|
|
99
|
+
self.db_conn.commit()
|
|
100
|
+
|
|
101
|
+
def extract_from_file(self, file_path: Path, content: str) -> Tuple[List[Dict], List[Dict]]:
|
|
102
|
+
"""Extract symbols and relationships from file content.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
file_path: Path to the source file
|
|
106
|
+
content: File content as string
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Tuple of (symbols, relationships) where:
|
|
110
|
+
- symbols: List of symbol dicts with qualified_name, name, kind, file_path, start_line, end_line
|
|
111
|
+
- relationships: List of relationship dicts with source_scope, target, type, file_path, line
|
|
112
|
+
"""
|
|
113
|
+
ext = file_path.suffix.lower()
|
|
114
|
+
lang = self.LANGUAGE_MAP.get(ext)
|
|
115
|
+
|
|
116
|
+
if not lang or lang not in self.PATTERNS:
|
|
117
|
+
return [], []
|
|
118
|
+
|
|
119
|
+
patterns = self.PATTERNS[lang]
|
|
120
|
+
symbols = []
|
|
121
|
+
relationships = []
|
|
122
|
+
lines = content.split('\n')
|
|
123
|
+
|
|
124
|
+
current_scope = None
|
|
125
|
+
|
|
126
|
+
for line_num, line in enumerate(lines, 1):
|
|
127
|
+
# Extract function/class definitions
|
|
128
|
+
for kind in ['function', 'class']:
|
|
129
|
+
if kind in patterns:
|
|
130
|
+
match = re.search(patterns[kind], line)
|
|
131
|
+
if match:
|
|
132
|
+
name = match.group(1)
|
|
133
|
+
qualified_name = f"{file_path.stem}.{name}"
|
|
134
|
+
symbols.append({
|
|
135
|
+
'qualified_name': qualified_name,
|
|
136
|
+
'name': name,
|
|
137
|
+
'kind': kind,
|
|
138
|
+
'file_path': str(file_path),
|
|
139
|
+
'start_line': line_num,
|
|
140
|
+
'end_line': line_num, # Simplified - would need proper parsing for actual end
|
|
141
|
+
})
|
|
142
|
+
current_scope = name
|
|
143
|
+
|
|
144
|
+
# Extract imports
|
|
145
|
+
if 'import' in patterns:
|
|
146
|
+
match = re.search(patterns['import'], line)
|
|
147
|
+
if match:
|
|
148
|
+
import_target = match.group(1) or match.group(2) if match.lastindex >= 2 else match.group(1)
|
|
149
|
+
if import_target and current_scope:
|
|
150
|
+
relationships.append({
|
|
151
|
+
'source_scope': current_scope,
|
|
152
|
+
'target': import_target.strip(),
|
|
153
|
+
'type': 'imports',
|
|
154
|
+
'file_path': str(file_path),
|
|
155
|
+
'line': line_num,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
# Extract function calls (simplified)
|
|
159
|
+
if 'call' in patterns and current_scope:
|
|
160
|
+
for match in re.finditer(patterns['call'], line):
|
|
161
|
+
call_name = match.group(1)
|
|
162
|
+
# Skip common keywords and the current function
|
|
163
|
+
if call_name not in ['if', 'for', 'while', 'return', 'print', 'len', 'str', 'int', 'float', 'list', 'dict', 'set', 'tuple', current_scope]:
|
|
164
|
+
relationships.append({
|
|
165
|
+
'source_scope': current_scope,
|
|
166
|
+
'target': call_name,
|
|
167
|
+
'type': 'calls',
|
|
168
|
+
'file_path': str(file_path),
|
|
169
|
+
'line': line_num,
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return symbols, relationships
|
|
173
|
+
|
|
174
|
+
def save_symbols(self, symbols: List[Dict]) -> Dict[str, int]:
|
|
175
|
+
"""Save symbols to database and return name->id mapping.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
symbols: List of symbol dicts with qualified_name, name, kind, file_path, start_line, end_line
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dictionary mapping symbol name to database id
|
|
182
|
+
"""
|
|
183
|
+
if not self.db_conn or not symbols:
|
|
184
|
+
return {}
|
|
185
|
+
|
|
186
|
+
cursor = self.db_conn.cursor()
|
|
187
|
+
name_to_id = {}
|
|
188
|
+
|
|
189
|
+
for sym in symbols:
|
|
190
|
+
try:
|
|
191
|
+
cursor.execute('''
|
|
192
|
+
INSERT OR IGNORE INTO symbols
|
|
193
|
+
(qualified_name, name, kind, file_path, start_line, end_line)
|
|
194
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
195
|
+
''', (sym['qualified_name'], sym['name'], sym['kind'],
|
|
196
|
+
sym['file_path'], sym['start_line'], sym['end_line']))
|
|
197
|
+
|
|
198
|
+
# Get the id
|
|
199
|
+
cursor.execute('''
|
|
200
|
+
SELECT id FROM symbols
|
|
201
|
+
WHERE file_path = ? AND name = ? AND start_line = ?
|
|
202
|
+
''', (sym['file_path'], sym['name'], sym['start_line']))
|
|
203
|
+
|
|
204
|
+
row = cursor.fetchone()
|
|
205
|
+
if row:
|
|
206
|
+
name_to_id[sym['name']] = row[0]
|
|
207
|
+
except sqlite3.Error:
|
|
208
|
+
continue
|
|
209
|
+
|
|
210
|
+
self.db_conn.commit()
|
|
211
|
+
return name_to_id
|
|
212
|
+
|
|
213
|
+
def save_relationships(self, relationships: List[Dict], name_to_id: Dict[str, int]) -> None:
|
|
214
|
+
"""Save relationships to database.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
relationships: List of relationship dicts with source_scope, target, type, file_path, line
|
|
218
|
+
name_to_id: Dictionary mapping symbol names to database ids
|
|
219
|
+
"""
|
|
220
|
+
if not self.db_conn or not relationships:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
cursor = self.db_conn.cursor()
|
|
224
|
+
|
|
225
|
+
for rel in relationships:
|
|
226
|
+
source_id = name_to_id.get(rel['source_scope'])
|
|
227
|
+
if source_id:
|
|
228
|
+
try:
|
|
229
|
+
cursor.execute('''
|
|
230
|
+
INSERT INTO symbol_relationships
|
|
231
|
+
(source_symbol_id, target_symbol_fqn, relationship_type, file_path, line)
|
|
232
|
+
VALUES (?, ?, ?, ?, ?)
|
|
233
|
+
''', (source_id, rel['target'], rel['type'], rel['file_path'], rel['line']))
|
|
234
|
+
except sqlite3.Error:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
self.db_conn.commit()
|
|
238
|
+
|
|
239
|
+
def close(self) -> None:
|
|
240
|
+
"""Close database connection."""
|
|
241
|
+
if self.db_conn:
|
|
242
|
+
self.db_conn.close()
|
|
243
|
+
self.db_conn = None
|
|
Binary file
|