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,142 @@
1
+ require_relative "../saga_pending_dispatch"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ class Registry
6
+ # SAGA PERSISTENCE — no new DSL verb, no new world setting.
7
+ # Whatever adapter a domain's own aggregates already use for
8
+ # durability, its sagas use the same one, automatically: three
9
+ # OPTIONAL adapter methods (`save_saga`/`delete_saga`/`each_saga`),
10
+ # `respond_to?`-checked the same way `Ports::Persistence::AppendOnly`
11
+ # already treats an adapter's own optional methods
12
+ # (`find`/`all`/`count`/`reset!`/`events`/`record_event`).
13
+ module SagaPersistence
14
+ # Resolved and memoized per domain — the FIRST real aggregate
15
+ # declared in the domain's own bluebook is the resolution
16
+ # anchor. `Ports::Persistence::BindingPolicy.resolve` for that
17
+ # aggregate already falls back to a domain-level default bind
18
+ # (§0's `Hecksagon#bind_for`) before falling back to Memory, so
19
+ # this needs no separate "was a default declared" branch of its
20
+ # own — whatever adapter that anchor resolves to already IS the
21
+ # domain's own default in the normal case (every aggregate
22
+ # shares one adapter), and is the honest, documented fallback
23
+ # for the rarer domain genuinely split across more than one
24
+ # local adapter with no default declared.
25
+ #
26
+ # GENUINELY LAZY, GENUINELY POST-BOOT — unlike `rehydrate_sagas!`
27
+ # below (boot-only), this is called from live dispatch
28
+ # (`SagaInterpreter#checkpoint`/`#end_saga`, on every saga
29
+ # transition), so the FIRST call for a given domain can come from
30
+ # any dispatching thread, not just the boot thread. `@saga_persistence`
31
+ # itself is a plain Hash stood up once in `Registry#initialize` (no
32
+ # race on the container), but `resolve_saga_persistence` is real work
33
+ # (a `BindingPolicy.resolve` plus a lazy `repository` build) whose
34
+ # RESULT — the actual adapter instance a domain's sagas persist
35
+ # through — must be the SAME object for every caller: two threads
36
+ # racing the first lookup and each building their own adapter would
37
+ # silently split one domain's saga writes across two adapter
38
+ # instances (worse than `Dispatcher#reaction_depth`'s M20 — that bug
39
+ # corrupted a counter; this one can corrupt WHICH STORE a saga's
40
+ # state lands in). Double-checked locking against a DEDICATED mutex
41
+ # — never `@saga_mutex` — see `Registry#initialize`'s own comment for
42
+ # why reusing that one would deadlock.
43
+ def saga_persistence(domain)
44
+ key = domain.to_s
45
+ @saga_persistence[key] || @saga_persistence_mutex.synchronize { @saga_persistence[key] ||= resolve_saga_persistence(key) }
46
+ end
47
+
48
+ # WALKS EVERY LOADED DOMAIN, repopulating `saga_instances` from
49
+ # whatever `saga_persistence(domain)` resolves to — a real store
50
+ # for a domain whose adapter answers the capability, `each_saga`
51
+ # yielding real rows; `NULL_SAGA_STORE`'s own `each_saga` for
52
+ # everything else, which never yields at all, so this needs no
53
+ # `respond_to?` branch of its own — the SAME reason every other
54
+ # call site in this capability never needs one. Called once at
55
+ # boot (`Loader.boot`, between `verify!` and dispatcher
56
+ # construction) — a process that's been running has no reason to
57
+ # re-walk its own already-current `saga_instances`.
58
+ #
59
+ # BOOT-TIME-ONLY (OR ITS TEST-RUNNER EQUIVALENT) — this method's
60
+ # only callers are `Loader.run_boot_gates!` (single-threaded, before
61
+ # `dispatcher_for` ever exists) and `Registry#reset_runtime_state!`
62
+ # (single-threaded test runner — see that method's own comment).
63
+ # Never called from live dispatch, so `@saga_instances` mutation
64
+ # here has no concurrent caller to race, unlike its OTHER two write
65
+ # points inside `@saga_mutex.synchronize` blocks (`saga_interpreter.
66
+ # rb`), which genuinely do and are guarded accordingly.
67
+ # rubocop:disable-next Hecks/ThreadSharedIvarMutation
68
+ def rehydrate_sagas!
69
+ @hecksagons.each_key do |domain|
70
+ saga_persistence(domain).each_saga do |process_manager, correlation, state, memory, completed_compensations = []|
71
+ pending = memory.delete(SAGA_PENDING_DISPATCH_KEY)
72
+ @saga_instances[process_manager][correlation] =
73
+ { state: state, memory: memory, completed_compensations: completed_compensations || [] }
74
+ warn_stalled_saga(domain, process_manager, correlation, state, pending) if pending
75
+ end
76
+ end
77
+ self
78
+ end
79
+
80
+ private
81
+
82
+ # THE OTHER HALF OF THE OUTBOX-SHAPED FIX (see saga_pending_
83
+ # dispatch.rb) — `SAGA_PENDING_DISPATCH_KEY`, if the crashed
84
+ # process left it standing, means the row's own `state` was
85
+ # checkpointed but the dispatch cascade that justifies it may
86
+ # never have run. Stripped out of `memory` before it becomes
87
+ # this instance's LIVE `:memory` (so nothing downstream — a
88
+ # `given`, a `with:` mapping, the fuzzer — ever sees it), and
89
+ # surfaced loudly instead: a WARNING plus a `saga_log` entry,
90
+ # never an automatic redrive (see saga_pending_dispatch.rb for
91
+ # why redriving without idempotent delivery would be worse than
92
+ # the stall). This is real, durable crash-recovery VISIBILITY —
93
+ # still not the reconciliation itself, which stays a human's
94
+ # call until hecks has idempotent redelivery to make it safe.
95
+ # rubocop:disable-next Hecks/ThreadSharedIvarMutation -- same
96
+ # justification as `rehydrate_sagas!`'s own disable comment
97
+ # above: this method's ONLY caller is that boot-time-only walk,
98
+ # never live dispatch, so `@saga_log` has no concurrent writer
99
+ # to race here.
100
+ def warn_stalled_saga(domain, process_manager, correlation, state, pending)
101
+ # `.transform_keys(&:to_sym)` — Heki's own `each_saga` only
102
+ # symbolizes `memory`'s TOP-level keys (`SagaStore#each_saga`'s
103
+ # own `transform_keys`, one level deep); a value NESTED under
104
+ # one of those keys, like this marker, comes back with plain
105
+ # string keys from Heki specifically, symbol keys already from
106
+ # Postgres/SQLite/D1's own `symbolize_names: true` parse. Normalizing
107
+ # here, once, is simpler than teaching every adapter's own
108
+ # shallow/deep parsing convention about this one reserved key.
109
+ pending = pending.transform_keys(&:to_sym)
110
+ dispatches = Array(pending[:dispatches]).join(", ")
111
+ warn "[hecks] #{domain} rehydrated #{process_manager} instance #{correlation.inspect} in state " \
112
+ "#{state.inspect} with a dispatch left pending from before the last crash/restart — " \
113
+ "#{dispatches} (on #{pending[:on].inspect}, #{pending[:from].inspect} -> #{pending[:to].inspect}) " \
114
+ "may or may not have actually run. hecks does not auto-redrive a pending saga dispatch (that " \
115
+ "needs idempotent delivery, which this pipeline doesn't have yet); reconcile this instance by hand."
116
+ @saga_log << { process_manager: process_manager, instance: correlation, rehydrated_stalled: true,
117
+ state: state, pending: pending }
118
+ end
119
+
120
+ def resolve_saga_persistence(domain)
121
+ anchor = hecksagon(domain) && bluebook(domain)&.aggregates&.first
122
+ return Ports::Persistence::NULL_SAGA_STORE unless anchor
123
+
124
+ bind = Ports::Persistence::BindingPolicy.resolve(self, domain, anchor)
125
+ return Ports::Persistence::NULL_SAGA_STORE if adapter_class(bind.adapter) <= Ports::Persistence::RemoteRuntime
126
+
127
+ adapter = repository(domain, anchor).adapter
128
+ adapter.respond_to?(:save_saga) ? adapter : Ports::Persistence::NULL_SAGA_STORE
129
+ rescue WiringError
130
+ # A domain not cleanly wired for its own anchor aggregate's
131
+ # persistence (a test fixture binding only the aggregates it
132
+ # exercises, say) degrades saga persistence to the same no-op
133
+ # default an unbound domain gets — never raises out of a
134
+ # dispatch that would otherwise have succeeded. Matches this
135
+ # capability's own framing throughout: automatic wherever it's
136
+ # cleanly supported, silently absent wherever it isn't.
137
+ Ports::Persistence::NULL_SAGA_STORE
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,246 @@
1
+ require_relative "../../bluebook/hexagon"
2
+
3
+ module Hecks
4
+ module Runtime
5
+ class Registry
6
+ # The wiring gate: every bind names a declared aggregate, every
7
+ # adapter satisfies the verb its port declares, every world setting is
8
+ # a field the adapter admits, and the default adapter is usable at
9
+ # all. Included into Registry — `verify!` is what a boot calls after
10
+ # loading, and the smaller checks are also called piecemeal by the
11
+ # repository factory.
12
+ module Verification
13
+ def verify!
14
+ verify_default_adapter!
15
+ verify_singleton_port_answers!
16
+
17
+ @hecksagons.each_value do |hexagon|
18
+ refuse_ungoverned_roles!(hexagon)
19
+
20
+ hexagon.binds.each do |bind|
21
+ # A domain-level default (§0) — `persisted_by "Heki"` bare,
22
+ # applying to whichever aggregates don't override it — names
23
+ # no aggregate of its own, so there's nothing to look up in
24
+ # the bluebook for THIS row specifically. Still validate its
25
+ # own adapter/verb shape (the same reason
26
+ # `verify_default_adapter!` checks the framework-wide
27
+ # default the same way, aggregate-less). Coverage of real
28
+ # aggregates that only resolve THROUGH this default comes
29
+ # from their own dispatch-time `BindingPolicy.resolve` —
30
+ # deliberately not required to be exhaustive here, the same
31
+ # leniency this method already extended to any aggregate
32
+ # left out of an explicit bind list entirely (real test
33
+ # fixtures bind only the aggregates they exercise).
34
+ if bind.aggregate.nil?
35
+ check_verb(bind)
36
+ next
37
+ end
38
+
39
+ aggregate = bluebook(hexagon.domain)&.aggregate(bind.aggregate_name)
40
+ raise WiringError, "#{bind.aggregate} is bound but not declared in the bluebook" unless aggregate
41
+
42
+ check_verb(bind)
43
+
44
+ repository(hexagon.domain, aggregate)
45
+ end
46
+
47
+ warn_undurable_sagas!(hexagon)
48
+ end
49
+ self
50
+ end
51
+
52
+ def verify_default_adapter!
53
+ name = Ports::Persistence::DEFAULT_ADAPTER
54
+
55
+ check_verb(
56
+ Bluebook::Bind.new(
57
+ aggregate: "(default)",
58
+ verb: Ports::Persistence::VERB,
59
+ adapter: name
60
+ )
61
+ )
62
+ adapter_class(name)
63
+ self
64
+ rescue WiringError => error
65
+ raise WiringError,
66
+ "the default persistence adapter (#{name}) is not usable, so an " \
67
+ "aggregate with no bind could not be given one: #{error.message}"
68
+ end
69
+
70
+ def check_verb(bind)
71
+ port = port_for(bind)
72
+ check_answers(port, bind.adapter)
73
+ return if port.verb.to_s == bind.verb.to_s
74
+
75
+ raise WiringError,
76
+ "#{bind.adapter} implements the #{port.name} port (verb #{port.verb}) " \
77
+ "and cannot satisfy #{bind.verb}"
78
+ end
79
+
80
+ # THE METHOD CONTRACT A `.port` FILE'S `verb`/`signal` NEVER
81
+ # CARRIED — an adapter can name the right port, satisfy the right
82
+ # verb, and admit every `.world` setting `check_settings` checks,
83
+ # and still be missing the one method a live dispatch will
84
+ # actually call. `answers` is optional per port (an empty list is
85
+ # today's pre-existing behavior, unchecked), so this only ever
86
+ # tightens a port that opted in.
87
+ def check_answers(port, adapter_name)
88
+ answers = Array(port.answers)
89
+ return if answers.empty?
90
+
91
+ klass = adapter_class(adapter_name)
92
+ missing = answers.reject { |method_name| klass.respond_to?(method_name) }
93
+ return if missing.empty?
94
+
95
+ raise WiringError,
96
+ "#{adapter_name} declares the #{port.name} port but does not respond to " \
97
+ "#{missing.map(&:inspect).join(', ')} — #{port.name}.port declares answers " \
98
+ "#{answers.map(&:inspect).join(', ')}"
99
+ end
100
+
101
+ # THE NINE SINGLETON PORTS' OWN GAP — `persistence`, `projection`
102
+ # and `loading` are per-aggregate bindings, checked above through
103
+ # every real `bind` a hexagon declares; a singleton port
104
+ # (`clock`, `authorization`, …) is never bound to an aggregate at
105
+ # all, so nothing above ever resolves one and nothing above ever
106
+ # ran `check_answers` against it. Each one's own `Ports::*.adapter`
107
+ # already refuses zero or multiple implementations, live, at
108
+ # first dispatch — that stays exactly as-is here (0 or 2+ is
109
+ # ambiguity, not a method-contract question, and asserting every
110
+ # declared port MUST have exactly one adapter would wrongly
111
+ # refuse a boot that simply never wires a port it doesn't use).
112
+ # This only ever tightens the ONE case those checks don't cover:
113
+ # exactly one adapter, wired, missing a method `answers` names.
114
+ PER_AGGREGATE_PORTS = %w[persistence projection loading].freeze
115
+
116
+ def verify_singleton_port_answers!
117
+ @ports.each_value do |port|
118
+ next if PER_AGGREGATE_PORTS.include?(port.name)
119
+ next if Array(port.answers).empty?
120
+
121
+ implementations = @adapters.values.select { |a| a.port == port.name }
122
+ next unless implementations.size == 1
123
+
124
+ check_answers(port, implementations.first.name)
125
+ end
126
+ self
127
+ end
128
+
129
+ def check_settings(bind, settings)
130
+ adapter = @adapters[bind.adapter]
131
+ return unless adapter
132
+
133
+ declared = settings.keys - [:adapter]
134
+ unknown = declared.reject { |field| adapter.declares?(field) }
135
+ return if unknown.empty?
136
+
137
+ raise WiringError,
138
+ "#{bind.adapter} does not declare #{unknown.map(&:inspect).join(', ')} — " \
139
+ "it declares #{adapter.all_fields.map(&:inspect).join(', ')}. " \
140
+ "Add the field to the adapter, or remove it from the world."
141
+ end
142
+
143
+ def port_for(bind)
144
+ adapter = @adapters[bind.adapter]
145
+ raise WiringError, "unknown adapter #{bind.adapter.inspect}" unless adapter
146
+
147
+ @ports[adapter.port] ||
148
+ raise(WiringError, "adapter #{bind.adapter} declares unknown port #{adapter.port.inspect}")
149
+ end
150
+
151
+ def adapter_class(name)
152
+ Adapters.const_get(name)
153
+ rescue NameError
154
+ raise WiringError, "no Ruby adapter implementation for #{name.inspect} " \
155
+ "(expected Hecks::Adapters::#{name})"
156
+ end
157
+
158
+ private
159
+
160
+ # `role` IS REAL ACCESS CONTROL ONLY WHEN GOVERNANCE CAN CHECK IT
161
+ # AGAINST SOMETHING — a command that declares a role but whose
162
+ # domain never attaches Governance would leave that role forever
163
+ # unchecked, exactly the defect ADR 0025 §9 names ("role gates
164
+ # access control by exact string equality ... Governance ...
165
+ # connected to none of it"). Checked here, at `verify!` — RECOVERED
166
+ # and MOVED, not new: this used to run per-block, at HECKSAGON
167
+ # build time (Bluebook::DSL::HecksagonBuilder#build), which broke
168
+ # the moment a domain could be split across multiple hecksagon
169
+ # blocks (base + an `environments/<name>.hecksagon` overlay,
170
+ # Runtime::Loader.boot's `environment:` — see its own comment for
171
+ # the recovery provenance): every block but the one declaring
172
+ # `uses_framework "Governance"` would be refused there, even
173
+ # though `Registry#add_hecksagon` merges every block for a domain
174
+ # into ONE Hecksagon before anything ever dispatches against it.
175
+ # Checking the MERGED result once, here, after every file for
176
+ # this domain has loaded, is both more permissive (no need to
177
+ # repeat `uses_framework` in every file) and strictly more
178
+ # correct (a check against an incomplete, not-yet-merged
179
+ # hecksagon can never see the real final shape).
180
+ #
181
+ # GOVERNANCE ITSELF IS EXEMPT — it cannot `uses_framework` its own
182
+ # aggregates, and it IS the source of truth a role check runs
183
+ # against, the same self-reference `CommandRules::Authorization
184
+ # #governance_attached?` grants it at dispatch time.
185
+ def refuse_ungoverned_roles!(hexagon)
186
+ return if hexagon.domain == "Governance"
187
+ return if hexagon.framework_members.include?("Governance")
188
+
189
+ bluebook_ir = bluebook(hexagon.domain)
190
+ return unless bluebook_ir
191
+
192
+ offender = commands_in(bluebook_ir).find { |command| !command.role.to_s.empty? }
193
+ return unless offender
194
+
195
+ raise WiringError,
196
+ "#{offender.hecks_fqn} declares role #{offender.role.inspect}, but " \
197
+ "#{hexagon.domain}'s hecksagon never uses_framework \"Governance\" — role is only " \
198
+ "real access control once Governance is attached to check it against; without that " \
199
+ "it is silent decoration, the exact defect this refusal exists to catch"
200
+ end
201
+
202
+ # Every command this domain declares, an aggregate's own AND every
203
+ # entity nested inside one — the same reach `refuse_role_mismatch`
204
+ # itself needs at dispatch time, just walked ahead of time here.
205
+ def commands_in(bluebook_ir)
206
+ bluebook_ir.aggregates.flat_map { |aggregate| aggregate.commands + aggregate.entities.flat_map(&:commands) }
207
+ end
208
+
209
+ # A domain that declares a `process_manager` but whose
210
+ # `saga_persistence` resolves to `NULL_SAGA_STORE` (no anchor
211
+ # aggregate, a RemoteRuntime-shaped adapter, an adapter that
212
+ # doesn't `respond_to?(:save_saga)`, or a rescued WiringError —
213
+ # see `SagaPersistence#resolve_saga_persistence`) gets sagas that
214
+ # advance correctly in-process and vanish on restart: no
215
+ # checkpoint written, nothing for `rehydrate_sagas!` to find, no
216
+ # compensation ever replayed. That is silent right up until the
217
+ # process actually dies mid-saga — the same "consistency/
218
+ # freshness defect applied to access control, failing open" ADR
219
+ # 0025 named for an unchecked `role`, here applied to saga
220
+ # durability instead.
221
+ #
222
+ # A WARNING, NOT A REFUSAL — unlike `refuse_ungoverned_roles!`,
223
+ # running sagas on a store with no `save_saga` is legitimate on
224
+ # purpose in a fast in-memory test/dev boot (this project's own
225
+ # `saga_durability_spec.rb` boots a process manager on `Memory`
226
+ # specifically to exercise the saga_mutex without real I/O), so
227
+ # refusing the boot outright would break a choice an author made
228
+ # deliberately. What a deploy needs is for the gap to be loud and
229
+ # undeniable, not for local dev/test to become impossible.
230
+ def warn_undurable_sagas!(hexagon)
231
+ bluebook_ir = bluebook(hexagon.domain)
232
+ return unless bluebook_ir
233
+ return if bluebook_ir.process_managers.empty?
234
+ return unless saga_persistence(hexagon.domain).equal?(Ports::Persistence::NULL_SAGA_STORE)
235
+
236
+ names = bluebook_ir.process_managers.map(&:name).join(", ")
237
+ warn "[hecks] #{hexagon.domain} declares process_manager(s) #{names} but its resolved " \
238
+ "persistence adapter has no save_saga — saga state advances correctly in-process " \
239
+ "and is LOST on restart (no checkpoint, no rehydration, no compensation replay). " \
240
+ "Bind this domain to an adapter that implements save_saga if this process_manager " \
241
+ "must survive a crash."
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,279 @@
1
+ require_relative "registry/verification"
2
+ require_relative "registry/saga_persistence"
3
+
4
+ module Hecks
5
+ module Runtime
6
+ class WiringError < StandardError; end
7
+
8
+ # The collections a boot gathers — bluebooks, hexagons, ports, adapters,
9
+ # worlds, the logs — and how a repository is resolved from them. The
10
+ # wiring gate lives in registry/verification.rb, saga persistence
11
+ # resolution in registry/saga_persistence.rb.
12
+ class Registry
13
+ include Verification
14
+ include SagaPersistence
15
+
16
+ attr_reader :root, :bluebooks, :hecksagons, :ports, :adapters, :worlds, :event_log,
17
+ :reaction_log, :saga_log, :saga_instances, :translations, :saga_mutex,
18
+ :saga_dispatch_log, :policy_dispatch_log
19
+
20
+ def initialize(root: nil)
21
+ @root = root
22
+ @bluebooks = {}
23
+ @hecksagons = {}
24
+ @ports = {}
25
+ @adapters = {}
26
+ @worlds = {}
27
+ @translations = []
28
+ @event_log = []
29
+ @reaction_log = []
30
+ @saga_log = []
31
+ # ADDITIVE, RUBY-ONLY — never merged into saga_log/reaction_log.
32
+ # rust/src/kernel/orchestrate.rs ports THOSE two arrays' exact
33
+ # shape byte-for-byte (spec/rust_conformance_spec.rb's own
34
+ # equality check) — a landmine found by reading that spec before
35
+ # touching anything, not by hitting it. These carry the raw
36
+ # inputs a dispatch's own argument binding was resolved from
37
+ # (SagaInterpreter#deliver_saga_dispatch / PolicyInterpreter#
38
+ # trigger_args), for Properties.dispatch_binding_fidelity's own
39
+ # independent re-derivation — a fact neither existing log
40
+ # records at all, so there is nothing here for Rust to have
41
+ # matched or drifted from.
42
+ @saga_dispatch_log = []
43
+ @policy_dispatch_log = []
44
+ @saga_instances = Hash.new { |h, k| h[k] = {} }
45
+ # GUARDS `saga_instances`' OWN mutation+checkpoint sequence
46
+ # (`SagaInterpreter`'s 4 write points, §7) — the same shape of
47
+ # hazard this codebase's own prior audit already flagged for
48
+ # `Dispatcher#reenter`'s reaction-depth counter (M20: a
49
+ # thread-shared ivar with no lock, since fixed by moving it to
50
+ # `Thread.current`, dispatcher.rb), made meaningfully easier to
51
+ # hit here once a persistence write sits in the same critical
52
+ # section. Held across the in-memory
53
+ # mutation AND the checkpoint write together, never across a
54
+ # saga's own dispatch cascade — see `SagaInterpreter#advance_saga`'s
55
+ # own comment for why that distinction matters (non-reentrant
56
+ # Mutex, recursive re-entry is real).
57
+ @saga_mutex = Mutex.new
58
+ @repositories = {}
59
+ @projection_repositories = {}
60
+ @bluebook_builders = {}
61
+ # EAGER, NOT LAZY — see `#resolved_eras`'s own comment for why. Built
62
+ # here rather than `@resolved_eras ||= {}` on first access so there is
63
+ # no window, post-boot, where two concurrently dispatching threads
64
+ # could race creating this Hash (Hecks/ThreadSharedIvarMutation; the
65
+ # same shape of hazard `Dispatcher#reenter`'s `@reaction_depth` was
66
+ # fixed for). Every WRITE into it still only ever happens at boot,
67
+ # single-threaded (`EraResolver.check!`, a `:pre_verify` boot gate) —
68
+ # this only removes the race on standing up the container itself for
69
+ # a boot with no era-plugin domain at all, whose first touch would
70
+ # otherwise be a live dispatch's own `RepositoryFactory.build` read.
71
+ @resolved_eras = {}
72
+ # EAGER, NOT LAZY — see `#capability_graph`'s own comment for why.
73
+ # `CapabilityGraph.new` only stores the registry reference; there is
74
+ # no reason to defer it, and doing so removes the exact same
75
+ # first-access race `#resolved_eras` above does, while preserving the
76
+ # "same instance every call" identity `spec/runtime/capability_graph_
77
+ # spec.rb` already requires.
78
+ @capability_graph = CapabilityGraph.new(self)
79
+ # `@saga_persistence` itself is eager (see `#saga_persistence`'s own
80
+ # comment) — only the PER-DOMAIN resolution inside it is genuinely
81
+ # expensive and lazy, guarded by this dedicated mutex. NOT the same
82
+ # mutex as `@saga_mutex`: `checkpoint` (saga_interpreter.rb) calls
83
+ # `saga_persistence(domain)` from INSIDE an `@saga_mutex.synchronize`
84
+ # block, so reusing `@saga_mutex` here would deadlock the very first
85
+ # time a saga advanced (a `Mutex` is not reentrant — the exact
86
+ # warning `@saga_mutex`'s own comment already gives for a different
87
+ # reason).
88
+ @saga_persistence = {}
89
+ @saga_persistence_mutex = Mutex.new
90
+ end
91
+
92
+ # THE BUILDER STAYS OPEN FOR THE LIFE OF THIS REGISTRY, keyed by chapter
93
+ # name — see the comment on `BluebookBuilder.build`. A chapter split across
94
+ # several files (`language/bluebook/*.bluebook`, all `Hecks.bluebook "Bluebook"`)
95
+ # needs its declarations to accumulate into ONE builder rather than each
96
+ # file minting its own and silently discarding the one before.
97
+ def bluebook_builder(name)
98
+ @bluebook_builders[name.to_s] ||= yield
99
+ end
100
+
101
+ # BOOT-TIME-ONLY, SINGLE-THREADED — every `add_*` below (through
102
+ # `add_translation`) is called exclusively from `Hecks.collect`
103
+ # (hecks.rb), which is what `Hecks.bluebook`/`.hecksagon`/`.port`/
104
+ # `.adapter`/`.world`/`.translation` run inside while a `.bluebook`/
105
+ # `.hecksagon`/`.world` file is being `Kernel.load`ed — i.e. strictly
106
+ # during `Loader.boot`/`.boot_files`, before `dispatcher_for` ever
107
+ # hands this registry to a live, multi-threaded caller. Nothing
108
+ # downstream of boot ever calls these — verified by grepping every
109
+ # call site in lib/ and spec/ before writing this — so unlike
110
+ # `#resolved_eras`/`#capability_graph`/`#saga_persistence` (each
111
+ # reachable from live dispatch, and fixed for real above/in
112
+ # registry/saga_persistence.rb) there is no concurrent caller for
113
+ # `Hecks/ThreadSharedIvarMutation` to actually be warning about here.
114
+ # rubocop:disable Hecks/ThreadSharedIvarMutation
115
+ def add_bluebook(item) = @bluebooks[item.name] = item
116
+
117
+ # MERGED, NOT REPLACED — RECOVERED, not new (see Runtime::Loader
118
+ # .boot's own comment for the provenance). A domain's hecksagon can
119
+ # now load in more than one block for the same domain (base file
120
+ # plus an `environments/<name>.hecksagon` overlay), and the second
121
+ # block should ADD to what the first declared, not silently
122
+ # discard it.
123
+ def add_hecksagon(item)
124
+ existing = @hecksagons[item.domain]
125
+ @hecksagons[item.domain] = existing ? merge_hecksagons(existing, item) : item
126
+ end
127
+
128
+ def add_port(item) = @ports[item.name] = item
129
+ def add_adapter(item) = @adapters[item.name] = item
130
+
131
+ # MERGED, NOT REPLACED — the same generalization for `World` that
132
+ # `add_hecksagon` above recovers for `Hecksagon`: an
133
+ # `environments/<name>.world` overlay (or a host-owned tenancy
134
+ # overlay world, same mechanism) can now add or override settings
135
+ # for a domain a base `.world` file already declared, without
136
+ # restating everything the base file said. Settings merge shallow,
137
+ # keyed exactly the way WorldBuilder already stores them (both the
138
+ # bare verb key and the `"verb:adapter"` qualified key point at the
139
+ # same resolved hash) — an overlay's key wins over the base's same
140
+ # key; a key only the base declares survives untouched.
141
+ def add_world(item)
142
+ existing = @worlds[item.domain]
143
+ @worlds[item.domain] = existing ? merge_worlds(existing, item) : item
144
+ end
145
+
146
+ def add_translation(item) = @translations << item
147
+ # rubocop:enable Hecks/ThreadSharedIvarMutation
148
+
149
+ # {domain name => era ordinal} as resolved by the boot-time era
150
+ # gate. A lineage adapter writes into ITS OWN era's partition —
151
+ # which, for an old checkout booting a held-but-superseded shape,
152
+ # is not the newest one. The Hash itself is stood up in `initialize`
153
+ # (see that comment) — this is a plain reader, not a memoizer;
154
+ # `Hecks/ThreadSharedIvarMutation` is the reason there is no `||=`
155
+ # left here to flag.
156
+ attr_reader :resolved_eras
157
+
158
+ def bluebook(name) = @bluebooks[name.to_s]
159
+ def hecksagon(name) = @hecksagons[name.to_s]
160
+ def world(name) = @worlds[name.to_s]
161
+
162
+ def verbs = @bluebooks.values.flat_map(&:verbs).sort
163
+
164
+ def repository(domain, aggregate)
165
+ @repositories[[domain.to_s, aggregate.hecks_name]] ||= Ports::Persistence.repository(self, domain, aggregate)
166
+ end
167
+
168
+ # EVERYTHING A DISPATCH WROTE, CLEARED; NOTHING A BOOT DECLARED,
169
+ # TOUCHED. Bluebooks, hecksagons, ports, adapters, worlds and the
170
+ # resolved eras are what loading the files produced and stay as
171
+ # they are; the logs, the saga instances and the repositories are
172
+ # what running commands against them produced, and go back to
173
+ # exactly what a fresh boot of the same files hands out. Dropping
174
+ # the repositories (rather than emptying each) is deliberate: a
175
+ # fresh boot's own repositories are new adapter instances too, so
176
+ # a Memory adapter starts empty and a durable one sees whatever
177
+ # it persisted — the same reading either way. Sagas rehydrate off
178
+ # that store again, the way `Loader.boot_files` does after
179
+ # `verify!`.
180
+ #
181
+ # What this is for: a test runner that used to boot a runtime per
182
+ # test to get isolation (`Behaviors::Expectations.run_one`) — ~2s a
183
+ # boot, 76 chess behaviours = two and a half minutes of booting the
184
+ # same two files — can now boot once and reset between tests.
185
+ #
186
+ # SINGLE-THREADED CALLER, THE SAME REASON THE `add_*` CLUSTER ABOVE
187
+ # IS EXEMPT — `Behaviors::Expectations.run_one` is this method's ONLY
188
+ # caller (verified by grep before writing this), and it runs one
189
+ # test at a time: `Runner#run` maps over tests sequentially, and
190
+ # `Behaviors.rspec`'s generated examples run under RSpec's own
191
+ # single-threaded example loop. No production dispatch path calls
192
+ # this at all — a live Puma worker pool never resets a registry out
193
+ # from under itself mid-flight.
194
+ # rubocop:disable-next Hecks/ThreadSharedIvarMutation
195
+ def reset_runtime_state!
196
+ @event_log.clear
197
+ @reaction_log.clear
198
+ @saga_log.clear
199
+ @saga_dispatch_log.clear
200
+ @policy_dispatch_log.clear
201
+ @saga_instances.clear
202
+ @repositories = {}
203
+ @projection_repositories = {}
204
+ rehydrate_sagas!
205
+ self
206
+ end
207
+
208
+ # Built eagerly in `initialize` (see that comment) — this is a plain
209
+ # reader, not a memoizer; `spec/runtime/capability_graph_spec.rb`
210
+ # asserts the SAME instance comes back every call, which this still
211
+ # gives, just without a lazy `||=` race on standing it up.
212
+ attr_reader :capability_graph
213
+
214
+ def read_repository(domain, aggregate)
215
+ key = [domain.to_s, aggregate.hecks_name]
216
+ binding = Ports::Projection.binds_for(self, domain, aggregate).first
217
+ return repository(domain, aggregate) unless binding
218
+
219
+ projection = (@projection_repositories[key] ||= begin
220
+ projection = Ports::Persistence::RepositoryFactory.build(self, domain, aggregate, binding,
221
+ recover: true, settings_verb: Ports::Projection::VERB)
222
+ projection
223
+ end)
224
+ authoritative = repository(domain, aggregate)
225
+ projection_current?(projection, authoritative) ? projection : authoritative
226
+ end
227
+
228
+ def projection_current?(projection, authoritative)
229
+ projected_entries = projection.entries
230
+ source_entries = authoritative.entries
231
+ return false unless projected_entries.length == source_entries.length
232
+ return false unless projected_entries.zip(source_entries).all? do |projected, source|
233
+ projected.operation == source.operation && projected.id == source.id && projected.state == source.state
234
+ end
235
+
236
+ projected_rows = projection.all.map(&:to_h).sort_by { |row| row.fetch(:id).to_s }
237
+ source_rows = authoritative.all.map(&:to_h).sort_by { |row| row.fetch(:id).to_s }
238
+ projected_rows == source_rows
239
+ rescue StandardError
240
+ false
241
+ end
242
+
243
+ # RECOVERED — see `add_hecksagon`'s own comment for provenance.
244
+ # Concatenates every list-shaped fact; `binds` in particular is
245
+ # additive because an overlay REBINDING an aggregate (a new
246
+ # `persisted_by` for the same aggregate/verb) is meant to shadow
247
+ # the base's own bind at resolution time, not erase it outright —
248
+ # `Ports::Persistence::BindingPolicy.resolve`'s own "exactly one
249
+ # authoritative bind" check is what actually catches a genuine
250
+ # double-bind; this merge only concatenates, it does not itself
251
+ # decide which of two binds for the same aggregate wins.
252
+ def merge_hecksagons(a, b)
253
+ Bluebook::Hecksagon.new(
254
+ domain: a.domain,
255
+ binds: a.binds + b.binds,
256
+ subscriptions: a.subscriptions + b.subscriptions,
257
+ framework_members: a.framework_members + b.framework_members,
258
+ vendored_bluebooks: a.vendored_bluebooks + b.vendored_bluebooks
259
+ )
260
+ end
261
+
262
+ # RECOVERED AND GENERALIZED — see `add_world`'s own comment. `realm`/
263
+ # `latest` are scalars, so the overlay's value wins when present,
264
+ # else the base's survives; `settings` is a shallow merge keyed by
265
+ # verb (and `"verb:adapter"`) — an overlay entry for a key the base
266
+ # also declares REPLACES that key's whole resolved hash (the same
267
+ # all-or-nothing shape `WorldBuilder#method_missing` already builds
268
+ # each entry as), it does not deep-merge field by field within it.
269
+ def merge_worlds(a, b)
270
+ Bluebook::World.new(
271
+ domain: a.domain,
272
+ realm: b.realm || a.realm,
273
+ latest: b.latest || a.latest,
274
+ settings: a.settings.merge(b.settings)
275
+ )
276
+ end
277
+ end
278
+ end
279
+ end