hecks 1.3.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 (373) hide show
  1. checksums.yaml +4 -4
  2. data/lib/hecks/adapters/driven/claude_code.rb +72 -7
  3. data/lib/hecks/adapters/driven/d1.rb +187 -23
  4. data/lib/hecks/adapters/driven/folder.rb +83 -10
  5. data/lib/hecks/adapters/driven/google_authentication.rb +33 -12
  6. data/lib/hecks/adapters/driven/governance_authorization.rb +87 -18
  7. data/lib/hecks/adapters/driven/heki/journal.rb +13 -4
  8. data/lib/hecks/adapters/driven/heki/saga_store.rb +56 -10
  9. data/lib/hecks/adapters/driven/heki/snapshot.rb +1 -1
  10. data/lib/hecks/adapters/driven/heki.rb +106 -9
  11. data/lib/hecks/adapters/driven/identity_registry.rb +12 -2
  12. data/lib/hecks/adapters/driven/in_memory_ordering.rb +25 -3
  13. data/lib/hecks/adapters/driven/in_process_key_vault.adapter +3 -0
  14. data/lib/hecks/adapters/driven/in_process_key_vault.rb +53 -0
  15. data/lib/hecks/adapters/driven/lambda/client.rb +67 -14
  16. data/lib/hecks/adapters/driven/lambda.rb +82 -35
  17. data/lib/hecks/adapters/driven/local_storage.rb +83 -10
  18. data/lib/hecks/adapters/driven/memory.rb +205 -9
  19. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +21 -1
  20. data/lib/hecks/adapters/driven/postgres/codec.rb +27 -11
  21. data/lib/hecks/adapters/driven/postgres/outbox.rb +40 -2
  22. data/lib/hecks/adapters/driven/postgres/reconnect.rb +23 -7
  23. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +14 -14
  24. data/lib/hecks/adapters/driven/postgres.rb +175 -28
  25. data/lib/hecks/adapters/driven/prism.rb +50 -11
  26. data/lib/hecks/adapters/driven/secure_random_identity.rb +3 -0
  27. data/lib/hecks/adapters/driven/sql_query_builder.rb +34 -22
  28. data/lib/hecks/adapters/driven/sqlite/codec.rb +38 -10
  29. data/lib/hecks/adapters/driven/sqlite/projection.rb +60 -32
  30. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +12 -12
  31. data/lib/hecks/adapters/driven/sqlite.rb +181 -21
  32. data/lib/hecks/adapters/driven/system_clock.rb +3 -0
  33. data/lib/hecks/adapters/driven/tenant_provisioner.adapter +3 -0
  34. data/lib/hecks/adapters/driven/tenant_provisioner.rb +66 -0
  35. data/lib/hecks/adapters/driven.rb +6 -4
  36. data/lib/hecks/adapters/driving/github_webhook.rb +31 -18
  37. data/lib/hecks/behaviors/dsl.rb +60 -2
  38. data/lib/hecks/behaviors/expectations.rb +190 -29
  39. data/lib/hecks/behaviors/ir.rb +12 -1
  40. data/lib/hecks/behaviors/rspec.rb +9 -1
  41. data/lib/hecks/behaviors/runner.rb +21 -2
  42. data/lib/hecks/behaviors.rb +9 -1
  43. data/lib/hecks/bluebook/aggregate.rb +43 -13
  44. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +17 -10
  45. data/lib/hecks/bluebook/assembly/build.rb +26 -5
  46. data/lib/hecks/bluebook/assembly/contract.rb +98 -23
  47. data/lib/hecks/bluebook/assembly/contracts.rb +59 -52
  48. data/lib/hecks/bluebook/assembly/marks.rb +159 -30
  49. data/lib/hecks/bluebook/assembly/specializer.rb +38 -21
  50. data/lib/hecks/bluebook/assembly.rb +32 -14
  51. data/lib/hecks/bluebook/attribute.rb +26 -12
  52. data/lib/hecks/bluebook/behaviour/aggregate.rb +43 -11
  53. data/lib/hecks/bluebook/behaviour/attribute.rb +18 -5
  54. data/lib/hecks/bluebook/behaviour/chapter.rb +76 -5
  55. data/lib/hecks/bluebook/behaviour/command.rb +55 -25
  56. data/lib/hecks/bluebook/behaviour/domain_port.rb +27 -7
  57. data/lib/hecks/bluebook/behaviour/entity.rb +20 -8
  58. data/lib/hecks/bluebook/behaviour/hexagon.rb +30 -4
  59. data/lib/hecks/bluebook/behaviour/lifecycle.rb +27 -6
  60. data/lib/hecks/bluebook/behaviour/policy.rb +42 -17
  61. data/lib/hecks/bluebook/behaviour/process_manager.rb +39 -8
  62. data/lib/hecks/bluebook/behaviour/query.rb +6 -1
  63. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -8
  64. data/lib/hecks/bluebook/behaviour/traits.rb +48 -12
  65. data/lib/hecks/bluebook/behaviour/value_object.rb +21 -9
  66. data/lib/hecks/bluebook/capabilities.rb +27 -0
  67. data/lib/hecks/bluebook/chapter.rb +51 -9
  68. data/lib/hecks/bluebook/command.rb +62 -17
  69. data/lib/hecks/bluebook/domain_port.rb +34 -9
  70. data/lib/hecks/bluebook/dsl/adapter_builder.rb +24 -0
  71. data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +49 -49
  72. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +282 -123
  73. data/lib/hecks/bluebook/dsl/attribute_collector.rb +112 -75
  74. data/lib/hecks/bluebook/dsl/binding_proxy.rb +81 -2
  75. data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +486 -117
  76. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +179 -47
  77. data/lib/hecks/bluebook/dsl/bootstrap_table.rb +116 -0
  78. data/lib/hecks/bluebook/dsl/command_builder.rb +284 -122
  79. data/lib/hecks/bluebook/dsl/const_shim.rb +46 -15
  80. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +90 -25
  81. data/lib/hecks/bluebook/dsl/entity_builder.rb +191 -61
  82. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +148 -132
  83. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +130 -30
  84. data/lib/hecks/bluebook/dsl/identity_declaration.rb +38 -21
  85. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +27 -4
  86. data/lib/hecks/bluebook/dsl/policy_builder.rb +86 -36
  87. data/lib/hecks/bluebook/dsl/port_builder.rb +38 -7
  88. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +56 -22
  89. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +111 -47
  90. data/lib/hecks/bluebook/dsl/query_builder.rb +37 -8
  91. data/lib/hecks/bluebook/dsl/read_model_builder.rb +127 -52
  92. data/lib/hecks/bluebook/dsl/rule_reference.rb +97 -43
  93. data/lib/hecks/bluebook/dsl/translation_builder.rb +150 -44
  94. data/lib/hecks/bluebook/dsl/value_object_builder.rb +68 -20
  95. data/lib/hecks/bluebook/dsl/word_gate.rb +59 -53
  96. data/lib/hecks/bluebook/dsl/world_builder.rb +51 -8
  97. data/lib/hecks/bluebook/entity.rb +40 -11
  98. data/lib/hecks/bluebook/expression/ast_json.rb +128 -36
  99. data/lib/hecks/bluebook/expression/ast_reader.rb +32 -3
  100. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -16
  101. data/lib/hecks/bluebook/expression/evaluator.rb +221 -43
  102. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +54 -18
  103. data/lib/hecks/bluebook/expression/resolver.rb +369 -128
  104. data/lib/hecks/bluebook/hexagon.rb +35 -1
  105. data/lib/hecks/bluebook/lifecycle.rb +12 -1
  106. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +2 -1
  107. data/lib/hecks/bluebook/meta_validator/judge.rb +126 -108
  108. data/lib/hecks/bluebook/meta_validator/plan.rb +81 -46
  109. data/lib/hecks/bluebook/meta_validator/port_judge.rb +3 -2
  110. data/lib/hecks/bluebook/meta_validator/readings.rb +200 -50
  111. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +68 -41
  112. data/lib/hecks/bluebook/meta_validator/shapes.rb +166 -21
  113. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +286 -46
  114. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +11 -10
  115. data/lib/hecks/bluebook/meta_validator/world_judge.rb +6 -5
  116. data/lib/hecks/bluebook/meta_validator.rb +235 -139
  117. data/lib/hecks/bluebook/model_check.rb +434 -104
  118. data/lib/hecks/bluebook/pattern_subset.rb +32 -10
  119. data/lib/hecks/bluebook/policy.rb +15 -13
  120. data/lib/hecks/bluebook/process_manager.rb +27 -14
  121. data/lib/hecks/bluebook/project_discovery.rb +5 -0
  122. data/lib/hecks/bluebook/project_loader.rb +40 -0
  123. data/lib/hecks/bluebook/project_register.rb +50 -6
  124. data/lib/hecks/bluebook/query.rb +31 -4
  125. data/lib/hecks/bluebook/read_model.rb +35 -15
  126. data/lib/hecks/bluebook/reference.rb +26 -13
  127. data/lib/hecks/bluebook/smoke_test.rb +46 -23
  128. data/lib/hecks/bluebook/synthesizer.rb +46 -12
  129. data/lib/hecks/bluebook/translation.rb +34 -5
  130. data/lib/hecks/bluebook/value_object.rb +29 -11
  131. data/lib/hecks/bluebook.rb +5 -6
  132. data/lib/hecks/codemod.rb +138 -50
  133. data/lib/hecks/construct.rb +21 -7
  134. data/lib/hecks/corpus.rb +438 -0
  135. data/lib/hecks/deploy/bluebook/deploy.hecksagon +19 -0
  136. data/lib/hecks/doc/reference.rb +200 -31
  137. data/lib/hecks/embryonaut_bluebook.rb +38 -15
  138. data/lib/hecks/facade/cli_door.rb +69 -10
  139. data/lib/hecks/facade/cli_runner.rb +105 -24
  140. data/lib/hecks/facade/command_request.rb +23 -0
  141. data/lib/hecks/facade/handle.rb +155 -35
  142. data/lib/hecks/facade/json_door.rb +106 -25
  143. data/lib/hecks/facade/surface/aggregate_door.rb +50 -27
  144. data/lib/hecks/facade/surface/chapter.rb +26 -17
  145. data/lib/hecks/facade/surface.rb +16 -3
  146. data/lib/hecks/facade.rb +15 -4
  147. data/lib/hecks/forms/app.rb +46 -30
  148. data/lib/hecks/forms/command_form_renderer.rb +70 -9
  149. data/lib/hecks/forms/field_renderer.rb +142 -6
  150. data/lib/hecks/forms/field_shape.rb +183 -19
  151. data/lib/hecks/forms/html.rb +51 -7
  152. data/lib/hecks/forms/index_renderer.rb +14 -2
  153. data/lib/hecks/forms/page.rb +14 -0
  154. data/lib/hecks/forms/params.rb +120 -23
  155. data/lib/hecks/forms/port_argument.rb +14 -2
  156. data/lib/hecks/forms/query_form_renderer.rb +65 -2
  157. data/lib/hecks/forms/record_renderer.rb +60 -2
  158. data/lib/hecks/forms/record_table.rb +28 -1
  159. data/lib/hecks/forms/reference_options.rb +24 -0
  160. data/lib/hecks/forms/value_object_shape.rb +13 -3
  161. data/lib/hecks/forms.rb +24 -4
  162. data/lib/hecks/fqn.rb +59 -1
  163. data/lib/hecks/framework/bluebook/governance.bluebook +9 -0
  164. data/lib/hecks/framework/bluebook/privacy.bluebook +155 -0
  165. data/lib/hecks/framework/oidc.json +15 -0
  166. data/lib/hecks/framework.rb +79 -25
  167. data/lib/hecks/freezer.rb +27 -11
  168. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +241 -106
  169. data/lib/hecks/fuzzing/combination_miner.rb +178 -0
  170. data/lib/hecks/fuzzing/concurrent_dispatch.rb +241 -45
  171. data/lib/hecks/fuzzing/coverage_campaign.rb +161 -0
  172. data/lib/hecks/fuzzing/differential.rb +192 -0
  173. data/lib/hecks/fuzzing/domain_generator.rb +871 -0
  174. data/lib/hecks/fuzzing/era_boundary.rb +68 -19
  175. data/lib/hecks/fuzzing/form_census.rb +121 -20
  176. data/lib/hecks/fuzzing/generated_domain_check.rb +171 -0
  177. data/lib/hecks/fuzzing/invalid_value_generator.rb +45 -6
  178. data/lib/hecks/fuzzing/isolated_boot.rb +137 -80
  179. data/lib/hecks/fuzzing/nondeterministic.rb +79 -0
  180. data/lib/hecks/fuzzing/persistence_parity.rb +111 -21
  181. data/lib/hecks/fuzzing/properties/corrections.rb +34 -9
  182. data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +300 -51
  183. data/lib/hecks/fuzzing/properties/guards.rb +86 -42
  184. data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +80 -32
  185. data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +40 -35
  186. data/lib/hecks/fuzzing/properties/outbox.rb +70 -32
  187. data/lib/hecks/fuzzing/properties/querying.rb +82 -28
  188. data/lib/hecks/fuzzing/properties.rb +84 -51
  189. data/lib/hecks/fuzzing/qa_settings.rb +164 -0
  190. data/lib/hecks/fuzzing/replay.rb +241 -133
  191. data/lib/hecks/fuzzing/rotation_priority.rb +48 -28
  192. data/lib/hecks/fuzzing/rust_gap_manifest.rb +139 -0
  193. data/lib/hecks/fuzzing/self_consistency.rb +315 -137
  194. data/lib/hecks/fuzzing/sequence_generator/adversary.rb +46 -40
  195. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +18 -11
  196. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +13 -12
  197. data/lib/hecks/fuzzing/sequence_generator/picker.rb +21 -12
  198. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +59 -32
  199. data/lib/hecks/fuzzing/sequence_generator.rb +158 -23
  200. data/lib/hecks/fuzzing/shrinker.rb +309 -0
  201. data/lib/hecks/fuzzing/structural_skips.rb +37 -130
  202. data/lib/hecks/fuzzing/sweep_depth.rb +12 -4
  203. data/lib/hecks/fuzzing/target_capabilities.rb +107 -29
  204. data/lib/hecks/fuzzing/value_generator.rb +110 -22
  205. data/lib/hecks/fuzzing.rb +5 -0
  206. data/lib/hecks/grammar/evolve.rb +188 -12
  207. data/lib/hecks/grammar.rb +53 -7
  208. data/lib/hecks/ir.rb +51 -20
  209. data/lib/hecks/language/bluebook/bluebook.bluebook +41 -0
  210. data/lib/hecks/language/bluebook/policy.bluebook +11 -1
  211. data/lib/hecks/language/bluebook/vocabulary.bluebook +348 -13
  212. data/lib/hecks/language/hecksagon/hecksagon.bluebook +11 -0
  213. data/lib/hecks/language/oidc.json +5 -0
  214. data/lib/hecks/literal.rb +41 -9
  215. data/lib/hecks/naming.rb +112 -31
  216. data/lib/hecks/ports/access_control.rb +53 -2
  217. data/lib/hecks/ports/agent/answers.rb +83 -6
  218. data/lib/hecks/ports/agent.rb +119 -35
  219. data/lib/hecks/ports/authentication.rb +44 -4
  220. data/lib/hecks/ports/authorization.rb +50 -11
  221. data/lib/hecks/ports/clock.rb +42 -23
  222. data/lib/hecks/ports/extraction.rb +16 -0
  223. data/lib/hecks/ports/identity_assignment.rb +23 -2
  224. data/lib/hecks/ports/identity_generation.rb +17 -3
  225. data/lib/hecks/ports/identity_resolution.rb +17 -1
  226. data/lib/hecks/ports/key_vault.port +6 -0
  227. data/lib/hecks/ports/key_vault.rb +58 -0
  228. data/lib/hecks/ports/loading.rb +4 -0
  229. data/lib/hecks/ports/persistence/append_only.rb +172 -8
  230. data/lib/hecks/ports/persistence/binding_policy.rb +34 -0
  231. data/lib/hecks/ports/persistence/codec_boundary.rb +178 -0
  232. data/lib/hecks/ports/persistence/execution.rb +4 -0
  233. data/lib/hecks/ports/persistence/null_saga_store.rb +12 -1
  234. data/lib/hecks/ports/persistence/plugin.rb +42 -4
  235. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +218 -25
  236. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +77 -9
  237. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +81 -24
  238. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +29 -18
  239. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +144 -60
  240. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +103 -8
  241. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +98 -23
  242. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +282 -109
  243. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +63 -25
  244. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +118 -66
  245. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +51 -28
  246. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +34 -5
  247. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +25 -12
  248. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +129 -34
  249. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +51 -6
  250. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +33 -9
  251. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +16 -0
  252. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +95 -6
  253. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +25 -2
  254. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +314 -90
  255. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +68 -10
  256. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +9 -3
  257. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +9 -2
  258. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +48 -8
  259. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +16 -1
  260. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +36 -5
  261. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +23 -3
  262. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +58 -19
  263. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +96 -5
  264. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +15 -0
  265. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +9 -1
  266. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +2 -2
  267. data/lib/hecks/ports/persistence/plugins/era.rb +12 -2
  268. data/lib/hecks/ports/persistence/remote_runtime.rb +9 -2
  269. data/lib/hecks/ports/persistence/repository_factory.rb +23 -3
  270. data/lib/hecks/ports/persistence/state_codec.rb +319 -0
  271. data/lib/hecks/ports/persistence.rb +36 -1
  272. data/lib/hecks/ports/projection.rb +61 -7
  273. data/lib/hecks/ports/query/in_memory.rb +41 -8
  274. data/lib/hecks/ports/query/ordering.rb +21 -6
  275. data/lib/hecks/ports/query.rb +35 -0
  276. data/lib/hecks/ports.rb +1 -0
  277. data/lib/hecks/projections/bootstrap_table.rb +147 -0
  278. data/lib/hecks/projections/diagrams.rb +315 -79
  279. data/lib/hecks/projections/glossary/html.rb +91 -3
  280. data/lib/hecks/projections/glossary/markdown.rb +50 -3
  281. data/lib/hecks/projections/glossary/mermaid.rb +49 -1
  282. data/lib/hecks/projections/glossary/sections.rb +1 -1
  283. data/lib/hecks/projections/glossary/sentences.rb +110 -5
  284. data/lib/hecks/projections/glossary.rb +173 -31
  285. data/lib/hecks/projections/ir.rb +1 -1
  286. data/lib/hecks/projections/model/deviations.rb +62 -17
  287. data/lib/hecks/projections/model.rb +75 -21
  288. data/lib/hecks/projections/oidc.rb +45 -8
  289. data/lib/hecks/projections/parser_table.rb +53 -12
  290. data/lib/hecks/projections/reference.rb +15 -3
  291. data/lib/hecks/projections/rust_vocabulary.rb +646 -0
  292. data/lib/hecks/projections/shape.rb +10 -2
  293. data/lib/hecks/projections/statements.rb +69 -22
  294. data/lib/hecks/projections/vocabulary.rb +26 -9
  295. data/lib/hecks/projections.rb +5 -3
  296. data/lib/hecks/projector/cli_projector.rb +245 -37
  297. data/lib/hecks/projector/docs_projector.rb +154 -28
  298. data/lib/hecks/projector/exporter.rb +104 -29
  299. data/lib/hecks/projector/ir_projector.rb +7 -1
  300. data/lib/hecks/projector/narrate_projector.rb +144 -23
  301. data/lib/hecks/projector/target.rb +42 -18
  302. data/lib/hecks/projector.rb +86 -18
  303. data/lib/hecks/query_ir.rb +94 -47
  304. data/lib/hecks/query_specification/common/comparators.rb +19 -3
  305. data/lib/hecks/query_specification/common/comparison.rb +109 -27
  306. data/lib/hecks/query_specification/common/dsl.rb +65 -9
  307. data/lib/hecks/query_specification/common/null_policy.rb +61 -15
  308. data/lib/hecks/query_specification/common/null_semantics.rb +4 -0
  309. data/lib/hecks/query_specification/common/options.rb +25 -0
  310. data/lib/hecks/query_specification/field_path.rb +69 -15
  311. data/lib/hecks/query_specification/hop_path.rb +57 -20
  312. data/lib/hecks/query_specification/read_model/specification.rb +4 -0
  313. data/lib/hecks/rendering.rb +9 -3
  314. data/lib/hecks/router/namespace_installer.rb +16 -3
  315. data/lib/hecks/router.rb +56 -1
  316. data/lib/hecks/runtime/aggregate_lock.rb +20 -11
  317. data/lib/hecks/runtime/boot_gates.rb +21 -3
  318. data/lib/hecks/runtime/caller.rb +40 -8
  319. data/lib/hecks/runtime/capability_graph.rb +13 -2
  320. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +40 -41
  321. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +45 -67
  322. data/lib/hecks/runtime/command_interpreter.rb +195 -113
  323. data/lib/hecks/runtime/command_rules/admissibility.rb +231 -80
  324. data/lib/hecks/runtime/command_rules/arithmetic.rb +154 -96
  325. data/lib/hecks/runtime/command_rules/authorization.rb +38 -17
  326. data/lib/hecks/runtime/command_rules/emission.rb +18 -1
  327. data/lib/hecks/runtime/command_rules/references.rb +145 -55
  328. data/lib/hecks/runtime/command_rules.rb +3 -0
  329. data/lib/hecks/runtime/dependency_planning.rb +56 -11
  330. data/lib/hecks/runtime/dispatcher.rb +263 -92
  331. data/lib/hecks/runtime/entity_element.rb +304 -67
  332. data/lib/hecks/runtime/entity_interpreter.rb +149 -88
  333. data/lib/hecks/runtime/errors.rb +37 -23
  334. data/lib/hecks/runtime/event.rb +15 -10
  335. data/lib/hecks/runtime/identity.rb +91 -23
  336. data/lib/hecks/runtime/instance.rb +103 -18
  337. data/lib/hecks/runtime/interpreting.rb +24 -16
  338. data/lib/hecks/runtime/invocation.rb +358 -0
  339. data/lib/hecks/runtime/loader.rb +105 -19
  340. data/lib/hecks/runtime/outbox.rb +164 -26
  341. data/lib/hecks/runtime/policy_interpreter.rb +73 -60
  342. data/lib/hecks/runtime/port_operation_interpreter.rb +42 -19
  343. data/lib/hecks/runtime/query_interpreter.rb +93 -74
  344. data/lib/hecks/runtime/reaction_invocation.rb +73 -28
  345. data/lib/hecks/runtime/read_model_interpreter.rb +60 -44
  346. data/lib/hecks/runtime/rebuild_sweep.rb +32 -4
  347. data/lib/hecks/runtime/reference_hop.rb +48 -6
  348. data/lib/hecks/runtime/refusal_wording.rb +142 -115
  349. data/lib/hecks/runtime/registry/saga_persistence.rb +32 -21
  350. data/lib/hecks/runtime/registry/verification.rb +153 -28
  351. data/lib/hecks/runtime/registry.rb +202 -32
  352. data/lib/hecks/runtime/remote_dispatcher.rb +125 -24
  353. data/lib/hecks/runtime/routing.rb +36 -154
  354. data/lib/hecks/runtime/saga_interpreter/correlation.rb +25 -27
  355. data/lib/hecks/runtime/saga_interpreter.rb +90 -76
  356. data/lib/hecks/runtime/saga_pending_dispatch.rb +12 -12
  357. data/lib/hecks/runtime/tenant_check.rb +33 -13
  358. data/lib/hecks/runtime/tenant_scope.rb +23 -5
  359. data/lib/hecks/runtime/value/admission.rb +75 -30
  360. data/lib/hecks/runtime/value/coercion.rb +555 -142
  361. data/lib/hecks/runtime/value/entity_list_coercion.rb +132 -60
  362. data/lib/hecks/runtime/value.rb +71 -21
  363. data/lib/hecks/runtime.rb +39 -7
  364. data/lib/hecks/storehouse.rb +368 -72
  365. data/lib/hecks/tenancy/bluebook/tenancy.bluebook +130 -0
  366. data/lib/hecks/tenancy/bluebook/tenancy.hecksagon +32 -0
  367. data/lib/hecks/version.rb +3 -3
  368. data/lib/hecks/vocabulary.rb +205 -4
  369. data/lib/hecks.rb +91 -11
  370. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +19 -11
  371. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +24 -12
  372. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +38 -14
  373. metadata +28 -2
@@ -1,7 +1,7 @@
1
1
  require_relative "errors"
2
2
  require_relative "refusal_wording"
3
3
  require_relative "caller"
4
- require_relative "routing"
4
+ require_relative "invocation"
5
5
  require_relative "command_rules"
6
6
  require_relative "command_interpreter"
7
7
  require_relative "entity_interpreter"
@@ -25,6 +25,8 @@ module Hecks
25
25
  MAX_REACTION_DEPTH = 5
26
26
 
27
27
  Result = Struct.new(:verb, :instance, :events, :execution_plan, :persistence_outcome, keyword_init: true) do
28
+ # Reads the identity of the record the dispatch settled on.
29
+ #
28
30
  # `instance` is nil for a port operation dispatched by verb (below)
29
31
  # — nothing was hydrated or saved, the same reason
30
32
  # `PortOperationInterpreter#emit`'s own comment gives for sourcing
@@ -33,7 +35,15 @@ module Hecks
33
35
  # asks this Result for `.id`/`.state` made a category error the
34
36
  # domain itself already told it about (there is no record here),
35
37
  # not a crash-worthy one.
38
+ #
39
+ # @return [String, nil] the record's identity; nil for a port operation, which
40
+ # hydrates no record
36
41
  def id = instance&.id
42
+
43
+ # Reads the settled record's attributes as one Hash, for the same nil-safe reason as `id`.
44
+ #
45
+ # @return [Hash{Symbol => Object}, nil] the record's state with `:id` merged in
46
+ # last; nil for a port operation, which hydrates no record
37
47
  def state = instance&.to_h
38
48
 
39
49
  def to_s
@@ -46,6 +56,8 @@ module Hecks
46
56
 
47
57
  attr_reader :registry
48
58
 
59
+ # @param registry [Runtime::Registry] the booted registry every interpreter reads; its
60
+ # outbox is attached to this dispatcher's policy and saga interpreters
49
61
  def initialize(registry)
50
62
  @registry = registry
51
63
  rules = CommandRules.new(registry)
@@ -56,7 +68,7 @@ module Hecks
56
68
  @read_models = ReadModelInterpreter.new(registry)
57
69
  @policies = PolicyInterpreter.new(registry, door: self)
58
70
  @sagas = SagaInterpreter.new(registry, door: self)
59
- # THE RELAY IS THE REGISTRY'S, NOT THIS DISPATCHER'S — the
71
+ # The relay is the registry's, not this dispatcher's — the
60
72
  # interpreters enqueue through `@registry.outbox` from inside
61
73
  # the save transaction, and this dispatcher drains through the
62
74
  # same object, so there is exactly one relay per registry no
@@ -65,20 +77,108 @@ module Hecks
65
77
  @registry.outbox.attach(policies: @policies, sagas: @sagas)
66
78
  end
67
79
 
80
+ # Exposes the registry's outbox relay, the object this dispatcher drains reactions through.
81
+ #
68
82
  # `runtime.outbox.rows`, `.rows(status: "claimed")`, `.redrive!`,
69
83
  # `.log` — see `Runtime::Outbox`.
84
+ #
85
+ # @return [Runtime::Outbox::Relay] the registry's one relay, shared by every dispatcher
86
+ # fronting that registry
70
87
  def outbox = @registry.outbox
71
88
 
89
+ # Exposes every event emitted through this registry since boot or the last reset.
90
+ #
91
+ # @return [Array<Runtime::Event>] the registry's live event log, oldest first
72
92
  def events = @registry.event_log
73
93
 
94
+ # Exposes one record per policy reaction that was delivered, refused or left undelivered.
95
+ #
96
+ # @return [Array<Hash{Symbol => Object}>] the registry's live reaction log, oldest first
74
97
  def reactions = @registry.reaction_log
75
98
 
99
+ # Exposes one record per process-manager step: a start, an advance, a delivery, a refusal.
100
+ #
101
+ # @return [Array<Hash{Symbol => Object}>] the registry's live saga log, oldest first
76
102
  def sagas = @registry.saga_log
103
+
104
+ # Exposes the raw inputs each saga dispatch bound its arguments from, a Ruby-only log.
105
+ #
106
+ # @return [Array<Hash{Symbol => Object}>] the registry's live saga dispatch log, oldest first
77
107
  def saga_dispatches = @registry.saga_dispatch_log
108
+
109
+ # Exposes the raw inputs each policy trigger bound its arguments from, a Ruby-only log.
110
+ #
111
+ # @return [Array<Hash{Symbol => Object}>] the registry's live policy dispatch log, with
112
+ # keys `:policy`, `:on`, `:payload`, `:with_spec` and `:args`
78
113
  def policy_dispatches = @registry.policy_dispatch_log
114
+
115
+ # Lists every verb the loaded bluebooks declare.
116
+ #
117
+ # @return [Array<String>] the verbs of every loaded bluebook, sorted
79
118
  def verbs = @registry.verbs
80
119
 
81
- def dispatch(verb, to: nil, with: nil, saga_correlation: nil, **legacy_args)
120
+ # Runs one command, entity command or port operation, then every policy and saga reaction
121
+ # its events are owed.
122
+ #
123
+ # The receiver in `to:`, the facts in `with:`, and nothing else.
124
+ # Loose keyword facts — `dispatch(verb, amount: 5)`, one bag holding
125
+ # both the route and the payload — were deprecated in 1.3.x and are
126
+ # gone: Ruby now refuses them itself, by name ("unknown keyword:
127
+ # :amount"). Code holding a bag of data rather than written keywords
128
+ # calls `dispatch_flat` below; that door is not going anywhere.
129
+ #
130
+ # @param verb [String] the fully qualified verb: `"Domain::Aggregate.Command"`,
131
+ # `"Domain::Aggregate.Entity.Command"` or `"Domain::Aggregate.Port.Operation"`
132
+ # @param to [String, Hash, nil] the receiver: an aggregate identity, or an entity route
133
+ # Hash with `:aggregate` and one of `:entity`/`:entities`; nil when the facts carry
134
+ # the identity themselves
135
+ # @param with [Hash, nil] the command's facts, keyed by argument name (String or Symbol)
136
+ # @param saga_correlation [Hash, nil] correlation head => value, stamped on every
137
+ # emitted event when a saga leg causes this dispatch; nil otherwise
138
+ # @return [Runtime::Dispatcher::Result] the verb, settled instance (nil for a port
139
+ # operation), emitted events, execution plan and persistence outcome
140
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
141
+ # aggregate, command, entity or port operation that is not declared
142
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
143
+ # the call (`GivenNotMet`, `TypeMismatch`, `Unauthorized`, `NotFound`, …)
144
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
145
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
146
+ def dispatch(verb, to: nil, with: nil, saga_correlation: nil)
147
+ dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, flat: {})
148
+ end
149
+
150
+ # Dispatches a verb whose receiver and facts arrive together in one flat Hash.
151
+ #
152
+ # **The flat-facts wire form** — one Hash, not keywords: the shape
153
+ # `spec/corpus/*.json` steps, the Rust kernel's `cli.rs` contract, a
154
+ # reaction without a `with:` projection, and the self-hosted
155
+ # meta-domain all carry, where the receiver's identity is one of the
156
+ # keys because that is how the wire spells it. Routes exactly as the
157
+ # removed keyword door did: a Symbol `:to`, `:with` or
158
+ # `:saga_correlation` key is lifted out as that keyword, everything
159
+ # else is a fact (so a String "to" key stays a fact, as it did).
160
+ # Framework code that replays data calls this; application code
161
+ # calls `dispatch(verb, to:, with:)`.
162
+ #
163
+ # @param verb [String] the fully qualified verb, in any shape `dispatch` accepts
164
+ # @param args [Hash] the facts, plus optional Symbol keys `:to`, `:with` and
165
+ # `:saga_correlation`, read as `dispatch`'s keywords of the same names; not mutated
166
+ # @return [Runtime::Dispatcher::Result] the same result `dispatch` returns
167
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
168
+ # aggregate, command, entity or port operation that is not declared
169
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the domain refuses
170
+ # the call
171
+ # @raise [Runtime::StaleWrite] if concurrent writers beat this one through every retry
172
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
173
+ def dispatch_flat(verb, args = {})
174
+ facts = args.dup
175
+ to = facts.delete(:to)
176
+ with = facts.delete(:with)
177
+ saga_correlation = facts.delete(:saga_correlation)
178
+ dispatch_invocation(verb, to: to, with: with, saga_correlation: saga_correlation, flat: facts)
179
+ end
180
+
181
+ def dispatch_invocation(verb, to:, with:, saga_correlation:, flat:)
82
182
  domain, aggregate_name, command_name = parse(verb)
83
183
  aggregate = resolve_aggregate(domain, aggregate_name, verb)
84
184
 
@@ -86,7 +186,7 @@ module Hecks
86
186
  if command_name.include?(".")
87
187
  head, sub = command_name.split(".", 2)
88
188
  port = aggregate.port(head)
89
- # A PORT OPERATION, reached by the SAME verb shape an entity
189
+ # A port operation, reached by the same verb shape an entity
90
190
  # command already uses ("Domain::Aggregate.Head.Rest") — ports
91
191
  # are checked first, so an aggregate that ever declared both a
92
192
  # port and an entity of the same name would resolve to the
@@ -99,34 +199,35 @@ module Hecks
99
199
  # path.
100
200
  if port
101
201
  operation = port.operation(sub) ||
102
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "port_no_operation",
103
- port: head, operation: sub.inspect))
104
- route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
105
- [nil, @port_ops.call(domain, aggregate, operation, args, route: route), nil, nil, :enqueue]
202
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "port_no_operation",
203
+ port: head, operation: sub))
204
+ invocation = Invocation.from_call(verb, to: to, with: with, flat: flat,
205
+ receiver: :port, aggregate: aggregate) { operation }
206
+ [nil, @port_ops.call(domain, aggregate, operation, invocation), nil, nil, :enqueue]
106
207
  else
107
- entity_depth = command_name.split(".").size - 1
108
- route = Routing.envelope(to, entity_depth: entity_depth)
109
- @entities.call(domain, aggregate, command_name, legacy_args, route: route, with: with)
208
+ resolution = nil
209
+ invocation = Invocation.from_call(verb, to: to, with: with, flat: flat,
210
+ receiver: :entity, entity_depth: command_name.count(".")) do
211
+ (resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
212
+ end
213
+ @entities.call(domain, aggregate, resolution, invocation)
110
214
  end
111
215
  else
112
- command = aggregate.command(command_name) ||
113
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command",
114
- aggregate: aggregate_name, command: command_name.inspect))
115
- args = Routing.payload(command, with: with, legacy: legacy_args)
116
- route = Routing.envelope(to)
117
- @commands.call(domain, aggregate, command, args, saga_correlation, route: route)
216
+ command = command_of(aggregate, aggregate_name, command_name)
217
+ invocation = Invocation.from_call(verb, to: to, with: with, flat: flat) { command }
218
+ @commands.call(domain, aggregate, command, invocation, saga_correlation)
118
219
  end
119
220
 
120
- # Correlation is SET AT CONSTRUCTION now, not merged on here —
121
- # it is part of the transaction, known from this method's own
122
- # argument before a single event exists. It used to be stamped
123
- # onto already-emitted events, which is what kept an event
124
- # mutable after it had happened.
221
+ # Correlation is set when each event is constructed, not merged on
222
+ # here — it is part of the transaction, known from this method's
223
+ # own argument before a single event exists. Stamping it onto
224
+ # already-emitted events would keep an event mutable after it had
225
+ # happened.
125
226
  #
126
- # The ordering this note used to guard still holds, and more
127
- # simply: `SagaInterpreter#advance` runs on THIS domain's
128
- # `announced` events within this very call, and finds the
129
- # correlation already there because it was never absent.
227
+ # The ordering that depends on it holds for the same reason:
228
+ # `SagaInterpreter#advance` runs on this domain's `announced`
229
+ # events within this very call, and finds the correlation already
230
+ # there because it was never absent.
130
231
 
131
232
  react(announced, domain, aggregate, outbox_rows)
132
233
 
@@ -134,7 +235,12 @@ module Hecks
134
235
  execution_plan: execution_plan, persistence_outcome: persistence_outcome)
135
236
  end
136
237
 
137
- # EVERYTHING OWED BECAUSE `announced` COMMITTED — policies first,
238
+ # **The one body both doors run** — `dispatch` (keywords) and
239
+ # `dispatch_flat` (one Hash) differ only in how the call's parts are
240
+ # spelled, never in what happens next.
241
+ private :dispatch_invocation
242
+
243
+ # **Everything owed because `announced` committed** — policies first,
138
244
  # then sagas, the order this method always ran them in. The
139
245
  # command/entity interpreters hand back the outbox rows they
140
246
  # enqueued inside the save transaction (`Interpreting#
@@ -152,9 +258,11 @@ module Hecks
152
258
  end
153
259
  private :react
154
260
 
155
- # "IF THIS WERE DISPATCHED RIGHT NOW, WOULD IT SUCCEED" — the same
261
+ # Answers whether a command would succeed right now, without saving, emitting or reacting.
262
+ #
263
+ # "If this were dispatched right now, would it succeed" — the same
156
264
  # pipeline #dispatch itself runs (arguments coerced, givens checked,
157
- # mutations applied IN MEMORY, ensures checked against the settled
265
+ # mutations applied in memory, ensures checked against the settled
158
266
  # result), except `step_save`/`step_emit` never run, and neither do
159
267
  # policies or sagas afterward: nothing here is committed, so nothing
160
268
  # should react to it. Built for exactly the shape a whole-board
@@ -164,15 +272,26 @@ module Hecks
164
272
  # own move purely to trigger the check, which then had to avoid
165
273
  # interfering with the very position being tested).
166
274
  #
167
- # RAISES THE SAME REFUSALS #dispatch does — a DomainRefusal
275
+ # Raises the same refusals #dispatch does — a DomainRefusal
168
276
  # subclass propagates normally, so a caller checking "would this be
169
277
  # legal" writes the identical rescue clause a real dispatch already
170
278
  # needs; this returns `true` only when nothing was refused.
171
279
  #
172
- # NEVER A PORT VERB — `PortOperationInterpreter`'s own side effects
280
+ # **Never a port verb** — `PortOperationInterpreter`'s own side effects
173
281
  # (an external gateway call, say) have no meaningful in-memory-only
174
282
  # form, so this refuses one outright rather than silently running
175
283
  # it for real, which "dry" would otherwise quietly lie about.
284
+ #
285
+ # @param verb [String] the fully qualified aggregate or entity command verb
286
+ # @param args [Hash{Symbol => Object}] the command's facts as flat keywords; a key named
287
+ # `to` or `with` is an ordinary fact here, never routing (BUG#131)
288
+ # @return [true] whenever nothing refused; a refusal is raised, never returned as false
289
+ # @raise [Runtime::WiringError] if the verb names a port operation, or the aggregate's
290
+ # repository cannot be resolved
291
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
292
+ # aggregate, command or entity that is not declared
293
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` that the real dispatch
294
+ # would raise
176
295
  def dry_run?(verb, **args)
177
296
  domain, aggregate_name, command_name = parse(verb)
178
297
  aggregate = resolve_aggregate(domain, aggregate_name, verb)
@@ -185,18 +304,25 @@ module Hecks
185
304
  "only for aggregate and entity commands"
186
305
  end
187
306
 
188
- @entities.call(domain, aggregate, command_name, args, dry_run: true)
307
+ # `to:`/`with:` are not keywords of this method — a key named
308
+ # either is an ordinary fact here (BUG#131), so both go in as nil.
309
+ resolution = nil
310
+ invocation = Invocation.from_call(verb, to: nil, with: nil, flat: args, receiver: :entity) do
311
+ (resolution = EntityInterpreter::Resolution.of(aggregate, command_name)).command
312
+ end
313
+ @entities.call(domain, aggregate, resolution, invocation, dry_run: true)
189
314
  else
190
- command = aggregate.command(command_name) ||
191
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "aggregate_no_command",
192
- aggregate: aggregate_name, command: command_name.inspect))
193
- @commands.call(domain, aggregate, command, args, dry_run: true)
315
+ command = command_of(aggregate, aggregate_name, command_name)
316
+ invocation = Invocation.from_call(verb, to: nil, with: nil, flat: args) { command }
317
+ @commands.call(domain, aggregate, command, invocation, dry_run: true)
194
318
  end
195
319
 
196
320
  true
197
321
  end
198
322
 
199
- # THE DOOR AN ADAPTER OUTSIDE THE BLUEBOOK CALLS THROUGH — never the
323
+ # Runs one port operation named by its parts, then the reactions its events are owed.
324
+ #
325
+ # The door an adapter outside the bluebook calls through — never the
200
326
  # domain itself. `port_name`/`operation_name` are separate arguments
201
327
  # rather than one packed verb string on purpose: there is no established
202
328
  # wire spelling for "domain, aggregate, port, operation" yet, and
@@ -205,66 +331,68 @@ module Hecks
205
331
  # No adapter-to-port binding lookup happens here — that is
206
332
  # `Hecks.adapter`'s existing job (unchanged by this), and wiring "which
207
333
  # adapter may call this port" through is the next piece, not this one.
208
- def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, **legacy_args)
334
+ #
335
+ # @param domain [String, Symbol] name of the domain the aggregate belongs to
336
+ # @param aggregate_name [String, Symbol] name of the aggregate that declares the port
337
+ # @param port_name [String] name of the port, as the aggregate declares it
338
+ # @param operation_name [String] name of the operation on that port
339
+ # @param to [String, Hash, nil] the receiving aggregate's identity; when nil it is read
340
+ # from the facts, by the operation's reference or identity attribute
341
+ # @param with [Hash, nil] the operation's facts, keyed by argument name
342
+ # @param flat [Hash] the wire form `dispatch_flat` takes, for the driving adapter
343
+ # holding a decoded webhook rather than written keywords; it is where the operation's
344
+ # own reference attribute is read from and lifted into `to:` when `to:` is nil
345
+ # @return [Array<Runtime::Event>] the events the operation announced: one per declared
346
+ # `emits` for an inbound operation, the one answering or refusing event for an
347
+ # outbound one
348
+ # @raise [Runtime::UnknownVerb] if the domain, aggregate, port or operation is not declared
349
+ # @raise [Runtime::TypeMismatch] if no receiving identity can be found, or `to:`/`with:`
350
+ # is malformed
351
+ # @raise [Runtime::NotFound] if the receiving aggregate record does not exist
352
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
353
+ def dispatch_port(domain, aggregate_name, port_name, operation_name, to: nil, with: nil, flat: {})
209
354
  aggregate = resolve_aggregate(domain, aggregate_name, "#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}")
210
355
  port = aggregate.port(port_name) ||
211
356
  raise(UnknownVerb, "#{aggregate_name} has no port #{port_name.inspect}")
212
357
  operation = port.operation(operation_name) ||
213
358
  raise(UnknownVerb, "#{port_name} has no operation #{operation_name.inspect}")
214
359
 
215
- route, args = port_invocation(aggregate, operation, to: to, with: with, legacy: legacy_args)
216
- announced = @port_ops.call(domain, aggregate, operation, args, route: route)
360
+ invocation = Invocation.from_call("#{domain}::#{aggregate_name}.#{port_name}.#{operation_name}",
361
+ to: to, with: with, flat: flat,
362
+ receiver: :port, aggregate: aggregate) { operation }
363
+ announced = @port_ops.call(domain, aggregate, operation, invocation)
217
364
 
218
365
  react(announced, domain, aggregate, :enqueue)
219
366
 
220
367
  announced
221
368
  end
222
369
 
223
- def port_invocation(aggregate, operation, to:, with:, legacy:)
224
- legacy = legacy.dup
225
- identity = operation.identity_attribute(aggregate.hecks_name)
226
- if to.nil? && identity && legacy.key?(identity.name)
227
- to = legacy.delete(identity.name)
228
- elsif to.nil? && operation.to == aggregate.hecks_name
229
- # `to:`-DECLARED OPERATIONS carry no Reference-typed attribute at
230
- # all (PortOperationBuilder#initialize's own comment on why —
231
- # genuine routing metadata, not an attribute), so `identity`
232
- # above is always nil for these; this is the second, purely
233
- # additive lookup they need instead. The routing value sits in
234
- # a PLAIN external-fact attribute, named for the owning
235
- # aggregate's own identified_by field — the domain author's job
236
- # to match, same discipline reference_to's own `as:` always
237
- # required. Composite identity (more than one identified_by
238
- # component) isn't attempted here — `.first` only, no domain in
239
- # the real corpus has needed more for a port operation yet.
240
- #
241
- # READ, NOT deleted — unlike the Reference-attribute branch
242
- # above, this is a genuine declared operation attribute (Rust's
243
- # own comment: "declare only external facts with attribute"),
244
- # not synthetic routing-only state; the operation's own
245
- # attributes still expect to find it in the payload a few steps
246
- # later (refuse_absent_arguments), and a real, live
247
- # AbsentArgument confirmed this the hard way before `[]`
248
- # replaced `delete`.
249
- identity_name = Array(aggregate.identified_by).first
250
- to = legacy[identity_name] if identity_name && legacy.key?(identity_name)
251
- end
252
-
253
- route = Routing.envelope(to)
254
- raise TypeMismatch, "#{operation.hecks_name} requires its receiving aggregate in to:" unless route
255
-
256
- [route, Routing.payload(operation, with: with, legacy: legacy)]
257
- end
258
- private :port_invocation
259
-
370
+ # Answers a declared query: an aggregate query, an entity query, or a read model.
371
+ #
372
+ # The verb's shape picks the interpreter. `"Domain.ReadModel"` — no `::` before the
373
+ # dot — is a read model; `"Domain::Aggregate.Query"` is an aggregate query, and
374
+ # `"Domain::Aggregate.Entity.Query"` an entity query.
375
+ #
376
+ # @param verb [String, Symbol] the query's verb, in one of the three shapes above
377
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
378
+ # @return [Array<Hash>] for an aggregate query, one deep-frozen Hash per matching record,
379
+ # its state with `:id` merged in last; for an entity query, one Hash per matching
380
+ # element with the parent's reference key merged in first; for a read model, a
381
+ # one-element Array holding a Hash of head name to projected rows
382
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
383
+ # aggregate, entity, query or read model that is not declared
384
+ # @raise [Runtime::NotFound] if a read model's root reference names no record
385
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
386
+ # @raise [KeyError] if a rooted read model is asked without its reference argument
387
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
260
388
  def query(verb, **args)
261
389
  domain, query_name = verb.to_s.split(".", 2)
262
390
  if query_name && !domain.include?("::")
263
391
  bluebook = @registry.bluebook(domain) ||
264
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
392
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_domain", domain: domain, verb: verb))
265
393
  model = bluebook.read_model(query_name) ||
266
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_read_model",
267
- domain: domain, query: query_name.inspect))
394
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_read_model",
395
+ domain: domain, query: query_name))
268
396
  return @read_models.call(domain, model, args)
269
397
  end
270
398
 
@@ -274,10 +402,22 @@ module Hecks
274
402
  @queries.call(domain, aggregate, query_name, args)
275
403
  end
276
404
 
277
- # The same ask, answered by the reference interpreter alone — never
278
- # the bound adapter's native hook. Read models have no reference
279
- # twin, so only the aggregate-query form answers here; the fuzzer's
280
- # query oracle diffs this against #query's answer.
405
+ # Answers an aggregate or entity query through the reference interpreter alone.
406
+ #
407
+ # The same ask as `#query`, never answered by the bound adapter's
408
+ # native hook. Read models have no reference twin, so only the
409
+ # `"Domain::Aggregate.Query"` forms answer here; the fuzzer's
410
+ # query oracle diffs this against `#query`'s answer.
411
+ #
412
+ # @param verb [String] the fully qualified query verb, `"Domain::Aggregate.Query"` or
413
+ # `"Domain::Aggregate.Entity.Query"`
414
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
415
+ # @return [Array<Hash>] one Hash per matching record, its state with `:id` merged in
416
+ # last; for an entity query, one Hash per matching element
417
+ # @raise [Runtime::UnknownVerb] if the verb is not fully qualified, or names a domain,
418
+ # aggregate, entity or query that is not declared
419
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its declared type
420
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
281
421
  def reference_query(verb, **args)
282
422
  domain, aggregate_name, query_name = parse(verb)
283
423
  aggregate = resolve_aggregate(domain, aggregate_name, verb)
@@ -285,7 +425,10 @@ module Hecks
285
425
  @queries.reference_call(domain, aggregate, query_name, args)
286
426
  end
287
427
 
288
- # A reaction is the SYSTEM acting, not the caller who happened to be
428
+ # Dispatches a reaction's command one level deeper in the cascade, as the system rather
429
+ # than as the triggering caller.
430
+ #
431
+ # A reaction is the system acting, not the caller who happened to be
289
432
  # on the stack when the triggering command ran — the ambient caller
290
433
  # is cleared for the reaction's own dispatch, so a triggering
291
434
  # caller's role can neither satisfy nor block a reaction command it
@@ -296,39 +439,67 @@ module Hecks
296
439
  # dispatching through it (a Puma worker pool, say), so a plain ivar
297
440
  # here is exactly the known Puma-concurrency bug class: two
298
441
  # concurrent top-level dispatches on different threads would
299
- # increment/decrement the SAME counter, letting one thread's nested
442
+ # increment/decrement the same counter, letting one thread's nested
300
443
  # reaction depth leak into another thread's unrelated dispatch. A
301
444
  # `Mutex` is not the answer either — a reaction cascade re-enters
302
- # `reenter` on the SAME thread (see `SagaInterpreter#advance_saga`'s
445
+ # `reenter` on the same thread (see `SagaInterpreter#advance_saga`'s
303
446
  # own comment on why a non-reentrant `Mutex` can't guard this).
304
447
  # `Thread.current`-backed, saved/restored around the call with a
305
448
  # plain local + `ensure`, is the same idiom `Runtime::Caller`
306
449
  # (`caller.rb`) already established for exactly this shape of
307
450
  # per-thread ambient state.
451
+ #
452
+ # The depth is not checked here: a reacting interpreter asks
453
+ # `reaction_depth_reached?` first and records an undelivered reaction
454
+ # instead of calling this.
455
+ #
456
+ # @param verb [String] the fully qualified verb of the reaction's target command
457
+ # @param saga_correlation [Hash{String => Object}, nil] correlation head => value when a
458
+ # saga leg dispatches; nil for a policy
459
+ # @param args [Hash{Symbol => Object}] the flat facts, read exactly as `dispatch_flat`
460
+ # reads them, so `:to` and `:with` keys route rather than count as facts
461
+ # @return [Runtime::Dispatcher::Result] the result of the nested dispatch
462
+ # @raise [Runtime::UnknownVerb] if the verb names nothing declared
463
+ # @raise [StandardError] any class in `Runtime::DOMAIN_REFUSALS` when the target
464
+ # refuses; the policy and saga interpreters rescue these as recorded outcomes
308
465
  def reenter(verb, saga_correlation: nil, **args)
309
466
  depth = Thread.current[:hecks_reaction_depth].to_i
310
467
  Thread.current[:hecks_reaction_depth] = depth + 1
311
- Caller.without { dispatch(verb, saga_correlation: saga_correlation, **args) }
468
+ Caller.without { dispatch_flat(verb, args.merge(saga_correlation: saga_correlation)) }
312
469
  ensure
313
470
  Thread.current[:hecks_reaction_depth] = depth
314
471
  end
315
472
 
473
+ # Answers whether the calling thread's reaction cascade is as deep as it may go.
474
+ #
475
+ # @return [Boolean] true once this thread's nested `reenter` calls number
476
+ # `MAX_REACTION_DEPTH` or more; other threads' cascades are not counted
316
477
  def reaction_depth_reached? = Thread.current[:hecks_reaction_depth].to_i >= MAX_REACTION_DEPTH
478
+
479
+ # Reads the cascade limit, for the reason an interpreter records when it declines to react.
480
+ #
481
+ # @return [Integer] `MAX_REACTION_DEPTH`, the number of nested reactions allowed
317
482
  def max_reaction_depth = MAX_REACTION_DEPTH
318
483
 
319
484
  private
320
485
 
321
486
  def parse(verb)
322
487
  Naming.split_verb(verb) ||
323
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "not_fully_qualified", verb: verb.inspect))
488
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "not_fully_qualified", verb: verb))
489
+ end
490
+
491
+ def command_of(aggregate, aggregate_name, command_name)
492
+ aggregate.command(command_name) ||
493
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "aggregate_no_command",
494
+ aggregate: aggregate_name, command: command_name))
324
495
  end
325
496
 
326
497
  def resolve_aggregate(domain, aggregate_name, verb)
327
498
  bluebook = @registry.bluebook(domain) ||
328
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
499
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_domain", domain: domain, verb: verb))
329
500
  bluebook.aggregate(aggregate_name) ||
330
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_aggregate",
331
- domain: domain, aggregate: aggregate_name.inspect))
501
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_aggregate",
502
+ domain: domain, aggregate: aggregate_name))
332
503
  end
333
504
  end
334
505
  end