decision_agent 0.1.4 → 0.1.6

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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -233
  3. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +46 -10
  4. data/lib/decision_agent/agent.rb +5 -3
  5. data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
  6. data/lib/decision_agent/auth/authenticator.rb +127 -0
  7. data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
  8. data/lib/decision_agent/auth/password_reset_token.rb +33 -0
  9. data/lib/decision_agent/auth/permission.rb +29 -0
  10. data/lib/decision_agent/auth/permission_checker.rb +43 -0
  11. data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
  12. data/lib/decision_agent/auth/rbac_config.rb +51 -0
  13. data/lib/decision_agent/auth/role.rb +56 -0
  14. data/lib/decision_agent/auth/session.rb +33 -0
  15. data/lib/decision_agent/auth/session_manager.rb +57 -0
  16. data/lib/decision_agent/auth/user.rb +70 -0
  17. data/lib/decision_agent/context.rb +24 -4
  18. data/lib/decision_agent/decision.rb +10 -3
  19. data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
  20. data/lib/decision_agent/dsl/schema_validator.rb +8 -1
  21. data/lib/decision_agent/errors.rb +38 -0
  22. data/lib/decision_agent/evaluation.rb +10 -3
  23. data/lib/decision_agent/evaluation_validator.rb +8 -13
  24. data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
  25. data/lib/decision_agent/monitoring/metrics_collector.rb +17 -5
  26. data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
  27. data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
  28. data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
  29. data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
  30. data/lib/decision_agent/testing/test_scenario.rb +42 -0
  31. data/lib/decision_agent/version.rb +10 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
  33. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
  34. data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
  35. data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
  36. data/lib/decision_agent/web/public/app.js +184 -29
  37. data/lib/decision_agent/web/public/batch_testing.html +640 -0
  38. data/lib/decision_agent/web/public/index.html +37 -9
  39. data/lib/decision_agent/web/public/login.html +298 -0
  40. data/lib/decision_agent/web/public/users.html +679 -0
  41. data/lib/decision_agent/web/server.rb +873 -7
  42. data/lib/decision_agent.rb +52 -0
  43. data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
  45. data/spec/ab_testing/ab_test_manager_spec.rb +282 -0
  46. data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
  47. data/spec/ab_testing/storage/adapter_spec.rb +64 -0
  48. data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
  49. data/spec/advanced_operators_spec.rb +1003 -0
  50. data/spec/agent_spec.rb +40 -0
  51. data/spec/audit_adapters_spec.rb +18 -0
  52. data/spec/auth/access_audit_logger_spec.rb +394 -0
  53. data/spec/auth/authenticator_spec.rb +112 -0
  54. data/spec/auth/password_reset_spec.rb +294 -0
  55. data/spec/auth/permission_checker_spec.rb +207 -0
  56. data/spec/auth/permission_spec.rb +73 -0
  57. data/spec/auth/rbac_adapter_spec.rb +550 -0
  58. data/spec/auth/rbac_config_spec.rb +82 -0
  59. data/spec/auth/role_spec.rb +51 -0
  60. data/spec/auth/session_manager_spec.rb +172 -0
  61. data/spec/auth/session_spec.rb +112 -0
  62. data/spec/auth/user_spec.rb +130 -0
  63. data/spec/context_spec.rb +43 -0
  64. data/spec/decision_agent_spec.rb +96 -0
  65. data/spec/decision_spec.rb +423 -0
  66. data/spec/dsl/condition_evaluator_spec.rb +774 -0
  67. data/spec/evaluation_spec.rb +364 -0
  68. data/spec/evaluation_validator_spec.rb +165 -0
  69. data/spec/examples.txt +1542 -612
  70. data/spec/monitoring/metrics_collector_spec.rb +220 -2
  71. data/spec/monitoring/storage/activerecord_adapter_spec.rb +153 -1
  72. data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
  73. data/spec/performance_optimizations_spec.rb +486 -0
  74. data/spec/spec_helper.rb +23 -0
  75. data/spec/testing/batch_test_importer_spec.rb +693 -0
  76. data/spec/testing/batch_test_runner_spec.rb +307 -0
  77. data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
  78. data/spec/testing/test_result_comparator_spec.rb +392 -0
  79. data/spec/testing/test_scenario_spec.rb +113 -0
  80. data/spec/versioning/adapter_spec.rb +156 -0
  81. data/spec/versioning_spec.rb +253 -0
  82. data/spec/web/middleware/auth_middleware_spec.rb +133 -0
  83. data/spec/web/middleware/permission_middleware_spec.rb +247 -0
  84. data/spec/web_ui_rack_spec.rb +1705 -0
  85. metadata +99 -6
@@ -0,0 +1,640 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DecisionAgent Batch Testing</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ <style>
9
+ .batch-testing-container {
10
+ max-width: 1400px;
11
+ margin: 0 auto;
12
+ padding: 20px;
13
+ }
14
+
15
+ .step-section {
16
+ background: var(--panel-bg);
17
+ border-radius: 10px;
18
+ padding: 20px;
19
+ margin-bottom: 20px;
20
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
21
+ }
22
+
23
+ .step-header {
24
+ display: flex;
25
+ align-items: center;
26
+ margin-bottom: 15px;
27
+ }
28
+
29
+ .step-number {
30
+ background: var(--primary-color);
31
+ color: white;
32
+ width: 30px;
33
+ height: 30px;
34
+ border-radius: 50%;
35
+ display: flex;
36
+ align-items: center;
37
+ justify-content: center;
38
+ font-weight: bold;
39
+ margin-right: 10px;
40
+ }
41
+
42
+ .file-upload-area {
43
+ border: 2px dashed var(--border-color);
44
+ border-radius: 8px;
45
+ padding: 40px;
46
+ text-align: center;
47
+ cursor: pointer;
48
+ transition: all 0.3s;
49
+ }
50
+
51
+ .file-upload-area:hover {
52
+ border-color: var(--primary-color);
53
+ background: var(--hover-bg);
54
+ }
55
+
56
+ .file-upload-area.dragover {
57
+ border-color: var(--primary-color);
58
+ background: var(--hover-bg);
59
+ }
60
+
61
+ .results-table {
62
+ width: 100%;
63
+ border-collapse: collapse;
64
+ margin-top: 15px;
65
+ }
66
+
67
+ .results-table th,
68
+ .results-table td {
69
+ padding: 10px;
70
+ text-align: left;
71
+ border-bottom: 1px solid var(--border-color);
72
+ }
73
+
74
+ .results-table th {
75
+ background: var(--hover-bg);
76
+ font-weight: 600;
77
+ }
78
+
79
+ .status-badge {
80
+ display: inline-block;
81
+ padding: 4px 12px;
82
+ border-radius: 12px;
83
+ font-size: 0.85rem;
84
+ font-weight: 500;
85
+ }
86
+
87
+ .status-imported {
88
+ background: #dbeafe;
89
+ color: #1e40af;
90
+ }
91
+
92
+ .status-running {
93
+ background: #fef3c7;
94
+ color: #92400e;
95
+ }
96
+
97
+ .status-completed {
98
+ background: #d1fae5;
99
+ color: #065f46;
100
+ }
101
+
102
+ .status-failed {
103
+ background: #fee2e2;
104
+ color: #991b1b;
105
+ }
106
+
107
+ .progress-bar {
108
+ width: 100%;
109
+ height: 20px;
110
+ background: var(--border-color);
111
+ border-radius: 10px;
112
+ overflow: hidden;
113
+ margin-top: 10px;
114
+ }
115
+
116
+ .progress-fill {
117
+ height: 100%;
118
+ background: var(--primary-color);
119
+ transition: width 0.3s;
120
+ }
121
+
122
+ .metrics-grid {
123
+ display: grid;
124
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
125
+ gap: 15px;
126
+ margin-top: 15px;
127
+ }
128
+
129
+ .metric-card {
130
+ background: var(--hover-bg);
131
+ padding: 15px;
132
+ border-radius: 8px;
133
+ text-align: center;
134
+ }
135
+
136
+ .metric-value {
137
+ font-size: 2rem;
138
+ font-weight: bold;
139
+ color: var(--primary-color);
140
+ }
141
+
142
+ .metric-label {
143
+ font-size: 0.9rem;
144
+ color: var(--text-secondary);
145
+ margin-top: 5px;
146
+ }
147
+
148
+ .hidden {
149
+ display: none;
150
+ }
151
+
152
+ .error-message {
153
+ background: #fee2e2;
154
+ color: #991b1b;
155
+ padding: 12px;
156
+ border-radius: 6px;
157
+ margin-top: 10px;
158
+ }
159
+
160
+ .success-message {
161
+ background: #d1fae5;
162
+ color: #065f46;
163
+ padding: 12px;
164
+ border-radius: 6px;
165
+ margin-top: 10px;
166
+ }
167
+ </style>
168
+ </head>
169
+ <body>
170
+ <div class="batch-testing-container">
171
+ <header>
172
+ <h1>🧪 DecisionAgent Batch Testing</h1>
173
+ <p class="subtitle">Test your decision rules against multiple scenarios</p>
174
+ </header>
175
+
176
+ <!-- Step 1: Upload Test Scenarios -->
177
+ <div class="step-section">
178
+ <div class="step-header">
179
+ <div class="step-number">1</div>
180
+ <h2>Upload Test Scenarios</h2>
181
+ </div>
182
+ <p>Upload a CSV or Excel file with your test scenarios. Required columns: <code>id</code>, context fields. Optional: <code>expected_decision</code>, <code>expected_confidence</code></p>
183
+
184
+ <div class="file-upload-area" id="uploadArea">
185
+ <p>📁 Drag and drop your CSV/Excel file here, or click to browse</p>
186
+ <input type="file" id="fileInput" accept=".csv,.xlsx,.xls" style="display: none;">
187
+ </div>
188
+
189
+ <div id="uploadStatus" class="hidden"></div>
190
+ <div id="uploadProgress" class="hidden">
191
+ <div class="progress-bar">
192
+ <div class="progress-fill" id="progressFill" style="width: 0%"></div>
193
+ </div>
194
+ <p id="progressText" style="text-align: center; margin-top: 10px;">0%</p>
195
+ </div>
196
+ </div>
197
+
198
+ <!-- Step 2: Configure Rules -->
199
+ <div class="step-section">
200
+ <div class="step-header">
201
+ <div class="step-number">2</div>
202
+ <h2>Configure Rules</h2>
203
+ </div>
204
+ <p>Paste your rules JSON or import from file</p>
205
+
206
+ <div class="form-group">
207
+ <label for="rulesJson">Rules JSON:</label>
208
+ <textarea id="rulesJson" class="input" rows="10" placeholder='{"version": "1.0", "ruleset": "my_ruleset", "rules": [...]}'></textarea>
209
+ </div>
210
+
211
+ <div class="actions">
212
+ <label for="importRulesFile" class="btn btn-secondary">📁 Import Rules JSON
213
+ <input type="file" id="importRulesFile" accept=".json" style="display: none;">
214
+ </label>
215
+ <button class="btn btn-secondary" id="validateRulesBtn">✓ Validate Rules</button>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- Step 3: Run Tests -->
220
+ <div class="step-section">
221
+ <div class="step-header">
222
+ <div class="step-number">3</div>
223
+ <h2>Run Batch Test</h2>
224
+ </div>
225
+
226
+ <div class="form-group">
227
+ <label>
228
+ <input type="checkbox" id="parallelExecution" checked> Parallel execution
229
+ </label>
230
+ </div>
231
+ <div class="form-group">
232
+ <label for="threadCount">Thread count:</label>
233
+ <input type="number" id="threadCount" value="4" min="1" max="16" class="input" style="width: 100px;">
234
+ </div>
235
+
236
+ <button class="btn btn-primary" id="runTestBtn" disabled>▶ Run Batch Test</button>
237
+ <button class="btn btn-secondary" id="resumeTestBtn" disabled>▶ Resume Test</button>
238
+
239
+ <div id="runStatus" class="hidden"></div>
240
+ <div id="runProgress" class="hidden">
241
+ <div class="progress-bar">
242
+ <div class="progress-fill" id="runProgressFill" style="width: 0%"></div>
243
+ </div>
244
+ <p id="runProgressText" style="text-align: center; margin-top: 10px;">0%</p>
245
+ </div>
246
+ </div>
247
+
248
+ <!-- Step 4: Results -->
249
+ <div class="step-section" id="resultsSection" style="display: none;">
250
+ <div class="step-header">
251
+ <div class="step-number">4</div>
252
+ <h2>Test Results</h2>
253
+ </div>
254
+
255
+ <div id="testInfo">
256
+ <p><strong>Test ID:</strong> <span id="testId"></span></p>
257
+ <p><strong>Status:</strong> <span id="testStatus" class="status-badge"></span></p>
258
+ </div>
259
+
260
+ <!-- Statistics -->
261
+ <div id="statisticsSection" class="hidden">
262
+ <h3>Statistics</h3>
263
+ <div class="metrics-grid" id="statisticsGrid"></div>
264
+ </div>
265
+
266
+ <!-- Comparison Results -->
267
+ <div id="comparisonSection" class="hidden">
268
+ <h3>Comparison Results</h3>
269
+ <div class="metrics-grid" id="comparisonGrid"></div>
270
+ </div>
271
+
272
+ <!-- Coverage Report -->
273
+ <div id="coverageSection" class="hidden">
274
+ <h3>Coverage Report</h3>
275
+ <div class="metrics-grid" id="coverageGrid"></div>
276
+ </div>
277
+
278
+ <!-- Results Table -->
279
+ <div id="resultsTableSection" class="hidden">
280
+ <h3>Detailed Results</h3>
281
+ <div style="max-height: 400px; overflow-y: auto;">
282
+ <table class="results-table">
283
+ <thead>
284
+ <tr>
285
+ <th>Scenario ID</th>
286
+ <th>Decision</th>
287
+ <th>Confidence</th>
288
+ <th>Expected Decision</th>
289
+ <th>Expected Confidence</th>
290
+ <th>Match</th>
291
+ <th>Execution Time (ms)</th>
292
+ </tr>
293
+ </thead>
294
+ <tbody id="resultsTableBody"></tbody>
295
+ </table>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </div>
300
+
301
+ <script>
302
+ // Helper function to get the base path for API calls
303
+ function getBasePath() {
304
+ const baseTag = document.querySelector('base');
305
+ if (baseTag && baseTag.href) {
306
+ try {
307
+ const baseUrl = new URL(baseTag.href, window.location.href);
308
+ let path = baseUrl.pathname;
309
+ if (path && !path.endsWith('/')) {
310
+ path += '/';
311
+ }
312
+ return path;
313
+ } catch (e) {
314
+ if (baseTag.href.startsWith('/')) {
315
+ const match = baseTag.href.match(/^(https?:\/\/[^\/]+)?(\/.*?)\/?$/);
316
+ if (match && match[2]) {
317
+ return match[2].endsWith('/') ? match[2] : match[2] + '/';
318
+ }
319
+ } else if (baseTag.href.startsWith('./')) {
320
+ const pathname = window.location.pathname;
321
+ return pathname.substring(0, pathname.lastIndexOf('/') + 1);
322
+ }
323
+ }
324
+ }
325
+ const pathname = window.location.pathname;
326
+ if (pathname.includes('/decision_agent')) {
327
+ const match = pathname.match(/^(\/.*?\/decision_agent)\/?/);
328
+ if (match) {
329
+ return match[1].endsWith('/') ? match[1] : match[1] + '/';
330
+ }
331
+ }
332
+ const dirPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
333
+ return dirPath || '/';
334
+ }
335
+
336
+ const basePath = getBasePath();
337
+ let currentTestId = null;
338
+ let currentScenarios = null;
339
+
340
+ // File upload
341
+ const uploadArea = document.getElementById('uploadArea');
342
+ const fileInput = document.getElementById('fileInput');
343
+ const uploadStatus = document.getElementById('uploadStatus');
344
+ const uploadProgress = document.getElementById('uploadProgress');
345
+ const progressFill = document.getElementById('progressFill');
346
+ const progressText = document.getElementById('progressText');
347
+
348
+ uploadArea.addEventListener('click', () => fileInput.click());
349
+ uploadArea.addEventListener('dragover', (e) => {
350
+ e.preventDefault();
351
+ uploadArea.classList.add('dragover');
352
+ });
353
+ uploadArea.addEventListener('dragleave', () => {
354
+ uploadArea.classList.remove('dragover');
355
+ });
356
+ uploadArea.addEventListener('drop', (e) => {
357
+ e.preventDefault();
358
+ uploadArea.classList.remove('dragover');
359
+ const files = e.dataTransfer.files;
360
+ if (files.length > 0) {
361
+ handleFileUpload(files[0]);
362
+ }
363
+ });
364
+ fileInput.addEventListener('change', (e) => {
365
+ if (e.target.files.length > 0) {
366
+ handleFileUpload(e.target.files[0]);
367
+ }
368
+ });
369
+
370
+ function handleFileUpload(file) {
371
+ const formData = new FormData();
372
+ formData.append('file', file);
373
+
374
+ uploadStatus.className = 'hidden';
375
+ uploadProgress.classList.remove('hidden');
376
+ progressFill.style.width = '0%';
377
+ progressText.textContent = 'Uploading...';
378
+
379
+ fetch(`${basePath}api/testing/batch/import`, {
380
+ method: 'POST',
381
+ body: formData
382
+ })
383
+ .then(response => response.json())
384
+ .then(data => {
385
+ if (data.error) {
386
+ uploadStatus.className = 'error-message';
387
+ uploadStatus.textContent = `Error: ${data.error}`;
388
+ uploadProgress.classList.add('hidden');
389
+ } else {
390
+ currentTestId = data.test_id;
391
+ currentScenarios = data.scenarios_count;
392
+ uploadStatus.className = 'success-message';
393
+ uploadStatus.textContent = `Successfully imported ${data.scenarios_count} scenarios. Test ID: ${data.test_id}`;
394
+ uploadProgress.classList.add('hidden');
395
+ document.getElementById('runTestBtn').disabled = false;
396
+
397
+ if (data.errors && data.errors.length > 0) {
398
+ uploadStatus.textContent += `\nWarnings: ${data.errors.join(', ')}`;
399
+ }
400
+ }
401
+ })
402
+ .catch(error => {
403
+ uploadStatus.className = 'error-message';
404
+ uploadStatus.textContent = `Error: ${error.message}`;
405
+ uploadProgress.classList.add('hidden');
406
+ });
407
+ }
408
+
409
+ // Import rules JSON
410
+ document.getElementById('importRulesFile').addEventListener('change', (e) => {
411
+ const file = e.target.files[0];
412
+ if (file) {
413
+ const reader = new FileReader();
414
+ reader.onload = (e) => {
415
+ try {
416
+ const json = JSON.parse(e.target.result);
417
+ document.getElementById('rulesJson').value = JSON.stringify(json, null, 2);
418
+ } catch (error) {
419
+ alert('Invalid JSON file: ' + error.message);
420
+ }
421
+ };
422
+ reader.readAsText(file);
423
+ }
424
+ });
425
+
426
+ // Validate rules
427
+ document.getElementById('validateRulesBtn').addEventListener('click', () => {
428
+ const rulesJson = document.getElementById('rulesJson').value;
429
+ try {
430
+ JSON.parse(rulesJson);
431
+ fetch(`${basePath}api/validate`, {
432
+ method: 'POST',
433
+ headers: { 'Content-Type': 'application/json' },
434
+ body: JSON.stringify(JSON.parse(rulesJson))
435
+ })
436
+ .then(response => response.json())
437
+ .then(data => {
438
+ if (data.valid) {
439
+ alert('Rules are valid!');
440
+ } else {
441
+ alert('Validation errors: ' + data.errors.join(', '));
442
+ }
443
+ });
444
+ } catch (error) {
445
+ alert('Invalid JSON: ' + error.message);
446
+ }
447
+ });
448
+
449
+ // Run test
450
+ document.getElementById('runTestBtn').addEventListener('click', () => {
451
+ if (!currentTestId) {
452
+ alert('Please upload test scenarios first');
453
+ return;
454
+ }
455
+
456
+ const rulesJson = document.getElementById('rulesJson').value;
457
+ if (!rulesJson.trim()) {
458
+ alert('Please provide rules JSON');
459
+ return;
460
+ }
461
+
462
+ try {
463
+ JSON.parse(rulesJson);
464
+ } catch (error) {
465
+ alert('Invalid JSON: ' + error.message);
466
+ return;
467
+ }
468
+
469
+ const runStatus = document.getElementById('runStatus');
470
+ const runProgress = document.getElementById('runProgress');
471
+ const runProgressFill = document.getElementById('runProgressFill');
472
+ const runProgressText = document.getElementById('runProgressText');
473
+
474
+ runStatus.className = 'hidden';
475
+ runProgress.classList.remove('hidden');
476
+ runProgressFill.style.width = '0%';
477
+ runProgressText.textContent = 'Starting...';
478
+
479
+ const options = {
480
+ parallel: document.getElementById('parallelExecution').checked,
481
+ thread_count: parseInt(document.getElementById('threadCount').value) || 4
482
+ };
483
+
484
+ fetch(`${basePath}api/testing/batch/run`, {
485
+ method: 'POST',
486
+ headers: { 'Content-Type': 'application/json' },
487
+ body: JSON.stringify({
488
+ test_id: currentTestId,
489
+ rules: JSON.parse(rulesJson),
490
+ options: options
491
+ })
492
+ })
493
+ .then(response => response.json())
494
+ .then(data => {
495
+ if (data.error) {
496
+ runStatus.className = 'error-message';
497
+ runStatus.textContent = `Error: ${data.error}`;
498
+ runProgress.classList.add('hidden');
499
+ } else {
500
+ runStatus.className = 'success-message';
501
+ runStatus.textContent = 'Test completed successfully!';
502
+ runProgress.classList.add('hidden');
503
+ loadResults(currentTestId);
504
+ }
505
+ })
506
+ .catch(error => {
507
+ runStatus.className = 'error-message';
508
+ runStatus.textContent = `Error: ${error.message}`;
509
+ runProgress.classList.add('hidden');
510
+ });
511
+ });
512
+
513
+ // Load results
514
+ function loadResults(testId) {
515
+ fetch(`${basePath}api/testing/batch/${testId}/results`)
516
+ .then(response => response.json())
517
+ .then(data => {
518
+ if (data.error) {
519
+ alert('Error loading results: ' + data.error);
520
+ return;
521
+ }
522
+
523
+ document.getElementById('resultsSection').style.display = 'block';
524
+ document.getElementById('testId').textContent = data.test_id;
525
+ document.getElementById('testStatus').textContent = data.status;
526
+ document.getElementById('testStatus').className = 'status-badge status-' + data.status;
527
+
528
+ // Statistics
529
+ if (data.statistics) {
530
+ const statsGrid = document.getElementById('statisticsGrid');
531
+ statsGrid.innerHTML = `
532
+ <div class="metric-card">
533
+ <div class="metric-value">${data.statistics.total}</div>
534
+ <div class="metric-label">Total Tests</div>
535
+ </div>
536
+ <div class="metric-card">
537
+ <div class="metric-value">${data.statistics.successful}</div>
538
+ <div class="metric-label">Successful</div>
539
+ </div>
540
+ <div class="metric-card">
541
+ <div class="metric-value">${data.statistics.failed}</div>
542
+ <div class="metric-label">Failed</div>
543
+ </div>
544
+ <div class="metric-card">
545
+ <div class="metric-value">${(data.statistics.success_rate * 100).toFixed(1)}%</div>
546
+ <div class="metric-label">Success Rate</div>
547
+ </div>
548
+ <div class="metric-card">
549
+ <div class="metric-value">${data.statistics.avg_execution_time_ms.toFixed(2)}ms</div>
550
+ <div class="metric-label">Avg Execution Time</div>
551
+ </div>
552
+ `;
553
+ document.getElementById('statisticsSection').classList.remove('hidden');
554
+ }
555
+
556
+ // Comparison
557
+ if (data.comparison) {
558
+ const compGrid = document.getElementById('comparisonGrid');
559
+ compGrid.innerHTML = `
560
+ <div class="metric-card">
561
+ <div class="metric-value">${(data.comparison.accuracy_rate * 100).toFixed(1)}%</div>
562
+ <div class="metric-label">Accuracy Rate</div>
563
+ </div>
564
+ <div class="metric-card">
565
+ <div class="metric-value">${data.comparison.matches}</div>
566
+ <div class="metric-label">Matches</div>
567
+ </div>
568
+ <div class="metric-card">
569
+ <div class="metric-value">${data.comparison.mismatches}</div>
570
+ <div class="metric-label">Mismatches</div>
571
+ </div>
572
+ `;
573
+ document.getElementById('comparisonSection').classList.remove('hidden');
574
+ }
575
+
576
+ // Results table
577
+ if (data.results && data.results.length > 0) {
578
+ const tbody = document.getElementById('resultsTableBody');
579
+ tbody.innerHTML = '';
580
+ data.results.forEach(result => {
581
+ const row = tbody.insertRow();
582
+ row.insertCell(0).textContent = result.scenario_id;
583
+ row.insertCell(1).textContent = result.decision || 'N/A';
584
+ row.insertCell(2).textContent = result.confidence ? result.confidence.toFixed(3) : 'N/A';
585
+ row.insertCell(3).textContent = 'N/A'; // Expected decision
586
+ row.insertCell(4).textContent = 'N/A'; // Expected confidence
587
+ row.insertCell(5).textContent = result.success ? '✓' : '✗';
588
+ row.insertCell(6).textContent = result.execution_time_ms.toFixed(2);
589
+ });
590
+ document.getElementById('resultsTableSection').classList.remove('hidden');
591
+ }
592
+
593
+ // Load coverage
594
+ loadCoverage(testId);
595
+ })
596
+ .catch(error => {
597
+ alert('Error loading results: ' + error.message);
598
+ });
599
+ }
600
+
601
+ // Load coverage
602
+ function loadCoverage(testId) {
603
+ fetch(`${basePath}api/testing/batch/${testId}/coverage`)
604
+ .then(response => response.json())
605
+ .then(data => {
606
+ if (data.error) {
607
+ return; // Coverage not available
608
+ }
609
+
610
+ if (data.coverage) {
611
+ const covGrid = document.getElementById('coverageGrid');
612
+ covGrid.innerHTML = `
613
+ <div class="metric-card">
614
+ <div class="metric-value">${(data.coverage.coverage_percentage * 100).toFixed(1)}%</div>
615
+ <div class="metric-label">Coverage</div>
616
+ </div>
617
+ <div class="metric-card">
618
+ <div class="metric-value">${data.coverage.covered_rules}</div>
619
+ <div class="metric-label">Covered Rules</div>
620
+ </div>
621
+ <div class="metric-card">
622
+ <div class="metric-value">${data.coverage.total_rules}</div>
623
+ <div class="metric-label">Total Rules</div>
624
+ </div>
625
+ <div class="metric-card">
626
+ <div class="metric-value">${data.coverage.untested_rules.length}</div>
627
+ <div class="metric-label">Untested Rules</div>
628
+ </div>
629
+ `;
630
+ document.getElementById('coverageSection').classList.remove('hidden');
631
+ }
632
+ })
633
+ .catch(() => {
634
+ // Coverage not available, ignore
635
+ });
636
+ }
637
+ </script>
638
+ </body>
639
+ </html>
640
+