hecks 1.4.0 → 1.5.1

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 (275) 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.bluebook +105 -0
  112. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  113. data/lib/hecks/deploy/oidc.json +5 -0
  114. data/lib/hecks/doc/reference.rb +185 -16
  115. data/lib/hecks/embryonaut_bluebook.rb +32 -9
  116. data/lib/hecks/facade/handle.rb +76 -3
  117. data/lib/hecks/facade/surface/aggregate_door.rb +8 -0
  118. data/lib/hecks/forms/field_shape.rb +3 -0
  119. data/lib/hecks/forms/page.rb +14 -0
  120. data/lib/hecks/forms/port_argument.rb +12 -0
  121. data/lib/hecks/forms/query_form_renderer.rb +63 -0
  122. data/lib/hecks/forms/record_renderer.rb +58 -0
  123. data/lib/hecks/forms/record_table.rb +27 -0
  124. data/lib/hecks/forms/reference_options.rb +24 -0
  125. data/lib/hecks/forms/value_object_shape.rb +10 -0
  126. data/lib/hecks/fqn.rb +58 -0
  127. data/lib/hecks/framework/bluebook/compliance.bluebook +221 -0
  128. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  129. data/lib/hecks/framework/oidc.json +15 -0
  130. data/lib/hecks/framework.rb +43 -20
  131. data/lib/hecks/freezer.rb +17 -1
  132. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +159 -24
  133. data/lib/hecks/fuzzing/combination_miner.rb +59 -0
  134. data/lib/hecks/fuzzing/concurrent_dispatch.rb +109 -8
  135. data/lib/hecks/fuzzing/coverage_campaign.rb +56 -13
  136. data/lib/hecks/fuzzing/differential.rb +34 -0
  137. data/lib/hecks/fuzzing/domain_generator.rb +188 -11
  138. data/lib/hecks/fuzzing/era_boundary.rb +45 -15
  139. data/lib/hecks/fuzzing/form_census.rb +86 -0
  140. data/lib/hecks/fuzzing/generated_domain_check.rb +76 -0
  141. data/lib/hecks/fuzzing/invalid_value_generator.rb +39 -0
  142. data/lib/hecks/fuzzing/isolated_boot.rb +79 -22
  143. data/lib/hecks/fuzzing/nondeterministic.rb +13 -1
  144. data/lib/hecks/fuzzing/persistence_parity.rb +95 -3
  145. data/lib/hecks/fuzzing/properties/corrections.rb +25 -0
  146. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +158 -14
  147. data/lib/hecks/fuzzing/properties/guards.rb +44 -0
  148. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +48 -0
  149. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +18 -0
  150. data/lib/hecks/fuzzing/properties/outbox.rb +49 -11
  151. data/lib/hecks/fuzzing/properties/querying.rb +68 -14
  152. data/lib/hecks/fuzzing/properties.rb +24 -15
  153. data/lib/hecks/fuzzing/qa_settings.rb +12 -0
  154. data/lib/hecks/fuzzing/replay.rb +137 -29
  155. data/lib/hecks/fuzzing/rotation_priority.rb +41 -21
  156. data/lib/hecks/fuzzing/rust_gap_manifest.rb +46 -20
  157. data/lib/hecks/fuzzing/self_consistency.rb +189 -40
  158. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +12 -6
  159. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +1 -1
  160. data/lib/hecks/fuzzing/sequence_generator.rb +47 -0
  161. data/lib/hecks/fuzzing/shrinker.rb +114 -2
  162. data/lib/hecks/fuzzing/structural_skips.rb +18 -4
  163. data/lib/hecks/fuzzing/sweep_depth.rb +8 -0
  164. data/lib/hecks/fuzzing/target_capabilities.rb +61 -14
  165. data/lib/hecks/fuzzing/value_generator.rb +98 -10
  166. data/lib/hecks/grammar/evolve.rb +178 -2
  167. data/lib/hecks/grammar.rb +46 -0
  168. data/lib/hecks/ir.rb +38 -7
  169. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  170. data/lib/hecks/literal.rb +32 -0
  171. data/lib/hecks/naming.rb +88 -7
  172. data/lib/hecks/ports/access_control.rb +5 -10
  173. data/lib/hecks/ports/authorization.rb +3 -6
  174. data/lib/hecks/ports/identity_assignment.rb +1 -2
  175. data/lib/hecks/ports/identity_resolution.rb +1 -2
  176. data/lib/hecks/ports/key_vault.port +6 -0
  177. data/lib/hecks/ports/key_vault.rb +85 -0
  178. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +6 -0
  179. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +38 -2
  180. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +6 -0
  181. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +40 -0
  182. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +92 -1
  183. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  184. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +8 -0
  185. data/lib/hecks/ports/query/in_memory.rb +39 -6
  186. data/lib/hecks/ports/query/ordering.rb +15 -0
  187. data/lib/hecks/ports.rb +1 -0
  188. data/lib/hecks/projections/bootstrap_table.rb +43 -8
  189. data/lib/hecks/projections/deploy/fargate.rb +666 -0
  190. data/lib/hecks/projections/deploy/lambda.rb +2423 -0
  191. data/lib/hecks/projections/deploy/shared.rb +624 -0
  192. data/lib/hecks/projections/diagrams.rb +243 -7
  193. data/lib/hecks/projections/glossary/html.rb +88 -0
  194. data/lib/hecks/projections/glossary/markdown.rb +47 -0
  195. data/lib/hecks/projections/glossary/mermaid.rb +48 -0
  196. data/lib/hecks/projections/glossary/sentences.rb +105 -0
  197. data/lib/hecks/projections/glossary.rb +161 -19
  198. data/lib/hecks/projections/model/deviations.rb +44 -0
  199. data/lib/hecks/projections/model.rb +51 -1
  200. data/lib/hecks/projections/oidc.rb +40 -3
  201. data/lib/hecks/projections/parser_table.rb +49 -8
  202. data/lib/hecks/projections/reference.rb +12 -0
  203. data/lib/hecks/projections/rust_vocabulary.rb +219 -16
  204. data/lib/hecks/projections/shape.rb +8 -0
  205. data/lib/hecks/projections/statements.rb +63 -16
  206. data/lib/hecks/projections/vocabulary.rb +17 -0
  207. data/lib/hecks/projections.rb +3 -0
  208. data/lib/hecks/projector/cli_projector.rb +218 -10
  209. data/lib/hecks/projector/docs_projector.rb +145 -19
  210. data/lib/hecks/projector/exporter.rb +65 -11
  211. data/lib/hecks/projector/ir_projector.rb +6 -0
  212. data/lib/hecks/projector/narrate_projector.rb +136 -15
  213. data/lib/hecks/projector/target.rb +47 -10
  214. data/lib/hecks/projector.rb +101 -19
  215. data/lib/hecks/query_ir.rb +47 -0
  216. data/lib/hecks/query_specification/common/null_policy.rb +5 -3
  217. data/lib/hecks/rendering.rb +6 -0
  218. data/lib/hecks/router/namespace_installer.rb +13 -0
  219. data/lib/hecks/router.rb +55 -0
  220. data/lib/hecks/runtime/aggregate_lock.rb +9 -0
  221. data/lib/hecks/runtime/boot_gates.rb +18 -0
  222. data/lib/hecks/runtime/caller.rb +32 -0
  223. data/lib/hecks/runtime/capability_graph.rb +11 -0
  224. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +23 -21
  225. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +14 -15
  226. data/lib/hecks/runtime/command_interpreter.rb +42 -17
  227. data/lib/hecks/runtime/command_rules/admissibility.rb +165 -14
  228. data/lib/hecks/runtime/command_rules/arithmetic.rb +17 -5
  229. data/lib/hecks/runtime/command_rules/references.rb +118 -28
  230. data/lib/hecks/runtime/dependency_planning.rb +45 -0
  231. data/lib/hecks/runtime/dispatcher.rb +28 -50
  232. data/lib/hecks/runtime/entity_element.rb +161 -8
  233. data/lib/hecks/runtime/entity_interpreter.rb +44 -9
  234. data/lib/hecks/runtime/errors.rb +18 -4
  235. data/lib/hecks/runtime/event.rb +10 -5
  236. data/lib/hecks/runtime/identity.rb +71 -3
  237. data/lib/hecks/runtime/instance.rb +67 -7
  238. data/lib/hecks/runtime/interpreting.rb +13 -5
  239. data/lib/hecks/runtime/invocation.rb +118 -36
  240. data/lib/hecks/runtime/loader.rb +94 -8
  241. data/lib/hecks/runtime/outbox.rb +145 -7
  242. data/lib/hecks/runtime/policy_interpreter.rb +22 -9
  243. data/lib/hecks/runtime/port_operation_interpreter.rb +20 -0
  244. data/lib/hecks/runtime/query_interpreter.rb +40 -12
  245. data/lib/hecks/runtime/reaction_invocation.rb +53 -8
  246. data/lib/hecks/runtime/read_model_interpreter.rb +23 -7
  247. data/lib/hecks/runtime/rebuild_sweep.rb +28 -0
  248. data/lib/hecks/runtime/reference_hop.rb +42 -0
  249. data/lib/hecks/runtime/refusal_wording.rb +50 -0
  250. data/lib/hecks/runtime/registry/saga_persistence.rb +11 -0
  251. data/lib/hecks/runtime/registry/verification.rb +119 -4
  252. data/lib/hecks/runtime/registry.rb +157 -4
  253. data/lib/hecks/runtime/remote_dispatcher.rb +92 -6
  254. data/lib/hecks/runtime/routing.rb +27 -2
  255. data/lib/hecks/runtime/saga_interpreter/correlation.rb +10 -12
  256. data/lib/hecks/runtime/saga_interpreter.rb +27 -13
  257. data/lib/hecks/runtime/tenant_check.rb +26 -6
  258. data/lib/hecks/runtime/tenant_scope.rb +18 -0
  259. data/lib/hecks/runtime/value/coercion.rb +255 -33
  260. data/lib/hecks/runtime/value/entity_list_coercion.rb +102 -30
  261. data/lib/hecks/runtime/value.rb +50 -0
  262. data/lib/hecks/runtime.rb +32 -0
  263. data/lib/hecks/storehouse.rb +305 -9
  264. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  265. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  266. data/lib/hecks/version.rb +1 -1
  267. data/lib/hecks.rb +79 -1
  268. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +8 -0
  269. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +12 -2
  270. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +29 -5
  271. metadata +14 -5
  272. data/lib/hecks/codemod/legacy_dispatch_args.rb +0 -299
  273. data/lib/hecks/codemod/legacy_dispatch_recorder.rb +0 -186
  274. data/lib/hecks/deprecation.rb +0 -95
  275. data/lib/hecks/framework/bluebook/compliance.bluebook +0 -1
@@ -16,12 +16,17 @@ module Hecks
16
16
 
17
17
  attr_reader :root, :bluebooks, :hecksagons, :ports, :adapters, :worlds, :event_log,
18
18
  :reaction_log, :saga_log, :saga_instances, :translations, :saga_mutex,
19
- :saga_dispatch_log, :policy_dispatch_log
19
+ :saga_dispatch_log, :policy_dispatch_log, :bluebook_sources,
20
+ :pending_privacy_markings
20
21
 
22
+ # @param root [String, nil] the booting project's root directory, the base
23
+ # a shared ports/adapters root and a `.world`'s own relative paths resolve
24
+ # against; nil for a registry with no such root
21
25
  def initialize(root: nil)
22
26
  @root = root
23
27
  @bluebooks = {}
24
- @hecksagons = {}
28
+ @bluebook_sources = {}
29
+ @hecksagons = {}
25
30
  @ports = {}
26
31
  @adapters = {}
27
32
  @worlds = {}
@@ -29,6 +34,14 @@ module Hecks
29
34
  @event_log = []
30
35
  @reaction_log = []
31
36
  @saga_log = []
37
+ # A DECLARATIVE FACT, NOT YET A DISPATCHED ONE — `AggregateDoor#
38
+ # mark_sensitive` (called from a `.hecksagon` file, the same way
39
+ # `port`/`persisted_by` already are) appends here at
40
+ # hecksagon-build time; `Runtime::Loader.boot`'s own post-dispatcher
41
+ # step turns each entry into a real `Privacy::Marking.Mark`,
42
+ # idempotently, the same "declared here, taken effect once boot
43
+ # actually has a dispatcher" shape `redrive_outbox!` already has.
44
+ @pending_privacy_markings = []
32
45
  # **Additive, Ruby-only** — never merged into saga_log/reaction_log.
33
46
  # rust/src/kernel/orchestrate.rs ports those two arrays' exact
34
47
  # shape byte-for-byte (spec/rust_conformance_spec.rb's own
@@ -112,6 +125,14 @@ module Hecks
112
125
  # several files (`language/bluebook/*.bluebook`, all `Hecks.bluebook "Bluebook"`)
113
126
  # needs its declarations to accumulate into one builder rather than each
114
127
  # file minting its own and silently discarding the one before.
128
+ #
129
+ # @param name [String, Symbol] the chapter name the builder accumulates
130
+ # declarations for
131
+ # @yield the block that mints a fresh builder, called only the first time
132
+ # `name` is asked for
133
+ # @yieldreturn [Bluebook::DSL::BluebookBuilder] a fresh builder for `name`
134
+ # @return [Bluebook::DSL::BluebookBuilder] the builder already open for
135
+ # `name`, or the block's freshly minted one on the first call
115
136
  def bluebook_builder(name)
116
137
  @bluebook_builders[name.to_s] ||= yield
117
138
  end
@@ -130,20 +151,53 @@ module Hecks
130
151
  # registry/saga_persistence.rb) there is no concurrent caller for
131
152
  # `Hecks/ThreadSharedIvarMutation` to actually be warning about here.
132
153
  # rubocop:disable Hecks/ThreadSharedIvarMutation
154
+ # Registers a loaded chapter, keyed by its own declared name.
155
+ #
156
+ # @param item [Bluebook::Chapter] the loaded, judged chapter
157
+ # @return [Bluebook::Chapter] `item`, unchanged
133
158
  def add_bluebook(item) = @bluebooks[item.name] = item
134
159
 
160
+ # PROVENANCE, SIDE-CHANNEL — which real `.bluebook` file(s)
161
+ # contributed to a chapter name, never part of the exported IR (a
162
+ # boot-time loading fact, not a domain fact) and never Rust-mirrored
163
+ # (the same "additive, Ruby-only" shape `@translations` above already
164
+ # is). Legitimate accumulation (several files declaring the SAME
165
+ # chapter name on purpose — `lib/hecks/language/bluebook/*.bluebook`
166
+ # all open `Hecks.bluebook "Bluebook"`) pushes more than one path
167
+ # here too; that alone is not a problem. What this exists to let
168
+ # `refuse_cross_package_bluebook_merge!` (registry/verification.rb)
169
+ # catch is TWO UNRELATED PACKAGES accumulating into the same name by
170
+ # coincidence — a stale vendored fork's own copy of a real gem's
171
+ # chapter, still reachable on the load path, silently merging its
172
+ # aggregates into the real one via this exact accumulation mechanism.
173
+ def record_bluebook_source(name, path)
174
+ (@bluebook_sources[name.to_s] ||= []) << path
175
+ end
176
+
135
177
  # Merged, not replaced — recovered, not new (see Runtime::Loader
136
178
  # .boot's own comment for the provenance). A domain's hecksagon can
137
179
  # now load in more than one block for the same domain (base file
138
180
  # plus an `environments/<name>.hecksagon` overlay), and the second
139
181
  # block should add to what the first declared, not silently
140
182
  # discard it.
183
+ #
184
+ # @param item [Bluebook::Hecksagon] the declared wiring to register
185
+ # @return [void]
141
186
  def add_hecksagon(item)
142
187
  existing = @hecksagons[item.domain]
143
188
  @hecksagons[item.domain] = existing ? merge_hecksagons(existing, item) : item
144
189
  end
145
190
 
191
+ # Registers a loaded port, keyed by its own declared name.
192
+ #
193
+ # @param item [Bluebook::Port, Bluebook::DomainPort] the loaded port
194
+ # @return [Bluebook::Port, Bluebook::DomainPort] `item`, unchanged
146
195
  def add_port(item) = @ports[item.name] = item
196
+
197
+ # Registers a loaded adapter, keyed by its own declared name.
198
+ #
199
+ # @param item [Bluebook::Adapter] the loaded, judged adapter
200
+ # @return [Bluebook::Adapter] `item`, unchanged
147
201
  def add_adapter(item) = @adapters[item.name] = item
148
202
 
149
203
  # Merged, not replaced — the same generalization for `World` that
@@ -156,12 +210,40 @@ module Hecks
156
210
  # bare verb key and the `"verb:adapter"` qualified key point at the
157
211
  # same resolved hash) — an overlay's key wins over the base's same
158
212
  # key; a key only the base declares survives untouched.
213
+ #
214
+ # @param item [Bluebook::World] the declared world settings to register
215
+ # @return [void]
159
216
  def add_world(item)
160
217
  existing = @worlds[item.domain]
161
218
  @worlds[item.domain] = existing ? merge_worlds(existing, item) : item
162
219
  end
163
220
 
221
+ # Registers a loaded translation.
222
+ #
223
+ # @param item [Bluebook::Translation] the loaded, judged translation
224
+ # @return [Array<Bluebook::Translation>] every translation registered so far,
225
+ # `item` last
164
226
  def add_translation(item) = @translations << item
227
+
228
+ # Declares one attribute of one domain's own aggregate sensitive — called from a
229
+ # terminal `has_<category>(readable_by:)` on a `Bluebook::DSL::AttributePath`
230
+ # (reached by chaining off a bare `Domain::Aggregate` inside a `.hecksagon` file
231
+ # being `Kernel.load`ed, or off an already-installed `AggregateDoor`), same timing
232
+ # (and same thread-safety argument, above) as `add_bluebook`/`add_port`. Recorded,
233
+ # not dispatched: `Runtime::Loader.boot`'s own `seed_privacy_markings!` turns each
234
+ # entry into a real `Privacy::Marking.Mark` once a dispatcher exists.
235
+ #
236
+ # @param domain [String] the marked attribute's own aggregate FQN, e.g.
237
+ # `"Lifeadelics::Registration"`
238
+ # @param attribute_path [String] the dotted path within that aggregate, e.g.
239
+ # `"attendee.medications"`
240
+ # @param category [String] the marking's own sensitivity category, e.g. `"phi"`
241
+ # @param readable_by [String] the Governance role a read must hold, unredacted
242
+ # @return [void]
243
+ def add_pending_privacy_marking(domain:, attribute_path:, category:, readable_by:)
244
+ @pending_privacy_markings << { domain: domain, attribute_path: attribute_path,
245
+ category: category, readable_by: readable_by }
246
+ end
165
247
  # rubocop:enable Hecks/ThreadSharedIvarMutation
166
248
 
167
249
  # {domain name => era ordinal} as resolved by the boot-time era
@@ -176,10 +258,30 @@ module Hecks
176
258
  # an old checkout only — see `initialize`'s own comment on it.
177
259
  attr_reader :superseded_eras
178
260
 
261
+ # Finds a loaded chapter by name.
262
+ #
263
+ # @param name [String, Symbol] the chapter's declared name
264
+ # @return [Bluebook::Chapter, nil] the chapter, or nil if none is registered
265
+ # under `name`
179
266
  def bluebook(name) = @bluebooks[name.to_s]
267
+
268
+ # Finds a domain's registered wiring by name.
269
+ #
270
+ # @param name [String, Symbol] the domain name
271
+ # @return [Bluebook::Hecksagon, nil] the domain's wiring, or nil if none is
272
+ # registered under `name`
180
273
  def hecksagon(name) = @hecksagons[name.to_s]
274
+
275
+ # Finds a domain's registered world settings by name.
276
+ #
277
+ # @param name [String, Symbol] the domain name
278
+ # @return [Bluebook::World, nil] the domain's world, or nil if none is
279
+ # registered under `name`
181
280
  def world(name) = @worlds[name.to_s]
182
281
 
282
+ # Every verb every loaded chapter declares, sorted.
283
+ #
284
+ # @return [Array<String>] every declared verb, across every loaded chapter
183
285
  def verbs = @bluebooks.values.flat_map(&:verbs).sort
184
286
 
185
287
  # The chapter that answers a role check for `domain` — the domain's
@@ -188,6 +290,10 @@ module Hecks
188
290
  # every check for the literal name "Governance": Governance is
189
291
  # recognised by what it declares, and a chapter that declares the
190
292
  # same thing is recognised the same way.
293
+ #
294
+ # @param domain [String, Symbol] the domain whose role checks are being resolved
295
+ # @return [Bluebook::Chapter, nil] the chapter that answers `domain`'s role
296
+ # checks, or nil if none does
191
297
  def authorization_provider_for(domain)
192
298
  names = [domain.to_s, *Array(hecksagon(domain)&.framework_members)]
193
299
  names.filter_map { |name| bluebook(name) }
@@ -195,10 +301,24 @@ module Hecks
195
301
  end
196
302
 
197
303
  # Every loaded chapter declaring `provides "authorization"`.
304
+ #
305
+ # @return [Array<Bluebook::Chapter>] every loaded chapter that provides
306
+ # authorization
198
307
  def authorization_providers
199
308
  @bluebooks.values.select { |chapter| chapter.provides?(Bluebook::Capabilities::AUTHORIZATION) }
200
309
  end
201
310
 
311
+ # Resolves and memoizes `aggregate`'s authoritative repository.
312
+ #
313
+ # @param domain [String, Symbol] name of the domain `aggregate` belongs to
314
+ # @param aggregate [Bluebook::Aggregate] the aggregate to resolve a repository for
315
+ # @return [Persistence::AppendOnly] repository over the aggregate's authoritative
316
+ # adapter, or over a `Memory` adapter when the domain declares no hecksagon
317
+ # @raise [Runtime::WiringError] if the aggregate has no authoritative bind, more than
318
+ # one, or a bind with a role this port does not support; or if the bound adapter is
319
+ # unknown, answers a different verb, is given a setting it does not declare, has no
320
+ # Ruby implementation, or lacks a method its port's `answers` list or the
321
+ # append-only contract (`append`, `project`, `entries`) requires
202
322
  def repository(domain, aggregate)
203
323
  @repositories[[domain.to_s, aggregate.hecks_name]] ||= Ports::Persistence.repository(self, domain, aggregate)
204
324
  end
@@ -216,8 +336,8 @@ module Hecks
216
336
  # that store again, the way `Loader.boot_files` does after
217
337
  # `verify!`.
218
338
  #
219
- # What this is for: a test runner that used to boot a runtime per
220
- # test to get isolation (`Behaviors::Expectations.run_one`) — ~2s a
339
+ # What this is for: a test runner that would otherwise boot a runtime
340
+ # per test to get isolation (`Behaviors::Expectations.run_one`) — ~2s a
221
341
  # boot, 76 chess behaviours = two and a half minutes of booting the
222
342
  # same two files — can now boot once and reset between tests.
223
343
  #
@@ -229,6 +349,8 @@ module Hecks
229
349
  # single-threaded example loop. No production dispatch path calls
230
350
  # this at all — a live Puma worker pool never resets a registry out
231
351
  # from under itself mid-flight.
352
+ #
353
+ # @return [Runtime::Registry] self
232
354
  # rubocop:disable-next Hecks/ThreadSharedIvarMutation
233
355
  def reset_runtime_state!
234
356
  @event_log.clear
@@ -250,6 +372,17 @@ module Hecks
250
372
  # gives, just without a lazy `||=` race on standing it up.
251
373
  attr_reader :capability_graph
252
374
 
375
+ # Resolves and memoizes the repository to read `aggregate` from — a caught-up
376
+ # projection when one is bound and current, otherwise the authoritative repository.
377
+ #
378
+ # @param domain [String, Symbol] name of the domain `aggregate` belongs to
379
+ # @param aggregate [Bluebook::Aggregate] the aggregate to resolve a read
380
+ # repository for
381
+ # @return [Persistence::AppendOnly] the projection repository when one is bound
382
+ # and caught up with the authoritative store; the authoritative repository
383
+ # otherwise
384
+ # @raise [Runtime::WiringError] if the authoritative or projection bind cannot
385
+ # be resolved
253
386
  def read_repository(domain, aggregate)
254
387
  key = [domain.to_s, aggregate.hecks_name]
255
388
  binding = Ports::Projection.binds_for(self, domain, aggregate).first
@@ -264,6 +397,15 @@ module Hecks
264
397
  projection_current?(projection, authoritative) ? projection : authoritative
265
398
  end
266
399
 
400
+ # Reports whether `projection`'s own journal entries and rows agree with
401
+ # `authoritative`'s, entry-for-entry.
402
+ #
403
+ # @param projection [Persistence::AppendOnly] the projection repository to check
404
+ # @param authoritative [Persistence::AppendOnly] the authoritative repository to
405
+ # check `projection` against
406
+ # @return [Boolean] true when `projection` holds the same entries and rows as
407
+ # `authoritative`, in the same order; false on any mismatch, or if comparing
408
+ # them raises
267
409
  def projection_current?(projection, authoritative)
268
410
  projected_entries = projection.entries
269
411
  source_entries = authoritative.entries
@@ -288,6 +430,12 @@ module Hecks
288
430
  # authoritative bind" check is what actually catches a genuine
289
431
  # double-bind; this merge only concatenates, it does not itself
290
432
  # decide which of two binds for the same aggregate wins.
433
+ #
434
+ # @param base [Bluebook::Hecksagon] the domain's already-registered wiring
435
+ # @param overlay [Bluebook::Hecksagon] the newly loaded block's own wiring to
436
+ # fold in
437
+ # @return [Bluebook::Hecksagon] a new wiring with every list-shaped fact
438
+ # concatenated, `base` then `overlay`
291
439
  def merge_hecksagons(base, overlay)
292
440
  Bluebook::Hecksagon.new(
293
441
  domain: base.domain,
@@ -305,6 +453,11 @@ module Hecks
305
453
  # also declares replaces that key's whole resolved hash (the same
306
454
  # all-or-nothing shape `WorldBuilder#method_missing` already builds
307
455
  # each entry as), it does not deep-merge field by field within it.
456
+ #
457
+ # @param base [Bluebook::World] the domain's already-registered world
458
+ # @param overlay [Bluebook::World] the newly loaded block's own world to fold in
459
+ # @return [Bluebook::World] a new world with `overlay`'s scalars winning when
460
+ # present, and `settings` shallow-merged, `overlay`'s keys winning
308
461
  def merge_worlds(base, overlay)
309
462
  Bluebook::World.new(
310
463
  domain: base.domain,
@@ -16,7 +16,7 @@ module Hecks
16
16
  # closely enough that everything built on top of a dispatcher —
17
17
  # `Handle`, `AggregateDoor`, `Facade::Surface` — needs no changes
18
18
  # at all: `Handle#run`'s entire contract is
19
- # `@dispatcher.dispatch("#{fqn}.#{command}", **identity, **args).instance.state`,
19
+ # `@dispatcher.dispatch("#{fqn}.#{command}", to: @id, with: args).instance.state`,
20
20
  # and both classes answer that identically.
21
21
  #
22
22
  # **Reads delegate, writes don't**. `query`/`reference_query` hand off
@@ -31,12 +31,27 @@ module Hecks
31
31
  # against incomplete local state and then merely persisted.
32
32
  class RemoteDispatcher
33
33
  Result = Struct.new(:verb, :instance, :events, keyword_init: true) do
34
+ # Reads the identity of the record the dispatch settled on.
35
+ #
36
+ # @return [String] the settled record's identity
34
37
  def id = instance.id
38
+
39
+ # Reads the settled record's attributes as one Hash.
40
+ #
41
+ # @return [Hash{Symbol => Object}] the settled record's attributes, `:id` merged
42
+ # in last
35
43
  def state = instance.to_h
36
44
  end
37
45
 
38
46
  attr_reader :registry
39
47
 
48
+ # @param registry [Runtime::Registry] the booted registry this dispatcher fronts;
49
+ # read-side calls (`query`/`reference_query`) delegate to a local `Dispatcher`
50
+ # built over the same registry
51
+ # @param region [String] the AWS region the routed Lambda function lives in
52
+ # @param function [String, nil] the `.world`'s own `dispatched_by("Lambda")`
53
+ # function name, when the deployment stack is not named `hecks-<domain>`; nil
54
+ # resolves the function name from `ENV["DOMAIN_NAME"]` or `registry.root`
40
55
  def initialize(registry, region: "us-east-1", function: nil)
41
56
  @registry = registry
42
57
  # `File.basename(registry.root)`, not `bluebooks.keys.first` —
@@ -62,14 +77,55 @@ module Hecks
62
77
  @local = Dispatcher.new(registry)
63
78
  end
64
79
 
65
- # Same deprecation as `Dispatcher#dispatch` — loose keyword facts warn;
66
- # `to:`/`with:` do not.
80
+ # Dispatches a command by verb, routing to the local `Dispatcher` or the
81
+ # remote Lambda depending on the aggregate's bound adapter.
82
+ #
83
+ # Same shape as `Dispatcher#dispatch_flat` — everything but
84
+ # `saga_correlation:` is forwarded through unread, `to:`/`with:`
85
+ # included, and lifted out downstream by whichever path actually
86
+ # dispatches (`@local.dispatch_flat` locally, the flat wire form
87
+ # remotely). Not the strict `to:`/`with:`-only door `Dispatcher#
88
+ # dispatch` is — see that class's own comment for why this file
89
+ # never had one.
90
+ #
91
+ # @param verb [String] the fully qualified verb, `"Domain::Aggregate.Command"`
92
+ # or `"Domain::Aggregate.Entity.Command"`
93
+ # @param saga_correlation [Hash, nil] correlation head => value, stamped on every
94
+ # emitted event when a saga leg causes this dispatch; nil otherwise
95
+ # @param args [Hash] the facts, plus optional `:to`/`:with` keys, read the same
96
+ # way `dispatch_flat` reads them
97
+ # @return [RemoteDispatcher::Result] the verb, settled instance and emitted events
98
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
99
+ # domain or aggregate that is not declared
100
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
101
+ # locally-routed aggregate refuses the call
102
+ # @raise [Runtime::StaleWrite] if concurrent local writers beat this one through
103
+ # every retry, for a locally-routed aggregate
104
+ # @raise [Runtime::WiringError] if the aggregate's adapter or repository cannot be
105
+ # resolved, or the remote call is accepted but reports no mutation for it
106
+ # @raise [Runtime::RemoteRefusal] if the routed Lambda refuses the call
67
107
  def dispatch(verb, saga_correlation: nil, **args)
68
- Dispatcher.deprecate_loose_facts(args.except(:to, :with))
69
108
  dispatch_flat(verb, args.merge(saga_correlation: saga_correlation))
70
109
  end
71
110
 
72
- # Same flat-facts wire form as `Dispatcher#dispatch_flat`.
111
+ # Routes a dispatch to the local `Dispatcher` when the aggregate's bound
112
+ # adapter is not remote-backed (`Ports::Persistence::RemoteRuntime`),
113
+ # otherwise dispatches through the routed Lambda. Same flat-facts wire
114
+ # form as `Dispatcher#dispatch_flat`.
115
+ #
116
+ # @param verb [String] the fully qualified verb, in any shape `dispatch` accepts
117
+ # @param args [Hash] the facts, plus optional Symbol keys `:to`, `:with` and
118
+ # `:saga_correlation`, read as `dispatch`'s keywords of the same names; not mutated
119
+ # @return [RemoteDispatcher::Result] the verb, settled instance and emitted events
120
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
121
+ # domain or aggregate that is not declared
122
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when a
123
+ # locally-routed aggregate refuses the call
124
+ # @raise [Runtime::StaleWrite] if concurrent local writers beat this one through
125
+ # every retry, for a locally-routed aggregate
126
+ # @raise [Runtime::WiringError] if the aggregate's adapter or repository cannot be
127
+ # resolved, or the remote call is accepted but reports no mutation for it
128
+ # @raise [Runtime::RemoteRefusal] if the routed Lambda refuses the call
73
129
  def dispatch_flat(verb, args = {})
74
130
  args = args.dup
75
131
  saga_correlation = args.delete(:saga_correlation)
@@ -125,16 +181,46 @@ module Hecks
125
181
  Result.new(verb: verb, instance: instance, events: step_events(response))
126
182
  end
127
183
 
184
+ # Delegates to the local `Dispatcher` built over the same registry — see the
185
+ # class comment on why reads, unlike writes, need no remote-specific logic.
186
+ #
187
+ # @param verb [String, Symbol] the query's verb, in one of `Dispatcher#query`'s
188
+ # three shapes
189
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
190
+ # @return [Array<Hash>] see `Dispatcher#query`'s own return
191
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
192
+ # domain, aggregate, entity, query or read model that is not declared
193
+ # @raise [Runtime::NotFound] if a read model's root reference names no record
194
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
195
+ # @raise [KeyError] if a rooted read model is asked without its reference argument
196
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
128
197
  def query(verb, **args) = @local.query(verb, **args)
198
+
199
+ # Delegates to the local `Dispatcher` built over the same registry — see the
200
+ # class comment on why reads, unlike writes, need no remote-specific logic.
201
+ #
202
+ # @param verb [String] the fully qualified query verb, `"Domain::Aggregate.Query"`
203
+ # or `"Domain::Aggregate.Entity.Query"`
204
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
205
+ # @return [Array<Hash>] one Hash per matching record, its state with `:id` merged
206
+ # in last; for an entity query, one Hash per matching element
207
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a
208
+ # domain, aggregate, entity or query that is not declared
209
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
210
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
129
211
  def reference_query(verb, **args) = @local.reference_query(verb, **args)
130
212
 
131
- # The full domain's event history, on every call — `{"read":
213
+ # Fetches the full domain's event history, on every call — `{"read":
132
214
  # true}` replays the whole journal (Phase 1's `dispatch::read`),
133
215
  # so its own `events` array already is the complete log, the
134
216
  # same thing `@registry.event_log` would answer for a local
135
217
  # dispatch. Not cached: `AggregateDoor.events`/`Handle#events`
136
218
  # are not called in this codebase's own hot paths today: if that
137
219
  # changes, caching belongs here, not in every caller.
220
+ #
221
+ # @return [Array<Runtime::Event>] every event in the routed Lambda's domain
222
+ # journal, oldest first, with `occurred_at` always nil (the kernel is
223
+ # timestamp-free by design)
138
224
  def events
139
225
  @client.read.fetch("events", []).map { |e| build_event(e) }
140
226
  end
@@ -10,6 +10,9 @@ module Hecks
10
10
  # naming them; the dispatcher itself builds an Invocation instead.
11
11
  module Routing
12
12
  Envelope = Struct.new(:aggregate, :entities, keyword_init: true) do
13
+ # @param aggregate [String, #to_s] the receiving aggregate's identity
14
+ # @param entities [Array<String, #to_s>, nil] the entity-hop identities after the
15
+ # aggregate, in call order; empty for an aggregate-level command
13
16
  def initialize(aggregate:, entities: [])
14
17
  super(aggregate: aggregate.to_s, entities: Array(entities).map(&:to_s).freeze)
15
18
  freeze
@@ -18,12 +21,34 @@ module Hecks
18
21
 
19
22
  module_function
20
23
 
24
+ # Resolves a `to:` argument into a routing envelope. A thin delegator to
25
+ # `Invocation.route`; the dispatcher itself builds an `Invocation` instead.
26
+ #
27
+ # @param to [String, Hash, nil] a bare aggregate identity, or a Hash with
28
+ # `aggregate:` and `entity:`/`entities:`; nil for no receiver
29
+ # @param entity_depth [Integer] the number of entity-hop identities the call expects
30
+ # @return [Routing::Envelope, nil] the resolved envelope, or nil when `to` is nil
31
+ # @raise [Runtime::TypeMismatch] if `to` is malformed or its entity count does not
32
+ # match `entity_depth`
21
33
  def envelope(to, entity_depth: 0) = Invocation.route(to, entity_depth: entity_depth)
22
34
 
35
+ # Resolves a command's offered facts into its flat args hash. A thin
36
+ # delegator to `Invocation.facts_for` for any caller still naming it.
37
+ #
23
38
  # Loaded by invocation.rb (which requires this file), so `Invocation`
24
39
  # is always defined by the time either method runs.
25
- def payload(command, with:, legacy:)
26
- facts = Invocation.facts_for(command, with: with, legacy: legacy)
40
+ #
41
+ # @param command [Class] a `Bluebook::Command` subclass (or port-operation class)
42
+ # responding to `hecks_name` and `attributes`
43
+ # @param with [Hash, nil] the command's facts, keyed by attribute name; nil when the
44
+ # caller offers `flat` instead
45
+ # @param flat [Hash] the command's facts as a flat args hash, used when `with` is nil
46
+ # @return [Hash{String, Symbol => Object}] the offered facts by attribute name; an
47
+ # attribute offered as nil is kept as nil
48
+ # @raise [Runtime::TypeMismatch] if both `with` and a non-empty `flat` are given, if
49
+ # `with` is not a Hash, or if `with` names an unknown or omits a required attribute
50
+ def payload(command, with:, flat:)
51
+ facts = Invocation.facts_for(command, with: with, flat: flat)
27
52
  Invocation.new(verb: nil, target: nil, facts: facts).to_args
28
53
  end
29
54
  end
@@ -50,18 +50,16 @@ module Hecks
50
50
 
51
51
  # A self-referencing leg carries the correlation forward under its
52
52
  # own emitting record's identity — `event.id`, not a field dug back
53
- # out of the payload. This used to read `event.payload[own_key]`
54
- # (`own_key` the aggregate's own reference-key convention, "wire",
55
- # "transfer"), which only ever held a value because legacy dispatch
56
- # left the self-addressing key riding along in the payload
57
- # unfiltered. Routing separated from payload (`to:`/`with:`, the
58
- # facade's own `Handle#run` always uses it) closed exactly that
59
- # leak — correctly, since an addressing key is not a fact the
60
- # payload should carry — which left this tier reading an empty
61
- # Hash for any self-referencing leg with no other declared
62
- # attributes (`OnboardingCase.Clear`, `.Decline` — no `attribute`
63
- # lines at all): the saga silently stopped advancing, forever, for
64
- # exactly the leg this tier exists to correlate.
53
+ # out of the payload, because an addressing key is not a fact the
54
+ # payload should carry: routing is kept separate from payload
55
+ # (`to:`/`with:`, the facade's own `Handle#run` always uses it), so
56
+ # `own_key` (the aggregate's own reference-key convention, "wire",
57
+ # "transfer") never rides along in the payload unfiltered. Digging
58
+ # for it there would read an empty Hash for any self-referencing
59
+ # leg with no other declared attributes (`OnboardingCase.Clear`,
60
+ # `.Decline` — no `attribute` lines at all), silently stopping the
61
+ # saga from advancing, forever, for exactly the leg this tier
62
+ # exists to correlate.
65
63
  #
66
64
  # `event.id` says the identical thing this tier always meant —
67
65
  # "the record that just emitted this event, by its own identity" —
@@ -28,14 +28,29 @@ module Hecks
28
28
 
29
29
  attr_reader :registry
30
30
 
31
+ # @param registry [Runtime::Registry] the booted registry whose declared
32
+ # process managers and saga persistence this interpreter runs against
33
+ # @param door [Runtime::Dispatcher] the dispatcher a saga leg's own dispatch
34
+ # re-enters through
31
35
  def initialize(registry, door:)
32
36
  @registry = registry
33
37
  @door = door
34
38
  end
35
39
 
40
+ # Runs `domain`'s declared process managers against `event`: begins,
41
+ # advances or ends each matching saga instance, checkpointing durably
42
+ # before its own dispatches run.
43
+ #
36
44
  # `only:` — one process manager, the outbox relay's way of running
37
45
  # exactly the consumer a row names (`Runtime::Outbox::Relay#
38
46
  # run_consumer`); nil advances every manager the domain declares.
47
+ #
48
+ # @param event [Runtime::Event] the just-emitted event to react to
49
+ # @param domain [String, Symbol] the domain whose declared process managers
50
+ # are checked
51
+ # @param only [Bluebook::ProcessManager, nil] one process manager to run
52
+ # exactly, instead of every manager `domain` declares
53
+ # @return [void]
39
54
  def advance(event, domain, only: nil)
40
55
  bluebook = @registry.bluebook(domain)
41
56
  return unless bluebook
@@ -56,12 +71,11 @@ module Hecks
56
71
  # could otherwise interleave their writes out of order, silently
57
72
  # reordering a saga's own transition history — worse for the
58
73
  # adapters with no locking of their own (Heki) than for Postgres.
59
- # `deep_copy` guards against the exact shape of bug PR #175 itself
60
- # already found once (over-freezing a live, still-mutated Hash) —
61
- # never hand a persistence adapter the same object `advance_saga`/
62
- # `unwind` go on to mutate in place; round-tripping through JSON
63
- # is also what guarantees the value is safe for every adapter that
64
- # itself calls `JSON.generate` on it.
74
+ # `deep_copy` guards against a real bug shape (over-freezing a live,
75
+ # still-mutated Hash) — never hand a persistence adapter the same
76
+ # object `advance_saga`/`unwind` go on to mutate in place;
77
+ # round-tripping through JSON is also what guarantees the value is
78
+ # safe for every adapter that itself calls `JSON.generate` on it.
65
79
  # `pending:` — see saga_pending_dispatch.rb. Injected into the
66
80
  # written copy of memory only, never into `instance[:memory]`
67
81
  # itself: every other reader of a live instance's memory
@@ -423,7 +437,7 @@ module Hecks
423
437
  # Same non-reentrancy reasoning as `advance_saga`'s own comment —
424
438
  # the mutex covers only the check-and-mutate-and-checkpoint step.
425
439
  advanced = @registry.saga_mutex.synchronize do
426
- # The compensating leg is selected by (REFUSED, current state)
440
+ # The compensating leg is selected by (`REFUSED`, current state)
427
441
  # too — C10.3, one rule for every leg.
428
442
  handler = process_manager.handler_for(REFUSED, instance[:state])
429
443
  unless handler
@@ -507,9 +521,9 @@ module Hecks
507
521
  end
508
522
 
509
523
  # BUG#6 — unconditionally the saga's own home domain, never inferred
510
- # from `command_name`'s own shape. This used to guess: a leftover
511
- # `::` after `Naming.command_ref`'s own rewrite was read as "already
512
- # domain-qualified" and left alone. That heuristic cannot actually
524
+ # from `command_name`'s own shape. Guessing from a leftover
525
+ # `::` after `Naming.command_ref`'s own rewrite (read as "already
526
+ # domain-qualified" and left alone) cannot actually
513
527
  # tell a genuinely cross-domain reference (`Banking::Account::
514
528
  # Debit` -> one `::` survives) apart from a same-domain entity
515
529
  # command reference (`Manifest::Slot::Fill` -> one `::` survives
@@ -519,11 +533,11 @@ module Hecks
519
533
  # (confirmed against `Naming.command_ref`'s own rewrite: it only
520
534
  # ever strips the last `::`, so the count of what remains is blind
521
535
  # to why it's there). Picking the cross-domain reading unconditionally
522
- # left `qa/stress_domains/waybill`'s own `Packing` saga dispatching
536
+ # would leave `qa/stress_domains/waybill`'s own `Packing` saga dispatching
523
537
  # `Manifest::Slot::Fill` — an entity command in its own domain —
524
- # unprefixed, so `Naming.split_verb` read "Manifest" as a domain
538
+ # unprefixed, so `Naming.split_verb` would read "Manifest" as a domain
525
539
  # name instead of this chapter's own aggregate, and the dispatch
526
- # failed with `UnknownVerb`, silently recorded as an ordinary
540
+ # would fail with `UnknownVerb`, silently recorded as an ordinary
527
541
  # domain refusal rather than surfacing as the real bug it is.
528
542
  #
529
543
  # The fix mirrors `PolicyInterpreter#deliver`'s own mechanism,