hecks 1.0.2 → 1.1.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 (239) 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 +97 -0
  15. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
  16. data/lib/hecks/adapters/driven/postgres.rb +25 -14
  17. data/lib/hecks/adapters/driven/prism.rb +11 -0
  18. data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
  19. data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
  20. data/lib/hecks/adapters/driven/sqlite/projection.rb +17 -1
  21. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
  22. data/lib/hecks/adapters/driven/sqlite.rb +87 -8
  23. data/lib/hecks/adapters/driven.rb +4 -0
  24. data/lib/hecks/adapters.rb +4 -0
  25. data/lib/hecks/behaviors/dsl.rb +7 -0
  26. data/lib/hecks/behaviors/expectations.rb +32 -4
  27. data/lib/hecks/behaviors/rspec.rb +2 -0
  28. data/lib/hecks/behaviors/runner.rb +4 -0
  29. data/lib/hecks/bluebook/aggregate.rb +6 -5
  30. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
  31. data/lib/hecks/bluebook/assembly/build.rb +6 -3
  32. data/lib/hecks/bluebook/assembly/contract.rb +5 -1
  33. data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
  34. data/lib/hecks/bluebook/assembly/marks.rb +2 -1
  35. data/lib/hecks/bluebook/assembly.rb +1 -3
  36. data/lib/hecks/bluebook/attribute.rb +6 -0
  37. data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
  38. data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
  39. data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
  40. data/lib/hecks/bluebook/command.rb +6 -5
  41. data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
  42. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
  43. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +19 -472
  44. data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
  45. data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
  46. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
  47. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
  48. data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
  49. data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
  50. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
  51. data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
  52. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
  53. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
  54. data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
  55. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
  56. data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
  57. data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
  58. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
  59. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
  60. data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
  61. data/lib/hecks/bluebook/dsl/read_model_builder.rb +11 -3
  62. data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
  63. data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
  64. data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
  65. data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
  66. data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
  67. data/lib/hecks/bluebook/entity.rb +5 -3
  68. data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
  69. data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
  70. data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
  71. data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
  72. data/lib/hecks/bluebook/expression/projection.json +12 -0
  73. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
  74. data/lib/hecks/bluebook/expression/resolver.rb +83 -11
  75. data/lib/hecks/bluebook/expression.rb +1 -0
  76. data/lib/hecks/bluebook/hexagon.rb +10 -0
  77. data/lib/hecks/bluebook/lifecycle.rb +9 -0
  78. data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
  79. data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
  80. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
  81. data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
  82. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
  83. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
  84. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
  85. data/lib/hecks/bluebook/meta_validator.rb +5 -5
  86. data/lib/hecks/bluebook/model_check.rb +119 -82
  87. data/lib/hecks/bluebook/pattern_subset.rb +9 -0
  88. data/lib/hecks/bluebook/policy.rb +2 -1
  89. data/lib/hecks/bluebook/process_manager.rb +6 -0
  90. data/lib/hecks/bluebook/project_register.rb +45 -25
  91. data/lib/hecks/bluebook/query.rb +4 -0
  92. data/lib/hecks/bluebook/read_model.rb +2 -2
  93. data/lib/hecks/bluebook/smoke_test.rb +5 -0
  94. data/lib/hecks/bluebook/synthesizer.rb +2 -1
  95. data/lib/hecks/bluebook/translation.rb +3 -0
  96. data/lib/hecks/bluebook/value_object.rb +2 -2
  97. data/lib/hecks/codemod.rb +13 -0
  98. data/lib/hecks/doc/reference.rb +17 -11
  99. data/lib/hecks/facade/cli_runner.rb +2 -2
  100. data/lib/hecks/facade/command_request.rb +11 -11
  101. data/lib/hecks/facade/handle.rb +3 -3
  102. data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
  103. data/lib/hecks/facade.rb +4 -0
  104. data/lib/hecks/forms/app.rb +10 -6
  105. data/lib/hecks/forms/command_form_renderer.rb +3 -3
  106. data/lib/hecks/forms/field_renderer.rb +14 -9
  107. data/lib/hecks/forms/field_shape.rb +7 -2
  108. data/lib/hecks/forms/html.rb +5 -2
  109. data/lib/hecks/forms/index_renderer.rb +1 -1
  110. data/lib/hecks/forms/page.rb +7 -3
  111. data/lib/hecks/forms/query_form_renderer.rb +3 -2
  112. data/lib/hecks/forms/record_renderer.rb +22 -16
  113. data/lib/hecks/forms.rb +4 -0
  114. data/lib/hecks/fqn.rb +21 -9
  115. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
  116. data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
  117. data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
  118. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
  119. data/lib/hecks/fuzzing/properties/guards.rb +251 -0
  120. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
  121. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
  122. data/lib/hecks/fuzzing/properties/querying.rb +206 -0
  123. data/lib/hecks/fuzzing/properties.rb +41 -1065
  124. data/lib/hecks/fuzzing/replay.rb +60 -16
  125. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
  126. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
  127. data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
  128. data/lib/hecks/fuzzing/value_generator.rb +2 -2
  129. data/lib/hecks/grammar/evolve.rb +34 -10
  130. data/lib/hecks/grammar/expression_operators.json +124 -0
  131. data/lib/hecks/grammar.rb +15 -5
  132. data/lib/hecks/ir.rb +9 -0
  133. data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
  134. data/lib/hecks/language/bluebook/command.bluebook +2 -1
  135. data/lib/hecks/language/bluebook/entity.bluebook +2 -1
  136. data/lib/hecks/language/bluebook/projection.bluebook +2 -1
  137. data/lib/hecks/language/bluebook/query.bluebook +4 -2
  138. data/lib/hecks/language/bluebook/shape.bluebook +2 -1
  139. data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
  140. data/lib/hecks/literal.rb +18 -8
  141. data/lib/hecks/naming.rb +6 -3
  142. data/lib/hecks/ports/access_control.rb +1 -1
  143. data/lib/hecks/ports/agent.rb +5 -3
  144. data/lib/hecks/ports/authentication.rb +3 -2
  145. data/lib/hecks/ports/authorization.rb +1 -1
  146. data/lib/hecks/ports/clock.rb +1 -1
  147. data/lib/hecks/ports/extraction.rb +5 -1
  148. data/lib/hecks/ports/identity_assignment.rb +1 -1
  149. data/lib/hecks/ports/identity_generation.rb +1 -1
  150. data/lib/hecks/ports/identity_resolution.rb +1 -1
  151. data/lib/hecks/ports/loading.rb +4 -1
  152. data/lib/hecks/ports/persistence/append_only.rb +40 -4
  153. data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
  154. data/lib/hecks/ports/persistence/plugin.rb +3 -0
  155. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
  156. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
  157. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
  158. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
  159. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
  160. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
  161. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
  162. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
  163. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
  164. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
  165. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
  166. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
  167. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
  168. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +23 -13
  169. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
  170. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
  171. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
  172. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
  173. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
  174. data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
  175. data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
  176. data/lib/hecks/ports/persistence.rb +7 -3
  177. data/lib/hecks/ports/projection.rb +13 -5
  178. data/lib/hecks/ports/query/in_memory.rb +8 -2
  179. data/lib/hecks/ports/query.rb +1 -3
  180. data/lib/hecks/ports.rb +2 -0
  181. data/lib/hecks/projections/diagrams.rb +11 -3
  182. data/lib/hecks/projections/ir.rb +2 -0
  183. data/lib/hecks/projections/model/deviations.rb +2 -1
  184. data/lib/hecks/projections/model.rb +15 -10
  185. data/lib/hecks/projections/vocabulary.rb +1 -3
  186. data/lib/hecks/projector/cli_projector.rb +49 -29
  187. data/lib/hecks/projector/docs_projector.rb +21 -12
  188. data/lib/hecks/projector/exporter.rb +10 -1
  189. data/lib/hecks/projector/narrate_projector.rb +83 -25
  190. data/lib/hecks/projector.rb +2 -2
  191. data/lib/hecks/query_ir.rb +60 -36
  192. data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
  193. data/lib/hecks/query_specification/common/comparators.rb +4 -0
  194. data/lib/hecks/query_specification/common/comparison.rb +8 -1
  195. data/lib/hecks/query_specification/common/dsl.rb +5 -0
  196. data/lib/hecks/query_specification/common/null_policy.rb +6 -2
  197. data/lib/hecks/query_specification/common/options.rb +17 -5
  198. data/lib/hecks/query_specification/field_path.rb +4 -4
  199. data/lib/hecks/query_specification/read_model/specification.rb +5 -2
  200. data/lib/hecks/router/namespace_installer.rb +22 -10
  201. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
  202. data/lib/hecks/runtime/command_interpreter.rb +104 -65
  203. data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
  204. data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
  205. data/lib/hecks/runtime/dependency_planning.rb +33 -21
  206. data/lib/hecks/runtime/dispatcher.rb +41 -7
  207. data/lib/hecks/runtime/entity_element.rb +25 -6
  208. data/lib/hecks/runtime/entity_interpreter.rb +28 -15
  209. data/lib/hecks/runtime/errors.rb +28 -2
  210. data/lib/hecks/runtime/instance.rb +9 -3
  211. data/lib/hecks/runtime/interpreting.rb +29 -2
  212. data/lib/hecks/runtime/loader.rb +21 -0
  213. data/lib/hecks/runtime/outbox.rb +365 -0
  214. data/lib/hecks/runtime/policy_interpreter.rb +32 -17
  215. data/lib/hecks/runtime/query_interpreter.rb +36 -16
  216. data/lib/hecks/runtime/reaction_invocation.rb +35 -22
  217. data/lib/hecks/runtime/read_model_interpreter.rb +26 -3
  218. data/lib/hecks/runtime/reference_hop.rb +1 -1
  219. data/lib/hecks/runtime/refusal_wording.rb +55 -49
  220. data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
  221. data/lib/hecks/runtime/registry/verification.rb +39 -2
  222. data/lib/hecks/runtime/registry.rb +21 -11
  223. data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
  224. data/lib/hecks/runtime/routing.rb +52 -39
  225. data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
  226. data/lib/hecks/runtime/saga_interpreter.rb +119 -90
  227. data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
  228. data/lib/hecks/runtime/tenant_scope.rb +7 -0
  229. data/lib/hecks/runtime/value/coercion.rb +207 -48
  230. data/lib/hecks/runtime/value.rb +1 -1
  231. data/lib/hecks/runtime.rb +7 -2
  232. data/lib/hecks/storehouse.rb +15 -8
  233. data/lib/hecks/version.rb +1 -1
  234. data/lib/hecks/vocabulary.rb +1 -0
  235. data/lib/hecks.rb +17 -13
  236. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
  237. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
  238. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
  239. metadata +14 -2
@@ -74,27 +74,43 @@ module Hecks
74
74
  held_attrs = (held_shape["attributes"] || []).to_h { |attribute| [attribute["name"], attribute] }
75
75
  current_attrs = (current_shape["attributes"] || []).to_h { |attribute| [attribute["name"], attribute] }
76
76
 
77
- # retype pass: same attribute name, same member structure, the
78
- # TYPE's own name changed
79
- (held_attrs.keys & current_attrs.keys).each do |name|
77
+ rules.concat(retype_rules(held_attrs, current_attrs))
78
+ retyped = rules.filter_map { |rule| rule[:from] if rule[:kind] == :retype }
79
+
80
+ vanished = vanished_paths(held_attrs, current_attrs, retyped)
81
+ appeared = appeared_paths(held_attrs, current_attrs, retyped)
82
+
83
+ # Mutates `rules` in place (not build-and-concat, like the two
84
+ # passes above) because it must READ its own earlier writes:
85
+ # `taken`, below, is recomputed from `rules` at the top of every
86
+ # iteration, so a target this same loop already claimed for an
87
+ # earlier vanished path is excluded from a later one. Passing a
88
+ # snapshot instead of the live array would let two vanished
89
+ # paths both match the same appeared target.
90
+ resolve_vanished_rules!(rules, vanished, appeared)
91
+ rules
92
+ end
93
+
94
+ # retype pass: same attribute name, same member structure, the
95
+ # TYPE's own name changed
96
+ def retype_rules(held_attrs, current_attrs)
97
+ (held_attrs.keys & current_attrs.keys).filter_map do |name|
80
98
  held = held_attrs[name]
81
99
  current = current_attrs[name]
82
100
  next if held == current
83
101
  next unless container?(held) && container?(current)
84
102
  next unless held["type"]["members"] == current["type"]["members"] && held["list"] == current["list"]
85
103
 
86
- rules << { kind: :retype, from: held["type"]["type"], to: current["type"]["type"] }
104
+ { kind: :retype, from: held["type"]["type"], to: current["type"]["type"] }
87
105
  end
88
- retyped = rules.filter_map { |rule| rule[:from] if rule[:kind] == :retype }
89
-
90
- vanished = vanished_paths(held_attrs, current_attrs, retyped)
91
- appeared = appeared_paths(held_attrs, current_attrs, retyped)
106
+ end
92
107
 
108
+ def resolve_vanished_rules!(rules, vanished, appeared)
93
109
  vanished.each do |path, signature|
94
110
  matches = appeared.select { |_, candidate| candidate == signature }.keys
95
111
  taken = rules.filter_map { |rule| rule[:to] if %i[rename move].include?(rule[:kind]) }
96
112
  matches -= taken
97
- if matches.size == 1 && vanished.count { |_, other| other == signature } == 1
113
+ if matches.size == 1 && vanished.one? { |_, other| other == signature }
98
114
  target = matches.first
99
115
  kind = path.include?(".") || target.include?(".") ? :move : :rename
100
116
  rules << { kind: kind, from: path, to: target }
@@ -103,7 +119,6 @@ module Hecks
103
119
  rules << { kind: :unresolved, from: path, candidates: candidates }
104
120
  end
105
121
  end
106
- rules
107
122
  end
108
123
 
109
124
  # Held paths that need explaining: whole attributes that vanished,
@@ -10,12 +10,8 @@ module Hecks
10
10
  edge.aggregates.each do |aggregate|
11
11
  header = " aggregate #{aggregate.name.inspect}"
12
12
  header += ", was: #{aggregate.was.inspect}" if aggregate.was
13
- if aggregate.rules.empty?
14
- lines << "#{header} do"
15
- else
16
- lines << "#{header} do"
17
- aggregate.rules.each { |rule| lines << " #{render_rule(rule)}" }
18
- end
13
+ lines << "#{header} do"
14
+ aggregate.rules.each { |rule| lines << " #{render_rule(rule)}" } unless aggregate.rules.empty?
19
15
  lines << " end"
20
16
  end
21
17
  edge.retired.each { |name| lines << " retired #{name.inspect}" }
@@ -35,7 +35,9 @@ module Hecks
35
35
  # unchanged: registered only when this registry has an aggregate
36
36
  # actually bound to a lineage-capable adapter.
37
37
  def contribute_boot_gates(registry, gates)
38
- gates.register(:era_compute_rules, ->(reg, _dir) { Runtime::EraCheck.check_compute_rules_for_registry!(reg) }, phase: :pre_verify)
38
+ gates.register(:era_compute_rules, lambda { |reg, _dir|
39
+ Runtime::EraCheck.check_compute_rules_for_registry!(reg)
40
+ }, phase: :pre_verify)
39
41
  gates.register(:era_check, Runtime::EraCheck.method(:check_lineage!), phase: :pre_verify) if
40
42
  Runtime::EraCheck.lineage_capable_registry?(registry)
41
43
  end
@@ -33,7 +33,7 @@ module Hecks
33
33
  "#{self.class.name} is a remote-runtime delegate — dispatch through " \
34
34
  "Runtime::RemoteDispatcher instead of writing through the repository directly"
35
35
  end
36
- alias_method :project, :append
36
+ alias project append
37
37
 
38
38
  def entries = []
39
39
  end
@@ -1,9 +1,9 @@
1
1
  module Hecks
2
2
  module Ports
3
3
  module Persistence
4
- NAME = "persistence"
5
- VERB = "persisted_by"
6
- DEFAULT_ADAPTER = "Memory"
4
+ NAME = "persistence".freeze
5
+ VERB = "persisted_by".freeze
6
+ DEFAULT_ADAPTER = "Memory".freeze
7
7
  end
8
8
  end
9
9
  end
@@ -18,6 +18,10 @@ require_relative "persistence/null_saga_store"
18
18
 
19
19
  module Hecks
20
20
  module Ports
21
+ # Reopened once `BindingPolicy`/`RepositoryFactory` (required above) are
22
+ # loaded, to add the persistence port's actual call surface: resolving
23
+ # which adapter authoritatively binds an aggregate and building the
24
+ # repository that reads/writes it.
21
25
  module Persistence
22
26
  module_function
23
27
 
@@ -7,8 +7,8 @@ module Hecks
7
7
  # Projection is a read-side port. Its stores are rebuildable consumers of
8
8
  # authoritative entries and never participate in command decisions.
9
9
  module Projection
10
- NAME = "projection"
11
- VERB = "projected_by"
10
+ NAME = "projection".freeze
11
+ VERB = "projected_by".freeze
12
12
 
13
13
  module_function
14
14
 
@@ -26,6 +26,11 @@ module Hecks
26
26
  Worker.new(authoritative, target, policy: policy)
27
27
  end
28
28
 
29
+ # Catches one projection's store up to its authoritative journal:
30
+ # replays whatever entries the store hasn't yet appended/projected.
31
+ # Constructed via Projection.worker; #catch_up! runs from a
32
+ # separate process or scheduler, never from the command-side write
33
+ # path.
29
34
  class Worker
30
35
  attr_reader :projection
31
36
 
@@ -67,7 +72,10 @@ module Hecks
67
72
  unless consistent?(entries, present)
68
73
  raise Runtime::WiringError, "projection history does not match its authoritative history" if @policy == :strict
69
74
  end
70
- entries.drop(present.length).each { |entry| @projection.append(entry); @projection.project(entry) }
75
+ entries.drop(present.length).each do |entry|
76
+ @projection.append(entry)
77
+ @projection.project(entry)
78
+ end
71
79
  @projection
72
80
  end
73
81
 
@@ -78,9 +86,9 @@ module Hecks
78
86
  def same_entry?(left, right) = left.operation == right.operation && left.id == right.id && left.state == right.state
79
87
 
80
88
  def consistent?(entries, present)
81
- present.length <= entries.length && entries.first(present.length).zip(present).all? { |left, right|
89
+ present.length <= entries.length && entries.first(present.length).zip(present).all? do |left, right|
82
90
  same_entry?(left, right)
83
- }
91
+ end
84
92
  end
85
93
  end
86
94
 
@@ -6,6 +6,12 @@ require_relative "../../query_specification/common/comparison"
6
6
  module Hecks
7
7
  module Ports
8
8
  module Query
9
+ # In-memory implementation of a declared query specification:
10
+ # applies wheres/order_by/offset/limit directly to an Array of
11
+ # records instead of compiling SQL. Deliberately kept in exact
12
+ # LIMIT/OFFSET-order agreement with SqlQueryBuilder (see #execute's
13
+ # own comment) so a query answers the same page regardless of
14
+ # which adapter backs it.
9
15
  module InMemory
10
16
  FieldPath = QuerySpecification::FieldPath
11
17
  Comparison = QuerySpecification::Common::Comparison
@@ -14,9 +20,9 @@ module Hecks
14
20
 
15
21
  def execute(records, declared, args = {}, registry: nil)
16
22
  matched = records.select do |record|
17
- declared.wheres.all? { |clause|
23
+ declared.wheres.all? do |clause|
18
24
  holds?(clause, comparable(FieldPath.dig(record, clause.field)), args, registry: registry)
19
- }
25
+ end
20
26
  end
21
27
  field = declared.order_by&.field
22
28
  matched = Ordering.apply(matched, declared.order_by, declared.null_semantics,
@@ -19,9 +19,7 @@ module Hecks
19
19
  end
20
20
 
21
21
  def validate!(specification, adapter)
22
- if specification.cursor && specification.offset
23
- raise Unsupported, "a query cannot combine cursor and offset pagination"
24
- end
22
+ raise Unsupported, "a query cannot combine cursor and offset pagination" if specification.cursor && specification.offset
25
23
 
26
24
  return if specification.inspection.nil? || adapter.respond_to?(:inspect_query)
27
25
  return if specification.inspection.mode.to_s == "sql" && adapter.respond_to?(:query)
data/lib/hecks/ports.rb CHANGED
@@ -8,6 +8,8 @@
8
8
  # and the Folder adapter load as data.
9
9
 
10
10
  module Hecks
11
+ # Declared empty here — each `ports/*.rb` file required below reopens it
12
+ # to add its own port; see the header comment above for what a port is.
11
13
  module Ports
12
14
  end
13
15
  end
@@ -448,9 +448,17 @@ module Hecks
448
448
  # single source at all — its own field NAMES are the fact worth
449
449
  # stating (real: `Order.AddTopping` appends `name, amount`).
450
450
  def surface_diagram(bluebook, holder)
451
- lines = holder.commands.map { |command| " #{holder.hecks_name}[(#{holder.hecks_name})] -->|does| #{command_node(holder.hecks_name, command.hecks_name)}" }
452
- lines += holder.commands.flat_map { |command| command.mutations.map { |mutation| mutation_edge(holder, command, mutation) } }
453
- lines += holder.queries.map { |query| " #{holder.hecks_name}[(#{holder.hecks_name})] -.->|asks| #{query_node(holder.hecks_name, query.hecks_name)}" }
451
+ lines = holder.commands.map do |command|
452
+ " #{holder.hecks_name}[(#{holder.hecks_name})] -->|does| #{command_node(holder.hecks_name, command.hecks_name)}"
453
+ end
454
+ lines += holder.commands.flat_map do |command|
455
+ command.mutations.map do |mutation|
456
+ mutation_edge(holder, command, mutation)
457
+ end
458
+ end
459
+ lines += holder.queries.map do |query|
460
+ " #{holder.hecks_name}[(#{holder.hecks_name})] -.->|asks| #{query_node(holder.hecks_name, query.hecks_name)}"
461
+ end
454
462
 
455
463
  subject = "#{holder.hecks_name}'s own declared commands (and what each writes) and queries"
456
464
  "#{header(bluebook.name, subject)}flowchart LR\n#{lines.uniq.join("\n")}\n"
@@ -1,6 +1,8 @@
1
1
  require_relative "../projector"
2
2
 
3
3
  module Hecks
4
+ # Reopened from projections.rb (see there for the namespace's full
5
+ # rationale) to define and register the :ir target below.
4
6
  module Projections
5
7
  # The canonical IR, as a constant. The implementation already existed
6
8
  # and is already golden-tested (`Projector::IRProjector`, registered
@@ -60,7 +60,8 @@ module Hecks
60
60
  "Bluebook" => { ir_version: "the EMISSION's own version, not the domain's",
61
61
  canonical_form: "the normalisation table every reader needs beside the IR" },
62
62
  "ValueObject" => { closed_set: "an empty one_of and no one_of are otherwise indistinguishable" },
63
- "Aggregate" => { ports: "declared in the hecksagon, attached after the aggregate exists" }
63
+ "Aggregate" => { ports: "declared in the hecksagon, attached after the aggregate exists" },
64
+ "Policy" => { where_ast: "the structured form of `where`, derived from the same text at emission" }
64
65
  }.freeze
65
66
 
66
67
  # DECLARED, AND DELIBERATELY NOT EMITTED. The category that had no
@@ -50,7 +50,11 @@ module Hecks
50
50
  # a binding that reads an event field and one that supplies a
51
51
  # literal string are otherwise indistinguishable once written
52
52
  # down (see `MetaValidator::Readings`' own note on exactly that).
53
- renders: { with_spec: "-> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }" },
53
+ renders: { with_spec: "-> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }",
54
+ # COMPUTED (Deviations::COMPUTED["Policy"]): the structured
55
+ # form of `where`, a pure function of that text, the same
56
+ # `ast` every rule row carries beside its `canonical`.
57
+ where_ast: "-> { where_ast }" },
54
58
  settles: false
55
59
  }
56
60
  }.freeze
@@ -66,7 +70,7 @@ module Hecks
66
70
  # GENERATED — projected from the language's own #{name} aggregate.
67
71
  # DO NOT EDIT: the holding half is rendered, and #{host.fetch(:behaviour)}
68
72
  # is where anything hand-written belongs.
69
- require_relative "behaviour/#{File.basename(host.fetch(:file), ".rb")}"
73
+ require_relative "behaviour/#{File.basename(host.fetch(:file), '.rb')}"
70
74
 
71
75
  module Hecks
72
76
  module Bluebook
@@ -87,14 +91,15 @@ module Hecks
87
91
 
88
92
  # The emission, keyed as the model spells it and sourced as the
89
93
  # language declares it.
94
+ # A COMPUTED field (Deviations::COMPUTED) is emitted too — it is
95
+ # model-only by definition, so it rides after the declared fields
96
+ # and MUST have a `renders` entry, there being nothing to `send`.
90
97
  def emits(bluebook, name)
91
- fields = emitted_fields(bluebook, name)
98
+ fields = emitted_fields(bluebook, name) + Deviations.computed(name)
92
99
  renders = HOST.fetch(name).fetch(:renders, {})
93
100
  width = fields.map { |f| f.to_s.length }.max.to_i
94
101
 
95
- "emits_ir(\n" +
96
- fields.map { |f| " #{"#{f}:".ljust(width + 1)} #{renders.fetch(f, ":#{f}")}" }.join(",\n") +
97
- "\n)"
102
+ "emits_ir(\n#{fields.map { |f| " #{"#{f}:".ljust(width + 1)} #{renders.fetch(f, ":#{f}")}" }.join(",\n")}\n)"
98
103
  end
99
104
 
100
105
  # WHAT THE CONSTRUCT EMITS: what the language declares, less every
@@ -121,18 +126,18 @@ module Hecks
121
126
  # here rather than typed in — a comment that survives
122
127
  # regeneration is one the generator writes.
123
128
  reasons = Deviations::OFF_THE_WIRE.fetch(host.fetch(:construct, ""), {})
124
- (lines + accessors.map { |a|
129
+ (lines + accessors.map do |a|
125
130
  why = reasons[a]
126
131
  (why ? "\n# #{a.upcase}, DECLARED AND DELIBERATELY OFF THE WIRE\n# #{wrap(why)}\n" : "") +
127
132
  "attr_accessor :#{a}"
128
- }).join("\n")
133
+ end).join("\n")
129
134
  end
130
135
 
131
136
  def constructor(host)
132
137
  args = host.fetch(:defaults).map { |f, d| d ? "#{f}: #{d}" : "#{f}:" }.join(", ")
133
- body = host.fetch(:defaults).keys.map { |f|
138
+ body = host.fetch(:defaults).keys.map do |f|
134
139
  " @#{f} = #{f}#{host.fetch(:coerce, {})[f]}"
135
- }
140
+ end
136
141
  body << "\n settle" if host.fetch(:settles, true)
137
142
 
138
143
  "def initialize(#{args})\n#{body.join("\n")}\nend"
@@ -58,9 +58,7 @@ module Hecks
58
58
 
59
59
  def render(bluebook)
60
60
  rows = tables(bluebook).sort_by(&:first).map do |name, members|
61
- " #{name.inspect} => [\n" +
62
- members.map { |row| " #{row.inspect}.freeze" }.join(",\n") +
63
- "\n ].freeze"
61
+ " #{name.inspect} => [\n#{members.map { |row| " #{row.inspect}.freeze" }.join(",\n")}\n ].freeze"
64
62
  end
65
63
 
66
64
  <<~RUBY
@@ -54,12 +54,12 @@ module Hecks
54
54
  aggregate.queries.each { |q| claim(questions, name_for(aggregate, q), query_spec(bluebook, aggregate, nil, q)) }
55
55
 
56
56
  aggregate.entities.each do |entity|
57
- entity.commands.each { |c|
57
+ entity.commands.each do |c|
58
58
  claim(verbs, name_for(aggregate, c, entity), command_spec(bluebook, aggregate, entity, c))
59
- }
60
- entity.queries.each { |q|
59
+ end
60
+ entity.queries.each do |q|
61
61
  claim(questions, name_for(aggregate, q, entity), query_spec(bluebook, aggregate, entity, q))
62
- }
62
+ end
63
63
  end
64
64
 
65
65
  # A PORT IS A VERB TOO, and leaving it off the map was a real gap
@@ -235,7 +235,11 @@ module Hecks
235
235
  # `CommandInterpreter`/`EntityInterpreter` call it, never the port
236
236
  # dispatch path), so `role_gated: false` always, unlike
237
237
  # `command_spec` where the same key name means a real one.
238
- role: operation.outbound? ? "#{aggregate.hecks_name} asking #{port.name}" : "#{port.name} telling #{aggregate.hecks_name}",
238
+ role: if operation.outbound?
239
+ "#{aggregate.hecks_name} asking #{port.name}"
240
+ else
241
+ "#{port.name} telling #{aggregate.hecks_name}"
242
+ end,
239
243
  role_gated: false,
240
244
  summary: port_summary(port, operation), arguments: arguments }
241
245
  end
@@ -289,8 +293,8 @@ module Hecks
289
293
  # malformed records into a real store, which is what it did on its first
290
294
  # run against the pizzas database.
291
295
  def options_for(attribute, holder, aggregate, prefix = nil, optional = nil)
292
- path = [prefix, attribute.name].compact.join(".")
293
- optional = optional || attribute.optional?
296
+ path = [prefix, attribute.name].compact.join(".")
297
+ optional ||= attribute.optional?
294
298
  return [reference_option(attribute)] if attribute.reference?
295
299
 
296
300
  value_object = value_object_for(attribute, holder, aggregate)
@@ -408,38 +412,54 @@ module Hecks
408
412
  text.to_s.split(/(?<=\.)\s/).first.to_s
409
413
  end
410
414
 
415
+ # FOUR TEXT BLOCKS, IN FIXED DISPLAY ORDER — meta (name/kind/role),
416
+ # invocation, arguments, refusals. Each block is independent of the
417
+ # others' content (only the OUTPUT ORDER is fixed, and stays fixed
418
+ # below), so each is its own method returning the lines it
419
+ # contributes — `[]` when it contributes none — concatenated in the
420
+ # same order the original inline version built them in.
411
421
  def verb_help(program, name, spec, ask: false)
422
+ out = verb_help_meta_lines(name, spec)
423
+ out.concat(verb_help_invocation_lines(program, name, spec, ask))
424
+ out.concat(verb_help_argument_lines(spec))
425
+ out.concat(verb_help_refusal_lines(spec))
426
+ out.join("\n")
427
+ end
428
+
429
+ def verb_help_meta_lines(name, spec)
412
430
  out = ["#{name} — #{spec[:summary]}", ""]
413
431
  out << "dispatches #{spec[:verb]}" if spec[:kind] == :command
414
432
  out << "reads #{spec[:verb]}" if spec[:kind] == :query
415
433
  out << "issued by #{spec[:role]}" if spec[:role]
416
- out << ""
434
+ out
435
+ end
436
+
437
+ def verb_help_invocation_lines(program, name, spec, ask)
417
438
  invocation = ask ? "#{program} ask #{name}" : "#{program} #{name}"
418
- out << " #{invocation}#{spec[:arguments].map { |a| " #{a[:path]}=…" }.join}"
419
- out << ""
439
+ ["", " #{invocation}#{spec[:arguments].map { |a| " #{a[:path]}=…" }.join}", ""]
440
+ end
420
441
 
421
- unless spec[:arguments].empty?
422
- width = spec[:arguments].map { |a| a[:path].length }.max
423
- spec[:arguments].each do |argument|
424
- notes = []
425
- notes << argument[:type]
426
- notes << "one of #{argument[:enum].join(', ')}" if argument[:enum]
427
- notes << "matches #{argument[:pattern]}" if argument[:pattern]
428
- notes << "defaults to #{argument[:default].inspect}" unless argument[:default].nil?
429
- notes << argument[:note] if argument[:note]
430
- notes << "optional" unless argument[:required]
431
- out << " #{argument[:path].ljust(width)} #{notes.join('; ')}"
432
- end
433
- out << ""
442
+ def verb_help_argument_lines(spec)
443
+ return [] if spec[:arguments].empty?
444
+
445
+ width = spec[:arguments].map { |a| a[:path].length }.max
446
+ lines = spec[:arguments].map do |argument|
447
+ notes = []
448
+ notes << argument[:type]
449
+ notes << "one of #{argument[:enum].join(', ')}" if argument[:enum]
450
+ notes << "matches #{argument[:pattern]}" if argument[:pattern]
451
+ notes << "defaults to #{argument[:default].inspect}" unless argument[:default].nil?
452
+ notes << argument[:note] if argument[:note]
453
+ notes << "optional" unless argument[:required]
454
+ " #{argument[:path].ljust(width)} #{notes.join('; ')}"
434
455
  end
456
+ lines << ""
457
+ end
435
458
 
436
- unless Array(spec[:refusals]).empty?
437
- out << "refused when:"
438
- spec[:refusals].each { |refusal| out << " #{refusal}" }
439
- out << ""
440
- end
459
+ def verb_help_refusal_lines(spec)
460
+ return [] if Array(spec[:refusals]).empty?
441
461
 
442
- out.join("\n")
462
+ ["refused when:", *spec[:refusals].map { |refusal| " #{refusal}" }, ""]
443
463
  end
444
464
  end
445
465
  end
@@ -52,7 +52,7 @@ module Hecks
52
52
  out << chapter_header(bluebook, depth) unless only
53
53
  Array(aggregates(bluebook, only)).each { |aggregate| out << aggregate_section(aggregate, only ? depth : depth + 1) }
54
54
  out << closing(bluebook, depth + 1) unless only
55
- out.compact.join("\n").rstrip + "\n"
55
+ "#{out.compact.join("\n").rstrip}\n"
56
56
  end
57
57
 
58
58
  # A NAME THAT NAMES NOTHING IS REFUSED, not answered with an empty
@@ -179,7 +179,7 @@ module Hecks
179
179
  value_object = value_object_for(attribute, holder)
180
180
  inner =
181
181
  if value_object
182
- "{ " + value_object.attributes.map { |f| "#{f.name}: #{f.type}" }.join(", ") + " }"
182
+ "{ #{value_object.attributes.map { |f| "#{f.name}: #{f.type}" }.join(', ')} }"
183
183
  else
184
184
  attribute.type.to_s
185
185
  end
@@ -244,19 +244,13 @@ module Hecks
244
244
  end
245
245
 
246
246
  def command_entry(command, holder, depth)
247
- out = [h(depth, "#{command.hecks_name}#{command.creates? ? ' *(creates)*' : ''}"), ""]
247
+ out = [h(depth, "#{command.hecks_name}#{' *(creates)*' if command.creates?}"), ""]
248
248
  out += [command.goal, ""] if command.goal
249
249
  out << "Issued by: **#{command.role}**." if command.role
250
250
  out << ""
251
251
 
252
252
  arguments = command.attributes
253
- unless arguments.empty?
254
- rows = arguments.map do |attribute|
255
- shape = attribute.reference? ? "id of a `#{attribute.type.target_name}`" : shape_of(attribute, holder)
256
- ["`#{attribute.name}`", shape, attribute.optional? ? "" : "required"]
257
- end
258
- out << table(%w[argument shape needed], rows)
259
- end
253
+ out << table(%w[argument shape needed], command_argument_rows(arguments, holder)) unless arguments.empty?
260
254
 
261
255
  refusals = refusals_of(command, holder)
262
256
  unless refusals.empty?
@@ -272,6 +266,17 @@ module Hecks
272
266
  out.join("\n")
273
267
  end
274
268
 
269
+ # The argument table's own rows — a pure per-attribute mapping with
270
+ # nothing to share with `command_entry`'s other sections, extracted
271
+ # only to keep that method to the one shape every section there
272
+ # follows: build a chunk, append it if non-empty.
273
+ def command_argument_rows(arguments, holder)
274
+ arguments.map do |attribute|
275
+ shape = attribute.reference? ? "id of a `#{attribute.type.target_name}`" : shape_of(attribute, holder)
276
+ ["`#{attribute.name}`", shape, attribute.optional? ? "" : "required"]
277
+ end
278
+ end
279
+
275
280
  # EVERY WAY THIS VERB CAN SAY NO, gathered from the three places a
276
281
  # chapter states them — the lifecycle it is an edge of, its own
277
282
  # `given`s, and the fact that a reference has to resolve. A caller
@@ -284,7 +289,11 @@ module Hecks
284
289
  froms = lifecycle && lifecycle.transitions.filter_map do |name, transition|
285
290
  Array(transition.from) if name.to_s == command.hecks_name
286
291
  end.flatten.uniq
287
- refusals << "`#{lifecycle.field}` is anything other than #{froms.map { |f| "`#{f}`" }.join(' or ')}" if froms && !froms.empty?
292
+ if froms && !froms.empty?
293
+ refusals << "`#{lifecycle.field}` is anything other than #{froms.map do |f|
294
+ "`#{f}`"
295
+ end.join(' or ')}"
296
+ end
288
297
 
289
298
  command.attributes.select(&:reference?).each do |reference|
290
299
  refusals << "no `#{reference.type.target_name}` exists for the id given as `#{reference.name}`"
@@ -302,7 +311,7 @@ module Hecks
302
311
  queries.each do |query|
303
312
  shape = query.to_h
304
313
  takes = Array(shape[:attributes]).map { |a| "`#{a[:name]}`" }.join(", ")
305
- out << "**#{query.hecks_name}**#{takes.empty? ? '' : " (#{takes})"} "
314
+ out << "**#{query.hecks_name}**#{" (#{takes})" unless takes.empty?} "
306
315
  out << (query.description ? "#{query.description} " : "")
307
316
  filters = Array(shape[:wheres]).map { |w| "`#{w[:field]} #{w[:op]} #{w[:value].inspect}`" }
308
317
  out << "Filters: #{filters.join(', ')}." unless filters.empty?
@@ -3,6 +3,13 @@ require_relative "../ports/persistence"
3
3
 
4
4
  module Hecks
5
5
  module Projector
6
+ # Registry-wide serialization to Hash/JSON: bluebook IR
7
+ # (`call`/`json`), era-adapter lineage-capability flags (`lineage`),
8
+ # and translation edges in both their DIGEST-relevant declared shape
9
+ # (`translation_hash`, what ApprovalDigest hashes) and their
10
+ # consumer-ready COMPILED shape with precompiled SQL attached
11
+ # (`translations`/`compiled_translation_aggregate`). Read directly by
12
+ # bin/ir, bin/project_rust, and the translation/audit approval digest.
6
13
  module Exporter
7
14
  module_function
8
15
 
@@ -150,7 +157,9 @@ module Hecks
150
157
 
151
158
  translation_aggregate(aggregate).merge(
152
159
  compiled_state_expression: Translation::RuleCompiler.compile_rules(aggregate),
153
- compiled_id_expression: Translation::RuleCompiler.rekeyed?(aggregate) ? Translation::RuleCompiler.compile_id_expression(aggregate) : nil
160
+ compiled_id_expression: (if Translation::RuleCompiler.rekeyed?(aggregate)
161
+ Translation::RuleCompiler.compile_id_expression(aggregate)
162
+ end)
154
163
  )
155
164
  end
156
165
  end