decision_agent 0.3.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +272 -7
  3. data/lib/decision_agent/agent.rb +72 -1
  4. data/lib/decision_agent/context.rb +1 -0
  5. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  6. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  7. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  8. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  9. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  10. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  11. data/lib/decision_agent/decision.rb +102 -2
  12. data/lib/decision_agent/dmn/feel/evaluator.rb +28 -6
  13. data/lib/decision_agent/dsl/condition_evaluator.rb +982 -839
  14. data/lib/decision_agent/dsl/schema_validator.rb +51 -13
  15. data/lib/decision_agent/evaluators/dmn_evaluator.rb +106 -19
  16. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  17. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  18. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  19. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  20. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  21. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  22. data/lib/decision_agent/simulation/errors.rb +18 -0
  23. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  24. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  25. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  26. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  27. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  28. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  29. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  30. data/lib/decision_agent/simulation.rb +17 -0
  31. data/lib/decision_agent/version.rb +1 -1
  32. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  33. data/lib/decision_agent/web/public/app.js +119 -0
  34. data/lib/decision_agent/web/public/index.html +49 -0
  35. data/lib/decision_agent/web/public/simulation.html +130 -0
  36. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  37. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  38. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  39. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  40. data/lib/decision_agent/web/public/styles.css +65 -0
  41. data/lib/decision_agent/web/server.rb +594 -23
  42. data/lib/decision_agent.rb +60 -2
  43. metadata +53 -73
  44. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  45. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  46. data/spec/ab_testing/ab_test_spec.rb +0 -270
  47. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -655
  48. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  49. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  50. data/spec/activerecord_thread_safety_spec.rb +0 -553
  51. data/spec/advanced_operators_spec.rb +0 -3150
  52. data/spec/agent_spec.rb +0 -289
  53. data/spec/api_contract_spec.rb +0 -430
  54. data/spec/audit_adapters_spec.rb +0 -92
  55. data/spec/auth/access_audit_logger_spec.rb +0 -394
  56. data/spec/auth/authenticator_spec.rb +0 -112
  57. data/spec/auth/password_reset_spec.rb +0 -294
  58. data/spec/auth/permission_checker_spec.rb +0 -207
  59. data/spec/auth/permission_spec.rb +0 -73
  60. data/spec/auth/rbac_adapter_spec.rb +0 -778
  61. data/spec/auth/rbac_config_spec.rb +0 -82
  62. data/spec/auth/role_spec.rb +0 -51
  63. data/spec/auth/session_manager_spec.rb +0 -172
  64. data/spec/auth/session_spec.rb +0 -112
  65. data/spec/auth/user_spec.rb +0 -130
  66. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  67. data/spec/context_spec.rb +0 -127
  68. data/spec/decision_agent_spec.rb +0 -96
  69. data/spec/decision_spec.rb +0 -423
  70. data/spec/dmn/decision_graph_spec.rb +0 -282
  71. data/spec/dmn/decision_tree_spec.rb +0 -203
  72. data/spec/dmn/feel/errors_spec.rb +0 -18
  73. data/spec/dmn/feel/functions_spec.rb +0 -400
  74. data/spec/dmn/feel/simple_parser_spec.rb +0 -274
  75. data/spec/dmn/feel/types_spec.rb +0 -176
  76. data/spec/dmn/feel_parser_spec.rb +0 -489
  77. data/spec/dmn/hit_policy_spec.rb +0 -202
  78. data/spec/dmn/integration_spec.rb +0 -226
  79. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  80. data/spec/dsl_validation_spec.rb +0 -648
  81. data/spec/edge_cases_spec.rb +0 -353
  82. data/spec/evaluation_spec.rb +0 -364
  83. data/spec/evaluation_validator_spec.rb +0 -165
  84. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  85. data/spec/examples.txt +0 -1909
  86. data/spec/fixtures/dmn/complex_decision.dmn +0 -81
  87. data/spec/fixtures/dmn/invalid_structure.dmn +0 -31
  88. data/spec/fixtures/dmn/simple_decision.dmn +0 -40
  89. data/spec/issue_verification_spec.rb +0 -759
  90. data/spec/json_rule_evaluator_spec.rb +0 -587
  91. data/spec/monitoring/alert_manager_spec.rb +0 -378
  92. data/spec/monitoring/metrics_collector_spec.rb +0 -501
  93. data/spec/monitoring/monitored_agent_spec.rb +0 -225
  94. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  96. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  98. data/spec/performance_optimizations_spec.rb +0 -493
  99. data/spec/replay_edge_cases_spec.rb +0 -699
  100. data/spec/replay_spec.rb +0 -210
  101. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  102. data/spec/scoring_spec.rb +0 -225
  103. data/spec/spec_helper.rb +0 -60
  104. data/spec/testing/batch_test_importer_spec.rb +0 -693
  105. data/spec/testing/batch_test_runner_spec.rb +0 -307
  106. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  107. data/spec/testing/test_result_comparator_spec.rb +0 -392
  108. data/spec/testing/test_scenario_spec.rb +0 -113
  109. data/spec/thread_safety_spec.rb +0 -490
  110. data/spec/thread_safety_spec.rb.broken +0 -878
  111. data/spec/versioning/adapter_spec.rb +0 -156
  112. data/spec/versioning_spec.rb +0 -1030
  113. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  114. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  115. data/spec/web_ui_rack_spec.rb +0 -2134
@@ -0,0 +1,551 @@
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>Historical Replay / Backtesting</title>
7
+ <link rel="stylesheet" href="styles.css">
8
+ <style>
9
+ .simulation-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
+ .metrics-grid {
80
+ display: grid;
81
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
82
+ gap: 15px;
83
+ margin-top: 15px;
84
+ }
85
+
86
+ .metric-card {
87
+ background: var(--hover-bg);
88
+ padding: 15px;
89
+ border-radius: 8px;
90
+ text-align: center;
91
+ }
92
+
93
+ .metric-value {
94
+ font-size: 2rem;
95
+ font-weight: bold;
96
+ color: var(--primary-color);
97
+ }
98
+
99
+ .metric-label {
100
+ font-size: 0.9rem;
101
+ color: var(--text-secondary);
102
+ margin-top: 5px;
103
+ }
104
+
105
+ .hidden {
106
+ display: none;
107
+ }
108
+
109
+ .error-message {
110
+ background: #fee2e2;
111
+ color: #991b1b;
112
+ padding: 12px;
113
+ border-radius: 6px;
114
+ margin-top: 10px;
115
+ }
116
+
117
+ .success-message {
118
+ background: #d1fae5;
119
+ color: #065f46;
120
+ padding: 12px;
121
+ border-radius: 6px;
122
+ margin-top: 10px;
123
+ }
124
+
125
+ .header-links {
126
+ margin-top: 20px;
127
+ }
128
+
129
+ .header-links a {
130
+ margin-right: 15px;
131
+ text-decoration: none;
132
+ color: var(--primary-color);
133
+ }
134
+ </style>
135
+ </head>
136
+ <body>
137
+ <div class="simulation-container">
138
+ <header>
139
+ <h1>🔄 Historical Replay / Backtesting</h1>
140
+ <p class="subtitle">Replay historical decisions with different rule versions</p>
141
+ <div class="header-links">
142
+ <a href="/simulation">← Back to Simulation Dashboard</a>
143
+ </div>
144
+ </header>
145
+
146
+ <!-- Step 1: Upload Historical Data -->
147
+ <div class="step-section">
148
+ <div class="step-header">
149
+ <div class="step-number">1</div>
150
+ <h2>Upload Historical Data</h2>
151
+ </div>
152
+ <p>Upload a CSV or JSON file with historical decision contexts. Each row should contain the context fields used in your rules.</p>
153
+
154
+ <div class="file-upload-area" id="uploadArea">
155
+ <p>📁 Drag and drop your CSV/JSON file here, or click to browse</p>
156
+ <input type="file" id="fileInput" accept=".csv,.json" style="display: none;">
157
+ </div>
158
+
159
+ <div id="uploadStatus" class="hidden"></div>
160
+ </div>
161
+
162
+ <!-- Step 2: Configure Rules -->
163
+ <div class="step-section">
164
+ <div class="step-header">
165
+ <div class="step-number">2</div>
166
+ <h2>Configure Rules</h2>
167
+ </div>
168
+ <p>Paste your rules JSON or select a version to use</p>
169
+
170
+ <div class="form-group">
171
+ <label for="rulesJson">Rules JSON:</label>
172
+ <textarea id="rulesJson" class="input" rows="10" placeholder='{"version": "1.0", "ruleset": "my_ruleset", "rules": [...]}'></textarea>
173
+ </div>
174
+
175
+ <div class="form-group">
176
+ <label for="ruleVersion">Or select version:</label>
177
+ <select id="ruleVersion" class="input">
178
+ <option value="">-- Select Version --</option>
179
+ </select>
180
+ </div>
181
+
182
+ <div class="form-group">
183
+ <label for="compareVersion">Compare with version (optional):</label>
184
+ <select id="compareVersion" class="input">
185
+ <option value="">-- None --</option>
186
+ </select>
187
+ </div>
188
+
189
+ <div class="actions">
190
+ <label for="importRulesFile" class="btn btn-secondary">📁 Import Rules JSON
191
+ <input type="file" id="importRulesFile" accept=".json" style="display: none;">
192
+ </label>
193
+ <button class="btn btn-secondary" id="validateRulesBtn">✓ Validate Rules</button>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Step 3: Run Replay -->
198
+ <div class="step-section">
199
+ <div class="step-header">
200
+ <div class="step-number">3</div>
201
+ <h2>Run Historical Replay</h2>
202
+ </div>
203
+
204
+ <div class="form-group">
205
+ <label>
206
+ <input type="checkbox" id="parallelExecution" checked> Parallel execution
207
+ </label>
208
+ </div>
209
+ <div class="form-group">
210
+ <label for="threadCount">Thread count:</label>
211
+ <input type="number" id="threadCount" value="4" min="1" max="16" class="input" style="width: 100px;">
212
+ </div>
213
+
214
+ <button class="btn btn-primary" id="runReplayBtn" disabled>▶ Run Replay</button>
215
+
216
+ <div id="runStatus" class="hidden"></div>
217
+ </div>
218
+
219
+ <!-- Step 4: Results -->
220
+ <div class="step-section" id="resultsSection" style="display: none;">
221
+ <div class="step-header">
222
+ <div class="step-number">4</div>
223
+ <h2>Replay Results</h2>
224
+ </div>
225
+
226
+ <div id="replayInfo">
227
+ <p><strong>Replay ID:</strong> <span id="replayId"></span></p>
228
+ </div>
229
+
230
+ <!-- Statistics -->
231
+ <div id="statisticsSection" class="hidden">
232
+ <h3>Statistics</h3>
233
+ <div class="metrics-grid" id="statisticsGrid"></div>
234
+ </div>
235
+
236
+ <!-- Decision Distribution -->
237
+ <div id="distributionSection" class="hidden">
238
+ <h3>Decision Distribution</h3>
239
+ <div id="distributionContent"></div>
240
+ </div>
241
+
242
+ <!-- Comparison Results (if comparing) -->
243
+ <div id="comparisonSection" class="hidden">
244
+ <h3>Comparison Results</h3>
245
+ <div class="metrics-grid" id="comparisonGrid"></div>
246
+ </div>
247
+ </div>
248
+ </div>
249
+
250
+ <script>
251
+ // Helper function to get the base path for API calls
252
+ function getBasePath() {
253
+ const baseTag = document.querySelector('base');
254
+ if (baseTag && baseTag.href) {
255
+ try {
256
+ const baseUrl = new URL(baseTag.href, window.location.href);
257
+ let path = baseUrl.pathname;
258
+ if (path && !path.endsWith('/')) {
259
+ path += '/';
260
+ }
261
+ return path;
262
+ } catch (e) {
263
+ if (baseTag.href.startsWith('/')) {
264
+ const match = baseTag.href.match(/^(https?:\/\/[^\/]+)?(\/.*?)\/?$/);
265
+ if (match && match[2]) {
266
+ return match[2].endsWith('/') ? match[2] : match[2] + '/';
267
+ }
268
+ } else if (baseTag.href.startsWith('./')) {
269
+ const pathname = window.location.pathname;
270
+ return pathname.substring(0, pathname.lastIndexOf('/') + 1);
271
+ }
272
+ }
273
+ }
274
+ const pathname = window.location.pathname;
275
+ if (pathname.includes('/decision_agent')) {
276
+ const match = pathname.match(/^(\/.*?\/decision_agent)\/?/);
277
+ if (match) {
278
+ return match[1].endsWith('/') ? match[1] : match[1] + '/';
279
+ }
280
+ }
281
+ const dirPath = pathname.substring(0, pathname.lastIndexOf('/') + 1);
282
+ return dirPath || '/';
283
+ }
284
+
285
+ const basePath = getBasePath();
286
+ let historicalData = null;
287
+
288
+ // Load versions
289
+ function loadVersions() {
290
+ fetch(`${basePath}api/versions`)
291
+ .then(response => response.json())
292
+ .then(data => {
293
+ const ruleVersionSelect = document.getElementById('ruleVersion');
294
+ const compareVersionSelect = document.getElementById('compareVersion');
295
+
296
+ ruleVersionSelect.innerHTML = '<option value="">-- Select Version --</option>';
297
+ compareVersionSelect.innerHTML = '<option value="">-- None --</option>';
298
+
299
+ if (data.versions) {
300
+ data.versions.forEach(v => {
301
+ const option1 = document.createElement('option');
302
+ option1.value = v.id;
303
+ option1.textContent = `${v.rule_id} v${v.version_number} (${v.status})`;
304
+ ruleVersionSelect.appendChild(option1);
305
+
306
+ const option2 = document.createElement('option');
307
+ option2.value = v.id;
308
+ option2.textContent = `${v.rule_id} v${v.version_number} (${v.status})`;
309
+ compareVersionSelect.appendChild(option2);
310
+ });
311
+ }
312
+ })
313
+ .catch(error => console.error('Error loading versions:', error));
314
+ }
315
+
316
+ loadVersions();
317
+
318
+ // File upload
319
+ const uploadArea = document.getElementById('uploadArea');
320
+ const fileInput = document.getElementById('fileInput');
321
+ const uploadStatus = document.getElementById('uploadStatus');
322
+
323
+ uploadArea.addEventListener('click', () => fileInput.click());
324
+ uploadArea.addEventListener('dragover', (e) => {
325
+ e.preventDefault();
326
+ uploadArea.classList.add('dragover');
327
+ });
328
+ uploadArea.addEventListener('dragleave', () => {
329
+ uploadArea.classList.remove('dragover');
330
+ });
331
+ uploadArea.addEventListener('drop', (e) => {
332
+ e.preventDefault();
333
+ uploadArea.classList.remove('dragover');
334
+ const files = e.dataTransfer.files;
335
+ if (files.length > 0) {
336
+ handleFileUpload(files[0]);
337
+ }
338
+ });
339
+ fileInput.addEventListener('change', (e) => {
340
+ if (e.target.files.length > 0) {
341
+ handleFileUpload(e.target.files[0]);
342
+ }
343
+ });
344
+
345
+ function handleFileUpload(file) {
346
+ const reader = new FileReader();
347
+ reader.onload = (e) => {
348
+ try {
349
+ if (file.name.endsWith('.json')) {
350
+ historicalData = JSON.parse(e.target.result);
351
+ } else if (file.name.endsWith('.csv')) {
352
+ // Simple CSV parsing
353
+ const lines = e.target.result.split('\n');
354
+ const headers = lines[0].split(',');
355
+ historicalData = lines.slice(1).filter(l => l.trim()).map(line => {
356
+ const values = line.split(',');
357
+ const obj = {};
358
+ headers.forEach((h, i) => {
359
+ obj[h.trim()] = values[i]?.trim() || '';
360
+ });
361
+ return obj;
362
+ });
363
+ }
364
+
365
+ uploadStatus.className = 'success-message';
366
+ uploadStatus.textContent = `Successfully loaded ${historicalData.length} historical records`;
367
+ uploadStatus.classList.remove('hidden');
368
+ document.getElementById('runReplayBtn').disabled = false;
369
+ } catch (error) {
370
+ uploadStatus.className = 'error-message';
371
+ uploadStatus.textContent = `Error: ${error.message}`;
372
+ uploadStatus.classList.remove('hidden');
373
+ }
374
+ };
375
+ reader.readAsText(file);
376
+ }
377
+
378
+ // Import rules JSON
379
+ document.getElementById('importRulesFile').addEventListener('change', (e) => {
380
+ const file = e.target.files[0];
381
+ if (file) {
382
+ const reader = new FileReader();
383
+ reader.onload = (e) => {
384
+ try {
385
+ const json = JSON.parse(e.target.result);
386
+ document.getElementById('rulesJson').value = JSON.stringify(json, null, 2);
387
+ } catch (error) {
388
+ alert('Invalid JSON file: ' + error.message);
389
+ }
390
+ };
391
+ reader.readAsText(file);
392
+ }
393
+ });
394
+
395
+ // Validate rules
396
+ document.getElementById('validateRulesBtn').addEventListener('click', () => {
397
+ const rulesJson = document.getElementById('rulesJson').value;
398
+ try {
399
+ JSON.parse(rulesJson);
400
+ fetch(`${basePath}api/validate`, {
401
+ method: 'POST',
402
+ headers: { 'Content-Type': 'application/json' },
403
+ body: JSON.stringify(JSON.parse(rulesJson))
404
+ })
405
+ .then(response => response.json())
406
+ .then(data => {
407
+ if (data.valid) {
408
+ alert('Rules are valid!');
409
+ } else {
410
+ alert('Validation errors: ' + data.errors.join(', '));
411
+ }
412
+ });
413
+ } catch (error) {
414
+ alert('Invalid JSON: ' + error.message);
415
+ }
416
+ });
417
+
418
+ // Run replay
419
+ document.getElementById('runReplayBtn').addEventListener('click', () => {
420
+ if (!historicalData) {
421
+ alert('Please upload historical data first');
422
+ return;
423
+ }
424
+
425
+ const rulesJson = document.getElementById('rulesJson').value;
426
+ const ruleVersion = document.getElementById('ruleVersion').value;
427
+ const compareVersion = document.getElementById('compareVersion').value;
428
+
429
+ if (!rulesJson.trim() && !ruleVersion) {
430
+ alert('Please provide rules JSON or select a version');
431
+ return;
432
+ }
433
+
434
+ try {
435
+ const rules = rulesJson.trim() ? JSON.parse(rulesJson) : null;
436
+
437
+ const runStatus = document.getElementById('runStatus');
438
+ runStatus.className = 'hidden';
439
+
440
+ const requestData = {
441
+ historical_data: historicalData,
442
+ options: {
443
+ parallel: document.getElementById('parallelExecution').checked,
444
+ thread_count: parseInt(document.getElementById('threadCount').value) || 4
445
+ }
446
+ };
447
+
448
+ if (rules) {
449
+ requestData.rules = rules;
450
+ }
451
+ if (ruleVersion) {
452
+ requestData.rule_version = ruleVersion;
453
+ }
454
+ if (compareVersion) {
455
+ requestData.compare_with = compareVersion;
456
+ }
457
+
458
+ fetch(`${basePath}api/simulation/replay`, {
459
+ method: 'POST',
460
+ headers: { 'Content-Type': 'application/json' },
461
+ body: JSON.stringify(requestData)
462
+ })
463
+ .then(response => response.json())
464
+ .then(data => {
465
+ if (data.error) {
466
+ runStatus.className = 'error-message';
467
+ runStatus.textContent = `Error: ${data.error}`;
468
+ runStatus.classList.remove('hidden');
469
+ } else {
470
+ runStatus.className = 'success-message';
471
+ runStatus.textContent = 'Replay completed successfully!';
472
+ runStatus.classList.remove('hidden');
473
+ displayResults(data);
474
+ }
475
+ })
476
+ .catch(error => {
477
+ runStatus.className = 'error-message';
478
+ runStatus.textContent = `Error: ${error.message}`;
479
+ runStatus.classList.remove('hidden');
480
+ });
481
+ } catch (error) {
482
+ alert('Invalid JSON: ' + error.message);
483
+ }
484
+ });
485
+
486
+ function displayResults(data) {
487
+ document.getElementById('resultsSection').style.display = 'block';
488
+ document.getElementById('replayId').textContent = data.replay_id;
489
+
490
+ const results = data.results;
491
+
492
+ // Statistics
493
+ if (results.total_decisions !== undefined) {
494
+ const statsGrid = document.getElementById('statisticsGrid');
495
+ statsGrid.innerHTML = `
496
+ <div class="metric-card">
497
+ <div class="metric-value">${results.total_decisions}</div>
498
+ <div class="metric-label">Total Decisions</div>
499
+ </div>
500
+ ${results.changed_decisions !== undefined ? `
501
+ <div class="metric-card">
502
+ <div class="metric-value">${results.changed_decisions}</div>
503
+ <div class="metric-label">Changed Decisions</div>
504
+ </div>
505
+ <div class="metric-card">
506
+ <div class="metric-value">${(results.change_rate * 100).toFixed(2)}%</div>
507
+ <div class="metric-label">Change Rate</div>
508
+ </div>
509
+ ` : ''}
510
+ ${results.average_confidence_delta !== undefined ? `
511
+ <div class="metric-card">
512
+ <div class="metric-value">${results.average_confidence_delta.toFixed(3)}</div>
513
+ <div class="metric-label">Avg Confidence Delta</div>
514
+ </div>
515
+ ` : ''}
516
+ `;
517
+ document.getElementById('statisticsSection').classList.remove('hidden');
518
+ }
519
+
520
+ // Decision distribution
521
+ if (results.decision_distribution) {
522
+ const distContent = document.getElementById('distributionContent');
523
+ let html = '<table class="results-table"><thead><tr><th>Decision</th><th>Count</th></tr></thead><tbody>';
524
+ Object.entries(results.decision_distribution).forEach(([decision, count]) => {
525
+ html += `<tr><td>${decision}</td><td>${count}</td></tr>`;
526
+ });
527
+ html += '</tbody></table>';
528
+ distContent.innerHTML = html;
529
+ document.getElementById('distributionSection').classList.remove('hidden');
530
+ }
531
+
532
+ // Comparison results
533
+ if (results.changed_decisions !== undefined) {
534
+ const compGrid = document.getElementById('comparisonGrid');
535
+ compGrid.innerHTML = `
536
+ <div class="metric-card">
537
+ <div class="metric-value">${results.changed_decisions}</div>
538
+ <div class="metric-label">Changed Decisions</div>
539
+ </div>
540
+ <div class="metric-card">
541
+ <div class="metric-value">${(results.change_rate * 100).toFixed(2)}%</div>
542
+ <div class="metric-label">Change Rate</div>
543
+ </div>
544
+ `;
545
+ document.getElementById('comparisonSection').classList.remove('hidden');
546
+ }
547
+ }
548
+ </script>
549
+ </body>
550
+ </html>
551
+