hecks 0.3.0 → 1.0.1

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 (202) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/d1.rb +38 -14
  3. data/lib/hecks/adapters/driven/governance_authorization.rb +41 -2
  4. data/lib/hecks/adapters/driven/heki/journal.rb +7 -2
  5. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  6. data/lib/hecks/adapters/driven/heki/snapshot.rb +31 -4
  7. data/lib/hecks/adapters/driven/heki.rb +40 -9
  8. data/lib/hecks/adapters/driven/lambda.rb +16 -2
  9. data/lib/hecks/adapters/driven/postgres/codec.rb +8 -0
  10. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +45 -6
  11. data/lib/hecks/adapters/driven/postgres.rb +76 -29
  12. data/lib/hecks/adapters/driven/postgres_era.adapter +11 -0
  13. data/lib/hecks/adapters/driven/sql_query_builder.rb +1 -1
  14. data/lib/hecks/adapters/driven/sqlite/codec.rb +17 -0
  15. data/lib/hecks/adapters/driven/sqlite/projection.rb +76 -9
  16. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +17 -5
  17. data/lib/hecks/adapters/driven/sqlite.rb +30 -9
  18. data/lib/hecks/adapters/driven.rb +19 -1
  19. data/lib/hecks/behaviors/dsl.rb +29 -0
  20. data/lib/hecks/behaviors/expectations.rb +62 -2
  21. data/lib/hecks/bluebook/assembly/contracts.rb +36 -7
  22. data/lib/hecks/bluebook/assembly/marks.rb +4 -3
  23. data/lib/hecks/bluebook/assembly.rb +14 -1
  24. data/lib/hecks/bluebook/behaviour/lifecycle.rb +18 -1
  25. data/lib/hecks/bluebook/behaviour/process_manager.rb +14 -1
  26. data/lib/hecks/bluebook/behaviour/value_object.rb +13 -7
  27. data/lib/hecks/bluebook/chapter.rb +21 -11
  28. data/lib/hecks/bluebook/command.rb +1 -1
  29. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +117 -5
  30. data/lib/hecks/bluebook/dsl/attribute_collector.rb +21 -0
  31. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +71 -2
  32. data/lib/hecks/bluebook/dsl/command_builder.rb +144 -3
  33. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +49 -7
  34. data/lib/hecks/bluebook/dsl/entity_builder.rb +129 -4
  35. data/lib/hecks/bluebook/dsl/policy_builder.rb +18 -3
  36. data/lib/hecks/bluebook/dsl/port_builder.rb +12 -3
  37. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +109 -10
  38. data/lib/hecks/bluebook/dsl/rule_reference.rb +1 -0
  39. data/lib/hecks/bluebook/dsl/word_gate.rb +9 -2
  40. data/lib/hecks/bluebook/dsl/world_builder.rb +44 -4
  41. data/lib/hecks/bluebook/expression/ast_json.rb +149 -0
  42. data/lib/hecks/bluebook/expression/canonical_form.rb +71 -3
  43. data/lib/hecks/bluebook/expression/evaluator.rb +50 -7
  44. data/lib/hecks/bluebook/expression/projection.json +48 -0
  45. data/lib/hecks/bluebook/expression/resolver.rb +161 -10
  46. data/lib/hecks/bluebook/expression.rb +1 -0
  47. data/lib/hecks/bluebook/hexagon.rb +1 -1
  48. data/lib/hecks/bluebook/meta_validator/judge.rb +78 -16
  49. data/lib/hecks/bluebook/meta_validator/port_judge.rb +4 -0
  50. data/lib/hecks/bluebook/meta_validator/readings.rb +14 -4
  51. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +42 -3
  52. data/lib/hecks/bluebook/meta_validator/shapes.rb +30 -10
  53. data/lib/hecks/bluebook/meta_validator.rb +103 -13
  54. data/lib/hecks/bluebook/model_check.rb +147 -6
  55. data/lib/hecks/bluebook/pattern_subset.rb +66 -2
  56. data/lib/hecks/bluebook/process_manager.rb +53 -11
  57. data/lib/hecks/bluebook/project_register.rb +25 -0
  58. data/lib/hecks/bluebook/value_object.rb +19 -2
  59. data/lib/hecks/doc/reference.rb +22 -1
  60. data/lib/hecks/facade/cli_door.rb +6 -3
  61. data/lib/hecks/facade/json_door.rb +16 -4
  62. data/lib/hecks/forms/app.rb +47 -6
  63. data/lib/hecks/forms/command_form_renderer.rb +1 -1
  64. data/lib/hecks/forms/field_renderer.rb +11 -4
  65. data/lib/hecks/forms/html.rb +31 -0
  66. data/lib/hecks/forms/params.rb +30 -1
  67. data/lib/hecks/forms/port_argument.rb +46 -0
  68. data/lib/hecks/forms/record_renderer.rb +6 -2
  69. data/lib/hecks/forms/record_table.rb +6 -1
  70. data/lib/hecks/framework/bluebook/console_settings.bluebook +19 -19
  71. data/lib/hecks/framework/bluebook/governance.bluebook +26 -11
  72. data/lib/hecks/framework/bluebook/identity.bluebook +2 -2
  73. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  74. data/lib/hecks/fuzzing/invalid_value_generator.rb +3 -2
  75. data/lib/hecks/fuzzing/isolated_boot.rb +212 -18
  76. data/lib/hecks/fuzzing/properties.rb +52 -6
  77. data/lib/hecks/fuzzing/replay.rb +51 -18
  78. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +28 -2
  79. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +71 -6
  80. data/lib/hecks/fuzzing/sequence_generator.rb +5 -4
  81. data/lib/hecks/fuzzing/value_generator.rb +25 -2
  82. data/lib/hecks/grammar/evolve.rb +33 -0
  83. data/lib/hecks/grammar/expression.bluebook +8 -5
  84. data/lib/hecks/grammar/expression_operators.json +497 -1
  85. data/lib/hecks/language/bluebook/aggregate.bluebook +12 -12
  86. data/lib/hecks/language/bluebook/bluebook.bluebook +3 -3
  87. data/lib/hecks/language/bluebook/command.bluebook +42 -8
  88. data/lib/hecks/language/bluebook/entity.bluebook +86 -10
  89. data/lib/hecks/language/bluebook/policy.bluebook +21 -4
  90. data/lib/hecks/language/bluebook/process_manager.bluebook +135 -18
  91. data/lib/hecks/language/bluebook/projection.bluebook +6 -6
  92. data/lib/hecks/language/bluebook/query.bluebook +4 -4
  93. data/lib/hecks/language/bluebook/shape.bluebook +6 -6
  94. data/lib/hecks/language/bluebook/syntax.bluebook +12 -11
  95. data/lib/hecks/language/bluebook/vocabulary.bluebook +22 -7
  96. data/lib/hecks/language/hecksagon/domain_port.bluebook +25 -1
  97. data/lib/hecks/language/oidc.json +20 -0
  98. data/lib/hecks/language/port.bluebook +30 -2
  99. data/lib/hecks/naming.rb +54 -1
  100. data/lib/hecks/ports/access_control.port +7 -2
  101. data/lib/hecks/ports/access_control.rb +1 -1
  102. data/lib/hecks/ports/agent.port +6 -2
  103. data/lib/hecks/ports/agent.rb +1 -1
  104. data/lib/hecks/ports/authentication.port +4 -2
  105. data/lib/hecks/ports/authentication.rb +1 -1
  106. data/lib/hecks/ports/authorization.port +5 -2
  107. data/lib/hecks/ports/authorization.rb +14 -11
  108. data/lib/hecks/ports/clock.port +3 -2
  109. data/lib/hecks/ports/clock.rb +1 -1
  110. data/lib/hecks/ports/extraction.port +3 -2
  111. data/lib/hecks/ports/extraction.rb +1 -1
  112. data/lib/hecks/ports/identity_assignment.port +3 -2
  113. data/lib/hecks/ports/identity_assignment.rb +1 -1
  114. data/lib/hecks/ports/identity_generation.port +3 -2
  115. data/lib/hecks/ports/identity_generation.rb +1 -1
  116. data/lib/hecks/ports/identity_resolution.port +3 -2
  117. data/lib/hecks/ports/identity_resolution.rb +1 -1
  118. data/lib/hecks/ports/persistence/append_only.rb +40 -4
  119. data/lib/hecks/ports/persistence/execution.rb +6 -1
  120. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  121. data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_check.rb +41 -8
  122. data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard.rb +24 -56
  123. data/lib/hecks/ports/persistence/{lineage.rb → plugins/era/lineage.rb} +31 -4
  124. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/era_store.rb +3 -3
  125. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/head_compiler.rb +59 -10
  126. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/mint_transaction.rb +2 -2
  127. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/provisioning.rb +29 -1
  128. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/tail_merge.rb +11 -4
  129. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/transform_installer.rb +20 -0
  130. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage.rb +1 -1
  131. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/coverage_check.rb +5 -5
  132. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/era_resolver.rb +5 -2
  133. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/merge_coordinator.rb +2 -2
  134. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager/minter.rb +4 -4
  135. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage_manager.rb +2 -2
  136. data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era.rb +116 -29
  137. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/approval_digest.rb +1 -1
  138. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_one.rb +14 -5
  139. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/layer_two.rb +31 -6
  140. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit/unfed_report.rb +5 -1
  141. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/reattest.rb +3 -3
  142. data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/differ.rb +1 -1
  143. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  144. data/lib/hecks/ports/persistence.rb +1 -1
  145. data/lib/hecks/ports/projection.rb +20 -0
  146. data/lib/hecks/projections/diagrams.rb +230 -1
  147. data/lib/hecks/projections/shape.rb +15 -2
  148. data/lib/hecks/projector/cli_projector.rb +10 -1
  149. data/lib/hecks/projector/exporter.rb +19 -2
  150. data/lib/hecks/query_ir.rb +19 -0
  151. data/lib/hecks/query_specification/common/null_policy.rb +13 -1
  152. data/lib/hecks/query_specification/field_path.rb +20 -2
  153. data/lib/hecks/query_specification/hop_path.rb +7 -5
  154. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  155. data/lib/hecks/runtime/boot_gates.rb +41 -0
  156. data/lib/hecks/runtime/caller.rb +19 -3
  157. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +13 -2
  158. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +12 -0
  159. data/lib/hecks/runtime/command_interpreter.rb +165 -34
  160. data/lib/hecks/runtime/command_rules/admissibility.rb +101 -16
  161. data/lib/hecks/runtime/command_rules/arithmetic.rb +40 -4
  162. data/lib/hecks/runtime/command_rules/authorization.rb +2 -1
  163. data/lib/hecks/runtime/command_rules/references.rb +27 -19
  164. data/lib/hecks/runtime/dependency_planning.rb +66 -5
  165. data/lib/hecks/runtime/dispatcher.rb +19 -4
  166. data/lib/hecks/runtime/entity_interpreter.rb +92 -14
  167. data/lib/hecks/runtime/errors.rb +22 -0
  168. data/lib/hecks/runtime/identity.rb +30 -2
  169. data/lib/hecks/runtime/instance.rb +59 -4
  170. data/lib/hecks/runtime/interpreting.rb +21 -0
  171. data/lib/hecks/runtime/loader.rb +59 -18
  172. data/lib/hecks/runtime/query_interpreter.rb +36 -4
  173. data/lib/hecks/runtime/reaction_invocation.rb +9 -1
  174. data/lib/hecks/runtime/read_model_interpreter.rb +89 -5
  175. data/lib/hecks/runtime/refusal_wording.rb +2 -0
  176. data/lib/hecks/runtime/registry/saga_persistence.rb +75 -3
  177. data/lib/hecks/runtime/registry/verification.rb +88 -0
  178. data/lib/hecks/runtime/registry.rb +69 -8
  179. data/lib/hecks/runtime/saga_interpreter.rb +215 -13
  180. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  181. data/lib/hecks/runtime/value/admission.rb +19 -1
  182. data/lib/hecks/runtime/value/coercion.rb +75 -10
  183. data/lib/hecks/runtime.rb +17 -5
  184. data/lib/hecks/storehouse.rb +632 -0
  185. data/lib/hecks/version.rb +1 -1
  186. data/lib/hecks/vocabulary.rb +6 -1
  187. data/lib/hecks.rb +28 -3
  188. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  189. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  190. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  191. metadata +49 -37
  192. /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_guard/shape_diff.rb +0 -0
  193. /data/lib/hecks/{runtime → ports/persistence/plugins/era}/era_tamper.rb +0 -0
  194. /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/field_cache.rb +0 -0
  195. /data/lib/hecks/{adapters/driven → ports/persistence/plugins/era}/postgres_era/lineage/resumable_backfill.rb +0 -0
  196. /data/lib/hecks/{runtime → ports/persistence/plugins/era}/storage_shape.rb +0 -0
  197. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/audit.rb +0 -0
  198. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/rule_compiler.rb +0 -0
  199. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/renderer.rb +0 -0
  200. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold/writer.rb +0 -0
  201. /data/lib/hecks/{translation → ports/persistence/plugins/era/translation}/scaffold.rb +0 -0
  202. /data/lib/hecks/{translation.rb → ports/persistence/plugins/era/translation.rb} +0 -0
@@ -11,7 +11,8 @@ module Hecks
11
11
  include WordGate
12
12
 
13
13
  def initialize(name, owner_value_objects: [], owner_named_givens: {},
14
- identity_name_prefix: nil, identity_value_object_installer: nil)
14
+ identity_name_prefix: nil, identity_value_object_installer: nil,
15
+ aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: [])
15
16
  @name = name
16
17
  @commands = []
17
18
  @queries = []
@@ -30,6 +31,18 @@ module Hecks
30
31
  # reference reads from it via `CommandBuilder#
31
32
  # reference_named_given`.
32
33
  @owner_named_givens = owner_named_givens
34
+ # ONE LEVEL WIDER STILL — the CHAPTER-WIDE, ENTITY-SCOPED pool
35
+ # (the piece analogue of `AggregateBuilder#@chapter_named_givens`,
36
+ # one level down). `@aggregate_name` names THIS piece's own
37
+ # root, so the write-through below can key itself
38
+ # "AggregateName.EntityName" — the same dotted addressing
39
+ # convention `declared_by:` already uses chapter-wide, one
40
+ # level up. See `#given_impl`'s own comment for what this
41
+ # closes and `docs/implemented/resolution-rules/
42
+ # chapter-entity-given.md` for the full algorithm.
43
+ @aggregate_name = aggregate_name || Naming.demodulise(name)
44
+ @chapter_entity_named_givens = chapter_entity_named_givens
45
+ @chapter_entity_pending_givens = chapter_entity_pending_givens
33
46
  # DEFERRED CONSTRUCTION — see `AggregateBuilder#drain_pending!`'s
34
47
  # own comment; the identical mechanism, one level down, so a
35
48
  # nested piece's own commands (Dispatch inside Handler) see
@@ -150,7 +163,40 @@ module Hecks
150
163
  # magic.
151
164
  # RENAMED FROM `given` — item #13's full metaprogrammed dispatch
152
165
  # (slice 4b), same reasoning as reference_to_impl above.
153
- def given_impl(description, &predicate)
166
+ #
167
+ # BARE — NO BLOCK — REFERENCES ANOTHER PIECE'S OWN DECLARATION,
168
+ # ANYWHERE IN THE CHAPTER, not just a sibling under this same
169
+ # aggregate — one level wider than round 4's own cross-entity
170
+ # sharing, mirroring `AggregateBuilder#given_impl`'s own
171
+ # chapter-wide shape exactly one level down. Real, live corpus
172
+ # this closes: `Account::LedgerEntry` and `SafeDepositBox::Visit`
173
+ # — two pieces under two DIFFERENT aggregates — independently
174
+ # typed `given("customer is active") { parent.customer.status ==
175
+ # "active" }` byte for byte; neither the aggregate-level chapter
176
+ # pool (a DIFFERENT canonical — bare `customer.status`, the
177
+ # wrong scope for a piece's own command) nor the existing
178
+ # same-aggregate cross-entity pool (`@owner_named_givens`, scoped
179
+ # to ONE aggregate's own entity tree) could reach across the
180
+ # aggregate boundary. Resolved against `@chapter_entity_named_
181
+ # givens`, keyed "AggregateName.EntityName" — see
182
+ # `#reference_named_chapter_entity_given`'s own comment for the
183
+ # algorithm and `docs/implemented/resolution-rules/
184
+ # chapter-entity-given.md` for the full write-up.
185
+ #
186
+ # `declared_by:` is a PLAIN STRING ("Account.LedgerEntry"), not a
187
+ # constant — unlike `AggregateBuilder#given_impl`'s own
188
+ # `declared_by:`, which names a real aggregate constant. A piece
189
+ # has no first-class, independently-addressable reference
190
+ # anywhere in this language (only its owning aggregate does);
191
+ # inventing one to make this ONE argument spelling symmetrical
192
+ # with the aggregate-level word is a real, separate, unscoped
193
+ # feature this fix does not need — ships textual now, the same
194
+ # way `admits:` shipped textual before its own constant-bridge
195
+ # existed, revisited only if a genuine, separate need for
196
+ # constant-addressed pieces shows up later.
197
+ def given_impl(description, declared_by: nil, &predicate)
198
+ return reference_named_chapter_entity_given(description, declared_by: declared_by) unless predicate
199
+
154
200
  named = build_rule(Given, description, predicate, owner_name: @name, word: "given",
155
201
  extraction_failure: "its source could not be read, so no other runtime could ever evaluate it")
156
202
  @named_givens[description] = named
@@ -163,6 +209,17 @@ module Hecks
163
209
  # by coincidence, same as an aggregate-level given already
164
210
  # tolerates today).
165
211
  @owner_named_givens[description] ||= named
212
+ # WRITE-THROUGH, PER OWNER — the chapter-wide analogue of the
213
+ # line above, keyed by [description, this piece's own dotted
214
+ # "Aggregate.Entity" name] rather than description alone, the
215
+ # identical reasoning `AggregateBuilder#given_impl`'s own
216
+ # chapter write-through gives: two DIFFERENT pieces (anywhere
217
+ # in the chapter) independently declaring the SAME description
218
+ # are two DISTINCT candidates a later bare reference chooses
219
+ # between (via `declared_by:` once there is more than one),
220
+ # never silently merged into one slot.
221
+ @chapter_entity_named_givens[description] ||= {}
222
+ @chapter_entity_named_givens[description]["#{@aggregate_name}.#{@name}"] ||= named
166
223
  end
167
224
 
168
225
  # A PIECE'S OWN SHAPE RULE (S10, ADR 0025's own "Rules" shape,
@@ -203,16 +260,81 @@ module Hecks
203
260
  end
204
261
 
205
262
  def self.build(name, owner_value_objects: [], owner_named_givens: {},
206
- identity_name_prefix: nil, identity_value_object_installer: nil, &block)
263
+ identity_name_prefix: nil, identity_value_object_installer: nil,
264
+ aggregate_name: nil, chapter_entity_named_givens: {}, chapter_entity_pending_givens: [], &block)
207
265
  builder = new(name, owner_value_objects: owner_value_objects, owner_named_givens: owner_named_givens,
208
266
  identity_name_prefix: identity_name_prefix,
209
- identity_value_object_installer: identity_value_object_installer)
267
+ identity_value_object_installer: identity_value_object_installer,
268
+ aggregate_name: aggregate_name,
269
+ chapter_entity_named_givens: chapter_entity_named_givens,
270
+ chapter_entity_pending_givens: chapter_entity_pending_givens)
210
271
  builder.instance_eval(&block) if block
211
272
  builder.build
212
273
  end
213
274
 
214
275
  private
215
276
 
277
+ # PRIMITIVE 2 (RuleReference#resolve_owner_keyed) — the CHAPTER-
278
+ # WIDE, ENTITY-SCOPED analogue of `AggregateBuilder#
279
+ # reference_named_chapter_given`; the three branches below are
280
+ # this construct's OWN refusal wording, not shared, matching that
281
+ # method's own precedent (`declared_by:` only exists on `given`
282
+ # so far, at either scope). UNRESOLVED is deferred, not raised
283
+ # here — see `#pending_chapter_entity_given`, below.
284
+ #
285
+ # WRITES THROUGH TO `@owner_named_givens` TOO — not just
286
+ # `@named_givens` — or this piece resolving a description via the
287
+ # WIDER, chapter pool would leave the NARROWER, same-aggregate
288
+ # pool (`EntityBuilder#given_impl`'s own block-form write-through)
289
+ # never populated for this description, breaking any SIBLING
290
+ # piece's existing command-level bare reference
291
+ # (`CommandBuilder#reference_named_given`) that depends on it —
292
+ # real, live corpus: `SafeDepositBox::KeyIssuance.Return`'s own
293
+ # bare `given("customer is active")` resolves through
294
+ # `@owner_named_givens`, populated by `Visit`'s declaration
295
+ # whether `Visit` types the predicate itself OR (now) references
296
+ # `Account::LedgerEntry`'s instead — this write keeps that
297
+ # working unchanged either way, `||=` so nothing here overrides
298
+ # an actual local declaration if one is ever added later.
299
+ def reference_named_chapter_entity_given(description, declared_by:)
300
+ verify_resolves_via!("given", "Entity", "owner_keyed")
301
+ candidates = resolve_owner_keyed(@chapter_entity_named_givens, description)
302
+
303
+ named =
304
+ if declared_by
305
+ candidates[declared_by] || pending_chapter_entity_given(description, declared_by: declared_by)
306
+ elsif candidates.size == 1
307
+ candidates.values.first
308
+ elsif candidates.empty?
309
+ pending_chapter_entity_given(description, declared_by: nil)
310
+ else
311
+ raise(Malformed,
312
+ "#{@aggregate_name}::#{@name}'s given #{description.inspect} is ambiguous " \
313
+ "across the chapter's own pieces — #{candidates.keys.join(', ')} each declare " \
314
+ "a DIFFERENT predicate under this same description; name which one with " \
315
+ "declared_by: (e.g. given(#{description.inspect}, declared_by: " \
316
+ "#{candidates.keys.first.inspect}))")
317
+ end
318
+
319
+ @named_givens[description] = named
320
+ @owner_named_givens[description] ||= named
321
+ end
322
+
323
+ # A CHAPTER MAY BE SPLIT ACROSS FILES — the identical reason
324
+ # `AggregateBuilder#pending_chapter_given` defers rather than
325
+ # raising the moment a bare reference outruns what's loaded so
326
+ # far. Hands back a PLACEHOLDER `Given`, embedded by Ruby object
327
+ # reference in this piece's own `preconditions`, and queues the
328
+ # request in `@chapter_entity_pending_givens` —
329
+ # `BluebookBuilder#resolve_pending_chapter_entity_givens!`
330
+ # mutates it in place once every file in the chapter has loaded.
331
+ def pending_chapter_entity_given(description, declared_by:)
332
+ placeholder = Given.new(description: description, canonical: nil, predicate: nil)
333
+ @chapter_entity_pending_givens << { entity: "#{@aggregate_name}.#{@name}", description: description,
334
+ declared_by: declared_by, placeholder: placeholder }
335
+ placeholder
336
+ end
337
+
216
338
  # A PIECE'S OWN `one_of` LANDS ON ITS AGGREGATE. A type-position
217
339
  # `one_of("never_moved", "moved")` on an entity attribute
218
340
  # synthesizes a closed-set value object — and until this, that
@@ -257,6 +379,9 @@ module Hecks
257
379
  owner_named_givens: @owner_named_givens,
258
380
  identity_name_prefix: "#{@identity_name_prefix}#{Naming.demodulise(name)}",
259
381
  identity_value_object_installer: @identity_value_object_installer,
382
+ aggregate_name: @aggregate_name,
383
+ chapter_entity_named_givens: @chapter_entity_named_givens,
384
+ chapter_entity_pending_givens: @chapter_entity_pending_givens,
260
385
  &block)
261
386
  end
262
387
 
@@ -11,9 +11,24 @@ module Hecks
11
11
  @name = name
12
12
  end
13
13
 
14
- # `on` — item #13's full metaprogrammed dispatch, slice 1
15
- # (whole-project table-unification survey): a bare, kind-driven
16
- # coerce-and-assign, now executed by `GenericDispatch`.
14
+ # `on Account::AccountFrozen` — A BARE CONSTANT ACCEPTED (ADR
15
+ # 0025, S6 "events first-class"), resolved through `ConstShim`
16
+ # the same way `trigger`/`dispatch` already resolve a command
17
+ # reference (`Naming.event_ref`, that method's own header). NOT
18
+ # a required spelling yet, unlike `trigger`'s own quoted-text
19
+ # refusal — see `policy.bluebook`'s own KeywordSeed comment for
20
+ # why: event names aren't 100% migrated across the live corpus
21
+ # the way command references are, so both `on "Account.
22
+ # AccountFrozen"` (quoted) and `on Account::AccountFrozen`
23
+ # (bare) stay admitted until a full migration lands.
24
+ #
25
+ # RENAMED FROM the generic single-fill coercion — item #13's
26
+ # full metaprogrammed dispatch, slice 1 (whole-project
27
+ # table-unification survey), now overridden here the same way
28
+ # `trigger_impl` overrides its own generic default.
29
+ def on_impl(event_ref)
30
+ @on_event = Naming.event_ref(event_ref)
31
+ end
17
32
 
18
33
  # `with:` — WHAT THE TRIGGER IS GIVEN, when the event's own shape
19
34
  # is not it. Omitted, the whole event payload forwards verbatim,
@@ -8,15 +8,24 @@ module Hecks
8
8
  include WordGate
9
9
 
10
10
  def initialize(name)
11
- @name = name
12
- @signal = :reply
11
+ @name = name
12
+ @signal = :reply
13
+ @answers = []
13
14
  end
14
15
 
15
16
  def verb(value) = @verb = value.to_s
16
17
  def signal(value) = @signal = value.to_sym
17
18
 
19
+ # THE METHOD CONTRACT — the fact a `.port` file's `verb`/`signal`
20
+ # never carried: what an adapter must actually RESPOND TO for a
21
+ # dispatch to reach it without a bare `NoMethodError`. Declared the
22
+ # same repeatable way `AdapterBuilder#field`/`#secret` already are,
23
+ # so `verify!` can check it with `respond_to?` at boot instead of
24
+ # the runtime discovering it live.
25
+ def answers(name) = @answers << name.to_sym
26
+
18
27
  def build
19
- MetaValidator.call_port(Port.new(name: @name, verb: @verb, signal: @signal))
28
+ MetaValidator.call_port(Port.new(name: @name, verb: @verb, signal: @signal, answers: @answers))
20
29
  end
21
30
 
22
31
  def self.build(name, &block)
@@ -14,10 +14,38 @@ module Hecks
14
14
  @handlers = []
15
15
  end
16
16
 
17
- # `correlates_by`/`starts_on`/`ends_on` — item #13's full
18
- # metaprogrammed dispatch, slice 1 (whole-project table-
19
- # unification survey): each a bare, kind-driven coerce-and-
20
- # assign with nothing else, now executed by `GenericDispatch`.
17
+ # `starts_on Transfer::TransferRequested` — BARE CONSTANT
18
+ # ACCEPTED (ADR 0025, S6 "events first-class"), resolved
19
+ # through `ConstShim` the same way `on_impl`/`transition_impl`
20
+ # resolve an event reference but through `Naming.
21
+ # event_name_ref`, NOT `Naming.event_ref` (that method's own
22
+ # header has the full account: `SagaInterpreter` matches
23
+ # `pm.starts_on`/`pm.ends_on` against a BARE `event.name`, never
24
+ # a "." qualified one). A plain String still passes through
25
+ # unchanged, both for `shadow_parse` and for any corpus site a
26
+ # future pass hasn't migrated yet.
27
+ def starts_on_impl(event_ref)
28
+ @starts_on = Naming.event_name_ref(event_ref)
29
+ end
30
+
31
+ # `ends_on` — same reasoning as `starts_on_impl`, above.
32
+ def ends_on_impl(event_ref)
33
+ @ends_on = Naming.event_name_ref(event_ref)
34
+ end
35
+
36
+ # `correlates_by` — item #13's full metaprogrammed dispatch,
37
+ # slice 1 (whole-project table-unification survey): a bare,
38
+ # kind-driven coerce-and-assign with nothing else, executed by
39
+ # `GenericDispatch`.
40
+ #
41
+ # `starts_on`/`ends_on` USED TO be table-driven the same way,
42
+ # until ADR 0025 S6 gave each a second, `kind: "constant"`
43
+ # ArgumentSeed row (`starts_on Transfer::TransferRequested`) —
44
+ # `GenericDispatch::COERCE_BY_KIND` only knows `"text"`/
45
+ # `"symbol"`, and `shape_for` refuses to pick a `single_fill`
46
+ # shape at all once two Argument rows share a `fills:` target
47
+ # (`arguments.size == 1` below), so both words are hand-written
48
+ # again, `calls:`-routed like `transition` already is.
21
49
 
22
50
  # ONE STATE-MACHINE VOCABULARY (S7, ADR 0025 — "events and
23
51
  # reactions"): the SAME word `Lifecycle#transition` already
@@ -85,8 +113,25 @@ module Hecks
85
113
  handler.instance_eval(&block) if block
86
114
 
87
115
  mapping.each do |event_type, target|
116
+ # BARE CONSTANT ACCEPTED (ADR 0025, S6 — "events first-
117
+ # class"), `transition Account::AccountDebited => "state"` —
118
+ # `Naming.event_name_ref`, NOT the DOTTED `Naming.event_ref`
119
+ # transform `PolicyBuilder#on_impl` uses (that method's own
120
+ # header has the full account, found live wiring a real
121
+ # migrated corpus site into `bin/model_check` for the first
122
+ # time: `SagaInterpreter#advance_saga` matches `handler.
123
+ # event_type` against a BARE `event.name`, never a "."
124
+ # qualified one — a policy's own cross-aggregate match
125
+ # works differently, splitting the qualifier apart from the
126
+ # name rather than comparing the whole string). Writing the
127
+ # qualifier is still worth it (the same provenance `trigger
128
+ # Account::Debit` gives a reader) — `event_name_ref` keeps
129
+ # only the final segment, so `Account::AccountDebited` and a
130
+ # bare `AccountDebited` store identically. A plain String
131
+ # still passes through unchanged, both for `shadow_parse`
132
+ # and for every corpus site this pass didn't migrate.
88
133
  state_transition = StateTransition.new(target: target, from: from)
89
- expand(event_type.to_s, state_transition, handler.dispatches).each { |row| @handlers << row }
134
+ expand(Naming.event_name_ref(event_type), state_transition, handler.dispatches).each { |row| @handlers << row }
90
135
  end
91
136
  end
92
137
 
@@ -185,19 +230,73 @@ module Hecks
185
230
  #
186
231
  # RENAMED FROM `dispatch` — item #13's full metaprogrammed
187
232
  # dispatch (slice 4), same reasoning as trigger_impl above.
188
- def dispatch_impl(command_ref, with: nil)
233
+ #
234
+ # AN OPTIONAL BLOCK OPENS `compensates` ON THIS DISPATCH
235
+ # SPECIFICALLY — per-dispatch saga compensation, replacing a
236
+ # hand-written list at the saga's own `on :refused` leg. Real,
237
+ # live bug this closes: `examples/banking/bluebook/transfers_
238
+ # and_payments.bluebook`'s own `Settlement` saga wrote a
239
+ # compensating reversal by hand that depended on an event only
240
+ # fired if someone dispatched it manually — "the reversal was
241
+ # written and never armed" (that file's own comment). The
242
+ # runtime now tracks which legs actually completed
243
+ # (`SagaInterpreter`'s own `completed_compensations`) and
244
+ # compensates only those, newest first, instead of trusting an
245
+ # author's static list to be complete and correctly ordered.
246
+ def dispatch_impl(command_ref, with: nil, &block)
189
247
  if command_ref.is_a?(::String) && !MetaValidator.shadow_parsing?
190
248
  raise InvalidProcessManager,
191
249
  "dispatch #{command_ref.inspect} is quoted text — give the bare command constant " \
192
250
  "instead, e.g. dispatch Account::Debit"
193
251
  end
194
252
 
195
- DispatchSpec.new(
253
+ spec = DispatchSpec.new(
196
254
  command_name: Naming.command_ref(command_ref),
197
255
  with_spec: (with || {}).to_a
198
- ).tap do |dispatch|
199
- dispatch.instance_variable_set(:@projection_declared, !with.nil?)
200
- @dispatches << dispatch
256
+ )
257
+ spec.instance_variable_set(:@projection_declared, !with.nil?)
258
+
259
+ if block
260
+ builder = DispatchBuilder.new
261
+ builder.instance_eval(&block)
262
+ # `instance_variable_get`, not a public `compensates_spec`
263
+ # reader — a public one would be a method the grammar
264
+ # never declares, exactly what `spec/syntax_conformance_
265
+ # spec.rb`'s "answers only words the language declares"
266
+ # check exists to catch.
267
+ spec.compensates = builder.instance_variable_get(:@compensates_spec)
268
+ end
269
+
270
+ @dispatches << spec
271
+ spec
272
+ end
273
+
274
+ # THE NESTED SCOPE `dispatch ... do ... end` OPENS — one word
275
+ # only (`compensates`), the compensating half of the dispatch it
276
+ # sits inside. Its own `compensates_impl` builds a SECOND
277
+ # `DispatchSpec`, shape-identical to `HandlerBuilder#dispatch_
278
+ # impl`'s own — a compensation takes the exact same two
279
+ # arguments (a bare command constant, an optional `with:`)
280
+ # because it resolves through the identical scope (current
281
+ # event payload, opening event memory, correlation binding)
282
+ # any saga dispatch already does
283
+ # (`SagaInterpreter#dispatch_args`).
284
+ class DispatchBuilder
285
+ GRAMMAR_CONTEXT = "Dispatch"
286
+
287
+ include WordGate
288
+
289
+ def compensates_impl(command_ref, with: nil)
290
+ if command_ref.is_a?(::String) && !MetaValidator.shadow_parsing?
291
+ raise InvalidProcessManager,
292
+ "compensates #{command_ref.inspect} is quoted text — give the bare command constant " \
293
+ "instead, e.g. compensates Account::Credit"
294
+ end
295
+
296
+ @compensates_spec = DispatchSpec.new(
297
+ command_name: Naming.command_ref(command_ref),
298
+ with_spec: (with || {}).to_a
299
+ )
201
300
  end
202
301
  end
203
302
  end
@@ -129,6 +129,7 @@ module Hecks
129
129
  # the real table, every time, no exception.
130
130
  BOOTSTRAP_FALLBACK = {
131
131
  %w[given Aggregate] => { resolves_via: "owner_keyed", disambiguator: "declared_by" },
132
+ %w[given Entity] => { resolves_via: "owner_keyed", disambiguator: "declared_by" },
132
133
  %w[given Command] => { resolves_via: "hash_chain" },
133
134
  %w[invariant ValueObject] => { resolves_via: "sibling_scan" }
134
135
  }.freeze
@@ -110,8 +110,15 @@ module Hecks
110
110
  # below checks them in, for the same reason: `list_of`/
111
111
  # `one_of` used in an attribute's own type position never
112
112
  # arrive with `self.class::GRAMMAR_CONTEXT == "Type"`.
113
- target = fallback[[self.class::GRAMMAR_CONTEXT, word.to_s]] || fallback[["Type", word.to_s]]
114
- return send(target, *args, **kwargs, &block) if target
113
+ own_key = [self.class::GRAMMAR_CONTEXT, word.to_s]
114
+ type_key = ["Type", word.to_s]
115
+ # key? first, never `||` — a target method name is never
116
+ # actually stored as `false`, but the fallback lookup itself
117
+ # must not silently prefer "Type" over this class's own
118
+ # context on the strength of a falsy-looking value.
119
+ found = fallback.key?(own_key) || fallback.key?(type_key)
120
+ target = fallback.key?(own_key) ? fallback[own_key] : fallback[type_key]
121
+ return send(target, *args, **kwargs, &block) if found
115
122
 
116
123
  return super
117
124
  end
@@ -14,6 +14,30 @@ module Hecks
14
14
  def to_h = @values
15
15
  end
16
16
 
17
+ # THE AGGREGATE-QUALIFIED MIRROR (#143) — a `.world` file's own
18
+ # `Pizzas::Order.charged_by("Stripe") do ... end` visually mirrors the
19
+ # SAME bind line the sibling `.hecksagon` file already writes
20
+ # (`HecksagonBuilder`'s own `BindingProxy`), but `IR::World#for_verb`/
21
+ # `#for_binding` key purely by verb and adapter name — the aggregate
22
+ # qualifier is never read back out, it exists only for that visual
23
+ # mirroring. So unlike `BindingProxy`, nothing here needs to hold onto
24
+ # the resolved constant chain at all: every verb call, qualified or
25
+ # not, has to land in the exact same `@settings` write path
26
+ # (`WorldBuilder#record_binding`).
27
+ class WorldConstProxy
28
+ def self.namespace(builder)
29
+ Module.new do
30
+ define_singleton_method(:const_missing) { |_aggregate| WorldConstProxy.new(builder) }
31
+ end
32
+ end
33
+
34
+ def initialize(builder) = @builder = builder
35
+
36
+ def method_missing(verb, *args, **kwargs, &block) = @builder.record_binding(verb, args, kwargs, block)
37
+
38
+ def respond_to_missing?(_name, _include_private = false) = true
39
+ end
40
+
17
41
  class WorldBuilder
18
42
  GRAMMAR_CONTEXT = "World"
19
43
 
@@ -44,6 +68,17 @@ module Hecks
44
68
  result = word_gate_dispatch(verb, args, kwargs, block)
45
69
  return result unless result.equal?(WordGate::NOT_ADMITTED)
46
70
 
71
+ record_binding(verb, args, kwargs, block)
72
+ end
73
+
74
+ def respond_to_missing?(_name, _include_private = false) = true
75
+
76
+ # EXTRACTED from the old `method_missing` body (#143) so
77
+ # `WorldConstProxy`'s own aggregate-qualified verb calls
78
+ # (`Pizzas::Order.charged_by(...)`) write into the exact same
79
+ # place the bare top-level spelling (`charged_by(...)`) already
80
+ # does — one write path, two spellings.
81
+ def record_binding(verb, args, kwargs, block)
47
82
  collector = SettingsCollector.new
48
83
  collector.instance_eval(&block) if block
49
84
  value = { adapter: args.first.to_s }.merge(kwargs).merge(collector.to_h)
@@ -51,17 +86,22 @@ module Hecks
51
86
  @settings["#{verb}:#{args.first.to_s.downcase}"] = value
52
87
  end
53
88
 
54
- def respond_to_missing?(_name, _include_private = false) = true
55
-
56
89
  def build
57
90
  MetaValidator.call_world(
58
91
  World.new(domain: @domain, realm: @realm, latest: @latest, settings: @settings)
59
92
  )
60
93
  end
61
94
 
95
+ # `ConstShim`'s resolver, the same bridge `HecksagonBuilder`/
96
+ # `BluebookBuilder` already wrap their own `instance_eval` in
97
+ # (#143) — without it, `Pizzas::Order.charged_by(...)` raised
98
+ # `NameError: uninitialized constant Pizzas` for every `.world`
99
+ # file using the aggregate-qualified mirror form, since a bare
100
+ # `Pizzas` has no real constant to resolve to.
62
101
  def self.build(domain, &block)
63
- builder = new(domain)
64
- builder.instance_eval(&block) if block
102
+ builder = new(domain)
103
+ resolver = ->(_domain) { WorldConstProxy.namespace(builder) }
104
+ ConstShim.with(resolver) { builder.instance_eval(&block) } if block
65
105
  builder.build
66
106
  end
67
107
 
@@ -0,0 +1,149 @@
1
+ require_relative "evaluator"
2
+ require_relative "resolver"
3
+
4
+ module Hecks
5
+ module Bluebook
6
+ module Expression
7
+ # ── AST → JSON — walks the REAL Evaluator/Resolver AST (the same
8
+ # objects a live dispatch parses `given`/`ensures`/invariant text
9
+ # into — see docs/implemented/guides/running-a-runtime.md's "The
10
+ # expression grammar") and emits plain, JSON-serializable Ruby
11
+ # Hashes, tagged by `"op"`.
12
+ #
13
+ # THE SAME TWO-METHOD WALK `rust/project/expr_emitter.rb`'s own
14
+ # `emit_bool`/`emit_resolver` already do, over the SAME AST — that
15
+ # file's own methods build RUST SOURCE-CODE STRINGS for `rust/
16
+ # project`'s codegen (`rust/src/kernel::expr::Expr` literals, baked
17
+ # into a generated domain's own compiled binary); this builds DATA
18
+ # instead, for a genuinely different consumer with a genuinely
19
+ # different constraint: `rust/host` can never link the `rust`
20
+ # (kernel) crate at all (a real, load-bearing build constraint —
21
+ # `reference_validate.rs`'s own header has the full reasoning: one
22
+ # path dependency would statically bake every domain's generated
23
+ # dispatch code into every Lambda binary), so a value object's own
24
+ # `invariant` predicate has to travel as something `rust/host` can
25
+ # deserialize and interpret itself, at RUNTIME, from `ir.json` — the
26
+ # exact same relationship `rust/project`'s own `Expr` literals
27
+ # already have to the compiled kernel, one layer further out.
28
+ #
29
+ # LIVES IN CORE `lib/hecks`, NOT `rust/project/` — `rust/project.rb`
30
+ # is a separate, downstream toolchain
31
+ # (`lib/hecks/projector.rb`'s own header: "a whole separate Ruby
32
+ # program"), never `require`d by core `lib/hecks/bluebook/*.rb`
33
+ # files (confirmed: no core file does). `value_object.rb`'s own
34
+ # `invariants:` IR emission needs this for EVERY domain's ordinary
35
+ # `to_h`/`ir.json` export — golden fixtures, `hecks-parse`'s parity
36
+ # comparisons, and any deploy artifact, not only a `bin/project_rust`
37
+ # run — so it belongs beside `Evaluator`/`Resolver` themselves, not
38
+ # bolted onto a tool that only sometimes runs.
39
+ #
40
+ # COMPLETE, not corpus-scoped: every node this grammar admits gets
41
+ # a real arm, the identical "raise, don't silently drop" discipline
42
+ # `expr_emitter.rb`'s own `emit_bool`/`emit_resolver` already hold
43
+ # to — even though, as of this writing, no real corpus VALUE OBJECT
44
+ # invariant exercises `Include`/`Modulo`/`BlockPredicate`/`Find`/
45
+ # `Array`/`MatchesRegex`/`Presence`/`Split`/`StartsWith`/`EndsWith`/
46
+ # `First`/`Last` (only `given`/`ensures` clauses do, elsewhere in
47
+ # the corpus — a different consumer of this same grammar).
48
+ # `rust/host/src/expr_json.rs`'s own header names exactly which of
49
+ # these its interpreter evaluates for real today versus refuses
50
+ # cleanly — a narrower, deliberate, documented boundary on the
51
+ # INTERPRETING side, not on this EMITTING side: an author is free
52
+ # to write ANY real expression in a value object's own `invariant`,
53
+ # and this always emits it faithfully; whether `rust/host` can yet
54
+ # CHECK it at mint time is that file's own question to answer, not
55
+ # this one's to pre-empt by refusing to even try.
56
+ module AstJson
57
+ module_function
58
+
59
+ def emit_predicate(canonical)
60
+ emit_bool(Evaluator.parse(canonical))
61
+ end
62
+
63
+ def emit_bool(node)
64
+ case node
65
+ when Evaluator::Or then { "op" => "or", "left" => emit_bool(node.left), "right" => emit_bool(node.right) }
66
+ when Evaluator::And then { "op" => "and", "left" => emit_bool(node.left), "right" => emit_bool(node.right) }
67
+ when Evaluator::Not then { "op" => "not", "expr" => emit_bool(node.node) }
68
+ when Evaluator::Compare
69
+ { "op" => "compare", "cmp" => emit_comparison(node.operator),
70
+ "left" => emit_resolver(node.left), "right" => emit_resolver(node.right) }
71
+ when Evaluator::Include
72
+ emit_include(node)
73
+ when Evaluator::Resolve
74
+ emit_resolver(node.expr)
75
+ else
76
+ raise "unhandled evaluator node #{node.class} — no JSON rendering exists for it (lib/hecks/bluebook/expression/ast_json.rb#emit_bool)"
77
+ end
78
+ end
79
+
80
+ def emit_comparison(op)
81
+ { "less_than" => op.compares_less_than, "equal" => op.compares_equal, "negated" => op.negated }
82
+ end
83
+
84
+ # The JSON-target sibling of `expr_emitter.rb`'s own
85
+ # `emit_include` — see that method's own comment for the full
86
+ # reasoning (a LITERAL array haystack has no `Expr::Include`-
87
+ # representable shape on EITHER target, kernel or host, so both
88
+ # rewrite it identically into an OR-of-equalities at emission
89
+ # time rather than carrying a shape neither interpreter could
90
+ # evaluate). A non-literal haystack still emits `include`
91
+ # unchanged.
92
+ EQ = Evaluator::OPERATORS.find { |op| op.symbol == "==" }
93
+ private_constant :EQ
94
+
95
+ def emit_include(node)
96
+ return { "op" => "include", "haystack" => emit_resolver(node.haystack), "needle" => emit_resolver(node.needle) } \
97
+ unless node.haystack.is_a?(Resolver::ArrayLiteral)
98
+
99
+ return { "op" => "bool", "value" => false } if node.haystack.elements.empty?
100
+
101
+ equalities = node.haystack.elements.map do |element|
102
+ { "op" => "compare", "cmp" => emit_comparison(EQ), "left" => emit_resolver(node.needle), "right" => emit_resolver(element) }
103
+ end
104
+ equalities.reduce { |left, right| { "op" => "or", "left" => left, "right" => right } }
105
+ end
106
+
107
+ def emit_resolver(node)
108
+ case node
109
+ when Resolver::IntegerLiteral then { "op" => "int", "value" => node.value }
110
+ when Resolver::FloatLiteral then { "op" => "float", "value" => node.value }
111
+ when Resolver::StringLiteral then { "op" => "str", "value" => node.value }
112
+ when Resolver::BoolLiteral then { "op" => "bool", "value" => node.value }
113
+ when Resolver::NilLiteral then { "op" => "nil" }
114
+ when Resolver::Lookup then { "op" => "lookup", "path" => node.path }
115
+ when Resolver::Addition then { "op" => "add", "left" => emit_resolver(node.left), "right" => emit_resolver(node.right) }
116
+ when Resolver::SignTest
117
+ { "op" => "sign_test", "cmp" => emit_comparison(node.operator), "receiver" => emit_resolver(node.receiver) }
118
+ when Resolver::Empty then { "op" => "empty", "receiver" => emit_resolver(node.receiver) }
119
+ when Resolver::ToS then { "op" => "to_s", "receiver" => emit_resolver(node.receiver) }
120
+ when Resolver::Modulo then { "op" => "modulo", "receiver" => emit_resolver(node.receiver), "divisor" => emit_resolver(node.divisor) }
121
+ when Resolver::Size then { "op" => "size", "receiver" => emit_resolver(node.receiver) }
122
+ when Resolver::BlockPredicate
123
+ { "op" => "block_predicate", "mode" => node.mode.to_s, "receiver" => emit_resolver(node.receiver),
124
+ "param" => node.param.to_s, "predicate" => emit_bool(node.predicate) }
125
+ when Resolver::Find
126
+ { "op" => "find", "receiver" => emit_resolver(node.receiver), "param" => node.param.to_s,
127
+ "predicate" => emit_bool(node.predicate), "path" => node.path.map(&:to_s) }
128
+ when Resolver::ArrayLiteral
129
+ { "op" => "array", "elements" => node.elements.map { |element| emit_resolver(element) } }
130
+ when Resolver::MatchesRegex
131
+ { "op" => "matches_regex", "receiver" => emit_resolver(node.receiver), "pattern" => node.pattern, "flags" => node.flags }
132
+ when Resolver::Presence
133
+ { "op" => "presence", "receiver" => emit_resolver(node.receiver), "negated" => node.negated }
134
+ when Resolver::Split
135
+ { "op" => "split", "receiver" => emit_resolver(node.receiver), "separator" => node.separator }
136
+ when Resolver::StartsWith
137
+ { "op" => "starts_with", "receiver" => emit_resolver(node.receiver), "substring" => node.substring }
138
+ when Resolver::EndsWith
139
+ { "op" => "ends_with", "receiver" => emit_resolver(node.receiver), "substring" => node.substring }
140
+ when Resolver::First then { "op" => "first", "receiver" => emit_resolver(node.receiver) }
141
+ when Resolver::Last then { "op" => "last", "receiver" => emit_resolver(node.receiver) }
142
+ else
143
+ raise "unhandled resolver node #{node.class} — no JSON rendering exists for it (lib/hecks/bluebook/expression/ast_json.rb#emit_resolver)"
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end