claude-self-reflect 3.3.1 โ†’ 4.0.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.
@@ -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
 
@@ -55,9 +55,120 @@ def get_import_status():
55
55
  return "๐Ÿ“š CSR: Error"
56
56
 
57
57
 
58
+ def categorize_issues(file_reports):
59
+ """
60
+ Categorize issues from AST analysis into critical/medium/low.
61
+ """
62
+ critical = 0
63
+ medium = 0
64
+ low = 0
65
+
66
+ for file_path, report in file_reports.items():
67
+ # Only use top_issues for accurate counting (avoid double-counting from recommendations)
68
+ for issue in report.get('top_issues', []):
69
+ severity = issue.get('severity', 'medium')
70
+ count = issue.get('count', 0)
71
+ issue_id = issue.get('id', '').lower()
72
+
73
+ if severity == 'high' or severity == 'critical':
74
+ critical += count
75
+ elif severity == 'medium':
76
+ # Console.log and print statements are low severity
77
+ if 'print' in issue_id or 'console' in issue_id:
78
+ low += count
79
+ else:
80
+ medium += count
81
+ else:
82
+ low += count
83
+
84
+ return critical, medium, low
85
+
86
+
87
+ def get_quality_icon(critical=0, medium=0, low=0):
88
+ """
89
+ Determine quality icon based on issue severity counts.
90
+ """
91
+ # Icon selection based on highest severity present
92
+ if critical > 0:
93
+ if critical >= 10:
94
+ return "๐Ÿ”ด" # Red circle - Critical issues need immediate attention
95
+ else:
96
+ return "๐ŸŸ " # Orange circle - Some critical issues
97
+ elif medium > 0:
98
+ if medium >= 50:
99
+ return "๐ŸŸก" # Yellow circle - Many medium issues
100
+ else:
101
+ return "๐ŸŸข" # Green circle - Few medium issues
102
+ elif low > 0:
103
+ if low >= 100:
104
+ return "โšช" # White circle - Many minor issues (prints)
105
+ else:
106
+ return "โœ…" # Check mark - Only minor issues
107
+ else:
108
+ return "โœจ" # Sparkles - Perfect, no issues
109
+
110
+
111
+ def format_statusline_quality(critical=0, medium=0, low=0):
112
+ """
113
+ Format statusline with colored dot and labeled numbers.
114
+ """
115
+ import os
116
+ icon = get_quality_icon(critical, medium, low)
117
+
118
+ # Check if we should use colors (when in a TTY)
119
+ use_colors = os.isatty(sys.stdout.fileno()) if hasattr(sys.stdout, 'fileno') else False
120
+
121
+ # Build count display with colors if supported
122
+ counts = []
123
+ if critical > 0:
124
+ if use_colors:
125
+ # Use bright red for critical
126
+ counts.append(f"\033[1;31mC:{critical}\033[0m")
127
+ else:
128
+ counts.append(f"C:{critical}")
129
+ if medium > 0:
130
+ if use_colors:
131
+ # Use bright yellow for medium
132
+ counts.append(f"\033[1;33mM:{medium}\033[0m")
133
+ else:
134
+ counts.append(f"M:{medium}")
135
+ if low > 0:
136
+ if use_colors:
137
+ # Use bright white/gray for low
138
+ counts.append(f"\033[1;37mL:{low}\033[0m")
139
+ else:
140
+ counts.append(f"L:{low}")
141
+
142
+ if counts:
143
+ return f"{icon} {' '.join(counts)}"
144
+ else:
145
+ return f"{icon}" # Perfect - no counts needed
146
+
147
+
58
148
  def get_session_health():
59
- """Get cached session health."""
60
- cache_file = Path.home() / ".claude-self-reflect" / "session_quality.json"
149
+ """Get cached session health with icon-based quality display."""
150
+ # Check for session edit tracker to show appropriate label
151
+ tracker_file = Path.home() / ".claude-self-reflect" / "current_session_edits.json"
152
+
153
+ # Get quality cache file for current project
154
+ project_name = Path.cwd().name
155
+ cache_file = Path.home() / ".claude-self-reflect" / "quality_cache" / f"{project_name}.json"
156
+
157
+ # Default label prefix
158
+ label_prefix = ""
159
+
160
+ # Check if we have a session tracker with edited files
161
+ if tracker_file.exists():
162
+ try:
163
+ with open(tracker_file, 'r') as f:
164
+ tracker_data = json.load(f)
165
+ edited_files = tracker_data.get('edited_files', [])
166
+ if edited_files:
167
+ # Show session label with file count
168
+ file_count = len(edited_files)
169
+ label_prefix = f"Session ({file_count} file{'s' if file_count > 1 else ''}): "
170
+ except:
171
+ pass
61
172
 
62
173
  if not cache_file.exists():
63
174
  # Fall back to import status if no health data
@@ -79,22 +190,26 @@ def get_session_health():
79
190
  # Fall back to import status if no session
80
191
  return get_import_status()
81
192
 
82
- summary = data['summary']
83
- grade = summary['quality_grade']
84
- issues = summary['total_issues']
193
+ # Extract issue counts by severity
194
+ file_reports = data.get('file_reports', {})
195
+ critical, medium, low = categorize_issues(file_reports)
85
196
 
86
- # Color coding
87
- if grade in ['A+', 'A']:
88
- emoji = '๐ŸŸข'
89
- elif grade in ['B', 'C']:
90
- emoji = '๐ŸŸก'
91
- else:
92
- emoji = '๐Ÿ”ด'
197
+ # Use the icon-based display with optional label
198
+ quality_display = format_statusline_quality(critical, medium, low)
93
199
 
94
- if issues > 0:
95
- return f"{emoji} Code: {grade} ({issues})"
96
- else:
97
- return f"{emoji} Code: {grade}"
200
+ # Add session label if we have one
201
+ if data.get('scope_label') == 'Session':
202
+ # For session scope, always show the label with counts
203
+ if label_prefix:
204
+ if critical == 0 and medium == 0 and low == 0:
205
+ return f"{label_prefix}0 0 0 {quality_display}"
206
+ else:
207
+ return f"{label_prefix}{critical} {medium} {low} {quality_display}"
208
+ else:
209
+ # Fallback if no tracker file
210
+ return f"Session: {critical} {medium} {low} {quality_display}"
211
+
212
+ return quality_display
98
213
 
99
214
  except Exception:
100
215
  return get_import_status()
@@ -257,6 +372,14 @@ def get_compact_status():
257
372
  cache_dir = Path.home() / ".claude-self-reflect" / "quality_cache"
258
373
  cache_file = cache_dir / f"{safe_project_name}.json"
259
374
 
375
+ # If the exact cache file doesn't exist, try to find one that ends with this project name
376
+ # This handles cases like "metafora-Atlas-gold.json" for project "Atlas-gold"
377
+ if not cache_file.exists():
378
+ # Look for files ending with the project name
379
+ possible_files = list(cache_dir.glob(f"*-{safe_project_name}.json"))
380
+ if possible_files:
381
+ cache_file = possible_files[0] # Use the first match
382
+
260
383
  # Validate cache file path stays within cache directory
261
384
  if cache_file.exists() and not str(cache_file.resolve()).startswith(str(cache_dir.resolve())):
262
385
  # Security issue - return placeholder
@@ -273,30 +396,38 @@ def get_compact_status():
273
396
  mtime = datetime.fromtimestamp(cache_file.stat().st_mtime)
274
397
  age = datetime.now() - mtime
275
398
 
276
- # Use quality data up to 24 hours old (more reasonable)
277
- if age < timedelta(hours=24):
399
+ # Use quality data up to 30 minutes old for fresher results
400
+ if age < timedelta(minutes=30):
278
401
  with open(cache_file, 'r') as f:
279
402
  data = json.load(f)
280
403
 
281
- if data.get('status') == 'success':
282
- summary = data['summary']
283
- grade = summary['quality_grade']
284
- issues = summary.get('total_issues', 0)
285
- scope = data.get('scope_label', 'Core') # Get scope label
286
-
287
- # GPT-5 fix: Remove forced downgrades, trust the analyzer's grade
288
- # Grade should reflect actual quality metrics, not arbitrary thresholds
289
-
290
- # Pick emoji based on grade
291
- if grade in ['A+', 'A']:
292
- emoji = '๐ŸŸข'
293
- elif grade in ['B', 'C']:
294
- emoji = '๐ŸŸก'
404
+ if data.get('status') == 'non-code':
405
+ # Non-code project - show documentation indicator
406
+ grade_str = "[๐Ÿ“š:Docs]"
407
+ quality_valid = True
408
+ elif data.get('status') == 'success':
409
+ # Extract issue counts by severity for icon display
410
+ file_reports = data.get('file_reports', {})
411
+ critical, medium, low = categorize_issues(file_reports)
412
+
413
+ # Get icon based on severity
414
+ icon = get_quality_icon(critical, medium, low)
415
+
416
+ # Build compact display with ANSI colors for each severity level
417
+ colored_parts = []
418
+ if critical > 0:
419
+ colored_parts.append(f"\033[31m{critical}\033[0m") # Standard red for critical
420
+ if medium > 0:
421
+ colored_parts.append(f"\033[33m{medium}\033[0m") # Standard yellow for medium
422
+ if low > 0:
423
+ colored_parts.append(f"\033[37m{low}\033[0m") # White/light gray for low
424
+
425
+ # Join with middle dot separator
426
+ if colored_parts:
427
+ grade_str = f"[{icon}:{'ยท'.join(colored_parts)}]"
295
428
  else:
296
- emoji = '๐Ÿ”ด'
429
+ grade_str = f"[{icon}]"
297
430
 
298
- # Simple, clear display without confusing scope labels
299
- grade_str = f"[{emoji}:{grade}/{issues}]"
300
431
  quality_valid = True
301
432
  except:
302
433
  pass
@@ -308,17 +439,31 @@ def get_compact_status():
308
439
  if cache_file.exists():
309
440
  with open(cache_file, 'r') as f:
310
441
  old_data = json.load(f)
311
- if old_data.get('status') == 'success':
312
- old_grade = old_data['summary']['quality_grade']
313
- old_issues = old_data['summary'].get('total_issues', 0)
314
- # Show with dimmed indicator that it's old
315
- if old_grade in ['A+', 'A']:
316
- emoji = '๐ŸŸข'
317
- elif old_grade in ['B', 'C']:
318
- emoji = '๐ŸŸก'
442
+ if old_data.get('status') == 'non-code':
443
+ # Non-code project - show documentation indicator
444
+ grade_str = "[๐Ÿ“š:Docs]"
445
+ elif old_data.get('status') == 'success':
446
+ # Extract issue counts by severity for icon display
447
+ file_reports = old_data.get('file_reports', {})
448
+ critical, medium, low = categorize_issues(file_reports)
449
+
450
+ # Get icon based on severity
451
+ icon = get_quality_icon(critical, medium, low)
452
+
453
+ # Build compact display with ANSI colors for each severity level
454
+ colored_parts = []
455
+ if critical > 0:
456
+ colored_parts.append(f"\033[31m{critical}\033[0m") # Standard red for critical
457
+ if medium > 0:
458
+ colored_parts.append(f"\033[33m{medium}\033[0m") # Standard yellow for medium
459
+ if low > 0:
460
+ colored_parts.append(f"\033[37m{low}\033[0m") # White/light gray for low
461
+
462
+ # Join with middle dot separator
463
+ if colored_parts:
464
+ grade_str = f"[{icon}:{'ยท'.join(colored_parts)}]"
319
465
  else:
320
- emoji = '๐Ÿ”ด'
321
- grade_str = f"[{emoji}:{old_grade}/{old_issues}]"
466
+ grade_str = f"[{icon}]"
322
467
  else:
323
468
  grade_str = "[...]"
324
469
  else: