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,546 @@
|
|
|
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>Shadow Testing</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
|
+
.context-builder {
|
|
24
|
+
margin-top: 15px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.context-item {
|
|
28
|
+
background: var(--hover-bg);
|
|
29
|
+
padding: 15px;
|
|
30
|
+
border-radius: 8px;
|
|
31
|
+
margin-bottom: 10px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.context-fields {
|
|
35
|
+
display: grid;
|
|
36
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
37
|
+
gap: 10px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.field-input {
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-direction: column;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.field-input label {
|
|
46
|
+
font-size: 0.85rem;
|
|
47
|
+
color: var(--text-secondary);
|
|
48
|
+
margin-bottom: 5px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.metrics-grid {
|
|
52
|
+
display: grid;
|
|
53
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
54
|
+
gap: 15px;
|
|
55
|
+
margin-top: 15px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.metric-card {
|
|
59
|
+
background: var(--hover-bg);
|
|
60
|
+
padding: 15px;
|
|
61
|
+
border-radius: 8px;
|
|
62
|
+
text-align: center;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.metric-value {
|
|
66
|
+
font-size: 2rem;
|
|
67
|
+
font-weight: bold;
|
|
68
|
+
color: var(--primary-color);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.metric-label {
|
|
72
|
+
font-size: 0.9rem;
|
|
73
|
+
color: var(--text-secondary);
|
|
74
|
+
margin-top: 5px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.hidden {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.error-message {
|
|
82
|
+
background: #fee2e2;
|
|
83
|
+
color: #991b1b;
|
|
84
|
+
padding: 12px;
|
|
85
|
+
border-radius: 6px;
|
|
86
|
+
margin-top: 10px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.success-message {
|
|
90
|
+
background: #d1fae5;
|
|
91
|
+
color: #065f46;
|
|
92
|
+
padding: 12px;
|
|
93
|
+
border-radius: 6px;
|
|
94
|
+
margin-top: 10px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.header-links {
|
|
98
|
+
margin-top: 20px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.header-links a {
|
|
102
|
+
margin-right: 15px;
|
|
103
|
+
text-decoration: none;
|
|
104
|
+
color: var(--primary-color);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.results-table {
|
|
108
|
+
width: 100%;
|
|
109
|
+
border-collapse: collapse;
|
|
110
|
+
margin-top: 15px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.results-table th,
|
|
114
|
+
.results-table td {
|
|
115
|
+
padding: 10px;
|
|
116
|
+
text-align: left;
|
|
117
|
+
border-bottom: 1px solid var(--border-color);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.results-table th {
|
|
121
|
+
background: var(--hover-bg);
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.match-badge {
|
|
126
|
+
display: inline-block;
|
|
127
|
+
padding: 4px 12px;
|
|
128
|
+
border-radius: 12px;
|
|
129
|
+
font-size: 0.85rem;
|
|
130
|
+
font-weight: 500;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.match-yes {
|
|
134
|
+
background: #d1fae5;
|
|
135
|
+
color: #065f46;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.match-no {
|
|
139
|
+
background: #fee2e2;
|
|
140
|
+
color: #991b1b;
|
|
141
|
+
}
|
|
142
|
+
</style>
|
|
143
|
+
</head>
|
|
144
|
+
<body>
|
|
145
|
+
<div class="simulation-container">
|
|
146
|
+
<header>
|
|
147
|
+
<h1>👻 Shadow Testing</h1>
|
|
148
|
+
<p class="subtitle">Compare new rules against production without affecting outcomes</p>
|
|
149
|
+
<div class="header-links">
|
|
150
|
+
<a href="/simulation">← Back to Simulation Dashboard</a>
|
|
151
|
+
</div>
|
|
152
|
+
</header>
|
|
153
|
+
|
|
154
|
+
<!-- Step 1: Configure Production Rules -->
|
|
155
|
+
<div class="step-section">
|
|
156
|
+
<h2>Production Rules</h2>
|
|
157
|
+
<p>Configure the production rules (or use active version)</p>
|
|
158
|
+
|
|
159
|
+
<div class="form-group">
|
|
160
|
+
<label for="productionRulesJson">Production Rules JSON:</label>
|
|
161
|
+
<textarea id="productionRulesJson" class="input" rows="10" placeholder='{"version": "1.0", "ruleset": "my_ruleset", "rules": [...]}'></textarea>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="form-group">
|
|
165
|
+
<label>
|
|
166
|
+
<input type="checkbox" id="useActiveVersion" checked> Use active version (if available)
|
|
167
|
+
</label>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- Step 2: Configure Shadow Rules -->
|
|
172
|
+
<div class="step-section">
|
|
173
|
+
<h2>Shadow Rules</h2>
|
|
174
|
+
<p>Configure the shadow rules to test (or select a version)</p>
|
|
175
|
+
|
|
176
|
+
<div class="form-group">
|
|
177
|
+
<label for="shadowRulesJson">Shadow Rules JSON:</label>
|
|
178
|
+
<textarea id="shadowRulesJson" class="input" rows="10" placeholder='{"version": "1.0", "ruleset": "my_ruleset", "rules": [...]}'></textarea>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="form-group">
|
|
182
|
+
<label for="shadowVersion">Or select shadow version:</label>
|
|
183
|
+
<select id="shadowVersion" class="input">
|
|
184
|
+
<option value="">-- Select Version --</option>
|
|
185
|
+
</select>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="actions">
|
|
189
|
+
<label for="importShadowRulesFile" class="btn btn-secondary">📁 Import Shadow Rules JSON
|
|
190
|
+
<input type="file" id="importShadowRulesFile" accept=".json" style="display: none;">
|
|
191
|
+
</label>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<!-- Step 3: Define Contexts -->
|
|
196
|
+
<div class="step-section">
|
|
197
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">
|
|
198
|
+
<h2>Test Contexts</h2>
|
|
199
|
+
<button class="btn btn-primary" id="addContextBtn">+ Add Context</button>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div id="contextsContainer" class="context-builder">
|
|
203
|
+
<div class="context-item">
|
|
204
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
205
|
+
<strong>Context 1</strong>
|
|
206
|
+
<button class="btn btn-secondary" onclick="removeContext(this)" style="padding: 5px 10px;">Remove</button>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="context-fields" id="contextFields1">
|
|
209
|
+
<div class="field-input">
|
|
210
|
+
<label>Field Name</label>
|
|
211
|
+
<input type="text" class="input context-field-name" placeholder="e.g., credit_score">
|
|
212
|
+
</div>
|
|
213
|
+
<div class="field-input">
|
|
214
|
+
<label>Value</label>
|
|
215
|
+
<input type="text" class="input context-field-value" placeholder="e.g., 650">
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
<button class="btn btn-secondary" onclick="addField(this)" style="margin-top: 10px; padding: 5px 10px;">+ Add Field</button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<!-- Step 4: Run Shadow Test -->
|
|
224
|
+
<div class="step-section">
|
|
225
|
+
<h2>Run Shadow Test</h2>
|
|
226
|
+
|
|
227
|
+
<div class="form-group">
|
|
228
|
+
<label>
|
|
229
|
+
<input type="checkbox" id="parallelExecution" checked> Parallel execution (for batch)
|
|
230
|
+
</label>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="form-group">
|
|
233
|
+
<label for="threadCount">Thread count:</label>
|
|
234
|
+
<input type="number" id="threadCount" value="4" min="1" max="16" class="input" style="width: 100px;">
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<button class="btn btn-primary" id="runShadowBtn">▶ Run Shadow Test</button>
|
|
238
|
+
|
|
239
|
+
<div id="runStatus" class="hidden"></div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<!-- Step 5: Results -->
|
|
243
|
+
<div class="step-section" id="resultsSection" style="display: none;">
|
|
244
|
+
<h2>Shadow Test Results</h2>
|
|
245
|
+
|
|
246
|
+
<!-- Statistics -->
|
|
247
|
+
<div id="statisticsSection" class="hidden">
|
|
248
|
+
<h3>Statistics</h3>
|
|
249
|
+
<div class="metrics-grid" id="statisticsGrid"></div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<!-- Results Table -->
|
|
253
|
+
<div id="resultsTableSection" class="hidden">
|
|
254
|
+
<h3>Detailed Results</h3>
|
|
255
|
+
<div id="resultsTableContent"></div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
<script>
|
|
261
|
+
function getBasePath() {
|
|
262
|
+
const baseTag = document.querySelector('base');
|
|
263
|
+
if (baseTag && baseTag.href) {
|
|
264
|
+
try {
|
|
265
|
+
const baseUrl = new URL(baseTag.href, window.location.href);
|
|
266
|
+
let path = baseUrl.pathname;
|
|
267
|
+
if (path && !path.endsWith('/')) path += '/';
|
|
268
|
+
return path;
|
|
269
|
+
} catch (e) {
|
|
270
|
+
if (baseTag.href.startsWith('/')) {
|
|
271
|
+
const match = baseTag.href.match(/^(https?:\/\/[^\/]+)?(\/.*?)\/?$/);
|
|
272
|
+
if (match && match[2]) {
|
|
273
|
+
return match[2].endsWith('/') ? match[2] : match[2] + '/';
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const pathname = window.location.pathname;
|
|
279
|
+
if (pathname.includes('/decision_agent')) {
|
|
280
|
+
const match = pathname.match(/^(\/.*?\/decision_agent)\/?/);
|
|
281
|
+
if (match) {
|
|
282
|
+
return match[1].endsWith('/') ? match[1] : match[1] + '/';
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return pathname.substring(0, pathname.lastIndexOf('/') + 1) || '/';
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const basePath = getBasePath();
|
|
289
|
+
let contextCount = 1;
|
|
290
|
+
|
|
291
|
+
// Load versions
|
|
292
|
+
function loadVersions() {
|
|
293
|
+
fetch(`${basePath}api/versions`)
|
|
294
|
+
.then(response => response.json())
|
|
295
|
+
.then(data => {
|
|
296
|
+
const select = document.getElementById('shadowVersion');
|
|
297
|
+
select.innerHTML = '<option value="">-- Select Version --</option>';
|
|
298
|
+
if (data.versions) {
|
|
299
|
+
data.versions.forEach(v => {
|
|
300
|
+
const option = document.createElement('option');
|
|
301
|
+
option.value = v.id;
|
|
302
|
+
option.textContent = `${v.rule_id} v${v.version_number} (${v.status})`;
|
|
303
|
+
select.appendChild(option);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
.catch(error => console.error('Error loading versions:', error));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
loadVersions();
|
|
311
|
+
|
|
312
|
+
// Add context
|
|
313
|
+
document.getElementById('addContextBtn').addEventListener('click', () => {
|
|
314
|
+
contextCount++;
|
|
315
|
+
const container = document.getElementById('contextsContainer');
|
|
316
|
+
const context = document.createElement('div');
|
|
317
|
+
context.className = 'context-item';
|
|
318
|
+
context.innerHTML = `
|
|
319
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
|
320
|
+
<strong>Context ${contextCount}</strong>
|
|
321
|
+
<button class="btn btn-secondary" onclick="removeContext(this)" style="padding: 5px 10px;">Remove</button>
|
|
322
|
+
</div>
|
|
323
|
+
<div class="context-fields" id="contextFields${contextCount}">
|
|
324
|
+
<div class="field-input">
|
|
325
|
+
<label>Field Name</label>
|
|
326
|
+
<input type="text" class="input context-field-name" placeholder="e.g., credit_score">
|
|
327
|
+
</div>
|
|
328
|
+
<div class="field-input">
|
|
329
|
+
<label>Value</label>
|
|
330
|
+
<input type="text" class="input context-field-value" placeholder="e.g., 650">
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
<button class="btn btn-secondary" onclick="addField(this)" style="margin-top: 10px; padding: 5px 10px;">+ Add Field</button>
|
|
334
|
+
`;
|
|
335
|
+
container.appendChild(context);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
function removeContext(btn) {
|
|
339
|
+
btn.closest('.context-item').remove();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function addField(btn) {
|
|
343
|
+
const fieldsContainer = btn.previousElementSibling;
|
|
344
|
+
const fieldDiv = document.createElement('div');
|
|
345
|
+
fieldDiv.className = 'field-input';
|
|
346
|
+
fieldDiv.innerHTML = `
|
|
347
|
+
<label>Field Name</label>
|
|
348
|
+
<input type="text" class="input context-field-name" placeholder="e.g., amount">
|
|
349
|
+
<label style="margin-top: 5px;">Value</label>
|
|
350
|
+
<input type="text" class="input context-field-value" placeholder="e.g., 100000">
|
|
351
|
+
`;
|
|
352
|
+
fieldsContainer.appendChild(fieldDiv);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Import shadow rules
|
|
356
|
+
document.getElementById('importShadowRulesFile').addEventListener('change', (e) => {
|
|
357
|
+
const file = e.target.files[0];
|
|
358
|
+
if (file) {
|
|
359
|
+
const reader = new FileReader();
|
|
360
|
+
reader.onload = (e) => {
|
|
361
|
+
try {
|
|
362
|
+
const json = JSON.parse(e.target.result);
|
|
363
|
+
document.getElementById('shadowRulesJson').value = JSON.stringify(json, null, 2);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
alert('Invalid JSON file: ' + error.message);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
reader.readAsText(file);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Run shadow test
|
|
373
|
+
document.getElementById('runShadowBtn').addEventListener('click', () => {
|
|
374
|
+
// Collect contexts
|
|
375
|
+
const contexts = [];
|
|
376
|
+
document.querySelectorAll('.context-item').forEach(item => {
|
|
377
|
+
const context = {};
|
|
378
|
+
const fields = item.querySelectorAll('.context-fields');
|
|
379
|
+
fields.forEach(fieldContainer => {
|
|
380
|
+
const names = fieldContainer.querySelectorAll('.context-field-name');
|
|
381
|
+
const values = fieldContainer.querySelectorAll('.context-field-value');
|
|
382
|
+
names.forEach((name, i) => {
|
|
383
|
+
if (name.value && values[i] && values[i].value) {
|
|
384
|
+
const value = values[i].value;
|
|
385
|
+
context[name.value] = isNaN(value) ? value : parseFloat(value);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
if (Object.keys(context).length > 0) {
|
|
390
|
+
contexts.push(context);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (contexts.length === 0) {
|
|
395
|
+
alert('Please define at least one context');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const productionRules = document.getElementById('productionRulesJson').value;
|
|
400
|
+
const shadowRules = document.getElementById('shadowRulesJson').value;
|
|
401
|
+
const shadowVersion = document.getElementById('shadowVersion').value;
|
|
402
|
+
const useActiveVersion = document.getElementById('useActiveVersion').checked;
|
|
403
|
+
|
|
404
|
+
if (!shadowRules.trim() && !shadowVersion) {
|
|
405
|
+
alert('Please provide shadow rules JSON or select a version');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (!productionRules.trim() && !useActiveVersion) {
|
|
410
|
+
alert('Please provide production rules JSON or enable "Use active version"');
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
const prodRules = productionRules.trim() ? JSON.parse(productionRules) : null;
|
|
416
|
+
const shadRules = shadowRules.trim() ? JSON.parse(shadowRules) : null;
|
|
417
|
+
|
|
418
|
+
const runStatus = document.getElementById('runStatus');
|
|
419
|
+
runStatus.className = 'hidden';
|
|
420
|
+
|
|
421
|
+
const isBatch = contexts.length > 1;
|
|
422
|
+
const endpoint = isBatch ? `${basePath}api/simulation/shadow/batch` : `${basePath}api/simulation/shadow`;
|
|
423
|
+
|
|
424
|
+
const requestData = {
|
|
425
|
+
contexts: isBatch ? contexts : contexts[0],
|
|
426
|
+
options: {
|
|
427
|
+
parallel: document.getElementById('parallelExecution').checked,
|
|
428
|
+
thread_count: parseInt(document.getElementById('threadCount').value) || 4
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
if (prodRules) {
|
|
433
|
+
requestData.production_rules = prodRules;
|
|
434
|
+
}
|
|
435
|
+
if (shadRules) {
|
|
436
|
+
requestData.shadow_rules = shadRules;
|
|
437
|
+
}
|
|
438
|
+
if (shadowVersion) {
|
|
439
|
+
requestData.shadow_version = shadowVersion;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
fetch(endpoint, {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
headers: { 'Content-Type': 'application/json' },
|
|
445
|
+
body: JSON.stringify(requestData)
|
|
446
|
+
})
|
|
447
|
+
.then(response => response.json())
|
|
448
|
+
.then(data => {
|
|
449
|
+
if (data.error) {
|
|
450
|
+
runStatus.className = 'error-message';
|
|
451
|
+
runStatus.textContent = `Error: ${data.error}`;
|
|
452
|
+
runStatus.classList.remove('hidden');
|
|
453
|
+
} else {
|
|
454
|
+
runStatus.className = 'success-message';
|
|
455
|
+
runStatus.textContent = 'Shadow test completed successfully!';
|
|
456
|
+
runStatus.classList.remove('hidden');
|
|
457
|
+
displayResults(data, isBatch);
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
.catch(error => {
|
|
461
|
+
runStatus.className = 'error-message';
|
|
462
|
+
runStatus.textContent = `Error: ${error.message}`;
|
|
463
|
+
runStatus.classList.remove('hidden');
|
|
464
|
+
});
|
|
465
|
+
} catch (error) {
|
|
466
|
+
alert('Invalid JSON: ' + error.message);
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
function displayResults(data, isBatch) {
|
|
471
|
+
document.getElementById('resultsSection').style.display = 'block';
|
|
472
|
+
|
|
473
|
+
if (isBatch) {
|
|
474
|
+
const results = data.results;
|
|
475
|
+
|
|
476
|
+
// Statistics
|
|
477
|
+
if (results.match_rate !== undefined) {
|
|
478
|
+
const statsGrid = document.getElementById('statisticsGrid');
|
|
479
|
+
statsGrid.innerHTML = `
|
|
480
|
+
<div class="metric-card">
|
|
481
|
+
<div class="metric-value">${(results.match_rate * 100).toFixed(2)}%</div>
|
|
482
|
+
<div class="metric-label">Match Rate</div>
|
|
483
|
+
</div>
|
|
484
|
+
<div class="metric-card">
|
|
485
|
+
<div class="metric-value">${results.matches || 0}</div>
|
|
486
|
+
<div class="metric-label">Matches</div>
|
|
487
|
+
</div>
|
|
488
|
+
<div class="metric-card">
|
|
489
|
+
<div class="metric-value">${results.mismatches || 0}</div>
|
|
490
|
+
<div class="metric-label">Mismatches</div>
|
|
491
|
+
</div>
|
|
492
|
+
`;
|
|
493
|
+
document.getElementById('statisticsSection').classList.remove('hidden');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Results table
|
|
497
|
+
if (results.results) {
|
|
498
|
+
const tableContent = document.getElementById('resultsTableContent');
|
|
499
|
+
let html = '<table class="results-table"><thead><tr><th>Context</th><th>Production Decision</th><th>Shadow Decision</th><th>Match</th><th>Confidence Delta</th></tr></thead><tbody>';
|
|
500
|
+
results.results.forEach(r => {
|
|
501
|
+
html += `<tr>
|
|
502
|
+
<td>${JSON.stringify(r.context)}</td>
|
|
503
|
+
<td>${r.production_decision}</td>
|
|
504
|
+
<td>${r.shadow_decision}</td>
|
|
505
|
+
<td><span class="match-badge ${r.matches ? 'match-yes' : 'match-no'}">${r.matches ? '✓ Match' : '✗ Mismatch'}</span></td>
|
|
506
|
+
<td>${r.confidence_delta ? r.confidence_delta.toFixed(3) : 'N/A'}</td>
|
|
507
|
+
</tr>`;
|
|
508
|
+
});
|
|
509
|
+
html += '</tbody></table>';
|
|
510
|
+
tableContent.innerHTML = html;
|
|
511
|
+
document.getElementById('resultsTableSection').classList.remove('hidden');
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
const result = data.result;
|
|
515
|
+
|
|
516
|
+
// Statistics
|
|
517
|
+
const statsGrid = document.getElementById('statisticsGrid');
|
|
518
|
+
statsGrid.innerHTML = `
|
|
519
|
+
<div class="metric-card">
|
|
520
|
+
<div class="metric-value"><span class="match-badge ${result.matches ? 'match-yes' : 'match-no'}">${result.matches ? '✓ Match' : '✗ Mismatch'}</span></div>
|
|
521
|
+
<div class="metric-label">Result</div>
|
|
522
|
+
</div>
|
|
523
|
+
<div class="metric-card">
|
|
524
|
+
<div class="metric-value">${result.confidence_delta ? result.confidence_delta.toFixed(3) : 'N/A'}</div>
|
|
525
|
+
<div class="metric-label">Confidence Delta</div>
|
|
526
|
+
</div>
|
|
527
|
+
`;
|
|
528
|
+
document.getElementById('statisticsSection').classList.remove('hidden');
|
|
529
|
+
|
|
530
|
+
// Results table
|
|
531
|
+
const tableContent = document.getElementById('resultsTableContent');
|
|
532
|
+
let html = '<table class="results-table"><thead><tr><th>Metric</th><th>Value</th></tr></thead><tbody>';
|
|
533
|
+
html += `<tr><td>Production Decision</td><td>${result.production_decision}</td></tr>`;
|
|
534
|
+
html += `<tr><td>Shadow Decision</td><td>${result.shadow_decision}</td></tr>`;
|
|
535
|
+
html += `<tr><td>Production Confidence</td><td>${result.production_confidence ? result.production_confidence.toFixed(3) : 'N/A'}</td></tr>`;
|
|
536
|
+
html += `<tr><td>Shadow Confidence</td><td>${result.shadow_confidence ? result.shadow_confidence.toFixed(3) : 'N/A'}</td></tr>`;
|
|
537
|
+
html += `<tr><td>Matches</td><td>${result.matches ? 'Yes' : 'No'}</td></tr>`;
|
|
538
|
+
html += '</tbody></table>';
|
|
539
|
+
tableContent.innerHTML = html;
|
|
540
|
+
document.getElementById('resultsTableSection').classList.remove('hidden');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
</script>
|
|
544
|
+
</body>
|
|
545
|
+
</html>
|
|
546
|
+
|