decision_agent 0.2.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.
- checksums.yaml +4 -4
- data/README.md +313 -8
- data/bin/decision_agent +104 -0
- data/lib/decision_agent/agent.rb +72 -1
- data/lib/decision_agent/context.rb +1 -0
- data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
- data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
- data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
- data/lib/decision_agent/data_enrichment/client.rb +220 -0
- data/lib/decision_agent/data_enrichment/config.rb +78 -0
- data/lib/decision_agent/data_enrichment/errors.rb +36 -0
- data/lib/decision_agent/decision.rb +102 -2
- data/lib/decision_agent/dmn/adapter.rb +135 -0
- data/lib/decision_agent/dmn/cache.rb +306 -0
- data/lib/decision_agent/dmn/decision_graph.rb +327 -0
- data/lib/decision_agent/dmn/decision_tree.rb +192 -0
- data/lib/decision_agent/dmn/errors.rb +30 -0
- data/lib/decision_agent/dmn/exporter.rb +217 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
- data/lib/decision_agent/dmn/feel/functions.rb +420 -0
- data/lib/decision_agent/dmn/feel/parser.rb +349 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
- data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
- data/lib/decision_agent/dmn/feel/types.rb +276 -0
- data/lib/decision_agent/dmn/importer.rb +77 -0
- data/lib/decision_agent/dmn/model.rb +197 -0
- data/lib/decision_agent/dmn/parser.rb +191 -0
- data/lib/decision_agent/dmn/testing.rb +333 -0
- data/lib/decision_agent/dmn/validator.rb +315 -0
- data/lib/decision_agent/dmn/versioning.rb +229 -0
- data/lib/decision_agent/dmn/visualizer.rb +513 -0
- data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
- data/lib/decision_agent/dsl/schema_validator.rb +53 -14
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
- data/lib/decision_agent/explainability/condition_trace.rb +83 -0
- data/lib/decision_agent/explainability/explainability_result.rb +52 -0
- data/lib/decision_agent/explainability/rule_trace.rb +39 -0
- data/lib/decision_agent/explainability/trace_collector.rb +24 -0
- data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
- data/lib/decision_agent/simulation/errors.rb +18 -0
- data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
- data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
- data/lib/decision_agent/simulation/replay_engine.rb +486 -0
- data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
- data/lib/decision_agent/simulation/scenario_library.rb +163 -0
- data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
- data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
- data/lib/decision_agent/simulation.rb +17 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
- data/lib/decision_agent/web/dmn_editor.rb +426 -0
- data/lib/decision_agent/web/public/app.js +119 -0
- data/lib/decision_agent/web/public/dmn-editor.css +596 -0
- data/lib/decision_agent/web/public/dmn-editor.html +250 -0
- data/lib/decision_agent/web/public/dmn-editor.js +553 -0
- data/lib/decision_agent/web/public/index.html +52 -0
- data/lib/decision_agent/web/public/simulation.html +130 -0
- data/lib/decision_agent/web/public/simulation_impact.html +478 -0
- data/lib/decision_agent/web/public/simulation_replay.html +551 -0
- data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
- data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
- data/lib/decision_agent/web/public/styles.css +86 -0
- data/lib/decision_agent/web/server.rb +1059 -23
- data/lib/decision_agent.rb +60 -2
- metadata +105 -61
- data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
- data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
- data/spec/ab_testing/ab_test_spec.rb +0 -270
- data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
- data/spec/ab_testing/storage/adapter_spec.rb +0 -64
- data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
- data/spec/activerecord_thread_safety_spec.rb +0 -553
- data/spec/advanced_operators_spec.rb +0 -3150
- data/spec/agent_spec.rb +0 -289
- data/spec/api_contract_spec.rb +0 -430
- data/spec/audit_adapters_spec.rb +0 -92
- data/spec/auth/access_audit_logger_spec.rb +0 -394
- data/spec/auth/authenticator_spec.rb +0 -112
- data/spec/auth/password_reset_spec.rb +0 -294
- data/spec/auth/permission_checker_spec.rb +0 -207
- data/spec/auth/permission_spec.rb +0 -73
- data/spec/auth/rbac_adapter_spec.rb +0 -550
- data/spec/auth/rbac_config_spec.rb +0 -82
- data/spec/auth/role_spec.rb +0 -51
- data/spec/auth/session_manager_spec.rb +0 -172
- data/spec/auth/session_spec.rb +0 -112
- data/spec/auth/user_spec.rb +0 -130
- data/spec/comprehensive_edge_cases_spec.rb +0 -1777
- data/spec/context_spec.rb +0 -127
- data/spec/decision_agent_spec.rb +0 -96
- data/spec/decision_spec.rb +0 -423
- data/spec/dsl/condition_evaluator_spec.rb +0 -774
- data/spec/dsl_validation_spec.rb +0 -648
- data/spec/edge_cases_spec.rb +0 -353
- data/spec/evaluation_spec.rb +0 -364
- data/spec/evaluation_validator_spec.rb +0 -165
- data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
- data/spec/examples.txt +0 -1633
- data/spec/issue_verification_spec.rb +0 -759
- data/spec/json_rule_evaluator_spec.rb +0 -587
- data/spec/monitoring/alert_manager_spec.rb +0 -378
- data/spec/monitoring/metrics_collector_spec.rb +0 -499
- data/spec/monitoring/monitored_agent_spec.rb +0 -222
- data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
- data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
- data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
- data/spec/performance_optimizations_spec.rb +0 -486
- data/spec/replay_edge_cases_spec.rb +0 -699
- data/spec/replay_spec.rb +0 -210
- data/spec/rfc8785_canonicalization_spec.rb +0 -215
- data/spec/scoring_spec.rb +0 -225
- data/spec/spec_helper.rb +0 -60
- data/spec/testing/batch_test_importer_spec.rb +0 -693
- data/spec/testing/batch_test_runner_spec.rb +0 -307
- data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
- data/spec/testing/test_result_comparator_spec.rb +0 -392
- data/spec/testing/test_scenario_spec.rb +0 -113
- data/spec/thread_safety_spec.rb +0 -482
- data/spec/thread_safety_spec.rb.broken +0 -878
- data/spec/versioning/adapter_spec.rb +0 -156
- data/spec/versioning_spec.rb +0 -1030
- data/spec/web/middleware/auth_middleware_spec.rb +0 -133
- data/spec/web/middleware/permission_middleware_spec.rb +0 -247
- data/spec/web_ui_rack_spec.rb +0 -1840
|
@@ -0,0 +1,532 @@
|
|
|
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>What-If Analysis</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
|
+
.scenario-builder {
|
|
24
|
+
margin-top: 15px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.scenario-item {
|
|
28
|
+
background: var(--hover-bg);
|
|
29
|
+
padding: 15px;
|
|
30
|
+
border-radius: 8px;
|
|
31
|
+
margin-bottom: 10px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.scenario-item-header {
|
|
35
|
+
display: flex;
|
|
36
|
+
justify-content: space-between;
|
|
37
|
+
align-items: center;
|
|
38
|
+
margin-bottom: 10px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.scenario-fields {
|
|
42
|
+
display: grid;
|
|
43
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
44
|
+
gap: 10px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.field-input {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.field-input label {
|
|
53
|
+
font-size: 0.85rem;
|
|
54
|
+
color: var(--text-secondary);
|
|
55
|
+
margin-bottom: 5px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.metrics-grid {
|
|
59
|
+
display: grid;
|
|
60
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
61
|
+
gap: 15px;
|
|
62
|
+
margin-top: 15px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.metric-card {
|
|
66
|
+
background: var(--hover-bg);
|
|
67
|
+
padding: 15px;
|
|
68
|
+
border-radius: 8px;
|
|
69
|
+
text-align: center;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.metric-value {
|
|
73
|
+
font-size: 2rem;
|
|
74
|
+
font-weight: bold;
|
|
75
|
+
color: var(--primary-color);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.metric-label {
|
|
79
|
+
font-size: 0.9rem;
|
|
80
|
+
color: var(--text-secondary);
|
|
81
|
+
margin-top: 5px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.hidden {
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.error-message {
|
|
89
|
+
background: #fee2e2;
|
|
90
|
+
color: #991b1b;
|
|
91
|
+
padding: 12px;
|
|
92
|
+
border-radius: 6px;
|
|
93
|
+
margin-top: 10px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.success-message {
|
|
97
|
+
background: #d1fae5;
|
|
98
|
+
color: #065f46;
|
|
99
|
+
padding: 12px;
|
|
100
|
+
border-radius: 6px;
|
|
101
|
+
margin-top: 10px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.header-links {
|
|
105
|
+
margin-top: 20px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.header-links a {
|
|
109
|
+
margin-right: 15px;
|
|
110
|
+
text-decoration: none;
|
|
111
|
+
color: var(--primary-color);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.results-table {
|
|
115
|
+
width: 100%;
|
|
116
|
+
border-collapse: collapse;
|
|
117
|
+
margin-top: 15px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.results-table th,
|
|
121
|
+
.results-table td {
|
|
122
|
+
padding: 10px;
|
|
123
|
+
text-align: left;
|
|
124
|
+
border-bottom: 1px solid var(--border-color);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.results-table th {
|
|
128
|
+
background: var(--hover-bg);
|
|
129
|
+
font-weight: 600;
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
132
|
+
</head>
|
|
133
|
+
<body>
|
|
134
|
+
<div class="simulation-container">
|
|
135
|
+
<header>
|
|
136
|
+
<h1>❓ What-If Analysis</h1>
|
|
137
|
+
<p class="subtitle">Simulate different scenarios and analyze how decisions change</p>
|
|
138
|
+
<div class="header-links">
|
|
139
|
+
<a href="/simulation">← Back to Simulation Dashboard</a>
|
|
140
|
+
</div>
|
|
141
|
+
</header>
|
|
142
|
+
|
|
143
|
+
<!-- Step 1: Configure Rules -->
|
|
144
|
+
<div class="step-section">
|
|
145
|
+
<h2>Configure Rules</h2>
|
|
146
|
+
<p>Paste your rules JSON or select a version to use</p>
|
|
147
|
+
|
|
148
|
+
<div class="form-group">
|
|
149
|
+
<label for="rulesJson">Rules JSON:</label>
|
|
150
|
+
<textarea id="rulesJson" class="input" rows="10" placeholder='{"version": "1.0", "ruleset": "my_ruleset", "rules": [...]}'></textarea>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="form-group">
|
|
154
|
+
<label for="ruleVersion">Or select version:</label>
|
|
155
|
+
<select id="ruleVersion" class="input">
|
|
156
|
+
<option value="">-- Select Version --</option>
|
|
157
|
+
</select>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div class="actions">
|
|
161
|
+
<label for="importRulesFile" class="btn btn-secondary">📁 Import Rules JSON
|
|
162
|
+
<input type="file" id="importRulesFile" accept=".json" style="display: none;">
|
|
163
|
+
</label>
|
|
164
|
+
<button class="btn btn-secondary" id="validateRulesBtn">✓ Validate Rules</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- Step 2: Define Scenarios -->
|
|
169
|
+
<div class="step-section">
|
|
170
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
|
171
|
+
<h2>Define Scenarios</h2>
|
|
172
|
+
<button class="btn btn-primary" id="addScenarioBtn">+ Add Scenario</button>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div id="scenariosContainer" class="scenario-builder">
|
|
176
|
+
<div class="scenario-item">
|
|
177
|
+
<div class="scenario-item-header">
|
|
178
|
+
<strong>Scenario 1</strong>
|
|
179
|
+
<button class="btn btn-secondary" onclick="removeScenario(this)" style="padding: 5px 10px;">Remove</button>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="scenario-fields" id="scenarioFields1">
|
|
182
|
+
<div class="field-input">
|
|
183
|
+
<label>Field Name</label>
|
|
184
|
+
<input type="text" class="input scenario-field-name" placeholder="e.g., credit_score">
|
|
185
|
+
</div>
|
|
186
|
+
<div class="field-input">
|
|
187
|
+
<label>Value</label>
|
|
188
|
+
<input type="text" class="input scenario-field-value" placeholder="e.g., 650">
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<button class="btn btn-secondary" onclick="addField(this)" style="margin-top: 10px; padding: 5px 10px;">+ Add Field</button>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<!-- Step 3: Run Analysis -->
|
|
197
|
+
<div class="step-section">
|
|
198
|
+
<h2>Run What-If Analysis</h2>
|
|
199
|
+
|
|
200
|
+
<div class="form-group">
|
|
201
|
+
<label>
|
|
202
|
+
<input type="checkbox" id="parallelExecution" checked> Parallel execution
|
|
203
|
+
</label>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="form-group">
|
|
206
|
+
<label>
|
|
207
|
+
<input type="checkbox" id="sensitivityAnalysis"> Perform sensitivity analysis
|
|
208
|
+
</label>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="form-group">
|
|
211
|
+
<label for="threadCount">Thread count:</label>
|
|
212
|
+
<input type="number" id="threadCount" value="4" min="1" max="16" class="input" style="width: 100px;">
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<button class="btn btn-primary" id="runAnalysisBtn">▶ Run Analysis</button>
|
|
216
|
+
|
|
217
|
+
<div id="runStatus" class="hidden"></div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<!-- Step 4: Results -->
|
|
221
|
+
<div class="step-section" id="resultsSection" style="display: none;">
|
|
222
|
+
<h2>Analysis Results</h2>
|
|
223
|
+
|
|
224
|
+
<!-- Statistics -->
|
|
225
|
+
<div id="statisticsSection" class="hidden">
|
|
226
|
+
<h3>Statistics</h3>
|
|
227
|
+
<div class="metrics-grid" id="statisticsGrid"></div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<!-- Decision Distribution -->
|
|
231
|
+
<div id="distributionSection" class="hidden">
|
|
232
|
+
<h3>Decision Distribution</h3>
|
|
233
|
+
<div id="distributionContent"></div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<!-- Scenario Results -->
|
|
237
|
+
<div id="scenarioResultsSection" class="hidden">
|
|
238
|
+
<h3>Scenario Results</h3>
|
|
239
|
+
<div id="scenarioResultsContent"></div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- Sensitivity Analysis -->
|
|
243
|
+
<div id="sensitivitySection" class="hidden">
|
|
244
|
+
<h3>Sensitivity Analysis</h3>
|
|
245
|
+
<div id="sensitivityContent"></div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<script>
|
|
251
|
+
function getBasePath() {
|
|
252
|
+
const baseTag = document.querySelector('base');
|
|
253
|
+
if (baseTag && baseTag.href) {
|
|
254
|
+
try {
|
|
255
|
+
const baseUrl = new URL(baseTag.href, window.location.href);
|
|
256
|
+
let path = baseUrl.pathname;
|
|
257
|
+
if (path && !path.endsWith('/')) path += '/';
|
|
258
|
+
return path;
|
|
259
|
+
} catch (e) {
|
|
260
|
+
if (baseTag.href.startsWith('/')) {
|
|
261
|
+
const match = baseTag.href.match(/^(https?:\/\/[^\/]+)?(\/.*?)\/?$/);
|
|
262
|
+
if (match && match[2]) {
|
|
263
|
+
return match[2].endsWith('/') ? match[2] : match[2] + '/';
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const pathname = window.location.pathname;
|
|
269
|
+
if (pathname.includes('/decision_agent')) {
|
|
270
|
+
const match = pathname.match(/^(\/.*?\/decision_agent)\/?/);
|
|
271
|
+
if (match) {
|
|
272
|
+
return match[1].endsWith('/') ? match[1] : match[1] + '/';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return pathname.substring(0, pathname.lastIndexOf('/') + 1) || '/';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const basePath = getBasePath();
|
|
279
|
+
let scenarioCount = 1;
|
|
280
|
+
|
|
281
|
+
// Load versions
|
|
282
|
+
function loadVersions() {
|
|
283
|
+
fetch(`${basePath}api/versions`)
|
|
284
|
+
.then(response => response.json())
|
|
285
|
+
.then(data => {
|
|
286
|
+
const select = document.getElementById('ruleVersion');
|
|
287
|
+
select.innerHTML = '<option value="">-- Select Version --</option>';
|
|
288
|
+
if (data.versions) {
|
|
289
|
+
data.versions.forEach(v => {
|
|
290
|
+
const option = document.createElement('option');
|
|
291
|
+
option.value = v.id;
|
|
292
|
+
option.textContent = `${v.rule_id} v${v.version_number} (${v.status})`;
|
|
293
|
+
select.appendChild(option);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
.catch(error => console.error('Error loading versions:', error));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
loadVersions();
|
|
301
|
+
|
|
302
|
+
// Add scenario
|
|
303
|
+
document.getElementById('addScenarioBtn').addEventListener('click', () => {
|
|
304
|
+
scenarioCount++;
|
|
305
|
+
const container = document.getElementById('scenariosContainer');
|
|
306
|
+
const scenario = document.createElement('div');
|
|
307
|
+
scenario.className = 'scenario-item';
|
|
308
|
+
scenario.innerHTML = `
|
|
309
|
+
<div class="scenario-item-header">
|
|
310
|
+
<strong>Scenario ${scenarioCount}</strong>
|
|
311
|
+
<button class="btn btn-secondary" onclick="removeScenario(this)" style="padding: 5px 10px;">Remove</button>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="scenario-fields" id="scenarioFields${scenarioCount}">
|
|
314
|
+
<div class="field-input">
|
|
315
|
+
<label>Field Name</label>
|
|
316
|
+
<input type="text" class="input scenario-field-name" placeholder="e.g., credit_score">
|
|
317
|
+
</div>
|
|
318
|
+
<div class="field-input">
|
|
319
|
+
<label>Value</label>
|
|
320
|
+
<input type="text" class="input scenario-field-value" placeholder="e.g., 650">
|
|
321
|
+
</div>
|
|
322
|
+
</div>
|
|
323
|
+
<button class="btn btn-secondary" onclick="addField(this)" style="margin-top: 10px; padding: 5px 10px;">+ Add Field</button>
|
|
324
|
+
`;
|
|
325
|
+
container.appendChild(scenario);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
function removeScenario(btn) {
|
|
329
|
+
btn.closest('.scenario-item').remove();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function addField(btn) {
|
|
333
|
+
const fieldsContainer = btn.previousElementSibling;
|
|
334
|
+
const fieldDiv = document.createElement('div');
|
|
335
|
+
fieldDiv.className = 'field-input';
|
|
336
|
+
fieldDiv.innerHTML = `
|
|
337
|
+
<label>Field Name</label>
|
|
338
|
+
<input type="text" class="input scenario-field-name" placeholder="e.g., amount">
|
|
339
|
+
<label style="margin-top: 5px;">Value</label>
|
|
340
|
+
<input type="text" class="input scenario-field-value" placeholder="e.g., 100000">
|
|
341
|
+
`;
|
|
342
|
+
fieldsContainer.appendChild(fieldDiv);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Import rules
|
|
346
|
+
document.getElementById('importRulesFile').addEventListener('change', (e) => {
|
|
347
|
+
const file = e.target.files[0];
|
|
348
|
+
if (file) {
|
|
349
|
+
const reader = new FileReader();
|
|
350
|
+
reader.onload = (e) => {
|
|
351
|
+
try {
|
|
352
|
+
const json = JSON.parse(e.target.result);
|
|
353
|
+
document.getElementById('rulesJson').value = JSON.stringify(json, null, 2);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
alert('Invalid JSON file: ' + error.message);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
reader.readAsText(file);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Validate rules
|
|
363
|
+
document.getElementById('validateRulesBtn').addEventListener('click', () => {
|
|
364
|
+
const rulesJson = document.getElementById('rulesJson').value;
|
|
365
|
+
try {
|
|
366
|
+
JSON.parse(rulesJson);
|
|
367
|
+
fetch(`${basePath}api/validate`, {
|
|
368
|
+
method: 'POST',
|
|
369
|
+
headers: { 'Content-Type': 'application/json' },
|
|
370
|
+
body: JSON.stringify(JSON.parse(rulesJson))
|
|
371
|
+
})
|
|
372
|
+
.then(response => response.json())
|
|
373
|
+
.then(data => {
|
|
374
|
+
if (data.valid) {
|
|
375
|
+
alert('Rules are valid!');
|
|
376
|
+
} else {
|
|
377
|
+
alert('Validation errors: ' + data.errors.join(', '));
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
} catch (error) {
|
|
381
|
+
alert('Invalid JSON: ' + error.message);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// Run analysis
|
|
386
|
+
document.getElementById('runAnalysisBtn').addEventListener('click', () => {
|
|
387
|
+
// Collect scenarios
|
|
388
|
+
const scenarios = [];
|
|
389
|
+
document.querySelectorAll('.scenario-item').forEach(item => {
|
|
390
|
+
const scenario = {};
|
|
391
|
+
const fields = item.querySelectorAll('.scenario-fields');
|
|
392
|
+
fields.forEach(fieldContainer => {
|
|
393
|
+
const names = fieldContainer.querySelectorAll('.scenario-field-name');
|
|
394
|
+
const values = fieldContainer.querySelectorAll('.scenario-field-value');
|
|
395
|
+
names.forEach((name, i) => {
|
|
396
|
+
if (name.value && values[i] && values[i].value) {
|
|
397
|
+
const value = values[i].value;
|
|
398
|
+
// Try to parse as number
|
|
399
|
+
scenario[name.value] = isNaN(value) ? value : parseFloat(value);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
if (Object.keys(scenario).length > 0) {
|
|
404
|
+
scenarios.push(scenario);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (scenarios.length === 0) {
|
|
409
|
+
alert('Please define at least one scenario');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const rulesJson = document.getElementById('rulesJson').value;
|
|
414
|
+
const ruleVersion = document.getElementById('ruleVersion').value;
|
|
415
|
+
|
|
416
|
+
if (!rulesJson.trim() && !ruleVersion) {
|
|
417
|
+
alert('Please provide rules JSON or select a version');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
const rules = rulesJson.trim() ? JSON.parse(rulesJson) : null;
|
|
423
|
+
|
|
424
|
+
const runStatus = document.getElementById('runStatus');
|
|
425
|
+
runStatus.className = 'hidden';
|
|
426
|
+
|
|
427
|
+
const requestData = {
|
|
428
|
+
scenarios: scenarios,
|
|
429
|
+
options: {
|
|
430
|
+
parallel: document.getElementById('parallelExecution').checked,
|
|
431
|
+
thread_count: parseInt(document.getElementById('threadCount').value) || 4,
|
|
432
|
+
sensitivity_analysis: document.getElementById('sensitivityAnalysis').checked
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
if (rules) {
|
|
437
|
+
requestData.rules = rules;
|
|
438
|
+
}
|
|
439
|
+
if (ruleVersion) {
|
|
440
|
+
requestData.rule_version = ruleVersion;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
fetch(`${basePath}api/simulation/whatif`, {
|
|
444
|
+
method: 'POST',
|
|
445
|
+
headers: { 'Content-Type': 'application/json' },
|
|
446
|
+
body: JSON.stringify(requestData)
|
|
447
|
+
})
|
|
448
|
+
.then(response => response.json())
|
|
449
|
+
.then(data => {
|
|
450
|
+
if (data.error) {
|
|
451
|
+
runStatus.className = 'error-message';
|
|
452
|
+
runStatus.textContent = `Error: ${data.error}`;
|
|
453
|
+
runStatus.classList.remove('hidden');
|
|
454
|
+
} else {
|
|
455
|
+
runStatus.className = 'success-message';
|
|
456
|
+
runStatus.textContent = 'Analysis completed successfully!';
|
|
457
|
+
runStatus.classList.remove('hidden');
|
|
458
|
+
displayResults(data);
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
.catch(error => {
|
|
462
|
+
runStatus.className = 'error-message';
|
|
463
|
+
runStatus.textContent = `Error: ${error.message}`;
|
|
464
|
+
runStatus.classList.remove('hidden');
|
|
465
|
+
});
|
|
466
|
+
} catch (error) {
|
|
467
|
+
alert('Invalid JSON: ' + error.message);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
function displayResults(data) {
|
|
472
|
+
document.getElementById('resultsSection').style.display = 'block';
|
|
473
|
+
const results = data.results;
|
|
474
|
+
|
|
475
|
+
// Statistics
|
|
476
|
+
if (results.total_scenarios !== undefined) {
|
|
477
|
+
const statsGrid = document.getElementById('statisticsGrid');
|
|
478
|
+
statsGrid.innerHTML = `
|
|
479
|
+
<div class="metric-card">
|
|
480
|
+
<div class="metric-value">${results.total_scenarios}</div>
|
|
481
|
+
<div class="metric-label">Total Scenarios</div>
|
|
482
|
+
</div>
|
|
483
|
+
${results.average_confidence !== undefined ? `
|
|
484
|
+
<div class="metric-card">
|
|
485
|
+
<div class="metric-value">${results.average_confidence.toFixed(3)}</div>
|
|
486
|
+
<div class="metric-label">Avg Confidence</div>
|
|
487
|
+
</div>
|
|
488
|
+
` : ''}
|
|
489
|
+
`;
|
|
490
|
+
document.getElementById('statisticsSection').classList.remove('hidden');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Decision distribution
|
|
494
|
+
if (results.decision_distribution) {
|
|
495
|
+
const distContent = document.getElementById('distributionContent');
|
|
496
|
+
let html = '<table class="results-table"><thead><tr><th>Decision</th><th>Count</th></tr></thead><tbody>';
|
|
497
|
+
Object.entries(results.decision_distribution).forEach(([decision, count]) => {
|
|
498
|
+
html += `<tr><td>${decision}</td><td>${count}</td></tr>`;
|
|
499
|
+
});
|
|
500
|
+
html += '</tbody></table>';
|
|
501
|
+
distContent.innerHTML = html;
|
|
502
|
+
document.getElementById('distributionSection').classList.remove('hidden');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Scenario results
|
|
506
|
+
if (results.scenarios) {
|
|
507
|
+
const scenarioContent = document.getElementById('scenarioResultsContent');
|
|
508
|
+
let html = '<table class="results-table"><thead><tr><th>Scenario</th><th>Decision</th><th>Confidence</th></tr></thead><tbody>';
|
|
509
|
+
results.scenarios.forEach((s, i) => {
|
|
510
|
+
html += `<tr>
|
|
511
|
+
<td>${JSON.stringify(s.scenario)}</td>
|
|
512
|
+
<td>${s.decision}</td>
|
|
513
|
+
<td>${s.confidence.toFixed(3)}</td>
|
|
514
|
+
</tr>`;
|
|
515
|
+
});
|
|
516
|
+
html += '</tbody></table>';
|
|
517
|
+
scenarioContent.innerHTML = html;
|
|
518
|
+
document.getElementById('scenarioResultsSection').classList.remove('hidden');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Sensitivity analysis
|
|
522
|
+
if (results.sensitivity) {
|
|
523
|
+
const sensContent = document.getElementById('sensitivityContent');
|
|
524
|
+
let html = '<p>Sensitivity analysis results:</p><pre>' + JSON.stringify(results.sensitivity, null, 2) + '</pre>';
|
|
525
|
+
sensContent.innerHTML = html;
|
|
526
|
+
document.getElementById('sensitivitySection').classList.remove('hidden');
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
</script>
|
|
530
|
+
</body>
|
|
531
|
+
</html>
|
|
532
|
+
|
|
@@ -42,6 +42,7 @@ header {
|
|
|
42
42
|
background: linear-gradient(135deg, var(--primary-color), #6366f1);
|
|
43
43
|
color: white;
|
|
44
44
|
border-radius: 10px;
|
|
45
|
+
position: relative;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
header h1 {
|
|
@@ -49,6 +50,26 @@ header h1 {
|
|
|
49
50
|
margin-bottom: 10px;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
header .header-links {
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 20px;
|
|
56
|
+
right: 20px;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
header .header-links .btn-link {
|
|
60
|
+
color: white;
|
|
61
|
+
text-decoration: none;
|
|
62
|
+
padding: 8px 16px;
|
|
63
|
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
64
|
+
border-radius: 4px;
|
|
65
|
+
background: rgba(255, 255, 255, 0.1);
|
|
66
|
+
transition: background 0.2s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
header .header-links .btn-link:hover {
|
|
70
|
+
background: rgba(255, 255, 255, 0.2);
|
|
71
|
+
}
|
|
72
|
+
|
|
52
73
|
.subtitle {
|
|
53
74
|
font-size: 1.1rem;
|
|
54
75
|
opacity: 0.9;
|
|
@@ -775,3 +796,68 @@ textarea.input {
|
|
|
775
796
|
text-align: center;
|
|
776
797
|
padding: 20px;
|
|
777
798
|
}
|
|
799
|
+
|
|
800
|
+
/* Test Results Styles */
|
|
801
|
+
.test-results {
|
|
802
|
+
border: 1px solid var(--border-color);
|
|
803
|
+
border-radius: 6px;
|
|
804
|
+
padding: 20px;
|
|
805
|
+
background: var(--panel-bg);
|
|
806
|
+
margin-top: 20px;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
.test-results h3 {
|
|
810
|
+
margin-top: 0;
|
|
811
|
+
color: var(--primary-color);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
.test-result-item {
|
|
815
|
+
margin: 10px 0;
|
|
816
|
+
padding: 8px 0;
|
|
817
|
+
border-bottom: 1px solid var(--border-color);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
.test-result-item:last-child {
|
|
821
|
+
border-bottom: none;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.test-explainability {
|
|
825
|
+
margin-top: 20px;
|
|
826
|
+
padding-top: 20px;
|
|
827
|
+
border-top: 2px solid var(--border-color);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.test-explainability h4 {
|
|
831
|
+
margin-top: 0;
|
|
832
|
+
color: var(--primary-color);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
.test-because ul,
|
|
836
|
+
.test-failed ul {
|
|
837
|
+
list-style: none;
|
|
838
|
+
padding-left: 0;
|
|
839
|
+
margin: 10px 0;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.test-because li,
|
|
843
|
+
.test-failed li {
|
|
844
|
+
padding: 6px 12px;
|
|
845
|
+
margin: 4px 0;
|
|
846
|
+
border-radius: 4px;
|
|
847
|
+
background: var(--hover-bg);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
.test-because li {
|
|
851
|
+
border-left: 3px solid #28a745;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.test-failed li {
|
|
855
|
+
border-left: 3px solid #dc3545;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.form-hint {
|
|
859
|
+
display: block;
|
|
860
|
+
margin-top: 4px;
|
|
861
|
+
font-size: 0.85em;
|
|
862
|
+
color: var(--text-secondary);
|
|
863
|
+
}
|