kumi 0.0.11 → 0.0.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3eb46e14716bf14c3d9165ffac957211f41ad5a21a74ad37c47b37c37e01b312
4
- data.tar.gz: fd0e36d65ac41079c27cf9699a3e092ff762bc76b3af8fc90df2ec763fed3806
3
+ metadata.gz: 7af4bdb11ef5da5a25b799cef8387752636a8224634c70656efc203b9d923185
4
+ data.tar.gz: 4e604513e42dcb8754fab54ead4140d5ca2d8443ae4d0461e62dc3b48bb925d4
5
5
  SHA512:
6
- metadata.gz: 54ed5e72d6acf863e0f7ed0986c8db83fab22526bcffb97b4c1b96343e724b0ae505804baed5554933c452478ced8c46ec24a6568426813e69c4007994588de6
7
- data.tar.gz: '09aabb772643aab71d957060c934f05be5838a1056b7c635822b75759106a3119844cc7e20f96b8990ec658d969f2d2e0143ceb43d36f8d48da061c2bb80cb6a'
6
+ metadata.gz: dcbd93268081ff8f0bcf112101603b180932196b1d6fcb59aae2f363d6bbfe4e9ea07cf3cbe811290222dcd15cdf132ea04f409bea34f81599412f729211a5fa
7
+ data.tar.gz: 3e0c2e91609e742c2852457b2dd61a114a03b988ae983e0cfb23c9bbf46bc5c6c1714636961ed46b4b9ae924c6a77a79dc0aab9013cadbb73e49d5374f8b8dbc
data/.rubocop.yml CHANGED
@@ -4,7 +4,7 @@ plugins:
4
4
 
5
5
  AllCops:
6
6
  NewCops: enable
7
- TargetRubyVersion: 3.0
7
+ TargetRubyVersion: 3.1
8
8
  SuggestExtensions: false
9
9
  Exclude:
10
10
  - 'bin/*'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.0.13] – 2025-08-14
4
+ ### Added
5
+ - Runtime performance optimizations for interpreter execution
6
+ - Input load deduplication to cache loaded input values and avoid redundant operations
7
+ - Constant folding optimization to evaluate literal expressions during compilation
8
+ - Accessor memoization with proper cache isolation per input context
9
+ - Selective cache invalidation for incremental updates
10
+
11
+ ### Fixed
12
+ - Cache isolation between different input contexts preventing cross-context pollution
13
+ - Cascade mutual exclusion tests now pass correctly with proper trait evaluation
14
+ - Incremental update performance with targeted cache clearing
15
+
16
+ ## [0.0.12] – 2025-08-14
17
+ ### Added
18
+ - Hash objects input declarations with `hash :field do ... end` syntax
19
+ - Complete hash object integration with arrays, nesting, and broadcasting
20
+
1
21
  ## [0.0.11] – 2025-08-13
2
22
  ### Added
3
23
  - Intermediate Representation (IR) and slot-based VM interpreter.
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![CI](https://github.com/amuta/kumi/workflows/CI/badge.svg)](https://github.com/amuta/kumi/actions)
4
4
  [![Gem Version](https://badge.fury.io/rb/kumi.svg)](https://badge.fury.io/rb/kumi)
5
5
 
6
- Kumi is a Declarative logic and rules engine framework with static analysis for Ruby.
6
+ **Kumi** is a declarative rules-and-calculation DSL for Ruby. It compiles business logic into a **typed, analyzable dependency graph** with **vector semantics** over nested data, performs **static checks** at definition time, **lowers to a compact IR**, and executes **deterministically**.
7
7
 
8
8
  It handles complex, interdependent calculations with validation and consistency checking.
9
9
 
@@ -436,8 +436,8 @@ runner[:after_tax] # => 196,844.80 (cached)
436
436
  - Sequential procedural workflows
437
437
  - High-frequency processing
438
438
 
439
- ## JavaScript Transpiler
440
-
439
+ ## JavaScript Transpiler (V 0.0.10)
440
+ Note: On the current 0.0.11 this is disabled but will be back in later versions. reason: IR/Interpreter update.
441
441
  Transpiles compiled schemas to standalone JavaScript code. See [docs/features/javascript-transpiler.md](docs/features/javascript-transpiler.md) for details.
442
442
 
443
443
  ```ruby
@@ -465,7 +465,7 @@ See [docs/features/performance.md](docs/features/performance.md) for detailed be
465
465
 
466
466
  ## What Kumi does not guarantee
467
467
 
468
- Lambdas (e.g. -> inside the schema), external IO, floating-point vs bignum, JS transpiler edge cases, time-zone math differences, etc.
468
+ Lambdas (e.g. -> inside the schema), external IO, floating-point vs bignum, time-zone math differences, etc.
469
469
 
470
470
  ## Learn More
471
471
 
data/docs/SYNTAX.md CHANGED
@@ -214,12 +214,14 @@ Array broadcasting enables element-wise operations on array fields with automati
214
214
 
215
215
  ```ruby
216
216
  input do
217
+ # Structured array with defined fields
217
218
  array :line_items do
218
219
  float :price
219
220
  integer :quantity
220
221
  string :category
221
222
  end
222
223
 
224
+ # Nested arrays with hash objects
223
225
  array :orders do
224
226
  array :items do
225
227
  hash :product do
@@ -229,6 +231,15 @@ input do
229
231
  integer :quantity
230
232
  end
231
233
  end
234
+
235
+ # Dynamic arrays with flexible element types
236
+ array :api_responses do
237
+ element :any, :response_data # For dynamic/unknown hash structures
238
+ end
239
+
240
+ array :measurements do
241
+ element :float, :value # For simple scalar arrays
242
+ end
232
243
  end
233
244
  ```
234
245
 
@@ -290,6 +301,61 @@ value :avg_line_total, fn(:avg, line_totals)
290
301
  trait :has_expensive, fn(:any?, expensive_items)
291
302
  ```
292
303
 
304
+ ### Dynamic Hash Elements with `element :any`
305
+
306
+ For arrays containing hash data with unknown or flexible structure, use `element :any` instead of defining explicit hash objects:
307
+
308
+ ```ruby
309
+ input do
310
+ array :api_responses do
311
+ element :any, :response_data
312
+ end
313
+
314
+ array :user_profiles do
315
+ element :any, :profile_info
316
+ end
317
+ end
318
+
319
+ # Access hash data using fn(:fetch)
320
+ value :response_codes, fn(:fetch, input.api_responses.response_data, "status")
321
+ value :user_names, fn(:fetch, input.user_profiles.profile_info, "name")
322
+ value :user_ages, fn(:fetch, input.user_profiles.profile_info, "age")
323
+
324
+ # Mathematical operations on extracted values
325
+ value :avg_response_time, fn(:mean, fn(:fetch, input.api_responses.response_data, "response_time"))
326
+ value :total_users, fn(:size, input.user_profiles.profile_info)
327
+
328
+ # Traits using dynamic data
329
+ trait :success_responses, fn(:any?, fn(:fetch, input.api_responses.response_data, "status") == 200)
330
+ trait :adult_users, fn(:any?, fn(:fetch, input.user_profiles.profile_info, "age") >= 18)
331
+ ```
332
+
333
+ **Use Cases for `element :any`:**
334
+ - API responses with varying schemas
335
+ - Configuration data with flexible structure
336
+ - Dynamic hash structures (unknown keys at schema definition time)
337
+ - Legacy data where hash structure may vary
338
+ - When you need maximum flexibility without type constraints
339
+
340
+ **Comparison: `element :any` vs Hash Objects**
341
+
342
+ ```ruby
343
+ # element :any approach (flexible, dynamic)
344
+ array :users do
345
+ element :any, :data
346
+ end
347
+ value :names, fn(:fetch, input.users.data, "name")
348
+
349
+ # hash object approach (typed, structured)
350
+ array :users do
351
+ hash :data do
352
+ string :name
353
+ integer :age
354
+ end
355
+ end
356
+ value :names, input.users.data.name
357
+ ```
358
+
293
359
  ### Broadcasting Type Inference
294
360
 
295
361
  The type system automatically infers appropriate types:
@@ -67,6 +67,72 @@ value :cell_data, input.cube.layer.row.cell # 1D values
67
67
  - **Ranked polymorphism**: Same operations work across different dimensional arrays
68
68
  - **Clean code**: `fn(:size, input.cube.layer.row)` instead of `fn(:size, fn(:flatten_one, input.cube.layer))`
69
69
 
70
+ ### Dynamic Hash Elements with `element :any`
71
+
72
+ For arrays containing hash data with flexible or unknown structure, use `element :any` to access dynamic hash content:
73
+
74
+ ```ruby
75
+ input do
76
+ array :api_responses do
77
+ element :any, :response_data # Flexible hash structure
78
+ end
79
+
80
+ array :user_events do
81
+ element :any, :event_data # Dynamic event properties (includes event_type)
82
+ end
83
+ end
84
+
85
+ # Access hash fields using fn(:fetch)
86
+ value :response_codes, fn(:fetch, input.api_responses.response_data, "status")
87
+ value :error_messages, fn(:fetch, input.api_responses.response_data, "error")
88
+ value :event_types, fn(:fetch, input.user_events.event_data, "event_type")
89
+ value :user_ids, fn(:fetch, input.user_events.event_data, "user_id")
90
+ value :timestamps, fn(:fetch, input.user_events.event_data, "timestamp")
91
+
92
+ # Mathematical operations on extracted values
93
+ value :avg_response_time, fn(:mean, fn(:fetch, input.api_responses.response_data, "response_time"))
94
+ value :total_events, fn(:size, input.user_events.event_data)
95
+
96
+ # Traits and cascades with dynamic data
97
+ trait :has_errors, fn(:any?, fn(:fetch, input.api_responses.response_data, "status") >= 400)
98
+ trait :recent_events, fn(:any?, fn(:fetch, input.user_events.event_data, "timestamp") > 1640995200)
99
+
100
+ value :system_status do
101
+ on has_errors, "Error State"
102
+ on recent_events, "Active"
103
+ base "Idle"
104
+ end
105
+ ```
106
+
107
+ **When to use `element :any`:**
108
+ - API responses with varying JSON schemas
109
+ - Configuration files with flexible key-value structures
110
+ - Event data where properties vary by event type
111
+ - Legacy systems where data structure may change
112
+ - Prototyping when exact hash structure is unknown
113
+
114
+ **Comparison with Hash Objects:**
115
+
116
+ | Approach | Use Case | Flexibility | Type Safety |
117
+ |----------|----------|-------------|-------------|
118
+ | `hash :field do ... end` | Known structure, strong typing | Limited | High |
119
+ | `element :any, :field` | Unknown/flexible structure | High | Low |
120
+
121
+ ```ruby
122
+ # Known structure - use hash objects
123
+ array :orders do
124
+ hash :customer do
125
+ string :name
126
+ string :email
127
+ end
128
+ end
129
+
130
+ # Unknown/flexible structure - use element :any
131
+ array :api_calls do
132
+ element :any, :response # Could be any JSON structure
133
+ end
134
+ ```
135
+
70
136
  ## Business Use Cases
71
137
 
72
138
  Element access mode is essential for common business scenarios involving simple nested arrays:
@@ -14,10 +14,26 @@ schema do
14
14
  array :tags, elem: { type: :string }
15
15
  hash :metadata, key: { type: :string }, val: { type: :any }
16
16
  any :flexible
17
+
18
+ # Structured arrays with defined fields
19
+ array :orders do
20
+ hash :customer do
21
+ string :name
22
+ string :email
23
+ end
24
+ float :total
25
+ end
26
+
27
+ # Dynamic arrays with flexible elements
28
+ array :api_responses do
29
+ element :any, :response_data # For unknown/flexible hash structures
30
+ end
17
31
  end
18
32
 
19
33
  trait :adult, (input.age >= 18)
20
34
  value :status, input.verified ? "verified" : "pending"
35
+ value :customer_emails, input.orders.customer.email # Structured access
36
+ value :response_codes, fn(:fetch, input.api_responses.response_data, "status") # Dynamic access
21
37
  end
22
38
  ```
23
39
 
@@ -86,27 +86,21 @@ puts
86
86
  # ------------------------------------------------------------------
87
87
  Benchmark.ips do |x|
88
88
  schemas.each do |d, schema|
89
- # 1) HOT (memoized): expect ~flat, nanosecond-level if cached
90
- hot = schema.from(seed: 0)
91
- x.report("HOT fetch #{d}-deep") do
92
- hot[:final_result]
93
- end
94
-
95
- # 2) COLD via UPDATE (no memoized result): change a dependent input each iter
96
- upd = schema.from(seed: 0)
97
- i = 0
98
- x.report("COLD update #{d}-deep") do
99
- i += 1
100
- upd.update(seed: i) # invalidates v0..vN; forces recompute
101
- upd[:final_result]
102
- end
103
-
104
- # 3) COLD new runner (includes construction)
105
- prng = Random.new(42)
106
- x.report("COLD new #{d}-deep") do
107
- r = schema.from(seed: prng.rand(1_000_000))
108
- r[:final_result]
109
- end
89
+ runner = schema.from(seed: 0) # memoised runner
90
+ x.report("eval #{d}-deep") { runner[:final_result] }
110
91
  end
111
92
  x.compare!
112
93
  end
94
+ # Warming up --------------------------------------
95
+ # eval 50-deep 222.000 i/100ms
96
+ # eval 100-deep 57.000 i/100ms
97
+ # eval 150-deep 26.000 i/100ms
98
+ # Calculating -------------------------------------
99
+ # eval 50-deep 2.166k (± 1.9%) i/s (461.70 μs/i) - 10.878k in 5.024320s
100
+ # eval 100-deep 561.698 (± 1.4%) i/s (1.78 ms/i) - 2.850k in 5.075057s
101
+ # eval 150-deep 253.732 (± 0.8%) i/s (3.94 ms/i) - 1.274k in 5.021499s
102
+
103
+ # Comparison:
104
+ # eval 50-deep: 2165.9 i/s
105
+ # eval 100-deep: 561.7 i/s - 3.86x slower
106
+ # eval 150-deep: 253.7 i/s - 8.54x slower
@@ -85,11 +85,11 @@ module Kumi
85
85
  end
86
86
 
87
87
  case parent_meta.container
88
- when :object
88
+ when :object, :hash
89
89
  kids.each_value do |child|
90
90
  child.enter_via = :hash
91
91
  child.consume_alias = false
92
- child.access_mode = :field
92
+ child.access_mode ||= :field # Only set if not explicitly specified
93
93
  end
94
94
 
95
95
  when :array
@@ -139,7 +139,11 @@ module Kumi
139
139
  report_error(errors, "access_mode :element only valid for single scalar/array element (at :#{kname})", location: nil)
140
140
  end
141
141
  else
142
- report_error(errors, "access_mode :element only valid under array parent (at :#{kname})", location: nil)
142
+ # Only scalar children under non-array parents are invalid with :element mode
143
+ # Arrays under hash/object parents can have :element mode (for arrays of scalars)
144
+ if child.container == :scalar
145
+ report_error(errors, "access_mode :element only valid under array parent (at :#{kname})", location: nil)
146
+ end
143
147
  end
144
148
  end
145
149
  end
@@ -147,7 +151,8 @@ module Kumi
147
151
 
148
152
  def kind_from_type(t)
149
153
  return :array if t == :array
150
- return :field if t == :field
154
+ return :hash if t == :hash
155
+ return :object if t == :field
151
156
 
152
157
  :scalar
153
158
  end
@@ -139,9 +139,9 @@ module Kumi
139
139
  analysis_state = {
140
140
  evaluation_order: evaluation_order,
141
141
  declarations: declarations,
142
- # access_plans: access_plans,
142
+ access_plans: access_plans,
143
143
  join_reduce_plans: join_reduce_plans,
144
- # scope_plans: scope_plans,
144
+ scope_plans: scope_plans,
145
145
  input_metadata: input_metadata,
146
146
  vec_names: @vec_names,
147
147
  vec_meta: @vec_meta,
@@ -379,6 +379,7 @@ module Kumi
379
379
  when Syntax::InputReference
380
380
  plan_id = pick_plan_id_for_input([expr.name], access_plans,
381
381
  scope_plan: scope_plan, need_indices: need_indices)
382
+
382
383
  plans = access_plans.fetch(expr.name.to_s, [])
383
384
  selected = plans.find { |p| p.accessor_key == plan_id }
384
385
  scope = selected ? selected.scope : []
@@ -430,6 +431,13 @@ module Kumi
430
431
  when Syntax::CallExpression
431
432
  entry = Kumi::Registry.entry(expr.fn_name)
432
433
 
434
+ # Constant folding optimization: evaluate expressions with all literal arguments
435
+ if can_constant_fold?(expr, entry)
436
+ folded_value = constant_fold(expr, entry)
437
+ ops << Kumi::Core::IR::Ops.Const(folded_value)
438
+ return ops.size - 1
439
+ end
440
+
433
441
  if ENV["DEBUG_LOWER"] && has_nested_reducer?(expr)
434
442
  puts " NESTED_REDUCER_DETECTED in #{expr.fn_name} with req_scope=#{required_scope.inspect}"
435
443
  end
@@ -986,6 +994,31 @@ module Kumi
986
994
  end
987
995
  end
988
996
  end
997
+
998
+ # Constant folding optimization helpers
999
+ def can_constant_fold?(expr, entry)
1000
+ return false unless entry&.fn # Skip if function not found
1001
+ return false if entry.reducer # Skip reducer functions for now
1002
+ return false if expr.args.empty? # Need at least one argument
1003
+
1004
+ # Check if all arguments are literals
1005
+ expr.args.all? { |arg| arg.is_a?(Syntax::Literal) }
1006
+ end
1007
+
1008
+ def constant_fold(expr, entry)
1009
+ literal_values = expr.args.map(&:value)
1010
+
1011
+ begin
1012
+ # Call the function with literal values at compile time
1013
+ entry.fn.call(*literal_values)
1014
+ rescue StandardError => e
1015
+ # If constant folding fails, fall back to runtime evaluation
1016
+ # This shouldn't happen with pure functions, but be defensive
1017
+ puts "Constant folding failed for #{expr.fn_name}: #{e.message}" if ENV["DEBUG_LOWER"]
1018
+ raise "Cannot constant fold #{expr.fn_name}: #{e.message}"
1019
+ end
1020
+ end
1021
+
989
1022
  end
990
1023
  end
991
1024
  end
@@ -143,12 +143,12 @@ module Kumi
143
143
  else
144
144
  raise ArgumentError, "Invalid :enter_via '#{enter_via}' for array child '#{seg}'. Must be :hash or :array"
145
145
  end
146
- elsif container.nil? || container == :object
147
- # Root or object parent - always emit enter_hash
146
+ elsif container.nil? || container == :object || container == :hash
147
+ # Root, object, or hash parent - always emit enter_hash
148
148
  ops << enter_hash(seg)
149
149
  puts " Added: enter_hash('#{seg}')" if ENV["DEBUG_ACCESSOR_OPS"]
150
150
  else
151
- raise ArgumentError, "Invalid parent :container '#{container}' for segment '#{seg}'. Expected :array, :object, or nil (root)"
151
+ raise ArgumentError, "Invalid parent :container '#{container}' for segment '#{seg}'. Expected :array, :object, :hash, or nil (root)"
152
152
  end
153
153
 
154
154
  parent_meta = node
@@ -39,7 +39,7 @@ module Kumi
39
39
  end
40
40
 
41
41
  private_class_method def self.should_validate_type?(meta)
42
- meta[:type] && meta[:type] != :any
42
+ meta[:type] && meta[:type] != :any && !(meta[:children] && !meta[:children].empty?)
43
43
  end
44
44
 
45
45
  private_class_method def self.should_validate_domain?(meta)
@@ -42,7 +42,36 @@ module Kumi
42
42
  # - DEBUG_GROUP_ROWS=1 prints grouping decisions during Lift.
43
43
  module ExecutionEngine
44
44
  def self.run(ir_module, ctx, accessors:, registry:)
45
- Interpreter.run(ir_module, ctx, accessors: accessors, registry: registry)
45
+ # Use persistent accessor cache if available, otherwise create temporary one
46
+ if ctx[:accessor_cache]
47
+ # Include input data in cache key to avoid cross-context pollution
48
+ input_key = ctx[:input]&.hash || ctx["input"]&.hash || 0
49
+ memoized_accessors = add_persistent_memoization(accessors, ctx[:accessor_cache], input_key)
50
+ else
51
+ memoized_accessors = add_temporary_memoization(accessors)
52
+ end
53
+
54
+ Interpreter.run(ir_module, ctx, accessors: memoized_accessors, registry: registry)
55
+ end
56
+
57
+ private
58
+
59
+ def self.add_persistent_memoization(accessors, cache, input_key)
60
+ accessors.map do |plan_id, accessor_fn|
61
+ [plan_id, lambda do |input_data|
62
+ cache_key = [plan_id, input_key]
63
+ cache[cache_key] ||= accessor_fn.call(input_data)
64
+ end]
65
+ end.to_h
66
+ end
67
+
68
+ def self.add_temporary_memoization(accessors)
69
+ cache = {}
70
+ accessors.map do |plan_id, accessor_fn|
71
+ [plan_id, lambda do |input_data|
72
+ cache[plan_id] ||= accessor_fn.call(input_data)
73
+ end]
74
+ end.to_h
46
75
  end
47
76
  end
48
77
  end
@@ -33,10 +33,14 @@ module Kumi
33
33
  end
34
34
  end
35
35
 
36
- def hash(name_or_key_type, val_type = nil, **kwargs)
37
- return Kumi::Core::Types.hash(name_or_key_type, val_type) unless val_type.nil?
38
-
39
- create_hash_field(name_or_key_type, kwargs)
36
+ def hash(name_or_key_type, val_type = nil, **kwargs, &block)
37
+ if block_given?
38
+ create_hash_field_with_block(name_or_key_type, kwargs, &block)
39
+ elsif val_type.nil?
40
+ create_hash_field(name_or_key_type, kwargs)
41
+ else
42
+ Kumi::Core::Types.hash(name_or_key_type, val_type)
43
+ end
40
44
  end
41
45
 
42
46
  def method_missing(method_name, *_args)
@@ -174,6 +178,23 @@ module Kumi
174
178
  children, = collect_array_children(&block)
175
179
  @context.inputs << Kumi::Syntax::InputDeclaration.new(name, nil, :field, children, nil, loc: @context.current_location)
176
180
  end
181
+
182
+ def create_hash_field_with_block(field_name, options, &block)
183
+ domain = options[:domain]
184
+
185
+ # Collect children by creating a nested context (reuse array logic)
186
+ children, = collect_array_children(&block)
187
+
188
+ # Create the InputDeclaration with children and :field access_mode for hash objects
189
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(
190
+ field_name,
191
+ domain,
192
+ :hash,
193
+ children,
194
+ :field,
195
+ loc: @context.current_location
196
+ )
197
+ end
177
198
  end
178
199
  end
179
200
  end
@@ -5,7 +5,7 @@ module Kumi
5
5
  module Types
6
6
  # Validates type definitions and structures
7
7
  class Validator
8
- VALID_TYPES = %i[string integer float boolean any symbol regexp time date datetime array].freeze
8
+ VALID_TYPES = %i[string integer float boolean any symbol regexp time date datetime array hash].freeze
9
9
 
10
10
  def self.valid_type?(type)
11
11
  return true if VALID_TYPES.include?(type)
@@ -62,6 +62,7 @@ module Kumi
62
62
  @reg = registry
63
63
  @input_metadata = input_metadata.freeze
64
64
  @decl = @ir.decls.map { |d| [d.name, d] }.to_h
65
+ @accessor_cache = {} # Persistent accessor cache across evaluations
65
66
  end
66
67
 
67
68
  def decl?(name) = @decl.key?(name)
@@ -85,12 +86,20 @@ module Kumi
85
86
  def eval_decl(name, input, mode: :ruby)
86
87
  raise Kumi::Core::Errors::RuntimeError, "unknown decl #{name}" unless decl?(name)
87
88
 
88
- out = Kumi::Core::IR::ExecutionEngine.run(@ir, { input: input, target: name },
89
+ out = Kumi::Core::IR::ExecutionEngine.run(@ir, { input: input, target: name, accessor_cache: @accessor_cache },
89
90
  accessors: @acc, registry: @reg).fetch(name)
90
91
 
91
92
  mode == :ruby ? unwrap(@decl[name], out) : out
92
93
  end
93
94
 
95
+ def clear_field_accessor_cache(field_name)
96
+ # Clear cache entries for all accessor plans related to this field
97
+ # Cache keys are now [plan_id, input_key] arrays
98
+ @accessor_cache.delete_if { |cache_key, _|
99
+ cache_key.is_a?(Array) && cache_key[0].to_s.start_with?("#{field_name}:")
100
+ }
101
+ end
102
+
94
103
  private
95
104
 
96
105
  def validate_keys(keys)
@@ -153,9 +162,12 @@ module Kumi
153
162
 
154
163
  # Update the input data
155
164
  @input = deep_merge(@input, { field => value })
165
+
166
+ # Clear accessor cache for this specific field
167
+ @program.clear_field_accessor_cache(field)
156
168
  end
157
169
 
158
- # Clear cache after all updates
170
+ # Clear declaration evaluation cache after all updates
159
171
  @cache.clear
160
172
  self
161
173
  end
data/lib/kumi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kumi
4
- VERSION = "0.0.11"
4
+ VERSION = "0.0.13"
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kumi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.11
4
+ version: 0.0.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - André Muta
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2025-08-14 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: zeitwerk
@@ -23,6 +24,7 @@ dependencies:
23
24
  - - "~>"
24
25
  - !ruby/object:Gem::Version
25
26
  version: 2.6.0
27
+ description:
26
28
  email:
27
29
  - andremuta@gmail.com
28
30
  executables: []
@@ -198,6 +200,7 @@ metadata:
198
200
  source_code_uri: https://github.com/amuta/kumi
199
201
  changelog_uri: https://github.com/amuta/kumi/blob/main/CHANGELOG.md
200
202
  rubygems_mfa_required: 'true'
203
+ post_install_message:
201
204
  rdoc_options: []
202
205
  require_paths:
203
206
  - lib
@@ -205,14 +208,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
205
208
  requirements:
206
209
  - - ">="
207
210
  - !ruby/object:Gem::Version
208
- version: 3.0.0
211
+ version: 3.1.0
209
212
  required_rubygems_version: !ruby/object:Gem::Requirement
210
213
  requirements:
211
214
  - - ">="
212
215
  - !ruby/object:Gem::Version
213
216
  version: '0'
214
217
  requirements: []
215
- rubygems_version: 3.7.1
218
+ rubygems_version: 3.5.22
219
+ signing_key:
216
220
  specification_version: 4
217
- summary: A Declarative logic framework with static analysis for Ruby.
221
+ summary: Kumi is a declarative rules-and-calculation DSL for Ruby that compiles your
222
+ business logic into a typed, analyzable dependency graph with
223
+ vector semantics over nested data. It does static checks at definition time, lowers
224
+ to a small IR, and runs with consistent, predictable behavior.
218
225
  test_files: []