kumi-parser 0.0.13 → 0.0.14
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/lib/kumi/parser/direct_parser.rb +80 -124
- data/lib/kumi/parser/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 83ae18a38c78f9513121f579f024309c162ebbc1fdf909aecbe8aceff04e100a
|
4
|
+
data.tar.gz: 8bfab6fb7250f7e39289ceb07b84ae556958a77be19b66fe253b19c7284359ba
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39f5759930377f813b10d8ce148dc5f26bd9a6b4a21a577b91bbb24fe7d47d923cf3967f25454666bb9c8c23f0190909f6283fe629a7bd935d51ca9033c25047
|
7
|
+
data.tar.gz: d06bd58249f51ef7edafb51f291c2b18720ea259b3a375b9624ac593631f7786aa2c920da92bea99ec1ad27fc517c71c9b7bbd41e7626faa814949e317b56f29
|
@@ -25,7 +25,6 @@ module Kumi
|
|
25
25
|
def peek_token(offset = 1)
|
26
26
|
peek_pos = @pos + offset
|
27
27
|
return @tokens.last if peek_pos >= @tokens.length # Return EOF
|
28
|
-
|
29
28
|
@tokens[peek_pos]
|
30
29
|
end
|
31
30
|
|
@@ -72,7 +71,6 @@ module Kumi
|
|
72
71
|
|
73
72
|
expect_token(:end)
|
74
73
|
|
75
|
-
# Construct Root with exact AST.md structure
|
76
74
|
Kumi::Syntax::Root.new(
|
77
75
|
input_declarations,
|
78
76
|
value_declarations, # values
|
@@ -91,9 +89,7 @@ module Kumi
|
|
91
89
|
|
92
90
|
until %i[end eof].include?(current_token.type)
|
93
91
|
break unless current_token.metadata[:category] == :type_keyword
|
94
|
-
|
95
92
|
declarations << parse_input_declaration
|
96
|
-
|
97
93
|
skip_comments_and_newlines
|
98
94
|
end
|
99
95
|
|
@@ -101,28 +97,31 @@ module Kumi
|
|
101
97
|
declarations
|
102
98
|
end
|
103
99
|
|
104
|
-
# Input declaration: 'integer :name' or 'array :items do ... end'
|
100
|
+
# Input declaration: 'integer :name' or 'array :items do ... end' or 'element :type, :name'
|
101
|
+
#
|
102
|
+
# IMPORTANT: For array nodes with a block, this sets the node's access_mode:
|
103
|
+
# - :element if the block contains exactly one child introduced by `element`
|
104
|
+
# - :field otherwise
|
105
105
|
def parse_input_declaration
|
106
106
|
type_token = current_token
|
107
|
-
|
108
|
-
if type_token.metadata[:category] != :type_keyword
|
107
|
+
unless type_token.metadata[:category] == :type_keyword
|
109
108
|
raise_parse_error("Expected type keyword, got #{type_token.type}")
|
110
109
|
end
|
111
|
-
|
112
110
|
advance
|
113
|
-
|
114
|
-
#
|
115
|
-
|
111
|
+
|
112
|
+
# element :type, :name (syntactic sugar: the child was declared via `element`)
|
113
|
+
declared_with_element = (type_token.metadata[:type_name] == :element)
|
114
|
+
if declared_with_element
|
116
115
|
element_type_token = expect_token(:symbol)
|
117
116
|
expect_token(:comma)
|
118
117
|
name_token = expect_token(:symbol)
|
119
118
|
actual_type = element_type_token.value
|
120
119
|
else
|
121
|
-
name_token
|
120
|
+
name_token = expect_token(:symbol)
|
122
121
|
actual_type = type_token.metadata[:type_name]
|
123
122
|
end
|
124
123
|
|
125
|
-
#
|
124
|
+
# Optional: ', domain: ...'
|
126
125
|
domain = nil
|
127
126
|
if current_token.type == :comma
|
128
127
|
advance
|
@@ -131,13 +130,15 @@ module Kumi
|
|
131
130
|
expect_token(:colon)
|
132
131
|
domain = parse_domain_specification
|
133
132
|
else
|
134
|
-
# Put comma back for other parsers
|
135
133
|
@pos -= 1
|
136
134
|
end
|
137
135
|
end
|
138
136
|
|
139
|
-
#
|
137
|
+
# Parse nested declarations for block forms
|
140
138
|
children = []
|
139
|
+
any_element_children = false
|
140
|
+
any_field_children = false
|
141
|
+
|
141
142
|
if %i[array hash element].include?(actual_type) && current_token.type == :do
|
142
143
|
advance # consume 'do'
|
143
144
|
skip_comments_and_newlines
|
@@ -145,12 +146,31 @@ module Kumi
|
|
145
146
|
until %i[end eof].include?(current_token.type)
|
146
147
|
break unless current_token.metadata[:category] == :type_keyword
|
147
148
|
|
148
|
-
|
149
|
+
# Syntactic decision (NO counting): is this child introduced by `element`?
|
150
|
+
child_is_element_keyword = (current_token.metadata[:type_name] == :element)
|
151
|
+
any_element_children ||= child_is_element_keyword
|
152
|
+
any_field_children ||= !child_is_element_keyword
|
149
153
|
|
154
|
+
children << parse_input_declaration
|
150
155
|
skip_comments_and_newlines
|
151
156
|
end
|
152
157
|
|
153
158
|
expect_token(:end)
|
159
|
+
|
160
|
+
# For array blocks, access_mode derives strictly from syntax:
|
161
|
+
# - :element if ANY direct child used `element`
|
162
|
+
# - :field if NONE used `element`
|
163
|
+
# Mixing is invalid.
|
164
|
+
if actual_type == :array
|
165
|
+
if any_element_children && any_field_children
|
166
|
+
raise_parse_error("array :#{name_token.value} mixes `element` and field children; choose one style")
|
167
|
+
end
|
168
|
+
access_mode = any_element_children ? :element : :field
|
169
|
+
else
|
170
|
+
access_mode = :field # objects/hashes with blocks behave like field containers
|
171
|
+
end
|
172
|
+
else
|
173
|
+
access_mode = nil # leaves carry no access_mode
|
154
174
|
end
|
155
175
|
|
156
176
|
if children.empty?
|
@@ -162,71 +182,60 @@ module Kumi
|
|
162
182
|
loc: type_token.location
|
163
183
|
)
|
164
184
|
else
|
185
|
+
# 5th positional arg in your existing ctor is access_mode
|
165
186
|
Kumi::Syntax::InputDeclaration.new(
|
166
187
|
name_token.value,
|
167
188
|
domain,
|
168
189
|
actual_type,
|
169
190
|
children,
|
170
|
-
:field,
|
191
|
+
access_mode || :field,
|
171
192
|
loc: type_token.location
|
172
193
|
)
|
173
194
|
end
|
174
195
|
end
|
175
196
|
|
176
197
|
def parse_domain_specification
|
177
|
-
# Parse domain specifications: domain: ["x", "y"], domain: [1, 2, 3], domain: 1..10, domain: 1...10
|
178
198
|
case current_token.type
|
179
199
|
when :lbracket
|
180
|
-
# Array domain: ["a", "b", "c"] or [1, 2, 3]
|
181
200
|
array_expr = parse_array_literal
|
182
|
-
# Convert ArrayExpression to Ruby Array for analyzer compatibility
|
183
201
|
convert_array_expression_to_ruby_array(array_expr)
|
184
202
|
when :integer, :float
|
185
|
-
# Range domain: 1..10 or 1...10
|
186
203
|
parse_range_domain
|
187
204
|
else
|
188
|
-
# Skip unknown domain specs for now
|
189
205
|
advance until %i[comma newline eof end].include?(current_token.type)
|
190
206
|
nil
|
191
207
|
end
|
192
208
|
end
|
193
209
|
|
194
210
|
def parse_range_domain
|
195
|
-
# Parse numeric ranges like 1..10 or 0.0...100.0
|
196
211
|
start_token = current_token
|
197
212
|
start_value = start_token.type == :integer ? start_token.value.to_i : start_token.value.to_f
|
198
213
|
advance
|
199
214
|
|
200
215
|
case current_token.type
|
201
216
|
when :dot_dot
|
202
|
-
|
203
|
-
advance # consume ..
|
217
|
+
advance
|
204
218
|
end_token = current_token
|
205
219
|
end_value = end_token.type == :integer ? end_token.value.to_i : end_token.value.to_f
|
206
220
|
advance
|
207
221
|
(start_value..end_value)
|
208
222
|
when :dot_dot_dot
|
209
|
-
|
210
|
-
advance # consume ...
|
223
|
+
advance
|
211
224
|
end_token = current_token
|
212
225
|
end_value = end_token.type == :integer ? end_token.value.to_i : end_token.value.to_f
|
213
226
|
advance
|
214
227
|
(start_value...end_value)
|
215
228
|
else
|
216
|
-
# Just a single number, treat as single-element array
|
217
229
|
[start_value]
|
218
230
|
end
|
219
231
|
end
|
220
232
|
|
221
233
|
def convert_array_expression_to_ruby_array(array_expr)
|
222
234
|
return nil unless array_expr.is_a?(Kumi::Syntax::ArrayExpression)
|
223
|
-
|
224
235
|
array_expr.elements.map do |element|
|
225
236
|
if element.is_a?(Kumi::Syntax::Literal)
|
226
237
|
element.value
|
227
238
|
else
|
228
|
-
# For non-literal elements, we'd need more complex evaluation
|
229
|
-
# For now, just return the element as-is
|
230
239
|
element
|
231
240
|
end
|
232
241
|
end
|
@@ -238,10 +247,8 @@ module Kumi
|
|
238
247
|
name_token = expect_token(:symbol)
|
239
248
|
|
240
249
|
if current_token.type == :do
|
241
|
-
# Cascade expression: value :name do ... end
|
242
250
|
expression = parse_cascade_expression
|
243
251
|
else
|
244
|
-
# Simple expression: value :name, expression
|
245
252
|
expect_token(:comma)
|
246
253
|
expression = parse_expression
|
247
254
|
end
|
@@ -271,55 +278,42 @@ module Kumi
|
|
271
278
|
def parse_cascade_expression
|
272
279
|
start_token = expect_token(:do)
|
273
280
|
cases = []
|
274
|
-
|
275
281
|
skip_comments_and_newlines
|
276
282
|
while %i[on base].include?(current_token.type)
|
277
283
|
cases << parse_case_expression
|
278
284
|
skip_comments_and_newlines
|
279
285
|
end
|
280
|
-
|
281
286
|
expect_token(:end)
|
282
|
-
|
283
287
|
Kumi::Syntax::CascadeExpression.new(cases, loc: start_token.location)
|
284
288
|
end
|
285
289
|
|
286
|
-
# Case expression: 'on condition1, condition2, ..., result' or 'base result'
|
287
290
|
def parse_case_expression
|
288
291
|
case current_token.type
|
289
292
|
when :on
|
290
293
|
on_token = advance_and_return_token
|
291
|
-
|
292
|
-
# Parse all comma-separated expressions
|
294
|
+
|
293
295
|
expressions = []
|
294
296
|
expressions << parse_expression
|
295
|
-
|
296
|
-
# Continue parsing comma-separated expressions until end of case
|
297
297
|
while current_token.type == :comma
|
298
|
-
advance
|
298
|
+
advance
|
299
299
|
expressions << parse_expression
|
300
300
|
end
|
301
|
-
|
302
|
-
# Last expression is the result, all others are conditions
|
301
|
+
|
303
302
|
result = expressions.pop
|
304
303
|
conditions = expressions
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
# Multiple conditions: combine with cascade_and
|
313
|
-
condition = Kumi::Syntax::CallExpression.new(:cascade_and, conditions, loc: on_token.location)
|
314
|
-
end
|
304
|
+
condition =
|
305
|
+
if conditions.length == 1
|
306
|
+
c = conditions[0]
|
307
|
+
simple_trait_reference?(c) ? wrap_condition_in_all(c) : c
|
308
|
+
else
|
309
|
+
Kumi::Syntax::CallExpression.new(:cascade_and, conditions, loc: on_token.location)
|
310
|
+
end
|
315
311
|
|
316
312
|
Kumi::Syntax::CaseExpression.new(condition, result, loc: on_token.location)
|
317
313
|
|
318
314
|
when :base
|
319
315
|
base_token = advance_and_return_token
|
320
316
|
result = parse_expression
|
321
|
-
|
322
|
-
# Base case has condition = true
|
323
317
|
true_literal = Kumi::Syntax::Literal.new(true, loc: base_token.location)
|
324
318
|
Kumi::Syntax::CaseExpression.new(true_literal, result, loc: base_token.location)
|
325
319
|
|
@@ -334,26 +328,22 @@ module Kumi
|
|
334
328
|
token
|
335
329
|
end
|
336
330
|
|
337
|
-
#
|
331
|
+
# Pratt parser for expressions
|
338
332
|
def parse_expression(min_precedence = 0)
|
339
333
|
left = parse_primary_expression
|
340
|
-
|
341
|
-
# Skip whitespace before checking for operators
|
342
334
|
skip_comments_and_newlines
|
343
335
|
|
344
336
|
while current_token.operator? && current_token.precedence >= min_precedence
|
345
337
|
operator_token = current_token
|
346
338
|
advance
|
347
|
-
|
348
|
-
# Skip whitespace after operator
|
349
339
|
skip_comments_and_newlines
|
350
340
|
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
341
|
+
next_min_precedence =
|
342
|
+
if operator_token.left_associative?
|
343
|
+
operator_token.precedence + 1
|
344
|
+
else
|
345
|
+
operator_token.precedence
|
346
|
+
end
|
357
347
|
|
358
348
|
right = parse_expression(next_min_precedence)
|
359
349
|
left = Kumi::Syntax::CallExpression.new(
|
@@ -361,8 +351,6 @@ module Kumi
|
|
361
351
|
[left, right],
|
362
352
|
loc: operator_token.location
|
363
353
|
)
|
364
|
-
|
365
|
-
# Skip whitespace before checking for next operator
|
366
354
|
skip_comments_and_newlines
|
367
355
|
end
|
368
356
|
|
@@ -374,7 +362,6 @@ module Kumi
|
|
374
362
|
|
375
363
|
case token.type
|
376
364
|
when :integer, :float, :string, :boolean, :constant
|
377
|
-
# Direct AST construction using token metadata
|
378
365
|
value = convert_literal_value(token)
|
379
366
|
advance
|
380
367
|
Kumi::Syntax::Literal.new(value, loc: token.location)
|
@@ -392,7 +379,6 @@ module Kumi
|
|
392
379
|
end
|
393
380
|
|
394
381
|
when :input
|
395
|
-
# Handle input references in expressions (input.field)
|
396
382
|
if peek_token.type == :dot
|
397
383
|
parse_input_reference_from_input_token
|
398
384
|
else
|
@@ -400,7 +386,7 @@ module Kumi
|
|
400
386
|
end
|
401
387
|
|
402
388
|
when :lparen
|
403
|
-
advance
|
389
|
+
advance
|
404
390
|
expr = parse_expression
|
405
391
|
expect_token(:rparen)
|
406
392
|
expr
|
@@ -412,11 +398,9 @@ module Kumi
|
|
412
398
|
parse_function_call_from_fn_token
|
413
399
|
|
414
400
|
when :subtract
|
415
|
-
|
416
|
-
advance # consume '-'
|
401
|
+
advance
|
417
402
|
skip_comments_and_newlines
|
418
403
|
operand = parse_primary_expression
|
419
|
-
# Convert to subtraction from zero: (subtract 0 operand)
|
420
404
|
Kumi::Syntax::CallExpression.new(
|
421
405
|
:subtract,
|
422
406
|
[Kumi::Syntax::Literal.new(0, loc: token.location), operand],
|
@@ -424,7 +408,6 @@ module Kumi
|
|
424
408
|
)
|
425
409
|
|
426
410
|
when :newline, :comment
|
427
|
-
# Skip newlines and comments in expressions
|
428
411
|
skip_comments_and_newlines
|
429
412
|
parse_primary_expression
|
430
413
|
|
@@ -438,10 +421,8 @@ module Kumi
|
|
438
421
|
expect_token(:dot)
|
439
422
|
|
440
423
|
path = [expect_field_name_token.to_sym]
|
441
|
-
|
442
|
-
# Handle nested access: input.field.subfield
|
443
424
|
while current_token.type == :dot
|
444
|
-
advance
|
425
|
+
advance
|
445
426
|
path << expect_field_name_token.to_sym
|
446
427
|
end
|
447
428
|
|
@@ -453,14 +434,12 @@ module Kumi
|
|
453
434
|
end
|
454
435
|
|
455
436
|
def parse_input_reference_from_input_token
|
456
|
-
input_token = expect_token(:input)
|
437
|
+
input_token = expect_token(:input)
|
457
438
|
expect_token(:dot)
|
458
439
|
|
459
440
|
path = [expect_field_name_token.to_sym]
|
460
|
-
|
461
|
-
# Handle nested access: input.field.subfield
|
462
441
|
while current_token.type == :dot
|
463
|
-
advance
|
442
|
+
advance
|
464
443
|
path << expect_field_name_token.to_sym
|
465
444
|
end
|
466
445
|
|
@@ -478,54 +457,40 @@ module Kumi
|
|
478
457
|
expect_token(:rbracket)
|
479
458
|
|
480
459
|
base_ref = Kumi::Syntax::DeclarationReference.new(name_token.value.to_sym, loc: name_token.location)
|
481
|
-
Kumi::Syntax::CallExpression.new(
|
482
|
-
:at,
|
483
|
-
[base_ref, index_expr],
|
484
|
-
loc: name_token.location
|
485
|
-
)
|
460
|
+
Kumi::Syntax::CallExpression.new(:at, [base_ref, index_expr], loc: name_token.location)
|
486
461
|
end
|
487
462
|
|
488
463
|
def parse_function_call
|
489
|
-
fn_token = expect_token(:identifier)
|
490
|
-
|
464
|
+
fn_token = expect_token(:identifier)
|
491
465
|
if current_token.type == :lparen
|
492
|
-
|
493
|
-
advance # consume '('
|
466
|
+
advance
|
494
467
|
fn_name_token = expect_token(:symbol)
|
495
468
|
fn_name = fn_name_token.value
|
496
|
-
|
497
469
|
args = []
|
498
470
|
while current_token.type == :comma
|
499
|
-
advance
|
471
|
+
advance
|
500
472
|
args << parse_expression
|
501
473
|
end
|
502
|
-
|
503
474
|
expect_token(:rparen)
|
504
475
|
Kumi::Syntax::CallExpression.new(fn_name, args, loc: fn_name_token.location)
|
505
|
-
|
506
476
|
else
|
507
477
|
raise_parse_error("Expected '(' after 'fn'")
|
508
478
|
end
|
509
479
|
end
|
510
480
|
|
511
481
|
def parse_function_call_from_fn_token
|
512
|
-
fn_token = expect_token(:fn)
|
513
|
-
|
482
|
+
fn_token = expect_token(:fn)
|
514
483
|
if current_token.type == :lparen
|
515
|
-
|
516
|
-
advance # consume '('
|
484
|
+
advance
|
517
485
|
fn_name_token = expect_token(:symbol)
|
518
486
|
fn_name = fn_name_token.value
|
519
|
-
|
520
487
|
args = []
|
521
488
|
while current_token.type == :comma
|
522
|
-
advance
|
489
|
+
advance
|
523
490
|
args << parse_expression
|
524
491
|
end
|
525
|
-
|
526
492
|
expect_token(:rparen)
|
527
493
|
Kumi::Syntax::CallExpression.new(fn_name, args, loc: fn_name_token.location)
|
528
|
-
|
529
494
|
else
|
530
495
|
raise_parse_error("Expected '(' after 'fn'")
|
531
496
|
end
|
@@ -533,40 +498,36 @@ module Kumi
|
|
533
498
|
|
534
499
|
def parse_argument_list
|
535
500
|
args = []
|
536
|
-
|
537
501
|
unless current_token.type == :rparen
|
538
502
|
args << parse_expression
|
539
503
|
while current_token.type == :comma
|
540
|
-
advance
|
504
|
+
advance
|
541
505
|
args << parse_expression
|
542
506
|
end
|
543
507
|
end
|
544
|
-
|
545
508
|
args
|
546
509
|
end
|
547
510
|
|
548
511
|
def parse_array_literal
|
549
512
|
start_token = expect_token(:lbracket)
|
550
513
|
elements = []
|
551
|
-
|
552
514
|
unless current_token.type == :rbracket
|
553
515
|
elements << parse_expression
|
554
516
|
while current_token.type == :comma
|
555
|
-
advance
|
517
|
+
advance
|
556
518
|
elements << parse_expression unless current_token.type == :rbracket
|
557
519
|
end
|
558
520
|
end
|
559
|
-
|
560
521
|
expect_token(:rbracket)
|
561
522
|
Kumi::Syntax::ArrayExpression.new(elements, loc: start_token.location)
|
562
523
|
end
|
563
524
|
|
564
525
|
def convert_literal_value(token)
|
565
526
|
case token.type
|
566
|
-
when :integer
|
567
|
-
when :float
|
568
|
-
when :string
|
569
|
-
when :boolean
|
527
|
+
when :integer then token.value.gsub('_', '').to_i
|
528
|
+
when :float then token.value.gsub('_', '').to_f
|
529
|
+
when :string then token.value
|
530
|
+
when :boolean then token.value == 'true'
|
570
531
|
when :constant
|
571
532
|
case token.value
|
572
533
|
when 'Float::INFINITY' then Float::INFINITY
|
@@ -577,7 +538,6 @@ module Kumi
|
|
577
538
|
end
|
578
539
|
|
579
540
|
def expect_field_name_token
|
580
|
-
# Field names can be identifiers or keywords (like 'base', 'input', etc.)
|
581
541
|
token = current_token
|
582
542
|
if token.identifier? || token.keyword?
|
583
543
|
advance
|
@@ -592,28 +552,24 @@ module Kumi
|
|
592
552
|
raise Errors::ParseError.new(message, token: current_token)
|
593
553
|
end
|
594
554
|
|
595
|
-
# Helper method to check if condition is a simple trait reference
|
596
555
|
def simple_trait_reference?(condition)
|
597
556
|
condition.is_a?(Kumi::Syntax::DeclarationReference)
|
598
557
|
end
|
599
558
|
|
600
|
-
|
601
|
-
# Helper method to wrap condition in cascade_and function call
|
602
559
|
def wrap_condition_in_all(condition)
|
603
560
|
Kumi::Syntax::CallExpression.new(:cascade_and, [condition], loc: condition.loc)
|
604
561
|
end
|
605
562
|
|
606
|
-
# Map operator token types to function names for Ruby DSL compatibility
|
607
563
|
def map_operator_token_to_function_name(token_type)
|
608
564
|
case token_type
|
609
|
-
when :eq
|
610
|
-
when :ne
|
611
|
-
when :gt
|
612
|
-
when :lt
|
565
|
+
when :eq then :==
|
566
|
+
when :ne then :!=
|
567
|
+
when :gt then :>
|
568
|
+
when :lt then :<
|
613
569
|
when :gte then :>=
|
614
570
|
when :lte then :<=
|
615
571
|
when :and then :and
|
616
|
-
when :or
|
572
|
+
when :or then :or
|
617
573
|
when :exponent then :power
|
618
574
|
else token_type
|
619
575
|
end
|
data/lib/kumi/parser/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kumi-parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kumi Team
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: kumi
|