kumi 0.0.9 → 0.0.11
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/CHANGELOG.md +18 -0
- data/CLAUDE.md +18 -258
- data/README.md +188 -121
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +15 -2
- data/docs/features/hierarchical-broadcasting.md +349 -0
- data/docs/features/javascript-transpiler.md +148 -0
- data/docs/features/performance.md +1 -3
- data/docs/features/s-expression-printer.md +2 -2
- data/docs/schema_metadata.md +7 -7
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +34 -14
- data/lib/kumi/compiler.rb +4 -283
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
- data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -99
- data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
- data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +28 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +92 -48
- data/lib/kumi/core/analyzer/plans.rb +52 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
- data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
- data/lib/kumi/core/compiler/access_builder.rb +36 -0
- data/lib/kumi/core/compiler/access_planner.rb +219 -0
- data/lib/kumi/core/compiler/accessors/base.rb +69 -0
- data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
- data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
- data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
- data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
- data/lib/kumi/core/compiler_base.rb +137 -0
- data/lib/kumi/core/error_reporter.rb +6 -5
- data/lib/kumi/core/errors.rb +4 -0
- data/lib/kumi/core/explain.rb +157 -205
- data/lib/kumi/core/export/node_builders.rb +2 -2
- data/lib/kumi/core/export/node_serializers.rb +1 -1
- data/lib/kumi/core/function_registry/collection_functions.rb +100 -6
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -53
- data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +138 -98
- data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
- data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
- data/lib/kumi/core/ir/execution_engine.rb +50 -0
- data/lib/kumi/core/ir.rb +58 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +37 -16
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- data/lib/kumi/core/ruby_parser/parser.rb +1 -1
- data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
- data/lib/kumi/errors.rb +2 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +17 -22
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +15 -4
- data/lib/kumi/schema_metadata.rb +2 -2
- data/lib/kumi/support/ir_dump.rb +491 -0
- data/lib/kumi/support/s_expression_printer.rb +17 -16
- data/lib/kumi/syntax/array_expression.rb +6 -6
- data/lib/kumi/syntax/call_expression.rb +4 -4
- data/lib/kumi/syntax/cascade_expression.rb +4 -4
- data/lib/kumi/syntax/case_expression.rb +4 -4
- data/lib/kumi/syntax/declaration_reference.rb +4 -4
- data/lib/kumi/syntax/hash_expression.rb +4 -4
- data/lib/kumi/syntax/input_declaration.rb +6 -5
- data/lib/kumi/syntax/input_element_reference.rb +5 -5
- data/lib/kumi/syntax/input_reference.rb +5 -5
- data/lib/kumi/syntax/literal.rb +4 -4
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +33 -34
- data/lib/kumi/syntax/root.rb +6 -6
- data/lib/kumi/syntax/trait_declaration.rb +4 -4
- data/lib/kumi/syntax/value_declaration.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- data/scripts/analyze_broadcast_methods.rb +68 -0
- data/scripts/analyze_cascade_methods.rb +74 -0
- data/scripts/check_broadcasting_coverage.rb +51 -0
- data/scripts/find_dead_code.rb +114 -0
- metadata +36 -9
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/schema_instance.rb +0 -111
- data/lib/kumi/core/vectorization_metadata.rb +0 -110
- data/migrate_to_core_iterative.rb +0 -938
data/lib/kumi/cli.rb
DELETED
@@ -1,449 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "json"
|
4
|
-
require "yaml"
|
5
|
-
require "optparse"
|
6
|
-
require "irb"
|
7
|
-
|
8
|
-
module Kumi
|
9
|
-
module CLI
|
10
|
-
class Application
|
11
|
-
def initialize
|
12
|
-
@options = {
|
13
|
-
interactive: false,
|
14
|
-
schema_file: nil,
|
15
|
-
input_file: nil,
|
16
|
-
output_format: :pretty,
|
17
|
-
keys: [],
|
18
|
-
explain: false
|
19
|
-
}
|
20
|
-
end
|
21
|
-
|
22
|
-
def run(args = ARGV)
|
23
|
-
parse_options(args)
|
24
|
-
|
25
|
-
if @options[:interactive]
|
26
|
-
start_repl
|
27
|
-
elsif @options[:schema_file]
|
28
|
-
execute_schema_file
|
29
|
-
else
|
30
|
-
show_help_and_exit
|
31
|
-
end
|
32
|
-
rescue StandardError => e
|
33
|
-
puts "Error: #{e.message}"
|
34
|
-
exit 1
|
35
|
-
end
|
36
|
-
|
37
|
-
private
|
38
|
-
|
39
|
-
def parse_options(args)
|
40
|
-
parser = OptionParser.new do |opts|
|
41
|
-
opts.banner = "Usage: kumi [options]"
|
42
|
-
opts.separator ""
|
43
|
-
opts.separator "Options:"
|
44
|
-
|
45
|
-
opts.on("-i", "--interactive", "Start interactive REPL mode") do
|
46
|
-
@options[:interactive] = true
|
47
|
-
end
|
48
|
-
|
49
|
-
opts.on("-f", "--file FILE", "Load schema from Ruby file") do |file|
|
50
|
-
@options[:schema_file] = file
|
51
|
-
end
|
52
|
-
|
53
|
-
opts.on("-d", "--data FILE", "Load input data from JSON/YAML file") do |file|
|
54
|
-
@options[:input_file] = file
|
55
|
-
end
|
56
|
-
|
57
|
-
opts.on("-k", "--keys KEY1,KEY2", Array, "Extract specific keys (comma-separated)") do |keys|
|
58
|
-
@options[:keys] = keys.map(&:to_sym)
|
59
|
-
end
|
60
|
-
|
61
|
-
opts.on("-e", "--explain KEY", "Explain how a specific key is computed") do |key|
|
62
|
-
@options[:explain] = key.to_sym
|
63
|
-
end
|
64
|
-
|
65
|
-
opts.on("-o", "--format FORMAT", %i[pretty json yaml], "Output format: pretty, json, yaml") do |format|
|
66
|
-
@options[:output_format] = format
|
67
|
-
end
|
68
|
-
|
69
|
-
opts.on("-h", "--help", "Show this help message") do
|
70
|
-
puts opts
|
71
|
-
exit
|
72
|
-
end
|
73
|
-
end
|
74
|
-
|
75
|
-
parser.parse!(args)
|
76
|
-
end
|
77
|
-
|
78
|
-
def show_help_and_exit
|
79
|
-
puts <<~HELP
|
80
|
-
Kumi CLI - Declarative decision modeling for Ruby
|
81
|
-
|
82
|
-
Usage:
|
83
|
-
kumi -i # Start interactive mode
|
84
|
-
kumi -f schema.rb -d data.json # Execute schema with data
|
85
|
-
kumi -f schema.rb -k key1,key2 # Extract specific keys
|
86
|
-
kumi -f schema.rb -e key_name # Explain computation
|
87
|
-
|
88
|
-
Examples:
|
89
|
-
# Interactive mode for rapid testing
|
90
|
-
kumi -i
|
91
|
-
|
92
|
-
# Execute schema file with JSON data
|
93
|
-
kumi -f my_schema.rb -d input.json
|
94
|
-
|
95
|
-
# Get specific values in JSON format
|
96
|
-
kumi -f my_schema.rb -d input.yaml -k salary,bonus -o json
|
97
|
-
|
98
|
-
# Debug a specific computation
|
99
|
-
kumi -f my_schema.rb -d input.json -e total_compensation
|
100
|
-
|
101
|
-
For more information, see: https://github.com/amuta/kumi
|
102
|
-
HELP
|
103
|
-
exit
|
104
|
-
end
|
105
|
-
|
106
|
-
def start_repl
|
107
|
-
puts "🚀 Kumi Interactive REPL"
|
108
|
-
puts "Type 'help' for commands, 'exit' to quit"
|
109
|
-
puts
|
110
|
-
|
111
|
-
repl = InteractiveREPL.new
|
112
|
-
repl.start
|
113
|
-
end
|
114
|
-
|
115
|
-
def execute_schema_file
|
116
|
-
schema_module = load_schema_file(@options[:schema_file])
|
117
|
-
input_data = load_input_data(@options[:input_file])
|
118
|
-
|
119
|
-
runner = schema_module.from(input_data)
|
120
|
-
|
121
|
-
if @options[:explain]
|
122
|
-
result = schema_module.explain(input_data, @options[:explain])
|
123
|
-
puts result
|
124
|
-
elsif @options[:keys].any?
|
125
|
-
result = runner.slice(*@options[:keys])
|
126
|
-
output_result(result)
|
127
|
-
else
|
128
|
-
# Show available keys if no specific keys requested
|
129
|
-
puts "Schema loaded successfully!"
|
130
|
-
available_bindings = schema_module.__compiled_schema__.bindings.keys
|
131
|
-
puts "Available keys: #{available_bindings.join(', ')}"
|
132
|
-
puts "Use -k to extract specific keys or -e to explain computations"
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def load_schema_file(file_path)
|
137
|
-
raise "Schema file not found: #{file_path}" unless File.exist?(file_path)
|
138
|
-
|
139
|
-
# Load the file and extract the module
|
140
|
-
require_relative File.expand_path(file_path)
|
141
|
-
|
142
|
-
# Find the module name from the file
|
143
|
-
module_name = extract_module_name_from_file(file_path)
|
144
|
-
|
145
|
-
raise "Could not find module extending Kumi::Schema in #{file_path}" unless module_name
|
146
|
-
|
147
|
-
# Get the module constant
|
148
|
-
schema_module = Object.const_get(module_name)
|
149
|
-
|
150
|
-
raise "Module #{module_name} does not have a compiled schema" unless schema_module.__compiled_schema__
|
151
|
-
|
152
|
-
schema_module
|
153
|
-
end
|
154
|
-
|
155
|
-
def extract_module_name_from_file(file_path)
|
156
|
-
content = File.read(file_path)
|
157
|
-
|
158
|
-
# Look for "module ModuleName" pattern
|
159
|
-
if (match = content.match(/^\s*module\s+(\w+)/))
|
160
|
-
match[1]
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
def load_input_data(file_path)
|
165
|
-
return {} unless file_path
|
166
|
-
|
167
|
-
raise "Input file not found: #{file_path}" unless File.exist?(file_path)
|
168
|
-
|
169
|
-
case File.extname(file_path).downcase
|
170
|
-
when ".json"
|
171
|
-
JSON.parse(File.read(file_path), symbolize_names: true)
|
172
|
-
when ".yml", ".yaml"
|
173
|
-
YAML.safe_load_file(file_path, symbolize_names: true)
|
174
|
-
else
|
175
|
-
raise "Unsupported input file format. Use .json or .yaml"
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
def output_result(result)
|
180
|
-
case @options[:output_format]
|
181
|
-
when :json
|
182
|
-
puts JSON.pretty_generate(result)
|
183
|
-
when :yaml
|
184
|
-
puts result.to_yaml
|
185
|
-
else
|
186
|
-
output_pretty(result)
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
def output_pretty(result)
|
191
|
-
case result
|
192
|
-
when Hash
|
193
|
-
result.each do |key, value|
|
194
|
-
puts "#{key}: #{format_value(value)}"
|
195
|
-
end
|
196
|
-
when Kumi::Explain::Result
|
197
|
-
puts "Explanation for: #{result.key}"
|
198
|
-
puts "Value: #{format_value(result.value)}"
|
199
|
-
puts
|
200
|
-
puts "Computation trace:"
|
201
|
-
result.trace.each do |step|
|
202
|
-
puts " #{step[:operation]} -> #{format_value(step[:result])}"
|
203
|
-
end
|
204
|
-
else
|
205
|
-
puts format_value(result)
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
def format_value(value)
|
210
|
-
case value
|
211
|
-
when String
|
212
|
-
value.inspect
|
213
|
-
when Numeric
|
214
|
-
value.is_a?(Float) ? value.round(2) : value
|
215
|
-
when Array, Hash
|
216
|
-
value.inspect
|
217
|
-
else
|
218
|
-
value.to_s
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
|
223
|
-
class InteractiveREPL
|
224
|
-
def initialize
|
225
|
-
@schema_module = nil
|
226
|
-
@runner = nil
|
227
|
-
@input_data = {}
|
228
|
-
end
|
229
|
-
|
230
|
-
def start
|
231
|
-
loop do
|
232
|
-
print "kumi> "
|
233
|
-
input = gets&.chomp
|
234
|
-
break if input.nil? || input == "exit"
|
235
|
-
|
236
|
-
execute_command(input)
|
237
|
-
end
|
238
|
-
puts "Goodbye!"
|
239
|
-
end
|
240
|
-
|
241
|
-
private
|
242
|
-
|
243
|
-
def execute_command(input)
|
244
|
-
case input.strip
|
245
|
-
when "help"
|
246
|
-
show_help
|
247
|
-
when /^schema\s+(.+)/
|
248
|
-
load_schema_command(::Regexp.last_match(1))
|
249
|
-
when /^data\s+(.+)/
|
250
|
-
load_data_command(::Regexp.last_match(1))
|
251
|
-
when /^set\s+(\w+)\s+(.+)/
|
252
|
-
set_data_command(::Regexp.last_match(1), ::Regexp.last_match(2))
|
253
|
-
when /^get\s+(.+)/
|
254
|
-
get_value_command(::Regexp.last_match(1))
|
255
|
-
when /^explain\s+(.+)/
|
256
|
-
explain_command(::Regexp.last_match(1))
|
257
|
-
when /^slice\s+(.+)/
|
258
|
-
slice_command(::Regexp.last_match(1))
|
259
|
-
when "keys"
|
260
|
-
show_keys
|
261
|
-
when "clear"
|
262
|
-
clear_data
|
263
|
-
when ""
|
264
|
-
# ignore empty input
|
265
|
-
else
|
266
|
-
puts "Unknown command. Type 'help' for available commands."
|
267
|
-
end
|
268
|
-
rescue StandardError => e
|
269
|
-
puts "Error: #{e.message}"
|
270
|
-
puts e.backtrace.first if ENV["DEBUG"]
|
271
|
-
end
|
272
|
-
|
273
|
-
def show_help
|
274
|
-
puts <<~HELP
|
275
|
-
Available commands:
|
276
|
-
|
277
|
-
Schema management:
|
278
|
-
schema <file> Load schema from Ruby file
|
279
|
-
schema { ... } Define schema inline (experimental)
|
280
|
-
|
281
|
-
Data management:
|
282
|
-
data <file> Load input data from JSON/YAML file
|
283
|
-
set <key> <value> Set individual input value
|
284
|
-
clear Clear all input data
|
285
|
-
|
286
|
-
Evaluation:
|
287
|
-
get <key> Get computed value for key
|
288
|
-
explain <key> Show detailed computation trace
|
289
|
-
slice <key1,key2> Get multiple values
|
290
|
-
keys Show available keys
|
291
|
-
|
292
|
-
General:
|
293
|
-
help Show this help
|
294
|
-
exit Exit REPL
|
295
|
-
|
296
|
-
Examples:
|
297
|
-
schema examples/tax_2024.rb
|
298
|
-
data test_input.json
|
299
|
-
get total_tax
|
300
|
-
explain effective_rate
|
301
|
-
slice income,deductions,total_tax
|
302
|
-
HELP
|
303
|
-
end
|
304
|
-
|
305
|
-
def load_schema_command(file_path)
|
306
|
-
file_path = file_path.strip.gsub(/^["']|["']$/, "") # Remove quotes
|
307
|
-
|
308
|
-
unless File.exist?(file_path)
|
309
|
-
puts "Schema file not found: #{file_path}"
|
310
|
-
return
|
311
|
-
end
|
312
|
-
|
313
|
-
@schema_module = Module.new
|
314
|
-
@schema_module.extend(Kumi::Schema)
|
315
|
-
|
316
|
-
schema_content = File.read(file_path)
|
317
|
-
@schema_module.module_eval(schema_content, file_path)
|
318
|
-
|
319
|
-
puts "✅ Schema loaded from #{file_path}"
|
320
|
-
refresh_runner
|
321
|
-
rescue StandardError => e
|
322
|
-
puts "❌ Failed to load schema: #{e.message}"
|
323
|
-
end
|
324
|
-
|
325
|
-
def load_data_command(file_path)
|
326
|
-
file_path = file_path.strip.gsub(/^["']|["']$/, "") # Remove quotes
|
327
|
-
|
328
|
-
unless File.exist?(file_path)
|
329
|
-
puts "Data file not found: #{file_path}"
|
330
|
-
return
|
331
|
-
end
|
332
|
-
|
333
|
-
case File.extname(file_path).downcase
|
334
|
-
when ".json"
|
335
|
-
@input_data = JSON.parse(File.read(file_path), symbolize_names: true)
|
336
|
-
when ".yml", ".yaml"
|
337
|
-
@input_data = YAML.safe_load_file(file_path, symbolize_names: true)
|
338
|
-
else
|
339
|
-
puts "Unsupported file format. Use .json or .yaml"
|
340
|
-
return
|
341
|
-
end
|
342
|
-
|
343
|
-
puts "✅ Data loaded from #{file_path}"
|
344
|
-
puts "Keys: #{@input_data.keys.join(', ')}"
|
345
|
-
refresh_runner
|
346
|
-
rescue StandardError => e
|
347
|
-
puts "❌ Failed to load data: #{e.message}"
|
348
|
-
end
|
349
|
-
|
350
|
-
def set_data_command(key, value)
|
351
|
-
# Try to parse value as JSON first, then as literal
|
352
|
-
parsed_value = begin
|
353
|
-
JSON.parse(value)
|
354
|
-
rescue JSON::ParserError
|
355
|
-
# If not valid JSON, treat as string unless it looks like a number/boolean
|
356
|
-
case value
|
357
|
-
when /^\d+$/ then value.to_i
|
358
|
-
when /^\d+\.\d+$/ then value.to_f
|
359
|
-
when "true" then true
|
360
|
-
when "false" then false
|
361
|
-
else value
|
362
|
-
end
|
363
|
-
end
|
364
|
-
|
365
|
-
@input_data[key.to_sym] = parsed_value
|
366
|
-
puts "✅ Set #{key} = #{parsed_value.inspect}"
|
367
|
-
refresh_runner
|
368
|
-
end
|
369
|
-
|
370
|
-
def get_value_command(key)
|
371
|
-
ensure_runner_ready
|
372
|
-
|
373
|
-
key_sym = key.strip.to_sym
|
374
|
-
result = @runner[key_sym]
|
375
|
-
puts "#{key_sym}: #{format_value(result)}"
|
376
|
-
rescue StandardError => e
|
377
|
-
puts "❌ Error getting #{key}: #{e.message}"
|
378
|
-
end
|
379
|
-
|
380
|
-
def explain_command(key)
|
381
|
-
ensure_runner_ready
|
382
|
-
|
383
|
-
key_sym = key.strip.to_sym
|
384
|
-
puts @schema_module.explain(@input_data, key_sym)
|
385
|
-
rescue StandardError => e
|
386
|
-
puts "❌ Error explaining #{key}: #{e.message}"
|
387
|
-
end
|
388
|
-
|
389
|
-
def slice_command(keys_str)
|
390
|
-
ensure_runner_ready
|
391
|
-
|
392
|
-
keys = keys_str.split(",").map { |k| k.strip.to_sym }
|
393
|
-
result = @runner.slice(*keys)
|
394
|
-
|
395
|
-
result.each do |key, value|
|
396
|
-
puts "#{key}: #{format_value(value)}"
|
397
|
-
end
|
398
|
-
rescue StandardError => e
|
399
|
-
puts "❌ Error getting slice: #{e.message}"
|
400
|
-
end
|
401
|
-
|
402
|
-
def show_keys
|
403
|
-
if @schema_module
|
404
|
-
available_bindings = @schema_module.__compiled_schema__.bindings.keys
|
405
|
-
puts "Available keys: #{available_bindings.join(', ')}"
|
406
|
-
else
|
407
|
-
puts "No schema loaded. Use 'schema <file>' to load a schema."
|
408
|
-
end
|
409
|
-
end
|
410
|
-
|
411
|
-
def clear_data
|
412
|
-
@input_data = {}
|
413
|
-
@runner = nil
|
414
|
-
puts "✅ Input data cleared"
|
415
|
-
end
|
416
|
-
|
417
|
-
def ensure_runner_ready
|
418
|
-
raise "No schema loaded. Use 'schema <file>' to load a schema." unless @schema_module
|
419
|
-
|
420
|
-
return if @runner
|
421
|
-
|
422
|
-
raise "No runner available. Load data with 'data <file>' or set values with 'set <key> <value>'"
|
423
|
-
end
|
424
|
-
|
425
|
-
def refresh_runner
|
426
|
-
return unless @schema_module
|
427
|
-
|
428
|
-
@runner = @schema_module.from(@input_data)
|
429
|
-
puts "✅ Runner refreshed with current data"
|
430
|
-
rescue StandardError => e
|
431
|
-
puts "⚠️ Runner refresh failed: #{e.message}"
|
432
|
-
@runner = nil
|
433
|
-
end
|
434
|
-
|
435
|
-
def format_value(value)
|
436
|
-
case value
|
437
|
-
when String
|
438
|
-
value.inspect
|
439
|
-
when Numeric
|
440
|
-
value.is_a?(Float) ? value.round(2) : value
|
441
|
-
when Array, Hash
|
442
|
-
value.inspect
|
443
|
-
else
|
444
|
-
value.to_s
|
445
|
-
end
|
446
|
-
end
|
447
|
-
end
|
448
|
-
end
|
449
|
-
end
|
@@ -1,43 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Kumi
|
4
|
-
module Core
|
5
|
-
class CompiledSchema
|
6
|
-
attr_reader :bindings
|
7
|
-
|
8
|
-
def initialize(bindings)
|
9
|
-
@bindings = bindings.freeze
|
10
|
-
end
|
11
|
-
|
12
|
-
def evaluate(ctx, *key_names)
|
13
|
-
target_keys = key_names.empty? ? @bindings.keys : validate_keys(key_names)
|
14
|
-
|
15
|
-
target_keys.each_with_object({}) do |key, result|
|
16
|
-
result[key] = evaluate_binding(key, ctx)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def evaluate_binding(key, ctx)
|
21
|
-
memo = ctx.instance_variable_get(:@__schema_cache__)
|
22
|
-
return memo[key] if memo&.key?(key)
|
23
|
-
|
24
|
-
value = @bindings[key][1].call(ctx)
|
25
|
-
memo[key] = value if memo
|
26
|
-
value
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
def hash_like?(obj)
|
32
|
-
obj.respond_to?(:key?) && obj.respond_to?(:[])
|
33
|
-
end
|
34
|
-
|
35
|
-
def validate_keys(keys)
|
36
|
-
unknown_keys = keys - @bindings.keys
|
37
|
-
return keys if unknown_keys.empty?
|
38
|
-
|
39
|
-
raise Kumi::Errors::RuntimeError, "No binding named #{unknown_keys.first}"
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
@@ -1,40 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Kumi
|
4
|
-
module Core
|
5
|
-
EvaluationWrapper = Struct.new(:ctx) do
|
6
|
-
def initialize(ctx)
|
7
|
-
super
|
8
|
-
@__schema_cache__ = {} # memoization cache for bindings
|
9
|
-
end
|
10
|
-
|
11
|
-
def [](key)
|
12
|
-
ctx[key]
|
13
|
-
end
|
14
|
-
|
15
|
-
def []=(key, value)
|
16
|
-
ctx[key] = value
|
17
|
-
end
|
18
|
-
|
19
|
-
def keys
|
20
|
-
ctx.keys
|
21
|
-
end
|
22
|
-
|
23
|
-
def key?(key)
|
24
|
-
ctx.key?(key)
|
25
|
-
end
|
26
|
-
|
27
|
-
def clear
|
28
|
-
@__schema_cache__.clear
|
29
|
-
end
|
30
|
-
|
31
|
-
def clear_cache(*keys)
|
32
|
-
if keys.empty?
|
33
|
-
@__schema_cache__.clear
|
34
|
-
else
|
35
|
-
keys.each { |key| @__schema_cache__.delete(key) }
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
@@ -1,111 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Kumi
|
4
|
-
module Core
|
5
|
-
# A bound pair of <compiled schema + context>. Immutable.
|
6
|
-
#
|
7
|
-
# Public API ----------------------------------------------------------
|
8
|
-
# instance.evaluate # => full Hash of all bindings
|
9
|
-
# instance.evaluate(:tax_due, :rate)
|
10
|
-
# instance.slice(:tax_due) # alias for evaluate(*keys)
|
11
|
-
# instance.explain(:tax_due) # pretty trace string
|
12
|
-
# instance.input # original context (read‑only)
|
13
|
-
|
14
|
-
class SchemaInstance
|
15
|
-
attr_reader :compiled_schema, :metadata, :context
|
16
|
-
|
17
|
-
def initialize(compiled_schema, metadata, context)
|
18
|
-
@compiled_schema = compiled_schema # Kumi::Core::CompiledSchema
|
19
|
-
@metadata = metadata # Frozen state hash
|
20
|
-
@context = context.is_a?(EvaluationWrapper) ? context : EvaluationWrapper.new(context)
|
21
|
-
end
|
22
|
-
|
23
|
-
# Hash‑like read of one or many bindings
|
24
|
-
def evaluate(*key_names)
|
25
|
-
if key_names.empty?
|
26
|
-
@compiled_schema.evaluate(@context)
|
27
|
-
else
|
28
|
-
@compiled_schema.evaluate(@context, *key_names)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def slice(*key_names)
|
33
|
-
return {} if key_names.empty?
|
34
|
-
|
35
|
-
evaluate(*key_names)
|
36
|
-
end
|
37
|
-
|
38
|
-
def [](key_name)
|
39
|
-
evaluate(key_name)[key_name]
|
40
|
-
end
|
41
|
-
|
42
|
-
# Update input values and clear affected cached computations
|
43
|
-
def update(**changes)
|
44
|
-
changes.each do |field, value|
|
45
|
-
# Validate field exists
|
46
|
-
raise ArgumentError, "unknown input field: #{field}" unless input_field_exists?(field)
|
47
|
-
|
48
|
-
# Validate domain constraints
|
49
|
-
validate_domain_constraint(field, value)
|
50
|
-
|
51
|
-
# Update the input data
|
52
|
-
@context[field] = value
|
53
|
-
|
54
|
-
# Clear affected cached values using transitive closure by default
|
55
|
-
if ENV["KUMI_SIMPLE_CACHE"] == "true"
|
56
|
-
# Simple fallback: clear all cached values
|
57
|
-
@context.clear_cache
|
58
|
-
else
|
59
|
-
# Default: selective cache clearing using precomputed transitive closure
|
60
|
-
affected_keys = find_dependent_declarations_optimized(field)
|
61
|
-
affected_keys.each { |key| @context.clear_cache(key) }
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
self # Return self for chaining
|
66
|
-
end
|
67
|
-
|
68
|
-
private
|
69
|
-
|
70
|
-
def input_field_exists?(field)
|
71
|
-
# Check if field is declared in input block
|
72
|
-
input_meta = @metadata[:inputs] || {}
|
73
|
-
input_meta.key?(field) || @context.key?(field)
|
74
|
-
end
|
75
|
-
|
76
|
-
def validate_domain_constraint(field, value)
|
77
|
-
input_meta = @metadata[:inputs] || {}
|
78
|
-
field_meta = input_meta[field]
|
79
|
-
return unless field_meta&.dig(:domain)
|
80
|
-
|
81
|
-
domain = field_meta[:domain]
|
82
|
-
return unless violates_domain?(value, domain)
|
83
|
-
|
84
|
-
raise ArgumentError, "value #{value} is not in domain #{domain}"
|
85
|
-
end
|
86
|
-
|
87
|
-
def violates_domain?(value, domain)
|
88
|
-
case domain
|
89
|
-
when Range
|
90
|
-
!domain.include?(value)
|
91
|
-
when Array
|
92
|
-
!domain.include?(value)
|
93
|
-
when Proc
|
94
|
-
# For Proc domains, we can't statically analyze
|
95
|
-
false
|
96
|
-
else
|
97
|
-
false
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def find_dependent_declarations_optimized(field)
|
102
|
-
# Use precomputed transitive closure for true O(1) lookup!
|
103
|
-
transitive_dependents = @metadata[:dependents]
|
104
|
-
return [] unless transitive_dependents
|
105
|
-
|
106
|
-
# This is truly O(1) - just array lookup, no traversal needed
|
107
|
-
transitive_dependents[field] || []
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|