empathy-framework 4.8.0__py3-none-any.whl → 4.9.0__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.
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/METADATA +1 -1
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/RECORD +27 -38
- empathy_os/cache/hash_only.py +3 -6
- empathy_os/cache/hybrid.py +3 -6
- empathy_os/cli_legacy.py +1 -27
- empathy_os/cli_unified.py +0 -25
- empathy_os/memory/__init__.py +5 -19
- empathy_os/memory/short_term.py +132 -10
- empathy_os/memory/types.py +4 -0
- empathy_os/models/registry.py +4 -4
- empathy_os/project_index/scanner.py +3 -2
- empathy_os/socratic/ab_testing.py +1 -1
- empathy_os/workflow_commands.py +9 -9
- empathy_os/workflows/__init__.py +4 -4
- empathy_os/workflows/base.py +8 -54
- empathy_os/workflows/bug_predict.py +2 -2
- empathy_os/workflows/history.py +5 -3
- empathy_os/workflows/perf_audit.py +4 -4
- empathy_os/workflows/progress.py +22 -324
- empathy_os/workflows/routing.py +0 -5
- empathy_os/workflows/security_audit.py +0 -189
- empathy_os/workflows/security_audit_phase3.py +26 -2
- empathy_os/workflows/test_gen.py +7 -7
- empathy_os/vscode_bridge 2.py +0 -173
- empathy_os/workflows/output.py +0 -410
- empathy_os/workflows/progressive/README 2.md +0 -454
- empathy_os/workflows/progressive/__init__ 2.py +0 -92
- empathy_os/workflows/progressive/cli 2.py +0 -242
- empathy_os/workflows/progressive/core 2.py +0 -488
- empathy_os/workflows/progressive/orchestrator 2.py +0 -701
- empathy_os/workflows/progressive/reports 2.py +0 -528
- empathy_os/workflows/progressive/telemetry 2.py +0 -280
- empathy_os/workflows/progressive/test_gen 2.py +0 -514
- empathy_os/workflows/progressive/workflow 2.py +0 -628
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/WHEEL +0 -0
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/entry_points.txt +0 -0
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/licenses/LICENSE +0 -0
- {empathy_framework-4.8.0.dist-info → empathy_framework-4.9.0.dist-info}/top_level.txt +0 -0
|
@@ -342,29 +342,11 @@ class SecurityAuditWorkflow(BaseWorkflow):
|
|
|
342
342
|
if self._is_detection_code(line_content, match.group()):
|
|
343
343
|
continue
|
|
344
344
|
|
|
345
|
-
# Phase 2: Skip safe SQL parameterization patterns
|
|
346
|
-
if vuln_type == "sql_injection":
|
|
347
|
-
if self._is_safe_sql_parameterization(
|
|
348
|
-
line_content,
|
|
349
|
-
match.group(),
|
|
350
|
-
content,
|
|
351
|
-
):
|
|
352
|
-
continue
|
|
353
|
-
|
|
354
345
|
# Skip fake/test credentials
|
|
355
346
|
if vuln_type == "hardcoded_secret":
|
|
356
347
|
if self._is_fake_credential(match.group()):
|
|
357
348
|
continue
|
|
358
349
|
|
|
359
|
-
# Phase 2: Skip safe random usage (tests, demos, documented)
|
|
360
|
-
if vuln_type == "insecure_random":
|
|
361
|
-
if self._is_safe_random_usage(
|
|
362
|
-
line_content,
|
|
363
|
-
file_name,
|
|
364
|
-
content,
|
|
365
|
-
):
|
|
366
|
-
continue
|
|
367
|
-
|
|
368
350
|
# Skip command_injection in documentation strings
|
|
369
351
|
if vuln_type == "command_injection":
|
|
370
352
|
if self._is_documentation_or_string(
|
|
@@ -398,29 +380,6 @@ class SecurityAuditWorkflow(BaseWorkflow):
|
|
|
398
380
|
except OSError:
|
|
399
381
|
continue
|
|
400
382
|
|
|
401
|
-
# Phase 3: Apply AST-based filtering for command injection
|
|
402
|
-
try:
|
|
403
|
-
from .security_audit_phase3 import apply_phase3_filtering
|
|
404
|
-
|
|
405
|
-
# Separate command injection findings
|
|
406
|
-
cmd_findings = [f for f in findings if f["type"] == "command_injection"]
|
|
407
|
-
other_findings = [f for f in findings if f["type"] != "command_injection"]
|
|
408
|
-
|
|
409
|
-
# Apply Phase 3 filtering to command injection
|
|
410
|
-
filtered_cmd = apply_phase3_filtering(cmd_findings)
|
|
411
|
-
|
|
412
|
-
# Combine back
|
|
413
|
-
findings = other_findings + filtered_cmd
|
|
414
|
-
|
|
415
|
-
logger.info(
|
|
416
|
-
f"Phase 3: Filtered command_injection from {len(cmd_findings)} to {len(filtered_cmd)} "
|
|
417
|
-
f"({len(cmd_findings) - len(filtered_cmd)} false positives removed)"
|
|
418
|
-
)
|
|
419
|
-
except ImportError:
|
|
420
|
-
logger.debug("Phase 3 module not available, skipping AST-based filtering")
|
|
421
|
-
except Exception as e:
|
|
422
|
-
logger.warning(f"Phase 3 filtering failed: {e}")
|
|
423
|
-
|
|
424
383
|
input_tokens = len(str(input_data)) // 4
|
|
425
384
|
output_tokens = len(str(findings)) // 4
|
|
426
385
|
|
|
@@ -582,154 +541,6 @@ class SecurityAuditWorkflow(BaseWorkflow):
|
|
|
582
541
|
|
|
583
542
|
return False
|
|
584
543
|
|
|
585
|
-
def _is_safe_sql_parameterization(self, line_content: str, match_text: str, file_content: str) -> bool:
|
|
586
|
-
"""Check if SQL query uses safe parameterization despite f-string usage.
|
|
587
|
-
|
|
588
|
-
Phase 2 Enhancement: Detects safe patterns like:
|
|
589
|
-
- placeholders = ",".join("?" * len(ids))
|
|
590
|
-
- cursor.execute(f"... IN ({placeholders})", ids)
|
|
591
|
-
|
|
592
|
-
This prevents false positives for the SQLite-recommended pattern
|
|
593
|
-
of building dynamic placeholder strings.
|
|
594
|
-
|
|
595
|
-
Args:
|
|
596
|
-
line_content: The line containing the match (may be incomplete for multi-line)
|
|
597
|
-
match_text: The matched text
|
|
598
|
-
file_content: Full file content for context analysis
|
|
599
|
-
|
|
600
|
-
Returns:
|
|
601
|
-
True if this is safe parameterized SQL, False otherwise
|
|
602
|
-
"""
|
|
603
|
-
# Get the position of the match in the full file content
|
|
604
|
-
match_pos = file_content.find(match_text)
|
|
605
|
-
if match_pos == -1:
|
|
606
|
-
# Try to find cursor.execute
|
|
607
|
-
match_pos = file_content.find("cursor.execute")
|
|
608
|
-
if match_pos == -1:
|
|
609
|
-
return False
|
|
610
|
-
|
|
611
|
-
# Extract a larger context (next 200 chars after match)
|
|
612
|
-
context = file_content[match_pos:match_pos + 200]
|
|
613
|
-
|
|
614
|
-
# Also get lines before the match for placeholder detection
|
|
615
|
-
lines_before = file_content[:match_pos].split("\n")
|
|
616
|
-
recent_lines = lines_before[-10:] if len(lines_before) > 10 else lines_before
|
|
617
|
-
|
|
618
|
-
# Pattern 1: Check if this is a placeholder-based parameterized query
|
|
619
|
-
# Look for: cursor.execute(f"... IN ({placeholders})", params)
|
|
620
|
-
if "placeholders" in context or any("placeholders" in line for line in recent_lines[-5:]):
|
|
621
|
-
# Check if context has both f-string and separate parameters
|
|
622
|
-
# Pattern: f"...{placeholders}..." followed by comma and params
|
|
623
|
-
if re.search(r'f["\'][^"\']*\{placeholders\}[^"\']*["\']\s*,\s*\w+', context):
|
|
624
|
-
return True # Safe - has separate parameters
|
|
625
|
-
|
|
626
|
-
# Also check if recent lines built the placeholders
|
|
627
|
-
for prev_line in reversed(recent_lines):
|
|
628
|
-
if "placeholders" in prev_line and '"?"' in prev_line and "join" in prev_line:
|
|
629
|
-
# Found placeholder construction
|
|
630
|
-
# Now check if the execute has separate parameters
|
|
631
|
-
if "," in context and any(param in context for param in ["run_ids", "ids", "params", "values", ")"]):
|
|
632
|
-
return True
|
|
633
|
-
|
|
634
|
-
# Pattern 2: Check if f-string only builds SQL structure with constants
|
|
635
|
-
# Example: f"SELECT * FROM {TABLE_NAME}" where TABLE_NAME is a constant
|
|
636
|
-
f_string_vars = re.findall(r'\{(\w+)\}', context)
|
|
637
|
-
if f_string_vars:
|
|
638
|
-
# Check if all variables are constants (UPPERCASE or table/column names)
|
|
639
|
-
all_constants = all(
|
|
640
|
-
var.isupper() or "TABLE" in var.upper() or "COLUMN" in var.upper()
|
|
641
|
-
for var in f_string_vars
|
|
642
|
-
)
|
|
643
|
-
if all_constants:
|
|
644
|
-
return True # Safe - using constants, not user data
|
|
645
|
-
|
|
646
|
-
# Pattern 3: Check for security note comments nearby
|
|
647
|
-
# If developers added security notes, it's likely safe
|
|
648
|
-
for prev_line in reversed(recent_lines[-3:]):
|
|
649
|
-
if "security note" in prev_line.lower() and "safe" in prev_line.lower():
|
|
650
|
-
return True
|
|
651
|
-
|
|
652
|
-
return False
|
|
653
|
-
|
|
654
|
-
def _is_safe_random_usage(self, line_content: str, file_path: str, file_content: str) -> bool:
|
|
655
|
-
"""Check if random usage is in a safe context (tests, simulations, non-crypto).
|
|
656
|
-
|
|
657
|
-
Phase 2 Enhancement: Reduces false positives for random module usage
|
|
658
|
-
in test fixtures, A/B testing simulations, and demo code.
|
|
659
|
-
|
|
660
|
-
Args:
|
|
661
|
-
line_content: The line containing the match
|
|
662
|
-
file_path: Path to the file being scanned
|
|
663
|
-
file_content: Full file content for context analysis
|
|
664
|
-
|
|
665
|
-
Returns:
|
|
666
|
-
True if random usage is safe/documented, False if potentially insecure
|
|
667
|
-
"""
|
|
668
|
-
# Check if file is a test file
|
|
669
|
-
is_test = any(pattern in file_path.lower() for pattern in ["/test", "test_", "conftest"])
|
|
670
|
-
|
|
671
|
-
# Check for explicit security notes nearby
|
|
672
|
-
lines = file_content.split("\n")
|
|
673
|
-
line_index = None
|
|
674
|
-
for i, line in enumerate(lines):
|
|
675
|
-
if line_content.strip() in line:
|
|
676
|
-
line_index = i
|
|
677
|
-
break
|
|
678
|
-
|
|
679
|
-
if line_index is not None:
|
|
680
|
-
# Check 5 lines before and after for security notes
|
|
681
|
-
context_start = max(0, line_index - 5)
|
|
682
|
-
context_end = min(len(lines), line_index + 5)
|
|
683
|
-
context = "\n".join(lines[context_start:context_end]).lower()
|
|
684
|
-
|
|
685
|
-
# Look for clarifying comments
|
|
686
|
-
safe_indicators = [
|
|
687
|
-
"security note",
|
|
688
|
-
"not cryptographic",
|
|
689
|
-
"not for crypto",
|
|
690
|
-
"test data",
|
|
691
|
-
"demo data",
|
|
692
|
-
"simulation",
|
|
693
|
-
"reproducible",
|
|
694
|
-
"deterministic",
|
|
695
|
-
"fixed seed",
|
|
696
|
-
"not used for security",
|
|
697
|
-
"not used for secrets",
|
|
698
|
-
"not used for tokens",
|
|
699
|
-
]
|
|
700
|
-
|
|
701
|
-
if any(indicator in context for indicator in safe_indicators):
|
|
702
|
-
return True # Documented as safe
|
|
703
|
-
|
|
704
|
-
# Check for common safe random patterns
|
|
705
|
-
line_lower = line_content.lower()
|
|
706
|
-
|
|
707
|
-
# Pattern 1: Fixed seed (reproducible tests)
|
|
708
|
-
if "random.seed(" in line_lower:
|
|
709
|
-
return True # Fixed seed is for reproducibility, not security
|
|
710
|
-
|
|
711
|
-
# Pattern 2: A/B testing, simulations, demos
|
|
712
|
-
safe_contexts = [
|
|
713
|
-
"simulation",
|
|
714
|
-
"demo",
|
|
715
|
-
"a/b test",
|
|
716
|
-
"ab_test",
|
|
717
|
-
"fixture",
|
|
718
|
-
"mock",
|
|
719
|
-
"example",
|
|
720
|
-
"sample",
|
|
721
|
-
]
|
|
722
|
-
if any(context in file_path.lower() for context in safe_contexts):
|
|
723
|
-
return True
|
|
724
|
-
|
|
725
|
-
# If it's a test file without crypto indicators, it's probably safe
|
|
726
|
-
if is_test:
|
|
727
|
-
crypto_indicators = ["password", "secret", "token", "key", "crypto", "auth"]
|
|
728
|
-
if not any(indicator in file_path.lower() for indicator in crypto_indicators):
|
|
729
|
-
return True
|
|
730
|
-
|
|
731
|
-
return False
|
|
732
|
-
|
|
733
544
|
async def _assess(self, input_data: dict, tier: ModelTier) -> tuple[dict, int, int]:
|
|
734
545
|
"""Risk scoring and severity classification.
|
|
735
546
|
|
|
@@ -222,11 +222,31 @@ def enhanced_command_injection_detection(
|
|
|
222
222
|
if is_scanner_implementation_file(file_path):
|
|
223
223
|
return [] # Scanner files are allowed to mention eval/exec
|
|
224
224
|
|
|
225
|
-
# Step 2: For Python files, use AST-based detection
|
|
225
|
+
# Step 2: For Python files, use AST-based detection for eval/exec only
|
|
226
|
+
# Keep subprocess findings from regex detection
|
|
226
227
|
if file_path.endswith(".py"):
|
|
227
228
|
try:
|
|
229
|
+
# Separate eval/exec findings from subprocess/os.system findings
|
|
230
|
+
# Eval/exec findings will be replaced with AST-based findings
|
|
231
|
+
# Subprocess/os.system findings will be kept from regex detection
|
|
232
|
+
eval_exec_findings = []
|
|
233
|
+
subprocess_findings = []
|
|
234
|
+
|
|
235
|
+
for finding in original_findings:
|
|
236
|
+
match_text = finding.get("match", "").lower()
|
|
237
|
+
if "eval" in match_text or "exec" in match_text:
|
|
238
|
+
eval_exec_findings.append(finding)
|
|
239
|
+
else:
|
|
240
|
+
# subprocess, os.system, or other command injection patterns
|
|
241
|
+
subprocess_findings.append(finding)
|
|
242
|
+
|
|
243
|
+
# Use AST to validate eval/exec findings (reduces false positives)
|
|
228
244
|
ast_findings = analyze_file_for_eval_exec(file_path)
|
|
229
245
|
|
|
246
|
+
# Check if this is a test file (downgrade severity)
|
|
247
|
+
from .security_audit import TEST_FILE_PATTERNS
|
|
248
|
+
is_test_file = any(re.search(pat, file_path) for pat in TEST_FILE_PATTERNS)
|
|
249
|
+
|
|
230
250
|
# Convert AST findings to format compatible with original
|
|
231
251
|
filtered = []
|
|
232
252
|
for finding in ast_findings:
|
|
@@ -235,11 +255,15 @@ def enhanced_command_injection_detection(
|
|
|
235
255
|
"file": file_path,
|
|
236
256
|
"line": finding["line"],
|
|
237
257
|
"match": f"{finding['function']}(",
|
|
238
|
-
"severity": "critical",
|
|
258
|
+
"severity": "low" if is_test_file else "critical",
|
|
239
259
|
"owasp": "A03:2021 Injection",
|
|
240
260
|
"context": finding.get("context", ""),
|
|
261
|
+
"is_test": is_test_file,
|
|
241
262
|
})
|
|
242
263
|
|
|
264
|
+
# Keep subprocess/os.system findings (not filtered by AST)
|
|
265
|
+
filtered.extend(subprocess_findings)
|
|
266
|
+
|
|
243
267
|
return filtered
|
|
244
268
|
|
|
245
269
|
except Exception as e:
|
empathy_os/workflows/test_gen.py
CHANGED
|
@@ -597,8 +597,8 @@ class TestGenerationWorkflow(BaseWorkflow):
|
|
|
597
597
|
{
|
|
598
598
|
"candidates": candidates[:max_candidates],
|
|
599
599
|
"total_candidates": len(candidates),
|
|
600
|
-
"hotspot_count":
|
|
601
|
-
"untested_count":
|
|
600
|
+
"hotspot_count": sum(1 for c in candidates if c["is_hotspot"]),
|
|
601
|
+
"untested_count": sum(1 for c in candidates if not c["has_tests"]),
|
|
602
602
|
# Scope awareness fields for enterprise reporting
|
|
603
603
|
"total_source_files": total_source_files,
|
|
604
604
|
"existing_test_files": existing_test_files,
|
|
@@ -1503,13 +1503,13 @@ END OF REQUIRED FORMAT - output nothing after recommendations."""
|
|
|
1503
1503
|
lines.append(f"| **Total Test Functions** | {total_test_count} |")
|
|
1504
1504
|
lines.append(f"| **Files Covered** | {len(generated_tests)} |")
|
|
1505
1505
|
|
|
1506
|
-
# Count classes and functions
|
|
1506
|
+
# Count classes and functions (generator expressions for memory efficiency)
|
|
1507
1507
|
total_classes = sum(
|
|
1508
|
-
|
|
1508
|
+
sum(1 for t in item.get("tests", []) if t.get("type") == "class")
|
|
1509
1509
|
for item in generated_tests
|
|
1510
1510
|
)
|
|
1511
1511
|
total_functions = sum(
|
|
1512
|
-
|
|
1512
|
+
sum(1 for t in item.get("tests", []) if t.get("type") == "function")
|
|
1513
1513
|
for item in generated_tests
|
|
1514
1514
|
)
|
|
1515
1515
|
lines.append(f"| **Classes Tested** | {total_classes} |")
|
|
@@ -1799,8 +1799,8 @@ def format_test_gen_report(result: dict, input_data: dict) -> str:
|
|
|
1799
1799
|
lines.append("NEXT STEPS")
|
|
1800
1800
|
lines.append("-" * 60)
|
|
1801
1801
|
|
|
1802
|
-
high_findings =
|
|
1803
|
-
medium_findings =
|
|
1802
|
+
high_findings = sum(1 for f in xml_findings if f["severity"] == "high")
|
|
1803
|
+
medium_findings = sum(1 for f in xml_findings if f["severity"] == "medium")
|
|
1804
1804
|
|
|
1805
1805
|
if high_findings > 0:
|
|
1806
1806
|
lines.append(f" 🔴 Address {high_findings} high-priority finding(s) first")
|
empathy_os/vscode_bridge 2.py
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
"""VS Code Extension Bridge
|
|
2
|
-
|
|
3
|
-
Provides functions to write data that the VS Code extension can pick up.
|
|
4
|
-
Enables Claude Code CLI output to appear in VS Code webview panels.
|
|
5
|
-
|
|
6
|
-
Copyright 2026 Smart-AI-Memory
|
|
7
|
-
Licensed under Fair Source License 0.9
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import json
|
|
11
|
-
from dataclasses import asdict, dataclass
|
|
12
|
-
from datetime import datetime
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@dataclass
|
|
18
|
-
class ReviewFinding:
|
|
19
|
-
"""A code review finding."""
|
|
20
|
-
|
|
21
|
-
id: str
|
|
22
|
-
file: str
|
|
23
|
-
line: int
|
|
24
|
-
severity: str # 'critical' | 'high' | 'medium' | 'low' | 'info'
|
|
25
|
-
category: str # 'security' | 'performance' | 'maintainability' | 'style' | 'correctness'
|
|
26
|
-
message: str
|
|
27
|
-
column: int = 1
|
|
28
|
-
details: str | None = None
|
|
29
|
-
recommendation: str | None = None
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass
|
|
33
|
-
class CodeReviewResult:
|
|
34
|
-
"""Code review results for VS Code bridge."""
|
|
35
|
-
|
|
36
|
-
findings: list[dict[str, Any]]
|
|
37
|
-
summary: dict[str, Any]
|
|
38
|
-
verdict: str # 'approve' | 'approve_with_suggestions' | 'request_changes' | 'reject'
|
|
39
|
-
security_score: int
|
|
40
|
-
formatted_report: str
|
|
41
|
-
model_tier_used: str
|
|
42
|
-
timestamp: str
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def get_empathy_dir() -> Path:
|
|
46
|
-
"""Get the .empathy directory, creating if needed."""
|
|
47
|
-
empathy_dir = Path(".empathy")
|
|
48
|
-
empathy_dir.mkdir(exist_ok=True)
|
|
49
|
-
return empathy_dir
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def write_code_review_results(
|
|
53
|
-
findings: list[dict[str, Any]] | None = None,
|
|
54
|
-
summary: dict[str, Any] | None = None,
|
|
55
|
-
verdict: str = "approve_with_suggestions",
|
|
56
|
-
security_score: int = 85,
|
|
57
|
-
formatted_report: str = "",
|
|
58
|
-
model_tier_used: str = "capable",
|
|
59
|
-
) -> Path:
|
|
60
|
-
"""Write code review results for VS Code extension to pick up.
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
findings: List of finding dicts with keys: id, file, line, severity, category, message
|
|
64
|
-
summary: Summary dict with keys: total_findings, by_severity, by_category, files_affected
|
|
65
|
-
verdict: One of 'approve', 'approve_with_suggestions', 'request_changes', 'reject'
|
|
66
|
-
security_score: 0-100 score
|
|
67
|
-
formatted_report: Markdown formatted report
|
|
68
|
-
model_tier_used: 'cheap', 'capable', or 'premium'
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
Path to the written file
|
|
72
|
-
"""
|
|
73
|
-
findings = findings or []
|
|
74
|
-
|
|
75
|
-
# Build summary if not provided
|
|
76
|
-
if summary is None:
|
|
77
|
-
by_severity: dict[str, int] = {}
|
|
78
|
-
by_category: dict[str, int] = {}
|
|
79
|
-
files_affected: set[str] = set()
|
|
80
|
-
|
|
81
|
-
for f in findings:
|
|
82
|
-
sev = f.get("severity", "info")
|
|
83
|
-
cat = f.get("category", "correctness")
|
|
84
|
-
by_severity[sev] = by_severity.get(sev, 0) + 1
|
|
85
|
-
by_category[cat] = by_category.get(cat, 0) + 1
|
|
86
|
-
if f.get("file"):
|
|
87
|
-
files_affected.add(f["file"])
|
|
88
|
-
|
|
89
|
-
summary = {
|
|
90
|
-
"total_findings": len(findings),
|
|
91
|
-
"by_severity": by_severity,
|
|
92
|
-
"by_category": by_category,
|
|
93
|
-
"files_affected": list(files_affected),
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
result = CodeReviewResult(
|
|
97
|
-
findings=findings,
|
|
98
|
-
summary=summary,
|
|
99
|
-
verdict=verdict,
|
|
100
|
-
security_score=security_score,
|
|
101
|
-
formatted_report=formatted_report,
|
|
102
|
-
model_tier_used=model_tier_used,
|
|
103
|
-
timestamp=datetime.now().isoformat(),
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
output_path = get_empathy_dir() / "code-review-results.json"
|
|
107
|
-
|
|
108
|
-
with open(output_path, "w") as f:
|
|
109
|
-
json.dump(asdict(result), f, indent=2)
|
|
110
|
-
|
|
111
|
-
return output_path
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def write_pr_review_results(
|
|
115
|
-
pr_number: int | str,
|
|
116
|
-
title: str,
|
|
117
|
-
findings: list[dict[str, Any]],
|
|
118
|
-
verdict: str = "approve_with_suggestions",
|
|
119
|
-
summary_text: str = "",
|
|
120
|
-
) -> Path:
|
|
121
|
-
"""Write PR review results for VS Code extension.
|
|
122
|
-
|
|
123
|
-
Convenience wrapper for PR reviews from GitHub.
|
|
124
|
-
|
|
125
|
-
Args:
|
|
126
|
-
pr_number: The PR number
|
|
127
|
-
title: PR title
|
|
128
|
-
findings: List of review findings
|
|
129
|
-
verdict: Review verdict
|
|
130
|
-
summary_text: Summary of the review
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
Path to the written file
|
|
134
|
-
"""
|
|
135
|
-
formatted_report = f"""## PR #{pr_number}: {title}
|
|
136
|
-
|
|
137
|
-
{summary_text}
|
|
138
|
-
|
|
139
|
-
### Findings ({len(findings)})
|
|
140
|
-
|
|
141
|
-
"""
|
|
142
|
-
for f in findings:
|
|
143
|
-
formatted_report += f"- **{f.get('severity', 'info').upper()}** [{f.get('file', 'unknown')}:{f.get('line', 0)}]: {f.get('message', '')}\n"
|
|
144
|
-
|
|
145
|
-
return write_code_review_results(
|
|
146
|
-
findings=findings,
|
|
147
|
-
verdict=verdict,
|
|
148
|
-
formatted_report=formatted_report,
|
|
149
|
-
model_tier_used="capable",
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# Quick helper for Claude Code to call
|
|
154
|
-
def send_to_vscode(
|
|
155
|
-
message: str,
|
|
156
|
-
findings: list[dict[str, Any]] | None = None,
|
|
157
|
-
verdict: str = "approve_with_suggestions",
|
|
158
|
-
) -> str:
|
|
159
|
-
"""Quick helper to send review results to VS Code.
|
|
160
|
-
|
|
161
|
-
Usage in Claude Code:
|
|
162
|
-
from empathy_os.vscode_bridge import send_to_vscode
|
|
163
|
-
send_to_vscode("Review complete", findings=[...])
|
|
164
|
-
|
|
165
|
-
Returns:
|
|
166
|
-
Confirmation message
|
|
167
|
-
"""
|
|
168
|
-
path = write_code_review_results(
|
|
169
|
-
findings=findings or [],
|
|
170
|
-
formatted_report=message,
|
|
171
|
-
verdict=verdict,
|
|
172
|
-
)
|
|
173
|
-
return f"Results written to {path} - VS Code will update automatically"
|