claude-self-reflect 3.3.1 → 4.0.0

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.
@@ -135,13 +135,15 @@ def get_status() -> dict:
135
135
  # The actual structure has imported_files at the top level
136
136
  imported_files = data.get("imported_files", {})
137
137
 
138
- # Count all files in imported_files object (they are all fully imported)
138
+ # Count all files in imported_files object (only if they still exist on disk)
139
139
  for file_path in imported_files.keys():
140
140
  normalized_path = normalize_file_path(file_path)
141
141
  if normalized_path in file_to_project and normalized_path not in counted_files:
142
- project_name = file_to_project[normalized_path]
143
- project_stats[project_name]["indexed"] += 1
144
- counted_files.add(normalized_path)
142
+ # Verify file actually exists before counting it as indexed
143
+ if Path(normalized_path).exists():
144
+ project_name = file_to_project[normalized_path]
145
+ project_stats[project_name]["indexed"] += 1
146
+ counted_files.add(normalized_path)
145
147
 
146
148
  # Also check file_metadata for partially imported files
147
149
  file_metadata = data.get("file_metadata", {})
@@ -180,14 +182,17 @@ def get_status() -> dict:
180
182
  with open(watcher_state_file, 'r') as f:
181
183
  watcher_data = json.load(f)
182
184
 
183
- # Count files imported by the watcher
185
+ # Count files imported by the watcher (only if they still exist on disk)
184
186
  watcher_imports = watcher_data.get("imported_files", {})
185
187
  for file_path in watcher_imports.keys():
186
188
  normalized_path = normalize_file_path(file_path)
189
+ # CRITICAL: Only count if file exists on disk AND is in our project list
187
190
  if normalized_path in file_to_project and normalized_path not in counted_files:
188
- project_name = file_to_project[normalized_path]
189
- project_stats[project_name]["indexed"] += 1
190
- counted_files.add(normalized_path)
191
+ # Verify file actually exists before counting it as indexed
192
+ if Path(normalized_path).exists():
193
+ project_name = file_to_project[normalized_path]
194
+ project_stats[project_name]["indexed"] += 1
195
+ counted_files.add(normalized_path)
191
196
  except (json.JSONDecodeError, KeyError, OSError):
192
197
  # If watcher file is corrupted or unreadable, continue
193
198
  pass
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Test file with intentional quality issues for testing quality-fixer agent.
4
+ This file contains patterns that should be fixed:
5
+ - sync file operations that should be async
6
+ - global variables
7
+ - print statements
8
+ - long functions
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import asyncio
14
+ import logging
15
+ import aiofiles
16
+ from typing import List, Dict, Any
17
+
18
+ # Set up logger instead of print statements
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Configuration management class instead of global variables
22
+ class ConfigManager:
23
+ def __init__(self):
24
+ self.config = None
25
+ self.counter = 0
26
+
27
+ async def load_config(config_manager: ConfigManager) -> Dict[str, Any]:
28
+ """Load config using async file operations."""
29
+ # Async file operation using aiofiles
30
+ async with aiofiles.open("config.json", "r") as f:
31
+ content = await f.read()
32
+ config_manager.config = json.loads(content)
33
+
34
+ logger.info(f"Config loaded: {config_manager.config}")
35
+ return config_manager.config
36
+
37
+ async def save_data(data: Dict[str, Any], config_manager: ConfigManager) -> None:
38
+ """Save data using async operations."""
39
+ config_manager.counter += 1
40
+
41
+ # Async file operation using aiofiles
42
+ async with aiofiles.open("data.json", "w") as f:
43
+ await f.write(json.dumps(data))
44
+
45
+ logger.info(f"Data saved, counter: {config_manager.counter}")
46
+
47
+ def validate_items(items: List[str]) -> List[str]:
48
+ """Validate input items."""
49
+ valid_items = []
50
+ for item in items:
51
+ if not item:
52
+ logger.warning(f"Invalid item: {item}")
53
+ continue
54
+ valid_items.append(item)
55
+ return valid_items
56
+
57
+ def process_items(items: List[str]) -> List[str]:
58
+ """Process each item."""
59
+ return [item.upper() for item in items]
60
+
61
+ def filter_results(results: List[str]) -> List[str]:
62
+ """Filter results by length."""
63
+ return [result for result in results if len(result) > 3]
64
+
65
+ def create_summary(items: List[str], results: List[str], filtered: List[str]) -> Dict[str, int]:
66
+ """Create processing summary."""
67
+ return {
68
+ "total": len(items),
69
+ "processed": len(results),
70
+ "filtered": len(filtered)
71
+ }
72
+
73
+ async def save_results(filtered: List[str]) -> None:
74
+ """Save results to file asynchronously."""
75
+ async with aiofiles.open("results.txt", "w") as f:
76
+ for item in filtered:
77
+ await f.write(f"{item}\n")
78
+
79
+ async def process_items_improved(items: List[str], config_manager: ConfigManager) -> Dict[str, Any]:
80
+ """Improved function broken down into smaller functions."""
81
+ # Step 1: Validate items
82
+ valid_items = validate_items(items)
83
+
84
+ # Step 2: Process each item
85
+ results = process_items(valid_items)
86
+
87
+ # Step 3: Filter results
88
+ filtered = filter_results(results)
89
+
90
+ # Step 4: Sort results
91
+ filtered.sort()
92
+
93
+ # Step 5: Create summary
94
+ summary = create_summary(items, results, filtered)
95
+
96
+ # Step 6: Log summary
97
+ logger.info(f"Processing complete: {summary}")
98
+
99
+ # Step 7: Save results asynchronously
100
+ await save_results(filtered)
101
+
102
+ # Step 8: Update counter
103
+ config_manager.counter += len(filtered)
104
+
105
+ # Step 9: Create report
106
+ report = {
107
+ "summary": summary,
108
+ "results": filtered,
109
+ "counter": config_manager.counter
110
+ }
111
+
112
+ return report
113
+
114
+ async def debug_function() -> None:
115
+ """Function with debug statements."""
116
+ logger.debug("Debug: Starting function")
117
+
118
+ # Reading file asynchronously
119
+ if os.path.exists("debug.log"):
120
+ async with aiofiles.open("debug.log", "r") as f:
121
+ log_data = await f.read()
122
+ logger.debug(f"Log data: {log_data}")
123
+
124
+ logger.debug("Debug: Function complete")
125
+
126
+ # Using var instead of let/const (for JS patterns if analyzed)
127
+ var_example = "This would be flagged in JS"
128
+
129
+ async def main() -> None:
130
+ """Main execution function."""
131
+ # Set up logging
132
+ logging.basicConfig(level=logging.INFO)
133
+
134
+ # Initialize config manager
135
+ config_manager = ConfigManager()
136
+
137
+ logger.info("Starting application...")
138
+
139
+ try:
140
+ # Note: These operations would fail without actual files, but structure is correct
141
+ await load_config(config_manager)
142
+ await process_items_improved(["test", "data", "example"], config_manager)
143
+ await debug_function()
144
+ except FileNotFoundError:
145
+ logger.warning("Required files not found - this is expected in test context")
146
+ except Exception as e:
147
+ logger.error(f"Application error: {e}")
148
+
149
+ logger.info("Application complete!")
150
+
151
+ if __name__ == "__main__":
152
+ # Run async main function
153
+ asyncio.run(main())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-self-reflect",
3
- "version": "3.3.1",
3
+ "version": "4.0.0",
4
4
  "description": "Give Claude perfect memory of all your conversations - Installation wizard for Python MCP server",
5
5
  "keywords": [
6
6
  "claude",
@@ -50,6 +50,9 @@ class FinalASTGrepAnalyzer:
50
50
  with open(file_path, 'r', encoding='utf-8') as f:
51
51
  content = f.read()
52
52
 
53
+ # Count lines of code for normalization
54
+ lines_of_code = len(content.splitlines())
55
+
53
56
  # Create SgRoot for the detected language
54
57
  sg_language = self._get_sg_language(language)
55
58
  root = sg.SgRoot(content, sg_language)
@@ -105,8 +108,8 @@ class FinalASTGrepAnalyzer:
105
108
  'error': str(e)[:200]
106
109
  })
107
110
 
108
- # Calculate quality score
109
- quality_score = self.registry.calculate_quality_score(all_matches)
111
+ # Calculate quality score with LOC normalization
112
+ quality_score = self.registry.calculate_quality_score(all_matches, loc=lines_of_code)
110
113
 
111
114
  # Count good vs bad patterns
112
115
  good_matches = [m for m in all_matches if m['quality'] == 'good']
@@ -59,6 +59,9 @@ class UnifiedASTGrepRegistry:
59
59
  # JavaScript patterns (shared with TS)
60
60
  patterns.update(self._load_javascript_patterns())
61
61
 
62
+ # Shell script patterns
63
+ patterns.update(self._load_shell_patterns())
64
+
62
65
  return patterns
63
66
 
64
67
  def _load_python_patterns(self) -> Dict[str, List[Dict[str, Any]]]:
@@ -224,6 +227,41 @@ class UnifiedASTGrepRegistry:
224
227
  "quality": "bad",
225
228
  "weight": -4,
226
229
  "language": "python"
230
+ },
231
+ {
232
+ "id": "sync-voyage-embed",
233
+ "pattern": "$CLIENT.embed($$$)",
234
+ "description": "Blocking Voyage embed in async context",
235
+ "quality": "bad",
236
+ "weight": -5,
237
+ "language": "python",
238
+ "inside": "async def $FUNC($$$): $$$"
239
+ },
240
+ {
241
+ "id": "thread-join-async",
242
+ "pattern": "$THREAD.join($$$)",
243
+ "description": "Thread join blocking async context",
244
+ "quality": "bad",
245
+ "weight": -5,
246
+ "language": "python",
247
+ "inside": "async def $FUNC($$$): $$$"
248
+ },
249
+ {
250
+ "id": "invalid-env-var-hyphen",
251
+ "pattern": "os.getenv('$VAR')",
252
+ "description": "Environment variable with hyphen (invalid in shells)",
253
+ "quality": "bad",
254
+ "weight": -3,
255
+ "language": "python",
256
+ "constraint": "$VAR matches .*-.*"
257
+ },
258
+ {
259
+ "id": "dotenv-override-runtime",
260
+ "pattern": "load_dotenv($$$, override=True)",
261
+ "description": "Runtime environment mutation in MCP",
262
+ "quality": "bad",
263
+ "weight": -3,
264
+ "language": "python"
227
265
  }
228
266
  ],
229
267
  "python_qdrant": [
@@ -268,6 +306,50 @@ class UnifiedASTGrepRegistry:
268
306
  "quality": "good",
269
307
  "weight": 5,
270
308
  "language": "python"
309
+ },
310
+ {
311
+ "id": "missing-embedding-guard",
312
+ "pattern": "query_embedding = await $MGR.generate_embedding($$$)\n$$$\nawait $CLIENT.search($$$, query_vector=query_embedding, $$$)",
313
+ "description": "Missing None check after embedding generation",
314
+ "quality": "bad",
315
+ "weight": -4,
316
+ "language": "python"
317
+ },
318
+ {
319
+ "id": "attr-vs-api",
320
+ "pattern": "$MGR.model_name",
321
+ "description": "Accessing non-existent attribute instead of API",
322
+ "quality": "bad",
323
+ "weight": -3,
324
+ "language": "python",
325
+ "note": "Use get_model_info() instead"
326
+ },
327
+ {
328
+ "id": "duplicate-import",
329
+ "pattern": "import $MODULE\n$$$\ndef $FUNC($$$):\n $$$\n import $MODULE",
330
+ "description": "Duplicate import inside function",
331
+ "quality": "bad",
332
+ "weight": -2,
333
+ "language": "python"
334
+ }
335
+ ],
336
+ "python_runtime_modification": [
337
+ {
338
+ "id": "singleton-state-change",
339
+ "pattern": "$SINGLETON.$ATTR = $VALUE",
340
+ "description": "Runtime singleton state modification",
341
+ "quality": "neutral",
342
+ "weight": 0,
343
+ "language": "python",
344
+ "note": "Can be good for mode switching, bad if uncontrolled"
345
+ },
346
+ {
347
+ "id": "public-init-exposure",
348
+ "pattern": "def try_initialize_$TYPE(self): $$$",
349
+ "description": "Public initialization method for runtime config",
350
+ "quality": "neutral",
351
+ "weight": 0,
352
+ "language": "python"
271
353
  }
272
354
  ]
273
355
  }
@@ -386,6 +468,48 @@ class UnifiedASTGrepRegistry:
386
468
  ]
387
469
  }
388
470
 
471
+ def _load_shell_patterns(self) -> Dict[str, List[Dict[str, Any]]]:
472
+ """Shell script patterns."""
473
+ return {
474
+ "shell_env_handling": [
475
+ {
476
+ "id": "unused-shell-var",
477
+ "pattern": "$VAR=\"$VALUE\"",
478
+ "description": "Assigned but never referenced variable",
479
+ "quality": "bad",
480
+ "weight": -2,
481
+ "language": "bash",
482
+ "note": "Check if variable is used later"
483
+ },
484
+ {
485
+ "id": "unsafe-var-check",
486
+ "pattern": "[ ! -z \"$VAR\" ]",
487
+ "description": "Unsafe variable check (breaks with set -u)",
488
+ "quality": "bad",
489
+ "weight": -3,
490
+ "language": "bash",
491
+ "fix": "[ -n \"${VAR:-}\" ]"
492
+ },
493
+ {
494
+ "id": "redundant-export",
495
+ "pattern": "export $VAR=\"$VAR\"",
496
+ "description": "Redundant export of same value",
497
+ "quality": "bad",
498
+ "weight": -2,
499
+ "language": "bash"
500
+ },
501
+ {
502
+ "id": "missing-safety-flags",
503
+ "pattern": "#!/bin/bash",
504
+ "description": "Missing safety flags",
505
+ "quality": "bad",
506
+ "weight": -3,
507
+ "language": "bash",
508
+ "note": "Add 'set -euo pipefail' after shebang"
509
+ }
510
+ ]
511
+ }
512
+
389
513
  def _load_javascript_patterns(self) -> Dict[str, List[Dict[str, Any]]]:
390
514
  """JavaScript patterns (subset of TypeScript)."""
391
515
  return {
@@ -466,26 +590,56 @@ class UnifiedASTGrepRegistry:
466
590
  """Get only bad quality patterns (anti-patterns)."""
467
591
  return [p for p in self.get_all_patterns() if p.get('quality') == 'bad']
468
592
 
469
- def calculate_quality_score(self, matches: List[Dict]) -> float:
593
+ def calculate_quality_score(self, matches: List[Dict], loc: int = 1000) -> float:
470
594
  """
471
- Calculate quality score based on pattern matches.
472
- Each match includes the pattern and count.
595
+ Calculate quality score using penalty-based approach.
596
+ Issues dominate the score; good patterns provide minimal bonus.
597
+
598
+ Args:
599
+ matches: List of pattern matches with weight and count
600
+ loc: Lines of code (for normalization)
601
+
602
+ Returns:
603
+ Score from 0.0 to 1.0
473
604
  """
474
- total_weight = 0
475
- total_count = 0
605
+ import math
606
+
607
+ # Normalize to KLOC (thousands of lines)
608
+ kloc = max(1.0, loc / 1000.0)
609
+
610
+ # Separate issues (bad) from good patterns
611
+ issues = [m for m in matches if m.get('quality') == 'bad']
612
+ good_patterns = [m for m in matches if m.get('quality') == 'good']
613
+
614
+ # Calculate severity-weighted issue density
615
+ total_issues = 0
616
+ for issue in issues:
617
+ severity = abs(issue.get('weight', 1)) # Use weight as severity
618
+ count = issue.get('count', 0)
619
+ total_issues += severity * count
620
+
621
+ issues_per_kloc = total_issues / kloc
622
+
623
+ # Penalty calculation (logarithmic to avoid linear dominance)
624
+ # Calibrated so 50 issues/KLOC = ~50% penalty
625
+ penalty = min(0.7, 0.15 * math.log1p(issues_per_kloc))
626
+
627
+ # Small bonus for good patterns (capped at 5%)
628
+ good_score = 0
629
+ if good_patterns:
630
+ for pattern in good_patterns:
631
+ weight = pattern.get('weight', 1)
632
+ count = pattern.get('count', 0)
633
+ # Cap contribution per pattern type
634
+ normalized_count = min(count / kloc, 50) # Max 50 per KLOC
635
+ good_score += weight * normalized_count / 1000
476
636
 
477
- for match in matches:
478
- weight = match.get('weight', 0)
479
- count = match.get('count', 0)
480
- total_weight += weight * count
481
- total_count += abs(weight) * count
637
+ bonus = min(0.05, good_score) # Cap at 5% bonus
482
638
 
483
- if total_count == 0:
484
- return 0.5
639
+ # Final score: start at 100%, subtract penalty, add small bonus
640
+ score = max(0.0, min(1.0, 1.0 - penalty + bonus))
485
641
 
486
- # Normalize to 0-1 range
487
- normalized = (total_weight + 100) / 200
488
- return max(0.0, min(1.0, normalized))
642
+ return score
489
643
 
490
644
  def export_to_json(self, path: str):
491
645
  """Export registry to JSON file."""
@@ -545,7 +699,7 @@ if __name__ == "__main__":
545
699
  print(f" - {category}: {count} patterns")
546
700
 
547
701
  # Export to JSON
548
- export_path = "/Users/ramakrishnanannaswamy/projects/claude-self-reflect/scripts/unified_registry.json"
702
+ export_path = Path(__file__).parent / "unified_registry.json"
549
703
  registry.export_to_json(export_path)
550
704
  print(f"\n✅ Exported unified registry to {export_path}")
551
705