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,157 @@
1
+ require_relative "../fqn"
2
+ require_relative "../facade/handle"
3
+
4
+ module Hecks
5
+ class Router
6
+ # Installs optional Ruby syntax over an already-loaded router. FQN
7
+ # resolution stays in Router; this object only adapts it to constants and
8
+ # method calls.
9
+ class NamespaceInstaller
10
+ class OptionsProxy
11
+ def initialize(router:, realm:, domain:, aggregate:, options:)
12
+ unknown = options.keys - [:version]
13
+ raise ArgumentError, "unknown router options: #{unknown.join(', ')}" unless unknown.empty?
14
+
15
+ @router, @realm, @domain, @aggregate = router, realm, domain, aggregate
16
+ @version = options[:version]&.to_s
17
+ end
18
+
19
+ def method_missing(verb, **args)
20
+ fqn = fqn_for(verb)
21
+ fqn.command? ? @router.dispatch(fqn.to_s, **args) : @router.query(fqn.to_s, **args)
22
+ end
23
+
24
+ def respond_to_missing?(verb, include_private = false)
25
+ Fqn.command_name?(verb) || Fqn.query_name?(verb) || super
26
+ end
27
+
28
+ private
29
+
30
+ def fqn_for(verb)
31
+ if Fqn.command_name?(verb)
32
+ Fqn.command(realm: @realm, domain: @domain, version: @version, aggregate: @aggregate, command: verb)
33
+ elsif Fqn.query_name?(verb)
34
+ Fqn.query(realm: @realm, domain: @domain, version: @version, aggregate: @aggregate, query: verb)
35
+ else
36
+ raise NoMethodError, "#{verb.inspect} is not a Bluebook command or query"
37
+ end
38
+ end
39
+ end
40
+
41
+ def initialize(router)
42
+ @router = router
43
+ end
44
+
45
+ def install!
46
+ current_entries.each { |entry| install_namespace_entry(entry) }
47
+ install_shortcuts!
48
+ install_aggregate_doors!
49
+ self
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :router
55
+
56
+ def current_entries = router.available.reject { |entry| entry.fqn.version }
57
+
58
+ def install_namespace_entry(entry)
59
+ target = namespace_for(entry.fqn.realm, entry.fqn.domain, entry.fqn.aggregate)
60
+ address = entry.fqn.to_s
61
+ active_router = router
62
+ target.define_singleton_method(:options) do |**options|
63
+ OptionsProxy.new(router: active_router, realm: entry.fqn.realm, domain: entry.fqn.domain,
64
+ aggregate: entry.fqn.aggregate, options: options)
65
+ end
66
+ if entry.command?
67
+ target.define_singleton_method(entry.fqn.verb) { |**args| active_router.dispatch(address, **args) }
68
+ else
69
+ target.define_singleton_method(entry.fqn.verb) { |**args| active_router.query(address, **args) }
70
+ end
71
+ end
72
+
73
+ # `.find`/`.all`/`.count`/`.events`/`.repository` — the SAME read/CRUD
74
+ # surface `Facade::Surface::AggregateDoor` gives a plain `Hecks
75
+ # .boot`, missing here until now: `install_namespace_entry` above
76
+ # installs ONE method per declared VERB, so an aggregate with no
77
+ # commands or queries of its own shape (or simply never asked for
78
+ # a `find`-shaped query) had no way to look up one record by id
79
+ # through the router surface at all — real gap, hit live building
80
+ # a `List` aggregate meant to be read this way. Grouped by
81
+ # (realm, domain, aggregate) rather than installed per-verb,
82
+ # because unlike a command or query this is the SAME five methods
83
+ # regardless of which verb happened to trigger this aggregate's
84
+ # own namespace module into existing.
85
+ def install_aggregate_doors!
86
+ current_entries.reject { |entry| entry.fqn.aggregate.nil? }
87
+ .group_by { |entry| [entry.fqn.realm, entry.fqn.domain, entry.fqn.aggregate] }
88
+ .each_value { |entries| install_aggregate_door(entries.first) }
89
+ end
90
+
91
+ def install_aggregate_door(entry)
92
+ dispatcher = entry.dispatcher
93
+ domain = entry.fqn.domain
94
+ ir = dispatcher.registry.bluebook(domain)&.aggregate(entry.fqn.aggregate)
95
+ return unless ir # a query/command-only entry whose owner isn't a real aggregate root
96
+
97
+ fqn = "#{domain}::#{ir.hecks_name}"
98
+ target = namespace_for(entry.fqn.realm, domain, entry.fqn.aggregate)
99
+
100
+ target.define_singleton_method(:repository) { dispatcher.registry.repository(domain, ir) }
101
+ target.define_singleton_method(:count) { dispatcher.registry.repository(domain, ir).count }
102
+ target.define_singleton_method(:events) { dispatcher.events.select { |event| event.aggregate == fqn } }
103
+
104
+ target.define_singleton_method(:find) do |id|
105
+ found = dispatcher.registry.repository(domain, ir).find(id)
106
+ found && Facade::Handle.new(dispatcher: dispatcher, domain: domain, ir: ir, instance: found)
107
+ end
108
+
109
+ target.define_singleton_method(:all) do
110
+ dispatcher.registry.repository(domain, ir).all.map do |instance|
111
+ Facade::Handle.new(dispatcher: dispatcher, domain: domain, ir: ir, instance: instance)
112
+ end
113
+ end
114
+ end
115
+
116
+ def install_shortcuts!
117
+ installer = self
118
+ current_entries.reject { |entry| entry.fqn.aggregate.nil? }
119
+ .group_by { |entry| [entry.fqn.aggregate, entry.fqn.verb] }
120
+ .each do |(aggregate, verb), candidates|
121
+ shortcut_target(aggregate).define_singleton_method(verb) { |**args|
122
+ installer.send(:dispatch_short, candidates, **args)
123
+ }
124
+ end
125
+ end
126
+
127
+ def dispatch_short(candidates, **args)
128
+ if candidates.length > 1
129
+ shown = candidates.map { |entry| entry.fqn.to_s }.sort.join(", ")
130
+ aggregate, verb = candidates.first.fqn.aggregate, candidates.first.fqn.verb
131
+ raise AmbiguousShortRoute, "#{aggregate}.#{verb} is ambiguous — choose one of: #{shown}"
132
+ end
133
+
134
+ entry = candidates.fetch(0)
135
+ entry.command? ? router.dispatch(entry.fqn.to_s, **args) : router.query(entry.fqn.to_s, **args)
136
+ end
137
+
138
+ def shortcut_target(aggregate)
139
+ constant = Object.const_get(aggregate, false) if Object.const_defined?(aggregate, false)
140
+ raise NameError,
141
+ "cannot install Bluebook shortcut #{aggregate}: it is not a module" if constant && !constant.is_a?(Module)
142
+
143
+ constant || Object.const_set(aggregate, Module.new)
144
+ end
145
+
146
+ def namespace_for(realm, domain, aggregate)
147
+ [realm, domain, aggregate].compact.reduce(Object) do |parent, name|
148
+ constant = parent.const_get(name, false) if parent.const_defined?(name, false)
149
+ raise NameError,
150
+ "cannot install Bluebook route under #{parent}::#{name}: it is not a module" if constant && !constant.is_a?(Module)
151
+
152
+ constant || parent.const_set(name, Module.new)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,70 @@
1
+ module Hecks
2
+ # The public, project-wide dispatch door. It owns address resolution only;
3
+ # each discovered Bluebook keeps its own runtime and persistence bindings.
4
+ class Router
5
+ class UnknownAddress < StandardError; end
6
+ class WrongVerbKind < StandardError; end
7
+ class NotBooted < StandardError; end
8
+ class AmbiguousShortRoute < StandardError; end
9
+
10
+ attr_reader :register
11
+
12
+ def self.load(root) = new(Bluebook::ProjectLoader.load(root))
13
+
14
+ # Install one project router for ordinary application calls. The explicit
15
+ # `Router.load` API remains useful for tests and embedded hosts.
16
+ def self.boot(root)
17
+ router = load(root)
18
+ router.install_namespace!
19
+ @default = router
20
+ end
21
+
22
+ def self.default
23
+ @default || raise(NotBooted, "no project router is booted — call Hecks::Router.boot(root) first")
24
+ end
25
+
26
+ def self.dispatch(address, **args) = default.dispatch(address, **args)
27
+ def self.query(address, **args) = default.query(address, **args)
28
+
29
+ def initialize(register)
30
+ @register = register
31
+ end
32
+
33
+ def available = register.entries.values
34
+
35
+ def install_namespace! = NamespaceInstaller.new(self).install!
36
+
37
+ def resolve(address)
38
+ fqn = Fqn.parse(address)
39
+ raise UnknownAddress, "router addresses require a realm: #{address.inspect}" unless fqn.realm
40
+
41
+ register.fetch(fqn.to_s)
42
+ rescue KeyError
43
+ raise UnknownAddress, "no Bluebook route for #{address.inspect}"
44
+ end
45
+
46
+ def dispatch(address, **args)
47
+ entry = resolve(address)
48
+ raise WrongVerbKind, "#{address.inspect} names a query; use #query" unless entry.command?
49
+
50
+ entry.dispatcher.dispatch(local_verb(entry), **args)
51
+ end
52
+
53
+ def query(address, **args)
54
+ entry = resolve(address)
55
+ raise WrongVerbKind, "#{address.inspect} names a command; use #dispatch" unless entry.query?
56
+
57
+ entry.dispatcher.query(local_verb(entry), **args)
58
+ end
59
+
60
+ private
61
+
62
+ def local_verb(entry)
63
+ return "#{entry.fqn.domain}.#{entry.declared_verb}" unless entry.fqn.aggregate
64
+
65
+ "#{entry.fqn.domain}::#{entry.fqn.aggregate}.#{entry.declared_verb}"
66
+ end
67
+ end
68
+ end
69
+
70
+ require_relative "router/namespace_installer"
@@ -0,0 +1,45 @@
1
+ module Hecks
2
+ module Runtime
3
+ # A PROCESS-WIDE, STRIPED MUTEX REGISTRY — the concurrency-control
4
+ # mechanism for every adapter that does NOT declare
5
+ # `:optimistic_concurrency` (Heki, Memory today; see
6
+ # `CommandInterpreter#call`/`EntityInterpreter#call`, which choose
7
+ # between this and Postgres's CAS+retry purely off
8
+ # `repository.capabilities`).
9
+ #
10
+ # WHY A LOCK SUFFICES HERE AND CAS IS NOT NEEDED: both adapters hold
11
+ # process-local data. `Adapters::Memory.tenant_capable?`'s own comment
12
+ # states the confirmed fact this relies on — two `Runtime.boot` calls
13
+ # get two entirely separate adapter instances; there is never a SECOND
14
+ # PROCESS writing the same Heki file or the same Memory Hash, only
15
+ # possibly other THREADS within this one process. A `Mutex` held for
16
+ # the full hydrate-through-save critical section closes the identical
17
+ # lost-update gap CAS closes for Postgres, with no schema, no version
18
+ # column, and no retry loop — the second thread simply doesn't start
19
+ # its own hydrate until the first thread's save has landed.
20
+ #
21
+ # STRIPED, NOT ONE GLOBAL LOCK: keyed by `[domain, aggregate.hecks_name,
22
+ # id]`, so two dispatches against two DIFFERENT records never block
23
+ # each other. The registry Hash itself is guarded by its own top-level
24
+ # Mutex only for the moment a new per-key Mutex is created — two
25
+ # threads locking DIFFERENT keys for the first time never wait on one
26
+ # another beyond that brief creation window.
27
+ module AggregateLock
28
+ @registry_lock = Mutex.new
29
+ @locks = {}
30
+
31
+ class << self
32
+ # `AggregateLock.for(domain, aggregate, id).synchronize { ... }`
33
+ # `id: nil` — identity could not be resolved yet (see
34
+ # `Identity.best_effort`) — locks by aggregate TYPE alone, coarser
35
+ # (every record of this aggregate serializes against every other)
36
+ # but still correct: it can only ever make dispatch MORE
37
+ # conservative than a resolved id would.
38
+ def for(domain, aggregate, id = nil)
39
+ key = id.nil? ? [domain.to_s, aggregate.hecks_name] : [domain.to_s, aggregate.hecks_name, id.to_s]
40
+ @registry_lock.synchronize { @locks[key] ||= Mutex.new }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ module Hecks
2
+ module Runtime
3
+ # A boot's own small set of phase-tagged, conditionally-present gates —
4
+ # ADR 0031. Registration is instance-scoped, ONE PER `Loader.boot`
5
+ # CALL, never a module-level singleton: a process that boots more than
6
+ # one registry in its lifetime (every spec suite does) must never let
7
+ # one boot's capability profile leak into the next boot's gate list.
8
+ #
9
+ # A gate is anything `.call(registry, directory)`-able — an existing
10
+ # module method handed over as a `Method` object (`EraCheck.method
11
+ # (:check_lineage!)`) needs no wrapper; a bare block does. Phases run
12
+ # in the order `run!` is called, gates within a phase in registration
13
+ # order — today exactly one gate per phase, so ordering among
14
+ # same-phase gates has never been exercised.
15
+ #
16
+ # This is deliberately NOT the same registry `Hecks::Projector` uses
17
+ # (ADR 0027) — that one is a process-wide, static IR-in/artifact-out
18
+ # registry with no bindings and no live-state mutation; this one is
19
+ # per-boot and gates real I/O (a Postgres mint, a saga-store read).
20
+ # Sharing a primitive between them is deferred until a third consumer
21
+ # actually wants it (0031's own Rejected Alternatives).
22
+ class BootGates
23
+ def initialize
24
+ @gates = Hash.new { |h, k| h[k] = [] }
25
+ end
26
+
27
+ def register(name, gate, phase:)
28
+ @gates[phase] << [name, gate]
29
+ self
30
+ end
31
+
32
+ def registered?(name)
33
+ @gates.values.flatten(1).any? { |registered_name, _gate| registered_name == name }
34
+ end
35
+
36
+ def run!(phase, registry, directory)
37
+ @gates[phase].each { |pair| pair.last.call(registry, directory) }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,66 @@
1
+ module Hecks
2
+ module Runtime
3
+ # WHO IS DISPATCHING, bound for the duration of a block — the ambient
4
+ # counterpart to `Runtime.with_registry`, but `Thread.current`-backed
5
+ # rather than a plain module ivar: registry-binding happens at BOOT
6
+ # time (single-threaded by construction), this happens at DISPATCH
7
+ # time, which a future per-request caller (docs/rails-integration.md's
8
+ # still-unbuilt `WebDoor`) needs to bind safely under concurrency.
9
+ #
10
+ # A child `Thread.new` spawned mid-block does not inherit this — plain
11
+ # Ruby thread-local semantics — but nothing in this codebase spawns a
12
+ # worker thread mid-dispatch today, so this is a fact worth naming
13
+ # rather than a gap worth solving.
14
+ module Caller
15
+ # `actor_id` is OPTIONAL, on top of the role that has always been
16
+ # here — a caller that states only a role (every caller before this)
17
+ # is checked the way it always was, string equality against the
18
+ # command's own `role`. A caller that also names WHO it is lets
19
+ # `CommandRules::Authorization` check a real Governance
20
+ # `RoleAssignment` instead, once the command's domain has Governance
21
+ # attached — see that module's own header for the full split.
22
+ #
23
+ # `as_of` and `scope` are BOTH optional too, and both self-asserted
24
+ # by the caller rather than derived from the command — deliberately:
25
+ # `as_of` is filled at the door from `Ports::Clock.now`, never
26
+ # inside the interpreter (see `Ports::Clock`'s own header), so
27
+ # there is no other place to source it from. `scope` stays here
28
+ # rather than becoming a command-level DSL construct on purpose —
29
+ # a scope check that lived in the bluebook would put an
30
+ # authorization concern inside the domain declaration itself; this
31
+ # keeps it an application-boundary fact instead, the same shape
32
+ # `role`/`actor_id` already are. The tradeoff: a caller states what
33
+ # scope it is acting in, and `holds_role?` verifies a live grant
34
+ # exists for THAT scope — it does not independently confirm the
35
+ # scope matches whatever the command's own target data belongs to.
36
+ Current = Struct.new(:role, :actor_id, :as_of, :scope, keyword_init: true)
37
+
38
+ module_function
39
+
40
+ def current = Thread.current[:hecks_caller]
41
+
42
+ def as(role:, actor_id: nil, as_of: nil, scope: nil)
43
+ previous = Thread.current[:hecks_caller]
44
+ Thread.current[:hecks_caller] = Current.new(
45
+ role: role.to_s, actor_id: actor_id&.to_s, as_of: as_of, scope: scope&.to_s
46
+ )
47
+ yield
48
+ ensure
49
+ Thread.current[:hecks_caller] = previous
50
+ end
51
+
52
+ # A REACTION IS THE SYSTEM ACTING, not the caller who happened to be
53
+ # on the stack when the triggering command ran — `Dispatcher#reenter`
54
+ # clears the ambient caller around a reaction's own dispatch so a
55
+ # triggering caller's role can neither satisfy nor block a reaction
56
+ # command it has nothing to do with.
57
+ def without
58
+ previous = Thread.current[:hecks_caller]
59
+ Thread.current[:hecks_caller] = nil
60
+ yield
61
+ ensure
62
+ Thread.current[:hecks_caller] = previous
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,44 @@
1
+ module Hecks
2
+ module Runtime
3
+ # WHICH PORTS A BOOT CAN ACTUALLY FULFILL, read off the registry it already
4
+ # holds. `registry.ports` names every port a domain declared a dependency
5
+ # on ; `registry.adapters` names every adapter wired to implement one — the
6
+ # same `adapter.port == port.name` match `Ports::Extraction` and
7
+ # `Ports::IdentityGeneration` already make for themselves, one at a time,
8
+ # each time they resolve. This is that same question asked once, for
9
+ # every port at once, so a gap in the wiring is something a caller can ASK
10
+ # about rather than something a live dispatch discovers by refusing.
11
+ #
12
+ # `cycles` answers `[]`, always, and honestly: nothing in this port model
13
+ # lets one port depend on another — a port names a verb and a signal, an
14
+ # adapter names the port it implements, and neither can point at a third
15
+ # port. There is no edge for a cycle to be made of.
16
+ class CapabilityGraph
17
+ def initialize(registry)
18
+ @registry = registry
19
+ end
20
+
21
+ # { port name => [adapter name, ...] }, for every port the registry
22
+ # declares — including the ports nothing implements, so a caller can
23
+ # tell "declared, unfulfilled" from "never declared at all".
24
+ def fulfillments
25
+ @fulfillments ||= @registry.ports.each_key.to_h { |name| [name, adapters_for(name)] }
26
+ end
27
+
28
+ # The ports with zero adapters bound — the gap `Runtime::WiringError`
29
+ # would otherwise only surface at the moment something tries to dispatch
30
+ # through one.
31
+ def unfulfilled
32
+ fulfillments.select { |_, adapters| adapters.empty? }.keys
33
+ end
34
+
35
+ def cycles = []
36
+
37
+ private
38
+
39
+ def adapters_for(port_name)
40
+ @registry.adapters.values.select { |adapter| adapter.port == port_name }.map(&:name)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,135 @@
1
+ require_relative "../../naming"
2
+ require_relative "../errors"
3
+ require_relative "../refusal_wording"
4
+
5
+ module Hecks
6
+ module Runtime
7
+ class CommandInterpreter
8
+ # The payload gate: a command takes the arguments it declares — all of
9
+ # them, and no others.
10
+ module ArgumentGate
11
+ private
12
+
13
+ # Anything else used to ride along in the payload untouched —
14
+ # normalize_args walks the DECLARED attributes, so a name the command
15
+ # never had was simply never looked at. A misspelled argument did
16
+ # nothing, in silence.
17
+ #
18
+ # The keys that are legitimately not attributes are the ones that ADDRESS
19
+ # the aggregate rather than describe it : `id`, whatever the aggregate is
20
+ # identified by, and the reference key of the root a command reaches
21
+ # through. Refusing those would refuse every dispatch there is.
22
+ #
23
+ # `extra_identity_heads:` — EntityInterpreter's own callers only. An
24
+ # entity dispatch addresses not just the root aggregate but every
25
+ # entity ALONG THE CHAIN it walks to reach the piece the command
26
+ # actually belongs to (`Handler.Dispatch.Bind` is two hops), and each
27
+ # hop's own `identity_heads` is addressing the same way the root's
28
+ # are — `element_of` (entity_element.rb) reads them straight out of
29
+ # `args` to find the element, never as a fact the command itself
30
+ # declares. `[]` for a plain aggregate/port-operation dispatch, which
31
+ # has no chain to add.
32
+ def refuse_unknown_arguments(domain, aggregate, command, args, extra_identity_heads: [])
33
+ addressing = [:id, *aggregate.identity_heads, *extra_identity_heads, reference_key(command)] +
34
+ correlation_keys(domain)
35
+ known = (command.attributes.map(&:name) + addressing).compact.map(&:to_sym)
36
+ # SORTED. Payload order is whatever the caller happened to write, and
37
+ # refusal wording is contract — pinned byte-for-byte by the corpus, so
38
+ # it cannot depend on hash iteration order.
39
+ unknown = (args.keys.map(&:to_sym) - known).sort
40
+ return if unknown.empty?
41
+
42
+ raise UnknownArgument,
43
+ RefusalWording.render("UnknownArgument", "unknown_args",
44
+ command: command.hecks_name, unknown: unknown.join(", "),
45
+ declared: declared_reading(command))
46
+ end
47
+
48
+ # And it takes ALL of them. The other half of the same sentence, missing
49
+ # until fuzz went looking : a name the command never declared was refused,
50
+ # while a name it DID declare could simply be left out.
51
+ #
52
+ # `Customer.Register` without its `name` used to be refused — but by
53
+ # ACCIDENT, and with a lie for a message. `then_set :name, to: :name` found
54
+ # nothing to resolve, passed the literal symbol on, and coercion reported
55
+ # `name is a PersonName — pass its fields as an object`, which describes a
56
+ # mistake the caller did not make. The real mistake — an argument simply
57
+ # missing — was never the one named, and nothing refused it on purpose.
58
+ #
59
+ # No command attribute anywhere in the corpus carries a default — checked,
60
+ # all eight chapters, zero — so there is no optional argument for this to
61
+ # step on. Every declared attribute is a fact the command needs.
62
+ #
63
+ # `aggregate:` — PRESENT ONLY FOR A PORT OPERATION. An aggregate
64
+ # command's own self-address never reaches this list at all
65
+ # (`CommandBuilder#reference_to`'s bare self-reference mints no
66
+ # attribute to be absent) — but `PortOperationBuilder#reference_to`
67
+ # ALWAYS mints one (this file's own header on `PortOperation`: "no
68
+ # creates?/acts_on distinction to protect"), because an operation
69
+ # historically had no OTHER way to say which record it addressed.
70
+ # `to:` is that other way now (`Dispatcher#port_invocation`
71
+ # promotes the identity attribute's own value out of the payload
72
+ # and into routing) — which left the attribute still DECLARED,
73
+ # still non-optional, and now never present in `args` at all: every
74
+ # operation with an identity attribute refused its own well-formed
75
+ # calls, dispatched exactly the way `to:` intends. Exempted here on
76
+ # the same terms an aggregate command's self-address always was —
77
+ # an address, not a fact the operation still needs handed back.
78
+ def refuse_absent_arguments(command, args, aggregate: nil)
79
+ given = args.keys.map(&:to_sym)
80
+ exempt = aggregate && command.respond_to?(:identity_attribute) &&
81
+ command.identity_attribute(aggregate.hecks_name)&.name
82
+ required = command.attributes.reject(&:optional?).map { |attribute| attribute.name.to_sym } - [exempt]
83
+ # SORTED, for the same reason the unknown list is : refusal wording is
84
+ # contract, and a pinned wording cannot depend on the order a set
85
+ # difference happens to be computed in.
86
+ absent = (required - given).sort
87
+ return if absent.empty?
88
+
89
+ raise AbsentArgument,
90
+ RefusalWording.render("AbsentArgument", "absent_args",
91
+ command: command.hecks_name, absent: absent.join(", "),
92
+ declared: declared_reading(command))
93
+ end
94
+
95
+ # A COMMAND THAT DECLARES NOTHING STILL HAS TO SAY SO. `Account
96
+ # .Freeze` is `reference_to Account` and no attributes at all, so
97
+ # `{declared}` rendered empty and the sentence trailed off mid-
98
+ # clause : "Freeze does not declare standing — it takes ". Read
99
+ # against a real refusal that shape is worse than unhelpful — it
100
+ # looks like the message itself is broken, right where a caller
101
+ # is already trying to work out what went wrong.
102
+ #
103
+ # Only the empty case changes. Every command that declares even
104
+ # one attribute renders exactly as it did before, because refusal
105
+ # wording is contract and pinned byte-for-byte by the corpus (see
106
+ # `refuse_unknown_arguments`' own note on sorting for why).
107
+ def declared_reading(command)
108
+ declared = command.attributes.map(&:name)
109
+ declared.empty? ? "none" : declared.join(", ")
110
+ end
111
+
112
+ # What a process manager correlates by is ROUTING, not description. A saga
113
+ # threads its correlation key through every leg it dispatches so the event
114
+ # each leg emits carries it and the next step can be correlated — so the key
115
+ # arrives on commands that never declare it, and legitimately.
116
+ #
117
+ # This is the weakest part of the gate. Correlation is the SAGA's business,
118
+ # and the better shape is for the saga to stamp its own key onto the event
119
+ # it caused rather than smuggle it through the command's payload. Until it
120
+ # does, refusing the key here would break every saga in the corpus.
121
+ def correlation_keys(domain)
122
+ Array(@registry.bluebook(domain)&.process_managers)
123
+ .filter_map { |saga| saga.correlates_by && saga.correlation_head }
124
+ end
125
+
126
+ def reference_key(command)
127
+ target = command.references.to_s
128
+ return nil if target.empty?
129
+
130
+ Naming.reference_key(target)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end