hecks 1.0.2 → 1.2.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 (247) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +3 -3
  3. data/lib/hecks/adapters/driven/d1.rb +48 -28
  4. data/lib/hecks/adapters/driven/folder.rb +10 -3
  5. data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
  6. data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
  7. data/lib/hecks/adapters/driven/heki.rb +3 -5
  8. data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
  9. data/lib/hecks/adapters/driven/lambda.rb +2 -2
  10. data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
  11. data/lib/hecks/adapters/driven/local_storage.rb +120 -0
  12. data/lib/hecks/adapters/driven/memory.rb +48 -3
  13. data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
  14. data/lib/hecks/adapters/driven/postgres/outbox.rb +100 -0
  15. data/lib/hecks/adapters/driven/postgres/reconnect.rb +57 -0
  16. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +22 -2
  17. data/lib/hecks/adapters/driven/postgres.rb +45 -31
  18. data/lib/hecks/adapters/driven/prism.rb +11 -0
  19. data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
  20. data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
  21. data/lib/hecks/adapters/driven/sqlite/projection.rb +22 -3
  22. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
  23. data/lib/hecks/adapters/driven/sqlite.rb +87 -8
  24. data/lib/hecks/adapters/driven.rb +4 -0
  25. data/lib/hecks/adapters.rb +4 -0
  26. data/lib/hecks/behaviors/dsl.rb +7 -0
  27. data/lib/hecks/behaviors/expectations.rb +32 -4
  28. data/lib/hecks/behaviors/rspec.rb +2 -0
  29. data/lib/hecks/behaviors/runner.rb +4 -0
  30. data/lib/hecks/bluebook/aggregate.rb +6 -5
  31. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
  32. data/lib/hecks/bluebook/assembly/build.rb +6 -3
  33. data/lib/hecks/bluebook/assembly/contract.rb +5 -1
  34. data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
  35. data/lib/hecks/bluebook/assembly/marks.rb +15 -4
  36. data/lib/hecks/bluebook/assembly.rb +1 -3
  37. data/lib/hecks/bluebook/attribute.rb +6 -0
  38. data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
  39. data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
  40. data/lib/hecks/bluebook/behaviour/read_model.rb +53 -8
  41. data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
  42. data/lib/hecks/bluebook/command.rb +6 -5
  43. data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
  44. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
  45. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +29 -477
  46. data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
  47. data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
  48. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
  49. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
  50. data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
  51. data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
  52. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
  53. data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
  54. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
  55. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
  56. data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
  57. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
  58. data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
  59. data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
  60. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
  61. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
  62. data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
  63. data/lib/hecks/bluebook/dsl/read_model_builder.rb +142 -19
  64. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
  65. data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
  66. data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
  67. data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
  68. data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
  69. data/lib/hecks/bluebook/entity.rb +5 -3
  70. data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
  71. data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
  72. data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
  73. data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
  74. data/lib/hecks/bluebook/expression/projection.json +12 -0
  75. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
  76. data/lib/hecks/bluebook/expression/resolver.rb +83 -11
  77. data/lib/hecks/bluebook/expression.rb +1 -0
  78. data/lib/hecks/bluebook/hexagon.rb +10 -0
  79. data/lib/hecks/bluebook/lifecycle.rb +9 -0
  80. data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
  81. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
  82. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
  83. data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
  84. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
  85. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
  86. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
  87. data/lib/hecks/bluebook/meta_validator.rb +5 -5
  88. data/lib/hecks/bluebook/model_check.rb +119 -82
  89. data/lib/hecks/bluebook/pattern_subset.rb +9 -0
  90. data/lib/hecks/bluebook/policy.rb +2 -1
  91. data/lib/hecks/bluebook/process_manager.rb +6 -0
  92. data/lib/hecks/bluebook/project_register.rb +45 -25
  93. data/lib/hecks/bluebook/query.rb +4 -0
  94. data/lib/hecks/bluebook/read_model.rb +2 -2
  95. data/lib/hecks/bluebook/smoke_test.rb +5 -0
  96. data/lib/hecks/bluebook/synthesizer.rb +2 -1
  97. data/lib/hecks/bluebook/translation.rb +3 -0
  98. data/lib/hecks/bluebook/value_object.rb +2 -2
  99. data/lib/hecks/codemod.rb +13 -0
  100. data/lib/hecks/doc/reference.rb +17 -11
  101. data/lib/hecks/facade/cli_runner.rb +2 -2
  102. data/lib/hecks/facade/command_request.rb +11 -11
  103. data/lib/hecks/facade/handle.rb +3 -3
  104. data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
  105. data/lib/hecks/facade.rb +4 -0
  106. data/lib/hecks/forms/app.rb +10 -6
  107. data/lib/hecks/forms/command_form_renderer.rb +3 -3
  108. data/lib/hecks/forms/field_renderer.rb +14 -9
  109. data/lib/hecks/forms/field_shape.rb +7 -2
  110. data/lib/hecks/forms/html.rb +5 -2
  111. data/lib/hecks/forms/index_renderer.rb +1 -1
  112. data/lib/hecks/forms/page.rb +7 -3
  113. data/lib/hecks/forms/query_form_renderer.rb +3 -2
  114. data/lib/hecks/forms/record_renderer.rb +22 -16
  115. data/lib/hecks/forms.rb +4 -0
  116. data/lib/hecks/fqn.rb +21 -9
  117. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
  118. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
  119. data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
  120. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
  121. data/lib/hecks/fuzzing/properties/guards.rb +251 -0
  122. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
  123. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
  124. data/lib/hecks/fuzzing/properties/querying.rb +206 -0
  125. data/lib/hecks/fuzzing/properties.rb +41 -1065
  126. data/lib/hecks/fuzzing/replay.rb +60 -16
  127. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
  128. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
  129. data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
  130. data/lib/hecks/fuzzing/value_generator.rb +2 -2
  131. data/lib/hecks/grammar/evolve.rb +34 -10
  132. data/lib/hecks/grammar/expression_operators.json +124 -0
  133. data/lib/hecks/grammar.rb +15 -5
  134. data/lib/hecks/ir.rb +9 -0
  135. data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
  136. data/lib/hecks/language/bluebook/command.bluebook +2 -1
  137. data/lib/hecks/language/bluebook/entity.bluebook +2 -1
  138. data/lib/hecks/language/bluebook/projection.bluebook +2 -1
  139. data/lib/hecks/language/bluebook/query.bluebook +4 -2
  140. data/lib/hecks/language/bluebook/shape.bluebook +2 -1
  141. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
  142. data/lib/hecks/literal.rb +18 -8
  143. data/lib/hecks/naming.rb +6 -3
  144. data/lib/hecks/ports/access_control.rb +1 -1
  145. data/lib/hecks/ports/agent.rb +5 -3
  146. data/lib/hecks/ports/authentication.rb +3 -2
  147. data/lib/hecks/ports/authorization.rb +1 -1
  148. data/lib/hecks/ports/clock.rb +1 -1
  149. data/lib/hecks/ports/extraction.rb +5 -1
  150. data/lib/hecks/ports/identity_assignment.rb +1 -1
  151. data/lib/hecks/ports/identity_generation.rb +1 -1
  152. data/lib/hecks/ports/identity_resolution.rb +1 -1
  153. data/lib/hecks/ports/loading.rb +4 -1
  154. data/lib/hecks/ports/persistence/append_only.rb +51 -4
  155. data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
  156. data/lib/hecks/ports/persistence/plugin.rb +3 -0
  157. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
  158. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
  159. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
  160. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
  161. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
  162. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
  163. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
  164. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
  165. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
  166. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
  167. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
  168. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
  169. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
  170. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +77 -18
  171. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
  172. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
  173. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
  174. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
  175. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
  176. data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
  177. data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
  178. data/lib/hecks/ports/persistence.rb +7 -3
  179. data/lib/hecks/ports/projection.rb +13 -5
  180. data/lib/hecks/ports/query/in_memory.rb +8 -2
  181. data/lib/hecks/ports/query.rb +1 -3
  182. data/lib/hecks/ports.rb +2 -0
  183. data/lib/hecks/projections/diagrams.rb +11 -3
  184. data/lib/hecks/projections/glossary.rb +374 -0
  185. data/lib/hecks/projections/ir.rb +2 -0
  186. data/lib/hecks/projections/model/deviations.rb +2 -1
  187. data/lib/hecks/projections/model.rb +15 -10
  188. data/lib/hecks/projections/vocabulary.rb +1 -3
  189. data/lib/hecks/projections.rb +1 -0
  190. data/lib/hecks/projector/cli_projector.rb +78 -42
  191. data/lib/hecks/projector/docs_projector.rb +21 -12
  192. data/lib/hecks/projector/exporter.rb +34 -1
  193. data/lib/hecks/projector/narrate_projector.rb +83 -25
  194. data/lib/hecks/projector.rb +2 -2
  195. data/lib/hecks/query_ir.rb +60 -36
  196. data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
  197. data/lib/hecks/query_specification/common/comparators.rb +4 -0
  198. data/lib/hecks/query_specification/common/comparison.rb +8 -1
  199. data/lib/hecks/query_specification/common/dsl.rb +5 -0
  200. data/lib/hecks/query_specification/common/limit_spec.rb +9 -2
  201. data/lib/hecks/query_specification/common/null_policy.rb +6 -2
  202. data/lib/hecks/query_specification/common/offset_spec.rb +9 -2
  203. data/lib/hecks/query_specification/common/options.rb +17 -5
  204. data/lib/hecks/query_specification/common/order_by.rb +9 -2
  205. data/lib/hecks/query_specification/common/where_clause.rb +15 -2
  206. data/lib/hecks/query_specification/field_path.rb +4 -4
  207. data/lib/hecks/query_specification/read_model/specification.rb +5 -2
  208. data/lib/hecks/router/namespace_installer.rb +22 -10
  209. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
  210. data/lib/hecks/runtime/command_interpreter.rb +104 -65
  211. data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
  212. data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
  213. data/lib/hecks/runtime/dependency_planning.rb +33 -21
  214. data/lib/hecks/runtime/dispatcher.rb +41 -7
  215. data/lib/hecks/runtime/entity_element.rb +25 -6
  216. data/lib/hecks/runtime/entity_interpreter.rb +28 -15
  217. data/lib/hecks/runtime/errors.rb +28 -2
  218. data/lib/hecks/runtime/instance.rb +9 -3
  219. data/lib/hecks/runtime/interpreting.rb +45 -9
  220. data/lib/hecks/runtime/loader.rb +21 -0
  221. data/lib/hecks/runtime/outbox.rb +365 -0
  222. data/lib/hecks/runtime/policy_interpreter.rb +32 -17
  223. data/lib/hecks/runtime/query_interpreter.rb +36 -16
  224. data/lib/hecks/runtime/reaction_invocation.rb +35 -22
  225. data/lib/hecks/runtime/read_model_interpreter.rb +31 -7
  226. data/lib/hecks/runtime/reference_hop.rb +1 -1
  227. data/lib/hecks/runtime/refusal_wording.rb +55 -49
  228. data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
  229. data/lib/hecks/runtime/registry/verification.rb +39 -2
  230. data/lib/hecks/runtime/registry.rb +21 -11
  231. data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
  232. data/lib/hecks/runtime/routing.rb +52 -39
  233. data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
  234. data/lib/hecks/runtime/saga_interpreter.rb +119 -90
  235. data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
  236. data/lib/hecks/runtime/tenant_scope.rb +7 -0
  237. data/lib/hecks/runtime/value/coercion.rb +271 -49
  238. data/lib/hecks/runtime/value.rb +1 -1
  239. data/lib/hecks/runtime.rb +7 -2
  240. data/lib/hecks/storehouse.rb +15 -8
  241. data/lib/hecks/version.rb +1 -1
  242. data/lib/hecks/vocabulary.rb +1 -0
  243. data/lib/hecks.rb +17 -13
  244. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
  245. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
  246. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
  247. metadata +16 -2
@@ -2,8 +2,15 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses a `process_manager "Name" do ... end` block into a
6
+ # `ProcessManager` — its own `starts_on`/`ends_on` events, what
7
+ # correlates its instances (`correlates_by`), and the `transition`-
8
+ # declared state machine whose `dispatch`es (each optionally paired
9
+ # with its own per-dispatch `compensates`) become its handlers. States
10
+ # are DERIVED from the transitions rather than declared separately
11
+ # (S7, ADR 0025).
5
12
  class ProcessManagerBuilder
6
- GRAMMAR_CONTEXT = "ProcessManager"
13
+ GRAMMAR_CONTEXT = "ProcessManager".freeze
7
14
 
8
15
  class InvalidProcessManager < StandardError; end
9
16
 
@@ -189,8 +196,10 @@ module Hecks
189
196
  end
190
197
 
191
198
  def validate!
192
- raise InvalidProcessManager, "#{@name} declares no correlates_by — " \
193
- "nothing would tie its events to one instance" unless @correlates_by
199
+ unless @correlates_by
200
+ raise InvalidProcessManager, "#{@name} declares no correlates_by " \
201
+ "nothing would tie its events to one instance"
202
+ end
194
203
 
195
204
  # THE FIELD, NAMED — never the value object that carries it. A bare
196
205
  # `correlates_by :end_to_end` reads whatever the payload holds under
@@ -202,19 +211,54 @@ module Hecks
202
211
  # holds a head to. This is a syntactic check, not a type check: it
203
212
  # does not know or care whether the field IS a value object, only
204
213
  # that the declaration cannot leave that question open.
205
- raise InvalidProcessManager, "#{@name} correlates_by #{@correlates_by.inspect}, which names a whole " \
206
- "field rather than one of its scalars say which one, e.g. " \
207
- "#{@correlates_by}.value" unless @correlates_by.to_s.include?(".")
214
+ unless @correlates_by.to_s.include?(".")
215
+ raise InvalidProcessManager, "#{@name} correlates_by #{@correlates_by.inspect}, which names a whole " \
216
+ "field rather than one of its scalars — say which one, e.g. " \
217
+ "#{@correlates_by}.value"
218
+ end
219
+
220
+ if @starts_on.to_s.empty?
221
+ raise InvalidProcessManager, "#{@name} declares no starts_on — " \
222
+ "nothing would ever begin it"
223
+ end
224
+
225
+ if @handlers.empty?
226
+ raise InvalidProcessManager, "#{@name} declares no transitions — " \
227
+ "it would start and then ignore every event"
228
+ end
208
229
 
209
- raise InvalidProcessManager, "#{@name} declares no starts_on — " \
210
- "nothing would ever begin it" if @starts_on.to_s.empty?
230
+ refuse_ambiguous_legs!
231
+ end
211
232
 
212
- raise InvalidProcessManager, "#{@name} declares no transitions " \
213
- "it would start and then ignore every event" if @handlers.empty?
233
+ # C10.3 a leg is selected by (event, current state), so two
234
+ # legs answering the SAME event from the SAME state would leave
235
+ # the runtime to pick by declaration order, silently. Refused
236
+ # here, where the declaration can still be read whole. (`from:
237
+ # [...]` fan-out counts: `transition E => "a", from: ["x", "y"]`
238
+ # and `transition E => "b", from: "y"` collide on ("E", "y").)
239
+ def refuse_ambiguous_legs!
240
+ return if MetaValidator.shadow_parsing? # frozen era text is history
241
+
242
+ seen = {}
243
+ @handlers.each do |handler|
244
+ key = [handler.event_type, handler.from_state]
245
+ if (earlier = seen[key])
246
+ raise InvalidProcessManager,
247
+ "#{@name} declares two transitions on #{handler.event_type.inspect} from " \
248
+ "#{handler.from_state.inspect} (=> #{earlier.to_state.inspect} and => " \
249
+ "#{handler.to_state.inspect}) — a leg is selected by (event, current state), so " \
250
+ "only one may answer"
251
+ end
252
+ seen[key] = handler
253
+ end
214
254
  end
215
255
 
256
+ # THE BODY OF ONE `transition ... do ... end` block — collects the
257
+ # `dispatch` calls (each optionally opening its own `compensates`
258
+ # via the nested `DispatchBuilder`) that fire when this transition
259
+ # is taken.
216
260
  class HandlerBuilder
217
- GRAMMAR_CONTEXT = "Handler"
261
+ GRAMMAR_CONTEXT = "Handler".freeze
218
262
 
219
263
  attr_reader :dispatches
220
264
 
@@ -282,7 +326,7 @@ module Hecks
282
326
  # any saga dispatch already does
283
327
  # (`SagaInterpreter#dispatch_args`).
284
328
  class DispatchBuilder
285
- GRAMMAR_CONTEXT = "Dispatch"
329
+ GRAMMAR_CONTEXT = "Dispatch".freeze
286
330
 
287
331
  include WordGate
288
332
 
@@ -2,8 +2,14 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses a `query "Name" do ... end` block, declared on an aggregate
6
+ # or one of its entities, into a `Query` — its own parameters (plain
7
+ # attributes, including reference-typed ones) plus whatever `where`/
8
+ # `order_by`/`limit`/etc. `QuerySpecification::Common::DSL` contributes.
9
+ # A block parameter naming an already-declared owner attribute has its
10
+ # type derived from the owner rather than restated (`derive_from_owner!`).
5
11
  class QueryBuilder
6
- GRAMMAR_CONTEXT = "Query"
12
+ GRAMMAR_CONTEXT = "Query".freeze
7
13
 
8
14
  include AttributeCollector
9
15
  include QuerySpecification::Common::DSL
@@ -2,8 +2,14 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses a `read_model "Name" do ... end` block into a `ReadModel` — a
6
+ # cross-aggregate projection built from an optional `reference_to`
7
+ # root (omitted, it's a rootless "bulk" read model) plus one or more
8
+ # `include`d aggregate heads, with `where`/`order_by`/`limit`/etc, and
9
+ # at most one of `group_by`, `count`, or `median` as its single
10
+ # reduction over the one eligible many-side collection.
5
11
  class ReadModelBuilder
6
- GRAMMAR_CONTEXT = "ReadModel"
12
+ GRAMMAR_CONTEXT = "ReadModel".freeze
7
13
 
8
14
  include QuerySpecification::Common::DSL
9
15
  include WordGate
@@ -48,6 +54,72 @@ module Hecks
48
54
  @includes << [Naming.demodulise(type), as]
49
55
  end
50
56
 
57
+ # `on:` (ADR 0055) — OVERRIDES of `QuerySpecification::Common::DSL`'s
58
+ # shared `where_impl`/`order_by_impl`/`limit_impl`/`offset_impl`,
59
+ # scoped to `ReadModelBuilder` alone rather than added to the shared
60
+ # module `Query` also mixes in: a plain `query` has no
61
+ # `aggregate_heads` at all, so `on:` there would be a silently-
62
+ # ignored no-op rather than a real answer. Overriding only here
63
+ # means a `Query`'s own `where(..., on: X)` gets Ruby's own loud
64
+ # `unknown keyword: :on` instead of quietly doing nothing.
65
+ #
66
+ # `on:` names the target by TYPE (`on: Character`), resolved the
67
+ # same way `reference_to`/`include` already resolve their own type
68
+ # argument (`Naming.demodulise`) — not by the include's own `as:`
69
+ # alias. A read model that `include`s the SAME type twice under two
70
+ # different `as:` has no way to say which one `on:` means today; no
71
+ # real corpus read model does this, so it's a real, deliberate scope
72
+ # limit (see ADR 0055), not an oversight.
73
+ #
74
+ # `*positional, on:, **rest` rather than a plain `(clauses, on: nil)`
75
+ # — found necessary, not stylistic, by reproducing the failure
76
+ # directly: `where(status: "disputed")` reaches here with its
77
+ # `status: "disputed"` captured as `**kwargs` (GenericDispatch's own
78
+ # `builder.send(calls, *args, **kwargs, &block)`), and Ruby stops
79
+ # auto-converting a bare `**hash` call into a plain positional Hash
80
+ # THE MOMENT a method declares any real keyword parameter — so a
81
+ # `(clauses, on: nil)` signature raised "wrong number of arguments
82
+ # (given 0, expected 1)" on every ordinary `where(field: value)`
83
+ # call, never reaching `on:` at all. `**rest` sidesteps this: Ruby
84
+ # still auto-splits `on:` into the declared keyword and gathers
85
+ # every OTHER key into `rest` regardless of how the caller wrote it.
86
+ #
87
+ # `QuerySpecification::Common::WhereClause` etc — FULLY QUALIFIED,
88
+ # not the bare names `dsl.rb`'s own shared `where_impl` gets away
89
+ # with. That file is lexically nested inside `Common` itself, so
90
+ # `WhereClause` resolves directly; this class is nested inside
91
+ # `Bluebook::DSL`, which has no lexical or ancestor path to
92
+ # `QuerySpecification::Common` at all — a bare `WhereClause` here
93
+ # falls through to `const_missing` and, mid-bluebook-load, that's
94
+ # `ConstShim`, which resolves it against the self-hosted grammar
95
+ # domain's OWN unrelated `WhereClause` construct instead (a `Module`,
96
+ # not this `Struct`) — found directly by reproducing "undefined
97
+ # method `new' for module WhereClause" against a real corpus load,
98
+ # not guessed.
99
+ def where_impl(*positional, on: nil, **rest)
100
+ raise ArgumentError, "wrong number of arguments (given #{positional.size}, expected 1)" if positional.size > 1
101
+
102
+ @wheres ||= []
103
+ target = resolve_target(on)
104
+ clauses = (positional.first || {}).merge(rest)
105
+ clauses.each do |field, value|
106
+ op, operand = split_comparator(value)
107
+ @wheres << QuerySpecification::Common::WhereClause.new(field: field, op: op, value: operand, target: target)
108
+ end
109
+ end
110
+
111
+ def order_by_impl(field, direction = :asc, on: nil)
112
+ @order_by = QuerySpecification::Common::OrderBy.new(field: field, direction: direction, target: resolve_target(on))
113
+ end
114
+
115
+ def limit_impl(value, on: nil)
116
+ @limit = QuerySpecification::Common::LimitSpec.new(value: value, target: resolve_target(on))
117
+ end
118
+
119
+ def offset_impl(value, on: nil)
120
+ @offset = QuerySpecification::Common::OffsetSpec.new(value: value, target: resolve_target(on))
121
+ end
122
+
51
123
  # NAMES which of the eligible head's own fields to nest its rows
52
124
  # under — one level per field, the leaf being that row with the
53
125
  # named fields removed (they're already spent, as the keys that
@@ -107,8 +179,10 @@ module Hecks
107
179
  # field values) has no root to speak of. Still needs to describe
108
180
  # SOMETHING — zero includes AND no reference is refused.
109
181
  def build
110
- raise Malformed,
111
- "#{@name} needs an aggregate-head reference or at least one include" if !@reference_target && Array(@includes).empty?
182
+ if !@reference_target && Array(@includes).empty?
183
+ raise Malformed,
184
+ "#{@name} needs an aggregate-head reference or at least one include"
185
+ end
112
186
 
113
187
  Array(@includes).each do |target, as|
114
188
  add_aggregate_head(target, as, many: target != @reference_target)
@@ -134,27 +208,70 @@ module Hecks
134
208
 
135
209
  private
136
210
 
137
- # where/order_by/limit/offset/authorize's tenant all apply to exactly
138
- # one collection — the single `include`d aggregate whose head is
139
- # "many" (the "one" side, the reference target itself, is a single
140
- # row; ordering, paging, or tenant-scoping one row means nothing). A
141
- # read model with zero many-heads has nothing for them to filter ;
142
- # one with several has no way to say WHICH of several unrelated
143
- # collections a caller meant — so both refuse here rather than
144
- # silently applying to an arbitrary one. `authorize`'s tenant counts
145
- # here too TenantScope enforces it against this same head, so an
146
- # ambiguous target is exactly as unusable as it is for the others.
211
+ # where/order_by/limit/offset/authorize's tenant all apply to
212
+ # collections — the `include`d aggregates whose heads are "many"
213
+ # (the "one" side, the reference target itself, is a single row;
214
+ # ordering, paging, or tenant-scoping one row means nothing). ADR
215
+ # 0055 gave `where`/`order_by`/`limit`/`offset` an `on:` to name
216
+ # WHICH many-side collection they mean, so this asks two questions
217
+ # now instead of one:
218
+ #
219
+ # 1. Does every declared `on:` actually name a many-side included
220
+ # aggregate? Checked regardless of how many many-side heads
221
+ # exist — a typo refuses immediately, not only once ambiguity
222
+ # would otherwise bite.
223
+ # 2. Is there still an UNTARGETED option declared (including
224
+ # `authorize`'s own `tenant:`, which has no `on:` of its own —
225
+ # a real, deliberate scope limit, see ADR 0055)? An untargeted
226
+ # option still needs exactly one many-side head to mean
227
+ # anything unambiguous — the ORIGINAL rule, unchanged, and
228
+ # still worded the same way (`spec/runtime/
229
+ # read_model_interpreter_spec.rb`'s existing refusal regex
230
+ # still matches).
231
+ #
232
+ # A read model with several many-side heads is legal precisely when
233
+ # every declared option names one; a read model with a single
234
+ # many-side head is unaffected either way, `on:` or not.
147
235
  def seal_query_options
148
- declared = @wheres&.any? || @order_by || @limit || @offset || @authorization&.tenant
149
- return unless declared
236
+ many = Array(@aggregate_heads).select { |head| head[:many] }
150
237
 
151
- many = Array(@aggregate_heads).count { |head| head[:many] }
152
- return if many == 1
238
+ validate_declared_targets!(many)
239
+ return unless untargeted_option_declared?
240
+ return if many.size == 1
153
241
 
154
242
  raise Malformed,
155
- "#{@name} declares where/order_by/limit/offset but includes #{many} many-side " \
243
+ "#{@name} declares where/order_by/limit/offset but includes #{many.size} many-side " \
156
244
  "aggregates, not exactly one — these options apply to a single collection; " \
157
- "name which one by including only it, or drop the options"
245
+ "name which one with `on:` (e.g. `where(field: value, on: Character)`), or drop the options"
246
+ end
247
+
248
+ # Question 1 of `seal_query_options`'s own two, split out to keep
249
+ # both under the same "one job per method" shape every OTHER seal in
250
+ # this file already holds to (each raises its own one Malformed, for
251
+ # its own one reason).
252
+ def validate_declared_targets!(many)
253
+ many_by_aggregate = many.to_h { |head| [head[:aggregate], head] }
254
+ declared_targets = Array(@wheres).map(&:target) + [@order_by&.target, @limit&.target, @offset&.target]
255
+
256
+ declared_targets.compact.uniq.each do |target|
257
+ next if many_by_aggregate.key?(target)
258
+
259
+ raise Malformed,
260
+ "#{@name}'s `on: #{target}` doesn't name one of its own many-side included " \
261
+ "aggregates (it includes #{many.map { |head| head[:aggregate] }.join(', ')} as " \
262
+ "many-side heads)"
263
+ end
264
+ end
265
+
266
+ # Question 2 of `seal_query_options`'s own two — see that method's
267
+ # header. `authorize`'s own `tenant:` has no `on:` at all (ADR 0055's
268
+ # own documented scope limit), so it always counts as untargeted.
269
+ def untargeted_option_declared?
270
+ Array(@wheres).any? { |where| where.target.nil? } ||
271
+ (@order_by && @order_by.target.nil?) ||
272
+ (@limit && @limit.target.nil?) ||
273
+ (@offset && @offset.target.nil?) ||
274
+ @authorization&.tenant
158
275
  end
159
276
 
160
277
  # Same shape as `seal_query_options`, same reason — `group_by`
@@ -219,6 +336,12 @@ module Hecks
219
336
  "pagination — use limit/offset instead"
220
337
  end
221
338
 
339
+ # `on:`'s own resolution (ADR 0055) — same demodulise `reference_to`/
340
+ # `include` already use for their own type argument. `nil` when `on:`
341
+ # is omitted, matching every other optional field's "absent, not
342
+ # false" reading in this file.
343
+ def resolve_target(on) = on && Naming.demodulise(on)
344
+
222
345
  def add_aggregate_head(type, name, many:)
223
346
  @aggregate_heads ||= []
224
347
  target = Naming.demodulise(type)
@@ -59,7 +59,9 @@ module Hecks
59
59
  "extraction — #{extraction_failure}"
60
60
  end
61
61
 
62
- struct_class.new(description: description, canonical: canonical, predicate: predicate)
62
+ ast = Expression::AstJson.refuse_unshared_patterns!(Expression::AstJson.emit_predicate(canonical),
63
+ owner: owner_name, word: "#{word} #{description.inspect}")
64
+ struct_class.new(description: description, canonical: canonical, predicate: predicate, ast: ast)
63
65
  end
64
66
 
65
67
  # PRIMITIVE 1 — an ORDERED CHAIN of flat `Hash[description] =>
@@ -2,8 +2,14 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses an `aggregate "Name" do ... end` block inside a `.translation`
6
+ # file into a `TranslationAggregate` — the per-aggregate rules
7
+ # (`rename`/`move`/`convert`/`retype`/`compute`/`rekey`/`backfill`/
8
+ # `drop`) that carry one era's stored data forward to the next, plus
9
+ # `unresolved` markers the scaffold writes wherever it cannot decide a
10
+ # rule for itself.
5
11
  class TranslationAggregateBuilder
6
- GRAMMAR_CONTEXT = "TranslationAggregate"
12
+ GRAMMAR_CONTEXT = "TranslationAggregate".freeze
7
13
 
8
14
  include WordGate
9
15
 
@@ -183,8 +189,13 @@ module Hecks
183
189
  end
184
190
  end
185
191
 
192
+ # Parses a whole `.translation` file into a `Translation` — the
193
+ # domain's own `from:`/`to:` era pair, its list of `aggregate`
194
+ # translation blocks (each built by `TranslationAggregateBuilder`
195
+ # above), and any `retired` aggregates that no longer exist in the
196
+ # destination era.
186
197
  class TranslationBuilder
187
- GRAMMAR_CONTEXT = "Translation"
198
+ GRAMMAR_CONTEXT = "Translation".freeze
188
199
 
189
200
  include WordGate
190
201
 
@@ -2,8 +2,15 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # Parses a `value_object "Name" do ... end` block into a
6
+ # `ValueObject` — its attributes, its own invariants (declared once
7
+ # and referenceable by name from sibling value objects on the same
8
+ # aggregate, `RuleReference`), and its `member` rows when it's a
9
+ # closed set (either bare `one_of` member lines, or the single-field
10
+ # `attribute ..., one_of: [...]` shorthand `install_inline_closed_set`
11
+ # installs).
5
12
  class ValueObjectBuilder
6
- GRAMMAR_CONTEXT = "ValueObject"
13
+ GRAMMAR_CONTEXT = "ValueObject".freeze
7
14
 
8
15
  include AttributeCollector
9
16
  include RuleReference
@@ -150,6 +150,11 @@ module Hecks
150
150
  # admitted-but-unimplemented, or admitted-somewhere-else-only —
151
151
  # those are real, useful refusals regardless of which
152
152
  # `method_missing` is asking.
153
+ # One ordered admission-then-dispatch pipeline (own context, then
154
+ # "Type" fallback, then admitted-elsewhere check, then dispatch) —
155
+ # see this method's own header comment above for the full,
156
+ # order-dependent account of why each check runs where it does.
157
+ # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
153
158
  def word_gate_dispatch(word, args, kwargs, block)
154
159
  context = self.class::GRAMMAR_CONTEXT
155
160
  rows = MetaValidator::SyntaxBoot.call
@@ -2,10 +2,15 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # A generic keyword-call sink for one bind's own settings block (e.g.
6
+ # `persisted_by "Heki" do dir :default end`) — every method call made
7
+ # on it inside the block is recorded verbatim by name, with no fixed
8
+ # vocabulary of its own; `WorldBuilder#record_binding` reads `#to_h`
9
+ # back out.
5
10
  class SettingsCollector
6
11
  def initialize = @values = {}
7
12
 
8
- def method_missing(key, *args, &_block)
13
+ def method_missing(key, *args, &)
9
14
  @values[key.to_sym] = args.size == 1 ? args.first : args
10
15
  end
11
16
 
@@ -38,8 +43,13 @@ module Hecks
38
43
  def respond_to_missing?(_name, _include_private = false) = true
39
44
  end
40
45
 
46
+ # Parses a `.world` file's top-level DSL block into a `World` — a
47
+ # domain's own `realm`/`latest` version markers plus its adapter bind
48
+ # SETTINGS, one entry per `verb("Adapter") do ... end` call (whether
49
+ # written bare or aggregate-qualified through `WorldConstProxy`'s
50
+ # visual mirror of a sibling `.hecksagon` file's own bind).
41
51
  class WorldBuilder
42
- GRAMMAR_CONTEXT = "World"
52
+ GRAMMAR_CONTEXT = "World".freeze
43
53
 
44
54
  include WordGate
45
55
 
@@ -107,7 +117,7 @@ module Hecks
107
117
 
108
118
  private
109
119
 
110
- def required(value, label)
120
+ def required(value, _label)
111
121
  # moved to the language: Realm / Latest invariants, in world.bluebook
112
122
  value.to_s
113
123
  end
@@ -1,4 +1,5 @@
1
1
  require_relative "behaviour/entity"
2
+ require_relative "expression/ast_json"
2
3
 
3
4
  module Hecks
4
5
  module Bluebook
@@ -51,7 +52,7 @@ module Hecks
51
52
  # documentation of what the piece itself declared, the same
52
53
  # relationship `Aggregate.preconditions` already has to its own
53
54
  # commands.
54
- preconditions: -> { preconditions.map { |rule| { description: rule.description, canonical: rule.canonical } } },
55
+ preconditions: -> { preconditions.map { |rule| Expression::AstJson.rule_row(rule) } },
55
56
  # A piece's OWN shape rule, checked against EVERY instance of
56
57
  # this piece the aggregate holds (Admissibility#enforce_
57
58
  # invariants' own recursive walk) — the SAME relationship
@@ -61,7 +62,7 @@ module Hecks
61
62
  # mutation, before save) the aggregate's own invariants always
62
63
  # were — see that method's own comment for why this does not
63
64
  # contradict "there is no separate entity invariant."
64
- invariants: -> { invariants.map { |rule| { description: rule.description, canonical: rule.canonical } } },
65
+ invariants: -> { invariants.map { |rule| Expression::AstJson.rule_row(rule) } },
65
66
  lifecycle: one(:lifecycle)
66
67
  )
67
68
 
@@ -84,7 +85,8 @@ module Hecks
84
85
  # Assigns what the language declares, then hands off to the
85
86
  # behaviour's own `settle` — derived identity and the name
86
87
  # indexes, neither of which the declaration states.
87
- def absorb(description:, identified_by:, attributes:, commands:, queries:, entities:, preconditions:, invariants:, lifecycle:)
88
+ def absorb(description:, identified_by:, attributes:, commands:, queries:, entities:, preconditions:, invariants:,
89
+ lifecycle:)
88
90
  @description = description
89
91
  @identified_by = identified_by
90
92
  @attributes = attributes
@@ -42,8 +42,8 @@ module Hecks
42
42
  # `expr_emitter.rb`'s own `emit_bool`/`emit_resolver` already hold
43
43
  # to — even though, as of this writing, no real corpus VALUE OBJECT
44
44
  # invariant exercises `Include`/`Modulo`/`BlockPredicate`/`Find`/
45
- # `Array`/`MatchesRegex`/`Presence`/`Split`/`StartsWith`/`EndsWith`/
46
- # `First`/`Last` (only `given`/`ensures` clauses do, elsewhere in
45
+ # `Array`/`MatchesRegex`/`Presence`/`Assignment`/`Split`/`StartsWith`/
46
+ # `EndsWith`/`First`/`Last` (only `given`/`ensures` clauses do, elsewhere in
47
47
  # the corpus — a different consumer of this same grammar).
48
48
  # `rust/host/src/expr_json.rs`'s own header names exactly which of
49
49
  # these its interpreter evaluates for real today versus refuses
@@ -56,10 +56,75 @@ module Hecks
56
56
  module AstJson
57
57
  module_function
58
58
 
59
+ # THE CLOSED OP ROSTER — every `"op"` tag the walkers below can
60
+ # emit, pinned so a reader (or a spec) can refuse a tag it does
61
+ # not know instead of guessing. A new node kind is a new entry
62
+ # here, a new arm below, and a new arm in every reader.
63
+ OPS = %w[
64
+ or and not compare include
65
+ int float str bool nil array lookup
66
+ add modulo sign_test empty size to_s
67
+ block_predicate find first last
68
+ matches_regex presence assignment split starts_with ends_with
69
+ ].freeze
70
+
71
+ # ONE RULE ROW, THE WAY EVERY RULE SITE EMITS IT — description and
72
+ # canonical text (what every reader has always had) plus the
73
+ # structured form, derived from the same text. `ast` is a pure
74
+ # function of `canonical`: the IR carries both so a reader that
75
+ # only displays keeps the text, and a reader that evaluates never
76
+ # re-parses it.
77
+ def rule_row(rule)
78
+ { description: rule.description, canonical: rule.canonical, ast: rule.ast || emit_predicate(rule.canonical) }
79
+ end
80
+
59
81
  def emit_predicate(canonical)
60
82
  emit_bool(Evaluator.parse(canonical))
61
83
  end
62
84
 
85
+ # C3.6 (docs/semantics/bluebook-semantics.md) — every `.match?`
86
+ # pattern a rule carries is held to `PatternSubset`, exactly as an
87
+ # attribute's own `pattern:` already is (`AttributeCollector#
88
+ # refuse_unshared_pattern`): a regex whose meaning depends on the
89
+ # engine reading it is a defect in the bluebook, refused at build.
90
+ # Walks the emitted AST, so every rule site (givens, ensures,
91
+ # invariants, preconditions, a policy's where) gets the one check.
92
+ def refuse_unshared_patterns!(ast, owner:, word:)
93
+ return ast if Hecks::Bluebook::MetaValidator.shadow_parsing? # frozen era text is history
94
+
95
+ each_node(ast) do |node|
96
+ next unless node["op"] == "matches_regex"
97
+
98
+ rejection = PatternSubset.validate(node["pattern"])
99
+ next unless rejection
100
+
101
+ raise DSL::Malformed,
102
+ "#{owner}'s #{word} matches against #{node['pattern'].inspect}, which uses a " \
103
+ "#{rejection.construct} — #{rejection.reason}"
104
+ end
105
+ ast
106
+ end
107
+
108
+ # Every name a rule resolves at its root — the first segment of
109
+ # each `lookup` path, unique, in first-seen order.
110
+ def lookup_heads(ast)
111
+ heads = []
112
+ each_node(ast) do |node|
113
+ heads << node["path"].first.to_s if node["op"] == "lookup" && node["path"].is_a?(::Array)
114
+ end
115
+ heads.uniq
116
+ end
117
+
118
+ def each_node(node, &block)
119
+ case node
120
+ when ::Hash
121
+ yield node
122
+ node.each_value { |child| each_node(child, &block) }
123
+ when ::Array
124
+ node.each { |child| each_node(child, &block) }
125
+ end
126
+ end
127
+
63
128
  def emit_bool(node)
64
129
  case node
65
130
  when Evaluator::Or then { "op" => "or", "left" => emit_bool(node.left), "right" => emit_bool(node.right) }
@@ -73,12 +138,13 @@ module Hecks
73
138
  when Evaluator::Resolve
74
139
  emit_resolver(node.expr)
75
140
  else
76
- raise "unhandled evaluator node #{node.class} — no JSON rendering exists for it (lib/hecks/bluebook/expression/ast_json.rb#emit_bool)"
141
+ raise "unhandled evaluator node #{node.class} — no JSON rendering exists for it " \
142
+ "(lib/hecks/bluebook/expression/ast_json.rb#emit_bool)"
77
143
  end
78
144
  end
79
145
 
80
- def emit_comparison(op)
81
- { "less_than" => op.compares_less_than, "equal" => op.compares_equal, "negated" => op.negated }
146
+ def emit_comparison(comparator)
147
+ { "less_than" => comparator.compares_less_than, "equal" => comparator.compares_equal, "negated" => comparator.negated }
82
148
  end
83
149
 
84
150
  # The JSON-target sibling of `expr_emitter.rb`'s own
@@ -99,11 +165,18 @@ module Hecks
99
165
  return { "op" => "bool", "value" => false } if node.haystack.elements.empty?
100
166
 
101
167
  equalities = node.haystack.elements.map do |element|
102
- { "op" => "compare", "cmp" => emit_comparison(EQ), "left" => emit_resolver(node.needle), "right" => emit_resolver(element) }
168
+ { "op" => "compare", "cmp" => emit_comparison(EQ), "left" => emit_resolver(node.needle),
169
+ "right" => emit_resolver(element) }
103
170
  end
104
171
  equalities.reduce { |left, right| { "op" => "or", "left" => left, "right" => right } }
105
172
  end
106
173
 
174
+ # One case arm per Resolver node type — the class header above is
175
+ # explicit that this dispatch must stay COMPLETE and in one place
176
+ # ("every node this grammar admits gets a real arm"); splitting it
177
+ # into several methods would hide whether the set is still
178
+ # exhaustive instead of making that visible at a glance.
179
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
107
180
  def emit_resolver(node)
108
181
  case node
109
182
  when Resolver::IntegerLiteral then { "op" => "int", "value" => node.value }
@@ -111,7 +184,9 @@ module Hecks
111
184
  when Resolver::StringLiteral then { "op" => "str", "value" => node.value }
112
185
  when Resolver::BoolLiteral then { "op" => "bool", "value" => node.value }
113
186
  when Resolver::NilLiteral then { "op" => "nil" }
114
- when Resolver::Lookup then { "op" => "lookup", "path" => node.path }
187
+ # `path` is the SAME shape `find.path` already has — segments, not
188
+ # a dotted string a reader would have to split by its own rule.
189
+ when Resolver::Lookup then { "op" => "lookup", "path" => node.path.split(".") }
115
190
  when Resolver::Addition then { "op" => "add", "left" => emit_resolver(node.left), "right" => emit_resolver(node.right) }
116
191
  when Resolver::SignTest
117
192
  { "op" => "sign_test", "cmp" => emit_comparison(node.operator), "receiver" => emit_resolver(node.receiver) }
@@ -128,9 +203,12 @@ module Hecks
128
203
  when Resolver::ArrayLiteral
129
204
  { "op" => "array", "elements" => node.elements.map { |element| emit_resolver(element) } }
130
205
  when Resolver::MatchesRegex
131
- { "op" => "matches_regex", "receiver" => emit_resolver(node.receiver), "pattern" => node.pattern, "flags" => node.flags }
206
+ { "op" => "matches_regex", "receiver" => emit_resolver(node.receiver), "pattern" => node.pattern,
207
+ "flags" => node.flags }
132
208
  when Resolver::Presence
133
209
  { "op" => "presence", "receiver" => emit_resolver(node.receiver), "negated" => node.negated }
210
+ when Resolver::Assignment
211
+ { "op" => "assignment", "receiver" => emit_resolver(node.receiver), "negated" => node.negated }
134
212
  when Resolver::Split
135
213
  { "op" => "split", "receiver" => emit_resolver(node.receiver), "separator" => node.separator }
136
214
  when Resolver::StartsWith
@@ -140,7 +218,8 @@ module Hecks
140
218
  when Resolver::First then { "op" => "first", "receiver" => emit_resolver(node.receiver) }
141
219
  when Resolver::Last then { "op" => "last", "receiver" => emit_resolver(node.receiver) }
142
220
  else
143
- raise "unhandled resolver node #{node.class} — no JSON rendering exists for it (lib/hecks/bluebook/expression/ast_json.rb#emit_resolver)"
221
+ raise "unhandled resolver node #{node.class} — no JSON rendering exists for it " \
222
+ "(lib/hecks/bluebook/expression/ast_json.rb#emit_resolver)"
144
223
  end
145
224
  end
146
225
  end