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
@@ -15,27 +15,43 @@ module Hecks
15
15
  # repository: prefers a native adapter hook (Ports::Query.execute)
16
16
  # when the store can answer directly, falling back to interpreting
17
17
  # wheres/order_by/limit/offset over every loaded record itself.
18
- # #reference_call/#reference_interpret are a SEPARATE, deliberately
18
+ # #reference_call/#reference_interpret are a separate, deliberately
19
19
  # naive re-implementation of the same evaluation, used only as the
20
20
  # fuzzer's oracle to catch divergence between adapters and this
21
21
  # interpreter's own native path.
22
22
  class QueryInterpreter
23
23
  attr_reader :registry
24
24
 
25
+ # @param registry [Runtime::Registry] the booted registry queries are answered
26
+ # against
25
27
  def initialize(registry)
26
28
  @registry = registry
27
29
  end
28
30
 
31
+ # Answers one declared aggregate or entity query, preferring a native adapter
32
+ # hook and falling back to interpreting the query over every loaded record.
33
+ #
34
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
35
+ # @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
36
+ # @param query_name [String] the query's declared name, or an entity query's
37
+ # dotted `"Entity.Query"` name
38
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
39
+ # @return [Array<Hash>] one frozen Hash per matching record, its state with
40
+ # `:id` merged in last
41
+ # @raise [Runtime::UnknownVerb] if `query_name` names no declared query
42
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
43
+ # declared type
44
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
29
45
  def call(domain, aggregate, query_name, args)
30
46
  return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
31
47
 
32
48
  declared = declared_query(aggregate, query_name)
33
49
  args = normalize_args(aggregate, declared, args)
34
50
  declared = TenantScope.apply(declared, args)
35
- # AFTER TenantScope, so its synthetic clause is already present
36
- # in `.wheres` and rides through as an ordinary LOCAL clause on
37
- # the OUTER query. It does not reach the hop's own inner
38
- # sub-query against the TARGET aggregate — a hop's target may
51
+ # After TenantScope, so its synthetic clause is already present
52
+ # in `.wheres` and rides through as an ordinary local clause on
53
+ # the outer query. It does not reach the hop's own inner
54
+ # sub-query against the target aggregate — a hop's target may
39
55
  # not even declare the same tenant boundary, and propagating one
40
56
  # aggregate's tenant scope onto an unrelated aggregate's own
41
57
  # query is a real design question of its own, not answered here.
@@ -51,14 +67,14 @@ module Hecks
51
67
  if (native = Ports::Query.execute(repository, declared, args,
52
68
  context: { domain: domain, aggregate: aggregate, registry: @registry }))
53
69
  records = native
54
- # `record.state.merge(id: record.id)` — id LAST, not first. See
70
+ # `record.state.merge(id: record.id)` — id last, not first. See
55
71
  # Instance#to_h's own comment: an aggregate free to declare its
56
72
  # own attribute literally named `id` has that attribute's own
57
73
  # wrapped value sitting in `record.state[:id]` already; merging
58
- # it OVER a `{id:}.merge(state)` used to let it silently
74
+ # it over a `{id:}.merge(state)` would let it silently
59
75
  # clobber the correct bare identity this row is supposed to
60
76
  # carry.
61
- # A QUERY ROW IS AN ANSWER, NOT A HANDLE. Mutating one edits
77
+ # A query row is an answer, not a handle. Mutating one edits
62
78
  # nobody's state and silently disagrees with the store.
63
79
  return Freezer.deep(records.map { |record| record.state.merge(id: record.id) })
64
80
  end
@@ -66,21 +82,33 @@ module Hecks
66
82
  Freezer.deep(interpret(repository.all, declared, args, domain: domain))
67
83
  end
68
84
 
69
- # The REFERENCE answer — this interpreter's own evaluation, never an
85
+ # The reference answer — this interpreter's own evaluation, never an
70
86
  # adapter's native hook. The fuzzer's query oracle replays every
71
87
  # generated ask through both paths and treats a difference as a
72
88
  # finding: the differential gate the retired cross-runtime harness
73
89
  # should always have been, aimed where the divergence actually
74
90
  # lives — between the engines inside this one runtime.
75
91
  #
76
- # A hop clause is answered here by its OWN, deliberately naive
92
+ # A hop clause is answered here by its own, deliberately naive
77
93
  # walk (reference_where_holds?) — never Runtime::ReferenceHop's
78
- # partition/fold/IN-clause. Sharing that algorithm would have made
94
+ # partition/fold/in-clause. Sharing that algorithm would have made
79
95
  # this oracle blind to exactly the code the hop feature adds: every
80
- # PHASE of a shared fold would still get diffed against the native
96
+ # phase of a shared fold would still get diffed against the native
81
97
  # adapters, but the fold itself — the empty candidate set, a
82
98
  # duplicate id, a dangling reference, a chain's inside-out
83
99
  # resolution order — would only ever be compared against itself.
100
+ #
101
+ # @param domain [String, Symbol] the domain `aggregate` belongs to
102
+ # @param aggregate [Bluebook::Aggregate] the aggregate the query is declared on
103
+ # @param query_name [String] the query's declared name, or an entity query's
104
+ # dotted `"Entity.Query"` name
105
+ # @param args [Hash{Symbol => Object}] the query's declared arguments
106
+ # @return [Array<Hash>] one Hash per matching record, its state with `:id`
107
+ # merged in last
108
+ # @raise [Runtime::UnknownVerb] if `query_name` names no declared query
109
+ # @raise [Runtime::TypeMismatch] if an argument cannot be coerced to its
110
+ # declared type
111
+ # @raise [Runtime::WiringError] if the aggregate's repository cannot be resolved
84
112
  def reference_call(domain, aggregate, query_name, args)
85
113
  return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
86
114
 
@@ -95,22 +123,22 @@ module Hecks
95
123
 
96
124
  def declared_query(aggregate, query_name)
97
125
  aggregate.query(query_name) ||
98
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_query",
99
- aggregate: aggregate.hecks_name, query: query_name.inspect))
126
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "no_query",
127
+ aggregate: aggregate.hecks_name, query: query_name))
100
128
  end
101
129
 
102
130
  def interpret(records, declared, args, domain: nil)
103
131
  matched = records.select { |r| declared.wheres.all? { |w| where_holds?(w, r, args, domain: domain) } }
104
132
  ordered = ordered(matched, declared.order_by, declared.null_semantics)
105
- # OFFSET FIRST, THEN LIMIT — the order SQL means by `LIMIT n
133
+ # **Offset first, then limit** — the order SQL means by `LIMIT n
106
134
  # OFFSET m`, and the order Ports::Query::InMemory#execute already
107
- # applies (see that file's own comment). This interpreter used to
108
- # never read declared.offset at all — offset silently vanished for
109
- # any query answered here, not just come out reversed.
135
+ # applies (see that file's own comment). Without reading
136
+ # `declared.offset`, offset would silently vanish for any query
137
+ # answered here, not just come out reversed.
110
138
  skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
111
139
  capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
112
140
 
113
- # id LAST — see the native-path comment above; same clobbering
141
+ # id last — see the native-path comment above; same clobbering
114
142
  # risk for the in-memory reference interpreter's own rows.
115
143
  capped.map { |r| r.state.merge(id: r.id) }
116
144
  end
@@ -120,7 +148,7 @@ module Hecks
120
148
  # reference is answered by reference_where_holds? instead of the
121
149
  # plain FieldPath.dig(record, field) `where_holds?` uses (which
122
150
  # has no concept of a reference at all — it would just read the
123
- # raw id straight off the record and compare THAT).
151
+ # raw id straight off the record and compare that).
124
152
  def reference_interpret(records, declared, args, domain:, shape:)
125
153
  matched = records.select do |r|
126
154
  declared.wheres.all? do |w|
@@ -128,20 +156,20 @@ module Hecks
128
156
  end
129
157
  end
130
158
  ordered = ordered(matched, declared.order_by, declared.null_semantics)
131
- # OFFSET FIRST, THEN LIMIT — same fix, same reasoning, as
159
+ # **Offset first, then limit** — same fix, same reasoning, as
132
160
  # #interpret's own rows above.
133
161
  skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
134
162
  capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
135
163
 
136
- # id LAST — same reasoning, same fix, as interpret's own rows.
164
+ # id last — same reasoning, same fix, as interpret's own rows.
137
165
  capped.map { |r| r.state.merge(id: r.id) }
138
166
  end
139
167
 
140
- # THE NAIVE READING OF A HOP: not a fold, not an id set — for
168
+ # **The naive reading of a hop**: not a fold, not an id set — for
141
169
  # each candidate row, walk the reference by hand and dig the
142
170
  # field out of whatever it actually points at. A nil reference,
143
171
  # or one that resolves to nothing (a dangling id), makes the
144
- # WHOLE clause false outright, whatever the comparator — "points
172
+ # whole clause false outright, whatever the comparator — "points
145
173
  # at a client that is not active" is false for a proposal with no
146
174
  # client at all, the same way it is false for one whose client
147
175
  # really is active; falling through to holds?(clause, nil, args)
@@ -177,8 +205,8 @@ module Hecks
177
205
 
178
206
  ordered = ordered_elements(rows, declared.order_by, declared.null_semantics,
179
207
  parent_key, entity.identity_heads)
180
- # OFFSET FIRST, THEN LIMIT — same fix, same reasoning, as
181
- # #interpret's own rows above. `entity_rows` is the ONLY engine
208
+ # **Offset first, then limit** — same fix, same reasoning, as
209
+ # #interpret's own rows above. `entity_rows` is the only engine
182
210
  # for entity/sub-list queries, so a declared offset here silently
183
211
  # vanished for every entity query, not merely one path among
184
212
  # several.
@@ -186,31 +214,28 @@ module Hecks
186
214
  declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
187
215
  end
188
216
 
189
- # THE THREE DECLARATIONS `entity_rows` NEEDS BEFORE IT CAN READ A
190
- # SINGLE RECORD — the entity itself, its declared query, and the
217
+ # The three declarations `entity_rows` needs before it can read a
218
+ # single record — the entity itself, its declared query, and the
191
219
  # list attribute that holds it on the aggregate. Extracted from
192
220
  # `entity_rows` (pure extraction, same lookups, same order, same
193
221
  # UnknownVerb refusals) purely to separate "which declarations does
194
222
  # this dotted name resolve to" from the row-computation that follows.
195
223
  def resolve_entity_query(aggregate, entity_name, query_name)
196
224
  entity = aggregate.entities.find { |piece| piece.hecks_name == entity_name } ||
197
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
198
- aggregate: aggregate.hecks_name, entity: entity_name.inspect))
225
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_unknown",
226
+ aggregate: aggregate.hecks_name, entity: entity_name))
199
227
  declared = entity.query(query_name) ||
200
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_query_missing",
201
- entity: entity_name, query: query_name.inspect))
228
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_query_missing",
229
+ entity: entity_name, query: query_name))
202
230
  list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
203
- raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_holds_no_list",
204
- aggregate: aggregate.hecks_name, entity: entity_name))
231
+ raise(UnknownVerb, RefusalWording.render_site("UnknownVerb", "entity_holds_no_list",
232
+ aggregate: aggregate.hecks_name, entity: entity_name))
205
233
  [entity, declared, list_attr]
206
234
  end
207
235
 
208
- # FieldPath.dig, not a raw `element[clause.field.to_sym]` — an
209
- # entity sub-list row is a plain hash merged from stored state
210
- # (mixed string/symbol keys depending on adapter, per `#cell`'s own
211
- # comment below), and a dotted `where` (`where "price.cents" < 100`)
212
- # needs the same segment-by-segment walk every other query path
213
- # already gets. Reading only the symbol spelling of the WHOLE
236
+ # FieldPath.dig, not a raw `element[clause.field.to_sym]` — a dotted
237
+ # `where` (`where "price.cents" < 100`) needs the same segment-by-
238
+ # segment walk every other query path already gets. Reading the whole
214
239
  # dotted string as one key always missed — `element[:"price.cents"]`
215
240
  # is never a real key — so a dotted where on an entity query
216
241
  # silently matched nothing, on the only engine entity queries have.
@@ -218,28 +243,22 @@ module Hecks
218
243
  holds?(clause, QuerySpecification::FieldPath.dig(element, clause.field), args)
219
244
  end
220
245
 
221
- # A row's own key, however the store spells it. A sub-list row is a plain hash
222
- # merged from stored state, so its keys arrive as strings from one adapter and
223
- # symbols from another — and reading only one spelling gave every row the SAME
224
- # identity, which is a tie, which is the exact nondeterminism this tier exists
225
- # to remove. It rides `comparable` for the same reason a where-clause does : an
226
- # identity is a value object, and `to_s` on one is an OBJECT ADDRESS — a sort key
246
+ # A row's own key. A sub-list row is a hydrated entity element (symbol-keyed
247
+ # since every adapter decodes through `Ports::Persistence::StateCodec` and
248
+ # `EntityListCoercion#hydrate_entity_list` symbolizes each element — PR A4
249
+ # removed the string-spelling fallback that coped with the old per-adapter
250
+ # shapes). It rides `comparable` for the same reason a where-clause does : an
251
+ # identity is a value object, and `to_s` on one is an object address — a sort key
227
252
  # that differs run to run, which is worse than the store order it replaced.
228
- # `key?` decides which spelling answers, never `||` — a row whose
229
- # value is a genuinely-held `false` must not fall through to the
230
- # other spelling (usually absent) and land on `nil`.
231
- def cell(row, key)
232
- sym = key.to_sym
233
- row.key?(sym) ? row[sym] : row[key.to_s]
234
- end
253
+ def cell(row, key) = row[key.to_sym]
235
254
 
236
- # A sub-list row is identified by its PARENT and then its own key : two
255
+ # A sub-list row is identified by its parent and then its own key : two
237
256
  # entities under different parents can share a sequence, so the parent has
238
257
  # to lead or the tie is not broken at all.
239
258
  #
240
- # EVERY KEY THE PIECE IS KNOWN BY, in declaration order, for the same
259
+ # Every key the piece is known by, in declaration order, for the same
241
260
  # reason the parent leads: a part that ties is a part that breaks no tie.
242
- # This took `identified_by`, which is the SINGLE head and is nil the
261
+ # This took `identified_by`, which is the single head and is nil the
243
262
  # moment an identity has two parts — and `cell(row, nil)` calls
244
263
  # `nil.to_sym`, so a query against a composite piece did not sort wrongly,
245
264
  # it raised. A piece known by one key sorts exactly as it did.
@@ -258,12 +277,12 @@ module Hecks
258
277
  end
259
278
 
260
279
  # The comparator table itself lives in
261
- # QuerySpecification::Common::Comparison. This method and
262
- # Ports::Query::InMemory#holds? used to carry a copy each and the
263
- # two drifted — `none_in_state` reached only one of them, and
264
- # `comparable` disagreed about value objects with two numeric
265
- # members. What stays here is how a value is REACHED for this
266
- # path: the registry is instance state rather than an argument.
280
+ # QuerySpecification::Common::Comparison, shared with
281
+ # Ports::Query::InMemory#holds? rather than each carrying its own
282
+ # copy — two copies once drifted: `none_in_state` reached only one
283
+ # of them, and `comparable` disagreed about value objects with two
284
+ # numeric members. What stays here is how a value is reached for
285
+ # this path: the registry is instance state rather than an argument.
267
286
  def holds?(clause, held, args, record: nil, domain: nil)
268
287
  QuerySpecification::Common::Comparison.holds?(
269
288
  clause.op, comparable(held), comparable(resolve_query_value(clause.value, args)), registry: @registry
@@ -276,40 +295,40 @@ module Hecks
276
295
 
277
296
  # `boundary: false` always (C3.8 — a query's declared argument types
278
297
  # name the argument for callers and generators, never a runtime
279
- # shape checked here). A null required VALUE-OBJECT-typed query
280
- # argument, though, is NOT a runtime-shape question at all: C3.7
298
+ # shape checked here). A null required value-object-typed query
299
+ # argument, though, is not a runtime-shape question at all: C3.7
281
300
  # says a named query's declared value-object arguments are checked
282
301
  # the same way a command argument's own is, so a `nil` offered for
283
302
  # a non-optional value-object-typed query attribute
284
303
  # (Governance::RoleAssignment.AssignmentsForActor's `actor_id`, say)
285
- # has to refuse — passing it through unchecked (as this used to)
286
- # let it through as a silent, unfiltered query instead, a real
304
+ # has to refuse — passing it through unchecked would let it through
305
+ # as a silent, unfiltered query instead, a real
287
306
  # Ruby/Rust divergence the fuzzer caught (QualityControl BUG#2).
288
307
  #
289
- # `checked_vo?` true is handled by `null_vo_argument!` DIRECTLY,
308
+ # `checked_vo?` true is handled by `null_vo_argument!` directly,
290
309
  # never by routing through `Value.for_attribute(argument: true)`
291
- # into the shared `Value::Coercion#nil_argument` the COMMAND door
310
+ # into the shared `Value::Coercion#nil_argument` the command door
292
311
  # (`Interpreting#coerce_declared_arguments`) still uses — that
293
- # method builds a null value object from ZERO fields, which
294
- # SUCCEEDS (silently absorbing the null via the type's own field
312
+ # method builds a null value object from zero fields, which
313
+ # succeeds (silently absorbing the null via the type's own field
295
314
  # defaults) whenever every field happens to have one
296
315
  # (`Lease.Expired`'s `now`, a `LeaseInstant` with a `default: 0`
297
316
  # field; `Account.Overdrawn`/`HighBalance`/`StrictlyAbove`/
298
317
  # `AtMost`'s `floor`/`cap`, a two-defaulted-field `Money`) and only
299
318
  # refuses when a field has none (`Order.CostingLessThan`'s
300
- # `ceiling`, a defaultless `Price`) — a real QUERY-side divergence
319
+ # `ceiling`, a defaultless `Price`) — a real query-side divergence
301
320
  # from Rust, which always refuses `TypeMismatch` on an explicit
302
321
  # null argument regardless of any default (QualityControl BUG#36).
303
322
  # `null_vo_argument!` instead treats an explicit null exactly the
304
- # way `Value.fields_for` already treats any other WRONG-SHAPED
323
+ # way `Value.fields_for` already treats any other wrong-shaped
305
324
  # (non-Hash, non-Value) value offered for that same attribute — a
306
325
  # single-field value object auto-wraps into `{field: nil}` (whose
307
- # OWN `nil` is a PRESENT key, so `Value.build`'s `apply_defaults`
326
+ # own `nil` is a present key, so `Value.build`'s `apply_defaults`
308
327
  # never fills it, and `check_required_fields` refuses it exactly
309
328
  # as any other missing required field would); a multi-field value
310
329
  # object refuses immediately with the same `value_object_shape`
311
330
  # wording an ordinary wrong-shaped scalar already gets. Command
312
- # arguments are deliberately UNTOUCHED — this is a query-only
331
+ # arguments are deliberately untouched — this is a query-only
313
332
  # door; `nil_argument`'s own default-absorbing fallback still
314
333
  # governs a null command argument exactly as it always has.
315
334
  #
@@ -6,10 +6,10 @@ require_relative "value"
6
6
  module Hecks
7
7
  module Runtime
8
8
  # Turns facts selected by a policy or process manager into the same
9
- # receiver/payload envelope an outside caller uses. Reaction declarations
10
- # historically selected both through one `with:` map, so this is the one
11
- # compatibility seam that separates receiver identities from facts after
12
- # resolving the declaration and before re-entering the dispatcher.
9
+ # receiver/payload envelope an outside caller uses. A reaction declares
10
+ # both receiver identities and facts through one `with:` map, so this is
11
+ # the one compatibility seam that separates them after resolving the
12
+ # declaration and before re-entering the dispatcher.
13
13
  module ReactionInvocation
14
14
  Target = Struct.new(:aggregate, :entities, :command, keyword_init: true)
15
15
  # :facts, not :values — Struct.new already defines #values (every
@@ -19,10 +19,17 @@ module Hecks
19
19
 
20
20
  module_function
21
21
 
22
- # The holding IR historically represented both an omitted projection and
23
- # an explicitly empty `with: {}` as the same empty array. Builders now
24
- # preserve declaration presence off-wire; reconstructed/legacy IR falls
25
- # back to the old non-empty reading.
22
+ # Reports whether a reaction declared an explicit `with:` projection.
23
+ #
24
+ # An omitted projection and an explicitly empty `with: {}` both hold
25
+ # as the same empty array on the holding IR. Builders preserve
26
+ # declaration presence off-wire, in `@projection_declared`;
27
+ # reconstructed/legacy IR without that ivar falls back to reading
28
+ # presence off whether `with_spec` is non-empty.
29
+ #
30
+ # @param declaration [Bluebook::Policy, Bluebook::DispatchSpec] the reacting
31
+ # declaration to check
32
+ # @return [Boolean] true if the declaration names an explicit `with:` projection
26
33
  def projection_declared?(declaration)
27
34
  if declaration.instance_variable_defined?(:@projection_declared)
28
35
  declaration.instance_variable_get(:@projection_declared)
@@ -31,11 +38,27 @@ module Hecks
31
38
  end
32
39
  end
33
40
 
41
+ # Resolves a declared `with:` projection against the scopes and bindings visible to it.
42
+ #
34
43
  # A reaction's source names resolve lexically, not globally. Policies
35
44
  # supply one event/row scope. Process managers supply current event then
36
45
  # opening-event memory, while correlation is an explicit binding ahead
37
46
  # of both. Missing names are refused here rather than materialized as nil
38
47
  # and accidentally presented as target command facts.
48
+ #
49
+ # @param with_spec [Hash{Symbol => Object}] each target fact name mapped to its source:
50
+ # a Symbol naming a fact visible in `bindings` or `scopes`, or any other value taken
51
+ # as a literal
52
+ # @param scopes [Array<Hecks::Runtime::ReactionInvocation::Scope, Array(String,
53
+ # Hash)>] the named fact scopes to resolve a Symbol source against, checked in order;
54
+ # a bare `[name, facts]` pair is wrapped into a `Scope`
55
+ # @param bindings [Hash] explicit bindings (such as a saga's correlation key), checked
56
+ # before any scope
57
+ # @param label [String] names this resolution in an `UnknownArgument` refusal
58
+ # @return [Hash{Symbol => Object}] `with_spec`'s keys mapped to their resolved,
59
+ # materialized values
60
+ # @raise [Runtime::UnknownArgument] if a Symbol source names a fact visible in no
61
+ # binding and no scope
39
62
  def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
40
63
  normalized_bindings = bindings.transform_keys(&:to_sym)
41
64
  normalized_scopes = scopes.map do |scope|
@@ -49,7 +72,7 @@ module Hecks
49
72
  end
50
73
  end
51
74
 
52
- # ONE `with:` SOURCE, RESOLVED — pulled out of resolve_mapping
75
+ # One `with:` source, resolved — pulled out of resolve_mapping
53
76
  # because it is a pure function of its own arguments (a literal, a
54
77
  # binding, or a name visible in some scope), with no dependency on
55
78
  # anything else resolve_mapping's own to_h block is doing.
@@ -60,10 +83,10 @@ module Hecks
60
83
  visible = scopes.find { |scope| scope.facts.key?(source) }
61
84
  unless visible
62
85
  names = scopes.map(&:name).join(" then ")
63
- # WHAT IS VISIBLE, NAMED. A refusal that only says which name is
86
+ # **What is visible, named**. A refusal that only says which name is
64
87
  # missing sent a modeler guessing field after field
65
88
  # (`number`, `reference`…) at a fan-out row that is addressed
66
- # by ONE key — `account`, the lowercase aggregate — which
89
+ # by one key — `account`, the lowercase aggregate — which
67
90
  # nothing else in the domain spells out. The names each scope
68
91
  # actually offers are the whole diagnosis; the refusal now
69
92
  # lists them, scope by scope.
@@ -80,12 +103,34 @@ module Hecks
80
103
  # command attributes enter `with:`, while identities become `to:`.
81
104
  # `consumed` accumulates across the aggregate-identity and entity-
82
105
  # identity steps below, and refuse_unconsumed! at the end reads the
83
- # FINAL list — an ordering dependency threaded through one shared
106
+ # final list — an ordering dependency threaded through one shared
84
107
  # local. Already leans on private helpers (identity_for,
85
108
  # source_receiver_for, command_facts, refuse_unconsumed!) for every
86
- # piece that IS self-contained; what remains is the sequencing
109
+ # piece that is self-contained; what remains is the sequencing
87
110
  # itself, which further splitting would only relocate, not remove.
88
111
  # rubocop:disable-next Metrics/MethodLength, Metrics/PerceivedComplexity
112
+ #
113
+ # @param registry [Runtime::Registry] the booted registry to resolve `verb` against
114
+ # @param verb [String] the fully qualified target command verb
115
+ # @param projected [Hash] the facts to send, already resolved (e.g. by
116
+ # `resolve_mapping`) or, for a legacy reaction, the raw event/row payload
117
+ # @param explicit [Boolean] true when the reaction declared its own `with:` projection
118
+ # (`projection_declared?`); false forwards `projected` wholesale as legacy args
119
+ # @param passthrough [Array<String, Symbol>] extra fact names allowed to ride along
120
+ # unconsumed, beyond the receiver identity and declared command facts
121
+ # @param source_receiver [Hash{Symbol => Object}, nil] the triggering event's own
122
+ # `{aggregate:, identity:}`, offered as a same-aggregate receiver when nothing else
123
+ # supplies one; nil when there is no such event to inherit from
124
+ # @return [Hash{Symbol => Object}] `{to:, with:}` for an explicit projection targeting
125
+ # a non-creating command (`with:` only for a creating command); otherwise `projected`
126
+ # (with `to:` merged in when a receiver could be inherited)
127
+ # @raise [Runtime::UnknownVerb] if `verb` does not resolve to a declared command,
128
+ # entity command, or port operation (only when `explicit` is true; a legacy call
129
+ # resolving `verb` only to check inheritance swallows this and forwards unchanged)
130
+ # @raise [Runtime::TypeMismatch] if an explicit projection resolves no receiver
131
+ # identity for the target aggregate or one of its entities
132
+ # @raise [Runtime::UnknownArgument] if an explicit projection's facts include a name
133
+ # that is neither a consumed receiver identity nor a declared command fact
89
134
  def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
90
135
  args = projected.transform_keys(&:to_sym)
91
136
  unless explicit
@@ -99,7 +144,7 @@ module Hecks
99
144
  rescue UnknownVerb
100
145
  return args
101
146
  end
102
- # AN ENTITY TARGET HAS NO SHAPE THIS BRANCH CAN BUILD — an
147
+ # An entity target has no shape this branch can build — an
103
148
  # implicit (unprojected) `to:` is always a bare scalar, the
104
149
  # aggregate's own identity alone; an entity command's receiver is
105
150
  # `{aggregate:, entities:}`, which needs the entity's own
@@ -167,13 +212,13 @@ module Hecks
167
212
 
168
213
  *entity_names, command_name = command_path.split(".")
169
214
 
170
- # A PORT OPERATION, reached by the SAME two-segment tail shape an
171
- # entity command uses ("Head.Rest") — checked FIRST, same order
215
+ # A port operation, reached by the same two-segment tail shape an
216
+ # entity command uses ("Head.Rest") — checked first, same order
172
217
  # `Dispatcher#dispatch` already resolves a live verb in (an
173
218
  # aggregate that declared both a port and an entity of the same
174
219
  # name would resolve to the port there too; no domain in this
175
220
  # corpus does). A port has no `entities` of its own — the
176
- # RECEIVER is always the aggregate itself — so `Target#entities`
221
+ # receiver is always the aggregate itself — so `Target#entities`
177
222
  # stays empty and `#command` holds the `PortOperation`, which
178
223
  # answers `#creates?` (always false) the same way an ordinary
179
224
  # `Command` does, letting `source_receiver_for` lift a same-
@@ -215,37 +260,37 @@ module Hecks
215
260
  private_class_method :aggregate_aliases
216
261
 
217
262
  # Event.id names the aggregate that emitted the event. It can therefore
218
- # supply only the receiver of a non-creating command on that SAME root;
263
+ # supply only the receiver of a non-creating command on that same root;
219
264
  # it cannot address another aggregate, invent an entity identity, or turn
220
265
  # a creation into a mutation. An explicit projected receiver is resolved
221
266
  # first and remains authoritative.
222
267
  #
223
- # ANSWERS REGARDLESS OF `target.entities` — the value this method hands
224
- # back is ONLY EVER the ROOT AGGREGATE'S OWN identity (`source_receiver`
268
+ # Answers regardless of `target.entities` — the value this method hands
269
+ # back is only ever the root aggregate's own identity (`source_receiver`
225
270
  # carries nothing else: `event.aggregate`/`event.id` name the emitting
226
- # AGGREGATE, never one of its entities). An entity target's own identity
271
+ # aggregate, never one of its entities). An entity target's own identity
227
272
  # is resolved entirely separately, from `args` (`build`'s own `entity_
228
273
  # identities` loop) — this method is never consulted for it, so
229
274
  # answering for an entity target does not "invent an entity identity"
230
- # any more than answering for a plain one invents THAT identity; it was
275
+ # any more than answering for a plain one invents that identity; it was
231
276
  # already the one thing this method has ever supplied. `build`'s own
232
- # EXPLICIT branch (below) already applies this correctly either way
277
+ # explicit branch (below) already applies this correctly either way
233
278
  # (`aggregate_identity ||= inherited_receiver`, entity identities read
234
279
  # from `args` regardless) — the caller that actually needed a guard
235
- # here is the IMPLICIT one, above, which has no shape to build a
280
+ # here is the implicit one, above, which has no shape to build a
236
281
  # `{aggregate:, entities:}` receiver from an inherited scalar alone,
237
282
  # and now guards itself for exactly that reason instead of this method
238
283
  # doing it on that caller's behalf and, as a side effect, also refusing
239
- # the EXPLICIT caller's own legitimate case — confirmed missing until
284
+ # the explicit caller's own legitimate case — confirmed missing until
240
285
  # `qa/stress_domains/waybill` (BUG#6) exercised it for the first time
241
286
  # anywhere in the corpus: a saga dispatching into a nested entity's own
242
287
  # command, inheriting its aggregate receiver from the event that
243
288
  # triggered it.
244
289
  def source_receiver_for(target, source_receiver)
245
290
  return nil unless source_receiver
246
- # `target.command.creates?` ALONE MISREADS EVERY ENTITY COMMAND —
291
+ # `target.command.creates?` alone misreads every entity command —
247
292
  # `Behaviour::Command#creates?`'s own comment: "a verb declared on
248
- # an ENTITY always acts on that piece... which means `creates?`
293
+ # an entity always acts on that piece... which means `creates?`
249
294
  # answers true for every one of them" (it never sets `@references`
250
295
  # the way an aggregate-level command's own `reference_to` does,
251
296
  # not because it brings anything new into being). `build`'s own
@@ -253,7 +298,7 @@ module Hecks
253
298
  # 20-odd lines below, already reads `creates?` correctly for
254
299
  # exactly this reason — an entity command is never a genuine
255
300
  # creation, whatever `creates?` alone answers — so this checks the
256
- # SAME compound condition instead of the bare, misleading half of
301
+ # same compound condition instead of the bare, misleading half of
257
302
  # it. Left unfixed, an entity target's own receiver was refused
258
303
  # here even after this method stopped refusing on `target.entities`
259
304
  # alone — the second half of BUG#6's own fix.