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
@@ -82,8 +82,26 @@ module Hecks
82
82
  Array(t.aggregates).each { |aggregate| judge_aggregate(t, aggregate) }
83
83
  end
84
84
 
85
+ # One Declare, then one AddX pass per rule collection the aggregate
86
+ # carries — each pass is independent of the others (they mutate
87
+ # different fields), so each is its own private method below; only
88
+ # Declare-before-any-Add is order-sensitive, and that order is
89
+ # preserved here.
85
90
  def judge_aggregate(translation, aggregate)
86
91
  name = aggregate.name
92
+ declare_aggregate(translation, aggregate, name)
93
+
94
+ judge_renames(aggregate, name)
95
+ judge_moves(aggregate, name)
96
+ judge_converts(aggregate, name)
97
+ judge_drops(aggregate, name)
98
+ judge_retypes(aggregate, name)
99
+ judge_computes(aggregate, name)
100
+ judge_rekeys(aggregate, name)
101
+ judge_backfills(aggregate, name)
102
+ end
103
+
104
+ def declare_aggregate(translation, aggregate, name)
87
105
  parent = {
88
106
  domain: v(translation.domain),
89
107
  from: v(translation.from),
@@ -91,45 +109,66 @@ module Hecks
91
109
  }
92
110
  send_to("Translation::TranslationAggregate.Declare", name,
93
111
  with: { translation_ref: parent, name: v(name), was: v(aggregate.was) })
112
+ end
94
113
 
114
+ def judge_renames(aggregate, name)
95
115
  Hash(aggregate.renames).each do |from, to|
96
116
  send_to("Translation::TranslationAggregate.AddRename", name, to: name,
97
117
  with: { from: v(from), to: v(to) })
98
118
  end
119
+ end
99
120
 
121
+ def judge_moves(aggregate, name)
100
122
  Array(aggregate.moves).each do |move|
101
123
  send_to("Translation::TranslationAggregate.AddMove", name, to: name,
102
124
  with: { from: v(move.from), to: v(move.to) })
103
125
  end
126
+ end
104
127
 
128
+ def judge_converts(aggregate, name)
105
129
  Array(aggregate.converts).each do |convert|
106
130
  send_to("Translation::TranslationAggregate.AddConvert", name, to: name,
107
- with: { from: v(convert.from), to: v(convert.to), values: v(convert.values.inspect) })
131
+ with: { from: v(convert.from),
132
+ to: v(convert.to),
133
+ values: v(convert.values.inspect) })
108
134
  end
135
+ end
109
136
 
137
+ def judge_drops(aggregate, name)
110
138
  Array(aggregate.drops).each do |dropped|
111
139
  send_to("Translation::TranslationAggregate.AddDrop", name, to: name,
112
140
  with: { value: v(dropped) })
113
141
  end
142
+ end
114
143
 
144
+ def judge_retypes(aggregate, name)
115
145
  Array(aggregate.retypes).each do |retype|
116
146
  send_to("Translation::TranslationAggregate.AddRetype", name, to: name,
117
147
  with: { from: v(retype.from), to: v(retype.to) })
118
148
  end
149
+ end
119
150
 
151
+ def judge_computes(aggregate, name)
120
152
  Array(aggregate.computes).each do |compute|
121
153
  send_to("Translation::TranslationAggregate.AddCompute", name, to: name,
122
- with: { from: v(compute.from), to: v(compute.to), sql: v(compute.sql) })
154
+ with: { from: v(compute.from),
155
+ to: v(compute.to),
156
+ sql: v(compute.sql) })
123
157
  end
158
+ end
124
159
 
160
+ def judge_rekeys(aggregate, name)
125
161
  Array(aggregate.rekeys).each do |rekey|
126
162
  send_to("Translation::TranslationAggregate.AddRekey", name, to: name,
127
163
  with: { sql: v(rekey.sql) })
128
164
  end
165
+ end
129
166
 
167
+ def judge_backfills(aggregate, name)
130
168
  Array(aggregate.backfills).each do |backfill|
131
169
  send_to("Translation::TranslationAggregate.AddBackfill", name, to: name,
132
- with: { name: v(backfill.name), default: v(backfill.default.inspect) })
170
+ with: { name: v(backfill.name),
171
+ default: v(backfill.default.inspect) })
133
172
  end
134
173
  end
135
174
  end
@@ -21,7 +21,7 @@ module Hecks
21
21
 
22
22
  def v(text) = text.nil? ? nil : { value: text.to_s }
23
23
 
24
- def args(pairs) = pairs.reject { |_, value| value.nil? }
24
+ def args(pairs) = pairs.compact
25
25
 
26
26
  def offer(label)
27
27
  yield
@@ -52,11 +52,11 @@ module Hecks
52
52
  # concept file requires no second catalog here; deterministic filename
53
53
  # order is the source order exported to IR.
54
54
  GRAMMAR_DIR = File.expand_path("../language/bluebook", __dir__).freeze
55
- GRAMMAR_FILES = Dir.glob(File.join(GRAMMAR_DIR, "*.bluebook")).sort.freeze
55
+ GRAMMAR_FILES = Dir.glob(File.join(GRAMMAR_DIR, "*.bluebook")).freeze
56
56
  # Sibling artifact languages use the same folder-is-the-chapter rule.
57
57
  # Their arrays are discovered, sorted source sets—not filename catalogs.
58
- WORLD_GRAMMAR = Dir.glob(File.expand_path("../language/world/*.bluebook", __dir__)).sort.freeze
59
- HECKSAGON_GRAMMAR = Dir.glob(File.expand_path("../language/hecksagon/*.bluebook", __dir__)).sort.freeze
58
+ WORLD_GRAMMAR = Dir.glob(File.expand_path("../language/world/*.bluebook", __dir__)).freeze
59
+ HECKSAGON_GRAMMAR = Dir.glob(File.expand_path("../language/hecksagon/*.bluebook", __dir__)).freeze
60
60
  # so is a port — whole-project table-unification survey, item #13's
61
61
  # remaining builders. Backs the new PortJudge door the same way
62
62
  # WORLD_GRAMMAR backs WorldJudge.
@@ -65,7 +65,7 @@ module Hecks
65
65
  ADAPTER_GRAMMAR = File.expand_path("../language/adapter.bluebook", __dir__).freeze
66
66
  # so is a translation — same reasoning, one file over. Backs
67
67
  # TranslationJudge.
68
- TRANSLATION_GRAMMAR = Dir.glob(File.expand_path("../language/translation/*.bluebook", __dir__)).sort.freeze
68
+ TRANSLATION_GRAMMAR = Dir.glob(File.expand_path("../language/translation/*.bluebook", __dir__)).freeze
69
69
 
70
70
  # ADR 0026's OWN SEAM: THE CORE DOES NOT NAME ITS EXTENSION POINTS.
71
71
  #
@@ -483,7 +483,7 @@ module Hecks
483
483
  # is added there.
484
484
  def self.load_attached_grammar_into(registry)
485
485
  Hecks.with_registry(registry) do
486
- Dir.glob(File.join(ATTACHED_GRAMMAR_DIR, "*.bluebook")).sort.each { |file| Kernel.load(file) }
486
+ Dir.glob(File.join(ATTACHED_GRAMMAR_DIR, "*.bluebook")).each { |file| Kernel.load(file) }
487
487
  end
488
488
  end
489
489
 
@@ -1,5 +1,3 @@
1
- require "set"
2
-
3
1
  module Hecks
4
2
  module Bluebook
5
3
  # Lightweight formal methods over the IR — the same family as TLA+/
@@ -89,7 +87,7 @@ module Hecks
89
87
  findings.concat(lifecycle_findings(aggregate, aggregate))
90
88
  aggregate.entities.each { |entity| findings.concat(lifecycle_findings(aggregate, entity)) }
91
89
  end
92
- bluebook.process_managers.each { |pm| findings.concat(saga_findings(bluebook, pm)) }
90
+ bluebook.process_managers.each { |process_manager| findings.concat(saga_findings(bluebook, process_manager)) }
93
91
  bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
94
92
  findings
95
93
  end
@@ -103,38 +101,46 @@ module Hecks
103
101
  subject = declaring.equal?(aggregate) ? aggregate.hecks_name : "#{aggregate.hecks_name}::#{declaring.hecks_name}"
104
102
  commands = Array(declaring.commands).map(&:hecks_name)
105
103
 
106
- findings = []
107
- findings.concat(unknown_transition_commands(lifecycle, commands, subject))
108
-
109
104
  full = full_states(lifecycle)
110
105
  reached = reachable_states(lifecycle)
111
106
 
112
- (full - reached.to_a).each do |state|
113
- findings << Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
114
- message: "#{state.inspect} is declared (in a transition's from: or target) " \
115
- "but no path from #{lifecycle.default.inspect} ever reaches it")
107
+ findings = []
108
+ findings.concat(unknown_transition_commands(lifecycle, commands, subject))
109
+ findings.concat(unreachable_state_findings(lifecycle, full, reached, subject))
110
+ findings.concat(dead_transition_findings(lifecycle, reached, subject))
111
+ findings.concat(stuck_state_findings(lifecycle, reached, subject))
112
+ findings
113
+ end
114
+
115
+ def unreachable_state_findings(lifecycle, full, reached, subject)
116
+ (full - reached.to_a).map do |state|
117
+ Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
118
+ message: "#{state.inspect} is declared (in a transition's from: or target) " \
119
+ "but no path from #{lifecycle.default.inspect} ever reaches it")
116
120
  end
121
+ end
117
122
 
118
- lifecycle.transitions.each do |command, transition|
123
+ def dead_transition_findings(lifecycle, reached, subject)
124
+ lifecycle.transitions.filter_map do |command, transition|
119
125
  next unless transition.constrained?
120
126
  next if Array(transition.from).any? { |source| reached.include?(source) }
121
127
 
122
- findings << Finding.new(kind: :dead_transition, severity: :error, subject: subject,
123
- message: "#{command} from #{Array(transition.from).inspect} can never fire — " \
124
- "none of those states is ever reached")
128
+ Finding.new(kind: :dead_transition, severity: :error, subject: subject,
129
+ message: "#{command} from #{Array(transition.from).inspect} can never fire — " \
130
+ "none of those states is ever reached")
125
131
  end
132
+ end
126
133
 
134
+ def stuck_state_findings(lifecycle, reached, subject)
127
135
  any_unconstrained = lifecycle.transitions.any? { |_, t| !t.constrained? }
128
- (reached - terminal_exempt(lifecycle)).each do |state|
136
+ (reached - terminal_exempt(lifecycle)).filter_map do |state|
129
137
  next if any_unconstrained
130
138
  next if lifecycle.transitions.any? { |_, t| t.constrained? && Array(t.from).include?(state) }
131
139
 
132
- findings << Finding.new(kind: :stuck_state, severity: :warning, subject: subject,
133
- message: "#{state.inspect} is reached but no transition ever leaves it — " \
134
- "fine if that is meant to be terminal")
140
+ Finding.new(kind: :stuck_state, severity: :warning, subject: subject,
141
+ message: "#{state.inspect} is reached but no transition ever leaves it — " \
142
+ "fine if that is meant to be terminal")
135
143
  end
136
-
137
- findings
138
144
  end
139
145
 
140
146
  def unknown_transition_commands(lifecycle, commands, subject)
@@ -165,6 +171,11 @@ module Hecks
165
171
  reached = Set.new([lifecycle.default])
166
172
  loop do
167
173
  grown = false
174
+ # `transitions` is an Array of [from, transition]-shaped entries
175
+ # (Lifecycle#transitions), not a Hash — Style/HashEachMethods'
176
+ # `each_value` rewrite assumed otherwise from the block shape
177
+ # alone. False positive.
178
+ # rubocop:disable-next Style/HashEachMethods
168
179
  lifecycle.transitions.each do |_, transition|
169
180
  next if reached.include?(transition.target)
170
181
  next if transition.constrained? && Array(transition.from).none? { |source| reached.include?(source) }
@@ -202,79 +213,105 @@ module Hecks
202
213
 
203
214
  # ── process managers / sagas ──────────────────────────────────────
204
215
 
205
- def saga_findings(bluebook, pm)
216
+ def saga_findings(bluebook, process_manager)
217
+ emitted = emitted_events(bluebook)
218
+ verbs = verbs_of(bluebook)
219
+ reached = pm_reachable_states(process_manager, emitted)
220
+
206
221
  findings = []
207
- emitted = emitted_events(bluebook)
208
- verbs = verbs_of(bluebook)
222
+ findings.concat(deaf_trigger_findings(process_manager, emitted))
223
+ findings.concat(unreachable_pm_state_findings(process_manager, reached))
224
+ process_manager.handlers.each do |handler|
225
+ findings.concat(handler_findings(bluebook, process_manager, emitted, verbs, handler))
226
+ end
227
+ findings.concat(dead_compensation_findings(process_manager, reached))
228
+ findings
229
+ end
209
230
 
210
- [pm.starts_on, pm.ends_on].compact.each do |event|
231
+ def deaf_trigger_findings(process_manager, emitted)
232
+ [process_manager.starts_on, process_manager.ends_on].compact.filter_map do |event|
211
233
  next if emitted.include?(bare(event))
212
234
 
213
- findings << Finding.new(kind: :deaf_trigger, severity: :error, subject: pm.name,
214
- message: "starts_on/ends_on names #{event.inspect}, which no command in this " \
215
- "domain emits")
235
+ Finding.new(kind: :deaf_trigger, severity: :error, subject: process_manager.name,
236
+ message: "starts_on/ends_on names #{event.inspect}, which no command in this " \
237
+ "domain emits")
216
238
  end
239
+ end
217
240
 
218
- reached = pm_reachable_states(pm, emitted)
219
- (Array(pm.states) - reached.to_a).each do |state|
220
- findings << Finding.new(kind: :unreachable_pm_state, severity: :error, subject: pm.name,
221
- message: "#{state.inspect} is declared but no handler chain from " \
222
- "#{pm.states.first.inspect} ever reaches it")
241
+ def unreachable_pm_state_findings(process_manager, reached)
242
+ (Array(process_manager.states) - reached.to_a).map do |state|
243
+ Finding.new(kind: :unreachable_pm_state, severity: :error, subject: process_manager.name,
244
+ message: "#{state.inspect} is declared but no handler chain from " \
245
+ "#{process_manager.states.first.inspect} ever reaches it")
223
246
  end
247
+ end
224
248
 
225
- pm.handlers.each do |handler|
226
- # The compensating leg answers REFUSED, a synthetic trigger no
227
- # command ever emits by name (ProcessManager::REFUSED) not
228
- # a deaf handler, the one handler this domain's own events can
229
- # never satisfy on purpose.
230
- if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
231
- findings << Finding.new(kind: :deaf_handler, severity: :error, subject: pm.name,
232
- message: "a handler answers #{handler.event_type.inspect}, which no command " \
233
- "in this domain emits")
234
- end
249
+ # ONE HANDLER'S OWN FINDINGS — deaf_handler, unknown_dispatch (one
250
+ # per dispatch), and unarmed_compensation (one per compensating
251
+ # dispatch), pulled out of saga_findings' own handler loop; each
252
+ # check reads only this handler plus the domain-wide emitted/verbs
253
+ # sets saga_findings already resolved once, no state shared BETWEEN
254
+ # handlers.
255
+ def handler_findings(bluebook, process_manager, emitted, verbs, handler)
256
+ findings = []
235
257
 
236
- handler.dispatches.each do |dispatch|
237
- # SAME-DOMAIN, same as `SagaInterpreter#qualified`a dispatch
238
- # naming no domain at all (the ordinary shape a bare command
239
- # constant now produces, S6) means THIS one, and is compared
240
- # against `verbs_of`'s own fully-qualified spelling qualified
241
- # the identical way, not left bare to miss it on a technicality.
242
- qualified = dispatch.command_name.include?("::") ? dispatch.command_name : "#{bluebook.name}::#{dispatch.command_name}"
243
- next if verbs.include?(qualified)
244
-
245
- findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: pm.name,
246
- message: "dispatches #{dispatch.command_name.inspect}, which this domain " \
247
- "declares no command at — cross-domain dispatch is out of this " \
248
- "checker's scope, same as CommandRules#resolve_references")
249
- end
258
+ # The compensating leg answers REFUSED, a synthetic trigger no
259
+ # command ever emits by name (ProcessManager::REFUSED) not
260
+ # a deaf handler, the one handler this domain's own events can
261
+ # never satisfy on purpose.
262
+ if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
263
+ findings << Finding.new(kind: :deaf_handler, severity: :error, subject: process_manager.name,
264
+ message: "a handler answers #{handler.event_type.inspect}, which no command " \
265
+ "in this domain emits")
266
+ end
250
267
 
251
- # A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
252
- # shape of the real bug this whole feature closes ("the
253
- # reversal was written and never armed"), caught at build/
254
- # model-check time instead of discovered in production. No
255
- # handler anywhere answers REFUSED (`pm.saga?` false) means
256
- # `SagaInterpreter#unwind` never runs for this process
257
- # manager at all, so a declared `compensates` is structurally
258
- # unreachable — not a warning about style, a dead declaration.
259
- if !pm.saga? && handler.dispatches.any?(&:compensates)
260
- handler.dispatches.select(&:compensates).each do |dispatch|
261
- findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: pm.name,
262
- message: "#{dispatch.command_name} compensates #{dispatch.compensates.command_name}, " \
263
- "but no handler anywhere in this saga answers a refusal — the " \
264
- "compensation is declared and can never fire")
265
- end
266
- end
268
+ handler.dispatches.each do |dispatch|
269
+ # SAME-DOMAIN, same as `SagaInterpreter#qualified` a dispatch
270
+ # naming no domain at all (the ordinary shape a bare command
271
+ # constant now produces, S6) means THIS one, and is compared
272
+ # against `verbs_of`'s own fully-qualified spelling qualified
273
+ # the identical way, not left bare to miss it on a technicality.
274
+ qualified = if dispatch.command_name.include?("::")
275
+ dispatch.command_name
276
+ else
277
+ "#{bluebook.name}::#{dispatch.command_name}"
278
+ end
279
+ next if verbs.include?(qualified)
280
+
281
+ findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: process_manager.name,
282
+ message: "dispatches #{dispatch.command_name.inspect}, which this domain " \
283
+ "declares no command at — cross-domain dispatch is out of this " \
284
+ "checker's scope, same as CommandRules#resolve_references")
267
285
  end
268
286
 
269
- if pm.saga? && !reached.include?(pm.saga.from_state)
270
- findings << Finding.new(kind: :dead_compensation, severity: :error, subject: pm.name,
271
- message: "the compensation leaves #{pm.saga.from_state.inspect}, which no " \
272
- "handler chain ever reaches a refusal here can never fire it")
287
+ # A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
288
+ # shape of the real bug this whole feature closes ("the
289
+ # reversal was written and never armed"), caught at build/
290
+ # model-check time instead of discovered in production. No
291
+ # handler anywhere answers REFUSED (`process_manager.saga?` false) means
292
+ # `SagaInterpreter#unwind` never runs for this process
293
+ # manager at all, so a declared `compensates` is structurally
294
+ # unreachable — not a warning about style, a dead declaration.
295
+ if !process_manager.saga? && handler.dispatches.any?(&:compensates)
296
+ handler.dispatches.select(&:compensates).each do |dispatch|
297
+ findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: process_manager.name,
298
+ message: "#{dispatch.command_name} compensates #{dispatch.compensates.command_name}, " \
299
+ "but no handler anywhere in this saga answers a refusal — the " \
300
+ "compensation is declared and can never fire")
301
+ end
273
302
  end
274
303
 
275
304
  findings
276
305
  end
277
306
 
307
+ def dead_compensation_findings(process_manager, reached)
308
+ return [] unless process_manager.saga? && !reached.include?(process_manager.saga.from_state)
309
+
310
+ [Finding.new(kind: :dead_compensation, severity: :error, subject: process_manager.name,
311
+ message: "the compensation leaves #{process_manager.saga.from_state.inspect}, which no " \
312
+ "handler chain ever reaches — a refusal here can never fire it")]
313
+ end
314
+
278
315
  # A handler edge is only usable in the closure if it can actually
279
316
  # FIRE — REFUSED always can (it is a compensation trigger, not an
280
317
  # event), and any other handler needs its event genuinely emitted.
@@ -283,13 +320,13 @@ module Hecks
283
320
  # it, which would hide exactly the states this walk exists to
284
321
  # catch (a state only "reachable" through a handler that itself
285
322
  # never fires).
286
- def pm_reachable_states(pm, emitted)
287
- return Set.new if Array(pm.states).empty?
323
+ def pm_reachable_states(process_manager, emitted)
324
+ return Set.new if Array(process_manager.states).empty?
288
325
 
289
- reached = Set.new([pm.states.first])
326
+ reached = Set.new([process_manager.states.first])
290
327
  loop do
291
328
  grown = false
292
- pm.handlers.each do |handler|
329
+ process_manager.handlers.each do |handler|
293
330
  next unless handler.event_type == ProcessManager::REFUSED || emitted.include?(bare(handler.event_type))
294
331
  next unless reached.include?(handler.from_state)
295
332
  next if reached.include?(handler.to_state)
@@ -446,9 +483,9 @@ module Hecks
446
483
  bluebook.aggregates.flat_map do |aggregate|
447
484
  verbs = aggregate.commands.map { |command| "#{bluebook.name}::#{aggregate.hecks_name}.#{command.hecks_name}" }
448
485
  verbs + aggregate.entities.flat_map do |entity|
449
- entity.commands.map { |command|
486
+ entity.commands.map do |command|
450
487
  "#{bluebook.name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name}"
451
- }
488
+ end
452
489
  end
453
490
  end
454
491
  end
@@ -63,6 +63,15 @@ module Hecks
63
63
  # characters "(?=" and says nothing about lookahead — which is why this
64
64
  # steps over each backslash pair rather than matching the pattern as a
65
65
  # whole.
66
+ # One character walk is the subset's DEFINITION (see the method
67
+ # comment above): each construct-check is a branch in a single
68
+ # ordered pass sharing `index`/`in_class`/`class_start`. Splitting
69
+ # the branches into separate methods would force those three cursor
70
+ # variables to thread as parameters/return values between them,
71
+ # turning "the subset is this walk" into "the subset is these
72
+ # methods agreeing about a shared cursor" — exactly what the walk's
73
+ # own comment says a cleverer spelling would obscure.
74
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
66
75
  def validate(pattern)
67
76
  chars = pattern.to_s.chars
68
77
  index = 0
@@ -16,7 +16,8 @@ module Hecks
16
16
  target_domain: :target_domain,
17
17
  where: :where,
18
18
  for_each: :for_each,
19
- with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }
19
+ with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } },
20
+ where_ast: -> { where_ast }
20
21
  )
21
22
 
22
23
  attr_reader :name, :on_event, :trigger_command, :target_domain, :where, :for_each, :with_spec
@@ -74,6 +74,12 @@ module Hecks
74
74
  def to_s = "#{trigger} → #{to_state} (#{undoes.join(', ')})"
75
75
  end
76
76
 
77
+ # The built form of a `process_manager "Name" do ... end` block,
78
+ # produced by `DSL::ProcessManagerBuilder` — its start/end events,
79
+ # correlation field, derived states, and handler rows. Its
80
+ # compensation half (`saga`, `Behaviour::ProcessManager#saga`) is
81
+ # DERIVED from the handler answering `REFUSED`, below, not declared as
82
+ # its own construct.
77
83
  class ProcessManager
78
84
  # The BLUEBOOK's name for this construct, asked the same way of a class
79
85
  # that has crossed over and of an IR object that has not. Collapses into
@@ -35,34 +35,56 @@ module Hecks
35
35
  "#{bluebook.name} in #{directory} declares latest #{world.latest.inspect}, not #{bluebook.version.inspect}"
36
36
  end
37
37
 
38
- bluebook.aggregates.each do |aggregate|
39
- aggregate.commands.each do |command|
40
- add(Fqn.command(realm: realm, domain: bluebook.name, version: bluebook.version,
41
- aggregate: aggregate.hecks_name, command: command.hecks_name), directory, dispatcher, command.hecks_name, bluebook.version) if bluebook.version
42
- add(Fqn.command(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
43
- command: command.hecks_name), directory, dispatcher, command.hecks_name, bluebook.version) if current?(
44
- bluebook, world
45
- )
46
- end
47
- aggregate.queries.each do |query|
48
- name = Naming.snake(query.name)
49
- add(Fqn.query(realm: realm, domain: bluebook.name, version: bluebook.version,
50
- aggregate: aggregate.hecks_name, query: name), directory, dispatcher, query.name, bluebook.version) if bluebook.version
51
- add(Fqn.query(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
52
- query: name), directory, dispatcher, query.name, bluebook.version) if current?(bluebook, world)
53
- end
38
+ bluebook.aggregates.each { |aggregate| register_aggregate!(aggregate, bluebook, world, realm, directory, dispatcher) }
39
+ bluebook.read_models.each { |model| register_read_model!(model, bluebook, world, realm, directory, dispatcher) }
40
+ end
41
+ self
42
+ end
43
+
44
+ private
45
+
46
+ # Split out of `register` per-aggregate: same versioned-then-current
47
+ # registration shape as `register_read_model!`, just applied to a
48
+ # command/query pair instead of a single report. No state threads
49
+ # back to `register` each `add` call is independent.
50
+ def register_aggregate!(aggregate, bluebook, world, realm, directory, dispatcher)
51
+ aggregate.commands.each do |command|
52
+ if bluebook.version
53
+ add(Fqn.command(realm: realm, domain: bluebook.name, version: bluebook.version,
54
+ aggregate: aggregate.hecks_name, command: command.hecks_name),
55
+ directory, dispatcher, command.hecks_name, bluebook.version)
54
56
  end
55
- bluebook.read_models.each do |model|
57
+ if current?(bluebook, world)
58
+ add(Fqn.command(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
59
+ command: command.hecks_name), directory, dispatcher, command.hecks_name, bluebook.version)
60
+ end
61
+ end
62
+ aggregate.queries.each do |query|
63
+ name = Naming.snake(query.name)
64
+ if bluebook.version
56
65
  add(Fqn.query(realm: realm, domain: bluebook.name, version: bluebook.version,
57
- query: model.query_name), directory, dispatcher, model.query_name, bluebook.version) if bluebook.version
58
- add(Fqn.query(realm: realm, domain: bluebook.name, query: model.query_name),
59
- directory, dispatcher, model.query_name, bluebook.version) if current?(bluebook, world)
66
+ aggregate: aggregate.hecks_name, query: name), directory, dispatcher, query.name, bluebook.version)
67
+ end
68
+ if current?(bluebook, world)
69
+ add(Fqn.query(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
70
+ query: name), directory, dispatcher, query.name, bluebook.version)
60
71
  end
61
72
  end
62
- self
63
73
  end
64
74
 
65
- private
75
+ # Split out of `register` per-read-model: registers the versioned FQN
76
+ # (if this bluebook declares a version) and the current/unversioned
77
+ # FQN (if this bluebook is the world's latest) for one report.
78
+ def register_read_model!(model, bluebook, world, realm, directory, dispatcher)
79
+ if bluebook.version
80
+ add(Fqn.query(realm: realm, domain: bluebook.name, version: bluebook.version,
81
+ query: model.query_name), directory, dispatcher, model.query_name, bluebook.version)
82
+ end
83
+ if current?(bluebook, world)
84
+ add(Fqn.query(realm: realm, domain: bluebook.name, query: model.query_name),
85
+ directory, dispatcher, model.query_name, bluebook.version)
86
+ end
87
+ end
66
88
 
67
89
  # THE ACTUAL "more than one tenant" MOMENT — Runtime::TenantCheck's
68
90
  # own header names this table as the one place real multitenancy
@@ -93,9 +115,7 @@ module Hecks
93
115
 
94
116
  def add(fqn, directory, dispatcher, declared_verb, domain_version)
95
117
  existing = entries[fqn.to_s]
96
- if existing
97
- raise DuplicateFqn, "#{fqn} is declared in both #{existing.source_directory} and #{directory}"
98
- end
118
+ raise DuplicateFqn, "#{fqn} is declared in both #{existing.source_directory} and #{directory}" if existing
99
119
 
100
120
  entries[fqn.to_s] = Entry.new(
101
121
  fqn: fqn, source_directory: directory, dispatcher: dispatcher,
@@ -1,6 +1,10 @@
1
1
  require_relative "behaviour/query"
2
2
 
3
3
  module Hecks
4
+ # Reopened here only to add `render_value` below — the pinned
5
+ # wire-literal rendering (`Literal.render`) the `emits_ir` Procs
6
+ # throughout this chapter reach for. See lib/hecks/bluebook.rb's own
7
+ # header for what `Bluebook` is as a whole.
4
8
  module Bluebook
5
9
  def self.render_value(value) = Literal.render(value)
6
10
 
@@ -35,8 +35,8 @@ module Hecks
35
35
  # `reference_target.nil?` stays a real, checkable fact for the
36
36
  # interpreter instead of silently becoming `""`.
37
37
  def initialize(name:, description: nil, reference_name: nil, reference_target: nil, aggregate_heads: [],
38
- group_by: [], count: nil, median_field: nil, **options)
39
- super(joins: aggregate_heads, **options)
38
+ group_by: [], count: nil, median_field: nil, **)
39
+ super(joins: aggregate_heads, **)
40
40
  @name = name.to_s
41
41
  @hecks_name = @name
42
42
  @description = description
@@ -118,6 +118,11 @@ module Hecks
118
118
  # the time that aggregate's own turn comes, the same reason
119
119
  # `Synthesizer#args_for`'s `created` map is threaded through in
120
120
  # this same order.
121
+ # `created` and `failures` thread through both the aggregate loop and the
122
+ # read_models loop below in the declaration order the comment above already
123
+ # documents; splitting would turn that ordering into an implicit contract on
124
+ # parameter/return passing instead of one visible sequence.
125
+ # rubocop:disable-next Metrics/AbcSize
121
126
  def smoke_domain(dispatcher, domain)
122
127
  chapter = dispatcher.registry.bluebook(domain)
123
128
  return [] unless chapter
@@ -64,7 +64,8 @@ module Hecks
64
64
  chapter.aggregates.filter_map { |a| a.value_object(type_name) }.first
65
65
  return "smoke-test" unless value_object
66
66
 
67
- return value_object.members.first.to_h if value_object.respond_to?(:closed_set?) && value_object.closed_set? && value_object.members.any?
67
+ closed_set = value_object.respond_to?(:closed_set?) && value_object.closed_set? && value_object.members.any?
68
+ return value_object.members.first.to_h if closed_set
68
69
 
69
70
  value_object.attributes.to_h { |field| [field.name, field_value_for(chapter, aggregate, field.type)] }
70
71
  end
@@ -11,6 +11,9 @@ module Hecks
11
11
  # Paths follow `TranslationMove`'s convention (dotted reaches a VO
12
12
  # member); `values` maps every old-era value that can appear to its
13
13
  # new-era replacement.
14
+ # `:values` shadows Struct#values on purpose (same reasoning as
15
+ # AttributeCollector::OneOf) — no caller reads the built-in behavior.
16
+ # rubocop:disable-next Lint/StructNewOverride
14
17
  TranslationConvert = Struct.new(:from, :to, :values)
15
18
 
16
19
  # A value object's or entity's own TYPE NAME changed with its member