empathy-framework 4.7.0__py3-none-any.whl → 4.8.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 (86) hide show
  1. empathy_framework-4.8.0.dist-info/METADATA +753 -0
  2. {empathy_framework-4.7.0.dist-info → empathy_framework-4.8.0.dist-info}/RECORD +83 -37
  3. {empathy_framework-4.7.0.dist-info → empathy_framework-4.8.0.dist-info}/WHEEL +1 -1
  4. {empathy_framework-4.7.0.dist-info → empathy_framework-4.8.0.dist-info}/entry_points.txt +2 -1
  5. empathy_os/__init__.py +2 -0
  6. empathy_os/cache/hash_only.py +6 -3
  7. empathy_os/cache/hybrid.py +6 -3
  8. empathy_os/cli/__init__.py +128 -238
  9. empathy_os/cli/__main__.py +5 -33
  10. empathy_os/cli/commands/__init__.py +1 -8
  11. empathy_os/cli/commands/help.py +331 -0
  12. empathy_os/cli/commands/info.py +140 -0
  13. empathy_os/cli/commands/inspect.py +437 -0
  14. empathy_os/cli/commands/metrics.py +92 -0
  15. empathy_os/cli/commands/orchestrate.py +184 -0
  16. empathy_os/cli/commands/patterns.py +207 -0
  17. empathy_os/cli/commands/provider.py +93 -81
  18. empathy_os/cli/commands/setup.py +96 -0
  19. empathy_os/cli/commands/status.py +235 -0
  20. empathy_os/cli/commands/sync.py +166 -0
  21. empathy_os/cli/commands/tier.py +121 -0
  22. empathy_os/cli/commands/workflow.py +574 -0
  23. empathy_os/cli/parsers/__init__.py +62 -0
  24. empathy_os/cli/parsers/help.py +41 -0
  25. empathy_os/cli/parsers/info.py +26 -0
  26. empathy_os/cli/parsers/inspect.py +66 -0
  27. empathy_os/cli/parsers/metrics.py +42 -0
  28. empathy_os/cli/parsers/orchestrate.py +61 -0
  29. empathy_os/cli/parsers/patterns.py +54 -0
  30. empathy_os/cli/parsers/provider.py +40 -0
  31. empathy_os/cli/parsers/setup.py +42 -0
  32. empathy_os/cli/parsers/status.py +47 -0
  33. empathy_os/cli/parsers/sync.py +31 -0
  34. empathy_os/cli/parsers/tier.py +33 -0
  35. empathy_os/cli/parsers/workflow.py +77 -0
  36. empathy_os/cli/utils/__init__.py +1 -0
  37. empathy_os/cli/utils/data.py +242 -0
  38. empathy_os/cli/utils/helpers.py +68 -0
  39. empathy_os/{cli.py → cli_legacy.py} +27 -27
  40. empathy_os/cli_minimal.py +662 -0
  41. empathy_os/cli_router.py +384 -0
  42. empathy_os/cli_unified.py +38 -2
  43. empathy_os/memory/__init__.py +19 -5
  44. empathy_os/memory/short_term.py +14 -404
  45. empathy_os/memory/types.py +437 -0
  46. empathy_os/memory/unified.py +61 -48
  47. empathy_os/models/fallback.py +1 -1
  48. empathy_os/models/provider_config.py +59 -344
  49. empathy_os/models/registry.py +31 -180
  50. empathy_os/monitoring/alerts.py +14 -20
  51. empathy_os/monitoring/alerts_cli.py +24 -7
  52. empathy_os/project_index/__init__.py +2 -0
  53. empathy_os/project_index/index.py +210 -5
  54. empathy_os/project_index/scanner.py +45 -14
  55. empathy_os/project_index/scanner_parallel.py +291 -0
  56. empathy_os/socratic/ab_testing.py +1 -1
  57. empathy_os/vscode_bridge 2.py +173 -0
  58. empathy_os/workflows/__init__.py +31 -2
  59. empathy_os/workflows/base.py +349 -325
  60. empathy_os/workflows/bug_predict.py +8 -0
  61. empathy_os/workflows/builder.py +273 -0
  62. empathy_os/workflows/caching.py +253 -0
  63. empathy_os/workflows/code_review_pipeline.py +1 -0
  64. empathy_os/workflows/history.py +510 -0
  65. empathy_os/workflows/output.py +410 -0
  66. empathy_os/workflows/perf_audit.py +125 -19
  67. empathy_os/workflows/progress.py +324 -22
  68. empathy_os/workflows/progressive/README 2.md +454 -0
  69. empathy_os/workflows/progressive/__init__ 2.py +92 -0
  70. empathy_os/workflows/progressive/cli 2.py +242 -0
  71. empathy_os/workflows/progressive/core 2.py +488 -0
  72. empathy_os/workflows/progressive/orchestrator 2.py +701 -0
  73. empathy_os/workflows/progressive/reports 2.py +528 -0
  74. empathy_os/workflows/progressive/telemetry 2.py +280 -0
  75. empathy_os/workflows/progressive/test_gen 2.py +514 -0
  76. empathy_os/workflows/progressive/workflow 2.py +628 -0
  77. empathy_os/workflows/routing.py +168 -0
  78. empathy_os/workflows/secure_release.py +1 -0
  79. empathy_os/workflows/security_audit.py +190 -0
  80. empathy_os/workflows/security_audit_phase3.py +328 -0
  81. empathy_os/workflows/telemetry_mixin.py +269 -0
  82. empathy_framework-4.7.0.dist-info/METADATA +0 -1598
  83. empathy_os/dashboard/__init__.py +0 -15
  84. empathy_os/dashboard/server.py +0 -941
  85. {empathy_framework-4.7.0.dist-info → empathy_framework-4.8.0.dist-info}/licenses/LICENSE +0 -0
  86. {empathy_framework-4.7.0.dist-info → empathy_framework-4.8.0.dist-info}/top_level.txt +0 -0
@@ -1,941 +0,0 @@
1
- """Dashboard Server for Empathy Framework
2
-
3
- Lightweight web server for viewing patterns, costs, and health.
4
- Uses built-in http.server to avoid external dependencies.
5
-
6
- Copyright 2025 Smart-AI-Memory
7
- Licensed under Fair Source License 0.9
8
- """
9
-
10
- import http.server
11
- import json
12
- import socketserver
13
- import threading
14
- import webbrowser
15
- from datetime import datetime
16
- from pathlib import Path
17
- from urllib.parse import urlparse
18
-
19
- # Try to import optional dependencies
20
- try:
21
- from empathy_os.cost_tracker import CostTracker
22
-
23
- HAS_COST_TRACKER = True
24
- except ImportError:
25
- CostTracker = None # type: ignore[misc, assignment]
26
- HAS_COST_TRACKER = False
27
-
28
- try:
29
- from empathy_os.discovery import DiscoveryEngine
30
-
31
- HAS_DISCOVERY = True
32
- except ImportError:
33
- DiscoveryEngine = None # type: ignore[misc, assignment]
34
- HAS_DISCOVERY = False
35
-
36
- try:
37
- from empathy_os.workflows import get_workflow_stats, list_workflows
38
-
39
- HAS_WORKFLOWS = True
40
- except ImportError:
41
- get_workflow_stats = None # type: ignore[assignment]
42
- list_workflows = None # type: ignore[assignment]
43
- HAS_WORKFLOWS = False
44
-
45
- try:
46
- from empathy_os.models.telemetry import TelemetryStore
47
-
48
- HAS_TELEMETRY = True
49
- except ImportError:
50
- TelemetryStore = None # type: ignore[misc, assignment]
51
- HAS_TELEMETRY = False
52
-
53
-
54
- class DashboardHandler(http.server.BaseHTTPRequestHandler):
55
- """HTTP request handler for the dashboard."""
56
-
57
- patterns_dir = "./patterns"
58
- empathy_dir = ".empathy"
59
-
60
- def log_message(self, format, *args):
61
- """Suppress default logging."""
62
-
63
- def do_GET(self):
64
- """Handle GET requests."""
65
- parsed = urlparse(self.path)
66
- path = parsed.path
67
-
68
- if path == "/" or path == "/index.html":
69
- self._serve_dashboard()
70
- elif path == "/api/patterns":
71
- self._serve_patterns()
72
- elif path == "/api/costs":
73
- self._serve_costs()
74
- elif path == "/api/stats":
75
- self._serve_stats()
76
- elif path == "/api/health":
77
- self._serve_health()
78
- elif path == "/api/workflows":
79
- self._serve_workflows()
80
- elif path == "/api/tests":
81
- self._serve_tests()
82
- else:
83
- self.send_error(404, "Not Found")
84
-
85
- def _serve_dashboard(self):
86
- """Serve the main dashboard HTML."""
87
- html = self._generate_dashboard_html()
88
- self.send_response(200)
89
- self.send_header("Content-type", "text/html")
90
- self.send_header("Content-Length", len(html))
91
- self.end_headers()
92
- self.wfile.write(html.encode())
93
-
94
- def _serve_patterns(self):
95
- """Serve patterns as JSON."""
96
- patterns = self._load_patterns()
97
- self._send_json(patterns)
98
-
99
- def _serve_costs(self):
100
- """Serve cost data as JSON."""
101
- if HAS_COST_TRACKER and CostTracker is not None:
102
- tracker = CostTracker(self.empathy_dir)
103
- data = tracker.get_summary(30)
104
- else:
105
- data = {"error": "Cost tracking not available"}
106
- self._send_json(data)
107
-
108
- def _serve_stats(self):
109
- """Serve discovery stats as JSON."""
110
- if HAS_DISCOVERY and DiscoveryEngine is not None:
111
- engine = DiscoveryEngine(self.empathy_dir)
112
- data = engine.get_stats()
113
- else:
114
- data = {"error": "Discovery not available"}
115
- self._send_json(data)
116
-
117
- def _serve_health(self):
118
- """Serve health check."""
119
- self._send_json({"status": "healthy", "timestamp": datetime.now().isoformat()})
120
-
121
- def _serve_workflows(self):
122
- """Serve workflow stats as JSON."""
123
- if HAS_WORKFLOWS and get_workflow_stats is not None:
124
- data = get_workflow_stats()
125
- # Add available workflows
126
- if list_workflows is not None:
127
- data["available_workflows"] = list_workflows()
128
- else:
129
- data = {"error": "Workflows not available"}
130
- self._send_json(data)
131
-
132
- def _serve_tests(self):
133
- """Serve test tracking data as JSON."""
134
- data = self._get_test_stats()
135
- self._send_json(data)
136
-
137
- def _get_test_stats(self) -> dict:
138
- """Get test tracking statistics.
139
-
140
- Returns:
141
- Dictionary with test tracking data including:
142
- - total_files: Total files with test records
143
- - passed_files: Files with passing tests
144
- - failed_files: Files with failing tests
145
- - coverage_avg: Average coverage percentage
146
- - recent_tests: Recent test executions
147
- - files_needing_tests: Files that need attention
148
- """
149
- if not HAS_TELEMETRY or TelemetryStore is None:
150
- return {"error": "Telemetry not available"}
151
-
152
- try:
153
- store = TelemetryStore(Path(self.empathy_dir))
154
-
155
- # Get file test records and convert to dicts
156
- file_tests_raw = store.get_file_tests(limit=100)
157
- file_tests = [t.to_dict() if hasattr(t, "to_dict") else t for t in file_tests_raw]
158
-
159
- # Calculate stats
160
- total_files = len(file_tests)
161
- passed_files = sum(1 for t in file_tests if t.get("last_test_result") == "passed")
162
- failed_files = sum(1 for t in file_tests if t.get("last_test_result") == "failed")
163
-
164
- # Coverage average (field is coverage_percent)
165
- coverages = [
166
- t.get("coverage_percent", 0) for t in file_tests if t.get("coverage_percent")
167
- ]
168
- coverage_avg = sum(coverages) / len(coverages) if coverages else 0
169
-
170
- # If no coverage data, use pass rate as a proxy
171
- if coverage_avg == 0 and total_files > 0:
172
- coverage_avg = (passed_files / total_files) * 100
173
-
174
- # Get files needing attention (failed or stale) and convert to dicts
175
- files_needing_raw = store.get_files_needing_tests(stale_only=False, failed_only=False)
176
- files_needing_tests = [
177
- t.to_dict() if hasattr(t, "to_dict") else t for t in files_needing_raw[:10]
178
- ]
179
-
180
- # Get recent test executions and convert to dicts
181
- recent_raw = store.get_test_executions(limit=10)
182
- recent_executions = [t.to_dict() if hasattr(t, "to_dict") else t for t in recent_raw]
183
-
184
- return {
185
- "total_files": total_files,
186
- "passed_files": passed_files,
187
- "failed_files": failed_files,
188
- "coverage_avg": round(coverage_avg, 1),
189
- "files_needing_tests": files_needing_tests,
190
- "recent_executions": recent_executions,
191
- "file_tests": file_tests[:20], # Most recent 20
192
- }
193
- except Exception as e:
194
- return {"error": str(e)}
195
-
196
- def _send_json(self, data):
197
- """Send JSON response."""
198
- content = json.dumps(data, indent=2, default=str)
199
- self.send_response(200)
200
- self.send_header("Content-type", "application/json")
201
- self.send_header("Content-Length", len(content))
202
- self.end_headers()
203
- self.wfile.write(content.encode())
204
-
205
- def _load_patterns(self) -> dict:
206
- """Load patterns from disk."""
207
- patterns = {
208
- "debugging": [],
209
- "security": [],
210
- "tech_debt": {"snapshots": []},
211
- "inspection": [],
212
- }
213
-
214
- patterns_path = Path(self.patterns_dir)
215
- if not patterns_path.exists():
216
- return patterns
217
-
218
- for name in ["debugging", "security", "tech_debt", "inspection"]:
219
- file_path = patterns_path / f"{name}.json"
220
- if file_path.exists():
221
- try:
222
- with open(file_path) as f:
223
- data = json.load(f)
224
- patterns[name] = data
225
- except (OSError, json.JSONDecodeError):
226
- pass
227
-
228
- return patterns
229
-
230
- def _generate_dashboard_html(self) -> str:
231
- """Generate the dashboard HTML page."""
232
- patterns = self._load_patterns()
233
-
234
- # Count patterns (handle both dict and list formats)
235
- debugging_data = patterns.get("debugging", {})
236
- if isinstance(debugging_data, dict):
237
- bug_count = len(debugging_data.get("patterns", []))
238
- else:
239
- bug_count = len(debugging_data) if isinstance(debugging_data, list) else 0
240
-
241
- security_data = patterns.get("security", {})
242
- if isinstance(security_data, dict):
243
- security_count = len(security_data.get("decisions", []))
244
- else:
245
- security_count = len(security_data) if isinstance(security_data, list) else 0
246
-
247
- debt_items = 0
248
- tech_debt_data = patterns.get("tech_debt", {})
249
- if isinstance(tech_debt_data, dict):
250
- snapshots = tech_debt_data.get("snapshots", [])
251
- if snapshots:
252
- debt_items = snapshots[-1].get("total_items", 0)
253
-
254
- # Get cost summary
255
- cost_summary = {"savings": 0, "savings_percent": 0, "requests": 0}
256
- if CostTracker is not None:
257
- try:
258
- tracker = CostTracker(self.empathy_dir)
259
- cost_summary = tracker.get_summary(30) # noqa: F841
260
- except Exception: # noqa: BLE001
261
- # INTENTIONAL: Dashboard should render even if cost tracking unavailable.
262
- pass
263
-
264
- # Get workflow stats
265
- workflow_stats = {
266
- "total_runs": 0,
267
- "by_workflow": {},
268
- "by_tier": {"cheap": 0, "capable": 0, "premium": 0},
269
- "recent_runs": [],
270
- "total_savings": 0.0,
271
- "avg_savings_percent": 0.0,
272
- }
273
- if HAS_WORKFLOWS and get_workflow_stats is not None:
274
- try:
275
- workflow_stats = get_workflow_stats()
276
- except Exception: # noqa: BLE001
277
- # INTENTIONAL: Dashboard should render even if workflow stats unavailable.
278
- pass
279
-
280
- # Get test stats
281
- test_stats = self._get_test_stats()
282
-
283
- return f"""<!DOCTYPE html>
284
- <html lang="en">
285
- <head>
286
- <meta charset="UTF-8">
287
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
288
- <title>Empathy Framework Dashboard</title>
289
- <style>
290
- :root {{
291
- --primary: #4f46e5;
292
- --success: #10b981;
293
- --warning: #f59e0b;
294
- --danger: #ef4444;
295
- --bg: #f3f4f6;
296
- --card-bg: #ffffff;
297
- --text: #1f2937;
298
- --text-muted: #6b7280;
299
- }}
300
-
301
- * {{
302
- margin: 0;
303
- padding: 0;
304
- box-sizing: border-box;
305
- }}
306
-
307
- body {{
308
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
309
- background: var(--bg);
310
- color: var(--text);
311
- line-height: 1.6;
312
- }}
313
-
314
- .container {{
315
- max-width: 1200px;
316
- margin: 0 auto;
317
- padding: 2rem;
318
- }}
319
-
320
- header {{
321
- text-align: center;
322
- margin-bottom: 2rem;
323
- }}
324
-
325
- h1 {{
326
- color: var(--primary);
327
- font-size: 2rem;
328
- margin-bottom: 0.5rem;
329
- }}
330
-
331
- .subtitle {{
332
- color: var(--text-muted);
333
- }}
334
-
335
- .grid {{
336
- display: grid;
337
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
338
- gap: 1.5rem;
339
- margin-bottom: 2rem;
340
- }}
341
-
342
- .card {{
343
- background: var(--card-bg);
344
- border-radius: 12px;
345
- padding: 1.5rem;
346
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
347
- }}
348
-
349
- .card h2 {{
350
- font-size: 0.875rem;
351
- color: var(--text-muted);
352
- text-transform: uppercase;
353
- letter-spacing: 0.05em;
354
- margin-bottom: 0.5rem;
355
- }}
356
-
357
- .card .value {{
358
- font-size: 2.5rem;
359
- font-weight: 700;
360
- color: var(--text);
361
- }}
362
-
363
- .card .label {{
364
- color: var(--text-muted);
365
- font-size: 0.875rem;
366
- }}
367
-
368
- .card.success .value {{
369
- color: var(--success);
370
- }}
371
-
372
- .card.warning .value {{
373
- color: var(--warning);
374
- }}
375
-
376
- .section {{
377
- background: var(--card-bg);
378
- border-radius: 12px;
379
- padding: 1.5rem;
380
- margin-bottom: 1.5rem;
381
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
382
- }}
383
-
384
- .section h2 {{
385
- font-size: 1.25rem;
386
- margin-bottom: 1rem;
387
- color: var(--text);
388
- }}
389
-
390
- table {{
391
- width: 100%;
392
- border-collapse: collapse;
393
- }}
394
-
395
- th, td {{
396
- text-align: left;
397
- padding: 0.75rem;
398
- border-bottom: 1px solid var(--bg);
399
- }}
400
-
401
- th {{
402
- color: var(--text-muted);
403
- font-weight: 500;
404
- font-size: 0.875rem;
405
- }}
406
-
407
- .status {{
408
- display: inline-block;
409
- padding: 0.25rem 0.75rem;
410
- border-radius: 9999px;
411
- font-size: 0.75rem;
412
- font-weight: 500;
413
- }}
414
-
415
- .status.resolved {{
416
- background: #d1fae5;
417
- color: #065f46;
418
- }}
419
-
420
- .status.investigating {{
421
- background: #fef3c7;
422
- color: #92400e;
423
- }}
424
-
425
- .commands {{
426
- display: flex;
427
- flex-wrap: wrap;
428
- gap: 0.5rem;
429
- margin-top: 1rem;
430
- }}
431
-
432
- .command {{
433
- background: var(--bg);
434
- padding: 0.5rem 1rem;
435
- border-radius: 6px;
436
- font-family: monospace;
437
- font-size: 0.875rem;
438
- }}
439
-
440
- .workflow-grid {{
441
- display: grid;
442
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
443
- gap: 1rem;
444
- margin-bottom: 1.5rem;
445
- }}
446
-
447
- .workflow-card {{
448
- background: var(--bg);
449
- border-radius: 8px;
450
- padding: 1rem;
451
- text-align: center;
452
- }}
453
-
454
- .workflow-card h3 {{
455
- font-size: 0.875rem;
456
- color: var(--text);
457
- margin-bottom: 0.5rem;
458
- }}
459
-
460
- .workflow-card .runs {{
461
- font-size: 1.5rem;
462
- font-weight: 600;
463
- color: var(--primary);
464
- }}
465
-
466
- .workflow-card .savings {{
467
- font-size: 0.875rem;
468
- color: var(--success);
469
- }}
470
-
471
- .tier-bar {{
472
- display: flex;
473
- height: 24px;
474
- border-radius: 4px;
475
- overflow: hidden;
476
- margin: 1rem 0;
477
- }}
478
-
479
- .tier-bar .tier {{
480
- display: flex;
481
- align-items: center;
482
- justify-content: center;
483
- font-size: 0.75rem;
484
- font-weight: 500;
485
- color: white;
486
- }}
487
-
488
- .tier-bar .cheap {{ background: #10b981; }}
489
- .tier-bar .capable {{ background: #3b82f6; }}
490
- .tier-bar .premium {{ background: #8b5cf6; }}
491
-
492
- .recent-run {{
493
- display: flex;
494
- align-items: center;
495
- gap: 1rem;
496
- padding: 0.75rem 0;
497
- border-bottom: 1px solid var(--bg);
498
- }}
499
-
500
- .recent-run:last-child {{
501
- border-bottom: none;
502
- }}
503
-
504
- .recent-run .name {{
505
- font-weight: 500;
506
- min-width: 100px;
507
- }}
508
-
509
- .recent-run .provider {{
510
- font-size: 0.75rem;
511
- color: var(--text-muted);
512
- background: var(--bg);
513
- padding: 0.125rem 0.5rem;
514
- border-radius: 4px;
515
- }}
516
-
517
- .recent-run .result {{
518
- margin-left: auto;
519
- display: flex;
520
- gap: 1rem;
521
- align-items: center;
522
- }}
523
-
524
- .recent-run .savings-badge {{
525
- font-size: 0.75rem;
526
- color: var(--success);
527
- font-weight: 500;
528
- }}
529
-
530
- .recent-run .time {{
531
- font-size: 0.75rem;
532
- color: var(--text-muted);
533
- }}
534
-
535
- footer {{
536
- text-align: center;
537
- color: var(--text-muted);
538
- font-size: 0.875rem;
539
- padding: 2rem 0;
540
- }}
541
-
542
- @media (prefers-color-scheme: dark) {{
543
- :root {{
544
- --bg: #111827;
545
- --card-bg: #1f2937;
546
- --text: #f9fafb;
547
- --text-muted: #9ca3af;
548
- }}
549
- }}
550
- </style>
551
- </head>
552
- <body>
553
- <div class="container">
554
- <header>
555
- <h1>Empathy Framework Dashboard</h1>
556
- <p class="subtitle">Pattern learning and cost optimization at a glance</p>
557
- </header>
558
-
559
- <div class="grid">
560
- <div class="card">
561
- <h2>Bug Patterns</h2>
562
- <div class="value">{bug_count}</div>
563
- <div class="label">patterns learned</div>
564
- </div>
565
-
566
- <div class="card">
567
- <h2>Security Decisions</h2>
568
- <div class="value">{security_count}</div>
569
- <div class="label">documented</div>
570
- </div>
571
-
572
- <div class="card warning">
573
- <h2>Tech Debt Items</h2>
574
- <div class="value">{debt_items}</div>
575
- <div class="label">tracked</div>
576
- </div>
577
-
578
- <div class="card">
579
- <h2>Workflow Runs</h2>
580
- <div class="value">{workflow_stats.get("total_runs", 0)}</div>
581
- <div class="label">{workflow_stats.get("avg_savings_percent", 0):.0f}% savings</div>
582
- </div>
583
-
584
- <div class="card success">
585
- <h2>Total Savings</h2>
586
- <div class="value">${workflow_stats.get("total_savings", 0):.2f}</div>
587
- <div class="label">workflows + API</div>
588
- </div>
589
-
590
- <div class="card {"success" if test_stats.get("failed_files", 0) == 0 else "warning"}">
591
- <h2>Test Coverage</h2>
592
- <div class="value">{test_stats.get("coverage_avg", 0):.0f}%</div>
593
- <div class="label">{test_stats.get("total_files", 0)} files tracked</div>
594
- </div>
595
- </div>
596
-
597
- <div class="section">
598
- <h2>Recent Bug Patterns</h2>
599
- <table>
600
- <thead>
601
- <tr>
602
- <th>Type</th>
603
- <th>Root Cause</th>
604
- <th>Status</th>
605
- <th>Resolved</th>
606
- </tr>
607
- </thead>
608
- <tbody>
609
- {self._render_bug_table(patterns)}
610
- </tbody>
611
- </table>
612
- </div>
613
-
614
- <div class="section">
615
- <h2>Multi-Model Workflows</h2>
616
- <div class="workflow-grid">
617
- {self._render_workflow_cards(workflow_stats)}
618
- </div>
619
-
620
- <h3 style="margin-bottom: 0.5rem; font-size: 1rem;">Model Tier Usage</h3>
621
- {self._render_tier_bar(workflow_stats)}
622
-
623
- <h3 style="margin-top: 1.5rem; margin-bottom: 0.5rem; font-size: 1rem;">Recent Runs</h3>
624
- {self._render_recent_runs(workflow_stats)}
625
- </div>
626
-
627
- <div class="section">
628
- <h2>Test Tracking</h2>
629
- {self._render_test_tracking(test_stats)}
630
- </div>
631
-
632
- <div class="section">
633
- <h2>Quick Commands</h2>
634
- <p>Run these commands for common tasks:</p>
635
- <div class="commands">
636
- <span class="command">empathy morning</span>
637
- <span class="command">empathy ship</span>
638
- <span class="command">empathy fix-all</span>
639
- <span class="command">empathy learn</span>
640
- <span class="command">empathy sync-claude</span>
641
- <span class="command">empathy costs</span>
642
- </div>
643
- </div>
644
-
645
- <footer>
646
- <p>Empathy Framework Dashboard - Refresh to update data</p>
647
- <p>Last updated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}</p>
648
- </footer>
649
- </div>
650
-
651
- <script>
652
- // Auto-refresh every 30 seconds
653
- setTimeout(() => location.reload(), 30000);
654
- </script>
655
- </body>
656
- </html>"""
657
-
658
- def _render_bug_table(self, patterns: dict) -> str:
659
- """Render bug patterns as table rows."""
660
- debugging_data = patterns.get("debugging", {})
661
- if isinstance(debugging_data, dict):
662
- bugs = debugging_data.get("patterns", [])
663
- elif isinstance(debugging_data, list):
664
- bugs = debugging_data
665
- else:
666
- bugs = []
667
- if not bugs:
668
- return (
669
- '<tr><td colspan="4">No patterns yet. Run "empathy learn" to get started.</td></tr>'
670
- )
671
-
672
- rows = []
673
- for bug in bugs[-10:]: # Last 10
674
- status_class = bug.get("status", "investigating")
675
- root = bug.get("root_cause", "")
676
- root_display = (root[:60] + "...") if len(root) > 60 else (root or "-")
677
- resolved = bug.get("resolved_at") or bug.get("timestamp")
678
- date_display = resolved[:10] if resolved else "-"
679
- rows.append(
680
- f"""
681
- <tr>
682
- <td>{bug.get("bug_type", "unknown")}</td>
683
- <td>{root_display}</td>
684
- <td><span class="status {status_class}">{status_class}</span></td>
685
- <td>{date_display}</td>
686
- </tr>
687
- """,
688
- )
689
-
690
- return "".join(rows)
691
-
692
- def _render_workflow_cards(self, workflow_stats: dict) -> str:
693
- """Render workflow stat cards."""
694
- by_workflow = workflow_stats.get("by_workflow", {})
695
-
696
- if not by_workflow:
697
- return '<div class="workflow-card"><p>No workflow runs yet.</p></div>'
698
-
699
- cards = []
700
- for name, stats in by_workflow.items():
701
- runs = stats.get("runs", 0)
702
- savings = stats.get("savings", 0)
703
- cards.append(
704
- f"""
705
- <div class="workflow-card">
706
- <h3>{name}</h3>
707
- <div class="runs">{runs}</div>
708
- <div class="savings">${savings:.4f} saved</div>
709
- </div>
710
- """,
711
- )
712
-
713
- return "".join(cards)
714
-
715
- def _render_tier_bar(self, workflow_stats: dict) -> str:
716
- """Render model tier usage bar."""
717
- by_tier = workflow_stats.get("by_tier", {})
718
- total = sum(by_tier.values())
719
-
720
- if total == 0:
721
- return '<div class="tier-bar"><div class="tier">No data yet</div></div>'
722
-
723
- cheap_pct = (by_tier.get("cheap", 0) / total) * 100 if total > 0 else 0
724
- capable_pct = (by_tier.get("capable", 0) / total) * 100 if total > 0 else 0
725
- premium_pct = (by_tier.get("premium", 0) / total) * 100 if total > 0 else 0
726
-
727
- return f"""
728
- <div class="tier-bar">
729
- <div class="tier cheap" style="width: {cheap_pct}%;">{cheap_pct:.0f}% cheap</div>
730
- <div class="tier capable" style="width: {capable_pct}%;">{capable_pct:.0f}% capable</div>
731
- <div class="tier premium" style="width: {premium_pct}%;">{premium_pct:.0f}% premium</div>
732
- </div>
733
- <div style="display: flex; gap: 1rem; font-size: 0.75rem; color: var(--text-muted);">
734
- <span>Cheap (Haiku/GPT-mini): ${by_tier.get("cheap", 0):.4f}</span>
735
- <span>Capable (Sonnet/GPT-4o): ${by_tier.get("capable", 0):.4f}</span>
736
- <span>Premium (Opus/GPT-5): ${by_tier.get("premium", 0):.4f}</span>
737
- </div>
738
- """
739
-
740
- def _render_recent_runs(self, workflow_stats: dict) -> str:
741
- """Render recent workflow runs."""
742
- recent_runs = workflow_stats.get("recent_runs", [])
743
-
744
- if not recent_runs:
745
- return '<p style="color: var(--text-muted);">No recent workflow runs.</p>'
746
-
747
- runs_html = []
748
- for run in recent_runs[:5]: # Show last 5
749
- name = run.get("workflow", "unknown")
750
- provider = run.get("provider", "unknown")
751
- success = run.get("success", False)
752
- savings_pct = run.get("savings_percent", 0)
753
- started = run.get("started_at", "")
754
-
755
- # Format time
756
- time_str = started[:16].replace("T", " ") if started else "-"
757
-
758
- status_icon = "&#10003;" if success else "&#10007;"
759
- status_color = "var(--success)" if success else "var(--danger)"
760
-
761
- runs_html.append(
762
- f"""
763
- <div class="recent-run">
764
- <span style="color: {status_color};">{status_icon}</span>
765
- <span class="name">{name}</span>
766
- <span class="provider">{provider}</span>
767
- <span class="result">
768
- <span class="savings-badge">{savings_pct:.0f}% saved</span>
769
- <span class="time">{time_str}</span>
770
- </span>
771
- </div>
772
- """,
773
- )
774
-
775
- return "".join(runs_html)
776
-
777
- def _render_test_tracking(self, test_stats: dict) -> str:
778
- """Render test tracking section."""
779
- if "error" in test_stats:
780
- return f'<p style="color: var(--text-muted);">{test_stats["error"]}</p>'
781
-
782
- total = test_stats.get("total_files", 0)
783
- passed = test_stats.get("passed_files", 0)
784
- failed = test_stats.get("failed_files", 0)
785
- coverage = test_stats.get("coverage_avg", 0)
786
-
787
- if total == 0:
788
- return '<p style="color: var(--text-muted);">No test tracking data yet. Run tests with empathy to start tracking.</p>'
789
-
790
- # Calculate pass rate
791
- pass_rate = (passed / total * 100) if total > 0 else 0
792
-
793
- # Stats grid
794
- html = f"""
795
- <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1.5rem;">
796
- <div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
797
- <div style="font-size: 1.5rem; font-weight: 600; color: var(--success);">{passed}</div>
798
- <div style="font-size: 0.75rem; color: var(--text-muted);">Passing</div>
799
- </div>
800
- <div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
801
- <div style="font-size: 1.5rem; font-weight: 600; color: var(--danger);">{failed}</div>
802
- <div style="font-size: 0.75rem; color: var(--text-muted);">Failing</div>
803
- </div>
804
- <div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
805
- <div style="font-size: 1.5rem; font-weight: 600; color: var(--primary);">{pass_rate:.0f}%</div>
806
- <div style="font-size: 0.75rem; color: var(--text-muted);">Pass Rate</div>
807
- </div>
808
- <div style="text-align: center; padding: 1rem; background: var(--bg); border-radius: 8px;">
809
- <div style="font-size: 1.5rem; font-weight: 600; color: var(--warning);">{coverage:.0f}%</div>
810
- <div style="font-size: 0.75rem; color: var(--text-muted);">Coverage</div>
811
- </div>
812
- </div>
813
- """
814
-
815
- # Files needing attention
816
- files_needing = test_stats.get("files_needing_tests", [])
817
- if files_needing:
818
- html += """
819
- <h3 style="margin-bottom: 0.5rem; font-size: 1rem;">Files Needing Attention</h3>
820
- <table>
821
- <thead>
822
- <tr>
823
- <th>File</th>
824
- <th>Status</th>
825
- <th>Last Run</th>
826
- </tr>
827
- </thead>
828
- <tbody>
829
- """
830
- for file_record in files_needing[:5]:
831
- file_path = file_record.get("file_path", "unknown")
832
- # Truncate long paths
833
- display_path = ("..." + file_path[-40:]) if len(file_path) > 40 else file_path
834
- result = file_record.get("last_test_result", "unknown")
835
- timestamp = (
836
- file_record.get("timestamp", "")[:10] if file_record.get("timestamp") else "-"
837
- )
838
- status_class = "resolved" if result == "passed" else "investigating"
839
- html += f"""
840
- <tr>
841
- <td title="{file_path}">{display_path}</td>
842
- <td><span class="status {status_class}">{result}</span></td>
843
- <td>{timestamp}</td>
844
- </tr>
845
- """
846
- html += "</tbody></table>"
847
-
848
- # Recent test executions
849
- recent = test_stats.get("recent_executions", [])
850
- if recent:
851
- html += """
852
- <h3 style="margin-top: 1.5rem; margin-bottom: 0.5rem; font-size: 1rem;">Recent Test Runs</h3>
853
- """
854
- for execution in recent[:5]:
855
- suite = execution.get("test_suite", "unknown")
856
- total_tests = execution.get("total_tests", 0)
857
- exec_passed = execution.get("passed", 0)
858
- exec_failed = execution.get("failed", 0)
859
- duration = execution.get("duration_seconds", 0)
860
- timestamp = (
861
- execution.get("timestamp", "")[:16].replace("T", " ")
862
- if execution.get("timestamp")
863
- else "-"
864
- )
865
- success = execution.get("success", False)
866
-
867
- status_icon = "&#10003;" if success else "&#10007;"
868
- status_color = "var(--success)" if success else "var(--danger)"
869
-
870
- html += f"""
871
- <div class="recent-run">
872
- <span style="color: {status_color};">{status_icon}</span>
873
- <span class="name">{suite}</span>
874
- <span class="provider">{total_tests} tests</span>
875
- <span class="result">
876
- <span style="color: var(--success);">{exec_passed} passed</span>
877
- {f'<span style="color: var(--danger);">{exec_failed} failed</span>' if exec_failed > 0 else ""}
878
- <span class="time">{duration:.1f}s | {timestamp}</span>
879
- </span>
880
- </div>
881
- """
882
-
883
- return html
884
-
885
-
886
- class ThreadedHTTPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
887
- """Threaded HTTP server."""
888
-
889
- allow_reuse_address = True
890
-
891
-
892
- def run_dashboard(
893
- port: int = 8765,
894
- patterns_dir: str = "./patterns",
895
- empathy_dir: str = ".empathy",
896
- open_browser: bool = True,
897
- ) -> None:
898
- """Run the dashboard server.
899
-
900
- Args:
901
- port: Port to run on (default: 8765)
902
- patterns_dir: Path to patterns directory
903
- empathy_dir: Path to empathy data directory
904
- open_browser: Open browser automatically
905
-
906
- """
907
- # Configure handler
908
- DashboardHandler.patterns_dir = patterns_dir
909
- DashboardHandler.empathy_dir = empathy_dir
910
-
911
- url = f"http://localhost:{port}"
912
-
913
- print("\n Empathy Dashboard")
914
- print(f" Running at: {url}")
915
- print(" Press Ctrl+C to stop\n")
916
-
917
- if open_browser:
918
- # Open browser in a separate thread to not block server start
919
- threading.Timer(0.5, lambda: webbrowser.open(url)).start()
920
-
921
- try:
922
- with ThreadedHTTPServer(("", port), DashboardHandler) as httpd:
923
- httpd.serve_forever()
924
- except KeyboardInterrupt:
925
- print("\n Dashboard stopped.")
926
-
927
-
928
- def cmd_dashboard(args):
929
- """CLI command handler for dashboard."""
930
- port = getattr(args, "port", 8765)
931
- patterns_dir = getattr(args, "patterns_dir", "./patterns")
932
- empathy_dir = getattr(args, "empathy_dir", ".empathy")
933
- no_browser = getattr(args, "no_browser", False)
934
-
935
- run_dashboard(
936
- port=port,
937
- patterns_dir=patterns_dir,
938
- empathy_dir=empathy_dir,
939
- open_browser=not no_browser,
940
- )
941
- return 0