hecks 0.2.0 → 0.3.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 (408) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +403 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +131 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,69 @@
1
+ require_relative "value"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ # What every stepwise interpreter shares: the traced step, and the
6
+ # coercion of a command's declared arguments. Two copies of each lived
7
+ # in CommandInterpreter and EntityInterpreter, where they could only
8
+ # ever drift.
9
+ module Interpreting
10
+ # Each including interpreter gets its own `trace` — set by a spec to
11
+ # observe dispatch order (Vocabulary::AggregateDispatchOrder and
12
+ # Vocabulary::EntityDispatchOrder in language/bluebook/vocabulary.bluebook);
13
+ # nil in production, always — one array push and a nil check per step
14
+ # is the entire cost of leaving this in.
15
+ def self.included(interpreter)
16
+ interpreter.singleton_class.attr_accessor :trace
17
+ end
18
+
19
+ private
20
+
21
+ # Logged AFTER the step's own work, so a step that wraps sub-steps logs
22
+ # itself once everything inside it has already logged — trace order is
23
+ # completion order, which is dispatch order.
24
+ def step(name)
25
+ result = yield
26
+ self.class.trace << name if self.class.trace
27
+ result
28
+ end
29
+
30
+ # DRIVES `DISPATCH_ORDER` (CommandInterpreter/EntityInterpreter, each
31
+ # mirroring its own Vocabulary::*DispatchOrder — vocabulary.bluebook,
32
+ # held equal to it by spec/vocabulary_conformance_spec.rb) by `send`ing
33
+ # each declared step name against the including interpreter's own
34
+ # `step_<name>` handler, in declared order. What used to be `call`'s own
35
+ # literal sequence of method calls is now DATA driving that sequence —
36
+ # tracing a real dispatch and comparing it to the declaration is
37
+ # tautological once `call` mechanically follows the declaration; a
38
+ # conditional step (assign_creation_attributes, advance_lifecycle) still
39
+ # has to guard ITSELF at the top of its own handler and skip tracing
40
+ # when it does not fire, rather than the caller branching around it —
41
+ # see CommandInterpreter#step_assign_creation_attributes.
42
+ def run_dispatch_order(order, ctx)
43
+ order.each { |name| send(:"step_#{name}", ctx) }
44
+ end
45
+
46
+ # Every declared attribute present in the payload passes the reference
47
+ # gate, then coercion — the same walk whether the command acts on an
48
+ # aggregate or on one of its entity's elements.
49
+ def coerce_declared_arguments(aggregate, command, args)
50
+ command.attributes.each_with_object(args.dup) do |attribute, normalized|
51
+ next unless normalized.key?(attribute.name)
52
+
53
+ Value.refuse_object_reference(command, attribute, normalized[attribute.name])
54
+ normalized[attribute.name] = Value.for_attribute(aggregate, attribute, normalized[attribute.name])
55
+ end
56
+ end
57
+
58
+ # Coercion only — CommandInterpreter's own refuse_unknown_arguments/
59
+ # refuse_absent_arguments are separate DISPATCH_ORDER steps now (the
60
+ # declared vocabulary lists all three as flat, sequential members, not
61
+ # one nesting the other two), and EntityInterpreter never had them here
62
+ # at all (an entity inherits its aggregate's own gate). One copy,
63
+ # shared, rather than the two identical ones that used to drift.
64
+ def normalize_args(aggregate, command, args)
65
+ coerce_declared_arguments(aggregate, command, args)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,143 @@
1
+ require_relative "../facade/surface"
2
+ require_relative "../ports/loading"
3
+ require_relative "dispatcher"
4
+ require_relative "remote_dispatcher"
5
+ require_relative "era_check"
6
+ require_relative "registry"
7
+
8
+ module Hecks
9
+ module Runtime
10
+ class Loader
11
+ # `install_facade:` defaults on — every ordinary caller wants
12
+ # `Widget::Item.Add(...)` sugar. A caller that only ever dispatches
13
+ # by FQN string (`SmokeTest`, the one caller so far) can pass
14
+ # `false` to skip it: `Facade::Surface.install` puts a bare global
15
+ # Ruby constant on `Object` per domain AND per aggregate name, with
16
+ # no scoping and no cleanup hook, so a tool booting arbitrary
17
+ # throwaway domains under generic names ("Widget", "Item", "Tag")
18
+ # would otherwise leak those names into the rest of the process —
19
+ # measured, not hypothetical: this exact leak once made an
20
+ # unrelated `dsl_spec.rb` example resolve a bare `Widget` constant
21
+ # to a stale smoke-test facade from a deleted temp directory instead
22
+ # of raising, corrupting that spec's own unrelated build. Skipping
23
+ # the install is safe because nothing downstream of a raw
24
+ # `Dispatcher` needs the sugar — `Dispatcher#dispatch`/`#query` work
25
+ # identically either way.
26
+ # `environment:` — see Adapters::Folder#load_domain's own comment
27
+ # for the mechanism. hecks never reads ENV itself (every other
28
+ # env-var lookup in this codebase lives in app-owned .world/
29
+ # .hecksagon files, never library internals) — a caller resolves
30
+ # its own env var name and passes the resulting string straight
31
+ # through, e.g. `Hecks.boot(path, environment:
32
+ # ENV.fetch("MYAPP_ENV", "development"))`.
33
+ def self.boot(path, shared: nil, install_facade: true, environment: nil)
34
+ loading = Ports::Loading.bootstrap
35
+ directory = loading.bluebook_directory(path)
36
+ root = loading.shared_root(shared, directory)
37
+ registry = Registry.new(root: File.dirname(directory))
38
+
39
+ Hecks.with_registry(registry) do
40
+ loading.load_library
41
+ loading.load_project(root)
42
+ loading.load_domain(directory, environment: environment)
43
+ end
44
+
45
+ # The era gate runs BEFORE verify! builds repositories: minting an
46
+ # era (Postgres) must have created its partition and head views
47
+ # before any adapter opens them, and a refused era must refuse
48
+ # before any adapter touches data.
49
+ EraCheck.check!(registry, directory)
50
+ registry.verify!
51
+ # AFTER verify! (conservative — any wiring error surfaces first,
52
+ # not strictly required since resolution only needs the
53
+ # hecksagon binds, already loaded), BEFORE the dispatcher is
54
+ # built — repopulates `saga_instances` from whatever durable
55
+ # store each domain's own adapter answers with (§2-§4), so a
56
+ # process manager mid-flight at the last shutdown/crash/cold-
57
+ # start doesn't start this boot looking like it never began.
58
+ registry.rehydrate_sagas!
59
+ dispatcher = dispatcher_for(registry)
60
+ install_facade ? bind_runtime(dispatcher) : dispatcher
61
+ end
62
+
63
+ # THE EXPLICIT-FILE FORM — `paths` names the exact bluebook/hecksagon/
64
+ # world files to boot, in place, wherever they actually live. `boot`
65
+ # above only ever takes a directory and globs it; that is the right
66
+ # shape for a real deployment (`examples/banking`, a domain someone
67
+ # `cd`s into), and the wrong one for a caller that wants to declare a
68
+ # narrow, explicit scope and have it booted exactly as declared — a
69
+ # `.behaviors` file's own `loads` line is the motivating caller
70
+ # (`Hecks::Behaviors`), but this carries no behaviors-specific
71
+ # logic and is not gated behind requiring that module.
72
+ #
73
+ # NO COPYING, NO TEMP DIRECTORY. A prior port of this same idea
74
+ # (vendored into a downstream consumer, read before writing this)
75
+ # scoped a per-test boot by copying files into `Dir.mktmpdir` — which
76
+ # destroys real relative paths, and worse, makes a `persisted_by`
77
+ # path resolve against the TEMP copy's root instead of the project's
78
+ # own (confirmed there: a file adapter kept reading and writing the
79
+ # same deterministic tmp copy across an entire session, because
80
+ # `Hecks.boot`'s own `root` is always `File.dirname` of whatever
81
+ # directory it was handed). `directory` here is `File.dirname` of the
82
+ # FIRST real path in `paths` — genuinely on disk, not a copy — so
83
+ # every downstream path (`EraCheck`, `persisted_by`, `shared_root`)
84
+ # resolves exactly as an ordinary directory boot's would.
85
+ def self.boot_files(paths, shared: nil, install_facade: true, environment: nil)
86
+ loading = Ports::Loading.bootstrap
87
+ files = Array(paths).map { |path| File.expand_path(path) }
88
+ directory = File.dirname(files.first)
89
+ root = loading.shared_root(shared, directory)
90
+ registry = Registry.new(root: File.dirname(directory))
91
+
92
+ Hecks.with_registry(registry) do
93
+ loading.load_library
94
+ loading.load_project(root)
95
+ loading.load_selected(files, environment: environment)
96
+ end
97
+
98
+ EraCheck.check!(registry, directory)
99
+ registry.verify!
100
+ registry.rehydrate_sagas!
101
+ dispatcher = dispatcher_for(registry)
102
+ install_facade ? bind_runtime(dispatcher) : dispatcher
103
+ end
104
+
105
+ # `RemoteDispatcher` for a domain routed through Lambda,
106
+ # `Dispatcher` otherwise — the ONE place this decision gets
107
+ # made, so everything built on top (`Handle`, `AggregateDoor`,
108
+ # `Facade::Surface`) never has to know which class it's holding.
109
+ # `registry.bluebooks.keys.first` is the just-booted domain's own
110
+ # name (insertion order — the target's own bluebook loads before
111
+ # any `uses_framework` chapter, `bin/project_rust`'s own header
112
+ # draws the identical distinction), not a directory basename.
113
+ #
114
+ # `dispatched_by("Lambda")` is its OWN, EXPLICIT verb — NOT
115
+ # inferred from `deployed_to("AwsLambda")`'s mere presence. Both
116
+ # Banking's and Embryonaut's `.world` files already declare
117
+ # `deployed_to("AwsLambda")` (it only means "a deploy target
118
+ # exists"), so treating that alone as "boot this domain against
119
+ # Lambda" would have silently rerouted Banking's every local
120
+ # boot — every spec, every `bin/console` session — the moment
121
+ # this landed. A domain opts in explicitly, the same way
122
+ # `persisted_by("PostgresEra")` is never inferred from anything
123
+ # else either.
124
+ def self.dispatcher_for(registry)
125
+ domain = registry.bluebooks.keys.first
126
+ settings = registry.world(domain)&.for_verb("dispatched_by") || {}
127
+ return Dispatcher.new(registry) unless settings[:adapter] == "Lambda"
128
+
129
+ RemoteDispatcher.new(registry, region: settings.fetch(:region, "us-east-1"))
130
+ end
131
+
132
+ # THE DOOR IS INSTALLED HERE, NOT STAMPED. This used to write the
133
+ # dispatcher onto every aggregate's class (`ruby_class.runtime =`) — the
134
+ # class-level global that made two boots in one process share one
135
+ # name. The facade's modules close over THIS dispatcher instead, so the
136
+ # binding lives in the surface a boot installs, not on anything shared.
137
+ def self.bind_runtime(dispatcher)
138
+ Facade::Surface.install(dispatcher)
139
+ dispatcher
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,366 @@
1
+ require_relative "../naming"
2
+ require_relative "errors"
3
+ require_relative "query_interpreter"
4
+ require_relative "reaction_invocation"
5
+ require_relative "refusal_wording"
6
+ require_relative "value"
7
+ require_relative "../bluebook/expression/evaluator"
8
+
9
+ module Hecks
10
+ module Runtime
11
+ class PolicyInterpreter
12
+ attr_reader :registry
13
+
14
+ def initialize(registry, door:)
15
+ @registry = registry
16
+ @door = door
17
+ end
18
+
19
+ # `deliver` returns `nil` for a policy whose `where` did not hold —
20
+ # SILENTLY, the same as a policy `policies_for` never selected at all
21
+ # (an `event_qualifier` miss carries no reaction_log entry either) —
22
+ # so nothing is appended for it. A `for_each` policy answers an ARRAY
23
+ # (one record per matched row) rather than one record ; `Array(...)`
24
+ # is wrong here (it would explode a plain record Hash into its own
25
+ # key/value pairs), so the two shapes are told apart explicitly.
26
+ def react(event, domain)
27
+ policies_for(event).each do |policy, home_domain|
28
+ result = deliver(policy, event, home_domain)
29
+ next if result.nil?
30
+
31
+ (result.is_a?(Array) ? result : [result]).each { |record| @registry.reaction_log << record }
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ # SCANS EVERY LOADED BLUEBOOK, not just the emitting command's own —
38
+ # a policy commonly lives in the CONSUMER's bluebook, reacting
39
+ # passively to an event a different domain's aggregate emits (the
40
+ # corpus-standard shape : `world/conception/bluebook/domain_cell.bluebook`'s
41
+ # ConceiveOnAbsorption reacts `on "DomainAbsorbed"`, an event
42
+ # `body/organs/bluebook/gut.bluebook`'s Gut.Absorb emits — two
43
+ # different bluebooks, joined only by the event's NAME). Restricting
44
+ # the scan to the emitting domain's own bluebook (the previous
45
+ # shape) silently drops every cross-domain reaction : the policy is
46
+ # never even a candidate, no refusal, no log entry, nothing —
47
+ # confirmed against the corpus's own .behaviors fixtures, several of
48
+ # which encode this exact cross-bluebook cascade as their contract.
49
+ #
50
+ # Returns [policy, home_domain] pairs rather than bare policies —
51
+ # `deliver`/`deliver_for_each` fall back to the REACTING policy's
52
+ # OWN domain (not the emitting one) when a trigger or for_each route
53
+ # is bare, and that fallback has to travel with each match now that
54
+ # a single event can surface policies from several different homes.
55
+ def policies_for(event)
56
+ emitting = Naming.demodulise(event.aggregate)
57
+
58
+ @registry.bluebooks.each_value.flat_map do |bluebook|
59
+ matching = bluebook.policies.select do |policy|
60
+ policy.event_name == event.name &&
61
+ (policy.event_qualifier.nil? || policy.event_qualifier == emitting)
62
+ end
63
+ matching.map { |policy| [policy, bluebook.name] }
64
+ end
65
+ end
66
+
67
+ # THE GUARD, evaluated against the triggering event's own payload —
68
+ # a policy has no aggregate instance of its own to read state from,
69
+ # so `state` is empty and every bare name a `where` resolves comes
70
+ # from `attrs` (Expression::Resolver#fetch checks `attrs` before
71
+ # `state`, so this is exactly the shape `enforce_givens` already
72
+ # gives a command's own given, minus the settled-record half a
73
+ # policy simply has none of). Called from inside `deliver`'s own
74
+ # rescue-guarded body (both callers, below) — never guarded here —
75
+ # so an EvaluationError (an unresolvable field, a bad comparison) is
76
+ # caught the SAME way any other reaction defect is, not swallowed as
77
+ # though the policy had merely declined to fire.
78
+ def where_holds?(policy, event)
79
+ return true if policy.where.to_s.empty?
80
+
81
+ Bluebook::Expression::Evaluator.call(policy.where, {}, event.payload.transform_keys(&:to_sym))
82
+ end
83
+
84
+ def deliver(policy, event, domain)
85
+ # `record` ASSIGNED BEFORE ANY BRANCH THAT CAN RAISE, same reason
86
+ # `deliver_for_each`'s own header gives : both rescue clauses below
87
+ # call `.merge` on it, and a defect raised before it existed would
88
+ # be caught here only to raise a second, different NoMethodError
89
+ # trying to record the first one.
90
+ target = "#{policy.target_domain || domain}::#{policy.trigger_command}"
91
+ record = { policy: policy.name, on: event.name, trigger: target }
92
+
93
+ return deliver_for_each(policy, event, domain, target, record) unless policy.for_each.to_s.empty?
94
+ return nil unless where_holds?(policy, event)
95
+
96
+ if @door.reaction_depth_reached?
97
+ return record.merge(delivered: false,
98
+ reason: "reaction depth #{@door.max_reaction_depth} reached")
99
+ end
100
+
101
+ args = trigger_args(policy, event)
102
+ @door.reenter(target, **reaction_invocation(target, args, policy, event))
103
+ record.merge(delivered: true)
104
+ rescue *DOMAIN_REFUSALS => error
105
+ # The target refused — a fact about the domain, recorded and not
106
+ # fatal to the command that emitted the event.
107
+ record.merge(delivered: false, reason: error.message)
108
+ rescue StandardError => error
109
+ # A DEFECT, not a refusal — a NoMethodError in an interpreter, a
110
+ # NameError from a missing constant, a TypeError from a bad
111
+ # assumption : exactly the class of thing DOMAIN_REFUSALS
112
+ # (errors.rb, see the comment above that constant) deliberately
113
+ # excludes, and for the reason that comment gives at length —
114
+ # folding a crash into the same `delivered: false` shape as an
115
+ # ordinary refusal makes a broken runtime read as normal operation
116
+ # in the log. This clause does not reopen that hole: it is a
117
+ # SECOND, narrower rescue, tried only once the first one above has
118
+ # already declined to match, so a legitimate refusal still takes
119
+ # the branch above and a defect always takes this one.
120
+ #
121
+ # Catching it HERE is safe for a fact this method's caller cannot
122
+ # see from where it sits: by the time `react` runs, the command
123
+ # that EMITTED `event` has already succeeded and PERSISTED —
124
+ # `Dispatcher#dispatch` calls `@policies.react` only after its own
125
+ # `announced` events are already in hand. Letting this exception
126
+ # keep propagating would not undo that command (nothing here is
127
+ # transactional across aggregates) — it would only blow up the
128
+ # ORIGINAL caller's `dispatch` call for a failure that happened in
129
+ # a DIFFERENT command, one the caller never asked to run and has no
130
+ # way to compensate for. So the defect is recorded, distinguishably
131
+ # (`defect: true`, plus the error's own class — nothing here is
132
+ # allowed to read like an ordinary refusal), warned to STDERR so it
133
+ # is never silent, and left exactly where it happened for a human
134
+ # to find — never re-raised, and never swallowed either.
135
+ warn "[hecks] defect in reaction — policy #{policy.name} on #{event.name} " \
136
+ "firing #{target}: #{error.class}: #{error.message}"
137
+ record.merge(delivered: false, reason: error.message, defect: true, error_class: error.class.name)
138
+ end
139
+
140
+ # THE FAN-OUT — `policy.for_each` names a query ; this runs it
141
+ # against the triggering event's own payload (the same source
142
+ # `deliver`'s own ordinary path forwards to `trigger` wholesale) and
143
+ # fires `trigger` once per row, merging each row's own id into the
144
+ # forwarded payload under whichever key THE TARGET COMMAND ITSELF
145
+ # expects to be addressed by (`Behaviour::Command#addressing_key_for`
146
+ # — never a guessed, one-size mint: `Account.Freeze`, addressed by
147
+ # `account` because it is declared ON Account and self-references
148
+ # it, refused every dispatch for as long as this hardcoded
149
+ # `<aggregate>_id` instead — a real bug, found wiring `for_each`
150
+ # into a real domain for the first time, not a hypothetical). A
151
+ # refusal is recorded per row and the fan-out continues ; a crash
152
+ # resolving the QUERY ITSELF, or the TARGET COMMAND'S OWN inability
153
+ # to address this aggregate at all (`addressing_key_for` answering
154
+ # `nil` — a domain-authoring mistake, not a data problem), is a
155
+ # single top-level defect for the policy, the same shape `deliver`'s
156
+ # own outer rescue already gives every other reaction.
157
+ def deliver_for_each(policy, event, domain, target, record)
158
+ return nil unless where_holds?(policy, event)
159
+
160
+ query_domain, aggregate_name, query_name = policy.for_each_route(domain)
161
+ aggregate = resolve_query_aggregate(query_domain, aggregate_name, policy.for_each)
162
+ # THE QUERY READS THE EVENT, never the projection — `with:` says
163
+ # what the TRIGGER is given, and the fan-out's query is asking a
164
+ # different question (WHICH rows) in the event's own vocabulary.
165
+ query_args = for_each_query_args(aggregate.query(query_name), event)
166
+ rows = QueryInterpreter.new(@registry).call(query_domain, aggregate, query_name, query_args)
167
+ reference_key = addressing_key_for(target, aggregate_name)
168
+
169
+ Array(rows).map do |row|
170
+ deliver_for_each_row(target, record, trigger_args(policy, event, reference_key => row[:id]), row, policy, event)
171
+ end
172
+ rescue *DOMAIN_REFUSALS => error
173
+ record.merge(delivered: false, reason: error.message)
174
+ rescue StandardError => error
175
+ warn "[hecks] defect in reaction — policy #{policy.name} on #{event.name} " \
176
+ "resolving for_each #{policy.for_each}: #{error.class}: #{error.message}"
177
+ record.merge(delivered: false, reason: error.message, defect: true, error_class: error.class.name)
178
+ end
179
+
180
+ # THE EVENT'S OWN IDENTITY IS A FACT TOO, not only its payload. A
181
+ # for_each query commonly filters by the EMITTING record's own
182
+ # identity (`OpenForCustomer`'s own `reference:`, scoping by the
183
+ # very customer who was just suspended) — which used to arrive for
184
+ # free because LEGACY dispatch left the self-addressing key riding
185
+ # along in `event.payload` unfiltered. Routing separated from
186
+ # payload (`to:`/`with:`, what the facade's own bang-methods always
187
+ # use) correctly stopped carrying it there, which left this query
188
+ # silently seeing NEITHER the field it needs NOR any error saying
189
+ # why — an empty result read as "nothing to freeze" instead of "the
190
+ # customer" the whole reaction exists to catch.
191
+ #
192
+ # Merged in ONLY when the query declares an argument by that exact
193
+ # name AND the emitting aggregate's own identity is genuinely what
194
+ # that name means (`construct.identity_heads`) — the same guard
195
+ # `SagaInterpreter::Correlation#self_identified?` uses for the
196
+ # identical shape one call away, so an unrelated aggregate sharing
197
+ # an argument name by coincidence never gets misread as this one.
198
+ # Never overrides a value the payload already supplied.
199
+ def for_each_query_args(query, event)
200
+ args = event.payload.transform_keys(&:to_sym)
201
+ return args unless query
202
+
203
+ domain, bare_name = event.aggregate.to_s.split("::", 2)
204
+ construct = bare_name && @registry.bluebook(domain)&.aggregate(bare_name)
205
+ return args unless construct
206
+
207
+ heads = construct.identity_heads.map(&:to_s)
208
+ query.attributes.each do |attribute|
209
+ next if args.key?(attribute.name)
210
+ next unless heads.include?(attribute.name.to_s)
211
+
212
+ args[attribute.name] = event.id
213
+ end
214
+ args
215
+ end
216
+
217
+ # WHAT THE TRIGGER IS GIVEN. Undeclared, the event's whole payload
218
+ # forwards verbatim — the behaviour every policy had before `with:`
219
+ # existed, and still the right default for a trigger shaped like
220
+ # its event.
221
+ #
222
+ # Declared, it is the same reading a saga's own `dispatch ...,
223
+ # with:` gets (`SagaInterpreter#dispatch_args`): a Symbol names a
224
+ # field on the SOURCE below, anything else is a literal the policy
225
+ # supplies itself. A saga additionally resolves against its own
226
+ # memory and correlation key; a policy has neither — it holds
227
+ # nothing between events — so the source is the event, plus:
228
+ #
229
+ # `extra` is a FAN-OUT'S ROW KEY, merged into the source BEFORE the
230
+ # projection rather than after it. That is what lets a `for_each`
231
+ # policy name the row it is acting on — `with: { account: :account }`
232
+ # — and therefore what lets one send the row and NOTHING ELSE. A
233
+ # trigger needing only which record to act on is the ordinary case
234
+ # for a fan-out, and before this it could not be written: the whole
235
+ # event rode along, and the target had to declare every field of it
236
+ # whether it read them or not.
237
+ def trigger_args(policy, event, extra = {})
238
+ payload = event.payload.transform_keys(&:to_sym).merge(extra)
239
+ return payload unless ReactionInvocation.projection_declared?(policy)
240
+
241
+ payload = emitter_identity(event).merge(payload)
242
+
243
+ args = ReactionInvocation.resolve_mapping(
244
+ with_spec: policy.with_spec,
245
+ scopes: [["event payload and fan-out row", payload]],
246
+ label: "#{policy.name}'s trigger"
247
+ )
248
+
249
+ # THE RAW INPUTS `args` WAS RESOLVED FROM — same additive,
250
+ # Ruby-only shape SagaInterpreter#deliver_saga_dispatch's own
251
+ # saga_dispatch_log gets, for Properties.dispatch_binding_
252
+ # fidelity's own independent re-derivation of Policy#with_spec's
253
+ # 2-branch resolution (Symbol → payload lookup, anything else →
254
+ # literal — a policy holds no correlation and no memory, so
255
+ # `payload` — the merged event-payload-plus-fan-out-row source —
256
+ # is the WHOLE source, unlike a saga's own 4-branch one).
257
+ @registry.policy_dispatch_log << { policy: policy.name, on: event.name, payload: payload,
258
+ with_spec: policy.with_spec, args: args }
259
+ args
260
+ end
261
+
262
+ # THE EMITTING RECORD'S OWN IDENTITY IS A FACT A PROJECTION MAY READ
263
+ # — the same reasoning `for_each_query_args` gives one method up,
264
+ # extended from the fan-out's QUERY to the trigger's own `with:`.
265
+ # Routing separated from payload (`to:`/`with:`) stopped carrying
266
+ # the emitting aggregate's identity in the payload, which is right
267
+ # for the event (a KnightCaptured is a fact about a knight, not a
268
+ # re-statement of which game) but left a CROSS-aggregate reaction
269
+ # with no way to say which record to address: chess's Graveyard is
270
+ # one-per-game, fed by policy from every piece's own Captured
271
+ # event, and its burials had no way to name the game — the
272
+ # dispatcher fell through to the captured piece's `id` as the
273
+ # graveyard's identity and refused every one ("no Graveyard with
274
+ # label.value \"bb\""). Same-aggregate targets were already covered
275
+ # (`ReactionInvocation.source_receiver_for` lifts Event.id), so
276
+ # this is the OTHER aggregate's half of that.
277
+ #
278
+ # Offered under the emitting aggregate's own identity heads, only
279
+ # to an EXPLICIT projection (a legacy wholesale forward keeps its
280
+ # exact old payload), and never over a value the payload itself
281
+ # carries. `event.id` is the scalar the identity resolves to, so a
282
+ # projection naming it as the target's own identity head routes it
283
+ # as the receiver (`Identity.of` coerces a scalar against a
284
+ # single-field VO head), and one naming it as a declared attribute
285
+ # carries it as a fact. The build-time validator admits the same
286
+ # names (`BluebookBuilder.check_with_spec!`).
287
+ def emitter_identity(event)
288
+ return {} if event.id.nil? || event.id.to_s.empty?
289
+
290
+ domain, bare_name = event.aggregate.to_s.split("::", 2)
291
+ construct = bare_name && @registry.bluebook(domain)&.aggregate(bare_name)
292
+ return {} unless construct
293
+
294
+ construct.identity_heads.to_h { |head| [head.to_sym, event.id] }
295
+ end
296
+
297
+ # RESOLVES `target` ("Domain::Aggregate.Command", the same shape
298
+ # `deliver`'s own caller already built) back to its OWN declared
299
+ # command, then asks IT how it expects to be addressed by a row of
300
+ # `aggregate_name` — see `Behaviour::Command#addressing_key_for`'s
301
+ # own comment for the two shapes that answers. Raises (caught by
302
+ # `deliver_for_each`'s own outer `rescue StandardError`, the same
303
+ # "a defect, not a refusal" treatment `resolve_query_aggregate`'s
304
+ # own `UnknownVerb` already gets one level up) rather than
305
+ # silently falling back on a guess when the target command cannot
306
+ # be resolved, or genuinely cannot be addressed by this aggregate
307
+ # at all — either is a domain-authoring mistake worth surfacing
308
+ # loudly, not a row this fan-out simply skips.
309
+ def addressing_key_for(target, aggregate_name)
310
+ target_domain, target_aggregate_name, target_command_name = Naming.split_verb(target)
311
+ command = @registry.bluebook(target_domain)&.aggregate(target_aggregate_name)&.command(target_command_name)
312
+ raise UnknownVerb, "for_each's own trigger #{target.inspect} does not resolve to a declared command" unless command
313
+
314
+ key = command.addressing_key_for(aggregate_name)
315
+ return key if key
316
+
317
+ raise ArgumentError,
318
+ "#{target} cannot be addressed by a row of #{aggregate_name} — it declares no self-reference to " \
319
+ "#{aggregate_name} and no reference-typed attribute targeting it"
320
+ end
321
+
322
+ def deliver_for_each_row(target, record, args, row, policy, event)
323
+ row_record = record.merge(for_row: row[:id])
324
+
325
+ if @door.reaction_depth_reached?
326
+ return row_record.merge(delivered: false,
327
+ reason: "reaction depth #{@door.max_reaction_depth} reached")
328
+ end
329
+
330
+ # ALREADY MERGED, by `trigger_args` — the row key belongs in the
331
+ # source a `with:` projection reads FROM, not bolted onto its
332
+ # result, or a projection could never name the row it acts on.
333
+ @door.reenter(target, **reaction_invocation(target, args, policy, event))
334
+ row_record.merge(delivered: true)
335
+ rescue *DOMAIN_REFUSALS => error
336
+ row_record.merge(delivered: false, reason: error.message)
337
+ end
338
+
339
+ def reaction_invocation(target, args, policy, event)
340
+ ReactionInvocation.build(
341
+ registry: @registry,
342
+ verb: target,
343
+ projected: args,
344
+ explicit: ReactionInvocation.projection_declared?(policy),
345
+ source_receiver: { aggregate: event.aggregate, identity: event.id }
346
+ )
347
+ end
348
+
349
+ # `for_each`'s own QUERY route moved onto the Policy itself
350
+ # (Behaviour::Policy#for_each_route) — one reading the interpreter
351
+ # and the fuzzer's fan-out property both call, rather than the
352
+ # same split spelled twice. The reference-KEY the dispatch itself
353
+ # uses is `addressing_key_for`, above — a property of the TARGET
354
+ # COMMAND, not of the policy, so it lives on `Behaviour::Command`
355
+ # instead.
356
+
357
+ def resolve_query_aggregate(domain, aggregate_name, verb)
358
+ bluebook = @registry.bluebook(domain) ||
359
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_domain", domain: domain.inspect, verb: verb))
360
+ bluebook.aggregate(aggregate_name) ||
361
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_aggregate",
362
+ domain: domain, aggregate: aggregate_name.inspect))
363
+ end
364
+ end
365
+ end
366
+ end