claude-self-reflect 3.3.0 ā 3.3.1
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/claude-self-reflect-test.md +426 -11
- package/installer/cli.js +16 -0
- package/installer/postinstall.js +14 -0
- package/installer/statusline-setup.js +289 -0
- package/mcp-server/run-mcp.sh +28 -4
- package/mcp-server/src/parallel_search.py +16 -82
- package/mcp-server/src/reflection_tools.py +13 -8
- package/mcp-server/src/search_tools.py +90 -42
- package/mcp-server/src/temporal_tools.py +10 -3
- package/package.json +6 -1
- package/scripts/ast_grep_final_analyzer.py +325 -0
- package/scripts/ast_grep_unified_registry.py +556 -0
- package/scripts/csr-status +366 -0
- package/scripts/import-conversations-unified.py +104 -23
- package/scripts/session_quality_tracker.py +481 -0
- package/scripts/streaming-watcher.py +140 -5
- package/scripts/update_patterns.py +334 -0
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
import time
|
|
7
|
+
import html
|
|
7
8
|
from typing import Optional, List, Dict, Any
|
|
8
9
|
from datetime import datetime, timezone
|
|
9
10
|
from pathlib import Path
|
|
@@ -168,20 +169,23 @@ class SearchTools:
|
|
|
168
169
|
if include_raw:
|
|
169
170
|
output += f"**Raw Payload:**\n```json\n{json.dumps(result.get('payload', {}), indent=2)}\n```\n\n"
|
|
170
171
|
else:
|
|
171
|
-
# XML format (default)
|
|
172
|
-
|
|
172
|
+
# XML format (default) with proper escaping
|
|
173
|
+
def _esc(x): return html.escape(str(x), quote=False)
|
|
174
|
+
|
|
175
|
+
output = f"<search_results>\n<query>{_esc(query)}</query>\n<count>{len(results)}</count>\n"
|
|
173
176
|
for i, result in enumerate(results, 1):
|
|
174
177
|
output += f"<result index=\"{i}\">\n"
|
|
175
178
|
output += f" <score>{result['score']:.3f}</score>\n"
|
|
176
|
-
output += f" <timestamp>{result.get('timestamp', 'N/A')}</timestamp>\n"
|
|
177
|
-
output += f" <conversation_id>{result.get('conversation_id', 'N/A')}</conversation_id>\n"
|
|
179
|
+
output += f" <timestamp>{_esc(result.get('timestamp', 'N/A'))}</timestamp>\n"
|
|
180
|
+
output += f" <conversation_id>{_esc(result.get('conversation_id', 'N/A'))}</conversation_id>\n"
|
|
178
181
|
if not brief:
|
|
179
182
|
# Handle both 'content' and 'excerpt' fields
|
|
180
|
-
content = result.get('content', result.get('excerpt', ''))
|
|
183
|
+
content = result.get('content', result.get('excerpt', result.get('text', '')))
|
|
181
184
|
truncated = content[:500] + ('...' if len(content) > 500 else '')
|
|
182
185
|
output += f" <content><![CDATA[{truncated}]]></content>\n"
|
|
183
186
|
if include_raw:
|
|
184
|
-
|
|
187
|
+
# Use CDATA for large JSON payloads
|
|
188
|
+
output += f" <raw_payload><![CDATA[{json.dumps(result.get('payload', {}), ensure_ascii=False)}]]></raw_payload>\n"
|
|
185
189
|
output += "</result>\n"
|
|
186
190
|
output += "</search_results>"
|
|
187
191
|
|
|
@@ -394,22 +398,29 @@ class SearchTools:
|
|
|
394
398
|
top_result = max(all_results, key=lambda x: x.get('score', 0)) if all_results else None
|
|
395
399
|
top_score = top_result.get('score', 0) if top_result else 0
|
|
396
400
|
|
|
397
|
-
# Format quick search response
|
|
401
|
+
# Format quick search response with proper XML escaping
|
|
402
|
+
def _esc(x): return html.escape(str(x), quote=False)
|
|
403
|
+
|
|
398
404
|
if not top_result:
|
|
399
405
|
return "<quick_search><count>0</count><message>No matches found</message></quick_search>"
|
|
400
|
-
|
|
406
|
+
|
|
407
|
+
# Get preview text and ensure we have content fallbacks
|
|
408
|
+
preview_text = top_result.get('excerpt', top_result.get('content', top_result.get('text', '')))[:200]
|
|
409
|
+
|
|
401
410
|
return f"""<quick_search>
|
|
402
|
-
<count>{collections_with_matches}
|
|
411
|
+
<count>{collections_with_matches}</count>
|
|
412
|
+
<collections_with_matches>{collections_with_matches}</collections_with_matches>
|
|
403
413
|
<top_result>
|
|
404
414
|
<score>{top_result['score']:.3f}</score>
|
|
405
|
-
<timestamp>{top_result.get('timestamp', 'N/A')}</timestamp>
|
|
406
|
-
<preview
|
|
415
|
+
<timestamp>{_esc(top_result.get('timestamp', 'N/A'))}</timestamp>
|
|
416
|
+
<preview><![CDATA[{preview_text}...]]></preview>
|
|
407
417
|
</top_result>
|
|
408
418
|
</quick_search>"""
|
|
409
|
-
|
|
419
|
+
|
|
410
420
|
except Exception as e:
|
|
411
421
|
logger.error(f"Quick search failed: {e}", exc_info=True)
|
|
412
|
-
|
|
422
|
+
def _esc(x): return html.escape(str(x), quote=False)
|
|
423
|
+
return f"<quick_search><error>Quick search failed: {_esc(str(e))}</error></quick_search>"
|
|
413
424
|
|
|
414
425
|
async def search_summary(
|
|
415
426
|
self,
|
|
@@ -606,46 +617,83 @@ class SearchTools:
|
|
|
606
617
|
project: Optional[str] = None
|
|
607
618
|
) -> str:
|
|
608
619
|
"""Search for conversations that analyzed a specific file."""
|
|
609
|
-
|
|
620
|
+
|
|
610
621
|
await ctx.debug(f"Searching for file: {file_path}, project={project}")
|
|
611
|
-
|
|
622
|
+
|
|
612
623
|
try:
|
|
613
|
-
#
|
|
614
|
-
|
|
615
|
-
|
|
624
|
+
# Create multiple path variants to match how paths are stored
|
|
625
|
+
# Import uses normalize_file_path which replaces /Users/ with ~/
|
|
626
|
+
path_variants = set()
|
|
627
|
+
|
|
628
|
+
# Original path
|
|
629
|
+
path_variants.add(file_path)
|
|
630
|
+
|
|
631
|
+
# Basename only
|
|
632
|
+
path_variants.add(os.path.basename(file_path))
|
|
633
|
+
|
|
634
|
+
# Try to resolve if it's a valid path
|
|
635
|
+
try:
|
|
636
|
+
resolved_path = str(Path(file_path).resolve())
|
|
637
|
+
path_variants.add(resolved_path)
|
|
638
|
+
|
|
639
|
+
# Convert resolved path to ~/ format (matching how import stores it)
|
|
640
|
+
home_dir = str(Path.home())
|
|
641
|
+
if resolved_path.startswith(home_dir):
|
|
642
|
+
tilde_path = resolved_path.replace(home_dir, '~', 1)
|
|
643
|
+
path_variants.add(tilde_path)
|
|
644
|
+
|
|
645
|
+
# Also try with /Users/ replaced by ~/
|
|
646
|
+
if '/Users/' in resolved_path:
|
|
647
|
+
path_variants.add(resolved_path.replace('/Users/', '~/', 1))
|
|
648
|
+
except:
|
|
649
|
+
pass
|
|
650
|
+
|
|
651
|
+
# If path starts with ~, also try expanded version
|
|
652
|
+
if file_path.startswith('~'):
|
|
653
|
+
expanded = os.path.expanduser(file_path)
|
|
654
|
+
path_variants.add(expanded)
|
|
655
|
+
|
|
656
|
+
# Convert all to forward slashes for consistency
|
|
657
|
+
path_variants = {p.replace('\\', '/') for p in path_variants if p}
|
|
658
|
+
|
|
659
|
+
await ctx.debug(f"Searching with path variants: {list(path_variants)}")
|
|
660
|
+
|
|
616
661
|
# Search for file mentions in metadata
|
|
617
662
|
collections_response = await self.qdrant_client.get_collections()
|
|
618
663
|
collections = collections_response.collections
|
|
619
|
-
|
|
620
|
-
# Define async function to search a single collection
|
|
664
|
+
|
|
665
|
+
# Define async function to search a single collection using scroll
|
|
621
666
|
async def search_collection(collection_name: str):
|
|
622
667
|
try:
|
|
623
|
-
|
|
624
|
-
|
|
668
|
+
from qdrant_client import models
|
|
669
|
+
|
|
670
|
+
# Use scroll with proper filter for metadata-only search
|
|
671
|
+
results, _ = await self.qdrant_client.scroll(
|
|
625
672
|
collection_name=collection_name,
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
673
|
+
scroll_filter=models.Filter(
|
|
674
|
+
should=[
|
|
675
|
+
models.FieldCondition(
|
|
676
|
+
key="files_analyzed",
|
|
677
|
+
match=models.MatchValue(value=path_variant)
|
|
678
|
+
)
|
|
679
|
+
for path_variant in path_variants
|
|
634
680
|
]
|
|
635
|
-
|
|
681
|
+
),
|
|
682
|
+
limit=limit,
|
|
683
|
+
with_payload=True
|
|
636
684
|
)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
for
|
|
640
|
-
|
|
641
|
-
'conversation_id':
|
|
642
|
-
'timestamp':
|
|
643
|
-
'content':
|
|
644
|
-
'files_analyzed':
|
|
645
|
-
'score':
|
|
685
|
+
|
|
686
|
+
formatted_results = []
|
|
687
|
+
for point in results:
|
|
688
|
+
formatted_results.append({
|
|
689
|
+
'conversation_id': point.payload.get('conversation_id'),
|
|
690
|
+
'timestamp': point.payload.get('timestamp'),
|
|
691
|
+
'content': point.payload.get('content', point.payload.get('text', '')),
|
|
692
|
+
'files_analyzed': point.payload.get('files_analyzed', []),
|
|
693
|
+
'score': 1.0 # No score in scroll, use 1.0 for found items
|
|
646
694
|
})
|
|
647
|
-
return
|
|
648
|
-
|
|
695
|
+
return formatted_results
|
|
696
|
+
|
|
649
697
|
except Exception as e:
|
|
650
698
|
await ctx.debug(f"Error searching {collection_name}: {e}")
|
|
651
699
|
return []
|
|
@@ -82,10 +82,17 @@ class TemporalTools:
|
|
|
82
82
|
|
|
83
83
|
# Filter collections by project
|
|
84
84
|
if target_project != 'all':
|
|
85
|
+
# Use asyncio.to_thread to avoid blocking the event loop
|
|
86
|
+
import asyncio
|
|
85
87
|
from qdrant_client import QdrantClient as SyncQdrantClient
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
|
|
89
|
+
def get_project_collections():
|
|
90
|
+
sync_client = SyncQdrantClient(url=self.qdrant_url)
|
|
91
|
+
resolver = ProjectResolver(sync_client)
|
|
92
|
+
return resolver.find_collections_for_project(target_project)
|
|
93
|
+
|
|
94
|
+
# Run sync client in thread pool to avoid blocking
|
|
95
|
+
project_collections = await asyncio.to_thread(get_project_collections)
|
|
89
96
|
|
|
90
97
|
if not project_collections:
|
|
91
98
|
normalized_name = self.normalize_project_name(target_project)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-self-reflect",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Give Claude perfect memory of all your conversations - Installation wizard for Python MCP server",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"installer/*.js",
|
|
38
|
+
"scripts/csr-status",
|
|
39
|
+
"scripts/session_quality_tracker.py",
|
|
40
|
+
"scripts/ast_grep_final_analyzer.py",
|
|
41
|
+
"scripts/ast_grep_unified_registry.py",
|
|
42
|
+
"scripts/update_patterns.py",
|
|
38
43
|
"mcp-server/src/**/*.py",
|
|
39
44
|
"mcp-server/pyproject.toml",
|
|
40
45
|
"mcp-server/run-mcp.sh",
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FINAL AST-GREP Analyzer with Unified Registry
|
|
4
|
+
MANDATORY: Uses ast-grep-py + unified pattern registry
|
|
5
|
+
NO regex fallbacks, NO simplifications
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ast_grep_py as sg
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, List, Any, Optional
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# Import the unified registry
|
|
16
|
+
sys.path.append(str(Path(__file__).parent))
|
|
17
|
+
from ast_grep_unified_registry import get_unified_registry
|
|
18
|
+
|
|
19
|
+
class FinalASTGrepAnalyzer:
|
|
20
|
+
"""
|
|
21
|
+
Final production-ready AST-GREP analyzer.
|
|
22
|
+
MANDATORY components:
|
|
23
|
+
- ast-grep-py for AST matching
|
|
24
|
+
- Unified pattern registry (custom + catalog)
|
|
25
|
+
- NO regex patterns
|
|
26
|
+
- NO fallbacks
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
"""Initialize with unified registry."""
|
|
31
|
+
self.registry = get_unified_registry()
|
|
32
|
+
all_patterns = self.registry.get_all_patterns()
|
|
33
|
+
|
|
34
|
+
print(f"ā
Loaded unified registry with {len(all_patterns)} patterns")
|
|
35
|
+
print(f" Languages: Python, TypeScript, JavaScript")
|
|
36
|
+
print(f" Good patterns: {len(self.registry.get_good_patterns())}")
|
|
37
|
+
print(f" Bad patterns: {len(self.registry.get_bad_patterns())}")
|
|
38
|
+
|
|
39
|
+
def analyze_file(self, file_path: str) -> Dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Analyze a file using unified AST-GREP patterns.
|
|
42
|
+
Returns detailed quality metrics and pattern matches.
|
|
43
|
+
"""
|
|
44
|
+
if not Path(file_path).exists():
|
|
45
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
46
|
+
|
|
47
|
+
# Detect language from file extension
|
|
48
|
+
language = self._detect_language(file_path)
|
|
49
|
+
|
|
50
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
51
|
+
content = f.read()
|
|
52
|
+
|
|
53
|
+
# Create SgRoot for the detected language
|
|
54
|
+
sg_language = self._get_sg_language(language)
|
|
55
|
+
root = sg.SgRoot(content, sg_language)
|
|
56
|
+
node = root.root()
|
|
57
|
+
|
|
58
|
+
# Get patterns for this language
|
|
59
|
+
language_patterns = self.registry.get_patterns_by_language(language)
|
|
60
|
+
|
|
61
|
+
# Track all matches
|
|
62
|
+
all_matches = []
|
|
63
|
+
pattern_errors = []
|
|
64
|
+
matches_by_category = {}
|
|
65
|
+
|
|
66
|
+
# Process each pattern
|
|
67
|
+
for pattern_def in language_patterns:
|
|
68
|
+
try:
|
|
69
|
+
pattern_str = pattern_def.get("pattern", "")
|
|
70
|
+
if not pattern_str:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Find matches using ast-grep-py
|
|
74
|
+
matches = node.find_all(pattern=pattern_str)
|
|
75
|
+
|
|
76
|
+
if matches:
|
|
77
|
+
category = pattern_def.get('category', 'unknown')
|
|
78
|
+
if category not in matches_by_category:
|
|
79
|
+
matches_by_category[category] = []
|
|
80
|
+
|
|
81
|
+
match_info = {
|
|
82
|
+
'category': category,
|
|
83
|
+
'id': pattern_def['id'],
|
|
84
|
+
'description': pattern_def.get('description', ''),
|
|
85
|
+
'quality': pattern_def.get('quality', 'neutral'),
|
|
86
|
+
'weight': pattern_def.get('weight', 0),
|
|
87
|
+
'count': len(matches),
|
|
88
|
+
'locations': [
|
|
89
|
+
{
|
|
90
|
+
'line': m.range().start.line + 1,
|
|
91
|
+
'column': m.range().start.column,
|
|
92
|
+
'text': m.text()[:80]
|
|
93
|
+
} for m in matches[:5] # First 5 examples
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
matches_by_category[category].append(match_info)
|
|
98
|
+
all_matches.append(match_info)
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
# Record all pattern errors for debugging
|
|
102
|
+
pattern_errors.append({
|
|
103
|
+
'pattern_id': pattern_def.get('id', '<unknown>'),
|
|
104
|
+
'category': pattern_def.get('category', 'unknown'),
|
|
105
|
+
'error': str(e)[:200]
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
# Calculate quality score
|
|
109
|
+
quality_score = self.registry.calculate_quality_score(all_matches)
|
|
110
|
+
|
|
111
|
+
# Count good vs bad patterns
|
|
112
|
+
good_matches = [m for m in all_matches if m['quality'] == 'good']
|
|
113
|
+
bad_matches = [m for m in all_matches if m['quality'] == 'bad']
|
|
114
|
+
|
|
115
|
+
good_count = sum(m['count'] for m in good_matches)
|
|
116
|
+
bad_count = sum(m['count'] for m in bad_matches)
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
'file': file_path,
|
|
120
|
+
'timestamp': datetime.now().isoformat(),
|
|
121
|
+
'language': language,
|
|
122
|
+
'engine': 'ast-grep-py + unified registry',
|
|
123
|
+
'registry_info': {
|
|
124
|
+
'total_patterns_available': len(language_patterns),
|
|
125
|
+
'patterns_matched': len(all_matches),
|
|
126
|
+
'patterns_errored': len(pattern_errors),
|
|
127
|
+
'categories_found': list(matches_by_category.keys())
|
|
128
|
+
},
|
|
129
|
+
'matches_by_category': matches_by_category,
|
|
130
|
+
'all_matches': all_matches,
|
|
131
|
+
'errors': pattern_errors[:5], # First 5 errors only
|
|
132
|
+
'quality_metrics': {
|
|
133
|
+
'quality_score': round(quality_score, 3),
|
|
134
|
+
'good_patterns_found': good_count,
|
|
135
|
+
'bad_patterns_found': bad_count,
|
|
136
|
+
'unique_patterns_matched': len(all_matches),
|
|
137
|
+
'total_issues': bad_count,
|
|
138
|
+
'total_good_practices': good_count
|
|
139
|
+
},
|
|
140
|
+
'recommendations': self._generate_recommendations(matches_by_category, quality_score)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def _detect_language(self, file_path: str) -> str:
|
|
144
|
+
"""Detect language from file extension."""
|
|
145
|
+
ext = Path(file_path).suffix.lower()
|
|
146
|
+
lang_map = {
|
|
147
|
+
'.py': 'python',
|
|
148
|
+
'.ts': 'typescript',
|
|
149
|
+
'.tsx': 'tsx',
|
|
150
|
+
'.js': 'javascript',
|
|
151
|
+
'.jsx': 'jsx'
|
|
152
|
+
}
|
|
153
|
+
return lang_map.get(ext, 'python')
|
|
154
|
+
|
|
155
|
+
def _get_sg_language(self, language: str) -> str:
|
|
156
|
+
"""Get ast-grep language identifier."""
|
|
157
|
+
# ast-grep-py uses different language identifiers
|
|
158
|
+
sg_map = {
|
|
159
|
+
'python': 'python',
|
|
160
|
+
'typescript': 'typescript',
|
|
161
|
+
'tsx': 'tsx',
|
|
162
|
+
'javascript': 'javascript',
|
|
163
|
+
'jsx': 'jsx'
|
|
164
|
+
}
|
|
165
|
+
return sg_map.get(language, 'python')
|
|
166
|
+
|
|
167
|
+
def _generate_recommendations(self, matches: Dict, score: float) -> List[str]:
|
|
168
|
+
"""Generate actionable recommendations based on matches."""
|
|
169
|
+
recommendations = []
|
|
170
|
+
|
|
171
|
+
if score < 0.3:
|
|
172
|
+
recommendations.append("š“ Critical: Code quality needs immediate attention")
|
|
173
|
+
elif score < 0.6:
|
|
174
|
+
recommendations.append("š” Warning: Several anti-patterns detected")
|
|
175
|
+
else:
|
|
176
|
+
recommendations.append("š¢ Good: Code follows most best practices")
|
|
177
|
+
|
|
178
|
+
# Check for specific issues
|
|
179
|
+
for category, category_matches in matches.items():
|
|
180
|
+
if 'antipatterns' in category:
|
|
181
|
+
total = sum(m['count'] for m in category_matches)
|
|
182
|
+
if total > 0:
|
|
183
|
+
recommendations.append(f"Fix {total} anti-patterns in {category}")
|
|
184
|
+
|
|
185
|
+
if 'logging' in category:
|
|
186
|
+
prints = sum(m['count'] for m in category_matches if 'print' in m['id'])
|
|
187
|
+
if prints > 0:
|
|
188
|
+
recommendations.append(f"Replace {prints} print statements with logger")
|
|
189
|
+
|
|
190
|
+
if 'error' in category:
|
|
191
|
+
bare = sum(m['count'] for m in category_matches if 'broad' in m['id'] or 'bare' in m['id'])
|
|
192
|
+
if bare > 0:
|
|
193
|
+
recommendations.append(f"Fix {bare} bare except clauses")
|
|
194
|
+
|
|
195
|
+
return recommendations
|
|
196
|
+
|
|
197
|
+
def generate_report(self, result: Dict[str, Any]) -> str:
|
|
198
|
+
"""Generate a comprehensive analysis report."""
|
|
199
|
+
report = []
|
|
200
|
+
report.append("# AST-GREP Pattern Analysis Report")
|
|
201
|
+
report.append(f"\n**File**: {result['file']}")
|
|
202
|
+
report.append(f"**Language**: {result['language']}")
|
|
203
|
+
report.append(f"**Timestamp**: {result['timestamp']}")
|
|
204
|
+
report.append(f"**Engine**: {result['engine']}")
|
|
205
|
+
|
|
206
|
+
# Quality overview
|
|
207
|
+
metrics = result['quality_metrics']
|
|
208
|
+
score = metrics['quality_score']
|
|
209
|
+
emoji = "š¢" if score > 0.7 else "š”" if score > 0.4 else "š“"
|
|
210
|
+
|
|
211
|
+
report.append("\n## Quality Overview")
|
|
212
|
+
report.append(f"- **Quality Score**: {emoji} {score:.1%}")
|
|
213
|
+
report.append(f"- **Good Practices**: {metrics['good_patterns_found']}")
|
|
214
|
+
report.append(f"- **Issues Found**: {metrics['total_issues']}")
|
|
215
|
+
report.append(f"- **Unique Patterns Matched**: {metrics['unique_patterns_matched']}")
|
|
216
|
+
|
|
217
|
+
# Recommendations
|
|
218
|
+
if result['recommendations']:
|
|
219
|
+
report.append("\n## Recommendations")
|
|
220
|
+
for rec in result['recommendations']:
|
|
221
|
+
report.append(f"- {rec}")
|
|
222
|
+
|
|
223
|
+
# Pattern matches by category
|
|
224
|
+
report.append("\n## Pattern Matches by Category")
|
|
225
|
+
for category, matches in result['matches_by_category'].items():
|
|
226
|
+
if matches:
|
|
227
|
+
total = sum(m['count'] for m in matches)
|
|
228
|
+
report.append(f"\n### {category} ({len(matches)} patterns, {total} matches)")
|
|
229
|
+
|
|
230
|
+
# Sort by count descending
|
|
231
|
+
sorted_matches = sorted(matches, key=lambda x: x['count'], reverse=True)
|
|
232
|
+
|
|
233
|
+
for match in sorted_matches[:5]: # Top 5 per category
|
|
234
|
+
quality_emoji = "ā
" if match['quality'] == 'good' else "ā" if match['quality'] == 'bad' else "āŖ"
|
|
235
|
+
report.append(f"- {quality_emoji} **{match['id']}**: {match['count']} instances")
|
|
236
|
+
report.append(f" - {match['description']}")
|
|
237
|
+
if match['locations']:
|
|
238
|
+
loc = match['locations'][0]
|
|
239
|
+
report.append(f" - Example (line {loc['line']}): `{loc['text'][:50]}...`")
|
|
240
|
+
|
|
241
|
+
# Registry info
|
|
242
|
+
report.append("\n## Pattern Registry Statistics")
|
|
243
|
+
info = result['registry_info']
|
|
244
|
+
report.append(f"- **Patterns Available**: {info['total_patterns_available']}")
|
|
245
|
+
report.append(f"- **Patterns Matched**: {info['patterns_matched']}")
|
|
246
|
+
report.append(f"- **Categories Found**: {', '.join(info['categories_found'])}")
|
|
247
|
+
|
|
248
|
+
report.append("\n## Compliance")
|
|
249
|
+
report.append("ā
Using unified AST-GREP registry (custom + catalog)")
|
|
250
|
+
report.append("ā
Using ast-grep-py for AST matching")
|
|
251
|
+
report.append("ā
NO regex patterns or fallbacks")
|
|
252
|
+
report.append("ā
Production-ready pattern analysis")
|
|
253
|
+
|
|
254
|
+
return '\n'.join(report)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def run_final_analysis():
|
|
258
|
+
"""Run final AST-GREP analysis with unified registry."""
|
|
259
|
+
print("š FINAL AST-GREP Analysis with Unified Registry")
|
|
260
|
+
print("=" * 60)
|
|
261
|
+
|
|
262
|
+
analyzer = FinalASTGrepAnalyzer()
|
|
263
|
+
|
|
264
|
+
# Analyze server.py
|
|
265
|
+
server_path = "/Users/ramakrishnanannaswamy/projects/claude-self-reflect/mcp-server/src/server.py"
|
|
266
|
+
|
|
267
|
+
print(f"\nAnalyzing: {server_path}")
|
|
268
|
+
print("-" * 40)
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
result = analyzer.analyze_file(server_path)
|
|
272
|
+
|
|
273
|
+
# Display results
|
|
274
|
+
metrics = result['quality_metrics']
|
|
275
|
+
score = metrics['quality_score']
|
|
276
|
+
|
|
277
|
+
print(f"\nš Analysis Results:")
|
|
278
|
+
print(f" Language: {result['language']}")
|
|
279
|
+
print(f" Quality Score: {score:.1%}")
|
|
280
|
+
print(f" Good Practices: {metrics['good_patterns_found']}")
|
|
281
|
+
print(f" Issues: {metrics['total_issues']}")
|
|
282
|
+
print(f" Patterns Matched: {metrics['unique_patterns_matched']}")
|
|
283
|
+
|
|
284
|
+
print(f"\nš” Recommendations:")
|
|
285
|
+
for rec in result['recommendations']:
|
|
286
|
+
print(f" {rec}")
|
|
287
|
+
|
|
288
|
+
# Top issues
|
|
289
|
+
bad_patterns = [m for m in result['all_matches'] if m['quality'] == 'bad']
|
|
290
|
+
if bad_patterns:
|
|
291
|
+
print(f"\nā ļø Top Issues to Fix:")
|
|
292
|
+
sorted_bad = sorted(bad_patterns, key=lambda x: x['count'] * abs(x['weight']), reverse=True)
|
|
293
|
+
for pattern in sorted_bad[:5]:
|
|
294
|
+
print(f" - {pattern['id']}: {pattern['count']} instances")
|
|
295
|
+
print(f" {pattern['description']}")
|
|
296
|
+
|
|
297
|
+
# Generate and save report
|
|
298
|
+
report = analyzer.generate_report(result)
|
|
299
|
+
report_path = "/Users/ramakrishnanannaswamy/projects/claude-self-reflect/scripts/final_analysis_report.md"
|
|
300
|
+
with open(report_path, 'w') as f:
|
|
301
|
+
f.write(report)
|
|
302
|
+
|
|
303
|
+
print(f"\nš Full report saved to: {report_path}")
|
|
304
|
+
|
|
305
|
+
# Save JSON results
|
|
306
|
+
json_path = "/Users/ramakrishnanannaswamy/projects/claude-self-reflect/scripts/final_analysis_result.json"
|
|
307
|
+
with open(json_path, 'w') as f:
|
|
308
|
+
json.dump(result, f, indent=2)
|
|
309
|
+
|
|
310
|
+
print(f"š JSON results saved to: {json_path}")
|
|
311
|
+
|
|
312
|
+
print("\nā
Final AST-GREP analysis complete!")
|
|
313
|
+
print(" - Unified registry with 41 patterns")
|
|
314
|
+
print(" - Support for Python, TypeScript, JavaScript")
|
|
315
|
+
print(" - Ready for production integration")
|
|
316
|
+
|
|
317
|
+
return result
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
print(f"\nā Analysis failed: {e}")
|
|
321
|
+
raise
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
if __name__ == "__main__":
|
|
325
|
+
run_final_analysis()
|