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
@@ -1,4 +1,8 @@
1
1
  module Hecks
2
+ # Namespace for every driven-side adapter (Memory, Folder, SQLite,
3
+ # Postgres, Heki, ...) a `persisted_by`/port binding can resolve to;
4
+ # concrete adapters are required in below from adapters/driven and
5
+ # register themselves into this namespace as they load.
2
6
  module Adapters
3
7
  end
4
8
  end
@@ -11,6 +11,10 @@ module Hecks
11
11
  # inside a boot, over a suite that was never going to be scoped.
12
12
  class Malformed < StandardError; end
13
13
 
14
+ # The `test "description" do ... end` block's own receiver — collects
15
+ # `tests`/`setup`/`input`/`expect` calls and builds a `TestCase` (ir.rb).
16
+ # `validate_expect!` (private, below) is where a malformed or empty
17
+ # `expect` is refused at build time rather than silently passing later.
14
18
  class TestCaseBuilder
15
19
  def initialize(description)
16
20
  @description = description
@@ -76,6 +80,9 @@ module Hecks
76
80
  end
77
81
  end
78
82
 
83
+ # The top-level `Hecks.behaviors "Name" do ... end` receiver — collects
84
+ # `vision`/`loads`/`test` calls and builds a `BehaviorsSuite` (ir.rb),
85
+ # refusing to build one missing either `vision` or `loads` (`#build`).
79
86
  class BehaviorsBuilder
80
87
  def initialize(name, source_path:)
81
88
  @name = name
@@ -38,6 +38,8 @@ require_relative "../ports/persistence/binding_policy"
38
38
  # always on and `emits:` is expected to see them.
39
39
  module Hecks
40
40
  module Behaviors
41
+ # See this file's own header above for what this module does and the
42
+ # two deliberate deviations from the prior port it's built from.
41
43
  module Expectations
42
44
  module_function
43
45
 
@@ -75,6 +77,10 @@ module Hecks
75
77
  # loop or a long rspec session actually relies on. `runtime:` lets
76
78
  # a caller that already holds a booted runtime (a spec, a REPL)
77
79
  # hand it in; it is reset the same way.
80
+ # NOT frozen — a real cache, mutated below (`RUNTIMES[key] ||=
81
+ # boot_and_guard(files)`) and by #reset!. False positive for
82
+ # Style/MutableConstant.
83
+ # rubocop:disable-next Style/MutableConstant
78
84
  RUNTIMES = {}
79
85
  RUNTIMES_LOCK = Mutex.new
80
86
  private_constant :RUNTIMES, :RUNTIMES_LOCK
@@ -137,11 +143,17 @@ module Hecks
137
143
  before = runtime.registry.event_log.length
138
144
  result = dispatch_command(runtime, verb, test.input)
139
145
 
140
- return fail_result(test, "expected refused: #{test.expect[:refused].inspect} but dispatch succeeded") if test.expect.key?(:refused)
146
+ if test.expect.key?(:refused)
147
+ return fail_result(test,
148
+ "expected refused: #{test.expect[:refused].inspect} but dispatch succeeded")
149
+ end
141
150
 
142
151
  if (expected_emits = test.expect[:emits])
143
152
  actual = runtime.registry.event_log[before..].map(&:name)
144
- return fail_result(test, "expected emits: #{expected_emits.inspect}, got #{actual.inspect}") unless actual == expected_emits
153
+ unless actual == expected_emits
154
+ return fail_result(test,
155
+ "expected emits: #{expected_emits.inspect}, got #{actual.inspect}")
156
+ end
145
157
  end
146
158
 
147
159
  check_ok(test) || check_fields(test, settled_state(runtime, verb, result)) || pass_result(test)
@@ -205,7 +217,10 @@ module Hecks
205
217
  def run_query(test, runtime, verb)
206
218
  rows = runtime.query(verb, **test.input)
207
219
 
208
- return fail_result(test, "expected refused: #{test.expect[:refused].inspect} but the query succeeded") if test.expect.key?(:refused)
220
+ if test.expect.key?(:refused)
221
+ return fail_result(test,
222
+ "expected refused: #{test.expect[:refused].inspect} but the query succeeded")
223
+ end
209
224
 
210
225
  if (expected = test.expect[:count])
211
226
  count = if rows.is_a?(Array)
@@ -307,6 +322,14 @@ module Hecks
307
322
  def qualify(command, on_aggregate, bluebooks, kind:)
308
323
  return command.to_s if command.to_s.include?(".")
309
324
 
325
+ candidates = qualify_candidates(command, on_aggregate, bluebooks, kind)
326
+ disambiguate_qualified_name(candidates, command, kind, bluebooks)
327
+ end
328
+
329
+ # THE SEARCH — every (bluebook, aggregate) pair that declares a
330
+ # command/query named `command`, narrowed to `on_aggregate` by name
331
+ # when given.
332
+ def qualify_candidates(command, on_aggregate, bluebooks, kind)
310
333
  members = kind == :query ? :queries : :commands
311
334
  pairs =
312
335
  if on_aggregate
@@ -314,8 +337,13 @@ module Hecks
314
337
  else
315
338
  bluebooks.flat_map { |bb| bb.aggregates.map { |agg| [bb, agg] } }
316
339
  end
317
- candidates = pairs.select { |_, agg| agg.public_send(members).any? { |m| m.hecks_name == command.to_s } }
340
+ pairs.select { |_, agg| agg.public_send(members).any? { |m| m.hecks_name == command.to_s } }
341
+ end
318
342
 
343
+ # THE REPORT — zero candidates and more-than-one candidates both
344
+ # refuse (with a different message); exactly one resolves to its
345
+ # dotted FQN.
346
+ def disambiguate_qualified_name(candidates, command, kind, bluebooks)
319
347
  case candidates.size
320
348
  when 0
321
349
  raise ArgumentError, "no aggregate among #{bluebooks.map(&:name).inspect} declares a #{kind} " \
@@ -16,6 +16,8 @@ require_relative "../behaviors"
16
16
  # end
17
17
  module Hecks
18
18
  module Behaviors
19
+ # See this file's own header above for what `describe_file` does and
20
+ # how a consumer wires it into their own `bundle exec rspec` run.
19
21
  module RSpec
20
22
  module_function
21
23
 
@@ -6,6 +6,10 @@ require_relative "expectations"
6
6
  # that actually touches a live runtime; this file finds `.behaviors`
7
7
  # files, loads them, and maps their tests through it.
8
8
  module Hecks
9
+ # See this file's own header above for `.run`/`.run_all`/`.parse` — the
10
+ # discovery-and-aggregation half of the `.behaviors` authoring surface;
11
+ # `Expectations` (expectations.rb) is the impure edge that actually runs
12
+ # one test against a live runtime.
9
13
  module Behaviors
10
14
  FileResult = Struct.new(:path, :parse_error, :runs, keyword_init: true)
11
15
  SweepResult = Struct.new(:root, :files_swept, :files, :summary, keyword_init: true)
@@ -1,4 +1,5 @@
1
1
  require_relative "behaviour/aggregate"
2
+ require_relative "expression/ast_json"
2
3
 
3
4
  module Hecks
4
5
  module Bluebook
@@ -40,14 +41,14 @@ module Hecks
40
41
  attributes: many(:attributes),
41
42
  value_objects: many(:value_objects),
42
43
  commands: many(:commands),
43
- invariants: -> { invariants.map { |rule| { description: rule.description, canonical: rule.canonical } } },
44
+ invariants: -> { invariants.map { |rule| Expression::AstJson.rule_row(rule) } },
44
45
  # A PRECONDITION SHARED ACROSS COMMANDS, DECLARED ONCE (S10, ADR
45
46
  # 0025) — the aggregate's OWN named `given`s, the declaration a
46
47
  # referencing command's own (already-resolved) `givens` entry
47
48
  # came from. Both sides of "declared once, referenced many"
48
49
  # are real IR, the same shape a value object's TYPE and an
49
50
  # attribute's own reference to it both are.
50
- preconditions: -> { preconditions.map { |rule| { description: rule.description, canonical: rule.canonical } } },
51
+ preconditions: -> { preconditions.map { |rule| Expression::AstJson.rule_row(rule) } },
51
52
  # S12, ADR 0025 — deliberately NOT folded into `attributes`:
52
53
  # `EraGuard::ShapeDiff` only ever walks `attributes` to decide
53
54
  # whether a NEW field leaves an existing record with something
@@ -55,10 +56,10 @@ module Hecks
55
56
  # is different — a record predating the `projects` declaration
56
57
  # is expected to be missing it until the rebuild sweep runs,
57
58
  # not a shape drift a translation needs to explain.
58
- projected_fields: -> {
59
- projected_fields.map { |field|
59
+ projected_fields: lambda {
60
+ projected_fields.map do |field|
60
61
  { name: field.name.to_s, reference: field.reference.to_s, remote_field: field.remote_field.to_s }
61
- }
62
+ end
62
63
  },
63
64
  lifecycle: one(:lifecycle),
64
65
  entities: many(:entities),
@@ -77,11 +77,11 @@ module Hecks
77
77
  # and resolve. Deliberately across every list that can carry one — a
78
78
  # reference the walk misses resolves to nil, and a nil target is SKIPPED
79
79
  # rather than refused, so the guarantee would go quiet instead of red.
80
- def stamp_references(ir)
81
- lists = [ir.attributes, *ir.commands.map(&:attributes), *ir.queries.map(&:attributes)]
82
- entity_reference_lists(ir.entities, lists)
80
+ def stamp_references(aggregate)
81
+ lists = [aggregate.attributes, *aggregate.commands.map(&:attributes), *aggregate.queries.map(&:attributes)]
82
+ entity_reference_lists(aggregate.entities, lists)
83
83
 
84
- lists.flatten.select(&:reference?).each { |field| field.type.declared_in = ir }
84
+ lists.flatten.select(&:reference?).each { |field| field.type.declared_in = aggregate }
85
85
  end
86
86
 
87
87
  # S17, ADR 0026 — walks NESTED entities too (Dispatch, inside
@@ -20,7 +20,7 @@ module Hecks
20
20
  contract = Assembly.contract(category)
21
21
  keywords = contract.fields.to_h { |keyword, (key, reader)| [keyword, read(reader, row[key])] }
22
22
 
23
- holder(contract).public_send(contract.make, **keywords.merge(extra))
23
+ holder(contract).public_send(contract.make, **keywords, **extra)
24
24
  end
25
25
 
26
26
  def holder(contract)
@@ -37,8 +37,11 @@ module Hecks
37
37
  when Array
38
38
  # [:each, reader] maps a list ; [:option, name] reads one named option,
39
39
  # which needs its name as well as its value.
40
- reader.first == :each ? Array(value).map { |held| Marks.public_send(reader.last, held) }
41
- : Marks.option(reader.last, value)
40
+ if reader.first == :each
41
+ Array(value).map { |held| Marks.public_send(reader.last, held) }
42
+ else
43
+ Marks.option(reader.last, value)
44
+ end
42
45
  else Marks.public_send(reader, value)
43
46
  end
44
47
  end
@@ -1,5 +1,9 @@
1
1
  module Hecks
2
2
  module Bluebook
3
+ # The rulebook translating between the language's own declarations and the
4
+ # IR objects `Build`/`Reconstruction` produce from them — the `Contract`
5
+ # struct format itself (this file) plus, reopened in contracts.rb, the
6
+ # `CONTRACTS` table of one `Contract` per construct category.
3
7
  class Assembly
4
8
  # WHAT A CONSTRUCT NEEDS THAT THE LANGUAGE CANNOT SAY, and how a claim that a
5
9
  # field needs no assembling is CHECKED.
@@ -78,7 +82,7 @@ module Hecks
78
82
  end
79
83
 
80
84
  def answers?(method)
81
- make == :declare ? holder.respond_to?(method) : holder.instance_methods.include?(method)
85
+ make == :declare ? holder.respond_to?(method) : holder.method_defined?(method)
82
86
  end
83
87
 
84
88
  def accepts?(keyword)
@@ -1,5 +1,9 @@
1
1
  module Hecks
2
2
  module Bluebook
3
+ # Reopened here for `CONTRACTS`/`.contract(category)` — the one table of
4
+ # every construct category's own `Contract` (Bluebook, Aggregate,
5
+ # Command, ValueObject, Query, ...); see `Contract`'s own header
6
+ # (contract.rb) for the struct format each entry below fills in.
3
7
  class Assembly
4
8
  # ONE TABLE, WHERE THERE WERE FIVE HAND-WRITTEN MIRRORS OF IT.
5
9
  #
@@ -129,7 +129,8 @@ module Hecks
129
129
  # `:delegate`/`:corrects` (CommandBuilder#delegates_to's and
130
130
  # #corrects_impl's own comments) ride the SAME multi-binding
131
131
  # shape `:append` does.
132
- return Mutation.new(target: target, op: op, source: appended(change[:fields])) if [:append, :delegate, :corrects].include?(op)
132
+ return Mutation.new(target: target, op: op, source: appended(change[:fields])) if [:append, :delegate,
133
+ :corrects].include?(op)
133
134
 
134
135
  Mutation.new(target: target, op: op, source: classified(change[:source]))
135
136
  end
@@ -169,9 +170,19 @@ module Hecks
169
170
  # a saga with nothing to do.
170
171
  def read(value) = Literal.read(value)
171
172
 
173
+ # `target:` (ADR 0055) — read straight off the wire, unconverted:
174
+ # it's already the bare aggregate-name STRING `WhereClause#to_h`/
175
+ # `OrderBy#to_h`/`LimitSpec#to_h` wrote (`resolve_target`'s own
176
+ # `Naming.demodulise` already ran once, at DSL-build time; this is
177
+ # the REPLAY path every real boot actually goes through, reading
178
+ # that same wire shape back — see this class's own header). Absent
179
+ # from `clause`/`declared` entirely on older wire data that never
180
+ # declared `on:` — `clause[:target]`/`declared[:target]` reads
181
+ # `nil` for a missing key exactly like an explicit `nil` would,
182
+ # so this is additive, not a migration.
172
183
  def where_clause(clause)
173
184
  QuerySpecification::Common::WhereClause.new(
174
- field: clause[:field], op: clause[:op].to_sym, value: read(clause[:value])
185
+ field: clause[:field], op: clause[:op].to_sym, value: read(clause[:value]), target: clause[:target]
175
186
  )
176
187
  end
177
188
 
@@ -179,14 +190,14 @@ module Hecks
179
190
  return nil unless declared
180
191
 
181
192
  QuerySpecification::Common::OrderBy.new(
182
- field: declared[:field], direction: declared[:direction].to_sym
193
+ field: declared[:field], direction: declared[:direction].to_sym, target: declared[:target]
183
194
  )
184
195
  end
185
196
 
186
197
  def limit(declared)
187
198
  return nil unless declared
188
199
 
189
- QuerySpecification::Common::LimitSpec.new(value: read(declared[:value]))
200
+ QuerySpecification::Common::LimitSpec.new(value: read(declared[:value]), target: declared[:target])
190
201
  end
191
202
 
192
203
  # EVERY OTHER SPECIFICATION OPTION, from one table.
@@ -38,15 +38,13 @@ module Hecks
38
38
  aggregates = Array(@declaration[:aggregates]).map { |row| AggregateAssembly.new(row).aggregate }
39
39
  models = Array(@declaration[:read_models]).map { |row| Build.call("ReadModel", row) }
40
40
 
41
- chapter = Build.call(
41
+ Build.call(
42
42
  "Bluebook", @declaration,
43
43
  aggregates: aggregates,
44
44
  read_models: models,
45
45
  policies: reactions(aggregates),
46
46
  process_managers: Array(@declaration[:process_managers]).map { |row| process_manager(row) }
47
47
  )
48
-
49
- chapter
50
48
  end
51
49
 
52
50
  private
@@ -5,6 +5,12 @@ require_relative "../naming"
5
5
 
6
6
  module Hecks
7
7
  module Bluebook
8
+ # One declared field on a construct — name, type (a primitive, a
9
+ # Reference, or another construct's name, always spelled via `#spell`),
10
+ # whether it's a list, its default/optional/pattern, and the closed set
11
+ # it `admits`. What `attribute :x, Type` (or `identified_by`,
12
+ # `reference_to`, etc.) actually mints, on an Aggregate, Entity, Command,
13
+ # ValueObject, Query or PortOperation alike.
8
14
  class Attribute
9
15
  include Hecks::IR
10
16
  include Behaviour::Attribute
@@ -24,6 +24,24 @@ module Hecks
24
24
  # event's own payload.
25
25
  def guarded? = !@where.to_s.empty?
26
26
 
27
+ # THE STRUCTURED FORM OF `where`, derived once — the same tree
28
+ # `AstJson.emit_predicate` spells for every rule row, memoized
29
+ # here because a policy is consulted once per event, not once per
30
+ # boot. Nil when there is no `where`, exactly as the wire carries
31
+ # it.
32
+ def where_ast
33
+ if defined?(@where_ast)
34
+ @where_ast
35
+ else
36
+ (@where_ast = guarded? ? Expression::AstJson.emit_predicate(@where) : nil)
37
+ end
38
+ end
39
+
40
+ # The rule-shaped reading of the guard, for `Evaluator.call_rule`
41
+ # — a policy's `where` has no description (nothing refuses with
42
+ # it; an unmet where is a silent skip).
43
+ def where_rule = @where_rule ||= Given.new(description: nil, canonical: @where, ast: where_ast)
44
+
27
45
  # THE FAN-OUT QUERY'S ROUTE, split the way the runtime runs it:
28
46
  # `[query_domain, aggregate_name, query_name]`. The query runs
29
47
  # against the triggering event's OWN domain unless `for_each`
@@ -7,7 +7,23 @@ module Hecks
7
7
  module ProcessManager
8
8
  def hecks_name = @name
9
9
 
10
- def handler_for(event) = @handlers.find { |h| h.event_type == event.to_s }
10
+ # THE LEG THAT ANSWERS selected by (event, current state), C10.3
11
+ # (docs/semantics/bluebook-semantics.md). Two legs may answer the
12
+ # same event from different states; which one runs is decided by
13
+ # the state the instance is in, never by declaration order. With
14
+ # no `state` the reading is declarative — "does any leg answer this
15
+ # event, and what is it" — for callers that ask about the
16
+ # procedure rather than an instance (`saga`, the outbox's consumer
17
+ # selection). Build refuses two legs on one (event, state) pair
18
+ # (`ProcessManagerBuilder#validate!`), so a stated lookup is
19
+ # unambiguous by construction.
20
+ def handler_for(event, state = nil)
21
+ @handlers.find { |h| h.event_type == event.to_s && (state.nil? || h.from_state == state.to_s) }
22
+ end
23
+
24
+ def handlers_for(event) = @handlers.select { |h| h.event_type == event.to_s }
25
+
26
+ def handles?(event) = @handlers.any? { |h| h.event_type == event.to_s }
11
27
 
12
28
  # WHETHER A STATE IS ONE THIS PROCEDURE DECLARES — asked of a value
13
29
  # a real run left a saga instance holding (its live or rehydrated
@@ -14,14 +14,59 @@ module Hecks
14
14
 
15
15
  def query_name = Naming.snake(@name)
16
16
 
17
- # WHICH GATHERED HEAD THE FILTERING APPLIES TO, so the read-model
18
- # interpreter can ask this directly rather than re-deriving or
19
- # re-checking it.
20
- def filtered_head_name
21
- return nil unless wheres.any? || order_by || limit || offset || authorization&.tenant ||
22
- @group_by.any? || count? || @median_field
23
-
24
- @aggregate_heads.find { |head| head[:many] }&.fetch(:as)
17
+ # WHICH GATHERED HEADS THE FILTERING APPLIES TO (ADR 0055) — plural,
18
+ # since `where`/`order_by`/`limit`/`offset` can now each independently
19
+ # name a many-side head via `on:` once there's more than one. A read
20
+ # model with a single many-side head keeps the old reading: every
21
+ # UNTARGETED option (plus `group_by`/`count`/`median`, still
22
+ # single-head-only ADR 0055) applies to it, same as before `on:`
23
+ # existed. With several many-side heads, only the ones actually named
24
+ # by a targeted option are eligible.
25
+ def filtered_head_names
26
+ many = @aggregate_heads.select { |head| head[:many] }
27
+ return [] if many.empty?
28
+
29
+ return single_filtered_head_name(many) if many.one?
30
+
31
+ targets = (wheres.map(&:target) + [order_by&.target, limit&.target, offset&.target]).compact.uniq
32
+ targets.filter_map { |target| many.find { |head| head[:aggregate] == target.to_s } }.map { |head| head[:as] }
33
+ end
34
+
35
+ # The pre-`on:` reading (ADR 0055), unchanged: with exactly one
36
+ # many-side head, every UNTARGETED option (plus `group_by`/`count`/
37
+ # `median`, still single-head-only) applies to it — split out only
38
+ # to keep `filtered_head_names` itself under this file's own
39
+ # complexity budget, not because the two questions differ in kind.
40
+ def single_filtered_head_name(many)
41
+ declared = wheres.any? || order_by || limit || offset || authorization&.tenant ||
42
+ @group_by.any? || count? || @median_field
43
+ declared ? [many.first[:as]] : []
44
+ end
45
+
46
+ # THE where/order_by/limit/offset THAT APPLY TO ONE ELIGIBLE HEAD
47
+ # (ADR 0055) — a small view `Ports::Query::InMemory.execute` reads
48
+ # exactly the way it already reads a whole `Query`/`ReadModel`
49
+ # (`.wheres`/`.order_by`/`.limit`/`.offset`/`.null_semantics`), scoped
50
+ # to `head_as`'s own aggregate: an UNTARGETED option applies when
51
+ # `head_as` is the read model's ONE many-side head (the pre-`on:`
52
+ # reading, unchanged) ; a TARGETED one applies when its `target`
53
+ # resolves to `head_as`'s own aggregate.
54
+ FilteredOptions = Struct.new(:wheres, :order_by, :limit, :offset, :null_semantics)
55
+
56
+ def options_for(head_as)
57
+ many = @aggregate_heads.select { |head| head[:many] }
58
+ aggregate_name = @aggregate_heads.find { |head| head[:as] == head_as }&.fetch(:aggregate)
59
+ applies = lambda do |target|
60
+ target.nil? ? many.one? : target.to_s == aggregate_name
61
+ end
62
+
63
+ FilteredOptions.new(
64
+ wheres.select { |where| applies.call(where.target) },
65
+ order_by && applies.call(order_by.target) ? order_by : nil,
66
+ limit && applies.call(limit.target) ? limit : nil,
67
+ offset && applies.call(offset.target) ? offset : nil,
68
+ null_semantics
69
+ )
25
70
  end
26
71
  end
27
72
  end
@@ -29,7 +29,7 @@ module Hecks
29
29
  # readers that need all of them ask for `identity_heads`.
30
30
  module Identified
31
31
  def derive_identity
32
- @identity_paths = Array(@identified_by).map { |path| path.to_s }.reject(&:empty?)
32
+ @identity_paths = Array(@identified_by).map(&:to_s).reject(&:empty?)
33
33
  @identity_heads = @identity_paths.map { |path| path.split(".").first.to_sym }.uniq
34
34
  @identified_by = @identity_heads.size == 1 ? @identity_heads.first : nil
35
35
  end
@@ -1,9 +1,10 @@
1
1
  require_relative "behaviour/command"
2
2
  require_relative "../vocabulary"
3
+ require_relative "expression/ast_json"
3
4
 
4
5
  module Hecks
5
6
  module Bluebook
6
- Given = Struct.new(:description, :canonical, :predicate, keyword_init: true)
7
+ Given = Struct.new(:description, :canonical, :predicate, :ast, keyword_init: true)
7
8
 
8
9
  Mutation = Struct.new(:target, :op, :source, keyword_init: true) do
9
10
  include Hecks::IR
@@ -24,8 +25,8 @@ module Hecks
24
25
  # real layering inversion (runtime depends on bluebook, not the
25
26
  # reverse). "" (not nil) for ops with no sign, matching every other
26
27
  # optional IR text field's own absent-is-empty-string convention.
27
- def self.sign_for(op)
28
- Hecks::Vocabulary.rows("MutationOp").find { |row| row["name"] == op.to_s }&.fetch("sign", "") || ""
28
+ def self.sign_for(oper)
29
+ Hecks::Vocabulary.rows("MutationOp").find { |row| row["name"] == oper.to_s }&.fetch("sign", "") || ""
29
30
  end
30
31
 
31
32
  emits_ir(target: :target, op: :op, sign: -> { Mutation.sign_for(op) })
@@ -74,8 +75,8 @@ module Hecks
74
75
  goal: :goal,
75
76
  references: :references,
76
77
  attributes: many(:attributes),
77
- givens: -> { givens.map { |rule| { description: rule.description, canonical: rule.canonical } } },
78
- ensures: -> { ensures.map { |rule| { description: rule.description, canonical: rule.canonical } } },
78
+ givens: -> { givens.map { |rule| Expression::AstJson.rule_row(rule) } },
79
+ ensures: -> { ensures.map { |rule| Expression::AstJson.rule_row(rule) } },
79
80
  mutations: many(:mutations),
80
81
  emits: :emits,
81
82
  # THE LIFECYCLE STATE THIS COMMAND IS ADMISSIBLE FROM (S10, ADR
@@ -2,8 +2,11 @@ require_relative "word_gate"
2
2
  module Hecks
3
3
  module Bluebook
4
4
  module DSL
5
+ # The `Hecks.adapter "Name" do port ...; field ...; secret ... end`
6
+ # receiver — collects the port it implements plus its own settings
7
+ # fields and secrets, then builds and judges an `Adapter` construct.
5
8
  class AdapterBuilder
6
- GRAMMAR_CONTEXT = "Adapter"
9
+ GRAMMAR_CONTEXT = "Adapter".freeze
7
10
 
8
11
  include WordGate
9
12