devsquad 3.6.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.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. skills/test/handler.py +78 -0
@@ -0,0 +1,604 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ DevSquad History Manager
5
+
6
+ Historical data storage and retrieval using SQLite.
7
+
8
+ Features:
9
+ - Metrics snapshots storage
10
+ - Alert history tracking
11
+ - API request logging
12
+ - Time-series queries
13
+ - Automatic data retention/cleanup
14
+
15
+ Usage:
16
+ from scripts.history_manager import HistoryManager
17
+
18
+ history = HistoryManager()
19
+
20
+ # Store metrics snapshot
21
+ history.save_metrics_snapshot({
22
+ "completion_rate": 75.5,
23
+ "avg_response_time_ms": 150.2,
24
+ "cpu_usage": 45.3
25
+ })
26
+
27
+ # Query historical data
28
+ data = history.get_metrics_history(hours=24)
29
+ """
30
+
31
+ import json
32
+ import logging
33
+ import sqlite3
34
+ import sys
35
+ import warnings
36
+ from datetime import datetime, timedelta
37
+ from pathlib import Path
38
+ from typing import Any, Dict, List, Optional, Tuple
39
+
40
+ import os
41
+
42
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+
47
+ class HistoryManager:
48
+ """
49
+ SQLite-based history manager for DevSquad.
50
+
51
+ Stores and retrieves time-series data including:
52
+ - Performance metrics snapshots
53
+ - Alert history
54
+ - API request logs
55
+ - Lifecycle state changes
56
+ """
57
+
58
+ def __init__(self, db_path: Optional[str] = None):
59
+ """
60
+ Initialize HistoryManager.
61
+
62
+ Args:
63
+ db_path: Path to SQLite database file.
64
+ Defaults to data/devsquad_history.db
65
+ """
66
+ if db_path is None:
67
+ # Default path in project's data directory
68
+ data_dir = Path(__file__).parent.parent / "data"
69
+ data_dir.mkdir(exist_ok=True)
70
+ db_path = str(data_dir / "devsquad_history.db")
71
+
72
+ self.db_path = db_path
73
+ self.conn = self._get_connection()
74
+
75
+ # Initialize database schema
76
+ self._init_schema()
77
+
78
+ logger.info(f"HistoryManager initialized (db={db_path})")
79
+
80
+ def _get_connection(self) -> sqlite3.Connection:
81
+ """Get database connection with row factory."""
82
+ conn = sqlite3.connect(self.db_path)
83
+ conn.row_factory = sqlite3.Row
84
+ conn.execute("PRAGMA journal_mode=WAL") # Better concurrency
85
+ conn.execute("PRAGMA foreign_keys=ON")
86
+ return conn
87
+
88
+ def _init_schema(self):
89
+ """Initialize database tables if they don't exist."""
90
+ cursor = self.conn.cursor()
91
+
92
+ # Metrics snapshots table
93
+ cursor.execute("""
94
+ CREATE TABLE IF NOT EXISTS metrics_snapshots (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
97
+ total_phases INTEGER,
98
+ completed_phases INTEGER,
99
+ running_phases INTEGER,
100
+ failed_phases INTEGER,
101
+ completion_rate REAL,
102
+ avg_response_time_ms REAL,
103
+ p95_latency_ms REAL,
104
+ success_rate REAL,
105
+ cpu_usage_percent REAL,
106
+ memory_usage_percent REAL,
107
+ custom_data TEXT, -- JSON blob for additional data
108
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
109
+ )
110
+ """)
111
+
112
+ # Alert history table
113
+ cursor.execute("""
114
+ CREATE TABLE IF NOT EXISTS alert_history (
115
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
116
+ alert_id VARCHAR(12) NOT NULL,
117
+ severity VARCHAR(20) NOT NULL,
118
+ title TEXT NOT NULL,
119
+ message TEXT,
120
+ source VARCHAR(100),
121
+ channel VARCHAR(20),
122
+ acknowledged BOOLEAN DEFAULT 0,
123
+ resolved_at DATETIME,
124
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
125
+ UNIQUE(alert_id)
126
+ )
127
+ """)
128
+
129
+ # API logs table
130
+ cursor.execute("""
131
+ CREATE TABLE IF NOT EXISTS api_logs (
132
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
133
+ timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
134
+ method VARCHAR(10) NOT NULL,
135
+ path TEXT NOT NULL,
136
+ status_code INTEGER,
137
+ response_time_ms REAL,
138
+ client_ip VARCHAR(45),
139
+ user_agent TEXT,
140
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
141
+ )
142
+ """)
143
+
144
+ # Lifecycle events table
145
+ cursor.execute("""
146
+ CREATE TABLE IF NOT EXISTS lifecycle_events (
147
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
148
+ timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
149
+ event_type VARCHAR(50) NOT NULL,
150
+ phase_id VARCHAR(10),
151
+ previous_status VARCHAR(20),
152
+ new_status VARCHAR(20),
153
+ user_id VARCHAR(100),
154
+ details TEXT,
155
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
156
+ )
157
+ """)
158
+
159
+ # Create indexes for better query performance
160
+ indexes = [
161
+ "CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics_snapshots(timestamp)",
162
+ "CREATE INDEX IF NOT EXISTS idx_alerts_timestamp ON alert_history(created_at)",
163
+ "CREATE INDEX IF NOT EXISTS idx_alerts_severity ON alert_history(severity)",
164
+ "CREATE INDEX IF NOT EXISTS idx_api_logs_timestamp ON api_logs(timestamp)",
165
+ "CREATE INDEX IF NOT EXISTS idx_api_logs_path ON api_logs(path)",
166
+ "CREATE INDEX IF NOT EXISTS idx_lifecycle_events_timestamp ON lifecycle_events(timestamp)",
167
+ "CREATE INDEX IF NOT EXISTS idx_lifecycle_events_phase ON lifecycle_events(phase_id)"
168
+ ]
169
+
170
+ for idx_sql in indexes:
171
+ cursor.execute(idx_sql)
172
+
173
+ self.conn.commit()
174
+ logger.debug("Database schema initialized/verified")
175
+
176
+ def save_metrics_snapshot(self, metrics_data: Dict[str, Any]) -> bool:
177
+ """
178
+ Save a metrics snapshot to the database.
179
+
180
+ Args:
181
+ metrics_data: Dictionary containing metric values
182
+
183
+ Returns:
184
+ True if saved successfully, False otherwise
185
+ """
186
+ try:
187
+ cursor = self.conn.cursor()
188
+
189
+ # Extract known fields
190
+ custom_data = {
191
+ k: v for k, v in metrics_data.items()
192
+ if k not in [
193
+ 'total_phases', 'completed_phases', 'running_phases',
194
+ 'failed_phases', 'completion_rate', 'avg_response_time_ms',
195
+ 'p95_latency_ms', 'success_rate', 'cpu_usage_percent',
196
+ 'memory_usage_percent'
197
+ ]
198
+ }
199
+
200
+ cursor.execute("""
201
+ INSERT INTO metrics_snapshots (
202
+ timestamp, total_phases, completed_phases, running_phases,
203
+ failed_phases, completion_rate, avg_response_time_ms,
204
+ p95_latency_ms, success_rate, cpu_usage_percent,
205
+ memory_usage_percent, custom_data
206
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
207
+ """, (
208
+ datetime.now(),
209
+ metrics_data.get('total_phases'),
210
+ metrics_data.get('completed_phases'),
211
+ metrics_data.get('running_phases'),
212
+ metrics_data.get('failed_phases'),
213
+ metrics_data.get('completion_rate'),
214
+ metrics_data.get('avg_response_time_ms'),
215
+ metrics_data.get('p95_latency_ms'),
216
+ metrics_data.get('success_rate'),
217
+ metrics_data.get('cpu_usage_percent'),
218
+ metrics_data.get('memory_usage_percent'),
219
+ json.dumps(custom_data) if custom_data else None
220
+ ))
221
+
222
+ self.conn.commit()
223
+ return True
224
+
225
+ except Exception as e:
226
+ logger.error(f"Failed to save metrics snapshot: {e}")
227
+ return False
228
+
229
+ def get_metrics_history(
230
+ self,
231
+ hours: int = 24,
232
+ interval_minutes: int = 60,
233
+ include_custom: bool = False
234
+ ) -> List[Dict[str, Any]]:
235
+ """
236
+ Retrieve metrics history.
237
+
238
+ Args:
239
+ hours: Number of hours to look back
240
+ interval_minutes: Interval between data points
241
+ include_custom: Whether to include custom data field
242
+
243
+ Returns:
244
+ List of metric snapshot dictionaries
245
+ """
246
+ try:
247
+ cursor = self.conn.cursor()
248
+
249
+ cutoff = datetime.now() - timedelta(hours=hours)
250
+
251
+ # Query with sampling based on interval
252
+ cursor.execute("""
253
+ SELECT * FROM metrics_snapshots
254
+ WHERE timestamp >= ?
255
+ ORDER BY timestamp ASC
256
+ """, (cutoff,))
257
+
258
+ rows = cursor.fetchall()
259
+
260
+ # Sample data based on interval if too many points
261
+ if len(rows) > 500:
262
+ sample_every = max(1, len(rows) // 500)
263
+ rows = rows[::sample_every]
264
+
265
+ results = []
266
+ for row in rows:
267
+ data = dict(row)
268
+
269
+ # Parse custom data if requested
270
+ if include_custom and data.get('custom_data'):
271
+ try:
272
+ data['custom_data'] = json.loads(data['custom_data'])
273
+ except (json.JSONDecodeError, TypeError):
274
+ pass
275
+
276
+ # Convert timestamp to string for JSON serialization
277
+ if isinstance(data.get('timestamp'), datetime):
278
+ data['timestamp'] = data['timestamp'].isoformat()
279
+
280
+ results.append(data)
281
+
282
+ logger.debug(f"Retrieved {len(results)} metrics snapshots")
283
+ return results
284
+
285
+ except Exception as e:
286
+ logger.error(f"Failed to get metrics history: {e}")
287
+ return []
288
+
289
+ def log_api_request(
290
+ self,
291
+ method: str,
292
+ path: str,
293
+ status_code: int,
294
+ response_time_ms: float,
295
+ client_ip: Optional[str] = None,
296
+ user_agent: Optional[str] = None
297
+ ) -> bool:
298
+ """
299
+ Log an API request to the database.
300
+
301
+ Args:
302
+ method: HTTP method (GET, POST, etc.)
303
+ path: Request path
304
+ status_code: HTTP status code
305
+ response_time_ms: Response time in milliseconds
306
+ client_ip: Client IP address
307
+ user_agent: User agent string
308
+
309
+ Returns:
310
+ True if logged successfully
311
+ """
312
+ try:
313
+ cursor = self.conn.cursor()
314
+ cursor.execute("""
315
+ INSERT INTO api_logs (
316
+ method, path, status_code, response_time_ms,
317
+ client_ip, user_agent
318
+ ) VALUES (?, ?, ?, ?, ?, ?)
319
+ """, (method, path, status_code, response_time_ms, client_ip, user_agent))
320
+
321
+ self.conn.commit()
322
+ return True
323
+
324
+ except Exception as e:
325
+ logger.error(f"Failed to log API request: {e}")
326
+ return False
327
+
328
+ def get_api_stats(
329
+ self,
330
+ hours: int = 1,
331
+ group_by_path: bool = True
332
+ ) -> Dict[str, Any]:
333
+ """
334
+ Get API request statistics.
335
+
336
+ Args:
337
+ hours: Statistics period in hours
338
+ group_by_path: Whether to group by endpoint path
339
+
340
+ Returns:
341
+ Dictionary with API statistics
342
+ """
343
+ try:
344
+ cursor = self.conn.cursor()
345
+ cutoff = datetime.now() - timedelta(hours=hours)
346
+
347
+ # Total requests
348
+ cursor.execute("""
349
+ SELECT COUNT(*) as total,
350
+ AVG(response_time_ms) as avg_response,
351
+ MAX(response_time_ms) as max_response,
352
+ MIN(response_time_ms) as min_response
353
+ FROM api_logs
354
+ WHERE timestamp >= ?
355
+ """, (cutoff,))
356
+
357
+ row = cursor.fetchone()
358
+ stats = {
359
+ "total_requests": row['total'] or 0,
360
+ "avg_response_time_ms": round(row['avg_response'], 2) if row['avg_response'] else 0,
361
+ "max_response_time_ms": round(row['max_response'], 2) if row['max_response'] else 0,
362
+ "min_response_time_ms": round(row['min_response'], 2) if row['min_response'] else 0,
363
+ "period_hours": hours
364
+ }
365
+
366
+ # Status code distribution
367
+ cursor.execute("""
368
+ SELECT status_code, COUNT(*) as count
369
+ FROM api_logs
370
+ WHERE timestamp >= ?
371
+ GROUP BY status_code
372
+ ORDER BY count DESC
373
+ """, (cutoff,))
374
+
375
+ stats["status_codes"] = [
376
+ {"status_code": row['status_code'], "count": row['count']}
377
+ for row in cursor.fetchall()
378
+ ]
379
+
380
+ # By endpoint (if requested)
381
+ if group_by_path:
382
+ cursor.execute("""
383
+ SELECT path, COUNT(*) as count,
384
+ AVG(response_time_ms) as avg_response
385
+ FROM api_logs
386
+ WHERE timestamp >= ?
387
+ GROUP BY path
388
+ ORDER BY count DESC
389
+ LIMIT 20
390
+ """, (cutoff,))
391
+
392
+ stats["endpoints"] = [
393
+ {
394
+ "path": row['path'],
395
+ "requests": row['count'],
396
+ "avg_response_ms": round(row['avg_response'], 2) if row['avg_response'] else 0
397
+ }
398
+ for row in cursor.fetchall()
399
+ ]
400
+
401
+ return stats
402
+
403
+ except Exception as e:
404
+ logger.error(f"Failed to get API stats: {e}")
405
+ return {}
406
+
407
+ def save_lifecycle_event(
408
+ self,
409
+ event_type: str,
410
+ phase_id: Optional[str] = None,
411
+ previous_status: Optional[str] = None,
412
+ new_status: Optional[str] = None,
413
+ user_id: Optional[str] = None,
414
+ details: Optional[str] = None
415
+ ) -> bool:
416
+ """
417
+ Save a lifecycle state change event.
418
+
419
+ Args:
420
+ event_type: Type of event (phase_advance, phase_complete, etc.)
421
+ phase_id: Phase identifier
422
+ previous_status: Status before change
423
+ new_status: Status after change
424
+ user_id: User who triggered the event
425
+ details: Additional details
426
+
427
+ Returns:
428
+ True if saved successfully
429
+ """
430
+ try:
431
+ cursor = self.conn.cursor()
432
+ cursor.execute("""
433
+ INSERT INTO lifecycle_events (
434
+ event_type, phase_id, previous_status,
435
+ new_status, user_id, details
436
+ ) VALUES (?, ?, ?, ?, ?, ?)
437
+ """, (event_type, phase_id, previous_status, new_status, user_id, details))
438
+
439
+ self.conn.commit()
440
+ return True
441
+
442
+ except Exception as e:
443
+ logger.error(f"Failed to save lifecycle event: {e}")
444
+ return False
445
+
446
+ def get_lifecycle_history(
447
+ self,
448
+ hours: int = 24,
449
+ phase_id: Optional[str] = None,
450
+ event_type: Optional[str] = None
451
+ ) -> List[Dict[str, Any]]:
452
+ """
453
+ Get lifecycle event history.
454
+
455
+ Args:
456
+ hours: Look back period
457
+ phase_id: Filter by specific phase
458
+ event_type: Filter by event type
459
+
460
+ Returns:
461
+ List of lifecycle event dictionaries
462
+ """
463
+ try:
464
+ cursor = self.conn.cursor()
465
+ cutoff = datetime.now() - timedelta(hours=hours)
466
+
467
+ query = """
468
+ SELECT * FROM lifecycle_events
469
+ WHERE timestamp >= ?
470
+ """
471
+ params = [cutoff]
472
+
473
+ if phase_id:
474
+ query += " AND phase_id = ?"
475
+ params.append(phase_id)
476
+
477
+ if event_type:
478
+ query += " AND event_type = ?"
479
+ params.append(event_type)
480
+
481
+ query += " ORDER BY timestamp DESC LIMIT 200"
482
+
483
+ cursor.execute(query, params)
484
+
485
+ return [dict(row) for row in cursor.fetchall()]
486
+
487
+ except Exception as e:
488
+ logger.error(f"Failed to get lifecycle history: {e}")
489
+ return []
490
+
491
+ def cleanup_old_data(self, retention_days: int = 30) -> Dict[str, int]:
492
+ """
493
+ Remove old data beyond retention period.
494
+
495
+ Args:
496
+ retention_days: Number of days to keep data
497
+
498
+ Returns:
499
+ Dictionary with counts of deleted records per table
500
+ """
501
+ try:
502
+ cursor = self.conn.cursor()
503
+ cutoff = datetime.now() - timedelta(days=retention_days)
504
+
505
+ deleted = {}
506
+
507
+ tables = ['metrics_snapshots', 'alert_history', 'api_logs', 'lifecycle_events']
508
+ for table in tables:
509
+ cursor.execute(f"DELETE FROM {table} WHERE timestamp < ?", (cutoff,))
510
+ deleted[table] = cursor.rowcount
511
+
512
+ self.conn.commit()
513
+
514
+ total_deleted = sum(deleted.values())
515
+ logger.info(f"Cleaned up {total_deleted} old records (retention={retention_days} days)")
516
+
517
+ return deleted
518
+
519
+ except Exception as e:
520
+ logger.error(f"Failed to cleanup old data: {e}")
521
+ return {}
522
+
523
+ def get_database_size(self) -> Dict[str, Any]:
524
+ """
525
+ Get database size information.
526
+
527
+ Returns:
528
+ Dictionary with size statistics
529
+ """
530
+ try:
531
+ cursor = self.conn.cursor()
532
+
533
+ # Table sizes
534
+ table_sizes = {}
535
+ tables = ['metrics_snapshots', 'alert_history', 'api_logs', 'lifecycle_events']
536
+
537
+ for table in tables:
538
+ cursor.execute(f"SELECT COUNT(*) as count FROM {table}")
539
+ table_sizes[table] = cursor.fetchone()['count']
540
+
541
+ # File size
542
+ db_file = Path(self.db_path)
543
+ file_size_mb = db_file.stat().st_size / (1024 * 1024) if db_file.exists() else 0
544
+
545
+ return {
546
+ "file_path": self.db_path,
547
+ "file_size_mb": round(file_size_mb, 2),
548
+ "tables": table_sizes,
549
+ "total_records": sum(table_sizes.values())
550
+ }
551
+
552
+ except Exception as e:
553
+ logger.error(f"Failed to get database size: {e}")
554
+ return {}
555
+
556
+ def close(self):
557
+ """Close database connection."""
558
+ if self.conn:
559
+ self.conn.close()
560
+ logger.info("Database connection closed")
561
+
562
+
563
+ if __name__ == "__main__":
564
+ # Demo: Test history manager
565
+ print("\nšŸ“Š DevSquad History Manager Demo\n")
566
+ print("=" * 50)
567
+
568
+ history = HistoryManager()
569
+
570
+ # Save some test data
571
+ test_metrics = {
572
+ "total_phases": 11,
573
+ "completed_phases": 7,
574
+ "running_phases": 1,
575
+ "failed_phases": 0,
576
+ "completion_rate": 63.6,
577
+ "avg_response_time_ms": 150.5,
578
+ "cpu_usage_percent": 45.2,
579
+ "memory_usage_percent": 62.8,
580
+ "custom_field": "test_value"
581
+ }
582
+
583
+ print("\nSaving test metrics snapshot...")
584
+ history.save_metrics_snapshot(test_metrics)
585
+ print("āœ… Metrics saved!")
586
+
587
+ # Log an API request
588
+ print("\nLogging test API request...")
589
+ history.log_api_request("GET", "/api/v1/lifecycle/phases", 200, 45.2)
590
+ print("āœ… API request logged!")
591
+
592
+ # Query data back
593
+ print("\nQuerying metrics history...")
594
+ data = history.get_metrics_history(hours=1)
595
+ print(f"āœ… Retrieved {len(data)} snapshots")
596
+
597
+ # Show database info
598
+ print("\nšŸ“ˆ Database Info:")
599
+ db_info = history.get_database_size()
600
+ for key, value in db_info.items():
601
+ print(f" {key}: {value}")
602
+
603
+ history.close()
604
+ print("\nāœ… Demo completed!")