codeshield-ai 0.1.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.
@@ -0,0 +1,543 @@
1
+ """
2
+ CodeShield Metrics - Transparent Statistics Tracking
3
+
4
+ Provides honest, verifiable metrics for all features:
5
+ - TrustGate: Detection rates, fix accuracy, execution stats
6
+ - StyleForge: Convention detection accuracy, correction counts
7
+ - ContextVault: Storage stats, restore success rates
8
+ - LLM: Token usage, cost efficiency, provider performance
9
+ """
10
+
11
+ import time
12
+ import json
13
+ import sqlite3
14
+ from pathlib import Path
15
+ from datetime import datetime, timedelta
16
+ from dataclasses import dataclass, field, asdict
17
+ from typing import Optional, Dict, List, Any
18
+ from collections import defaultdict
19
+ from threading import Lock
20
+ from contextlib import contextmanager
21
+
22
+
23
+ DB_PATH = Path.home() / ".codeshield" / "metrics.sqlite"
24
+
25
+ # Thread-safe lock for metrics updates
26
+ _metrics_lock = Lock()
27
+
28
+
29
+ @dataclass
30
+ class TrustGateMetrics:
31
+ """TrustGate verification metrics"""
32
+ total_verifications: int = 0
33
+ syntax_errors_detected: int = 0
34
+ missing_imports_detected: int = 0
35
+ undefined_names_detected: int = 0
36
+ auto_fixes_applied: int = 0
37
+ sandbox_executions: int = 0
38
+ sandbox_successes: int = 0
39
+ sandbox_failures: int = 0
40
+ total_processing_time_ms: int = 0
41
+
42
+ @property
43
+ def detection_rate(self) -> float:
44
+ """Percentage of verifications that found issues"""
45
+ if self.total_verifications == 0:
46
+ return 0.0
47
+ issues = self.syntax_errors_detected + self.missing_imports_detected + self.undefined_names_detected
48
+ return min(100.0, (issues / max(1, self.total_verifications)) * 100)
49
+
50
+ @property
51
+ def fix_success_rate(self) -> float:
52
+ """Percentage of issues that were auto-fixed"""
53
+ issues = self.syntax_errors_detected + self.missing_imports_detected + self.undefined_names_detected
54
+ if issues == 0:
55
+ return 100.0 # No issues = nothing to fix
56
+ return (self.auto_fixes_applied / issues) * 100
57
+
58
+ @property
59
+ def sandbox_success_rate(self) -> float:
60
+ """Sandbox execution success rate"""
61
+ if self.sandbox_executions == 0:
62
+ return 0.0
63
+ return (self.sandbox_successes / self.sandbox_executions) * 100
64
+
65
+ @property
66
+ def avg_processing_time_ms(self) -> float:
67
+ """Average processing time per verification"""
68
+ if self.total_verifications == 0:
69
+ return 0.0
70
+ return self.total_processing_time_ms / self.total_verifications
71
+
72
+ def to_dict(self) -> dict:
73
+ return {
74
+ **asdict(self),
75
+ "detection_rate": round(self.detection_rate, 2),
76
+ "fix_success_rate": round(self.fix_success_rate, 2),
77
+ "sandbox_success_rate": round(self.sandbox_success_rate, 2),
78
+ "avg_processing_time_ms": round(self.avg_processing_time_ms, 2),
79
+ }
80
+
81
+
82
+ @dataclass
83
+ class StyleForgeMetrics:
84
+ """StyleForge convention metrics"""
85
+ total_checks: int = 0
86
+ conventions_detected: int = 0
87
+ naming_issues_found: int = 0
88
+ corrections_suggested: int = 0
89
+ corrections_applied: int = 0
90
+ codebases_analyzed: int = 0
91
+ total_processing_time_ms: int = 0
92
+
93
+ @property
94
+ def detection_accuracy(self) -> float:
95
+ """Ratio of issues found to checks performed"""
96
+ if self.total_checks == 0:
97
+ return 0.0
98
+ return (self.naming_issues_found / self.total_checks) * 100
99
+
100
+ @property
101
+ def correction_rate(self) -> float:
102
+ """Percentage of suggestions that were applied"""
103
+ if self.corrections_suggested == 0:
104
+ return 0.0
105
+ return (self.corrections_applied / self.corrections_suggested) * 100
106
+
107
+ def to_dict(self) -> dict:
108
+ return {
109
+ **asdict(self),
110
+ "detection_accuracy": round(self.detection_accuracy, 2),
111
+ "correction_rate": round(self.correction_rate, 2),
112
+ }
113
+
114
+
115
+ @dataclass
116
+ class ContextVaultMetrics:
117
+ """ContextVault storage metrics"""
118
+ total_contexts_saved: int = 0
119
+ total_contexts_restored: int = 0
120
+ restore_successes: int = 0
121
+ restore_failures: int = 0
122
+ contexts_deleted: int = 0
123
+ total_files_tracked: int = 0
124
+ total_storage_bytes: int = 0
125
+
126
+ @property
127
+ def restore_success_rate(self) -> float:
128
+ """Context restore success rate"""
129
+ total_restores = self.restore_successes + self.restore_failures
130
+ if total_restores == 0:
131
+ return 0.0
132
+ return (self.restore_successes / total_restores) * 100
133
+
134
+ def to_dict(self) -> dict:
135
+ return {
136
+ **asdict(self),
137
+ "restore_success_rate": round(self.restore_success_rate, 2),
138
+ "storage_mb": round(self.total_storage_bytes / (1024 * 1024), 3),
139
+ }
140
+
141
+
142
+ @dataclass
143
+ class TokenMetrics:
144
+ """LLM Token Usage Metrics - For Cost Efficiency Tracking"""
145
+ total_input_tokens: int = 0
146
+ total_output_tokens: int = 0
147
+ total_tokens: int = 0
148
+ total_requests: int = 0
149
+ successful_requests: int = 0
150
+ failed_requests: int = 0
151
+
152
+ # Provider-specific tracking
153
+ provider_tokens: Dict[str, dict] = field(default_factory=lambda: defaultdict(lambda: {
154
+ "input": 0, "output": 0, "total": 0, "requests": 0, "cost_usd": 0.0
155
+ }))
156
+
157
+ # Cost per 1K tokens (estimates)
158
+ COST_PER_1K = {
159
+ "cometapi": {"input": 0.0001, "output": 0.0002}, # Free tier mostly
160
+ "novita": {"input": 0.0005, "output": 0.001},
161
+ "aiml": {"input": 0.001, "output": 0.002},
162
+ }
163
+
164
+ @property
165
+ def token_efficiency(self) -> float:
166
+ """Output tokens per input token (higher = more efficient responses)"""
167
+ if self.total_input_tokens == 0:
168
+ return 0.0
169
+ return self.total_output_tokens / self.total_input_tokens
170
+
171
+ @property
172
+ def avg_tokens_per_request(self) -> float:
173
+ """Average tokens used per request"""
174
+ if self.total_requests == 0:
175
+ return 0.0
176
+ return self.total_tokens / self.total_requests
177
+
178
+ @property
179
+ def estimated_cost_usd(self) -> float:
180
+ """Estimated total cost across all providers"""
181
+ total_cost = 0.0
182
+ for provider, stats in self.provider_tokens.items():
183
+ if isinstance(stats, dict):
184
+ total_cost += stats.get("cost_usd", 0.0)
185
+ return total_cost
186
+
187
+ @property
188
+ def success_rate(self) -> float:
189
+ """LLM request success rate"""
190
+ if self.total_requests == 0:
191
+ return 0.0
192
+ return (self.successful_requests / self.total_requests) * 100
193
+
194
+ def to_dict(self) -> dict:
195
+ return {
196
+ "total_input_tokens": self.total_input_tokens,
197
+ "total_output_tokens": self.total_output_tokens,
198
+ "total_tokens": self.total_tokens,
199
+ "total_requests": self.total_requests,
200
+ "successful_requests": self.successful_requests,
201
+ "failed_requests": self.failed_requests,
202
+ "token_efficiency": round(self.token_efficiency, 3),
203
+ "avg_tokens_per_request": round(self.avg_tokens_per_request, 1),
204
+ "estimated_cost_usd": round(self.estimated_cost_usd, 4),
205
+ "success_rate": round(self.success_rate, 2),
206
+ "by_provider": dict(self.provider_tokens),
207
+ }
208
+
209
+
210
+ class MetricsCollector:
211
+ """
212
+ Centralized metrics collection for all CodeShield features.
213
+
214
+ Provides:
215
+ - Real-time statistics
216
+ - Persistent storage for historical data
217
+ - Transparent, verifiable metrics
218
+ """
219
+
220
+ _instance = None
221
+ _initialized = False
222
+
223
+ def __new__(cls):
224
+ if cls._instance is None:
225
+ cls._instance = super().__new__(cls)
226
+ return cls._instance
227
+
228
+ def __init__(self):
229
+ if MetricsCollector._initialized:
230
+ return
231
+
232
+ self.trustgate = TrustGateMetrics()
233
+ self.styleforge = StyleForgeMetrics()
234
+ self.contextvault = ContextVaultMetrics()
235
+ self.tokens = TokenMetrics()
236
+
237
+ self._session_start = datetime.now()
238
+ self._ensure_db()
239
+ self._load_from_db()
240
+
241
+ MetricsCollector._initialized = True
242
+
243
+ def _ensure_db(self):
244
+ """Ensure database exists with schema"""
245
+ DB_PATH.parent.mkdir(parents=True, exist_ok=True)
246
+
247
+ conn = sqlite3.connect(str(DB_PATH))
248
+ cursor = conn.cursor()
249
+
250
+ cursor.execute("""
251
+ CREATE TABLE IF NOT EXISTS metrics_history (
252
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
253
+ timestamp TEXT NOT NULL,
254
+ category TEXT NOT NULL,
255
+ metric_name TEXT NOT NULL,
256
+ metric_value REAL NOT NULL,
257
+ metadata TEXT
258
+ )
259
+ """)
260
+
261
+ cursor.execute("""
262
+ CREATE TABLE IF NOT EXISTS metrics_snapshot (
263
+ category TEXT PRIMARY KEY,
264
+ data TEXT NOT NULL,
265
+ updated_at TEXT NOT NULL
266
+ )
267
+ """)
268
+
269
+ conn.commit()
270
+ conn.close()
271
+
272
+ def _load_from_db(self):
273
+ """Load persisted metrics"""
274
+ try:
275
+ conn = sqlite3.connect(str(DB_PATH))
276
+ cursor = conn.cursor()
277
+
278
+ cursor.execute("SELECT category, data FROM metrics_snapshot")
279
+ rows = cursor.fetchall()
280
+
281
+ for category, data_json in rows:
282
+ data = json.loads(data_json)
283
+ if category == "trustgate":
284
+ for k, v in data.items():
285
+ if hasattr(self.trustgate, k) and not k.startswith("_"):
286
+ setattr(self.trustgate, k, v)
287
+ elif category == "styleforge":
288
+ for k, v in data.items():
289
+ if hasattr(self.styleforge, k) and not k.startswith("_"):
290
+ setattr(self.styleforge, k, v)
291
+ elif category == "contextvault":
292
+ for k, v in data.items():
293
+ if hasattr(self.contextvault, k) and not k.startswith("_"):
294
+ setattr(self.contextvault, k, v)
295
+ elif category == "tokens":
296
+ for k, v in data.items():
297
+ if hasattr(self.tokens, k) and not k.startswith("_") and k != "provider_tokens":
298
+ setattr(self.tokens, k, v)
299
+
300
+ conn.close()
301
+ except Exception as e:
302
+ print(f"Warning: Could not load metrics: {e}")
303
+
304
+ def _save_to_db(self):
305
+ """Persist current metrics"""
306
+ try:
307
+ conn = sqlite3.connect(str(DB_PATH))
308
+ cursor = conn.cursor()
309
+
310
+ now = datetime.now().isoformat()
311
+
312
+ snapshots = [
313
+ ("trustgate", json.dumps(asdict(self.trustgate))),
314
+ ("styleforge", json.dumps(asdict(self.styleforge))),
315
+ ("contextvault", json.dumps(asdict(self.contextvault))),
316
+ ("tokens", json.dumps({
317
+ "total_input_tokens": self.tokens.total_input_tokens,
318
+ "total_output_tokens": self.tokens.total_output_tokens,
319
+ "total_tokens": self.tokens.total_tokens,
320
+ "total_requests": self.tokens.total_requests,
321
+ "successful_requests": self.tokens.successful_requests,
322
+ "failed_requests": self.tokens.failed_requests,
323
+ })),
324
+ ]
325
+
326
+ for category, data in snapshots:
327
+ cursor.execute("""
328
+ INSERT OR REPLACE INTO metrics_snapshot (category, data, updated_at)
329
+ VALUES (?, ?, ?)
330
+ """, (category, data, now))
331
+
332
+ conn.commit()
333
+ conn.close()
334
+ except Exception as e:
335
+ print(f"Warning: Could not save metrics: {e}")
336
+
337
+ @contextmanager
338
+ def track_time(self, category: str):
339
+ """Context manager to track processing time"""
340
+ start = time.time()
341
+ yield
342
+ elapsed_ms = int((time.time() - start) * 1000)
343
+
344
+ with _metrics_lock:
345
+ if category == "trustgate":
346
+ self.trustgate.total_processing_time_ms += elapsed_ms
347
+ elif category == "styleforge":
348
+ self.styleforge.total_processing_time_ms += elapsed_ms
349
+
350
+ # TrustGate tracking
351
+ def track_verification(self, syntax_error: bool = False, missing_imports: int = 0,
352
+ undefined_names: int = 0, auto_fixed: bool = False):
353
+ """Track a TrustGate verification"""
354
+ with _metrics_lock:
355
+ self.trustgate.total_verifications += 1
356
+ if syntax_error:
357
+ self.trustgate.syntax_errors_detected += 1
358
+ self.trustgate.missing_imports_detected += missing_imports
359
+ self.trustgate.undefined_names_detected += undefined_names
360
+ if auto_fixed:
361
+ self.trustgate.auto_fixes_applied += 1
362
+ self._save_to_db()
363
+
364
+ def track_sandbox(self, success: bool):
365
+ """Track a sandbox execution"""
366
+ with _metrics_lock:
367
+ self.trustgate.sandbox_executions += 1
368
+ if success:
369
+ self.trustgate.sandbox_successes += 1
370
+ else:
371
+ self.trustgate.sandbox_failures += 1
372
+ self._save_to_db()
373
+
374
+ # StyleForge tracking
375
+ def track_style_check(self, conventions_found: int = 0, issues_found: int = 0,
376
+ corrections_suggested: int = 0, corrections_applied: int = 0):
377
+ """Track a StyleForge check"""
378
+ with _metrics_lock:
379
+ self.styleforge.total_checks += 1
380
+ self.styleforge.conventions_detected += conventions_found
381
+ self.styleforge.naming_issues_found += issues_found
382
+ self.styleforge.corrections_suggested += corrections_suggested
383
+ self.styleforge.corrections_applied += corrections_applied
384
+ self._save_to_db()
385
+
386
+ def track_codebase_analyzed(self):
387
+ """Track a codebase analysis"""
388
+ with _metrics_lock:
389
+ self.styleforge.codebases_analyzed += 1
390
+ self._save_to_db()
391
+
392
+ # ContextVault tracking
393
+ def track_context_save(self, files_count: int = 0):
394
+ """Track a context save"""
395
+ with _metrics_lock:
396
+ self.contextvault.total_contexts_saved += 1
397
+ self.contextvault.total_files_tracked += files_count
398
+ self._save_to_db()
399
+
400
+ def track_context_restore(self, success: bool):
401
+ """Track a context restore"""
402
+ with _metrics_lock:
403
+ self.contextvault.total_contexts_restored += 1
404
+ if success:
405
+ self.contextvault.restore_successes += 1
406
+ else:
407
+ self.contextvault.restore_failures += 1
408
+ self._save_to_db()
409
+
410
+ def track_context_delete(self):
411
+ """Track a context deletion"""
412
+ with _metrics_lock:
413
+ self.contextvault.contexts_deleted += 1
414
+ self._save_to_db()
415
+
416
+ # Token tracking
417
+ def track_tokens(self, provider: str, input_tokens: int, output_tokens: int,
418
+ success: bool = True):
419
+ """Track LLM token usage"""
420
+ with _metrics_lock:
421
+ self.tokens.total_input_tokens += input_tokens
422
+ self.tokens.total_output_tokens += output_tokens
423
+ self.tokens.total_tokens += input_tokens + output_tokens
424
+ self.tokens.total_requests += 1
425
+
426
+ if success:
427
+ self.tokens.successful_requests += 1
428
+ else:
429
+ self.tokens.failed_requests += 1
430
+
431
+ # Provider-specific tracking
432
+ if provider not in self.tokens.provider_tokens:
433
+ self.tokens.provider_tokens[provider] = {
434
+ "input": 0, "output": 0, "total": 0, "requests": 0, "cost_usd": 0.0
435
+ }
436
+
437
+ stats = self.tokens.provider_tokens[provider]
438
+ stats["input"] += input_tokens
439
+ stats["output"] += output_tokens
440
+ stats["total"] += input_tokens + output_tokens
441
+ stats["requests"] += 1
442
+
443
+ # Estimate cost
444
+ costs = self.tokens.COST_PER_1K.get(provider, {"input": 0.001, "output": 0.002})
445
+ stats["cost_usd"] += (input_tokens / 1000) * costs["input"]
446
+ stats["cost_usd"] += (output_tokens / 1000) * costs["output"]
447
+
448
+ self._save_to_db()
449
+
450
+ def get_summary(self) -> dict:
451
+ """Get comprehensive metrics summary"""
452
+ session_duration = (datetime.now() - self._session_start).total_seconds()
453
+
454
+ return {
455
+ "session": {
456
+ "started_at": self._session_start.isoformat(),
457
+ "duration_seconds": round(session_duration, 2),
458
+ "duration_human": str(timedelta(seconds=int(session_duration))),
459
+ },
460
+ "trustgate": self.trustgate.to_dict(),
461
+ "styleforge": self.styleforge.to_dict(),
462
+ "contextvault": self.contextvault.to_dict(),
463
+ "tokens": self.tokens.to_dict(),
464
+ "totals": {
465
+ "total_operations": (
466
+ self.trustgate.total_verifications +
467
+ self.styleforge.total_checks +
468
+ self.contextvault.total_contexts_saved +
469
+ self.contextvault.total_contexts_restored +
470
+ self.tokens.total_requests
471
+ ),
472
+ "total_issues_detected": (
473
+ self.trustgate.syntax_errors_detected +
474
+ self.trustgate.missing_imports_detected +
475
+ self.trustgate.undefined_names_detected +
476
+ self.styleforge.naming_issues_found
477
+ ),
478
+ "total_auto_fixes": (
479
+ self.trustgate.auto_fixes_applied +
480
+ self.styleforge.corrections_applied
481
+ ),
482
+ },
483
+ }
484
+
485
+ def reset(self):
486
+ """Reset all metrics (for testing)"""
487
+ with _metrics_lock:
488
+ self.trustgate = TrustGateMetrics()
489
+ self.styleforge = StyleForgeMetrics()
490
+ self.contextvault = ContextVaultMetrics()
491
+ self.tokens = TokenMetrics()
492
+ self._session_start = datetime.now()
493
+
494
+
495
+ # Singleton instance
496
+ def get_metrics() -> MetricsCollector:
497
+ """Get the global metrics collector instance"""
498
+ return MetricsCollector()
499
+
500
+
501
+ # Convenience decorators for automatic tracking
502
+ def track_trustgate_verification(func):
503
+ """Decorator to track TrustGate verifications"""
504
+ def wrapper(*args, **kwargs):
505
+ metrics = get_metrics()
506
+ with metrics.track_time("trustgate"):
507
+ result = func(*args, **kwargs)
508
+
509
+ # Extract metrics from result if it has the right attributes
510
+ if hasattr(result, 'issues'):
511
+ syntax_error = any(i.severity == "error" and "Syntax" in i.message for i in result.issues)
512
+ missing_imports = sum(1 for i in result.issues if "Missing import" in i.message)
513
+ undefined = sum(1 for i in result.issues if "undefined" in i.message.lower())
514
+ auto_fixed = result.fixed_code is not None
515
+
516
+ metrics.track_verification(
517
+ syntax_error=syntax_error,
518
+ missing_imports=missing_imports,
519
+ undefined_names=undefined,
520
+ auto_fixed=auto_fixed
521
+ )
522
+
523
+ return result
524
+ return wrapper
525
+
526
+
527
+ def track_styleforge_check(func):
528
+ """Decorator to track StyleForge checks"""
529
+ def wrapper(*args, **kwargs):
530
+ metrics = get_metrics()
531
+ with metrics.track_time("styleforge"):
532
+ result = func(*args, **kwargs)
533
+
534
+ if hasattr(result, 'issues'):
535
+ metrics.track_style_check(
536
+ conventions_found=len(result.conventions_detected) if hasattr(result, 'conventions_detected') else 0,
537
+ issues_found=len(result.issues),
538
+ corrections_suggested=len(result.issues),
539
+ corrections_applied=1 if result.corrected_code else 0
540
+ )
541
+
542
+ return result
543
+ return wrapper