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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -8
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/agent.rb +72 -1
  5. data/lib/decision_agent/context.rb +1 -0
  6. data/lib/decision_agent/data_enrichment/cache/memory_adapter.rb +86 -0
  7. data/lib/decision_agent/data_enrichment/cache_adapter.rb +49 -0
  8. data/lib/decision_agent/data_enrichment/circuit_breaker.rb +135 -0
  9. data/lib/decision_agent/data_enrichment/client.rb +220 -0
  10. data/lib/decision_agent/data_enrichment/config.rb +78 -0
  11. data/lib/decision_agent/data_enrichment/errors.rb +36 -0
  12. data/lib/decision_agent/decision.rb +102 -2
  13. data/lib/decision_agent/dmn/adapter.rb +135 -0
  14. data/lib/decision_agent/dmn/cache.rb +306 -0
  15. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  16. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  17. data/lib/decision_agent/dmn/errors.rb +30 -0
  18. data/lib/decision_agent/dmn/exporter.rb +217 -0
  19. data/lib/decision_agent/dmn/feel/evaluator.rb +819 -0
  20. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  21. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  22. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  23. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  24. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  25. data/lib/decision_agent/dmn/importer.rb +77 -0
  26. data/lib/decision_agent/dmn/model.rb +197 -0
  27. data/lib/decision_agent/dmn/parser.rb +191 -0
  28. data/lib/decision_agent/dmn/testing.rb +333 -0
  29. data/lib/decision_agent/dmn/validator.rb +315 -0
  30. data/lib/decision_agent/dmn/versioning.rb +229 -0
  31. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  32. data/lib/decision_agent/dsl/condition_evaluator.rb +984 -838
  33. data/lib/decision_agent/dsl/schema_validator.rb +53 -14
  34. data/lib/decision_agent/evaluators/dmn_evaluator.rb +308 -0
  35. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +69 -9
  36. data/lib/decision_agent/explainability/condition_trace.rb +83 -0
  37. data/lib/decision_agent/explainability/explainability_result.rb +52 -0
  38. data/lib/decision_agent/explainability/rule_trace.rb +39 -0
  39. data/lib/decision_agent/explainability/trace_collector.rb +24 -0
  40. data/lib/decision_agent/monitoring/alert_manager.rb +5 -1
  41. data/lib/decision_agent/simulation/errors.rb +18 -0
  42. data/lib/decision_agent/simulation/impact_analyzer.rb +498 -0
  43. data/lib/decision_agent/simulation/monte_carlo_simulator.rb +635 -0
  44. data/lib/decision_agent/simulation/replay_engine.rb +486 -0
  45. data/lib/decision_agent/simulation/scenario_engine.rb +318 -0
  46. data/lib/decision_agent/simulation/scenario_library.rb +163 -0
  47. data/lib/decision_agent/simulation/shadow_test_engine.rb +287 -0
  48. data/lib/decision_agent/simulation/what_if_analyzer.rb +1002 -0
  49. data/lib/decision_agent/simulation.rb +17 -0
  50. data/lib/decision_agent/version.rb +1 -1
  51. data/lib/decision_agent/versioning/activerecord_adapter.rb +23 -8
  52. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  53. data/lib/decision_agent/web/public/app.js +119 -0
  54. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  55. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  56. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  57. data/lib/decision_agent/web/public/index.html +52 -0
  58. data/lib/decision_agent/web/public/simulation.html +130 -0
  59. data/lib/decision_agent/web/public/simulation_impact.html +478 -0
  60. data/lib/decision_agent/web/public/simulation_replay.html +551 -0
  61. data/lib/decision_agent/web/public/simulation_shadow.html +546 -0
  62. data/lib/decision_agent/web/public/simulation_whatif.html +532 -0
  63. data/lib/decision_agent/web/public/styles.css +86 -0
  64. data/lib/decision_agent/web/server.rb +1059 -23
  65. data/lib/decision_agent.rb +60 -2
  66. metadata +105 -61
  67. data/spec/ab_testing/ab_test_assignment_spec.rb +0 -253
  68. data/spec/ab_testing/ab_test_manager_spec.rb +0 -612
  69. data/spec/ab_testing/ab_test_spec.rb +0 -270
  70. data/spec/ab_testing/ab_testing_agent_spec.rb +0 -481
  71. data/spec/ab_testing/storage/adapter_spec.rb +0 -64
  72. data/spec/ab_testing/storage/memory_adapter_spec.rb +0 -485
  73. data/spec/activerecord_thread_safety_spec.rb +0 -553
  74. data/spec/advanced_operators_spec.rb +0 -3150
  75. data/spec/agent_spec.rb +0 -289
  76. data/spec/api_contract_spec.rb +0 -430
  77. data/spec/audit_adapters_spec.rb +0 -92
  78. data/spec/auth/access_audit_logger_spec.rb +0 -394
  79. data/spec/auth/authenticator_spec.rb +0 -112
  80. data/spec/auth/password_reset_spec.rb +0 -294
  81. data/spec/auth/permission_checker_spec.rb +0 -207
  82. data/spec/auth/permission_spec.rb +0 -73
  83. data/spec/auth/rbac_adapter_spec.rb +0 -550
  84. data/spec/auth/rbac_config_spec.rb +0 -82
  85. data/spec/auth/role_spec.rb +0 -51
  86. data/spec/auth/session_manager_spec.rb +0 -172
  87. data/spec/auth/session_spec.rb +0 -112
  88. data/spec/auth/user_spec.rb +0 -130
  89. data/spec/comprehensive_edge_cases_spec.rb +0 -1777
  90. data/spec/context_spec.rb +0 -127
  91. data/spec/decision_agent_spec.rb +0 -96
  92. data/spec/decision_spec.rb +0 -423
  93. data/spec/dsl/condition_evaluator_spec.rb +0 -774
  94. data/spec/dsl_validation_spec.rb +0 -648
  95. data/spec/edge_cases_spec.rb +0 -353
  96. data/spec/evaluation_spec.rb +0 -364
  97. data/spec/evaluation_validator_spec.rb +0 -165
  98. data/spec/examples/feedback_aware_evaluator_spec.rb +0 -460
  99. data/spec/examples.txt +0 -1633
  100. data/spec/issue_verification_spec.rb +0 -759
  101. data/spec/json_rule_evaluator_spec.rb +0 -587
  102. data/spec/monitoring/alert_manager_spec.rb +0 -378
  103. data/spec/monitoring/metrics_collector_spec.rb +0 -499
  104. data/spec/monitoring/monitored_agent_spec.rb +0 -222
  105. data/spec/monitoring/prometheus_exporter_spec.rb +0 -242
  106. data/spec/monitoring/storage/activerecord_adapter_spec.rb +0 -498
  107. data/spec/monitoring/storage/base_adapter_spec.rb +0 -61
  108. data/spec/monitoring/storage/memory_adapter_spec.rb +0 -247
  109. data/spec/performance_optimizations_spec.rb +0 -486
  110. data/spec/replay_edge_cases_spec.rb +0 -699
  111. data/spec/replay_spec.rb +0 -210
  112. data/spec/rfc8785_canonicalization_spec.rb +0 -215
  113. data/spec/scoring_spec.rb +0 -225
  114. data/spec/spec_helper.rb +0 -60
  115. data/spec/testing/batch_test_importer_spec.rb +0 -693
  116. data/spec/testing/batch_test_runner_spec.rb +0 -307
  117. data/spec/testing/test_coverage_analyzer_spec.rb +0 -292
  118. data/spec/testing/test_result_comparator_spec.rb +0 -392
  119. data/spec/testing/test_scenario_spec.rb +0 -113
  120. data/spec/thread_safety_spec.rb +0 -482
  121. data/spec/thread_safety_spec.rb.broken +0 -878
  122. data/spec/versioning/adapter_spec.rb +0 -156
  123. data/spec/versioning_spec.rb +0 -1030
  124. data/spec/web/middleware/auth_middleware_spec.rb +0 -133
  125. data/spec/web/middleware/permission_middleware_spec.rb +0 -247
  126. data/spec/web_ui_rack_spec.rb +0 -1840
@@ -0,0 +1,553 @@
1
+ // DMN Editor JavaScript
2
+
3
+ // State
4
+ const state = {
5
+ currentModel: null,
6
+ currentDecision: null,
7
+ models: [],
8
+ baseUrl: window.location.origin
9
+ };
10
+
11
+ // Initialize
12
+ document.addEventListener('DOMContentLoaded', () => {
13
+ initializeEventListeners();
14
+ loadModels();
15
+ });
16
+
17
+ // Event Listeners
18
+ function initializeEventListeners() {
19
+ // Header actions
20
+ document.getElementById('new-model-btn').addEventListener('click', () => {
21
+ openModal('new-model-modal');
22
+ });
23
+
24
+ document.getElementById('import-btn').addEventListener('click', () => {
25
+ openModal('import-modal');
26
+ });
27
+
28
+ document.getElementById('export-btn').addEventListener('click', exportModel);
29
+ document.getElementById('validate-btn').addEventListener('click', validateModel);
30
+
31
+ // Forms
32
+ document.getElementById('new-model-form').addEventListener('submit', createNewModel);
33
+ document.getElementById('add-decision-form').addEventListener('submit', addDecision);
34
+ document.getElementById('add-column-form').addEventListener('submit', addColumn);
35
+ document.getElementById('import-form').addEventListener('submit', importModel);
36
+
37
+ // Decision actions
38
+ document.getElementById('add-decision-btn').addEventListener('click', () => {
39
+ openModal('add-decision-modal');
40
+ });
41
+
42
+ document.getElementById('decision-select').addEventListener('change', (e) => {
43
+ loadDecision(e.target.value);
44
+ });
45
+
46
+ // Table controls
47
+ document.getElementById('add-input-btn').addEventListener('click', () => {
48
+ document.getElementById('column-type').value = 'input';
49
+ document.getElementById('column-modal-title').textContent = 'Add Input Column';
50
+ document.getElementById('expression-group').style.display = 'block';
51
+ openModal('add-column-modal');
52
+ });
53
+
54
+ document.getElementById('add-output-btn').addEventListener('click', () => {
55
+ document.getElementById('column-type').value = 'output';
56
+ document.getElementById('column-modal-title').textContent = 'Add Output Column';
57
+ document.getElementById('expression-group').style.display = 'none';
58
+ openModal('add-column-modal');
59
+ });
60
+
61
+ document.getElementById('add-rule-btn').addEventListener('click', addRule);
62
+ document.getElementById('hit-policy-select').addEventListener('change', updateHitPolicy);
63
+
64
+ // Tabs
65
+ document.querySelectorAll('.tab-btn').forEach(btn => {
66
+ btn.addEventListener('click', () => switchTab(btn.dataset.tab));
67
+ });
68
+
69
+ // Visualization
70
+ document.getElementById('visualize-graph-btn')?.addEventListener('click', visualizeGraph);
71
+ document.getElementById('visualize-tree-btn')?.addEventListener('click', visualizeTree);
72
+
73
+ // XML actions
74
+ document.getElementById('copy-xml-btn')?.addEventListener('click', copyXml);
75
+ document.getElementById('download-xml-btn')?.addEventListener('click', downloadXml);
76
+ }
77
+
78
+ // API Functions
79
+ async function apiCall(endpoint, method = 'GET', data = null) {
80
+ const options = {
81
+ method,
82
+ headers: {
83
+ 'Content-Type': 'application/json'
84
+ }
85
+ };
86
+
87
+ if (data) {
88
+ options.body = JSON.stringify(data);
89
+ }
90
+
91
+ const response = await fetch(`${state.baseUrl}/api/dmn${endpoint}`, options);
92
+
93
+ if (!response.ok) {
94
+ const error = await response.json();
95
+ throw new Error(error.error || 'API request failed');
96
+ }
97
+
98
+ return response.json();
99
+ }
100
+
101
+ // Model Management
102
+ async function loadModels() {
103
+ try {
104
+ const models = await apiCall('/models');
105
+ state.models = models;
106
+ renderModelList();
107
+ } catch (error) {
108
+ showNotification('Failed to load models: ' + error.message, 'error');
109
+ }
110
+ }
111
+
112
+ function renderModelList() {
113
+ const list = document.getElementById('model-list');
114
+
115
+ if (state.models.length === 0) {
116
+ list.innerHTML = '<p class="empty-state">No models yet. Create one to get started.</p>';
117
+ return;
118
+ }
119
+
120
+ list.innerHTML = state.models.map(model => `
121
+ <div class="model-item ${model.id === state.currentModel?.id ? 'active' : ''}"
122
+ onclick="loadModel('${model.id}')">
123
+ <h4>${escapeHtml(model.name)}</h4>
124
+ <p>${model.decision_count} decision(s)</p>
125
+ </div>
126
+ `).join('');
127
+ }
128
+
129
+ async function loadModel(modelId) {
130
+ try {
131
+ const model = await apiCall(`/models/${modelId}`);
132
+ state.currentModel = model;
133
+ renderModelEditor();
134
+ renderModelList();
135
+
136
+ // Hide welcome screen, show editor
137
+ document.getElementById('welcome-screen').style.display = 'none';
138
+ document.getElementById('editor-container').style.display = 'block';
139
+
140
+ // Load first decision if available
141
+ if (model.decisions && model.decisions.length > 0) {
142
+ loadDecision(model.decisions[0].id);
143
+ }
144
+ } catch (error) {
145
+ showNotification('Failed to load model: ' + error.message, 'error');
146
+ }
147
+ }
148
+
149
+ async function createNewModel(e) {
150
+ e.preventDefault();
151
+
152
+ const name = document.getElementById('model-name-input').value;
153
+ const namespace = document.getElementById('model-namespace-input').value;
154
+
155
+ try {
156
+ const model = await apiCall('/models', 'POST', { name, namespace });
157
+ state.models.push(model);
158
+ await loadModel(model.id);
159
+ closeModal('new-model-modal');
160
+ document.getElementById('new-model-form').reset();
161
+ showNotification('Model created successfully', 'success');
162
+ } catch (error) {
163
+ showNotification('Failed to create model: ' + error.message, 'error');
164
+ }
165
+ }
166
+
167
+ function renderModelEditor() {
168
+ if (!state.currentModel) return;
169
+
170
+ document.getElementById('model-name').textContent = state.currentModel.name;
171
+ document.getElementById('model-id').textContent = `ID: ${state.currentModel.id}`;
172
+
173
+ // Populate decision selector
174
+ const select = document.getElementById('decision-select');
175
+ if (state.currentModel.decisions && state.currentModel.decisions.length > 0) {
176
+ select.innerHTML = state.currentModel.decisions.map(d =>
177
+ `<option value="${d.id}">${escapeHtml(d.name)}</option>`
178
+ ).join('');
179
+ } else {
180
+ select.innerHTML = '<option value="">No decisions yet</option>';
181
+ }
182
+ }
183
+
184
+ // Decision Management
185
+ async function addDecision(e) {
186
+ e.preventDefault();
187
+
188
+ const decisionId = document.getElementById('decision-id-input').value;
189
+ const name = document.getElementById('decision-name-input').value;
190
+ const type = document.getElementById('decision-type-select').value;
191
+
192
+ try {
193
+ const decision = await apiCall(`/models/${state.currentModel.id}/decisions`, 'POST', {
194
+ decision_id: decisionId,
195
+ name,
196
+ type
197
+ });
198
+
199
+ state.currentModel.decisions.push(decision);
200
+ renderModelEditor();
201
+ loadDecision(decision.id);
202
+ closeModal('add-decision-modal');
203
+ document.getElementById('add-decision-form').reset();
204
+ showNotification('Decision added successfully', 'success');
205
+ } catch (error) {
206
+ showNotification('Failed to add decision: ' + error.message, 'error');
207
+ }
208
+ }
209
+
210
+ async function loadDecision(decisionId) {
211
+ const decision = state.currentModel.decisions.find(d => d.id === decisionId);
212
+ if (!decision) return;
213
+
214
+ state.currentDecision = decision;
215
+ document.getElementById('decision-select').value = decisionId;
216
+
217
+ if (decision.decision_table) {
218
+ renderDecisionTable(decision.decision_table);
219
+ }
220
+ }
221
+
222
+ // Decision Table
223
+ function renderDecisionTable(table) {
224
+ const headers = document.getElementById('table-headers');
225
+ const types = document.getElementById('table-types');
226
+ const body = document.getElementById('table-body');
227
+
228
+ // Set hit policy
229
+ document.getElementById('hit-policy-select').value = table.hit_policy;
230
+
231
+ // Render headers
232
+ let headerHtml = '<th class="rule-number">#</th>';
233
+ let typeHtml = '<th></th>';
234
+
235
+ table.inputs.forEach(input => {
236
+ headerHtml += `<th class="input-col">${escapeHtml(input.label)} <span class="col-type">(Input)</span></th>`;
237
+ typeHtml += `<th class="type-annotation">${input.type_ref || 'string'}</th>`;
238
+ });
239
+
240
+ table.outputs.forEach(output => {
241
+ headerHtml += `<th class="output-col">${escapeHtml(output.label)} <span class="col-type">(Output)</span></th>`;
242
+ typeHtml += `<th class="type-annotation">${output.type_ref || 'string'}</th>`;
243
+ });
244
+
245
+ headerHtml += '<th class="actions-col">Actions</th>';
246
+ typeHtml += '<th></th>';
247
+
248
+ headers.innerHTML = headerHtml;
249
+ types.innerHTML = typeHtml;
250
+
251
+ // Render rules
252
+ if (table.rules.length === 0) {
253
+ body.innerHTML = `<tr class="empty-row"><td colspan="${table.inputs.length + table.outputs.length + 2}">
254
+ No rules defined. Click "Add Rule" to create one.</td></tr>`;
255
+ return;
256
+ }
257
+
258
+ body.innerHTML = table.rules.map((rule, index) => {
259
+ let rowHtml = `<tr data-rule-id="${rule.id}">`;
260
+ rowHtml += `<td class="rule-number">${index + 1}</td>`;
261
+
262
+ rule.input_entries.forEach((entry, i) => {
263
+ rowHtml += `<td><input type="text" value="${escapeHtml(entry)}"
264
+ onchange="updateRuleEntry('${rule.id}', 'input', ${i}, this.value)"></td>`;
265
+ });
266
+
267
+ rule.output_entries.forEach((entry, i) => {
268
+ rowHtml += `<td><input type="text" value="${escapeHtml(entry)}"
269
+ onchange="updateRuleEntry('${rule.id}', 'output', ${i}, this.value)"></td>`;
270
+ });
271
+
272
+ rowHtml += `<td class="actions-col">
273
+ <button class="btn btn-sm btn-danger" onclick="deleteRule('${rule.id}')">Delete</button>
274
+ </td>`;
275
+ rowHtml += '</tr>';
276
+
277
+ return rowHtml;
278
+ }).join('');
279
+ }
280
+
281
+ async function addColumn(e) {
282
+ e.preventDefault();
283
+
284
+ const columnType = document.getElementById('column-type').value;
285
+ const id = document.getElementById('column-id-input').value;
286
+ const label = document.getElementById('column-label-input').value;
287
+ const typeRef = document.getElementById('column-type-input').value;
288
+ const expression = document.getElementById('column-expression-input').value;
289
+
290
+ try {
291
+ const endpoint = `/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/${columnType}s`;
292
+
293
+ const data = {
294
+ [`${columnType}_id`]: id,
295
+ label,
296
+ type_ref: typeRef
297
+ };
298
+
299
+ if (columnType === 'input' && expression) {
300
+ data.expression = expression;
301
+ } else if (columnType === 'output') {
302
+ data.name = id;
303
+ }
304
+
305
+ await apiCall(endpoint, 'POST', data);
306
+
307
+ // Reload model to get updated state
308
+ await loadModel(state.currentModel.id);
309
+ loadDecision(state.currentDecision.id);
310
+
311
+ closeModal('add-column-modal');
312
+ document.getElementById('add-column-form').reset();
313
+ showNotification(`${columnType === 'input' ? 'Input' : 'Output'} column added successfully`, 'success');
314
+ } catch (error) {
315
+ showNotification('Failed to add column: ' + error.message, 'error');
316
+ }
317
+ }
318
+
319
+ async function addRule() {
320
+ if (!state.currentDecision || !state.currentDecision.decision_table) return;
321
+
322
+ const table = state.currentDecision.decision_table;
323
+ const ruleId = `rule_${Date.now()}`;
324
+ const inputEntries = table.inputs.map(() => '-');
325
+ const outputEntries = table.outputs.map(() => '');
326
+
327
+ try {
328
+ await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/rules`, 'POST', {
329
+ rule_id: ruleId,
330
+ input_entries: inputEntries,
331
+ output_entries: outputEntries
332
+ });
333
+
334
+ // Reload model
335
+ await loadModel(state.currentModel.id);
336
+ loadDecision(state.currentDecision.id);
337
+ showNotification('Rule added successfully', 'success');
338
+ } catch (error) {
339
+ showNotification('Failed to add rule: ' + error.message, 'error');
340
+ }
341
+ }
342
+
343
+ async function updateRuleEntry(ruleId, type, index, value) {
344
+ try {
345
+ const rule = state.currentDecision.decision_table.rules.find(r => r.id === ruleId);
346
+ if (!rule) return;
347
+
348
+ if (type === 'input') {
349
+ rule.input_entries[index] = value;
350
+ } else {
351
+ rule.output_entries[index] = value;
352
+ }
353
+
354
+ await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/rules/${ruleId}`, 'PUT', {
355
+ input_entries: rule.input_entries,
356
+ output_entries: rule.output_entries
357
+ });
358
+ } catch (error) {
359
+ showNotification('Failed to update rule: ' + error.message, 'error');
360
+ }
361
+ }
362
+
363
+ async function deleteRule(ruleId) {
364
+ if (!confirm('Are you sure you want to delete this rule?')) return;
365
+
366
+ try {
367
+ await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/rules/${ruleId}`, 'DELETE');
368
+
369
+ // Reload model
370
+ await loadModel(state.currentModel.id);
371
+ loadDecision(state.currentDecision.id);
372
+ showNotification('Rule deleted successfully', 'success');
373
+ } catch (error) {
374
+ showNotification('Failed to delete rule: ' + error.message, 'error');
375
+ }
376
+ }
377
+
378
+ async function updateHitPolicy(e) {
379
+ if (!state.currentDecision || !state.currentDecision.decision_table) return;
380
+
381
+ try {
382
+ await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}`, 'PUT', {
383
+ logic: { hit_policy: e.target.value }
384
+ });
385
+
386
+ showNotification('Hit policy updated successfully', 'success');
387
+ } catch (error) {
388
+ showNotification('Failed to update hit policy: ' + error.message, 'error');
389
+ }
390
+ }
391
+
392
+ // Import/Export
393
+ async function exportModel() {
394
+ if (!state.currentModel) {
395
+ showNotification('No model selected', 'warning');
396
+ return;
397
+ }
398
+
399
+ try {
400
+ const xml = await apiCall(`/models/${state.currentModel.id}/export`);
401
+
402
+ // Update XML tab
403
+ document.getElementById('xml-content').innerHTML = `<code>${escapeHtml(xml)}</code>`;
404
+
405
+ showNotification('Model exported successfully', 'success');
406
+ } catch (error) {
407
+ showNotification('Failed to export model: ' + error.message, 'error');
408
+ }
409
+ }
410
+
411
+ async function importModel(e) {
412
+ e.preventDefault();
413
+
414
+ const fileInput = document.getElementById('import-file-input');
415
+ const file = fileInput.files[0];
416
+
417
+ if (!file) {
418
+ showNotification('Please select a file', 'warning');
419
+ return;
420
+ }
421
+
422
+ try {
423
+ const xmlContent = await file.text();
424
+ const model = await apiCall('/models/import', 'POST', { xml: xmlContent });
425
+
426
+ state.models.push(model);
427
+ await loadModel(model.id);
428
+ closeModal('import-modal');
429
+ document.getElementById('import-form').reset();
430
+ showNotification('Model imported successfully', 'success');
431
+ } catch (error) {
432
+ showNotification('Failed to import model: ' + error.message, 'error');
433
+ }
434
+ }
435
+
436
+ async function validateModel() {
437
+ if (!state.currentModel) {
438
+ showNotification('No model selected', 'warning');
439
+ return;
440
+ }
441
+
442
+ try {
443
+ const result = await apiCall(`/models/${state.currentModel.id}/validate`);
444
+
445
+ if (result.valid) {
446
+ showNotification('Model is valid!', 'success');
447
+ } else {
448
+ showNotification(`Validation failed: ${result.errors.join(', ')}`, 'error');
449
+ }
450
+ } catch (error) {
451
+ showNotification('Failed to validate model: ' + error.message, 'error');
452
+ }
453
+ }
454
+
455
+ // Visualization
456
+ async function visualizeGraph() {
457
+ if (!state.currentModel) return;
458
+
459
+ const format = document.getElementById('graph-format-select').value;
460
+
461
+ try {
462
+ const visualization = await apiCall(`/models/${state.currentModel.id}/visualize/graph?format=${format}`);
463
+ const container = document.getElementById('graph-visualization');
464
+
465
+ if (format === 'svg') {
466
+ container.innerHTML = visualization;
467
+ } else {
468
+ container.innerHTML = `<pre><code>${escapeHtml(visualization)}</code></pre>`;
469
+ }
470
+ } catch (error) {
471
+ showNotification('Failed to generate visualization: ' + error.message, 'error');
472
+ }
473
+ }
474
+
475
+ async function visualizeTree() {
476
+ if (!state.currentDecision) return;
477
+
478
+ try {
479
+ const svg = await apiCall(`/models/${state.currentModel.id}/decisions/${state.currentDecision.id}/visualize/tree`);
480
+ document.getElementById('tree-visualization').innerHTML = svg;
481
+ } catch (error) {
482
+ showNotification('Failed to generate tree visualization: ' + error.message, 'error');
483
+ }
484
+ }
485
+
486
+ function copyXml() {
487
+ const xmlContent = document.getElementById('xml-content').textContent;
488
+ navigator.clipboard.writeText(xmlContent);
489
+ showNotification('XML copied to clipboard', 'success');
490
+ }
491
+
492
+ function downloadXml() {
493
+ const xmlContent = document.getElementById('xml-content').textContent;
494
+ const blob = new Blob([xmlContent], { type: 'text/xml' });
495
+ const url = URL.createObjectURL(blob);
496
+ const a = document.createElement('a');
497
+ a.href = url;
498
+ a.download = `${state.currentModel.name}.dmn`;
499
+ a.click();
500
+ URL.revokeObjectURL(url);
501
+ showNotification('DMN file downloaded', 'success');
502
+ }
503
+
504
+ // UI Helpers
505
+ function switchTab(tabName) {
506
+ // Update tab buttons
507
+ document.querySelectorAll('.tab-btn').forEach(btn => {
508
+ btn.classList.toggle('active', btn.dataset.tab === tabName);
509
+ });
510
+
511
+ // Update tab panes
512
+ document.querySelectorAll('.tab-pane').forEach(pane => {
513
+ pane.classList.toggle('active', pane.id === `${tabName}-tab`);
514
+ });
515
+
516
+ // Load content based on tab
517
+ if (tabName === 'xml') {
518
+ exportModel();
519
+ } else if (tabName === 'graph') {
520
+ visualizeGraph();
521
+ }
522
+ }
523
+
524
+ function openModal(modalId) {
525
+ document.getElementById(modalId).classList.add('active');
526
+ }
527
+
528
+ function closeModal(modalId) {
529
+ document.getElementById(modalId).classList.remove('active');
530
+ }
531
+
532
+ function showNotification(message, type = 'info') {
533
+ const notification = document.getElementById('notification');
534
+ notification.textContent = message;
535
+ notification.className = `notification ${type} show`;
536
+
537
+ setTimeout(() => {
538
+ notification.classList.remove('show');
539
+ }, 3000);
540
+ }
541
+
542
+ function escapeHtml(text) {
543
+ const div = document.createElement('div');
544
+ div.textContent = text;
545
+ return div.innerHTML;
546
+ }
547
+
548
+ // Close modals when clicking outside
549
+ window.addEventListener('click', (e) => {
550
+ if (e.target.classList.contains('modal')) {
551
+ e.target.classList.remove('active');
552
+ }
553
+ });
@@ -11,6 +11,11 @@
11
11
  <header>
12
12
  <h1>๐ŸŽฏ DecisionAgent Rule Builder</h1>
13
13
  <p class="subtitle">Visual rule editor for non-technical users</p>
14
+ <div class="header-links">
15
+ <a href="/dmn/editor" class="btn btn-link">๐Ÿ“Š DMN Editor</a>
16
+ <a href="/simulation" class="btn btn-link">๐Ÿงช Simulation</a>
17
+ <a href="/testing/batch" class="btn btn-link">๐Ÿ“Š Batch Testing</a>
18
+ </div>
14
19
  </header>
15
20
 
16
21
  <div class="main-layout">
@@ -48,6 +53,7 @@
48
53
  <!-- Action Buttons -->
49
54
  <div class="actions">
50
55
  <button class="btn btn-success" id="validateBtn">โœ“ Validate Rules</button>
56
+ <button class="btn btn-info" id="testRuleBtn">๐Ÿงช Test Rule</button>
51
57
  <button class="btn btn-primary" id="saveVersionBtn">๐Ÿ’พ Save Version</button>
52
58
  <button class="btn btn-secondary" id="clearBtn">Clear All</button>
53
59
  </div>
@@ -330,6 +336,52 @@
330
336
  </div>
331
337
  </div>
332
338
  </div>
339
+
340
+ <!-- Test Rule Modal -->
341
+ <div id="testRuleModal" class="modal hidden">
342
+ <div class="modal-content modal-large">
343
+ <div class="modal-header">
344
+ <h2>๐Ÿงช Test Rule</h2>
345
+ <button class="close-btn" id="closeTestRuleBtn">&times;</button>
346
+ </div>
347
+ <div class="modal-body">
348
+ <div class="form-group">
349
+ <label for="testContext">Test Context (JSON):</label>
350
+ <textarea id="testContext" class="input" rows="8" placeholder='{"field1": "value1", "field2": 123}'></textarea>
351
+ <small class="form-hint">Enter context data as JSON to test your rules</small>
352
+ </div>
353
+ <div class="actions" style="margin-top: 1rem;">
354
+ <button class="btn btn-primary" id="runTestBtn">Run Test</button>
355
+ </div>
356
+ <div id="testResults" class="test-results hidden" style="margin-top: 2rem;">
357
+ <h3>Test Results</h3>
358
+ <div id="testDecision" class="test-result-item">
359
+ <strong>Decision:</strong> <span id="testDecisionValue">-</span>
360
+ </div>
361
+ <div id="testConfidence" class="test-result-item">
362
+ <strong>Confidence:</strong> <span id="testConfidenceValue">-</span>
363
+ </div>
364
+ <div id="testReason" class="test-result-item">
365
+ <strong>Reason:</strong> <span id="testReasonValue">-</span>
366
+ </div>
367
+ <div id="testExplainability" class="test-explainability" style="margin-top: 1.5rem;">
368
+ <h4>Explainability</h4>
369
+ <div id="testBecause" class="test-because">
370
+ <strong>Because (conditions that led to decision):</strong>
371
+ <ul id="testBecauseList"></ul>
372
+ </div>
373
+ <div id="testFailedConditions" class="test-failed" style="margin-top: 1rem;">
374
+ <strong>Failed Conditions:</strong>
375
+ <ul id="testFailedList"></ul>
376
+ </div>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ <div class="modal-footer">
381
+ <button class="btn btn-secondary" id="closeTestRuleModalBtn">Close</button>
382
+ </div>
383
+ </div>
384
+ </div>
333
385
  </div>
334
386
 
335
387
  <footer class="footer">