musa-dsl 0.30.2 → 0.40.0

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/.version +6 -0
  4. data/.yardopts +7 -0
  5. data/README.md +227 -6
  6. data/docs/README.md +83 -0
  7. data/docs/api-reference.md +86 -0
  8. data/docs/getting-started/quick-start.md +93 -0
  9. data/docs/getting-started/tutorial.md +58 -0
  10. data/docs/subsystems/core-extensions.md +316 -0
  11. data/docs/subsystems/datasets.md +465 -0
  12. data/docs/subsystems/generative.md +290 -0
  13. data/docs/subsystems/matrix.md +63 -0
  14. data/docs/subsystems/midi.md +123 -0
  15. data/docs/subsystems/music.md +233 -0
  16. data/docs/subsystems/musicxml-builder.md +264 -0
  17. data/docs/subsystems/neumas.md +71 -0
  18. data/docs/subsystems/repl.md +135 -0
  19. data/docs/subsystems/sequencer.md +98 -0
  20. data/docs/subsystems/series.md +302 -0
  21. data/docs/subsystems/transcription.md +152 -0
  22. data/docs/subsystems/transport.md +177 -0
  23. data/lib/musa-dsl/core-ext/array-explode-ranges.rb +68 -0
  24. data/lib/musa-dsl/core-ext/arrayfy.rb +110 -0
  25. data/lib/musa-dsl/core-ext/attribute-builder.rb +91 -30
  26. data/lib/musa-dsl/core-ext/deep-copy.rb +125 -2
  27. data/lib/musa-dsl/core-ext/dynamic-proxy.rb +78 -0
  28. data/lib/musa-dsl/core-ext/extension.rb +53 -0
  29. data/lib/musa-dsl/core-ext/hashify.rb +162 -1
  30. data/lib/musa-dsl/core-ext/inspect-nice.rb +154 -0
  31. data/lib/musa-dsl/core-ext/smart-proc-binder.rb +117 -0
  32. data/lib/musa-dsl/core-ext/with.rb +114 -0
  33. data/lib/musa-dsl/datasets/dataset.rb +109 -0
  34. data/lib/musa-dsl/datasets/delta-d.rb +78 -0
  35. data/lib/musa-dsl/datasets/e.rb +186 -2
  36. data/lib/musa-dsl/datasets/gdv.rb +279 -2
  37. data/lib/musa-dsl/datasets/gdvd.rb +201 -0
  38. data/lib/musa-dsl/datasets/helper.rb +75 -0
  39. data/lib/musa-dsl/datasets/p.rb +177 -2
  40. data/lib/musa-dsl/datasets/packed-v.rb +91 -0
  41. data/lib/musa-dsl/datasets/pdv.rb +136 -1
  42. data/lib/musa-dsl/datasets/ps.rb +134 -4
  43. data/lib/musa-dsl/datasets/score/queriable.rb +143 -1
  44. data/lib/musa-dsl/datasets/score/render.rb +105 -1
  45. data/lib/musa-dsl/datasets/score/to-mxml/process-pdv.rb +138 -1
  46. data/lib/musa-dsl/datasets/score/to-mxml/process-ps.rb +111 -0
  47. data/lib/musa-dsl/datasets/score/to-mxml/process-time.rb +200 -1
  48. data/lib/musa-dsl/datasets/score/to-mxml/to-mxml.rb +145 -1
  49. data/lib/musa-dsl/datasets/score.rb +279 -0
  50. data/lib/musa-dsl/datasets/v.rb +88 -0
  51. data/lib/musa-dsl/generative/darwin.rb +180 -1
  52. data/lib/musa-dsl/generative/generative-grammar.rb +359 -0
  53. data/lib/musa-dsl/generative/markov.rb +133 -3
  54. data/lib/musa-dsl/generative/rules.rb +258 -4
  55. data/lib/musa-dsl/generative/variatio.rb +217 -2
  56. data/lib/musa-dsl/logger/logger.rb +267 -2
  57. data/lib/musa-dsl/matrix/matrix.rb +256 -10
  58. data/lib/musa-dsl/midi/midi-recorder.rb +108 -1
  59. data/lib/musa-dsl/midi/midi-voices.rb +265 -4
  60. data/lib/musa-dsl/music/chord-definition.rb +233 -1
  61. data/lib/musa-dsl/music/chord-definitions.rb +33 -6
  62. data/lib/musa-dsl/music/chords.rb +308 -2
  63. data/lib/musa-dsl/music/equally-tempered-12-tone-scale-system.rb +315 -0
  64. data/lib/musa-dsl/music/scales.rb +957 -40
  65. data/lib/musa-dsl/musicxml/builder/attributes.rb +483 -3
  66. data/lib/musa-dsl/musicxml/builder/backup-forward.rb +166 -1
  67. data/lib/musa-dsl/musicxml/builder/direction.rb +243 -0
  68. data/lib/musa-dsl/musicxml/builder/helper.rb +240 -0
  69. data/lib/musa-dsl/musicxml/builder/measure.rb +284 -0
  70. data/lib/musa-dsl/musicxml/builder/note-complexities.rb +324 -8
  71. data/lib/musa-dsl/musicxml/builder/note.rb +285 -0
  72. data/lib/musa-dsl/musicxml/builder/part-group.rb +108 -1
  73. data/lib/musa-dsl/musicxml/builder/part.rb +139 -0
  74. data/lib/musa-dsl/musicxml/builder/pitched-note.rb +124 -0
  75. data/lib/musa-dsl/musicxml/builder/rest.rb +93 -0
  76. data/lib/musa-dsl/musicxml/builder/score-partwise.rb +276 -0
  77. data/lib/musa-dsl/musicxml/builder/typed-text.rb +62 -1
  78. data/lib/musa-dsl/musicxml/builder/unpitched-note.rb +83 -0
  79. data/lib/musa-dsl/neumalang/neumalang.rb +675 -0
  80. data/lib/musa-dsl/neumas/array-to-neumas.rb +149 -0
  81. data/lib/musa-dsl/neumas/neuma-decoder.rb +253 -0
  82. data/lib/musa-dsl/neumas/neuma-gdv-decoder.rb +142 -2
  83. data/lib/musa-dsl/neumas/neuma-gdvd-decoder.rb +82 -0
  84. data/lib/musa-dsl/neumas/neumas.rb +67 -0
  85. data/lib/musa-dsl/neumas/string-to-neumas.rb +233 -1
  86. data/lib/musa-dsl/repl/repl.rb +550 -0
  87. data/lib/musa-dsl/sequencer/base-sequencer-implementation-every.rb +118 -2
  88. data/lib/musa-dsl/sequencer/base-sequencer-implementation-move.rb +149 -2
  89. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-helper.rb +296 -0
  90. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play-timed.rb +88 -2
  91. data/lib/musa-dsl/sequencer/base-sequencer-implementation-play.rb +161 -0
  92. data/lib/musa-dsl/sequencer/base-sequencer-implementation.rb +263 -0
  93. data/lib/musa-dsl/sequencer/base-sequencer-tick-based.rb +173 -1
  94. data/lib/musa-dsl/sequencer/base-sequencer-tickless-based.rb +177 -0
  95. data/lib/musa-dsl/sequencer/base-sequencer.rb +710 -10
  96. data/lib/musa-dsl/sequencer/sequencer-dsl.rb +210 -0
  97. data/lib/musa-dsl/sequencer/timeslots.rb +79 -0
  98. data/lib/musa-dsl/series/array-to-serie.rb +37 -1
  99. data/lib/musa-dsl/series/base-series.rb +843 -5
  100. data/lib/musa-dsl/series/buffer-serie.rb +48 -0
  101. data/lib/musa-dsl/series/hash-or-array-serie-splitter.rb +41 -0
  102. data/lib/musa-dsl/series/main-serie-constructors.rb +398 -2
  103. data/lib/musa-dsl/series/main-serie-operations.rb +538 -16
  104. data/lib/musa-dsl/series/proxy-serie.rb +67 -0
  105. data/lib/musa-dsl/series/quantizer-serie.rb +45 -7
  106. data/lib/musa-dsl/series/queue-serie.rb +65 -0
  107. data/lib/musa-dsl/series/series-composer.rb +701 -0
  108. data/lib/musa-dsl/series/timed-serie.rb +473 -28
  109. data/lib/musa-dsl/transcription/from-gdv-to-midi.rb +404 -1
  110. data/lib/musa-dsl/transcription/from-gdv-to-musicxml.rb +118 -0
  111. data/lib/musa-dsl/transcription/from-gdv.rb +84 -1
  112. data/lib/musa-dsl/transcription/transcription.rb +265 -0
  113. data/lib/musa-dsl/transport/clock.rb +125 -0
  114. data/lib/musa-dsl/transport/dummy-clock.rb +89 -2
  115. data/lib/musa-dsl/transport/external-tick-clock.rb +91 -0
  116. data/lib/musa-dsl/transport/input-midi-clock.rb +133 -1
  117. data/lib/musa-dsl/transport/timer-clock.rb +183 -1
  118. data/lib/musa-dsl/transport/timer.rb +83 -0
  119. data/lib/musa-dsl/transport/transport.rb +318 -0
  120. data/lib/musa-dsl/version.rb +1 -1
  121. data/lib/musa-dsl.rb +132 -25
  122. data/musa-dsl.gemspec +12 -10
  123. metadata +87 -8
@@ -6,10 +6,83 @@ require_relative '../core-ext/arrayfy'
6
6
  module Musa
7
7
  module Series
8
8
  module Operations
9
+ # Creates a composer transformation pipeline for complex multi-stage transformations.
10
+ #
11
+ # Composer provides declarative DSL for building transformation pipelines
12
+ # with multiple inputs, outputs, and intermediate processing stages.
13
+ #
14
+ # ## Composer Concepts
15
+ #
16
+ # - **Pipelines**: Named transformation chains
17
+ # - **Inputs**: Named input series (proxied and buffered)
18
+ # - **Outputs**: Named output series
19
+ # - **Operations**: Transformation steps in pipeline
20
+ # - **Auto-commit**: Automatic finalization of pipelines
21
+ #
22
+ # ## DSL Structure
23
+ #
24
+ # ```ruby
25
+ # composer do
26
+ # input_name >> operation1 >> operation2 >> :output_name
27
+ # :other_input >> transform >> :other_output
28
+ # end
29
+ # ```
30
+ #
31
+ # ## Musical Applications
32
+ #
33
+ # - Complex multi-voice processing
34
+ # - Effect chains and routing
35
+ # - Algorithmic composition pipelines
36
+ # - Multi-stage transformations
37
+ # - Modular synthesis-style routing
38
+ #
39
+ # @example Basic composer
40
+ # s = S(1, 2, 3).composer do
41
+ # input.map { |x| x * 2 } >> :output
42
+ # end
43
+ # s.i.to_a # => [2, 4, 6]
44
+ #
45
+ # @example Multi-pipeline
46
+ # composer = Composer.new(input: S(1, 2, 3)) do
47
+ # input.map { |x| x * 2 } >> :doubled
48
+ # input.map { |x| x * 3 } >> :tripled
49
+ # end
50
+ # composer.output(:doubled).i.to_a # => [2, 4, 6]
51
+ # composer.output(:tripled).i.to_a # => [3, 6, 9]
52
+ #
53
+ # @yield composer DSL block
54
+ #
55
+ # @return [ComposerAsOperationSerie] composed serie
56
+ #
57
+ # @example Simple transformation
58
+ # s.composer do
59
+ # input.map { |x| x + 1 } >> :output
60
+ # end
61
+ #
62
+ # @api public
9
63
  def composer(&block)
10
64
  ComposerAsOperationSerie.new(self, &block)
11
65
  end
12
66
 
67
+ # Wrapper that exposes Composer as a serie operation.
68
+ #
69
+ # Allows {Composer::Composer} pipelines to be used as standard serie
70
+ # operations. The composer takes input from the source serie, processes
71
+ # it through the configured pipeline, and outputs transformed values.
72
+ #
73
+ # This enables complex multi-input/output serie transformations to
74
+ # be used in serie operation chains.
75
+ #
76
+ # @example Using composer as operation
77
+ # serie = FromArray.new([1, 2, 3, 4])
78
+ # transformed = serie.composer do
79
+ # # Composer pipeline configuration
80
+ # pipeline { |v| v * 2 }
81
+ # end
82
+ # transformed.next_value # => 2
83
+ #
84
+ # @see Composer::Composer Full composer implementation
85
+ # @api private
13
86
  class ComposerAsOperationSerie
14
87
  include Musa::Series::Serie.with(source: true)
15
88
 
@@ -37,7 +110,177 @@ module Musa
37
110
  end
38
111
  end
39
112
 
113
+ # Series composition and transformation pipeline system.
114
+ #
115
+ # Provides infrastructure for building complex multi-input/output
116
+ # transformation pipelines with named routes, intermediate stages,
117
+ # and declarative DSL syntax.
118
+ #
119
+ # The Composer system enables modular, synthesis-style routing of
120
+ # serie transformations with explicit input/output management.
121
+ #
122
+ # @see Composer::Composer Main composer implementation
123
+ # @see ComposerAsOperationSerie Composer as serie operation
40
124
  module Composer
125
+ # Multi-input/output serie transformation pipeline system.
126
+ #
127
+ # Composer enables building complex transformation graphs with multiple named
128
+ # inputs, outputs, and intermediate processing stages. It provides a declarative
129
+ # DSL for defining pipelines and routing data between them, similar to modular
130
+ # synthesis patch routing.
131
+ #
132
+ # ## Architecture
133
+ #
134
+ # The Composer system consists of:
135
+ #
136
+ # - **Pipelines**: Named transformation stages that process series
137
+ # - **Routes**: Connections between pipelines specifying data flow
138
+ # - **Inputs**: Named entry points (automatically proxied and buffered)
139
+ # - **Outputs**: Named exit points for consuming results
140
+ # - **DSL Context**: method_missing-based interface for pipeline definition
141
+ #
142
+ # ## Pipeline Definition
143
+ #
144
+ # Pipelines are defined using method calls in the DSL block. Each pipeline
145
+ # consists of:
146
+ #
147
+ # - **Constructor** (optional): Series constructor like `S`, `H`, etc.
148
+ # - **Operations** (one or more): Transformations like `reverse`, `skip`, etc.
149
+ #
150
+ # @example Pipeline with constructor
151
+ # composer = Composer.new(inputs: nil) do
152
+ # my_pipeline ({ S: [1, 2, 3] }), reverse, { skip: 1 }
153
+ # route my_pipeline, to: output
154
+ # end
155
+ # composer.output.i.to_a # => [2, 1]
156
+ # # Creates: S([1,2,3]) → reverse → skip(1)
157
+ #
158
+ # @example Pipeline with operations only
159
+ # composer = Composer.new(input: S(1, 2, 3)) do
160
+ # my_pipeline reverse, { skip: 1 }
161
+ # route input, to: my_pipeline
162
+ # route my_pipeline, to: output
163
+ # end
164
+ # composer.output.i.to_a # => [2, 1]
165
+ # # Applies: input → reverse → skip(1)
166
+ #
167
+ # ## Routing System
168
+ #
169
+ # Routes connect pipelines using the `route` method:
170
+ #
171
+ # ```ruby
172
+ # route from_pipeline, to: to_pipeline, on: :source, as: :key
173
+ # ```
174
+ #
175
+ # **Routing modes:**
176
+ #
177
+ # 1. **Hash assignment** (when `as:` provided):
178
+ #
179
+ # - Data is assigned to: `to_pipeline.input[on][as] = from_pipeline.output`
180
+ # - Used by operations accepting hash inputs (e.g., `H` constructor)
181
+ # - Default `on` is `:sources` when `as` is provided
182
+ #
183
+ # 2. **Setter method** (when `as:` omitted):
184
+ #
185
+ # - Calls: `to_pipeline.input.source = from_pipeline.output`
186
+ # - Used by operations with explicit source setters
187
+ # - Default `on` is `:source` when `as` is omitted
188
+ #
189
+ # @example Hash routing
190
+ # composer = Composer.new(inputs: [:a, :b], auto_commit: false) do
191
+ # step1 reverse
192
+ # step2 reverse
193
+ # hash_merge ({ H: {} })
194
+ #
195
+ # route a, to: step1
196
+ # route b, to: step2
197
+ # route step1, to: hash_merge, as: :x # hash_merge.input[:sources][:x] = step1.output
198
+ # route step2, to: hash_merge, as: :y # hash_merge.input[:sources][:y] = step2.output
199
+ # route hash_merge, to: output
200
+ # end
201
+ #
202
+ # composer.input(:a).proxy_source = S(1, 2, 3)
203
+ # composer.input(:b).proxy_source = S(10, 20, 30)
204
+ # composer.commit!
205
+ # composer.output.i.to_a # => [{x: 3, y: 30}, {x: 2, y: 20}, {x: 1, y: 10}]
206
+ #
207
+ # @example Setter routing
208
+ # composer = Composer.new(input: S(1, 2, 3)) do
209
+ # step1 reverse
210
+ # route input, to: step1 # step1.input.source = input.output
211
+ # route step1, to: output
212
+ # end
213
+ #
214
+ # composer.output.i.to_a # => [3, 2, 1]
215
+ #
216
+ # ## Commit System
217
+ #
218
+ # Composer uses two-phase initialization:
219
+ #
220
+ # 1. **Definition phase**: Routes and pipelines are declared
221
+ # 2. **Commit phase**: All connections are resolved and finalized
222
+ #
223
+ # **Key behaviors:**
224
+ # - Routes can be defined in any order (order-independent)
225
+ # - Output access is blocked until `commit!` is called
226
+ # - Commit resolves all routes and sets up buffering
227
+ # - `auto_commit: true` (default) commits automatically after DSL block
228
+ #
229
+ # ## DSL Context
230
+ #
231
+ # The DSL uses `method_missing` to enable natural pipeline definition:
232
+ #
233
+ # - **Named access**: `input`, `output`, pipeline names return symbols
234
+ # - **Pipeline creation**: `name arg1, arg2, ...` creates pipeline
235
+ # - **Operation symbols**: Series operations return as symbols for parsing
236
+ # - **Constructor symbols**: Series constructors return as symbols for parsing
237
+ #
238
+ # @example Basic pipeline
239
+ # composer = Composer.new(input: S(1, 2, 3)) do
240
+ # step1 reverse
241
+ # route input, to: step1
242
+ # route step1, to: output
243
+ # end
244
+ # composer.output.i.to_a # => [3, 2, 1]
245
+ #
246
+ # @example Multiple inputs merging
247
+ # composer = Composer.new(inputs: { a: S(1, 2), b: S(10, 20) }) do
248
+ # hash_merge ({ H: {} })
249
+ # route a, to: hash_merge, as: :x
250
+ # route b, to: hash_merge, as: :y
251
+ # route hash_merge, to: output
252
+ # end
253
+ # composer.output.i.to_a # => [{x: 1, y: 10}, {x: 2, y: 20}]
254
+ #
255
+ # @example Multiple outputs
256
+ # composer = Composer.new(input: S(1, 2, 3)) do
257
+ # doubled ({ eval: ->(v) { v * 2 } })
258
+ # tripled ({ eval: ->(v) { v * 3 } })
259
+ #
260
+ # route input, to: doubled
261
+ # route input, to: tripled
262
+ # route doubled, to: output
263
+ # end
264
+ # composer.output.i.to_a # => [2, 4, 6]
265
+ #
266
+ # @example Complex routing
267
+ # composer = Composer.new(inputs: [:a, :b], auto_commit: false) do
268
+ # step1 reverse
269
+ # step2 ({ skip: 1 })
270
+ # hash_merge ({ H: {} })
271
+ #
272
+ # route a, to: step1
273
+ # route b, to: step2
274
+ # route step1, to: hash_merge, as: :x
275
+ # route step2, to: hash_merge, as: :y
276
+ # route hash_merge, to: output
277
+ # end
278
+ #
279
+ # composer.input(:a).proxy_source = S(1, 2, 3)
280
+ # composer.input(:b).proxy_source = S(10, 20, 30)
281
+ # composer.commit!
282
+ #
283
+ # composer.output.i.to_a # => [{x: 3, y: 20}, {x: 2, y: 30}]
41
284
  class Composer
42
285
  using Musa::Extension::Arrayfy
43
286
 
@@ -87,11 +330,52 @@ module Musa
87
330
  commit! if auto_commit
88
331
  end
89
332
 
333
+ # Accesses named input proxy for dynamic source assignment.
334
+ #
335
+ # Returns the proxy series for the specified input, allowing dynamic
336
+ # assignment of source series after composer creation. Used with
337
+ # `auto_commit: false` to set sources before manual commit.
338
+ #
339
+ # @param name [Symbol, nil] input name (defaults to :input)
340
+ #
341
+ # @return [ProxySerie] proxy series for source assignment
342
+ #
343
+ # @example Set input source dynamically
344
+ # composer = Composer.new(auto_commit: false) do
345
+ # step reverse
346
+ # route input, to: step
347
+ # route step, to: output
348
+ # end
349
+ #
350
+ # composer.input.proxy_source = S(1, 2, 3)
351
+ # composer.commit!
352
+ #
353
+ # @api public
90
354
  def input(name = nil)
91
355
  name ||= :input
92
356
  @inputs[name].input
93
357
  end
94
358
 
359
+ # Accesses named output series for consumption.
360
+ #
361
+ # Returns the output series for the specified output. Can only be
362
+ # called after `commit!` has been invoked. Raises error if composer
363
+ # is not committed.
364
+ #
365
+ # @param name [Symbol, nil] output name (defaults to :output)
366
+ #
367
+ # @return [Serie] output series
368
+ #
369
+ # @raise [RuntimeError] if composer not yet committed
370
+ #
371
+ # @example Access output
372
+ # composer.output.i.to_a # => [3, 2, 1]
373
+ #
374
+ # @example Multiple outputs
375
+ # composer.output(:doubled).i.to_a # => [2, 4, 6]
376
+ # composer.output(:tripled).i.to_a # => [3, 6, 9]
377
+ #
378
+ # @api public
95
379
  def output(name = nil)
96
380
  raise "Can't access output if the Composer is uncommited. Call '.commit' first." unless @commited
97
381
 
@@ -99,18 +383,152 @@ module Musa
99
383
  @outputs[name].output
100
384
  end
101
385
 
386
+ # Defines routing connection between pipelines.
387
+ #
388
+ # Creates data flow route from source pipeline to destination pipeline.
389
+ # The routing behavior depends on whether `as:` parameter is provided.
390
+ #
391
+ # **With `as:` parameter (Hash routing):**
392
+ # - Assigns to hash: `to_pipeline.input[on][as] = from_pipeline.output`
393
+ # - Default `on` is `:sources`
394
+ # - Used by operations accepting hash inputs (e.g., `H` constructor)
395
+ #
396
+ # **Without `as:` parameter (Setter routing):**
397
+ # - Calls setter: `to_pipeline.input.source = from_pipeline.output`
398
+ # - Default `on` is `:source`
399
+ # - Used by operations with explicit source setter methods
400
+ #
401
+ # @param from [Symbol] source pipeline name
402
+ # @param to [Symbol] destination pipeline name
403
+ # @param on [Symbol, nil] attribute name (:source or :sources)
404
+ # @param as [Symbol, nil] hash key for assignment
405
+ #
406
+ # @return [void]
407
+ #
408
+ # @raise [ArgumentError] if pipeline names not found
409
+ # @raise [ArgumentError] if route already exists
410
+ #
411
+ # @example Hash routing (inside DSL block)
412
+ # composer = Composer.new(inputs: [:a, :b], auto_commit: false) do
413
+ # step1 reverse
414
+ # step2 reverse
415
+ # hash_merge ({ H: {} })
416
+ #
417
+ # route a, to: step1
418
+ # route b, to: step2
419
+ # route step1, to: hash_merge, as: :x # hash_merge.input[:sources][:x] = step1
420
+ # route step2, to: hash_merge, as: :y # hash_merge.input[:sources][:y] = step2
421
+ # route hash_merge, to: output
422
+ # end
423
+ #
424
+ # composer.input(:a).proxy_source = S(1, 2)
425
+ # composer.input(:b).proxy_source = S(10, 20)
426
+ # composer.commit!
427
+ # composer.output.i.to_a # => [{x: 2, y: 20}, {x: 1, y: 10}]
428
+ #
429
+ # @example Setter routing (inside DSL block)
430
+ # composer = Composer.new(input: S(1, 2, 3)) do
431
+ # step reverse
432
+ # route input, to: step # step.input.source = input
433
+ # route step, to: output
434
+ # end
435
+ #
436
+ # composer.output.i.to_a # => [3, 2, 1]
437
+ #
438
+ # @example Custom on parameter (inside DSL block)
439
+ # composer = Composer.new(input: S(1, 2, 3), auto_commit: false) do
440
+ # step reverse
441
+ # hash_merge ({ H: {} })
442
+ # route input, to: step
443
+ # route step, to: hash_merge, on: :sources, as: :x
444
+ # route hash_merge, to: output
445
+ # end
446
+ #
447
+ # composer.commit!
448
+ # composer.output.i.to_a # => [{x: 3}, {x: 2}, {x: 1}]
449
+ #
450
+ # @api public
102
451
  def route(from, to:, on: nil, as: nil)
103
452
  @dsl.route(from, to: to, on: on, as: as)
104
453
  end
105
454
 
455
+ # Defines named pipeline with transformation operations.
456
+ #
457
+ # Creates a pipeline from constructor and/or operations. This method
458
+ # is typically called implicitly through the DSL's method_missing, but
459
+ # can be called directly for dynamic pipeline creation.
460
+ #
461
+ # @param name [Symbol] pipeline name
462
+ # @param elements [Array] constructor and operations
463
+ #
464
+ # @return [void]
465
+ #
466
+ # @example Direct call (inside DSL block)
467
+ # composer = Composer.new(inputs: nil) do
468
+ # pipeline(:my_step, [{ S: [1, 2, 3] }, :reverse])
469
+ # route my_step, to: output
470
+ # end
471
+ #
472
+ # composer.output.i.to_a # => [3, 2, 1]
473
+ #
474
+ # @example DSL equivalent (method_missing)
475
+ # composer = Composer.new(inputs: nil) do
476
+ # my_step ({ S: [1, 2, 3] }), reverse
477
+ # route my_step, to: output
478
+ # end
479
+ #
480
+ # composer.output.i.to_a # => [3, 2, 1]
481
+ #
482
+ # @api public
106
483
  def pipeline(name, *elements)
107
484
  @dsl.pipeline(name, elements)
108
485
  end
109
486
 
487
+ # Updates composer with additional DSL block.
488
+ #
489
+ # Allows dynamic modification of composer after creation by executing
490
+ # additional DSL block in the composer's DSL context. Useful for
491
+ # progressive pipeline construction.
492
+ #
493
+ # @yield DSL block with additional pipeline definitions
494
+ #
495
+ # @return [void]
496
+ #
497
+ # @example Add routes dynamically
498
+ # composer.update do
499
+ # route step3, to: output
500
+ # end
501
+ #
502
+ # @api public
110
503
  def update(&block)
111
504
  @dsl.with &block
112
505
  end
113
506
 
507
+ # Finalizes composer by resolving all routes and connections.
508
+ #
509
+ # Commits the composer, resolving all route connections and setting up
510
+ # buffering. Must be called before accessing outputs. Cannot be called
511
+ # twice on same composer instance.
512
+ #
513
+ # The commit process:
514
+ # 1. Recursively commits all output pipelines
515
+ # 2. Each pipeline commits its input routes
516
+ # 3. Connects all sources through buffers
517
+ # 4. Sets committed flag enabling output access
518
+ #
519
+ # @return [void]
520
+ #
521
+ # @raise [RuntimeError] if already committed
522
+ #
523
+ # @example Manual commit
524
+ # composer = Composer.new(auto_commit: false) do
525
+ # # ... pipeline definitions ...
526
+ # end
527
+ # composer.input.proxy_source = S(1, 2, 3)
528
+ # composer.commit!
529
+ # result = composer.output.i.to_a
530
+ #
531
+ # @api public
114
532
  def commit!
115
533
  raise 'Already commited' if @commited
116
534
 
@@ -121,7 +539,38 @@ module Musa
121
539
  @commited = true
122
540
  end
123
541
 
542
+ # Internal representation of a transformation pipeline stage.
543
+ #
544
+ # Pipeline encapsulates a single named stage in the composer graph,
545
+ # storing its input/output series, routes, and transformation logic.
546
+ #
547
+ # ## Pipeline Types
548
+ #
549
+ # - **Input pipelines**: Created from `inputs:` parameter, wrap proxy series
550
+ # - **Output pipelines**: Created from `outputs:` parameter, wrap proxy series
551
+ # - **Transformation pipelines**: Created by DSL, contain transformation logic
552
+ #
553
+ # ## Pipeline Components
554
+ #
555
+ # - `@first_proc`: Proc creating initial series from UNDEFINED (constructors)
556
+ # - `@chain_proc`: Proc applying operations to existing series
557
+ # - `@routes`: Hash of incoming route connections
558
+ # - `@input`: Input series (set during commit)
559
+ # - `@output`: Output series (set during commit)
560
+ #
561
+ # @api private
124
562
  class Pipeline
563
+ # Creates new pipeline stage.
564
+ #
565
+ # @param name [Symbol] pipeline name
566
+ # @param is_output [Boolean] whether this is an output pipeline
567
+ # @param input [Serie, nil] input series
568
+ # @param output [Serie, nil] output series
569
+ # @param first_proc [Proc, nil] constructor proc
570
+ # @param chain_proc [Proc, nil] operations proc
571
+ # @param pipelines [Hash] reference to all pipelines
572
+ #
573
+ # @api private
125
574
  def initialize(name, is_output: false, input: nil, output: nil, first_proc: nil, chain_proc: nil, pipelines:)
126
575
  @name = name
127
576
  @is_output = is_output
@@ -136,14 +585,46 @@ module Musa
136
585
  attr_reader :name, :is_output
137
586
  attr_accessor :input, :output, :proc
138
587
 
588
+ # Retrieves route at specified connection point.
589
+ #
590
+ # @param on [Symbol] connection attribute name
591
+ # @param as [Symbol, nil] hash key for assignment
592
+ #
593
+ # @return [Route, nil] route at connection point
594
+ #
595
+ # @api private
139
596
  def [](on, as)
140
597
  @routes[[on, as]]
141
598
  end
142
599
 
600
+ # Stores route at specified connection point.
601
+ #
602
+ # @param on [Symbol] connection attribute name
603
+ # @param as [Symbol, nil] hash key for assignment
604
+ # @param source [Pipeline] source pipeline
605
+ #
606
+ # @return [Route] created route
607
+ #
608
+ # @api private
143
609
  def []=(on, as, source)
144
610
  @routes[[on, as]] = Route.new(on, as, source)
145
611
  end
146
612
 
613
+ # Finalizes pipeline by resolving routes and connecting series.
614
+ #
615
+ # The commit process:
616
+ # 1. Calls `@first_proc` with UNDEFINED to create initial series (if present)
617
+ # 2. Recursively commits all source pipelines
618
+ # 3. Connects input routes:
619
+ # - For output pipelines: assigns to proxy_source
620
+ # - For hash routes (with `as`): assigns to input[on][as]
621
+ # - For setter routes (without `as`): calls input.on=
622
+ # 4. Applies `@chain_proc` to transform input series (if present)
623
+ # 5. Sets output series (buffered)
624
+ #
625
+ # @return [self]
626
+ #
627
+ # @api private
147
628
  def commit!
148
629
  first_serie_operation = @first_proc&.call(Musa::Series::Constructors.UNDEFINED())
149
630
 
@@ -168,7 +649,20 @@ module Musa
168
649
  end
169
650
  end
170
651
 
652
+ # Route connection between two pipelines.
653
+ #
654
+ # Stores the metadata for a single route connection, including the
655
+ # connection point (on), hash key (as), and source pipeline.
656
+ #
657
+ # @api private
171
658
  class Route
659
+ # Creates new route.
660
+ #
661
+ # @param on [Symbol] connection attribute name
662
+ # @param as [Symbol, nil] hash key for assignment
663
+ # @param source [Pipeline] source pipeline
664
+ #
665
+ # @api private
172
666
  def initialize(on, as, source)
173
667
  @on = on
174
668
  @as = as
@@ -177,13 +671,56 @@ module Musa
177
671
  attr_accessor :on, :as, :source
178
672
  end
179
673
 
674
+ # DSL execution context for pipeline definition.
675
+ #
676
+ # DSLContext provides the execution environment for the composer DSL block.
677
+ # It uses `method_missing` to enable natural syntax for pipeline definition
678
+ # and routing operations.
679
+ #
680
+ # ## Method Resolution
681
+ #
682
+ # - **Series operations** (`reverse`, `skip`, etc.): Return symbol for parsing
683
+ # - **Series constructors** (`S`, `H`, etc.): Return symbol for parsing
684
+ # - **Pipeline names**: Return symbol for routing
685
+ # - **With arguments**: Create pipeline with those arguments
686
+ #
687
+ # ## Key Responsibilities
688
+ #
689
+ # - Parse pipeline definitions into first/chain procs
690
+ # - Validate and store route connections
691
+ # - Distinguish between constructors and operations
692
+ # - Handle dynamic parameter passing
693
+ #
694
+ # @api private
180
695
  class DSLContext
181
696
  include Musa::Extension::With
182
697
 
698
+ # Creates DSL context.
699
+ #
700
+ # @param pipelines [Hash] reference to all pipelines
701
+ #
702
+ # @api private
183
703
  def initialize(pipelines)
184
704
  @pipelines = pipelines
185
705
  end
186
706
 
707
+ # Defines route connection in DSL context.
708
+ #
709
+ # Validates pipeline existence, determines default `on` parameter,
710
+ # checks for duplicate routes, and stores route in destination pipeline.
711
+ #
712
+ # @param from [Symbol] source pipeline name
713
+ # @param to [Symbol] destination pipeline name
714
+ # @param on [Symbol, nil] connection attribute
715
+ # @param as [Symbol, nil] hash key
716
+ #
717
+ # @return [void]
718
+ #
719
+ # @raise [ArgumentError] if pipelines not found
720
+ # @raise [ArgumentError] if route already exists
721
+ # @raise [ArgumentError] if output pipeline with on/as parameters
722
+ #
723
+ # @api private
187
724
  def route(from, to:, on: nil, as: nil)
188
725
  from_pipeline = @pipelines[from]
189
726
  to_pipeline = @pipelines[to]
@@ -195,6 +732,7 @@ module Musa
195
732
  raise ArgumentError, "Output pipeline #{to_pipeline.name} only allows default routing"
196
733
  end
197
734
 
735
+ # Default on logic: :sources when as provided, :source otherwise
198
736
  on ||= (as ? :sources : :source)
199
737
 
200
738
  raise ArgumentError,
@@ -205,6 +743,17 @@ module Musa
205
743
  to_pipeline[on, as] = from_pipeline
206
744
  end
207
745
 
746
+ # Creates pipeline from elements in DSL context.
747
+ #
748
+ # Parses elements into first/chain procs, creates Pipeline instance,
749
+ # and defines DSL accessor method for pipeline name.
750
+ #
751
+ # @param name [Symbol] pipeline name
752
+ # @param elements [Array] constructor and/or operations
753
+ #
754
+ # @return [void]
755
+ #
756
+ # @api private
208
757
  def pipeline(name, elements)
209
758
  first, chain = parse(elements)
210
759
  @pipelines[name] = Pipeline.new(name, first_proc: first, chain_proc: chain, pipelines: @pipelines)
@@ -212,6 +761,41 @@ module Musa
212
761
  define_singleton_method(name) { name }
213
762
  end
214
763
 
764
+ # Parses pipeline elements into first/chain proc pair.
765
+ #
766
+ # The parser transforms DSL syntax into executable procs. It splits
767
+ # pipeline definition into:
768
+ # - `first`: Proc creating initial series from UNDEFINED (constructors)
769
+ # - `chain`: Proc applying operations to existing series
770
+ #
771
+ # **Parsing modes:**
772
+ #
773
+ # - **Array**: Sequence of operations/constructors
774
+ # - **Hash**: Single operation/constructor with parameters
775
+ # - **Symbol**: Operation/constructor name without parameters
776
+ # - **Proc**: Direct transformation function
777
+ #
778
+ # **First vs Chain logic:**
779
+ # - First element (if constructor) becomes `first`
780
+ # - Remaining elements compose into `chain`
781
+ # - Operations only: `first` is nil, all become `chain`
782
+ #
783
+ # @param thing [Array, Hash, Symbol, Proc, Object] element(s) to parse
784
+ #
785
+ # @return [Array(Proc, Proc), Proc, Object] [first, chain] pair or single proc
786
+ #
787
+ # @example Array with constructor + operations
788
+ # parse([{ S: [1, 2, 3] }, :reverse, { skip: 1 }])
789
+ # # => [first_proc, chain_proc]
790
+ # # first_proc: creates S([1,2,3])
791
+ # # chain_proc: applies reverse >> skip(1)
792
+ #
793
+ # @example Operations only
794
+ # parse([:reverse, { skip: 1 }])
795
+ # # => [nil, chain_proc]
796
+ # # chain_proc: applies reverse >> skip(1)
797
+ #
798
+ # @api private
215
799
  private def parse(thing)
216
800
  case thing
217
801
  when Array
@@ -263,6 +847,18 @@ module Musa
263
847
  end
264
848
  end
265
849
 
850
+ # Wraps operation/constructor as chainable proc.
851
+ #
852
+ # Creates proc that accepts previous result (`last`) and applies
853
+ # operation/constructor with parameters. Distinguishes between
854
+ # constructors (create series) and operations (transform series).
855
+ #
856
+ # @param operation [Symbol] operation or constructor name
857
+ # @param parameter [Object, nil] operation parameters
858
+ #
859
+ # @return [Proc] chainable proc accepting `last` argument
860
+ #
861
+ # @api private
266
862
  private def operation_as_chained_proc(operation, parameter = nil)
267
863
  if is_a_series_constructor?(operation)
268
864
  proc do |last|
@@ -278,6 +874,31 @@ module Musa
278
874
  end
279
875
  end
280
876
 
877
+ # Calls series constructor based on previous result type.
878
+ #
879
+ # Handles different `last` types to enable flexible constructor usage:
880
+ #
881
+ # - **UndefinedSerie**: Normal constructor call (pipeline start)
882
+ # - **Array**: Pass array elements as arguments
883
+ # - **Hash**: Pass hash as keyword arguments
884
+ # - **nil**: Call with parameter
885
+ # - **Serie**: Error (cannot reconstruct from serie)
886
+ # - **Proc**: Evaluate proc first, then call constructor
887
+ #
888
+ # This enables patterns like:
889
+ # - `({ S: [1, 2, 3] })` - Direct constructor
890
+ # - `operation, :S` - Constructor from operation result
891
+ #
892
+ # @param last [Object] previous pipeline result
893
+ # @param constructor [Symbol] constructor name (e.g., :S, :H)
894
+ # @param parameter [Object] constructor parameters
895
+ #
896
+ # @return [Serie] constructed series
897
+ #
898
+ # @raise [RuntimeError] if last is Serie (invalid)
899
+ # @raise [RuntimeError] if parameter type unexpected
900
+ #
901
+ # @api private
281
902
  private def call_constructor_according_to_last_and_parameter(last, constructor, parameter)
282
903
  case last
283
904
  when Proc
@@ -318,6 +939,33 @@ module Musa
318
939
  end
319
940
  end
320
941
 
942
+ # Calls series operation with appropriate parameter handling.
943
+ #
944
+ # Handles different parameter types for operation calls:
945
+ #
946
+ # - **nil**: No parameter, simple call
947
+ # - **Symbol**: Chained call (e.g., `operation.parameter`)
948
+ # - **Proc**: Block parameter (e.g., `operation { |x| x }`)
949
+ # - **Array of 2 Procs**: Composed block (proc1 >> proc2)
950
+ # - **Other**: Direct parameter
951
+ #
952
+ # @param target [Object] object to call operation on
953
+ # @param operation [Symbol] operation name
954
+ # @param parameter [Object, nil] operation parameter
955
+ #
956
+ # @return [Object] operation result
957
+ #
958
+ # @raise [ArgumentError] if Array parameter invalid
959
+ #
960
+ # @example No parameter
961
+ # call_operation_according_to_parameter(serie, :reverse, nil)
962
+ # # => serie.reverse
963
+ #
964
+ # @example With block
965
+ # call_operation_according_to_parameter(serie, :map, ->(x) { x * 2 })
966
+ # # => serie.map { |x| x * 2 }
967
+ #
968
+ # @api private
321
969
  private def call_operation_according_to_parameter(target, operation, parameter)
322
970
  case parameter
323
971
  when nil
@@ -337,14 +985,59 @@ module Musa
337
985
  end
338
986
  end
339
987
 
988
+ # Checks if symbol is a series constructor.
989
+ #
990
+ # @param operation [Symbol] operation name
991
+ #
992
+ # @return [Boolean] true if constructor
993
+ #
994
+ # @api private
340
995
  private def is_a_series_constructor?(operation)
341
996
  Musa::Series::Constructors.instance_methods.include?(operation)
342
997
  end
343
998
 
999
+ # Checks if symbol is a series operation.
1000
+ #
1001
+ # @param operation [Symbol] operation name
1002
+ #
1003
+ # @return [Boolean] true if operation
1004
+ #
1005
+ # @api private
344
1006
  private def is_a_series_operation?(operation)
345
1007
  Musa::Series::Operations.instance_methods.include?(operation)
346
1008
  end
347
1009
 
1010
+ # Enables DSL method syntax via method_missing.
1011
+ #
1012
+ # Implements the DSL's natural syntax by intercepting undefined methods:
1013
+ #
1014
+ # - **Series operations/constructors**: Return symbol for parsing
1015
+ # - **With arguments/block**: Create pipeline with those elements
1016
+ # - **Without arguments**: Return symbol for routing
1017
+ #
1018
+ # This allows expressions like:
1019
+ #
1020
+ # ```ruby
1021
+ # composer = Composer.new(input: S(1, 2, 3)) do
1022
+ # # `reverse` → returns :reverse (operation symbol)
1023
+ # # `my_step reverse, { skip: 1 }` → creates pipeline named :my_step
1024
+ # # `route input, to: step1` → uses :step1 symbol for routing
1025
+ #
1026
+ # my_step reverse, { skip: 1 }
1027
+ # route input, to: my_step
1028
+ # route my_step, to: output
1029
+ # end
1030
+ #
1031
+ # composer.output.i.to_a # => [2, 1]
1032
+ # ```
1033
+ #
1034
+ # @param symbol [Symbol] method name called
1035
+ # @param args [Array] method arguments
1036
+ # @param block [Proc, nil] block passed to method
1037
+ #
1038
+ # @return [Symbol, void] symbol for parsing or creates pipeline
1039
+ #
1040
+ # @api private
348
1041
  private def method_missing(symbol, *args, &block)
349
1042
  if is_a_series_constructor?(symbol) || is_a_series_operation?(symbol)
350
1043
  symbol
@@ -356,6 +1049,14 @@ module Musa
356
1049
  end
357
1050
  end
358
1051
 
1052
+ # Declares which methods respond_to for method_missing.
1053
+ #
1054
+ # @param method_name [Symbol] method to check
1055
+ # @param include_private [Boolean] include private methods
1056
+ #
1057
+ # @return [Boolean] true if responds to method
1058
+ #
1059
+ # @api private
359
1060
  private def respond_to_missing?(method_name, include_private = false)
360
1061
  Musa::Series::Operations.instance_methods.include?(method_name) ||
361
1062
  Musa::Series::Constructors.instance_methods.include?(method_name) ||