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,17 @@
1
+ # Simulation and What-If Analysis module
2
+ # Provides tools for scenario testing, historical replay, impact analysis, shadow testing, and Monte Carlo simulation
3
+
4
+ require_relative "simulation/errors"
5
+ require_relative "simulation/replay_engine"
6
+ require_relative "simulation/what_if_analyzer"
7
+ require_relative "simulation/impact_analyzer"
8
+ require_relative "simulation/shadow_test_engine"
9
+ require_relative "simulation/scenario_engine"
10
+ require_relative "simulation/scenario_library"
11
+ require_relative "simulation/monte_carlo_simulator"
12
+
13
+ module DecisionAgent
14
+ module Simulation
15
+ # Main entry point for simulation features
16
+ end
17
+ end
@@ -3,7 +3,7 @@ module DecisionAgent
3
3
  # MAJOR: Incremented for incompatible API changes
4
4
  # MINOR: Incremented for backward-compatible functionality additions
5
5
  # PATCH: Incremented for backward-compatible bug fixes
6
- VERSION = "0.2.0".freeze
6
+ VERSION = "1.0.1".freeze
7
7
 
8
8
  # Validate version format (semantic versioning)
9
9
  unless VERSION.match?(/\A\d+\.\d+\.\d+(-[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?(\+[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*)?\z/)
@@ -33,10 +33,10 @@ module DecisionAgent
33
33
  next_version_number = last_version ? last_version.version_number + 1 : 1
34
34
 
35
35
  # Deactivate previous active versions
36
- # Use update! instead of update_all to trigger validations
37
- rule_version_class.where(rule_id: rule_id, status: "active").find_each do |v|
38
- v.update!(status: "archived")
39
- end
36
+ # Use update_all for better concurrency (avoids SQLite locking issues)
37
+ # Status "archived" is valid, so no need to trigger validations
38
+ rule_version_class.where(rule_id: rule_id, status: "active")
39
+ .update_all(status: "archived")
40
40
 
41
41
  # Create new version
42
42
  version = rule_version_class.create!(
@@ -87,12 +87,11 @@ module DecisionAgent
87
87
 
88
88
  # Deactivate all other versions for this rule within the same transaction
89
89
  # The lock ensures only one thread can perform this operation at a time
90
- # Use update! instead of update_all to trigger validations
90
+ # Use update_all for better concurrency (avoids SQLite locking issues)
91
+ # Status "archived" is valid, so no need to trigger validations
91
92
  rule_version_class.where(rule_id: version.rule_id, status: "active")
92
93
  .where.not(id: version_id)
93
- .find_each do |v|
94
- v.update!(status: "archived")
95
- end
94
+ .update_all(status: "archived")
96
95
 
97
96
  # Activate this version
98
97
  version.update!(status: "active")
@@ -101,6 +100,22 @@ module DecisionAgent
101
100
  serialize_version(version)
102
101
  end
103
102
 
103
+ def delete_version(version_id:)
104
+ version = rule_version_class.find_by(id: version_id)
105
+
106
+ # Version not found
107
+ raise DecisionAgent::NotFoundError, "Version not found: #{version_id}" unless version
108
+
109
+ # Prevent deletion of active versions
110
+ raise DecisionAgent::ValidationError, "Cannot delete active version. Please activate another version first." if version.status == "active"
111
+
112
+ # Delete the version
113
+ version.destroy
114
+ true
115
+ rescue ActiveRecord::RecordNotFound
116
+ raise DecisionAgent::NotFoundError, "Version not found: #{version_id}"
117
+ end
118
+
104
119
  private
105
120
 
106
121
  def rule_version_class
@@ -0,0 +1,426 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../dmn/parser"
5
+ require_relative "../dmn/exporter"
6
+ require_relative "../dmn/importer"
7
+ require_relative "../dmn/validator"
8
+ require_relative "../dmn/model"
9
+ require_relative "../dmn/decision_tree"
10
+ require_relative "../dmn/decision_graph"
11
+ require_relative "../dmn/visualizer"
12
+
13
+ module DecisionAgent
14
+ module Web
15
+ # DMN Editor Backend
16
+ # Provides API endpoints for visual DMN modeling
17
+ class DmnEditor
18
+ attr_reader :storage
19
+
20
+ def initialize(storage: nil)
21
+ @storage = storage || {}
22
+ @storage_mutex = Mutex.new
23
+ end
24
+
25
+ # Create a new DMN model
26
+ def create_model(name:, namespace: nil)
27
+ model_id = generate_id
28
+ namespace ||= "http://decisonagent.com/dmn/#{model_id}"
29
+
30
+ model = Dmn::Model.new(
31
+ id: model_id,
32
+ name: name,
33
+ namespace: namespace
34
+ )
35
+
36
+ store_model(model_id, model)
37
+
38
+ {
39
+ id: model_id,
40
+ name: name,
41
+ namespace: namespace,
42
+ decisions: [],
43
+ created_at: Time.now.utc.iso8601
44
+ }
45
+ end
46
+
47
+ # Get a DMN model
48
+ def get_model(model_id)
49
+ model = retrieve_model(model_id)
50
+ return nil unless model
51
+
52
+ serialize_model(model)
53
+ end
54
+
55
+ # Update DMN model metadata
56
+ def update_model(model_id, name: nil, namespace: nil)
57
+ model = retrieve_model(model_id)
58
+ return nil unless model
59
+
60
+ model.instance_variable_set(:@name, name) if name
61
+ model.instance_variable_set(:@namespace, namespace) if namespace
62
+
63
+ store_model(model_id, model)
64
+ serialize_model(model)
65
+ end
66
+
67
+ # Delete a DMN model
68
+ # rubocop:disable Naming/PredicateMethod
69
+ def delete_model(model_id)
70
+ @storage_mutex.synchronize do
71
+ @storage.delete(model_id)
72
+ end
73
+ true
74
+ end
75
+ # rubocop:enable Naming/PredicateMethod
76
+
77
+ # Add a decision to a model
78
+ def add_decision(model_id:, decision_id:, name:, type: "decision_table")
79
+ model = retrieve_model(model_id)
80
+ return nil unless model
81
+
82
+ decision = Dmn::Decision.new(
83
+ id: decision_id,
84
+ name: name
85
+ )
86
+
87
+ # Initialize decision logic based on type
88
+ case type
89
+ when "decision_table"
90
+ decision.instance_variable_set(:@decision_table, Dmn::DecisionTable.new(
91
+ id: "#{decision_id}_table",
92
+ hit_policy: "FIRST"
93
+ ))
94
+ when "decision_tree"
95
+ decision.instance_variable_set(:@decision_tree, Dmn::DecisionTree.new(
96
+ id: "#{decision_id}_tree",
97
+ name: name
98
+ ))
99
+ when "literal"
100
+ decision.instance_variable_set(:@literal_expression, "")
101
+ end
102
+
103
+ model.add_decision(decision)
104
+ store_model(model_id, model)
105
+
106
+ serialize_decision(decision)
107
+ end
108
+
109
+ # Update a decision
110
+ def update_decision(model_id:, decision_id:, name: nil, logic: nil)
111
+ model = retrieve_model(model_id)
112
+ return nil unless model
113
+
114
+ decision = model.find_decision(decision_id)
115
+ return nil unless decision
116
+
117
+ decision.instance_variable_set(:@name, name) if name
118
+
119
+ update_decision_table(decision.decision_table, logic) if logic && decision.decision_table
120
+
121
+ store_model(model_id, model)
122
+ serialize_decision(decision)
123
+ end
124
+
125
+ # Delete a decision
126
+ # rubocop:disable Naming/PredicateMethod
127
+ def delete_decision(model_id:, decision_id:)
128
+ model = retrieve_model(model_id)
129
+ return false unless model
130
+
131
+ model.decisions.reject! { |d| d.id == decision_id }
132
+ store_model(model_id, model)
133
+ true
134
+ end
135
+ # rubocop:enable Naming/PredicateMethod
136
+
137
+ # Add input to decision table
138
+ def add_input(model_id:, decision_id:, input_id:, label:, type_ref: nil, expression: nil)
139
+ model = retrieve_model(model_id)
140
+ return nil unless model
141
+
142
+ decision = model.find_decision(decision_id)
143
+ return nil unless decision&.decision_table
144
+
145
+ input = Dmn::Input.new(
146
+ id: input_id,
147
+ label: label,
148
+ type_ref: type_ref,
149
+ expression: expression
150
+ )
151
+
152
+ decision.decision_table.inputs << input
153
+ store_model(model_id, model)
154
+
155
+ serialize_input(input)
156
+ end
157
+
158
+ # Add output to decision table
159
+ def add_output(model_id:, decision_id:, output_id:, label:, type_ref: nil, name: nil)
160
+ model = retrieve_model(model_id)
161
+ return nil unless model
162
+
163
+ decision = model.find_decision(decision_id)
164
+ return nil unless decision&.decision_table
165
+
166
+ output = Dmn::Output.new(
167
+ id: output_id,
168
+ label: label,
169
+ type_ref: type_ref,
170
+ name: name
171
+ )
172
+
173
+ decision.decision_table.outputs << output
174
+ store_model(model_id, model)
175
+
176
+ serialize_output(output)
177
+ end
178
+
179
+ # Add rule to decision table
180
+ def add_rule(model_id:, decision_id:, rule_id:, input_entries:, output_entries:, description: nil)
181
+ model = retrieve_model(model_id)
182
+ return nil unless model
183
+
184
+ decision = model.find_decision(decision_id)
185
+ return nil unless decision&.decision_table
186
+
187
+ rule = Dmn::Rule.new(id: rule_id)
188
+ rule.instance_variable_set(:@input_entries, input_entries)
189
+ rule.instance_variable_set(:@output_entries, output_entries)
190
+ rule.instance_variable_set(:@description, description) if description
191
+
192
+ decision.decision_table.rules << rule
193
+ store_model(model_id, model)
194
+
195
+ serialize_rule(rule)
196
+ end
197
+
198
+ # Update rule
199
+ def update_rule(model_id:, decision_id:, rule_id:, input_entries: nil, output_entries: nil, description: nil)
200
+ model = retrieve_model(model_id)
201
+ return nil unless model
202
+
203
+ decision = model.find_decision(decision_id)
204
+ return nil unless decision&.decision_table
205
+
206
+ rule = decision.decision_table.rules.find { |r| r.id == rule_id }
207
+ return nil unless rule
208
+
209
+ rule.instance_variable_set(:@input_entries, input_entries) if input_entries
210
+ rule.instance_variable_set(:@output_entries, output_entries) if output_entries
211
+ rule.instance_variable_set(:@description, description) if description
212
+
213
+ store_model(model_id, model)
214
+ serialize_rule(rule)
215
+ end
216
+
217
+ # Delete rule
218
+ # rubocop:disable Naming/PredicateMethod
219
+ def delete_rule(model_id:, decision_id:, rule_id:)
220
+ model = retrieve_model(model_id)
221
+ return false unless model
222
+
223
+ decision = model.find_decision(decision_id)
224
+ return false unless decision&.decision_table
225
+
226
+ decision.decision_table.rules.reject! { |r| r.id == rule_id }
227
+ store_model(model_id, model)
228
+ true
229
+ end
230
+ # rubocop:enable Naming/PredicateMethod
231
+
232
+ # Validate a DMN model
233
+ def validate_model(model_id)
234
+ model = retrieve_model(model_id)
235
+ return { valid: false, errors: ["Model not found"] } unless model
236
+
237
+ validator = Dmn::Validator.new
238
+ validator.validate(model)
239
+
240
+ {
241
+ valid: validator.valid?,
242
+ errors: validator.errors,
243
+ warnings: validator.warnings
244
+ }
245
+ end
246
+
247
+ # Export DMN model to XML
248
+ def export_to_xml(model_id)
249
+ model = retrieve_model(model_id)
250
+ return nil unless model
251
+
252
+ exporter = Dmn::Exporter.new
253
+ exporter.export(model)
254
+ end
255
+
256
+ # Import DMN model from XML
257
+ def import_from_xml(xml_content, name: nil)
258
+ parser = Dmn::Parser.new
259
+ model = parser.parse(xml_content)
260
+
261
+ # Generate new ID for imported model
262
+ model_id = generate_id
263
+ model.instance_variable_set(:@id, model_id)
264
+ model.instance_variable_set(:@name, name) if name
265
+
266
+ store_model(model_id, model)
267
+
268
+ serialize_model(model)
269
+ end
270
+
271
+ # Generate visualization for decision tree
272
+ def visualize_tree(model_id:, decision_id:, format: "svg")
273
+ model = retrieve_model(model_id)
274
+ return nil unless model
275
+
276
+ decision = model.find_decision(decision_id)
277
+ return nil unless decision || !decision.decision_tree
278
+
279
+ case format.to_s.downcase
280
+ when "svg"
281
+ Dmn::Visualizer.tree_to_svg(decision.decision_tree)
282
+ when "dot"
283
+ Dmn::Visualizer.tree_to_dot(decision.decision_tree)
284
+ when "mermaid"
285
+ Dmn::Visualizer.tree_to_mermaid(decision.decision_tree)
286
+ end
287
+ end
288
+
289
+ # Generate visualization for decision graph
290
+ def visualize_graph(model_id:, format: "svg")
291
+ model = retrieve_model(model_id)
292
+ return nil unless model
293
+
294
+ # Convert model to decision graph
295
+ graph = Dmn::DecisionGraph.new(id: model.id, name: model.name)
296
+ model.decisions.each do |decision|
297
+ node = Dmn::DecisionNode.new(
298
+ id: decision.id,
299
+ name: decision.name,
300
+ decision_logic: decision.decision_table || decision.decision_tree
301
+ )
302
+
303
+ # Add dependencies from information requirements
304
+ decision.information_requirements.each do |req|
305
+ node.add_dependency(req[:decision_id], req[:variable_name])
306
+ end
307
+
308
+ graph.add_decision(node)
309
+ end
310
+
311
+ case format.to_s.downcase
312
+ when "svg"
313
+ Dmn::Visualizer.graph_to_svg(graph)
314
+ when "dot"
315
+ Dmn::Visualizer.graph_to_dot(graph)
316
+ when "mermaid"
317
+ Dmn::Visualizer.graph_to_mermaid(graph)
318
+ end
319
+ end
320
+
321
+ # List all models
322
+ def list_models
323
+ @storage_mutex.synchronize do
324
+ @storage.map do |id, model|
325
+ {
326
+ id: id,
327
+ name: model.name,
328
+ namespace: model.namespace,
329
+ decision_count: model.decisions.size
330
+ }
331
+ end
332
+ end
333
+ end
334
+
335
+ private
336
+
337
+ def generate_id
338
+ "dmn_#{Time.now.to_i}_#{rand(10_000)}"
339
+ end
340
+
341
+ def store_model(model_id, model)
342
+ @storage_mutex.synchronize do
343
+ @storage[model_id] = model
344
+ end
345
+ end
346
+
347
+ def retrieve_model(model_id)
348
+ @storage_mutex.synchronize do
349
+ @storage[model_id]
350
+ end
351
+ end
352
+
353
+ def update_decision_table(table, logic)
354
+ table.instance_variable_set(:@hit_policy, logic[:hit_policy]) if logic[:hit_policy]
355
+ table.instance_variable_set(:@inputs, logic[:inputs]) if logic[:inputs]
356
+ table.instance_variable_set(:@outputs, logic[:outputs]) if logic[:outputs]
357
+ table.instance_variable_set(:@rules, logic[:rules]) if logic[:rules]
358
+ end
359
+
360
+ def serialize_model(model)
361
+ {
362
+ id: model.id,
363
+ name: model.name,
364
+ namespace: model.namespace,
365
+ decisions: model.decisions.map { |d| serialize_decision(d) }
366
+ }
367
+ end
368
+
369
+ def serialize_decision(decision)
370
+ result = {
371
+ id: decision.id,
372
+ name: decision.name
373
+ }
374
+
375
+ if decision.decision_table
376
+ result[:decision_table] = serialize_decision_table(decision.decision_table)
377
+ elsif decision.decision_tree
378
+ result[:decision_tree] = decision.decision_tree.to_h
379
+ elsif decision.instance_variable_get(:@literal_expression)
380
+ result[:literal_expression] = decision.instance_variable_get(:@literal_expression)
381
+ end
382
+
383
+ result[:information_requirements] = decision.information_requirements if decision.information_requirements.any?
384
+
385
+ result
386
+ end
387
+
388
+ def serialize_decision_table(table)
389
+ {
390
+ id: table.id,
391
+ hit_policy: table.hit_policy,
392
+ inputs: table.inputs.map { |i| serialize_input(i) },
393
+ outputs: table.outputs.map { |o| serialize_output(o) },
394
+ rules: table.rules.map { |r| serialize_rule(r) }
395
+ }
396
+ end
397
+
398
+ def serialize_input(input)
399
+ {
400
+ id: input.id,
401
+ label: input.label,
402
+ type_ref: input.type_ref,
403
+ expression: input.expression
404
+ }
405
+ end
406
+
407
+ def serialize_output(output)
408
+ {
409
+ id: output.id,
410
+ label: output.label,
411
+ type_ref: output.type_ref,
412
+ name: output.name
413
+ }
414
+ end
415
+
416
+ def serialize_rule(rule)
417
+ {
418
+ id: rule.id,
419
+ input_entries: rule.input_entries,
420
+ output_entries: rule.output_entries,
421
+ description: rule.description
422
+ }
423
+ end
424
+ end
425
+ end
426
+ end
@@ -84,6 +84,7 @@ class RuleBuilder {
84
84
 
85
85
  // Actions
86
86
  document.getElementById('validateBtn').addEventListener('click', () => this.validateRules());
87
+ document.getElementById('testRuleBtn').addEventListener('click', () => this.openTestRuleModal());
87
88
  document.getElementById('clearBtn').addEventListener('click', () => this.clearAll());
88
89
  document.getElementById('loadExampleBtn').addEventListener('click', () => this.loadExample());
89
90
 
@@ -101,6 +102,11 @@ class RuleBuilder {
101
102
  document.getElementById('closeCompareBtn').addEventListener('click', () => this.closeCompareModal());
102
103
  document.getElementById('closeCompareModalBtn').addEventListener('click', () => this.closeCompareModal());
103
104
 
105
+ // Test Rule
106
+ document.getElementById('runTestBtn').addEventListener('click', () => this.runTest());
107
+ document.getElementById('closeTestRuleBtn').addEventListener('click', () => this.closeTestRuleModal());
108
+ document.getElementById('closeTestRuleModalBtn').addEventListener('click', () => this.closeTestRuleModal());
109
+
104
110
  // Modal close on outside click
105
111
  document.getElementById('ruleModal').addEventListener('click', (e) => {
106
112
  if (e.target.id === 'ruleModal') {
@@ -1134,6 +1140,119 @@ class RuleBuilder {
1134
1140
  document.getElementById('compareVersionsModal').classList.add('hidden');
1135
1141
  }
1136
1142
 
1143
+ getRulesJSON() {
1144
+ const version = document.getElementById('rulesetVersion').value || '1.0';
1145
+ const ruleset = document.getElementById('rulesetName').value || 'my_ruleset';
1146
+ const rules = this.rules.map(rule => ({
1147
+ id: rule.id,
1148
+ if: rule.if,
1149
+ then: rule.then
1150
+ }));
1151
+
1152
+ return {
1153
+ version: version,
1154
+ ruleset: ruleset,
1155
+ rules: rules
1156
+ };
1157
+ }
1158
+
1159
+ openTestRuleModal() {
1160
+ document.getElementById('testRuleModal').classList.remove('hidden');
1161
+ document.getElementById('testContext').value = '{}';
1162
+ document.getElementById('testResults').classList.add('hidden');
1163
+ }
1164
+
1165
+ closeTestRuleModal() {
1166
+ document.getElementById('testRuleModal').classList.add('hidden');
1167
+ }
1168
+
1169
+ async runTest() {
1170
+ const contextText = document.getElementById('testContext').value.trim();
1171
+
1172
+ // Get current rules
1173
+ const rules = this.getRulesJSON();
1174
+
1175
+ if (!rules || !rules.rules || rules.rules.length === 0) {
1176
+ alert('Please add at least one rule before testing');
1177
+ return;
1178
+ }
1179
+
1180
+ let context;
1181
+ try {
1182
+ context = contextText ? JSON.parse(contextText) : {};
1183
+ } catch (e) {
1184
+ alert('Invalid JSON in context field: ' + e.message);
1185
+ return;
1186
+ }
1187
+
1188
+ try {
1189
+ const response = await fetch(`${this.basePath}api/evaluate`, {
1190
+ method: 'POST',
1191
+ headers: this.getAuthHeaders(),
1192
+ body: JSON.stringify({
1193
+ rules: rules,
1194
+ context: context
1195
+ })
1196
+ });
1197
+
1198
+ const data = await response.json();
1199
+
1200
+ if (!response.ok || !data.success) {
1201
+ alert('Test failed: ' + (data.error || 'Unknown error'));
1202
+ return;
1203
+ }
1204
+
1205
+ // Display results
1206
+ const resultsDiv = document.getElementById('testResults');
1207
+ resultsDiv.classList.remove('hidden');
1208
+
1209
+ if (data.decision) {
1210
+ document.getElementById('testDecisionValue').textContent = data.decision;
1211
+ document.getElementById('testConfidenceValue').textContent = (data.confidence || 0).toFixed(3);
1212
+ document.getElementById('testReasonValue').textContent = data.reason || 'N/A';
1213
+
1214
+ // Display explainability
1215
+ const becauseList = document.getElementById('testBecauseList');
1216
+ const failedList = document.getElementById('testFailedList');
1217
+
1218
+ if (data.because && data.because.length > 0) {
1219
+ becauseList.innerHTML = '';
1220
+ data.because.forEach(condition => {
1221
+ const li = document.createElement('li');
1222
+ li.textContent = condition;
1223
+ li.style.color = '#28a745';
1224
+ becauseList.appendChild(li);
1225
+ });
1226
+ document.getElementById('testBecause').style.display = 'block';
1227
+ } else {
1228
+ document.getElementById('testBecause').style.display = 'none';
1229
+ }
1230
+
1231
+ if (data.failed_conditions && data.failed_conditions.length > 0) {
1232
+ failedList.innerHTML = '';
1233
+ data.failed_conditions.forEach(condition => {
1234
+ const li = document.createElement('li');
1235
+ li.textContent = condition;
1236
+ li.style.color = '#dc3545';
1237
+ failedList.appendChild(li);
1238
+ });
1239
+ document.getElementById('testFailedConditions').style.display = 'block';
1240
+ } else {
1241
+ document.getElementById('testFailedConditions').style.display = 'none';
1242
+ }
1243
+
1244
+ document.getElementById('testExplainability').style.display = 'block';
1245
+ } else {
1246
+ document.getElementById('testDecisionValue').textContent = 'No match';
1247
+ document.getElementById('testConfidenceValue').textContent = 'N/A';
1248
+ document.getElementById('testReasonValue').textContent = data.message || 'No rules matched';
1249
+ document.getElementById('testExplainability').style.display = 'none';
1250
+ }
1251
+ } catch (error) {
1252
+ alert('Error running test: ' + error.message);
1253
+ }
1254
+ }
1255
+
1137
1256
  async deleteVersion(versionId) {
1138
1257
  if (!confirm('Are you sure you want to delete this version? This action cannot be undone.')) {
1139
1258
  return;