hecks 0.2.0 → 1.0.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 (419) 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 +427 -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 +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -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 +145 -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 +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,286 @@
1
+ require_relative "../naming"
2
+ require_relative "../ports/query"
3
+ require_relative "../ports/query/ordering"
4
+ require_relative "../query_specification/field_path"
5
+ require_relative "../query_specification/common/comparison"
6
+ require_relative "errors"
7
+ require_relative "reference_hop"
8
+ require_relative "refusal_wording"
9
+ require_relative "tenant_scope"
10
+ require_relative "value"
11
+
12
+ module Hecks
13
+ module Runtime
14
+ class QueryInterpreter
15
+ attr_reader :registry
16
+
17
+ def initialize(registry)
18
+ @registry = registry
19
+ end
20
+
21
+ def call(domain, aggregate, query_name, args)
22
+ return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
23
+
24
+ declared = declared_query(aggregate, query_name)
25
+ args = normalize_args(aggregate, declared, args)
26
+ declared = TenantScope.apply(declared, args)
27
+ # AFTER TenantScope, so its synthetic clause is already present
28
+ # in `.wheres` and rides through as an ordinary LOCAL clause on
29
+ # the OUTER query. It does not reach the hop's own inner
30
+ # sub-query against the TARGET aggregate — a hop's target may
31
+ # not even declare the same tenant boundary, and propagating one
32
+ # aggregate's tenant scope onto an unrelated aggregate's own
33
+ # query is a real design question of its own, not answered here.
34
+ declared = ReferenceHop.apply(declared, args, registry: @registry, domain: domain, aggregate: aggregate)
35
+
36
+ repository = @registry.repository(domain, aggregate)
37
+ # `registry:` is threaded through so a `none_in_state` where-clause
38
+ # can look its target aggregate up — see
39
+ # QuerySpecification::Common::Comparison#none_in_state? for the
40
+ # comparator itself, and for why an ordinary aggregate-level
41
+ # Memory query needs the registry passed explicitly rather than
42
+ # closing over an instance variable.
43
+ if (native = Ports::Query.execute(repository, declared, args,
44
+ context: { domain: domain, aggregate: aggregate, registry: @registry }))
45
+ records = native
46
+ # `record.state.merge(id: record.id)` — id LAST, not first. See
47
+ # Instance#to_h's own comment: an aggregate free to declare its
48
+ # own attribute literally named `id` has that attribute's own
49
+ # wrapped value sitting in `record.state[:id]` already; merging
50
+ # it OVER a `{id:}.merge(state)` used to let it silently
51
+ # clobber the correct bare identity this row is supposed to
52
+ # carry.
53
+ # A QUERY ROW IS AN ANSWER, NOT A HANDLE. Mutating one edits
54
+ # nobody's state and silently disagrees with the store.
55
+ return Freezer.deep(records.map { |record| record.state.merge(id: record.id) })
56
+ end
57
+
58
+ Freezer.deep(interpret(repository.all, declared, args, domain: domain))
59
+ end
60
+
61
+ # The REFERENCE answer — this interpreter's own evaluation, never an
62
+ # adapter's native hook. The fuzzer's query oracle replays every
63
+ # generated ask through both paths and treats a difference as a
64
+ # finding: the differential gate the retired cross-runtime harness
65
+ # should always have been, aimed where the divergence actually
66
+ # lives — between the engines inside this one runtime.
67
+ #
68
+ # A hop clause is answered here by its OWN, deliberately naive
69
+ # walk (reference_where_holds?) — never Runtime::ReferenceHop's
70
+ # partition/fold/IN-clause. Sharing that algorithm would have made
71
+ # this oracle blind to exactly the code the hop feature adds: every
72
+ # PHASE of a shared fold would still get diffed against the native
73
+ # adapters, but the fold itself — the empty candidate set, a
74
+ # duplicate id, a dangling reference, a chain's inside-out
75
+ # resolution order — would only ever be compared against itself.
76
+ def reference_call(domain, aggregate, query_name, args)
77
+ return entity_rows(domain, aggregate, query_name, args) if query_name.include?(".")
78
+
79
+ declared = declared_query(aggregate, query_name)
80
+ args = normalize_args(aggregate, declared, args)
81
+ declared = TenantScope.apply(declared, args)
82
+ reference_interpret(@registry.repository(domain, aggregate).all, declared, args,
83
+ domain: domain, shape: aggregate)
84
+ end
85
+
86
+ private
87
+
88
+ def declared_query(aggregate, query_name)
89
+ aggregate.query(query_name) ||
90
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "no_query",
91
+ aggregate: aggregate.hecks_name, query: query_name.inspect))
92
+ end
93
+
94
+ def interpret(records, declared, args, domain: nil)
95
+ matched = records.select { |r| declared.wheres.all? { |w| where_holds?(w, r, args, domain: domain) } }
96
+ ordered = ordered(matched, declared.order_by, declared.null_semantics)
97
+ # OFFSET FIRST, THEN LIMIT — the order SQL means by `LIMIT n
98
+ # OFFSET m`, and the order Ports::Query::InMemory#execute already
99
+ # applies (see that file's own comment). This interpreter used to
100
+ # never read declared.offset at all — offset silently vanished for
101
+ # any query answered here, not just come out reversed.
102
+ skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
103
+ capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
104
+
105
+ # id LAST — see the native-path comment above; same clobbering
106
+ # risk for the in-memory reference interpreter's own rows.
107
+ capped.map { |r| r.state.merge(id: r.id) }
108
+ end
109
+
110
+ # `interpret`'s own twin, for reference_call alone — same
111
+ # select/order/limit shape, but a clause that hops through a
112
+ # reference is answered by reference_where_holds? instead of the
113
+ # plain FieldPath.dig(record, field) `where_holds?` uses (which
114
+ # has no concept of a reference at all — it would just read the
115
+ # raw id straight off the record and compare THAT).
116
+ def reference_interpret(records, declared, args, domain:, shape:)
117
+ matched = records.select { |r|
118
+ declared.wheres.all? { |w|
119
+ reference_where_holds?(w, r, args, domain: domain, shape: shape)
120
+ }
121
+ }
122
+ ordered = ordered(matched, declared.order_by, declared.null_semantics)
123
+ # OFFSET FIRST, THEN LIMIT — same fix, same reasoning, as
124
+ # #interpret's own rows above.
125
+ skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
126
+ capped = declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
127
+
128
+ # id LAST — same reasoning, same fix, as interpret's own rows.
129
+ capped.map { |r| r.state.merge(id: r.id) }
130
+ end
131
+
132
+ # THE NAIVE READING OF A HOP: not a fold, not an id set — for
133
+ # each candidate row, walk the reference by hand and dig the
134
+ # field out of whatever it actually points at. A nil reference,
135
+ # or one that resolves to nothing (a dangling id), makes the
136
+ # WHOLE clause false outright, whatever the comparator — "points
137
+ # at a client that is not active" is false for a proposal with no
138
+ # client at all, the same way it is false for one whose client
139
+ # really is active; falling through to holds?(clause, nil, args)
140
+ # instead would answer `ne` wrong (nil != "active" is true).
141
+ def reference_where_holds?(clause, record, args, domain:, shape:)
142
+ step = QuerySpecification::HopPath.next_hop(clause.field, shape.attributes)
143
+ return where_holds?(clause, record, args) unless step
144
+
145
+ hop, rest = step
146
+ inner = QuerySpecification::Common::WhereClause.new(field: rest, op: clause.op, value: clause.value)
147
+ held = record[hop.attribute.name]
148
+ reference_ids = hop.attribute.list? ? Array(held) : [held]
149
+
150
+ reference_ids.compact.any? do |reference_id|
151
+ target_record = @registry.repository(domain, hop.target).find(reference_id)
152
+ next false unless target_record
153
+
154
+ reference_where_holds?(inner, target_record, args, domain: domain, shape: hop.target)
155
+ end
156
+ end
157
+
158
+ def entity_rows(domain, aggregate, dotted, args)
159
+ entity_name, query_name = Naming.split_dotted(dotted)
160
+ entity = aggregate.entities.find { |piece| piece.hecks_name == entity_name } ||
161
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_unknown",
162
+ aggregate: aggregate.hecks_name, entity: entity_name.inspect))
163
+ declared = entity.query(query_name) ||
164
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_query_missing",
165
+ entity: entity_name, query: query_name.inspect))
166
+ declared = TenantScope.apply(declared, args)
167
+ list_attr = aggregate.attributes.find { |a| a.list? && a.type.to_s == entity_name } ||
168
+ raise(UnknownVerb, RefusalWording.render("UnknownVerb", "entity_holds_no_list",
169
+ aggregate: aggregate.hecks_name, entity: entity_name))
170
+
171
+ parent_key = Naming.reference_key(aggregate.hecks_name)
172
+ rows = @registry.repository(domain, aggregate).all.flat_map do |record|
173
+ Array(record[list_attr.name])
174
+ .select { |el| declared.wheres.all? { |w| element_where_holds?(w, el, args) } }
175
+ .map { |el| { parent_key => record.id }.merge(el) }
176
+ end
177
+
178
+ ordered = ordered_elements(rows, declared.order_by, declared.null_semantics,
179
+ 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
182
+ # for entity/sub-list queries, so a declared offset here silently
183
+ # vanished for every entity query, not merely one path among
184
+ # several.
185
+ skipped = declared.offset ? ordered.drop(resolve_query_value(declared.offset.value, args).to_i) : ordered
186
+ declared.limit ? skipped.first(resolve_query_value(declared.limit.value, args).to_i) : skipped
187
+ end
188
+
189
+ # FieldPath.dig, not a raw `element[clause.field.to_sym]` — an
190
+ # entity sub-list row is a plain hash merged from stored state
191
+ # (mixed string/symbol keys depending on adapter, per `#cell`'s own
192
+ # comment below), and a dotted `where` (`where "price.cents" < 100`)
193
+ # needs the same segment-by-segment walk every other query path
194
+ # already gets. Reading only the symbol spelling of the WHOLE
195
+ # dotted string as one key always missed — `element[:"price.cents"]`
196
+ # is never a real key — so a dotted where on an entity query
197
+ # silently matched nothing, on the only engine entity queries have.
198
+ def element_where_holds?(clause, element, args)
199
+ holds?(clause, QuerySpecification::FieldPath.dig(element, clause.field), args)
200
+ end
201
+
202
+ # A row's own key, however the store spells it. A sub-list row is a plain hash
203
+ # merged from stored state, so its keys arrive as strings from one adapter and
204
+ # symbols from another — and reading only one spelling gave every row the SAME
205
+ # identity, which is a tie, which is the exact nondeterminism this tier exists
206
+ # to remove. It rides `comparable` for the same reason a where-clause does : an
207
+ # identity is a value object, and `to_s` on one is an OBJECT ADDRESS — a sort key
208
+ # that differs run to run, which is worse than the store order it replaced.
209
+ # `key?` decides which spelling answers, never `||` — a row whose
210
+ # value is a genuinely-held `false` must not fall through to the
211
+ # other spelling (usually absent) and land on `nil`.
212
+ def cell(row, key)
213
+ sym = key.to_sym
214
+ row.key?(sym) ? row[sym] : row[key.to_s]
215
+ end
216
+
217
+ # A sub-list row is identified by its PARENT and then its own key : two
218
+ # entities under different parents can share a sequence, so the parent has
219
+ # to lead or the tie is not broken at all.
220
+ #
221
+ # EVERY KEY THE PIECE IS KNOWN BY, in declaration order, for the same
222
+ # reason the parent leads: a part that ties is a part that breaks no tie.
223
+ # This took `identified_by`, which is the SINGLE head and is nil the
224
+ # moment an identity has two parts — and `cell(row, nil)` calls
225
+ # `nil.to_sym`, so a query against a composite piece did not sort wrongly,
226
+ # it raised. A piece known by one key sorts exactly as it did.
227
+ def ordered_elements(rows, order_by, null_semantics, parent_key, entity_keys)
228
+ field = order_by&.field
229
+ Ports::Query::Ordering.apply(
230
+ rows, order_by, null_semantics,
231
+ identity: lambda { |row|
232
+ [row[parent_key].to_s, *Array(entity_keys).map { |key| comparable(cell(row, key)) }]
233
+ }
234
+ ) { |row| comparable(QuerySpecification::FieldPath.dig(row, field)) }
235
+ end
236
+
237
+ def where_holds?(clause, record, args, domain: nil)
238
+ holds?(clause, QuerySpecification::FieldPath.dig(record, clause.field), args, record: record, domain: domain)
239
+ end
240
+
241
+ # The comparator table itself lives in
242
+ # QuerySpecification::Common::Comparison. This method and
243
+ # Ports::Query::InMemory#holds? used to carry a copy each and the
244
+ # two drifted — `none_in_state` reached only one of them, and
245
+ # `comparable` disagreed about value objects with two numeric
246
+ # members. What stays here is how a value is REACHED for this
247
+ # path: the registry is instance state rather than an argument.
248
+ def holds?(clause, held, args, record: nil, domain: nil)
249
+ QuerySpecification::Common::Comparison.holds?(
250
+ clause.op, comparable(held), comparable(resolve_query_value(clause.value, args)), registry: @registry
251
+ )
252
+ end
253
+
254
+ def resolve_query_value(value, args)
255
+ value.is_a?(Symbol) ? args[value] : value
256
+ end
257
+
258
+ def normalize_args(aggregate, declared, args)
259
+ declared.attributes.each_with_object(args.dup) do |attribute, normalized|
260
+ next unless normalized.key?(attribute.name)
261
+
262
+ normalized[attribute.name] = Value.for_attribute(aggregate, attribute, normalized[attribute.name])
263
+ end
264
+ end
265
+
266
+ def comparable(value) = QuerySpecification::Common::Comparison.comparable(value)
267
+
268
+ # FieldPath.dig, not a raw `record[field]` — `record` is an Instance
269
+ # here, and a dotted order_by (`order_by "price.cents"`) is a
270
+ # single symbol key (`:"price.cents"`) that never matches anything
271
+ # `Instance#[]` actually holds, so a dotted order_by silently sorted
272
+ # by all-nil (the identity tier alone deciding every tie) on this,
273
+ # the reference/no-native-hook engine — the same bug already fixed
274
+ # for `where` (see `where_holds?` above) and for entity rows (see
275
+ # `ordered_elements` below), just not yet for this, the aggregate-
276
+ # level order_by.
277
+ def ordered(records, order_by, null_semantics = nil)
278
+ field = order_by&.field
279
+ Ports::Query::Ordering.apply(records, order_by, null_semantics,
280
+ identity: ->(record) { record.id.to_s }) { |record|
281
+ comparable(QuerySpecification::FieldPath.dig(record, field))
282
+ }
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,253 @@
1
+ require_relative "../naming"
2
+ require_relative "errors"
3
+ require_relative "identity"
4
+ require_relative "value"
5
+
6
+ module Hecks
7
+ module Runtime
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.
13
+ module ReactionInvocation
14
+ Target = Struct.new(:aggregate, :entities, :command, keyword_init: true)
15
+ # :facts, not :values — Struct.new already defines #values (every
16
+ # member's own value, in order); naming this field :values silently
17
+ # shadowed that with the single Hash it actually holds.
18
+ Scope = Struct.new(:name, :facts, keyword_init: true)
19
+
20
+ module_function
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.
26
+ def projection_declared?(declaration)
27
+ if declaration.instance_variable_defined?(:@projection_declared)
28
+ declaration.instance_variable_get(:@projection_declared)
29
+ else
30
+ !declaration.with_spec.to_a.empty?
31
+ end
32
+ end
33
+
34
+ # A reaction's source names resolve lexically, not globally. Policies
35
+ # supply one event/row scope. Process managers supply current event then
36
+ # opening-event memory, while correlation is an explicit binding ahead
37
+ # of both. Missing names are refused here rather than materialized as nil
38
+ # and accidentally presented as target command facts.
39
+ def resolve_mapping(with_spec:, scopes:, bindings: {}, label: "reaction")
40
+ normalized_bindings = bindings.transform_keys(&:to_sym)
41
+ normalized_scopes = scopes.map do |scope|
42
+ scope = Scope.new(name: scope.first, facts: scope.last) unless scope.is_a?(Scope)
43
+ Scope.new(name: scope.name.to_s, facts: scope.facts.transform_keys(&:to_sym))
44
+ end
45
+
46
+ with_spec.to_h do |key, source|
47
+ value = if !source.is_a?(Symbol)
48
+ source
49
+ elsif normalized_bindings.key?(source)
50
+ normalized_bindings.fetch(source)
51
+ else
52
+ visible = normalized_scopes.find { |scope| scope.facts.key?(source) }
53
+ unless visible
54
+ names = normalized_scopes.map(&:name).join(" then ")
55
+ # WHAT IS VISIBLE, NAMED. A refusal that only says which name is
56
+ # missing sent a modeler guessing field after field
57
+ # (`number`, `reference`…) at a fan-out row that is addressed
58
+ # by ONE key — `account`, the lowercase aggregate — which
59
+ # nothing else in the domain spells out. The names each scope
60
+ # actually offers are the whole diagnosis; the refusal now
61
+ # lists them, scope by scope.
62
+ offered = normalized_scopes.map { |scope| "#{scope.name}: #{scope.facts.keys.sort.join(', ')}" }.join("; ")
63
+ raise UnknownArgument,
64
+ "#{label}'s with: reads :#{source}, which is not visible in #{names} (visible — #{offered})"
65
+ end
66
+ visible.facts.fetch(source)
67
+ end
68
+
69
+ [key.to_sym, Value.materialize(value)]
70
+ end
71
+ end
72
+
73
+ # A reaction without an explicit projection retains wholesale legacy
74
+ # forwarding. Choosing `with:` opts into the strict envelope: only target
75
+ # command attributes enter `with:`, while identities become `to:`.
76
+ def build(registry:, verb:, projected:, explicit:, passthrough: [], source_receiver: nil)
77
+ args = projected.transform_keys(&:to_sym)
78
+ unless explicit
79
+ return args unless source_receiver
80
+
81
+ # Compatibility calls still belong to Dispatcher when the target is
82
+ # absent: it owns the canonical UnknownVerb wording. Resolution here
83
+ # is only an optional opportunity to lift same-aggregate Event.id.
84
+ begin
85
+ target = resolve_target(registry, verb)
86
+ rescue UnknownVerb
87
+ return args
88
+ end
89
+ inherited_receiver = source_receiver_for(target, source_receiver)
90
+ return inherited_receiver ? args.merge(to: inherited_receiver) : args
91
+ end
92
+
93
+ target = resolve_target(registry, verb)
94
+ inherited_receiver = source_receiver_for(target, source_receiver)
95
+ facts = command_facts(target.command, args)
96
+ consumed = facts.keys
97
+
98
+ if target.entities.empty? && target.command.creates?
99
+ refuse_unconsumed!(target.command, args, consumed, passthrough)
100
+ return { with: facts }
101
+ end
102
+
103
+ aggregate_identity, aggregate_keys = identity_for(
104
+ target.aggregate,
105
+ args,
106
+ aliases: aggregate_aliases(target),
107
+ value_owner: target.aggregate
108
+ )
109
+ aggregate_identity ||= inherited_receiver
110
+ require_identity!(verb, target.aggregate, aggregate_identity)
111
+ consumed.concat(aggregate_keys)
112
+
113
+ entity_identities = target.entities.map do |entity|
114
+ identity, keys = identity_for(
115
+ entity,
116
+ args,
117
+ aliases: [Naming.reference_key(entity.hecks_name)],
118
+ value_owner: target.aggregate
119
+ )
120
+ require_identity!(verb, entity, identity)
121
+ consumed.concat(keys)
122
+ identity
123
+ end
124
+
125
+ refuse_unconsumed!(target.command, args, consumed, passthrough)
126
+
127
+ route = if entity_identities.empty?
128
+ aggregate_identity
129
+ else
130
+ { aggregate: aggregate_identity, entities: entity_identities }
131
+ end
132
+ { to: route, with: facts }
133
+ end
134
+
135
+ def resolve_target(registry, verb)
136
+ domain, aggregate_name, command_path = Naming.split_verb(verb)
137
+ raise UnknownVerb, "reaction target #{verb.inspect} is not a qualified command" unless command_path
138
+
139
+ bluebook = registry.bluebook(domain)
140
+ aggregate = bluebook&.aggregate(aggregate_name)
141
+ raise UnknownVerb, "reaction target #{verb.inspect} does not resolve to an aggregate" unless aggregate
142
+
143
+ *entity_names, command_name = command_path.split(".")
144
+ owner = aggregate
145
+ entities = entity_names.map do |entity_name|
146
+ entity = owner.entities.find { |candidate| candidate.hecks_name == entity_name }
147
+ raise UnknownVerb, "reaction target #{verb.inspect} does not resolve entity #{entity_name.inspect}" unless entity
148
+
149
+ owner = entity
150
+ entity
151
+ end
152
+ command = owner.command(command_name)
153
+ raise UnknownVerb, "reaction target #{verb.inspect} does not resolve to a command" unless command
154
+
155
+ Target.new(aggregate: aggregate, entities: entities, command: command)
156
+ end
157
+ private_class_method :resolve_target
158
+
159
+ def command_facts(command, args)
160
+ declared = command.attributes.map { |attribute| attribute.name.to_sym }
161
+ args.slice(*declared)
162
+ end
163
+ private_class_method :command_facts
164
+
165
+ def aggregate_aliases(target)
166
+ aliases = [:aggregate, Naming.reference_key(target.aggregate.name)]
167
+ aliases.unshift(target.command.addressing_key_for(target.aggregate.name)) if target.entities.empty?
168
+ aliases.compact.uniq
169
+ end
170
+ private_class_method :aggregate_aliases
171
+
172
+ # Event.id names the aggregate that emitted the event. It can therefore
173
+ # supply only the receiver of a non-creating command on that SAME root;
174
+ # it cannot address another aggregate, invent an entity identity, or turn
175
+ # a creation into a mutation. An explicit projected receiver is resolved
176
+ # first and remains authoritative.
177
+ def source_receiver_for(target, source_receiver)
178
+ return nil unless source_receiver
179
+ return nil unless target.entities.empty?
180
+ return nil if target.command.creates?
181
+
182
+ source = source_receiver.transform_keys(&:to_sym)
183
+ source_aggregate = source[:aggregate].to_s
184
+ same_aggregate = source_aggregate == if source_aggregate.include?("::")
185
+ target.aggregate.hecks_fqn
186
+ else
187
+ target.aggregate.hecks_name
188
+ end
189
+ return nil unless same_aggregate
190
+
191
+ identity = source[:identity]
192
+ identity.to_s unless identity.nil? || identity.to_s.empty?
193
+ end
194
+ private_class_method :source_receiver_for
195
+
196
+ def identity_for(construct, args, aliases:, value_owner:)
197
+ identity = Identity.of(construct, args, value_owner: value_owner)
198
+ return [identity, construct.identity_heads] if identity
199
+
200
+ aliases.each do |key|
201
+ key = key.to_sym
202
+ next unless args.key?(key)
203
+
204
+ identity = identity_from_alias(construct, args[key], value_owner)
205
+ return [identity, [key]] if identity
206
+ end
207
+
208
+ return [args[:id].to_s, [:id]] if args.key?(:id) && !args[:id].to_s.empty?
209
+
210
+ [nil, []]
211
+ end
212
+ private_class_method :identity_for
213
+
214
+ def identity_from_alias(construct, held, value_owner)
215
+ materialized = Value.materialize(held)
216
+ if materialized.is_a?(Hash)
217
+ keyed = materialized.transform_keys(&:to_sym)
218
+ nested = Identity.of(construct, keyed, value_owner: value_owner)
219
+ return nested if nested
220
+
221
+ if construct.identity_paths.one?
222
+ scalar = Identity.scalar(construct.identity_paths.first, keyed)
223
+ return scalar.to_s unless scalar.nil? || scalar.to_s.empty?
224
+ end
225
+
226
+ return nil
227
+ end
228
+
229
+ materialized.to_s unless materialized.nil? || materialized.to_s.empty?
230
+ end
231
+ private_class_method :identity_from_alias
232
+
233
+ def require_identity!(verb, construct, identity)
234
+ return if identity
235
+
236
+ raise TypeMismatch,
237
+ "reaction target #{verb} needs #{construct.hecks_name}'s receiver identity outside command facts"
238
+ end
239
+ private_class_method :require_identity!
240
+
241
+ def refuse_unconsumed!(command, args, consumed, passthrough)
242
+ allowed = consumed.map(&:to_sym) + Array(passthrough).map(&:to_sym)
243
+ unknown = args.keys - allowed
244
+ return if unknown.empty?
245
+
246
+ raise UnknownArgument,
247
+ "#{command.hecks_name} reaction projection contains neither receiver identity nor declared command facts: " \
248
+ "#{unknown.sort.join(', ')}"
249
+ end
250
+ private_class_method :refuse_unconsumed!
251
+ end
252
+ end
253
+ end