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
@@ -72,7 +72,11 @@ module Hecks
72
72
  parts << identity_sentence(aggregate)
73
73
 
74
74
  refs = aggregate.attributes.select(&:reference?)
75
- parts << "Each one is linked to #{to_sentence_list(refs.map { |r| "#{a_or_an(r.type.target_name)} #{r.type.target_name}" })}." unless refs.empty?
75
+ unless refs.empty?
76
+ parts << "Each one is linked to #{to_sentence_list(refs.map do |r|
77
+ "#{a_or_an(r.type.target_name)} #{r.type.target_name}"
78
+ end)}."
79
+ end
76
80
 
77
81
  parts << lifecycle_narrative(aggregate)
78
82
  parts << verbs_narrative(aggregate, depth + 1)
@@ -124,35 +128,84 @@ module Hecks
124
128
  "#{header}\n\n#{body}"
125
129
  end
126
130
 
131
+ # ONE PARAGRAPH, BUILT FROM INDEPENDENT SENTENCES — each sentence
132
+ # below states one unrelated fact about `command` (its goal, who
133
+ # issues it, whether it creates the holder, what it takes, what it
134
+ # references, what gates it, what it guarantees, what it emits), in
135
+ # the fixed order a reader expects them; nothing after the first
136
+ # sentence depends on anything before it. Same nil-or-string +
137
+ # `compact.join` shape `aggregate_narrative`/`entity_narrative`
138
+ # already use above for the identical reason.
127
139
  def command_paragraph(command, holder)
128
- # THE GOAL, VERBATIM — same rule `DocsProjector` holds to: quoted
129
- # exactly as declared, not recased to fit mid-sentence, because the
130
- # promise this whole projector makes is that a sentence here is a
131
- # sentence the chapter actually wrote.
132
- sentences = ["**#{command.hecks_name}**#{command.goal ? " — #{command.goal}." : '.'}"]
133
- sentences << "Issued by #{a_or_an(command.role)} #{command.role}." if command.role
134
- # `acts_on.nil?`, NOT `creates?` — `creates?` answers true for every
135
- # verb an ENTITY declares (it never references itself; see
136
- # `Command#acts_on`'s own comment), so reading it directly here would
137
- # tell an SME that `LedgerEntry.Amend` brings a new ledger entry into
138
- # being, which is exactly backwards.
139
- sentences << "This is how a new #{holder.hecks_name} comes into being." if command.acts_on.nil?
140
+ [
141
+ command_headline_sentence(command),
142
+ command_role_sentence(command),
143
+ command_creation_sentence(command, holder),
144
+ command_arguments_sentence(command),
145
+ command_references_sentence(command),
146
+ command_conditions_sentence(command, holder),
147
+ command_guarantees_sentence(command),
148
+ command_emits_sentence(command)
149
+ ].compact.join(" ")
150
+ end
151
+
152
+ # THE GOAL, VERBATIM — same rule `DocsProjector` holds to: quoted
153
+ # exactly as declared, not recased to fit mid-sentence, because the
154
+ # promise this whole projector makes is that a sentence here is a
155
+ # sentence the chapter actually wrote.
156
+ def command_headline_sentence(command)
157
+ "**#{command.hecks_name}**#{command.goal ? " — #{command.goal}." : '.'}"
158
+ end
159
+
160
+ def command_role_sentence(command)
161
+ return nil unless command.role
162
+
163
+ "Issued by #{a_or_an(command.role)} #{command.role}."
164
+ end
165
+
166
+ # `acts_on.nil?`, NOT `creates?` — `creates?` answers true for every
167
+ # verb an ENTITY declares (it never references itself; see
168
+ # `Command#acts_on`'s own comment), so reading it directly here would
169
+ # tell an SME that `LedgerEntry.Amend` brings a new ledger entry into
170
+ # being, which is exactly backwards.
171
+ def command_creation_sentence(command, holder)
172
+ return nil unless command.acts_on.nil?
173
+
174
+ "This is how a new #{holder.hecks_name} comes into being."
175
+ end
140
176
 
177
+ def command_arguments_sentence(command)
141
178
  arguments = command.attributes.reject(&:reference?)
142
- sentences << "It takes #{to_sentence_list(arguments.map { |a| Forms::Humanize.label(a.name.to_s).downcase })}." unless arguments.empty?
179
+ return nil if arguments.empty?
143
180
 
181
+ "It takes #{to_sentence_list(arguments.map { |a| Forms::Humanize.label(a.name.to_s).downcase })}."
182
+ end
183
+
184
+ def command_references_sentence(command)
144
185
  refs = command.attributes.select(&:reference?)
145
- sentences << "It's aimed at one existing #{to_sentence_list(refs.map { |r| r.type.target_name })}, by id." unless refs.empty?
186
+ return nil if refs.empty?
146
187
 
188
+ "It's aimed at one existing #{to_sentence_list(refs.map { |r| r.type.target_name })}, by id."
189
+ end
190
+
191
+ def command_conditions_sentence(command, holder)
147
192
  conditions = conditions_of(command, holder)
148
- sentences << "It only goes through if #{conditions.join('; ')}." unless conditions.empty?
193
+ return nil if conditions.empty?
149
194
 
195
+ "It only goes through if #{conditions.join('; ')}."
196
+ end
197
+
198
+ def command_guarantees_sentence(command)
150
199
  guarantees = command.ensures.map(&:description)
151
- sentences << "When it succeeds: #{guarantees.join('; ')}." unless guarantees.empty?
200
+ return nil if guarantees.empty?
201
+
202
+ "When it succeeds: #{guarantees.join('; ')}."
203
+ end
152
204
 
153
- sentences << "It records `#{command.emits.join('`, `')}` as a fact." unless command.emits.empty?
205
+ def command_emits_sentence(command)
206
+ return nil if command.emits.empty?
154
207
 
155
- sentences.join(" ")
208
+ "It records `#{command.emits.join('`, `')}` as a fact."
156
209
  end
157
210
 
158
211
  # EVERY REQUIRED CONDITION, STATED AS SOMETHING THAT MUST BE TRUE —
@@ -166,10 +219,14 @@ module Hecks
166
219
 
167
220
  lifecycle = holder.lifecycle
168
221
  if lifecycle
169
- froms = lifecycle.transitions.filter_map { |name, transition|
222
+ froms = lifecycle.transitions.filter_map do |name, transition|
170
223
  Array(transition.from) if name.to_s == command.hecks_name
171
- }.flatten.uniq
172
- conditions << "its `#{lifecycle.field}` is currently #{to_sentence_list(froms.map { |f| "`#{f}`" }, conj: 'or')}" unless froms.empty?
224
+ end.flatten.uniq
225
+ unless froms.empty?
226
+ conditions << "its `#{lifecycle.field}` is currently #{to_sentence_list(froms.map do |f|
227
+ "`#{f}`"
228
+ end, conj: 'or')}"
229
+ end
173
230
  end
174
231
 
175
232
  conditions + command.givens.map(&:description)
@@ -200,8 +257,8 @@ module Hecks
200
257
  "#{header}\n\n#{lines.join("\n")}"
201
258
  end
202
259
 
203
- def op_words(op)
204
- { eq: "is", lt: "under", lte: "at most", gt: "over", gte: "at least" }[op.to_s.to_sym] || op.to_s
260
+ def op_words(comparator)
261
+ { eq: "is", lt: "under", lte: "at most", gt: "over", gte: "at least" }[comparator.to_s.to_sym] || comparator.to_s
205
262
  end
206
263
 
207
264
  # ── what happens on its own ───────────────────────────────────────
@@ -212,7 +269,8 @@ module Hecks
212
269
  parts = [DocsProjector.h(depth, "Reactions")]
213
270
  bluebook.policies.each do |policy|
214
271
  elsewhere = policy.target_domain ? " in #{policy.target_domain}" : ""
215
- parts << "Whenever `#{policy.on_event}` happens, `#{policy.trigger_command}` fires on its own#{elsewhere} — nobody has to ask for it."
272
+ parts << "Whenever `#{policy.on_event}` happens, `#{policy.trigger_command}` fires on its own#{elsewhere} — " \
273
+ "nobody has to ask for it."
216
274
  end
217
275
 
218
276
  bluebook.process_managers.each do |saga|
@@ -70,9 +70,9 @@ module Hecks
70
70
  # any construct that emits IR, and `admits!` is what decides whether
71
71
  # THIS target can actually take the one handed over.
72
72
  def call(name, bluebook:, options: {})
73
- projector = registry.fetch(name.to_sym) {
73
+ projector = registry.fetch(name.to_sym) do
74
74
  raise UnknownProjector, "no projector registered for #{name.inspect} — registered: #{registered.sort.inspect}"
75
- }
75
+ end
76
76
  admits!(name, projector, bluebook)
77
77
  projector.call(bluebook: bluebook, options: options)
78
78
  end
@@ -1,4 +1,3 @@
1
- require "set"
2
1
  require_relative "bluebook"
3
2
  require_relative "codemod"
4
3
  require_relative "projections/model"
@@ -49,9 +48,9 @@ module Hecks
49
48
  # makes, reusing its own Deviations data so this can never silently
50
49
  # drift from what that gate actually checks.
51
50
  def construct_diff(name)
52
- klass = CONSTRUCTS.fetch(name) {
51
+ klass = CONSTRUCTS.fetch(name) do
53
52
  raise ArgumentError, "no such construct #{name.inspect} — known: #{CONSTRUCTS.keys.join(', ')}"
54
- }
53
+ end
55
54
  declared = meta_declared(name)
56
55
  emitted = klass.ir_spec.keys
57
56
 
@@ -101,33 +100,9 @@ module Hecks
101
100
  rules = []
102
101
  chapters = chapter_name ? [registry.bluebook(chapter_name)] : registry.bluebooks.values
103
102
 
104
- walk_construct = lambda do |construct, path|
105
- (construct.respond_to?(:preconditions) ? construct.preconditions : []).each do |rule|
106
- rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
107
- location: "#{path} (declared)")
108
- end
109
- (construct.respond_to?(:invariants) ? construct.invariants : []).each do |rule|
110
- rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical,
111
- location: "#{path} (declared)")
112
- end
113
- construct.commands.each do |command|
114
- command.givens.each do |rule|
115
- rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
116
- location: "#{path}.#{command.hecks_name}")
117
- end
118
- command.ensures.each do |rule|
119
- rules << Rule.new(kind: "ensures", description: rule.description, canonical: rule.canonical,
120
- location: "#{path}.#{command.hecks_name}")
121
- end
122
- end
123
- construct.entities.each { |piece|
124
- walk_construct.call(piece, "#{path}.#{piece.hecks_name}")
125
- } if construct.respond_to?(:entities)
126
- end
127
-
128
103
  chapters.each do |chapter|
129
104
  chapter.aggregates.each do |aggregate|
130
- walk_construct.call(aggregate, aggregate.hecks_name)
105
+ walk_construct_rules(aggregate, aggregate.hecks_name, rules)
131
106
  aggregate.value_objects.each do |vo|
132
107
  vo.invariants.each do |rule|
133
108
  rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical,
@@ -139,6 +114,39 @@ module Hecks
139
114
  rules
140
115
  end
141
116
 
117
+ # THE RECURSIVE WALK `collect_rules` drives — pulled out of that method
118
+ # (pure extraction, identical traversal and Rule shapes) as its own
119
+ # named, self-recursive method rather than a lambda closing over the
120
+ # same locals. `rules` is the one piece of state every call shares —
121
+ # threaded as a parameter and mutated in place, the same accumulator
122
+ # role the lambda's own closure played.
123
+ def walk_construct_rules(construct, path, rules)
124
+ (construct.respond_to?(:preconditions) ? construct.preconditions : []).each do |rule|
125
+ rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
126
+ location: "#{path} (declared)")
127
+ end
128
+ (construct.respond_to?(:invariants) ? construct.invariants : []).each do |rule|
129
+ rules << Rule.new(kind: "invariant", description: rule.description, canonical: rule.canonical,
130
+ location: "#{path} (declared)")
131
+ end
132
+ construct.commands.each do |command|
133
+ command.givens.each do |rule|
134
+ rules << Rule.new(kind: "given", description: rule.description, canonical: rule.canonical,
135
+ location: "#{path}.#{command.hecks_name}")
136
+ end
137
+ command.ensures.each do |rule|
138
+ rules << Rule.new(kind: "ensures", description: rule.description, canonical: rule.canonical,
139
+ location: "#{path}.#{command.hecks_name}")
140
+ end
141
+ end
142
+ return unless construct.respond_to?(:entities)
143
+
144
+ construct.entities.each do |piece|
145
+ walk_construct_rules(piece, "#{path}.#{piece.hecks_name}", rules)
146
+ end
147
+ end
148
+ private_class_method :walk_construct_rules
149
+
142
150
  # A rule's OWNER — the construct path a "(declared)" location names
143
151
  # directly, or (for a command-level `.givens`/`.ensures` entry) the
144
152
  # path with its trailing `.CommandName` segment stripped. Two rules
@@ -196,10 +204,10 @@ module Hecks
196
204
  all_rules.group_by { |r| [r.kind, r.description, r.canonical] }
197
205
  .map { |key, rules| [key, rules, declaration_count(rules)] }
198
206
  .select { |_, _, count| count > 1 }
199
- .map { |(kind, description, canonical), rules, _|
207
+ .map do |(kind, description, canonical), rules, _|
200
208
  { kind: kind, description: description, canonical: canonical,
201
209
  locations: rules.map(&:location) }
202
- }
210
+ end
203
211
  end
204
212
 
205
213
  # `given` ONLY (not `invariant`/`ensures`) also covers CROSS-ENTITY
@@ -254,9 +262,9 @@ module Hecks
254
262
  # resolved chapter-wide reference.
255
263
  def declaration_count(rules)
256
264
  declared = rules.select { |r| r.location.end_with?(" (declared)") }
257
- declared_owners = declared.map { |r| owner_of(r.location) }.to_set
265
+ declared_owners = declared.to_set { |r| owner_of(r.location) }
258
266
  declared_given_roots = declared.select { |r| r.kind == "given" }
259
- .map { |r| owner_of(r.location).split(".").first }.to_set
267
+ .to_set { |r| owner_of(r.location).split(".").first }
260
268
 
261
269
  standalone = rules.reject do |r|
262
270
  owner = owner_of(r.location)
@@ -282,8 +290,14 @@ module Hecks
282
290
  if diff[:missing_from_ruby].empty? && diff[:unaccounted_in_ruby].empty?
283
291
  lines << " clean — every declared field is emitted (or a named deviation), nothing emitted is undeclared"
284
292
  else
285
- lines << " MISSING FROM RUBY (declared, not emitted, not a named deviation): #{diff[:missing_from_ruby].join(', ')}" unless diff[:missing_from_ruby].empty?
286
- lines << " UNACCOUNTED IN RUBY (emitted, not declared, not a named deviation): #{diff[:unaccounted_in_ruby].join(', ')}" unless diff[:unaccounted_in_ruby].empty?
293
+ unless diff[:missing_from_ruby].empty?
294
+ lines << " MISSING FROM RUBY (declared, not emitted, not a named deviation): " \
295
+ "#{diff[:missing_from_ruby].join(', ')}"
296
+ end
297
+ unless diff[:unaccounted_in_ruby].empty?
298
+ lines << " UNACCOUNTED IN RUBY (emitted, not declared, not a named deviation): " \
299
+ "#{diff[:unaccounted_in_ruby].join(', ')}"
300
+ end
287
301
  end
288
302
  lines.join("\n")
289
303
  end.join("\n\n")
@@ -358,6 +372,10 @@ module Hecks
358
372
  # was never supposed to have this touchpoint at all.
359
373
  def reconstruction_reads?(name, field)
360
374
  method_name = RECONSTRUCTION_METHODS[name]
375
+ # rubocop:disable-next Style/ReturnNilInPredicateMethodDefinition -- nil vs
376
+ # false is a deliberate distinction here: nil means "not applicable" (no
377
+ # hand-typed method to check), false means "applicable, and it fails" —
378
+ # see the spec's own "not-applicable (nil), not false" example.
361
379
  return nil unless method_name
362
380
 
363
381
  file, start_line = Hecks::Bluebook::MetaValidator::Reconstruction.instance_method(method_name).source_location
@@ -365,7 +383,9 @@ module Hecks
365
383
  def_line = lines[start_line - 1]
366
384
  indent = def_line[/\A\s*/]
367
385
 
368
- body = lines[start_line..].take_while { |line| line.strip.empty? || line[/\A\s*/].size > indent.size || !line.lstrip.start_with?("def ") }
386
+ body = lines[start_line..].take_while do |line|
387
+ line.strip.empty? || line[/\A\s*/].size > indent.size || !line.lstrip.start_with?("def ")
388
+ end
369
389
  body.join.include?("#{field}:")
370
390
  end
371
391
  private_class_method :reconstruction_reads?
@@ -385,7 +405,11 @@ module Hecks
385
405
  def format_impact_preview(preview)
386
406
  lines = ["== #{preview[:name]}##{preview[:field]} =="]
387
407
  preview[:touchpoints].each do |t|
388
- mark = t[:present].nil? ? "n/a" : (t[:present] ? "yes" : "NOT YET")
408
+ mark = if t[:present].nil?
409
+ "n/a"
410
+ else
411
+ (t[:present] ? "yes" : "NOT YET")
412
+ end
389
413
  lines << " [#{mark.rjust(7)}] #{t[:touchpoint]}"
390
414
  end
391
415
  done = preview[:touchpoints].count { |t| t[:present] == true }
@@ -2,7 +2,7 @@ module Hecks
2
2
  module QuerySpecification
3
3
  module Common
4
4
  AuthorizationSpec = Struct.new(:policy, :tenant, keyword_init: true) do
5
- def to_h = { policy: policy.to_s, tenant: tenant && tenant.to_s }
5
+ def to_h = { policy: policy.to_s, tenant: tenant&.to_s }
6
6
  end
7
7
  end
8
8
  end
@@ -2,6 +2,10 @@ require_relative "../../literal"
2
2
  require_relative "../../vocabulary"
3
3
 
4
4
  module Hecks
5
+ # Reopened (see query_specification.rb for the namespace's own
6
+ # summary) to add Common::COMPARATORS — the vendored comparator
7
+ # vocabulary — and .render_value, the wire-rendering entry point
8
+ # shared by every literal-bearing spec struct.
5
9
  module QuerySpecification
6
10
  module Common
7
11
  # `none_in_state`, vendored addition not (yet) upstream hecks
@@ -44,7 +44,7 @@ module Hecks
44
44
  value = value.to_h if value.is_a?(Runtime::Value)
45
45
  return value unless value.is_a?(Hash)
46
46
 
47
- numerics = value.values.select { |field| field.is_a?(Numeric) }
47
+ numerics = value.values.grep(Numeric)
48
48
  return numerics.first if numerics.size == 1
49
49
  return value.values.first if value.size == 1
50
50
 
@@ -63,6 +63,13 @@ module Hecks
63
63
 
64
64
  NUMERIC_TYPES = %w[Integer Float Numeric].freeze
65
65
 
66
+ # A `case` over a closed, declared set (Vocabulary::QueryComparator,
67
+ # held equal to this list by spec/vocabulary_table_spec — see the
68
+ # `else` branch's own comment) is the whole point of THE ONE
69
+ # COMPARATOR TABLE this file's header describes: one place naming
70
+ # every comparator, not one method per comparator scattered across
71
+ # a module.
72
+ # rubocop:disable-next Metrics/CyclomaticComplexity
66
73
  def holds?(operation, held, want, registry: nil)
67
74
  # A NULL SATISFIES NO COMPARISON — NullPolicy.unmatchable? owns
68
75
  # the rule and the reasoning, including why `none_in_state` is
@@ -11,6 +11,11 @@ require_relative "where_clause"
11
11
  module Hecks
12
12
  module QuerySpecification
13
13
  module Common
14
+ # Shared builder-instance methods for the `where`/`order_by`/
15
+ # `limit`/`offset`/`cursor`/`authorize`/`nulls`/`inspect_query`
16
+ # bluebook DSL words, mixed into both QueryBuilder and
17
+ # ReadModelBuilder so the two specification kinds parse the same
18
+ # clauses identically rather than each carrying its own copy.
14
19
  module DSL
15
20
  # RENAMED FROM `where`/`order_by`/`limit`/`offset`/`authorize`
16
21
  # (all below) — item #13's full metaprogrammed dispatch (slice
@@ -1,6 +1,12 @@
1
1
  module Hecks
2
2
  module QuerySpecification
3
3
  module Common
4
+ # Null-handling shared between the in-memory and SQL query
5
+ # engines: stable null-aware ordering (#order, #sql_order) and
6
+ # which comparators a null value can never satisfy (#unmatchable?,
7
+ # #sql_predicate) — kept in one place because Memory, Postgres and
8
+ # SQLite each default nulls differently and must be made to agree
9
+ # explicitly rather than leaking their own native behavior.
4
10
  module NullPolicy
5
11
  module_function
6
12
 
@@ -82,8 +88,6 @@ module Hecks
82
88
  ["#{expression} IS NULL", []]
83
89
  elsif value.nil? && operation.to_s == "ne"
84
90
  ["#{expression} IS NOT NULL", []]
85
- else
86
- nil
87
91
  end
88
92
  end
89
93
  end
@@ -3,6 +3,12 @@ require_relative "null_semantics"
3
3
  module Hecks
4
4
  module QuerySpecification
5
5
  module Common
6
+ # The attribute set every query-shaped construct shares — wheres,
7
+ # ordering, paging, cursor, authorization, null semantics,
8
+ # inspection mode — plus `options_to_h`/`extra_options_to_h` for
9
+ # serializing it. Both Bluebook::Query and
10
+ # QuerySpecification::ReadModel::Specification subclass this
11
+ # rather than each declaring the same fields twice.
6
12
  class Options
7
13
  attr_reader :wheres, :order_by, :limit, :offset, :cursor,
8
14
  :authorization, :null_semantics, :inspection
@@ -10,8 +16,14 @@ module Hecks
10
16
  def initialize(wheres: [], order_by: nil, limit: nil, offset: nil, cursor: nil,
11
17
  authorization: nil,
12
18
  inspection: nil, null_semantics: NullSemantics.default)
13
- @wheres, @order_by, @limit, @offset, @cursor = wheres, order_by, limit, offset, cursor
14
- @authorization, @null_semantics, @inspection = authorization, null_semantics, inspection
19
+ @wheres = wheres
20
+ @order_by = order_by
21
+ @limit = limit
22
+ @offset = offset
23
+ @cursor = cursor
24
+ @authorization = authorization
25
+ @null_semantics = null_semantics
26
+ @inspection = inspection
15
27
  end
16
28
 
17
29
  def options_to_h
@@ -21,10 +33,10 @@ module Hecks
21
33
  end
22
34
 
23
35
  def extra_options_to_h
24
- options_to_h.reject { |key, value|
36
+ options_to_h.reject do |key, value|
25
37
  value.nil? || value == [] || (key == :null_semantics && value == { mode: "native" })
26
- }
27
- .reject { |key, _| %i[wheres order_by limit].include?(key) }
38
+ end
39
+ .except(:wheres, :order_by, :limit)
28
40
  end
29
41
  end
30
42
  end
@@ -85,8 +85,8 @@ module Hecks
85
85
  # or a value object carrying at least one numeric member. A dotted
86
86
  # path must land on a numeric primitive itself — the convention does
87
87
  # not reach through a named path, it IS the absence of one.
88
- def numeric?(attribute, segments, &value_object)
89
- leaf = leaf_attribute(attribute, segments, &value_object)
88
+ def numeric?(attribute, segments, &)
89
+ leaf = leaf_attribute(attribute, segments, &)
90
90
  return false if leaf.nil? || leaf.list? || leaf.reference?
91
91
  return true if NUMERIC_PRIMITIVES.include?(leaf.type.to_s)
92
92
  return false unless segments.empty?
@@ -98,8 +98,8 @@ module Hecks
98
98
  # A dotted path must end on a SCALAR member — landing on a value
99
99
  # object would hand SQL a JSON object where the reference
100
100
  # interpreter unwraps a hash, and the two would answer differently.
101
- def scalar_leaf?(attribute, segments, &value_object)
102
- leaf = leaf_attribute(attribute, segments, &value_object)
101
+ def scalar_leaf?(attribute, segments, &)
102
+ leaf = leaf_attribute(attribute, segments, &)
103
103
  !leaf.nil? && !leaf.list? && !leaf.reference? && SCALAR_PRIMITIVES.include?(leaf.type.to_s)
104
104
  end
105
105
  end
@@ -3,11 +3,14 @@ require_relative "../common/options"
3
3
  module Hecks
4
4
  module QuerySpecification
5
5
  module ReadModel
6
+ # The read-model-specific specification: Common::Options' shared
7
+ # query attributes plus `joins`, the one thing a read model
8
+ # declares that a plain Query does not.
6
9
  class Specification < Common::Options
7
10
  attr_reader :joins
8
11
 
9
- def initialize(joins: [], **options)
10
- super(**options)
12
+ def initialize(joins: [], **)
13
+ super(**)
11
14
  @joins = joins
12
15
  end
13
16
 
@@ -7,12 +7,19 @@ module Hecks
7
7
  # resolution stays in Router; this object only adapts it to constants and
8
8
  # method calls.
9
9
  class NamespaceInstaller
10
+ # Returned by the `.options(version:)` method installed on each
11
+ # namespace module — forwards any command/query verb called on it
12
+ # to the router, pinned to this specific FQN version rather than
13
+ # the router's default resolution.
10
14
  class OptionsProxy
11
15
  def initialize(router:, realm:, domain:, aggregate:, options:)
12
16
  unknown = options.keys - [:version]
13
17
  raise ArgumentError, "unknown router options: #{unknown.join(', ')}" unless unknown.empty?
14
18
 
15
- @router, @realm, @domain, @aggregate = router, realm, domain, aggregate
19
+ @router = router
20
+ @realm = realm
21
+ @domain = domain
22
+ @aggregate = aggregate
16
23
  @version = options[:version]&.to_s
17
24
  end
18
25
 
@@ -103,12 +110,12 @@ module Hecks
103
110
 
104
111
  target.define_singleton_method(:find) do |id|
105
112
  found = dispatcher.registry.repository(domain, ir).find(id)
106
- found && Facade::Handle.new(dispatcher: dispatcher, domain: domain, ir: ir, instance: found)
113
+ found && Facade::Handle.new(dispatcher: dispatcher, domain: domain, aggregate: ir, instance: found)
107
114
  end
108
115
 
109
116
  target.define_singleton_method(:all) do
110
117
  dispatcher.registry.repository(domain, ir).all.map do |instance|
111
- Facade::Handle.new(dispatcher: dispatcher, domain: domain, ir: ir, instance: instance)
118
+ Facade::Handle.new(dispatcher: dispatcher, domain: domain, aggregate: ir, instance: instance)
112
119
  end
113
120
  end
114
121
  end
@@ -118,16 +125,17 @@ module Hecks
118
125
  current_entries.reject { |entry| entry.fqn.aggregate.nil? }
119
126
  .group_by { |entry| [entry.fqn.aggregate, entry.fqn.verb] }
120
127
  .each do |(aggregate, verb), candidates|
121
- shortcut_target(aggregate).define_singleton_method(verb) { |**args|
128
+ shortcut_target(aggregate).define_singleton_method(verb) do |**args|
122
129
  installer.send(:dispatch_short, candidates, **args)
123
- }
130
+ end
124
131
  end
125
132
  end
126
133
 
127
134
  def dispatch_short(candidates, **args)
128
135
  if candidates.length > 1
129
136
  shown = candidates.map { |entry| entry.fqn.to_s }.sort.join(", ")
130
- aggregate, verb = candidates.first.fqn.aggregate, candidates.first.fqn.verb
137
+ aggregate = candidates.first.fqn.aggregate
138
+ verb = candidates.first.fqn.verb
131
139
  raise AmbiguousShortRoute, "#{aggregate}.#{verb} is ambiguous — choose one of: #{shown}"
132
140
  end
133
141
 
@@ -137,8 +145,10 @@ module Hecks
137
145
 
138
146
  def shortcut_target(aggregate)
139
147
  constant = Object.const_get(aggregate, false) if Object.const_defined?(aggregate, false)
140
- raise NameError,
141
- "cannot install Bluebook shortcut #{aggregate}: it is not a module" if constant && !constant.is_a?(Module)
148
+ if constant && !constant.is_a?(Module)
149
+ raise NameError,
150
+ "cannot install Bluebook shortcut #{aggregate}: it is not a module"
151
+ end
142
152
 
143
153
  constant || Object.const_set(aggregate, Module.new)
144
154
  end
@@ -146,8 +156,10 @@ module Hecks
146
156
  def namespace_for(realm, domain, aggregate)
147
157
  [realm, domain, aggregate].compact.reduce(Object) do |parent, name|
148
158
  constant = parent.const_get(name, false) if parent.const_defined?(name, false)
149
- raise NameError,
150
- "cannot install Bluebook route under #{parent}::#{name}: it is not a module" if constant && !constant.is_a?(Module)
159
+ if constant && !constant.is_a?(Module)
160
+ raise NameError,
161
+ "cannot install Bluebook route under #{parent}::#{name}: it is not a module"
162
+ end
151
163
 
152
164
  constant || parent.const_set(name, Module.new)
153
165
  end