devrel-origin 0.2.14__py3-none-any.whl
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.
- devrel_origin/__init__.py +15 -0
- devrel_origin/cli/__init__.py +92 -0
- devrel_origin/cli/_common.py +243 -0
- devrel_origin/cli/analytics.py +28 -0
- devrel_origin/cli/argus.py +497 -0
- devrel_origin/cli/auth.py +227 -0
- devrel_origin/cli/config.py +108 -0
- devrel_origin/cli/content.py +259 -0
- devrel_origin/cli/cost.py +108 -0
- devrel_origin/cli/cro.py +298 -0
- devrel_origin/cli/deliverables.py +65 -0
- devrel_origin/cli/docs.py +91 -0
- devrel_origin/cli/doctor.py +178 -0
- devrel_origin/cli/experiment.py +29 -0
- devrel_origin/cli/growth.py +97 -0
- devrel_origin/cli/init.py +472 -0
- devrel_origin/cli/intel.py +27 -0
- devrel_origin/cli/kb.py +96 -0
- devrel_origin/cli/listen.py +31 -0
- devrel_origin/cli/marketing.py +66 -0
- devrel_origin/cli/migrate.py +45 -0
- devrel_origin/cli/run.py +46 -0
- devrel_origin/cli/sales.py +57 -0
- devrel_origin/cli/schedule.py +62 -0
- devrel_origin/cli/synthesize.py +28 -0
- devrel_origin/cli/triage.py +29 -0
- devrel_origin/cli/video.py +35 -0
- devrel_origin/core/__init__.py +58 -0
- devrel_origin/core/agent_config.py +75 -0
- devrel_origin/core/argus.py +964 -0
- devrel_origin/core/atlas.py +1450 -0
- devrel_origin/core/base.py +372 -0
- devrel_origin/core/cyra.py +563 -0
- devrel_origin/core/dex.py +708 -0
- devrel_origin/core/echo.py +614 -0
- devrel_origin/core/growth/__init__.py +27 -0
- devrel_origin/core/growth/recommendations.py +219 -0
- devrel_origin/core/growth/target_kinds.py +51 -0
- devrel_origin/core/iris.py +513 -0
- devrel_origin/core/kai.py +1367 -0
- devrel_origin/core/llm.py +542 -0
- devrel_origin/core/llm_backends.py +274 -0
- devrel_origin/core/mox.py +514 -0
- devrel_origin/core/nova.py +349 -0
- devrel_origin/core/pax.py +1205 -0
- devrel_origin/core/rex.py +532 -0
- devrel_origin/core/sage.py +486 -0
- devrel_origin/core/sentinel.py +385 -0
- devrel_origin/core/types.py +98 -0
- devrel_origin/core/video/__init__.py +22 -0
- devrel_origin/core/video/assembler.py +131 -0
- devrel_origin/core/video/browser_recorder.py +118 -0
- devrel_origin/core/video/desktop_recorder.py +254 -0
- devrel_origin/core/video/overlay_renderer.py +143 -0
- devrel_origin/core/video/script_parser.py +147 -0
- devrel_origin/core/video/tts_engine.py +82 -0
- devrel_origin/core/vox.py +268 -0
- devrel_origin/core/watchdog.py +321 -0
- devrel_origin/project/__init__.py +1 -0
- devrel_origin/project/config.py +75 -0
- devrel_origin/project/cost_sink.py +61 -0
- devrel_origin/project/init.py +104 -0
- devrel_origin/project/paths.py +75 -0
- devrel_origin/project/state.py +241 -0
- devrel_origin/project/templates/__init__.py +4 -0
- devrel_origin/project/templates/config.toml +24 -0
- devrel_origin/project/templates/devrel.gitignore +10 -0
- devrel_origin/project/templates/slop-blocklist.md +45 -0
- devrel_origin/project/templates/style.md +24 -0
- devrel_origin/project/templates/voice.md +29 -0
- devrel_origin/quality/__init__.py +66 -0
- devrel_origin/quality/editorial.py +357 -0
- devrel_origin/quality/persona.py +84 -0
- devrel_origin/quality/readability.py +148 -0
- devrel_origin/quality/slop.py +167 -0
- devrel_origin/quality/style.py +110 -0
- devrel_origin/quality/voice.py +15 -0
- devrel_origin/tools/__init__.py +9 -0
- devrel_origin/tools/analytics.py +304 -0
- devrel_origin/tools/api_client.py +393 -0
- devrel_origin/tools/apollo_client.py +305 -0
- devrel_origin/tools/code_validator.py +428 -0
- devrel_origin/tools/github_tools.py +297 -0
- devrel_origin/tools/instantly_client.py +412 -0
- devrel_origin/tools/kb_harvester.py +340 -0
- devrel_origin/tools/mcp_server.py +578 -0
- devrel_origin/tools/notifications.py +245 -0
- devrel_origin/tools/run_report.py +193 -0
- devrel_origin/tools/scheduler.py +231 -0
- devrel_origin/tools/search_tools.py +321 -0
- devrel_origin/tools/self_improve.py +168 -0
- devrel_origin/tools/sheets.py +236 -0
- devrel_origin-0.2.14.dist-info/METADATA +354 -0
- devrel_origin-0.2.14.dist-info/RECORD +98 -0
- devrel_origin-0.2.14.dist-info/WHEEL +5 -0
- devrel_origin-0.2.14.dist-info/entry_points.txt +2 -0
- devrel_origin-0.2.14.dist-info/licenses/LICENSE +21 -0
- devrel_origin-0.2.14.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Code Validator — Syntax validation for code snippets in generated content.
|
|
3
|
+
|
|
4
|
+
Extracts fenced code blocks from markdown and validates syntax per language.
|
|
5
|
+
Supports: Python (ast.parse), JavaScript (esprima/basic checks), JSON (json.loads),
|
|
6
|
+
HTML (basic tag balance), and reports on unsupported languages without failing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import ast
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import re
|
|
13
|
+
import shlex
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
CODE_BLOCK_RE = re.compile(r"```(\w*)\n(.*?)```", re.DOTALL)
|
|
19
|
+
|
|
20
|
+
# Languages where we can validate syntax or safety heuristics
|
|
21
|
+
SUPPORTED_LANGUAGES = {
|
|
22
|
+
"python",
|
|
23
|
+
"py",
|
|
24
|
+
"javascript",
|
|
25
|
+
"js",
|
|
26
|
+
"json",
|
|
27
|
+
"html",
|
|
28
|
+
"sql",
|
|
29
|
+
"yaml",
|
|
30
|
+
"yml",
|
|
31
|
+
"bash",
|
|
32
|
+
"sh",
|
|
33
|
+
"shell",
|
|
34
|
+
"zsh",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Languages we skip validation for.
|
|
38
|
+
SKIP_LANGUAGES = {"toml", "nginx", "css", "text", ""}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class CodeBlock:
|
|
43
|
+
"""A single extracted code block."""
|
|
44
|
+
|
|
45
|
+
language: str
|
|
46
|
+
code: str
|
|
47
|
+
line_number: int # approximate line in the source markdown
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class ValidationResult:
|
|
52
|
+
"""Result of validating a single code block."""
|
|
53
|
+
|
|
54
|
+
block: CodeBlock
|
|
55
|
+
is_valid: bool
|
|
56
|
+
error: str = ""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ValidationReport:
|
|
61
|
+
"""Aggregated validation results for a piece of content."""
|
|
62
|
+
|
|
63
|
+
total_blocks: int = 0
|
|
64
|
+
validated: int = 0
|
|
65
|
+
passed: int = 0
|
|
66
|
+
failed: int = 0
|
|
67
|
+
skipped: int = 0
|
|
68
|
+
errors: list[ValidationResult] = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def all_passed(self) -> bool:
|
|
72
|
+
return self.failed == 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CodeValidator:
|
|
76
|
+
"""
|
|
77
|
+
Validates code blocks extracted from markdown content.
|
|
78
|
+
|
|
79
|
+
Performs syntax-level validation:
|
|
80
|
+
- Python: ast.parse() for syntax correctness
|
|
81
|
+
- JavaScript: checks balanced braces/brackets/parens and basic syntax
|
|
82
|
+
- JSON: json.loads() for valid JSON
|
|
83
|
+
- HTML: checks balanced tags
|
|
84
|
+
|
|
85
|
+
Does NOT execute code or check runtime correctness.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def extract_code_blocks(self, markdown: str) -> list[CodeBlock]:
|
|
89
|
+
"""Extract all fenced code blocks from markdown content."""
|
|
90
|
+
blocks = []
|
|
91
|
+
for match in CODE_BLOCK_RE.finditer(markdown):
|
|
92
|
+
lang = match.group(1).lower().strip()
|
|
93
|
+
code = match.group(2).strip()
|
|
94
|
+
# Approximate line number
|
|
95
|
+
line_num = markdown[: match.start()].count("\n") + 1
|
|
96
|
+
if code:
|
|
97
|
+
blocks.append(CodeBlock(language=lang, code=code, line_number=line_num))
|
|
98
|
+
return blocks
|
|
99
|
+
|
|
100
|
+
def validate_block(self, block: CodeBlock) -> ValidationResult:
|
|
101
|
+
"""Validate a single code block based on its language."""
|
|
102
|
+
lang = block.language
|
|
103
|
+
|
|
104
|
+
if lang in SKIP_LANGUAGES:
|
|
105
|
+
return ValidationResult(block=block, is_valid=True)
|
|
106
|
+
|
|
107
|
+
if lang in ("python", "py"):
|
|
108
|
+
return self._validate_python(block)
|
|
109
|
+
elif lang in ("javascript", "js"):
|
|
110
|
+
return self._validate_javascript(block)
|
|
111
|
+
elif lang == "json":
|
|
112
|
+
return self._validate_json(block)
|
|
113
|
+
elif lang == "html":
|
|
114
|
+
return self._validate_html(block)
|
|
115
|
+
elif lang == "sql":
|
|
116
|
+
return self._validate_sql(block)
|
|
117
|
+
elif lang in ("yaml", "yml"):
|
|
118
|
+
return self._validate_yaml(block)
|
|
119
|
+
elif lang in ("bash", "sh", "shell", "zsh"):
|
|
120
|
+
return self._validate_shell(block)
|
|
121
|
+
else:
|
|
122
|
+
# Unknown language — skip, don't fail
|
|
123
|
+
return ValidationResult(block=block, is_valid=True)
|
|
124
|
+
|
|
125
|
+
def validate_content(self, markdown: str) -> ValidationReport:
|
|
126
|
+
"""Validate all code blocks in a markdown document."""
|
|
127
|
+
blocks = self.extract_code_blocks(markdown)
|
|
128
|
+
report = ValidationReport(total_blocks=len(blocks))
|
|
129
|
+
|
|
130
|
+
for block in blocks:
|
|
131
|
+
if block.language in SKIP_LANGUAGES or block.language not in SUPPORTED_LANGUAGES:
|
|
132
|
+
report.skipped += 1
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
result = self.validate_block(block)
|
|
136
|
+
report.validated += 1
|
|
137
|
+
|
|
138
|
+
if result.is_valid:
|
|
139
|
+
report.passed += 1
|
|
140
|
+
else:
|
|
141
|
+
report.failed += 1
|
|
142
|
+
report.errors.append(result)
|
|
143
|
+
logger.warning(
|
|
144
|
+
f"Code validation failed (line ~{block.line_number}, "
|
|
145
|
+
f"{block.language}): {result.error}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return report
|
|
149
|
+
|
|
150
|
+
def _validate_python(self, block: CodeBlock) -> ValidationResult:
|
|
151
|
+
"""Validate Python syntax using ast.parse()."""
|
|
152
|
+
code = block.code
|
|
153
|
+
|
|
154
|
+
# Strip common non-parseable patterns that are valid in tutorials
|
|
155
|
+
# e.g., lines with "..." for ellipsis/placeholder
|
|
156
|
+
lines = code.splitlines()
|
|
157
|
+
cleaned_lines = []
|
|
158
|
+
for line in lines:
|
|
159
|
+
stripped = line.strip()
|
|
160
|
+
# Skip pure ellipsis lines (common in tutorial snippets)
|
|
161
|
+
if stripped == "...":
|
|
162
|
+
cleaned_lines.append(line.replace("...", "pass"))
|
|
163
|
+
# Skip lines that are just comments
|
|
164
|
+
elif stripped.startswith("#"):
|
|
165
|
+
cleaned_lines.append(line)
|
|
166
|
+
else:
|
|
167
|
+
cleaned_lines.append(line)
|
|
168
|
+
cleaned = "\n".join(cleaned_lines)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
ast.parse(cleaned)
|
|
172
|
+
return ValidationResult(block=block, is_valid=True)
|
|
173
|
+
except SyntaxError as e:
|
|
174
|
+
return ValidationResult(
|
|
175
|
+
block=block,
|
|
176
|
+
is_valid=False,
|
|
177
|
+
error=f"Python syntax error at line {e.lineno}: {e.msg}",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def _validate_javascript(self, block: CodeBlock) -> ValidationResult:
|
|
181
|
+
"""Basic JavaScript syntax validation — checks balanced delimiters and common errors."""
|
|
182
|
+
code = block.code
|
|
183
|
+
|
|
184
|
+
# Strip string literals and comments to avoid false positives
|
|
185
|
+
# Remove single-line comments
|
|
186
|
+
stripped = re.sub(r"//.*$", "", code, flags=re.MULTILINE)
|
|
187
|
+
# Remove multi-line comments
|
|
188
|
+
stripped = re.sub(r"/\*.*?\*/", "", stripped, flags=re.DOTALL)
|
|
189
|
+
# Remove template literals
|
|
190
|
+
stripped = re.sub(r"`[^`]*`", '""', stripped)
|
|
191
|
+
# Remove double-quoted strings
|
|
192
|
+
stripped = re.sub(r'"(?:[^"\\]|\\.)*"', '""', stripped)
|
|
193
|
+
# Remove single-quoted strings
|
|
194
|
+
stripped = re.sub(r"'(?:[^'\\]|\\.)*'", "''", stripped)
|
|
195
|
+
|
|
196
|
+
# Check balanced delimiters
|
|
197
|
+
stack = []
|
|
198
|
+
pairs = {")": "(", "]": "[", "}": "{"}
|
|
199
|
+
for i, ch in enumerate(stripped):
|
|
200
|
+
if ch in "({[":
|
|
201
|
+
stack.append(ch)
|
|
202
|
+
elif ch in ")}]":
|
|
203
|
+
if not stack:
|
|
204
|
+
return ValidationResult(
|
|
205
|
+
block=block,
|
|
206
|
+
is_valid=False,
|
|
207
|
+
error=f"Unmatched closing '{ch}' at position {i}",
|
|
208
|
+
)
|
|
209
|
+
if stack[-1] != pairs[ch]:
|
|
210
|
+
return ValidationResult(
|
|
211
|
+
block=block,
|
|
212
|
+
is_valid=False,
|
|
213
|
+
error=f"Mismatched '{stack[-1]}' and '{ch}' at position {i}",
|
|
214
|
+
)
|
|
215
|
+
stack.pop()
|
|
216
|
+
|
|
217
|
+
if stack:
|
|
218
|
+
return ValidationResult(
|
|
219
|
+
block=block,
|
|
220
|
+
is_valid=False,
|
|
221
|
+
error=f"Unclosed delimiter: '{stack[-1]}'",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return ValidationResult(block=block, is_valid=True)
|
|
225
|
+
|
|
226
|
+
def _validate_yaml(self, block: CodeBlock) -> ValidationResult:
|
|
227
|
+
"""Validate YAML syntax and flag stale GitHub Actions examples."""
|
|
228
|
+
if re.search(r"actions/(?:upload|download)-artifact@v3\b", block.code):
|
|
229
|
+
return ValidationResult(
|
|
230
|
+
block=block,
|
|
231
|
+
is_valid=False,
|
|
232
|
+
error="Deprecated GitHub Actions artifact action v3; use v4.",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
import yaml
|
|
237
|
+
except ImportError:
|
|
238
|
+
return ValidationResult(
|
|
239
|
+
block=block,
|
|
240
|
+
is_valid=False,
|
|
241
|
+
error="PyYAML is required for YAML validation.",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
yaml.safe_load(block.code)
|
|
246
|
+
return ValidationResult(block=block, is_valid=True)
|
|
247
|
+
except yaml.YAMLError as e:
|
|
248
|
+
return ValidationResult(block=block, is_valid=False, error=f"Invalid YAML: {e}")
|
|
249
|
+
|
|
250
|
+
def _validate_shell(self, block: CodeBlock) -> ValidationResult:
|
|
251
|
+
"""Validate shell snippets for parseability and obvious destructive commands."""
|
|
252
|
+
pending = ""
|
|
253
|
+
pending_start = 0
|
|
254
|
+
for offset, line in enumerate(block.code.splitlines(), start=1):
|
|
255
|
+
stripped = line.strip()
|
|
256
|
+
if not pending and (not stripped or stripped.startswith("#")):
|
|
257
|
+
continue
|
|
258
|
+
|
|
259
|
+
if re.search(r"\brm\s+-[rfRf-]*\s+/(?:\s|$)", stripped):
|
|
260
|
+
return ValidationResult(
|
|
261
|
+
block=block,
|
|
262
|
+
is_valid=False,
|
|
263
|
+
error=f"Unsafe shell command at line {offset}: refuses to delete root.",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if pending:
|
|
267
|
+
current = f"{pending}\n{stripped}"
|
|
268
|
+
start = pending_start
|
|
269
|
+
else:
|
|
270
|
+
current = stripped
|
|
271
|
+
start = offset
|
|
272
|
+
|
|
273
|
+
if current.rstrip().endswith("\\"):
|
|
274
|
+
pending = current.rstrip()[:-1].rstrip() + " "
|
|
275
|
+
pending_start = start
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
shlex.split(current)
|
|
280
|
+
except ValueError as e:
|
|
281
|
+
if "No closing quotation" in str(e):
|
|
282
|
+
pending = current
|
|
283
|
+
pending_start = start
|
|
284
|
+
continue
|
|
285
|
+
return ValidationResult(
|
|
286
|
+
block=block,
|
|
287
|
+
is_valid=False,
|
|
288
|
+
error=f"Shell parse error at line {start}: {e}",
|
|
289
|
+
)
|
|
290
|
+
pending = ""
|
|
291
|
+
pending_start = 0
|
|
292
|
+
|
|
293
|
+
if pending:
|
|
294
|
+
try:
|
|
295
|
+
shlex.split(pending)
|
|
296
|
+
except ValueError as e:
|
|
297
|
+
return ValidationResult(
|
|
298
|
+
block=block,
|
|
299
|
+
is_valid=False,
|
|
300
|
+
error=f"Shell parse error at line {pending_start}: {e}",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return ValidationResult(block=block, is_valid=True)
|
|
304
|
+
|
|
305
|
+
def _validate_json(self, block: CodeBlock) -> ValidationResult:
|
|
306
|
+
"""Validate JSON using json.loads()."""
|
|
307
|
+
try:
|
|
308
|
+
json.loads(block.code)
|
|
309
|
+
return ValidationResult(block=block, is_valid=True)
|
|
310
|
+
except json.JSONDecodeError as e:
|
|
311
|
+
return ValidationResult(
|
|
312
|
+
block=block,
|
|
313
|
+
is_valid=False,
|
|
314
|
+
error=f"Invalid JSON at line {e.lineno}, col {e.colno}: {e.msg}",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
def _validate_html(self, block: CodeBlock) -> ValidationResult:
|
|
318
|
+
"""Basic HTML validation — checks that opened tags are closed."""
|
|
319
|
+
from html.parser import HTMLParser
|
|
320
|
+
|
|
321
|
+
class TagChecker(HTMLParser):
|
|
322
|
+
def __init__(self):
|
|
323
|
+
super().__init__()
|
|
324
|
+
self.stack: list[str] = []
|
|
325
|
+
self.error: str = ""
|
|
326
|
+
# Void elements that don't need closing tags
|
|
327
|
+
self.void_tags = {
|
|
328
|
+
"area",
|
|
329
|
+
"base",
|
|
330
|
+
"br",
|
|
331
|
+
"col",
|
|
332
|
+
"embed",
|
|
333
|
+
"hr",
|
|
334
|
+
"img",
|
|
335
|
+
"input",
|
|
336
|
+
"link",
|
|
337
|
+
"meta",
|
|
338
|
+
"param",
|
|
339
|
+
"source",
|
|
340
|
+
"track",
|
|
341
|
+
"wbr",
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
def handle_starttag(self, tag, _attrs):
|
|
345
|
+
# _attrs is required by HTMLParser's interface but unused here.
|
|
346
|
+
if tag.lower() not in self.void_tags:
|
|
347
|
+
self.stack.append(tag.lower())
|
|
348
|
+
|
|
349
|
+
def handle_endtag(self, tag):
|
|
350
|
+
tag = tag.lower()
|
|
351
|
+
if tag in self.void_tags:
|
|
352
|
+
return
|
|
353
|
+
if not self.stack:
|
|
354
|
+
self.error = f"Unexpected closing tag </{tag}>"
|
|
355
|
+
elif self.stack[-1] != tag:
|
|
356
|
+
self.error = f"Mismatched tags: expected </{self.stack[-1]}>, got </{tag}>"
|
|
357
|
+
else:
|
|
358
|
+
self.stack.pop()
|
|
359
|
+
|
|
360
|
+
checker = TagChecker()
|
|
361
|
+
try:
|
|
362
|
+
checker.feed(block.code)
|
|
363
|
+
except Exception as e:
|
|
364
|
+
return ValidationResult(block=block, is_valid=False, error=f"HTML parse error: {e}")
|
|
365
|
+
|
|
366
|
+
if checker.error:
|
|
367
|
+
return ValidationResult(block=block, is_valid=False, error=checker.error)
|
|
368
|
+
if checker.stack:
|
|
369
|
+
return ValidationResult(
|
|
370
|
+
block=block,
|
|
371
|
+
is_valid=False,
|
|
372
|
+
error=f"Unclosed tags: {', '.join(f'<{t}>' for t in checker.stack)}",
|
|
373
|
+
)
|
|
374
|
+
return ValidationResult(block=block, is_valid=True)
|
|
375
|
+
|
|
376
|
+
def _validate_sql(self, block: CodeBlock) -> ValidationResult:
|
|
377
|
+
"""Basic SQL validation — checks balanced parentheses and statement structure."""
|
|
378
|
+
code = block.code.strip()
|
|
379
|
+
|
|
380
|
+
# Check balanced parentheses
|
|
381
|
+
depth = 0
|
|
382
|
+
for ch in code:
|
|
383
|
+
if ch == "(":
|
|
384
|
+
depth += 1
|
|
385
|
+
elif ch == ")":
|
|
386
|
+
depth -= 1
|
|
387
|
+
if depth < 0:
|
|
388
|
+
return ValidationResult(
|
|
389
|
+
block=block, is_valid=False, error="Unmatched closing parenthesis"
|
|
390
|
+
)
|
|
391
|
+
if depth != 0:
|
|
392
|
+
return ValidationResult(
|
|
393
|
+
block=block, is_valid=False, error="Unclosed parenthesis in SQL"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Check that it starts with a known SQL keyword
|
|
397
|
+
first_word = code.split()[0].upper() if code.split() else ""
|
|
398
|
+
sql_keywords = {
|
|
399
|
+
"SELECT",
|
|
400
|
+
"INSERT",
|
|
401
|
+
"UPDATE",
|
|
402
|
+
"DELETE",
|
|
403
|
+
"CREATE",
|
|
404
|
+
"ALTER",
|
|
405
|
+
"DROP",
|
|
406
|
+
"WITH",
|
|
407
|
+
"EXPLAIN",
|
|
408
|
+
"SET",
|
|
409
|
+
"SYSTEM",
|
|
410
|
+
"GRANT",
|
|
411
|
+
"REVOKE",
|
|
412
|
+
"BEGIN",
|
|
413
|
+
"COMMIT",
|
|
414
|
+
"ROLLBACK",
|
|
415
|
+
"TRUNCATE",
|
|
416
|
+
"MERGE",
|
|
417
|
+
"CALL",
|
|
418
|
+
"DECLARE",
|
|
419
|
+
"--",
|
|
420
|
+
}
|
|
421
|
+
if first_word and first_word not in sql_keywords:
|
|
422
|
+
return ValidationResult(
|
|
423
|
+
block=block,
|
|
424
|
+
is_valid=False,
|
|
425
|
+
error=f"SQL doesn't start with a known keyword: '{first_word}'",
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
return ValidationResult(block=block, is_valid=True)
|