hecks 1.4.0 → 1.5.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 (267) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +65 -0
  3. data/lib/hecks/adapters/driven/folder.rb +73 -0
  4. data/lib/hecks/adapters/driven/google_authentication.rb +25 -4
  5. data/lib/hecks/adapters/driven/governance_authorization.rb +57 -6
  6. data/lib/hecks/adapters/driven/heki/journal.rb +8 -0
  7. data/lib/hecks/adapters/driven/heki/saga_store.rb +53 -7
  8. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  9. data/lib/hecks/adapters/driven/heki.rb +93 -2
  10. data/lib/hecks/adapters/driven/identity_registry.rb +10 -0
  11. data/lib/hecks/adapters/driven/in_memory_ordering.rb +22 -0
  12. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  13. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  14. data/lib/hecks/adapters/driven/lambda/client.rb +35 -7
  15. data/lib/hecks/adapters/driven/lambda.rb +43 -2
  16. data/lib/hecks/adapters/driven/local_storage.rb +67 -1
  17. data/lib/hecks/adapters/driven/memory.rb +13 -13
  18. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +20 -0
  19. data/lib/hecks/adapters/driven/prism.rb +46 -7
  20. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  21. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  22. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  23. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  24. data/lib/hecks/adapters/driven.rb +2 -0
  25. data/lib/hecks/adapters/driving/github_webhook.rb +14 -1
  26. data/lib/hecks/behaviors/dsl.rb +58 -0
  27. data/lib/hecks/behaviors/expectations.rb +168 -7
  28. data/lib/hecks/behaviors/ir.rb +11 -0
  29. data/lib/hecks/behaviors/rspec.rb +8 -0
  30. data/lib/hecks/behaviors/runner.rb +19 -0
  31. data/lib/hecks/behaviors.rb +8 -0
  32. data/lib/hecks/bluebook/aggregate.rb +30 -0
  33. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +7 -0
  34. data/lib/hecks/bluebook/assembly/build.rb +25 -4
  35. data/lib/hecks/bluebook/assembly/contract.rb +66 -14
  36. data/lib/hecks/bluebook/assembly/contracts.rb +24 -19
  37. data/lib/hecks/bluebook/assembly/marks.rb +143 -14
  38. data/lib/hecks/bluebook/assembly/specializer.rb +19 -0
  39. data/lib/hecks/bluebook/assembly.rb +25 -7
  40. data/lib/hecks/bluebook/attribute.rb +17 -3
  41. data/lib/hecks/bluebook/behaviour/aggregate.rb +32 -0
  42. data/lib/hecks/bluebook/behaviour/attribute.rb +13 -0
  43. data/lib/hecks/bluebook/behaviour/chapter.rb +55 -2
  44. data/lib/hecks/bluebook/behaviour/command.rb +33 -3
  45. data/lib/hecks/bluebook/behaviour/domain_port.rb +20 -0
  46. data/lib/hecks/bluebook/behaviour/entity.rb +12 -0
  47. data/lib/hecks/bluebook/behaviour/hexagon.rb +26 -0
  48. data/lib/hecks/bluebook/behaviour/lifecycle.rb +23 -2
  49. data/lib/hecks/bluebook/behaviour/policy.rb +34 -9
  50. data/lib/hecks/bluebook/behaviour/process_manager.rb +32 -1
  51. data/lib/hecks/bluebook/behaviour/query.rb +5 -0
  52. data/lib/hecks/bluebook/behaviour/read_model.rb +21 -0
  53. data/lib/hecks/bluebook/behaviour/traits.rb +36 -0
  54. data/lib/hecks/bluebook/behaviour/value_object.rb +17 -5
  55. data/lib/hecks/bluebook/chapter.rb +23 -0
  56. data/lib/hecks/bluebook/command.rb +53 -8
  57. data/lib/hecks/bluebook/domain_port.rb +25 -0
  58. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +196 -37
  59. data/lib/hecks/bluebook/dsl/attribute_collector.rb +79 -42
  60. data/lib/hecks/bluebook/dsl/binding_proxy.rb +59 -0
  61. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +385 -53
  62. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +135 -21
  63. data/lib/hecks/bluebook/dsl/command_builder.rb +189 -27
  64. data/lib/hecks/bluebook/dsl/entity_builder.rb +139 -9
  65. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +41 -0
  66. data/lib/hecks/bluebook/dsl/identity_declaration.rb +23 -6
  67. data/lib/hecks/bluebook/dsl/policy_builder.rb +60 -19
  68. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +79 -15
  69. data/lib/hecks/bluebook/dsl/query_builder.rb +33 -4
  70. data/lib/hecks/bluebook/dsl/read_model_builder.rb +102 -27
  71. data/lib/hecks/bluebook/dsl/rule_reference.rb +60 -8
  72. data/lib/hecks/bluebook/dsl/translation_builder.rb +146 -40
  73. data/lib/hecks/bluebook/dsl/value_object_builder.rb +55 -7
  74. data/lib/hecks/bluebook/entity.rb +29 -0
  75. data/lib/hecks/bluebook/expression/ast_json.rb +115 -23
  76. data/lib/hecks/bluebook/expression/ast_reader.rb +29 -0
  77. data/lib/hecks/bluebook/expression/canonical_form.rb +48 -9
  78. data/lib/hecks/bluebook/expression/evaluator.rb +207 -29
  79. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +36 -0
  80. data/lib/hecks/bluebook/expression/resolver.rb +320 -77
  81. data/lib/hecks/bluebook/hexagon.rb +34 -0
  82. data/lib/hecks/bluebook/lifecycle.rb +11 -0
  83. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +1 -0
  84. data/lib/hecks/bluebook/meta_validator/judge.rb +30 -26
  85. data/lib/hecks/bluebook/meta_validator/plan.rb +43 -8
  86. data/lib/hecks/bluebook/meta_validator/port_judge.rb +1 -0
  87. data/lib/hecks/bluebook/meta_validator/readings.rb +157 -7
  88. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +24 -4
  89. data/lib/hecks/bluebook/meta_validator/shapes.rb +141 -0
  90. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +149 -23
  91. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +5 -4
  92. data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -0
  93. data/lib/hecks/bluebook/meta_validator.rb +180 -84
  94. data/lib/hecks/bluebook/model_check.rb +268 -24
  95. data/lib/hecks/bluebook/pattern_subset.rb +23 -1
  96. data/lib/hecks/bluebook/process_manager.rb +13 -0
  97. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  98. data/lib/hecks/bluebook/project_loader.rb +40 -0
  99. data/lib/hecks/bluebook/project_register.rb +44 -0
  100. data/lib/hecks/bluebook/query.rb +27 -0
  101. data/lib/hecks/bluebook/read_model.rb +21 -1
  102. data/lib/hecks/bluebook/reference.rb +21 -8
  103. data/lib/hecks/bluebook/smoke_test.rb +29 -6
  104. data/lib/hecks/bluebook/synthesizer.rb +34 -0
  105. data/lib/hecks/bluebook/translation.rb +30 -1
  106. data/lib/hecks/bluebook/value_object.rb +23 -5
  107. data/lib/hecks/bluebook.rb +3 -4
  108. data/lib/hecks/codemod.rb +107 -20
  109. data/lib/hecks/construct.rb +15 -1
  110. data/lib/hecks/corpus.rb +146 -25
  111. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  112. data/lib/hecks/doc/reference.rb +185 -16
  113. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  114. data/lib/hecks/facade/handle.rb +76 -3
  115. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  116. data/lib/hecks/forms/field_shape.rb +3 -0
  117. data/lib/hecks/forms/page.rb +14 -0
  118. data/lib/hecks/forms/port_argument.rb +12 -0
  119. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  120. data/lib/hecks/forms/record_renderer.rb +58 -0
  121. data/lib/hecks/forms/record_table.rb +27 -0
  122. data/lib/hecks/forms/reference_options.rb +24 -0
  123. data/lib/hecks/forms/value_object_shape.rb +10 -0
  124. data/lib/hecks/fqn.rb +58 -0
  125. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  126. data/lib/hecks/framework/oidc.json +15 -0
  127. data/lib/hecks/framework.rb +43 -20
  128. data/lib/hecks/freezer.rb +17 -1
  129. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  130. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  131. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  132. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  133. data/lib/hecks/fuzzing/differential.rb +34 -0
  134. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  135. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  136. data/lib/hecks/fuzzing/form_census.rb +86 -0
  137. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  138. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  139. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  140. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  141. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  142. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  143. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  144. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  145. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  146. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  147. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  148. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  149. data/lib/hecks/fuzzing/properties.rb +24 -15
  150. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  151. data/lib/hecks/fuzzing/replay.rb +137 -29
  152. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  153. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  154. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  155. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  156. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  157. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  158. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  159. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  160. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  161. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  162. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  163. data/lib/hecks/grammar/evolve.rb +178 -2
  164. data/lib/hecks/grammar.rb +46 -0
  165. data/lib/hecks/ir.rb +38 -7
  166. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  167. data/lib/hecks/literal.rb +32 -0
  168. data/lib/hecks/naming.rb +88 -7
  169. data/lib/hecks/ports/access_control.rb +5 -10
  170. data/lib/hecks/ports/authorization.rb +3 -6
  171. data/lib/hecks/ports/identity_assignment.rb +1 -2
  172. data/lib/hecks/ports/identity_resolution.rb +1 -2
  173. data/lib/hecks/ports/key_vault.port +6 -0
  174. data/lib/hecks/ports/key_vault.rb +58 -0
  175. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  176. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  177. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  178. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  179. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  180. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  182. data/lib/hecks/ports/query/in_memory.rb +39 -6
  183. data/lib/hecks/ports/query/ordering.rb +15 -0
  184. data/lib/hecks/ports.rb +1 -0
  185. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  186. data/lib/hecks/projections/diagrams.rb +243 -7
  187. data/lib/hecks/projections/glossary/html.rb +88 -0
  188. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  189. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  190. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  191. data/lib/hecks/projections/glossary.rb +161 -19
  192. data/lib/hecks/projections/model/deviations.rb +44 -0
  193. data/lib/hecks/projections/model.rb +51 -1
  194. data/lib/hecks/projections/oidc.rb +40 -3
  195. data/lib/hecks/projections/parser_table.rb +49 -8
  196. data/lib/hecks/projections/reference.rb +12 -0
  197. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  198. data/lib/hecks/projections/shape.rb +8 -0
  199. data/lib/hecks/projections/statements.rb +63 -16
  200. data/lib/hecks/projections/vocabulary.rb +17 -0
  201. data/lib/hecks/projector/cli_projector.rb +218 -10
  202. data/lib/hecks/projector/docs_projector.rb +145 -19
  203. data/lib/hecks/projector/exporter.rb +65 -11
  204. data/lib/hecks/projector/ir_projector.rb +6 -0
  205. data/lib/hecks/projector/narrate_projector.rb +136 -15
  206. data/lib/hecks/projector/target.rb +29 -5
  207. data/lib/hecks/projector.rb +74 -6
  208. data/lib/hecks/query_ir.rb +47 -0
  209. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  210. data/lib/hecks/rendering.rb +6 -0
  211. data/lib/hecks/router/namespace_installer.rb +13 -0
  212. data/lib/hecks/router.rb +55 -0
  213. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  214. data/lib/hecks/runtime/boot_gates.rb +18 -0
  215. data/lib/hecks/runtime/caller.rb +32 -0
  216. data/lib/hecks/runtime/capability_graph.rb +11 -0
  217. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  218. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  219. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  220. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  221. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  222. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  223. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  224. data/lib/hecks/runtime/dispatcher.rb +28 -50
  225. data/lib/hecks/runtime/entity_element.rb +161 -8
  226. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  227. data/lib/hecks/runtime/errors.rb +18 -4
  228. data/lib/hecks/runtime/event.rb +10 -5
  229. data/lib/hecks/runtime/identity.rb +71 -3
  230. data/lib/hecks/runtime/instance.rb +67 -7
  231. data/lib/hecks/runtime/interpreting.rb +13 -5
  232. data/lib/hecks/runtime/invocation.rb +118 -36
  233. data/lib/hecks/runtime/loader.rb +94 -8
  234. data/lib/hecks/runtime/outbox.rb +145 -7
  235. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  236. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  237. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  238. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  239. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  240. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  241. data/lib/hecks/runtime/reference_hop.rb +42 -0
  242. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  243. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  244. data/lib/hecks/runtime/registry/verification.rb +119 -4
  245. data/lib/hecks/runtime/registry.rb +157 -4
  246. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  247. data/lib/hecks/runtime/routing.rb +27 -2
  248. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  249. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  250. data/lib/hecks/runtime/tenant_check.rb +26 -6
  251. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  252. data/lib/hecks/runtime/value/coercion.rb +255 -33
  253. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  254. data/lib/hecks/runtime/value.rb +50 -0
  255. data/lib/hecks/runtime.rb +32 -0
  256. data/lib/hecks/storehouse.rb +305 -9
  257. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  258. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  259. data/lib/hecks/version.rb +1 -1
  260. data/lib/hecks.rb +79 -1
  261. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  262. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  263. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  264. metadata +11 -5
  265. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  266. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  267. data/lib/hecks/deprecation.rb +0 -95
@@ -22,7 +22,8 @@ module Hecks
22
22
  # `field DESC, id DESC`.
23
23
  #
24
24
  # @param records [Array<Object>] the rows to order, already in identity order
25
- # @param direction [Symbol, String] `desc` sorts descending; anything else ascending
25
+ # @param direction [Symbol, String] `desc` in any letter case sorts descending;
26
+ # anything else ascending
26
27
  # @param policy [NullSemantics, nil] where nulls go: mode `first` or `last`; `nil` or
27
28
  # any other mode (`native`) puts nulls first ascending and last descending
28
29
  # @yield reads the sort key out of one record
@@ -32,16 +33,17 @@ module Hecks
32
33
  # @return [Array<Object>] a new Array holding every record in the requested order
33
34
  # @raise [ArgumentError] if two non-nil keys cannot be compared with each other
34
35
  def order(records, direction:, policy: nil, &key)
36
+ descending = direction.to_s.downcase == "desc"
35
37
  null_rows, valued_rows = records.partition { |record| key.call(record).nil? }
36
38
  sorted = valued_rows.each_with_index.sort_by { |record, index| [key.call(record), index] }.map(&:first)
37
- if direction.to_s == "desc"
39
+ if descending
38
40
  sorted.reverse!
39
41
  null_rows.reverse!
40
42
  end
41
43
  case policy&.mode.to_s
42
44
  when "first" then null_rows + sorted
43
45
  when "last" then sorted + null_rows
44
- else direction.to_s == "desc" ? sorted + null_rows : null_rows + sorted
46
+ else descending ? sorted + null_rows : null_rows + sorted
45
47
  end
46
48
  end
47
49
 
@@ -17,6 +17,12 @@ module Hecks
17
17
  module Rendering
18
18
  module_function
19
19
 
20
+ # Renders a value the way it should read inside a refusal message.
21
+ #
22
+ # @param value [Object] the value to render
23
+ # @return [String] `"nil"` for nil, JSON for a Hash/Array or a duck-typed value
24
+ # object (unwrapped to its bare scalar when it has exactly one field), or
25
+ # `value.inspect` for anything else
20
26
  def describe(value)
21
27
  case value
22
28
  when nil then "nil"
@@ -12,6 +12,13 @@ module Hecks
12
12
  # to the router, pinned to this specific FQN version rather than
13
13
  # the router's default resolution.
14
14
  class OptionsProxy
15
+ # @param router [Router] router to dispatch resolved calls through
16
+ # @param realm [String, nil] realm segment of the FQN this proxy pins
17
+ # @param domain [String] domain segment of the FQN this proxy pins
18
+ # @param aggregate [String, nil] aggregate segment, or nil for a domain-level query
19
+ # @param options [Hash{Symbol => Object}] the `.options(...)` call's keywords; only
20
+ # `:version` is accepted
21
+ # @raise [ArgumentError] if `options` has a key other than `:version`
15
22
  def initialize(router:, realm:, domain:, aggregate:, options:)
16
23
  unknown = options.keys - [:version]
17
24
  raise ArgumentError, "unknown router options: #{unknown.join(', ')}" unless unknown.empty?
@@ -45,10 +52,16 @@ module Hecks
45
52
  end
46
53
  end
47
54
 
55
+ # @param router [Router] router whose current routes get Ruby constants and methods
48
56
  def initialize(router)
49
57
  @router = router
50
58
  end
51
59
 
60
+ # Installs every current-version route as a namespace constant/method,
61
+ # plus the aggregate `find`/`all`/`count`/`events`/`repository` door and
62
+ # short `Aggregate.verb` shortcuts.
63
+ #
64
+ # @return [self]
52
65
  def install!
53
66
  current_entries.each { |entry| install_namespace_entry(entry) }
54
67
  install_shortcuts!
data/lib/hecks/router.rb CHANGED
@@ -9,31 +9,60 @@ module Hecks
9
9
 
10
10
  attr_reader :register
11
11
 
12
+ # Loads a project's bluebooks and builds a router over them, without
13
+ # installing any Ruby namespace shortcuts.
14
+ #
15
+ # @param root [String] project root to discover bluebooks under
16
+ # @return [Router] the built router
12
17
  def self.load(root) = new(Bluebook::ProjectLoader.load(root))
13
18
 
14
19
  # Install one project router for ordinary application calls. The explicit
15
20
  # `Router.load` API remains useful for tests and embedded hosts.
21
+ #
22
+ # @param root [String] project root to discover bluebooks under
23
+ # @return [Router] the booted router, also stored as `.default`
16
24
  def self.boot(root)
17
25
  router = load(root)
18
26
  router.install_namespace!
19
27
  @default = router
20
28
  end
21
29
 
30
+ # The process-wide router `.boot` installed.
31
+ #
32
+ # @return [Router] the router `.boot` installed
33
+ # @raise [NotBooted] if `.boot` has not been called yet
22
34
  def self.default
23
35
  @default || raise(NotBooted, "no project router is booted — call Hecks::Router.boot(root) first")
24
36
  end
25
37
 
38
+ # (see #dispatch)
26
39
  def self.dispatch(address, **args) = default.dispatch(address, **args)
40
+ # (see #query)
27
41
  def self.query(address, **args) = default.query(address, **args)
28
42
 
43
+ # @param register [Bluebook::ProjectRegister, Bluebook::ProjectLoader] the FQN
44
+ # catalogue to resolve addresses against
29
45
  def initialize(register)
30
46
  @register = register
31
47
  end
32
48
 
49
+ # Lists every routed FQN entry.
50
+ #
51
+ # @return [Array<Bluebook::ProjectRegister::Entry>] every routed FQN entry
33
52
  def available = register.entries.values
34
53
 
54
+ # Installs Ruby namespace constants and shortcut methods for every
55
+ # current-version route.
56
+ #
57
+ # @return [NamespaceInstaller] the installer that performed the install
35
58
  def install_namespace! = NamespaceInstaller.new(self).install!
36
59
 
60
+ # Resolves an address to its routed FQN entry.
61
+ #
62
+ # @param address [String] a fully-qualified command or query address, realm included
63
+ # @return [Bluebook::ProjectRegister::Entry] the routed entry
64
+ # @raise [UnknownAddress] if `address` has no realm, or names no known route
65
+ # @raise [Fqn::Invalid] if `address` is not a well-formed FQN
37
66
  def resolve(address)
38
67
  fqn = Fqn.parse(address)
39
68
  raise UnknownAddress, "router addresses require a realm: #{address.inspect}" unless fqn.realm
@@ -43,6 +72,19 @@ module Hecks
43
72
  raise UnknownAddress, "no Bluebook route for #{address.inspect}"
44
73
  end
45
74
 
75
+ # Dispatches a command to its resolved aggregate.
76
+ #
77
+ # @param address [String] a fully-qualified command address, realm included
78
+ # @param args [Hash] command facts, plus the dispatcher's optional `:to`, `:with`,
79
+ # and `:saga_correlation` keys
80
+ # @return [Runtime::Dispatcher::Result] the dispatch result
81
+ # @raise [UnknownAddress] if `address` has no realm, or names no known route
82
+ # @raise [WrongVerbKind] if `address` names a query
83
+ # @raise [Runtime::UnknownVerb] if the resolved verb names something undeclared
84
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
85
+ # the call
86
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
87
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
46
88
  def dispatch(address, **args)
47
89
  entry = resolve(address)
48
90
  raise WrongVerbKind, "#{address.inspect} names a query; use #query" unless entry.command?
@@ -50,6 +92,19 @@ module Hecks
50
92
  entry.dispatcher.dispatch_flat(local_verb(entry), args)
51
93
  end
52
94
 
95
+ # Queries a resolved aggregate, entity, or read model.
96
+ #
97
+ # @param address [String] a fully-qualified query address, realm included
98
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
99
+ # @return [Array<Hash>] one row Hash per match; see `Runtime::Dispatcher#query`
100
+ # for the exact shape per address kind
101
+ # @raise [UnknownAddress] if `address` has no realm, or names no known route
102
+ # @raise [WrongVerbKind] if `address` names a command
103
+ # @raise [Runtime::UnknownVerb] if the resolved verb names something undeclared
104
+ # @raise [Runtime::NotFound] if a read model's root reference names no record
105
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
106
+ # @raise [KeyError] if a rooted read model is asked without its reference argument
107
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
53
108
  def query(address, **args)
54
109
  entry = resolve(address)
55
110
  raise WrongVerbKind, "#{address.inspect} names a command; use #dispatch" unless entry.query?
@@ -29,12 +29,21 @@ module Hecks
29
29
  @locks = {}
30
30
 
31
31
  class << self
32
+ # Returns the Mutex striped to one record, creating it on first use.
33
+ #
32
34
  # `AggregateLock.for(domain, aggregate, id).synchronize { ... }`
33
35
  # `id: nil` — identity could not be resolved yet (see
34
36
  # `Identity.best_effort`) — locks by aggregate type alone, coarser
35
37
  # (every record of this aggregate serializes against every other)
36
38
  # but still correct: it can only ever make dispatch more
37
39
  # conservative than a resolved id would.
40
+ #
41
+ # @param domain [String] the domain the aggregate belongs to
42
+ # @param aggregate [Bluebook::Aggregate] the aggregate being dispatched
43
+ # @param id [String, nil] the resolved record id, or nil to stripe by aggregate type
44
+ # alone
45
+ # @return [Mutex] the mutex for this `[domain, aggregate, id]` key, held for the full
46
+ # hydrate-through-save critical section
38
47
  def for(domain, aggregate, id = nil)
39
48
  key = id.nil? ? [domain.to_s, aggregate.hecks_name] : [domain.to_s, aggregate.hecks_name, id.to_s]
40
49
  @registry_lock.synchronize { @locks[key] ||= Mutex.new }
@@ -20,19 +20,37 @@ module Hecks
20
20
  # Sharing a primitive between them is deferred until a third consumer
21
21
  # actually wants it (0031's own Rejected Alternatives).
22
22
  class BootGates
23
+ # @return [void]
23
24
  def initialize
24
25
  @gates = Hash.new { |h, k| h[k] = [] }
25
26
  end
26
27
 
28
+ # Adds a gate to `phase`, run in registration order alongside any other gate there.
29
+ #
30
+ # @param name [Symbol] the gate's name, checked by `registered?`
31
+ # @param gate [Proc, Method] the gate; called as `gate.call(registry, directory)`
32
+ # @param phase [Symbol] the phase this gate runs under, such as `:pre_verify` or
33
+ # `:post_verify`
34
+ # @return [Hecks::Runtime::BootGates] self, for chaining
27
35
  def register(name, gate, phase:)
28
36
  @gates[phase] << [name, gate]
29
37
  self
30
38
  end
31
39
 
40
+ # Reports whether a gate named `name` has been registered, under any phase.
41
+ #
42
+ # @param name [Symbol] the gate name to look for, across every phase
43
+ # @return [Boolean] true if a gate named `name` was registered under any phase
32
44
  def registered?(name)
33
45
  @gates.values.flatten(1).any? { |registered_name, _gate| registered_name == name }
34
46
  end
35
47
 
48
+ # Runs every gate registered under `phase`, in registration order.
49
+ #
50
+ # @param phase [Symbol] the phase to run, such as `:pre_verify` or `:post_verify`
51
+ # @param registry [Runtime::Registry] the booted registry, passed to each gate
52
+ # @param directory [String] the boot directory, passed to each gate
53
+ # @return [void]
36
54
  def run!(phase, registry, directory)
37
55
  @gates[phase].each { |pair| pair.last.call(registry, directory) }
38
56
  end
@@ -37,8 +37,34 @@ module Hecks
37
37
 
38
38
  module_function
39
39
 
40
+ # The ambient caller bound by the innermost enclosing `as` block on this thread.
41
+ #
42
+ # @return [Runtime::Caller::Current, nil] the bound caller, or nil when no `as`
43
+ # block is on the stack for this thread
40
44
  def current = Thread.current[:hecks_caller]
41
45
 
46
+ # Binds the ambient caller for the duration of the block, restoring whatever
47
+ # caller (if any) was bound before, even if the block raises.
48
+ #
49
+ # `actor_id`, `as_of` and `scope` are each optional, self-asserted facts about
50
+ # the caller rather than derived from the command — see `Current`'s own header
51
+ # above for why each stays here instead of becoming a command-level DSL
52
+ # construct.
53
+ #
54
+ # @param role [String] the role the caller states it holds, checked by string
55
+ # equality against the command's declared `role`
56
+ # @param actor_id [String, nil] who the caller is; when given, lets
57
+ # `CommandRules::Authorization` check a real Governance `RoleAssignment`
58
+ # instead of the bare role-string comparison
59
+ # @param as_of [Integer, nil] Unix epoch seconds the caller asserts as "now",
60
+ # filled at the door from `Ports::Clock.now`; nil leaves a `RoleAssignment`'s
61
+ # `starts_at` unchecked
62
+ # @param scope [String, nil] the scope the caller states it is acting in,
63
+ # checked against the matching `RoleAssignment`'s own `scope`; nil skips
64
+ # that check
65
+ # @yield the code that should see `role`/`actor_id`/`as_of`/`scope` as the
66
+ # ambient caller
67
+ # @return [Object] the block's result
42
68
  def as(role:, actor_id: nil, as_of: nil, scope: nil)
43
69
  previous = Thread.current[:hecks_caller]
44
70
  Thread.current[:hecks_caller] = Current.new(
@@ -49,11 +75,17 @@ module Hecks
49
75
  Thread.current[:hecks_caller] = previous
50
76
  end
51
77
 
78
+ # Clears the ambient caller for the duration of the block, restoring
79
+ # whatever caller (if any) was bound before, even if the block raises.
80
+ #
52
81
  # A reaction is the system acting, not the caller who happened to be
53
82
  # on the stack when the triggering command ran — `Dispatcher#reenter`
54
83
  # clears the ambient caller around a reaction's own dispatch so a
55
84
  # triggering caller's role can neither satisfy nor block a reaction
56
85
  # command it has nothing to do with.
86
+ #
87
+ # @yield the code that should see no ambient caller bound
88
+ # @return [Object] the block's result
57
89
  def without
58
90
  previous = Thread.current[:hecks_caller]
59
91
  Thread.current[:hecks_caller] = nil
@@ -14,6 +14,8 @@ module Hecks
14
14
  # adapter names the port it implements, and neither can point at a third
15
15
  # port. There is no edge for a cycle to be made of.
16
16
  class CapabilityGraph
17
+ # @param registry [Runtime::Registry] the booted registry to read ports and adapters from
18
+ # @return [void]
17
19
  def initialize(registry)
18
20
  @registry = registry
19
21
  end
@@ -21,6 +23,9 @@ module Hecks
21
23
  # { port name => [adapter name, ...] }, for every port the registry
22
24
  # declares — including the ports nothing implements, so a caller can
23
25
  # tell "declared, unfulfilled" from "never declared at all".
26
+ #
27
+ # @return [Hash{String => Array<String>}] each declared port's name mapped to the names
28
+ # of every adapter bound to it (empty when none are)
24
29
  def fulfillments
25
30
  @fulfillments ||= @registry.ports.each_key.to_h { |name| [name, adapters_for(name)] }
26
31
  end
@@ -28,10 +33,16 @@ module Hecks
28
33
  # The ports with zero adapters bound — the gap `Runtime::WiringError`
29
34
  # would otherwise only surface at the moment something tries to dispatch
30
35
  # through one.
36
+ #
37
+ # @return [Array<String>] the names of every declared port with no adapter bound to it
31
38
  def unfulfilled
32
39
  fulfillments.select { |_, adapters| adapters.empty? }.keys
33
40
  end
34
41
 
42
+ # Reports the port-dependency cycles this registry has — always none; see the module
43
+ # header for why no port model here can express one.
44
+ #
45
+ # @return [Array] always empty
35
46
  def cycles = []
36
47
 
37
48
  private
@@ -10,10 +10,10 @@ module Hecks
10
10
  module ArgumentGate
11
11
  private
12
12
 
13
- # Anything else used to ride along in the payload untouched —
14
- # normalize_args walks the declared attributes, so a name the command
15
- # never had was simply never looked at. A misspelled argument did
16
- # nothing, in silence.
13
+ # Without this check, anything else would ride along in the payload
14
+ # untouched — normalize_args walks only the declared attributes, so
15
+ # a name the command never had is simply never looked at, and a
16
+ # misspelled argument would do nothing, in silence.
17
17
  #
18
18
  # The keys that are legitimately not attributes are the ones that address
19
19
  # the aggregate rather than describe it : `id`, whatever the aggregate is
@@ -49,12 +49,13 @@ module Hecks
49
49
  # until fuzz went looking : a name the command never declared was refused,
50
50
  # while a name it did declare could simply be left out.
51
51
  #
52
- # `Customer.Register` without its `name` used to be refused — but by
53
- # accident, and with a lie for a message. `then_set :name, to: :name` found
54
- # nothing to resolve, passed the literal symbol on, and coercion reported
55
- # `name is a PersonName — pass its fields as an object`, which describes a
56
- # mistake the caller did not make. The real mistake — an argument simply
57
- # missing — was never the one named, and nothing refused it on purpose.
52
+ # Refusing `Customer.Register` for a missing `name` needs to be
53
+ # deliberate, not an accident with a lie for a message. Without this
54
+ # check, `then_set :name, to: :name` finds nothing to resolve, passes
55
+ # the literal symbol on, and coercion reports `name is a PersonName —
56
+ # pass its fields as an object`, which describes a mistake the caller
57
+ # did not make. The real mistake — an argument simply missing — is
58
+ # never the one named unless this refuses it on purpose.
58
59
  #
59
60
  # No command attribute anywhere in the corpus carries a default — checked,
60
61
  # all eight chapters, zero — so there is no optional argument for this to
@@ -65,16 +66,17 @@ module Hecks
65
66
  # (`CommandBuilder#reference_to`'s bare self-reference mints no
66
67
  # attribute to be absent) — but `PortOperationBuilder#reference_to`
67
68
  # always mints one (this file's own header on `PortOperation`: "no
68
- # creates?/acts_on distinction to protect"), because an operation
69
- # historically had no other way to say which record it addressed.
70
- # `to:` is that other way now (`Dispatcher#port_invocation`
71
- # promotes the identity attribute's own value out of the payload
72
- # and into routing) — which left the attribute still declared,
73
- # still non-optional, and now never present in `args` at all: every
74
- # operation with an identity attribute refused its own well-formed
75
- # calls, dispatched exactly the way `to:` intends. Exempted here on
76
- # the same terms an aggregate command's self-address always was —
77
- # an address, not a fact the operation still needs handed back.
69
+ # creates?/acts_on distinction to protect"), because an operation had
70
+ # no other way to say which record it addressed before `to:` existed.
71
+ # `to:` is that way now (`Dispatcher#port_invocation` promotes the
72
+ # identity attribute's own value out of the payload and into
73
+ # routing) — which leaves the attribute still declared, still
74
+ # non-optional, and never present in `args` at all: without this
75
+ # exemption, every operation with an identity attribute would refuse
76
+ # its own well-formed calls, dispatched exactly the way `to:`
77
+ # intends. Exempted here on the same terms an aggregate command's
78
+ # self-address always was — an address, not a fact the operation
79
+ # still needs handed back.
78
80
  def refuse_absent_arguments(command, args, aggregate: nil)
79
81
  given = args.keys.map(&:to_sym)
80
82
  exempt = aggregate && command.respond_to?(:identity_attribute) &&
@@ -92,7 +94,7 @@ module Hecks
92
94
  declared: declared_names(command))
93
95
  end
94
96
 
95
- # **A command that declares nothing still has to say so**. `Account
97
+ # A command that declares nothing still has to say so. `Account
96
98
  # .Freeze` is `reference_to Account` and no attributes at all, so
97
99
  # `{declared}` rendered empty and the sentence trailed off mid-
98
100
  # clause : "Freeze does not declare standing — it takes ". Read
@@ -165,9 +165,9 @@ module Hecks
165
165
  fields)
166
166
  end
167
167
 
168
- # Frozen, like every other value the domain hands back. An
169
- # appended list used to come back mutable, so a caller could
170
- # push straight into an aggregate's own state after the
168
+ # Frozen, like every other value the domain hands back — without
169
+ # this, an appended list would come back mutable, letting a
170
+ # caller push straight into an aggregate's own state after the
171
171
  # dispatch had finished.
172
172
  Freezer.deep(Array(instance[mutation.target]) + [element])
173
173
  end
@@ -197,11 +197,11 @@ module Hecks
197
197
 
198
198
  # Vendored fix, not (yet) upstream hecks (migration plan
199
199
  # task 9): #apply's `:increment`/`:decrement`/`:multiply`
200
- # branches used to wrap `amount` into a `Value` unconditionally
200
+ # branches wrapping `amount` into a `Value` unconditionally
201
201
  # whenever the target attribute existed, never checking whether
202
202
  # `current` (the field's own existing value, read straight off
203
- # `instance[mutation.target]`) was also wrapped -- the two sides
204
- # of the same arithmetic call could disagree on Value-ness. On a
203
+ # `instance[mutation.target]`) was also wrapped, would let the two
204
+ # sides of the same arithmetic call disagree on Value-ness. On a
205
205
  # phantom-created field this is the common case, not an edge
206
206
  # one: `Instance.defaults`/`#default_for` leaves a VO-typed
207
207
  # attribute with no declared `default:` genuinely absent (nil),
@@ -276,16 +276,15 @@ module Hecks
276
276
  held.max.to_i + 1
277
277
  end
278
278
 
279
- # Moved to `EntityElement.check_entity_collision` (entity_element.rb)
280
- # — BUG#145. Used to live here, called only from `#entity_element`
281
- # above (an aggregate's own entity list, e.g. `Workspace.boards`).
279
+ # The entity-collision check lives in `EntityElement.check_entity_collision`
280
+ # (entity_element.rb) — BUG#145 — shared by both `#entity_element` above
281
+ # (an aggregate's own entity list, e.g. `Workspace.boards`) and
282
282
  # `EntityElement#appended_to_element`'s own nested-entity branch (an
283
- # entity's own entity list one hop further in, e.g. `Board.cards`)
284
- # needs the exact same guard — see that method's own call site and
285
- # comment for why a caller-supplied nested identity was silently
286
- # duplicating before this moved. Pure relocation, not a behavior
287
- # change here: the check's own doc comment (heads/composite/
288
- # auto-mint reasoning) now lives with the code, in entity_element.rb.
283
+ # entity's own entity list one hop further in, e.g. `Board.cards`),
284
+ # which needs the exact same guard against a caller-supplied nested
285
+ # identity silently duplicating. See that method's own call site and
286
+ # comment; the check's own doc comment (heads/composite/auto-mint
287
+ # reasoning) lives with the code, in entity_element.rb.
289
288
  end
290
289
  end
291
290
  end
@@ -31,7 +31,7 @@ module Hecks
31
31
  # both directions.
32
32
  DISPATCH_ORDER = Hecks::Vocabulary.symbols("AggregateDispatchOrder")
33
33
 
34
- # **A last-resort safety valve, not the normal outcome path** — see
34
+ # A last-resort safety valve, not the normal outcome path — see
35
35
  # `Runtime::StaleWrite`'s own comment. Two concurrent writers
36
36
  # against one aggregate resolve through exactly one retry in the
37
37
  # ordinary case (the loser's retried hydrate reads the winner's now-
@@ -41,14 +41,17 @@ module Hecks
41
41
  # two-writer case.
42
42
  MAX_STALE_WRITE_RETRIES = 5
43
43
 
44
- # Every cross-step local `call` used to thread through its own literal
45
- # sequence, held in one place now that the sequence is data-driven —
44
+ # Every cross-step local held in one place, now that the sequence is
45
+ # data-driven rather than `call`'s own literal method-call sequence —
46
46
  # `result` and `transition`/`old_state` default to nil until the step
47
- # that sets them runs, same as they were unset locals before that point.
47
+ # that sets them runs, the same as unset locals would be before that point.
48
48
  Context = Struct.new(:domain, :aggregate, :command, :args, :repository, :instance, :transition, :old_state,
49
49
  :result, :correlation, :route, :plan, :strategy, :persistence_outcome, :pending_delegation,
50
50
  :dry_run, :correction_bindings, :outbox_rows, :invocation)
51
51
 
52
+ # @param registry [Runtime::Registry] the booted registry this interpreter reads
53
+ # @param rules [Runtime::CommandRules] the shared rules engine (admissibility,
54
+ # references, arithmetic, authorization, emission) dispatch runs through
52
55
  def initialize(registry, rules:)
53
56
  @registry = registry
54
57
  @rules = rules
@@ -69,6 +72,27 @@ module Hecks
69
72
  # `invocation` — the `Runtime::Invocation` `Dispatcher` built for this
70
73
  # call. `ctx.args` is `invocation.to_args` (the same Hash routing
71
74
  # always handed this method), `ctx.route` its `target`.
75
+ #
76
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
77
+ # @param aggregate [Bluebook::Aggregate] the aggregate the command acts on
78
+ # @param command [Class] the command class (`Bluebook::Command` subclass) to dispatch
79
+ # @param invocation [Runtime::Invocation] the invocation `Dispatcher` built for
80
+ # this call
81
+ # @param correlation [Hash{Symbol => Object}, nil] correlation head => value,
82
+ # stamped on every emitted event when a saga leg causes this dispatch; nil
83
+ # otherwise
84
+ # @param dry_run [Boolean] whether to run every step through validation without
85
+ # saving, emitting or enqueueing
86
+ # @return [Array(Runtime::Instance, Array<Runtime::Event>,
87
+ # Runtime::DependencyPlanning::Plan, Ports::Persistence::Execution,
88
+ # Array<Runtime::Outbox::Row>)] the settled instance, emitted events,
89
+ # execution plan, persistence outcome and outbox rows — the last
90
+ # three nil on a dry run, which skips save/emit/outbox
91
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
92
+ # given/ensures/invariant/authorization/admissibility rule refuses
93
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every
94
+ # retry (`MAX_STALE_WRITE_RETRIES`)
95
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
72
96
  def call(domain, aggregate, command, invocation, correlation = nil, dry_run: false)
73
97
  args = invocation.to_args
74
98
  route = invocation.target
@@ -273,9 +297,9 @@ module Hecks
273
297
  # `ensures`/`enforce_invariants`/`save` steps still run after
274
298
  # this one. The target's emission is parked and performed by
275
299
  # `step_emit`, after the parent committed — where every other
276
- # command's events are emitted too. (Before this, the entity
277
- # leg's events were on the event log and in the adapter before
278
- # the parent could refuse.)
300
+ # command's events are emitted too. (Without parking it here, the
301
+ # entity leg's events would be on the event log and in the adapter
302
+ # before the parent could refuse.)
279
303
  ctx.pending_delegation = [target_command, target_args]
280
304
  end
281
305
  end
@@ -350,7 +374,7 @@ module Hecks
350
374
 
351
375
  def persist_instance(ctx)
352
376
  if ctx.strategy == DependencyPlanning::ATOMIC_PUT
353
- # **A second creation is not a fresh one** — see
377
+ # A second creation is not a fresh one — see
354
378
  # hydrate_complete_state's own comment; the
355
379
  # same refusal, on the same terms, for the
356
380
  # complete-state path. `insert_only:` asks the
@@ -441,8 +465,9 @@ module Hecks
441
465
  # `:delegate` mutation.
442
466
  if ctx.pending_delegation
443
467
  target_command, target_args = ctx.pending_delegation
444
- # The same dispatch, so the same correlation — a saga-driven
445
- # door's events used to lose their stamp here.
468
+ # The same dispatch, so the same correlation — without
469
+ # threading `ctx.correlation` through, a saga-driven door's
470
+ # events would lose their stamp here.
446
471
  next @rules.emit(target_command, ctx.domain, ctx.aggregate, ctx.instance, target_args, ctx.repository,
447
472
  ctx.correlation)
448
473
  end
@@ -562,7 +587,7 @@ module Hecks
562
587
  aggregate: aggregate.hecks_name,
563
588
  identity: identity_reading(aggregate)))
564
589
 
565
- # **A second creation is not a fresh one** — `creates?` on an identity
590
+ # A second creation is not a fresh one — `creates?` on an identity
566
591
  # a record already exists under refuses (`AlreadyExists`) rather
567
592
  # than silently overwriting it, the same refusal `hydrate_prior_
568
593
  # or_initial`'s own body gives for its own complete-but-state-
@@ -604,7 +629,7 @@ module Hecks
604
629
  identity: identity_reading(aggregate)))
605
630
  found = repository.find(id)
606
631
 
607
- # **A second creation is not a fresh one** — see hydrate_complete_
632
+ # A second creation is not a fresh one — see hydrate_complete_
608
633
  # state's own comment; the same refusal, on the same terms, for
609
634
  # a complete-but-state-dependent command (one with a `given`
610
635
  # reading its own prior state, which is what routes here instead
@@ -614,10 +639,10 @@ module Hecks
614
639
  # prior state this branch exists to supply), so only a genuine
615
640
  # creation reusing an already-occupied identity is a duplicate.
616
641
  # `SafeDepositBox.Rent` is exactly this shape — `given("box is
617
- # vacant")` makes it state-dependent, so a second Rent used to
618
- # silently hydrate the existing box as "prior state" and refuse
619
- # for the wrong reason (not vacant) instead of the right one
620
- # (already exists).
642
+ # vacant")` makes it state-dependent, so without this check a
643
+ # second Rent would silently hydrate the existing box as "prior
644
+ # state" and refuse for the wrong reason (not vacant) instead of
645
+ # the right one (already exists).
621
646
  if found && command.creates?
622
647
  raise(AlreadyExists, RefusalWording.render_site("AlreadyExists", "creating_duplicate",
623
648
  command: command.hecks_name, aggregate: aggregate.hecks_name,
@@ -628,7 +653,7 @@ module Hecks
628
653
  found ? found.dup : Instance.new(aggregate: aggregate, id: id, args: args)
629
654
  end
630
655
 
631
- # **The join, the dig, and the reading** — all shared with `EntityInterpreter`
656
+ # The join, the dig, and the reading — all shared with `EntityInterpreter`
632
657
  # now, in `Runtime::Identity`, rather than kept as two copies that could
633
658
  # only ever drift. See that module for the reasoning ; these three stay
634
659
  # here, at the old names, purely so nothing below has to change.