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,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
@@ -0,0 +1,210 @@
1
+ require_relative "interpreting"
2
+ require_relative "command_interpreter/argument_gate"
3
+ require_relative "event"
4
+ require_relative "value"
5
+ require_relative "../naming"
6
+
7
+ module Hecks
8
+ module Runtime
9
+ # The dispatch pipeline for a port operation — called by an adapter living
10
+ # outside the bluebook entirely, never by the domain itself. Deliberately
11
+ # a trimmed CommandInterpreter: the same payload gate and coercion
12
+ # (ArgumentGate, Interpreting#normalize_args), but no `given`, no
13
+ # `mutations`, no lifecycle, no save. A port operation does not act on an
14
+ # aggregate instance — it translates an external call into an event in
15
+ # this domain's own vocabulary, and whatever mutation follows happens
16
+ # wherever a `policy` reacts to that event, exactly as it would for any
17
+ # command-emitted one.
18
+ class PortOperationInterpreter
19
+ include Interpreting
20
+ include CommandInterpreter::ArgumentGate
21
+
22
+ Context = Struct.new(:domain, :aggregate, :operation, :args, :route, :instance, :result)
23
+
24
+ DISPATCH_ORDER = %i[
25
+ refuse_unknown_arguments refuse_absent_arguments normalize_args resolve_references resolve_route emit
26
+ ].freeze
27
+
28
+ def initialize(registry, rules:)
29
+ @registry = registry
30
+ @rules = rules
31
+ end
32
+
33
+ def call(domain, aggregate, operation, args, route:)
34
+ ctx = Context.new(domain, aggregate, operation, args)
35
+ ctx.route = route
36
+ run_dispatch_order(DISPATCH_ORDER, ctx)
37
+ ctx.result
38
+ end
39
+
40
+ private
41
+
42
+ def step_refuse_unknown_arguments(ctx)
43
+ step(:refuse_unknown_arguments) { refuse_unknown_arguments(ctx.domain, ctx.aggregate, ctx.operation, ctx.args) }
44
+ end
45
+
46
+ def step_refuse_absent_arguments(ctx)
47
+ step(:refuse_absent_arguments) { refuse_absent_arguments(ctx.operation, ctx.args, aggregate: ctx.aggregate) }
48
+ end
49
+
50
+ def step_normalize_args(ctx)
51
+ ctx.args = step(:normalize_args) { normalize_args(ctx.aggregate, ctx.operation, ctx.args) }
52
+ end
53
+
54
+ def step_resolve_references(ctx)
55
+ step(:resolve_references) { @rules.resolve_references(ctx.domain, ctx.operation, ctx.args) }
56
+ end
57
+
58
+ def step_resolve_route(ctx)
59
+ ctx.instance = step(:resolve_route) do
60
+ @registry.repository(ctx.domain, ctx.aggregate).find(ctx.route.aggregate) ||
61
+ raise(NotFound, "#{ctx.aggregate.hecks_name} #{ctx.route.aggregate.inspect} does not exist")
62
+ end
63
+ end
64
+
65
+ def step_emit(ctx)
66
+ ctx.result = step(:emit) { ctx.operation.outbound? ? ask(ctx) : emit(ctx) }
67
+ end
68
+
69
+ # THE DOMAIN CALLING OUT, AND BOTH ENDINGS RECORDED.
70
+ #
71
+ # The adapter is found the same way every other port's is — by name,
72
+ # across whatever adapters this boot loaded — so an `asks` is bound by
73
+ # an adapter declaring `port "IssueTracker"` and nothing new to learn.
74
+ #
75
+ # EVERY FAILURE IS AN ANSWER. A raise from the far side of a boundary is
76
+ # not an exception in this domain's terms, it is the outside saying no,
77
+ # and the chapter already named the word for that. So the rescue is
78
+ # deliberately wide: a timeout, a bad credential, an adapter that does
79
+ # not exist, a nil where a number was wanted — all of them become the
80
+ # `refuses` event, carrying what was said. A policy reacts to it, a
81
+ # retry counter reads it, and nothing has to catch anything.
82
+ #
83
+ # AN ASK IS HANDED THE RECORD IT IS ABOUT.
84
+ #
85
+ # An INBOUND operation deliberately cannot read state — it is the
86
+ # anti-corruption boundary, translating a fact from outside, and letting
87
+ # it read the aggregate would make it a second place rules live. That
88
+ # rule was written for that direction and does not survive the crossing.
89
+ #
90
+ # An outbound one almost always needs the record. `asks "File"` names
91
+ # `reference_to Ticket` and the adapter needs the ticket's repository,
92
+ # title and body — which are ON the ticket, and which the policy that
93
+ # triggered this cannot supply because a command's event payload is its
94
+ # ARGUMENTS, not its state. Without this, every ask would have to have
95
+ # its data re-passed through the command that fired it, so the same text
96
+ # would live in two places and could differ.
97
+ #
98
+ # ARGUMENTS WIN over state, because an argument is what THIS call said
99
+ # and state is what the record happens to hold.
100
+ def ask(ctx)
101
+ payload = held_state(ctx).merge(materialise(ctx.args))
102
+ answer = adapter_for(ctx).public_send(Naming.snake(ctx.operation.hecks_name), **payload)
103
+ announce(ctx, ctx.operation.answers, ctx.args.merge(spread(answer)))
104
+ rescue StandardError => e
105
+ announce(ctx, ctx.operation.refuses, ctx.args.merge(refusal: { value: "#{e.class}: #{e.message}" }))
106
+ end
107
+
108
+ # THE ANSWER IS SPREAD, NOT NESTED — and that is what makes the loop
109
+ # close. A policy re-enters its target with the event payload VERBATIM;
110
+ # it cannot reach inside a key. So an answer tucked under `answered:`
111
+ # can be read by a human and by nothing else, and the command that
112
+ # should record the issue number never gets one.
113
+ #
114
+ # Spread, the adapter's own keys ARE the arguments of whatever command
115
+ # reacts to the answering event. Which is a real contract on the adapter
116
+ # — it must return what that command takes, in the shape the runtime
117
+ # coerces (`{ number: { value: 43 } }`, not `43`) — and naming it here
118
+ # is cheaper than a mapping layer nobody could see into.
119
+ #
120
+ # A non-Hash answer keeps the old shape: a port that returns a URL
121
+ # string has nothing to spread, and `answered:` is the honest word for
122
+ # a single unnamed value.
123
+ def spread(answer)
124
+ return { answered: answer } unless answer.is_a?(Hash)
125
+
126
+ answer.to_h { |key, value| [key.to_sym, deep_symbolize(value)] }
127
+ end
128
+
129
+ def deep_symbolize(value)
130
+ case value
131
+ when Hash then value.to_h { |k, v| [k.to_sym, deep_symbolize(v)] }
132
+ when Array then value.map { |element| deep_symbolize(element) }
133
+ else value
134
+ end
135
+ end
136
+
137
+ # THE RECORD, IF THERE IS ONE. A record that does not exist yet is not
138
+ # an error here — the ask still goes, carrying only its arguments, and
139
+ # whatever the adapter makes of that is its own business. Refusing
140
+ # would put a second existence check behind the one `resolve_references`
141
+ # already performed.
142
+ def held_state(ctx)
143
+ ctx.instance ? Value.materialize(ctx.instance.state) : {}
144
+ rescue StandardError
145
+ {}
146
+ end
147
+
148
+ # THE PORT THIS OPERATION BELONGS TO, found by asking the aggregate
149
+ # rather than threading it through the call — the dispatcher already
150
+ # resolved it once to get here, and a second parameter carried purely so
151
+ # this method can read it would be a parameter every OTHER step ignores.
152
+ def port_name_for(ctx)
153
+ owning = ctx.aggregate.ports.find { |port| port.operations.any? { |op| op.equal?(ctx.operation) } }
154
+ owning&.name or raise WiringError,
155
+ "#{ctx.operation.hecks_name} belongs to no port on #{ctx.aggregate.hecks_name}"
156
+ end
157
+
158
+ def adapter_for(ctx)
159
+ name = port_name_for(ctx)
160
+ implementations = @registry.adapters.values.select { |adapter| adapter.port == name }
161
+
162
+ case implementations.size
163
+ when 1 then Adapters.const_get(implementations.first.name).new
164
+ when 0 then raise WiringError, "no adapter implements the #{name} port — nothing can answer #{ctx.operation.hecks_name}"
165
+ else raise WiringError,
166
+ "#{implementations.size} adapters implement the #{name} port " \
167
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
168
+ end
169
+ end
170
+
171
+ # A Value never crosses the boundary — an adapter is somebody else's
172
+ # code and should be handed plain data, the same reasoning `JsonDoor`
173
+ # gives for materialising before it hands anything to an HTTP caller.
174
+ def materialise(args) = Value.materialize(args)
175
+
176
+ def announce(ctx, event_name, payload)
177
+ event = Event.new(
178
+ name: event_name,
179
+ aggregate: "#{ctx.domain}::#{ctx.aggregate.hecks_name}",
180
+ id: ctx.route.aggregate,
181
+ payload: payload,
182
+ occurred_at: Time.now.utc.iso8601
183
+ )
184
+ @registry.event_log << event
185
+ [event]
186
+ end
187
+
188
+ # THE ONE PLACE THIS DIFFERS FROM CommandRules::Emission — there is no
189
+ # mutated instance to read an id off, because nothing was hydrated or
190
+ # saved. The record this event is ABOUT is named by whichever attribute
191
+ # is a reference to the owning aggregate (PortOperationBuilder#build
192
+ # already refused to build an operation with none), so its coerced
193
+ # value — already a plain id, never an object, per
194
+ # Value::Coercion#refuse_object_reference — is what stamps the event.
195
+ def emit(ctx)
196
+ ctx.operation.emits.map do |event_name|
197
+ event = Event.new(
198
+ name: event_name,
199
+ aggregate: "#{ctx.domain}::#{ctx.aggregate.hecks_name}",
200
+ id: ctx.route.aggregate,
201
+ payload: ctx.args,
202
+ occurred_at: Time.now.utc.iso8601
203
+ )
204
+ @registry.event_log << event.emit!
205
+ event
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end