hecks 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +403 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +62 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +51 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +0 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +38 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +148 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +131 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +80 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +168 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +391 -0
  31. data/lib/hecks/adapters/driven/postgres_era/lineage/era_store.rb +171 -0
  32. data/lib/hecks/adapters/driven/postgres_era/lineage/field_cache.rb +190 -0
  33. data/lib/hecks/adapters/driven/postgres_era/lineage/head_compiler.rb +429 -0
  34. data/lib/hecks/adapters/driven/postgres_era/lineage/mint_transaction.rb +166 -0
  35. data/lib/hecks/adapters/driven/postgres_era/lineage/provisioning.rb +286 -0
  36. data/lib/hecks/adapters/driven/postgres_era/lineage/resumable_backfill.rb +168 -0
  37. data/lib/hecks/adapters/driven/postgres_era/lineage/tail_merge.rb +163 -0
  38. data/lib/hecks/adapters/driven/postgres_era/lineage/transform_installer.rb +114 -0
  39. data/lib/hecks/adapters/driven/postgres_era/lineage.rb +137 -0
  40. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/coverage_check.rb +89 -0
  41. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/era_resolver.rb +82 -0
  42. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  43. data/lib/hecks/adapters/driven/postgres_era/lineage_manager/minter.rb +132 -0
  44. data/lib/hecks/adapters/driven/postgres_era/lineage_manager.rb +74 -0
  45. data/lib/hecks/adapters/driven/postgres_era.adapter +6 -0
  46. data/lib/hecks/adapters/driven/postgres_era.rb +671 -0
  47. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  48. data/lib/hecks/adapters/driven/prism.rb +80 -0
  49. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  50. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  51. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  52. data/lib/hecks/adapters/driven/sqlite/codec.rb +63 -0
  53. data/lib/hecks/adapters/driven/sqlite/projection.rb +105 -0
  54. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +180 -0
  55. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  56. data/lib/hecks/adapters/driven/sqlite.rb +285 -0
  57. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  58. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  59. data/lib/hecks/adapters/driven.rb +36 -0
  60. data/lib/hecks/adapters.rb +6 -0
  61. data/lib/hecks/behaviors/dsl.rb +96 -0
  62. data/lib/hecks/behaviors/expectations.rb +280 -0
  63. data/lib/hecks/behaviors/ir.rb +31 -0
  64. data/lib/hecks/behaviors/rspec.rb +42 -0
  65. data/lib/hecks/behaviors/runner.rb +96 -0
  66. data/lib/hecks/behaviors.rb +25 -0
  67. data/lib/hecks/bluebook/aggregate.rb +108 -0
  68. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  69. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  70. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  71. data/lib/hecks/bluebook/assembly/contracts.rb +409 -0
  72. data/lib/hecks/bluebook/assembly/marks.rb +227 -0
  73. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  74. data/lib/hecks/bluebook/assembly.rb +78 -0
  75. data/lib/hecks/bluebook/attribute.rb +96 -0
  76. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  77. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  78. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  79. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  80. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  81. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  82. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  83. data/lib/hecks/bluebook/behaviour/lifecycle.rb +51 -0
  84. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  85. data/lib/hecks/bluebook/behaviour/process_manager.rb +38 -0
  86. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  87. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  88. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  89. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  90. data/lib/hecks/bluebook/chapter.rb +68 -0
  91. data/lib/hecks/bluebook/command.rb +124 -0
  92. data/lib/hecks/bluebook/domain_port.rb +102 -0
  93. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  94. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +906 -0
  95. data/lib/hecks/bluebook/dsl/attribute_collector.rb +327 -0
  96. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  97. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1018 -0
  98. data/lib/hecks/bluebook/dsl/command_builder.rb +626 -0
  99. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  100. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  101. data/lib/hecks/bluebook/dsl/entity_builder.rb +305 -0
  102. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  103. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  104. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  105. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  106. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  107. data/lib/hecks/bluebook/dsl/policy_builder.rb +120 -0
  108. data/lib/hecks/bluebook/dsl/port_builder.rb +30 -0
  109. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  110. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +207 -0
  111. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  112. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  113. data/lib/hecks/bluebook/dsl/rule_reference.rb +172 -0
  114. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  115. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  116. data/lib/hecks/bluebook/dsl/word_gate.rb +221 -0
  117. data/lib/hecks/bluebook/dsl/world_builder.rb +77 -0
  118. data/lib/hecks/bluebook/dsl.rb +45 -0
  119. data/lib/hecks/bluebook/entity.rb +103 -0
  120. data/lib/hecks/bluebook/expression/canonical_form.rb +55 -0
  121. data/lib/hecks/bluebook/expression/evaluator.rb +261 -0
  122. data/lib/hecks/bluebook/expression/projection.json +170 -0
  123. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  124. data/lib/hecks/bluebook/expression/resolver.rb +630 -0
  125. data/lib/hecks/bluebook/expression.rb +13 -0
  126. data/lib/hecks/bluebook/hexagon.rb +60 -0
  127. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  128. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  129. data/lib/hecks/bluebook/meta_validator/judge.rb +559 -0
  130. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  131. data/lib/hecks/bluebook/meta_validator/port_judge.rb +47 -0
  132. data/lib/hecks/bluebook/meta_validator/readings.rb +350 -0
  133. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +312 -0
  134. data/lib/hecks/bluebook/meta_validator/shapes.rb +246 -0
  135. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  136. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  137. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  138. data/lib/hecks/bluebook/meta_validator.rb +452 -0
  139. data/lib/hecks/bluebook/model_check.rb +318 -0
  140. data/lib/hecks/bluebook/pattern_subset.rb +120 -0
  141. data/lib/hecks/bluebook/policy.rb +41 -0
  142. data/lib/hecks/bluebook/process_manager.rb +86 -0
  143. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  144. data/lib/hecks/bluebook/project_loader.rb +40 -0
  145. data/lib/hecks/bluebook/project_register.rb +82 -0
  146. data/lib/hecks/bluebook/query.rb +61 -0
  147. data/lib/hecks/bluebook/read_model.rb +109 -0
  148. data/lib/hecks/bluebook/reference.rb +74 -0
  149. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  150. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  151. data/lib/hecks/bluebook/translation.rb +92 -0
  152. data/lib/hecks/bluebook/value_object.rb +58 -0
  153. data/lib/hecks/bluebook.rb +74 -0
  154. data/lib/hecks/codemod.rb +342 -0
  155. data/lib/hecks/construct.rb +71 -0
  156. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  157. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  158. data/lib/hecks/deploy/oidc.json +18 -0
  159. data/lib/hecks/doc/reference.rb +389 -0
  160. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  161. data/lib/hecks/facade/cli_door.rb +116 -0
  162. data/lib/hecks/facade/cli_runner.rb +190 -0
  163. data/lib/hecks/facade/command_request.rb +105 -0
  164. data/lib/hecks/facade/handle.rb +173 -0
  165. data/lib/hecks/facade/json_door.rb +154 -0
  166. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  167. data/lib/hecks/facade/surface/chapter.rb +107 -0
  168. data/lib/hecks/facade/surface.rb +48 -0
  169. data/lib/hecks/facade.rb +44 -0
  170. data/lib/hecks/forms/app.rb +300 -0
  171. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  172. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  173. data/lib/hecks/forms/field_renderer.rb +170 -0
  174. data/lib/hecks/forms/field_shape.rb +232 -0
  175. data/lib/hecks/forms/html.rb +53 -0
  176. data/lib/hecks/forms/index_renderer.rb +35 -0
  177. data/lib/hecks/forms/page.rb +157 -0
  178. data/lib/hecks/forms/params.rb +132 -0
  179. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  180. data/lib/hecks/forms/record_renderer.rb +115 -0
  181. data/lib/hecks/forms/record_table.rb +63 -0
  182. data/lib/hecks/forms/reference_options.rb +30 -0
  183. data/lib/hecks/forms/value_object_shape.rb +46 -0
  184. data/lib/hecks/forms.rb +54 -0
  185. data/lib/hecks/fqn.rb +94 -0
  186. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  187. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  188. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  189. data/lib/hecks/framework/bluebook/governance.bluebook +130 -0
  190. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  191. data/lib/hecks/framework/oidc.json +39 -0
  192. data/lib/hecks/framework.rb +90 -0
  193. data/lib/hecks/freezer.rb +67 -0
  194. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  195. data/lib/hecks/fuzzing/isolated_boot.rb +92 -0
  196. data/lib/hecks/fuzzing/properties.rb +1146 -0
  197. data/lib/hecks/fuzzing/replay.rb +635 -0
  198. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  199. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +54 -0
  200. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  201. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +185 -0
  202. data/lib/hecks/fuzzing/sequence_generator.rb +130 -0
  203. data/lib/hecks/fuzzing/value_generator.rb +177 -0
  204. data/lib/hecks/fuzzing.rb +12 -0
  205. data/lib/hecks/grammar/evolve.rb +291 -0
  206. data/lib/hecks/grammar/expression.bluebook +417 -0
  207. data/lib/hecks/grammar/expression_operators.json +1152 -0
  208. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  209. data/lib/hecks/grammar/oidc.json +69 -0
  210. data/lib/hecks/grammar/translation.bluebook +203 -0
  211. data/lib/hecks/grammar.rb +178 -0
  212. data/lib/hecks/ir.rb +126 -0
  213. data/lib/hecks/language/adapter.bluebook +116 -0
  214. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  215. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  216. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  217. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  218. data/lib/hecks/language/bluebook/command.bluebook +437 -0
  219. data/lib/hecks/language/bluebook/entity.bluebook +316 -0
  220. data/lib/hecks/language/bluebook/policy.bluebook +172 -0
  221. data/lib/hecks/language/bluebook/process_manager.bluebook +264 -0
  222. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  223. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  224. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  225. data/lib/hecks/language/bluebook/syntax.bluebook +447 -0
  226. data/lib/hecks/language/bluebook/vocabulary.bluebook +364 -0
  227. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  228. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  229. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  230. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  231. data/lib/hecks/language/oidc.json +313 -0
  232. data/lib/hecks/language/port.bluebook +92 -0
  233. data/lib/hecks/language/translation/translation.bluebook +110 -0
  234. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  235. data/lib/hecks/language/world/wiring.bluebook +62 -0
  236. data/lib/hecks/language/world/world.bluebook +84 -0
  237. data/lib/hecks/literal.rb +125 -0
  238. data/lib/hecks/naming.rb +121 -0
  239. data/lib/hecks/ports/access_control.port +4 -0
  240. data/lib/hecks/ports/access_control.rb +62 -0
  241. data/lib/hecks/ports/agent/answers.rb +104 -0
  242. data/lib/hecks/ports/agent.port +4 -0
  243. data/lib/hecks/ports/agent.rb +167 -0
  244. data/lib/hecks/ports/authentication.port +4 -0
  245. data/lib/hecks/ports/authentication.rb +50 -0
  246. data/lib/hecks/ports/authorization.port +4 -0
  247. data/lib/hecks/ports/authorization.rb +59 -0
  248. data/lib/hecks/ports/clock.port +4 -0
  249. data/lib/hecks/ports/clock.rb +62 -0
  250. data/lib/hecks/ports/extraction.port +4 -0
  251. data/lib/hecks/ports/extraction.rb +37 -0
  252. data/lib/hecks/ports/identity_assignment.port +4 -0
  253. data/lib/hecks/ports/identity_assignment.rb +45 -0
  254. data/lib/hecks/ports/identity_generation.port +4 -0
  255. data/lib/hecks/ports/identity_generation.rb +49 -0
  256. data/lib/hecks/ports/identity_resolution.port +4 -0
  257. data/lib/hecks/ports/identity_resolution.rb +40 -0
  258. data/lib/hecks/ports/loading.port +4 -0
  259. data/lib/hecks/ports/loading.rb +13 -0
  260. data/lib/hecks/ports/persistence/append_only.rb +102 -0
  261. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  262. data/lib/hecks/ports/persistence/execution.rb +18 -0
  263. data/lib/hecks/ports/persistence/lineage.rb +277 -0
  264. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  265. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  266. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  267. data/lib/hecks/ports/persistence.port +4 -0
  268. data/lib/hecks/ports/persistence.rb +39 -0
  269. data/lib/hecks/ports/projection.port +4 -0
  270. data/lib/hecks/ports/projection.rb +76 -0
  271. data/lib/hecks/ports/query/in_memory.rb +60 -0
  272. data/lib/hecks/ports/query/ordering.rb +41 -0
  273. data/lib/hecks/ports/query.rb +36 -0
  274. data/lib/hecks/ports.rb +27 -0
  275. data/lib/hecks/projections/diagrams.rb +413 -0
  276. data/lib/hecks/projections/ir.rb +18 -0
  277. data/lib/hecks/projections/model/deviations.rb +98 -0
  278. data/lib/hecks/projections/model.rb +145 -0
  279. data/lib/hecks/projections/oidc.rb +110 -0
  280. data/lib/hecks/projections/parser_table.rb +159 -0
  281. data/lib/hecks/projections/reference.rb +38 -0
  282. data/lib/hecks/projections/shape.rb +31 -0
  283. data/lib/hecks/projections/statements.rb +110 -0
  284. data/lib/hecks/projections/vocabulary.rb +100 -0
  285. data/lib/hecks/projections.rb +32 -0
  286. data/lib/hecks/projector/cli_projector.rb +437 -0
  287. data/lib/hecks/projector/docs_projector.rb +321 -0
  288. data/lib/hecks/projector/exporter.rb +141 -0
  289. data/lib/hecks/projector/ir_projector.rb +18 -0
  290. data/lib/hecks/projector/narrate_projector.rb +243 -0
  291. data/lib/hecks/projector/target.rb +97 -0
  292. data/lib/hecks/projector.rb +186 -0
  293. data/lib/hecks/query_ir.rb +392 -0
  294. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  295. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  296. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  297. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  298. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  299. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  300. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  301. data/lib/hecks/query_specification/common/null_policy.rb +80 -0
  302. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  303. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  304. data/lib/hecks/query_specification/common/options.rb +32 -0
  305. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  306. data/lib/hecks/query_specification/common/specification.rb +10 -0
  307. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  308. data/lib/hecks/query_specification/field_path.rb +89 -0
  309. data/lib/hecks/query_specification/hop_path.rb +130 -0
  310. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  311. data/lib/hecks/query_specification.rb +14 -0
  312. data/lib/hecks/rendering.rb +48 -0
  313. data/lib/hecks/router/namespace_installer.rb +157 -0
  314. data/lib/hecks/router.rb +70 -0
  315. data/lib/hecks/runtime/caller.rb +50 -0
  316. data/lib/hecks/runtime/capability_graph.rb +44 -0
  317. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +124 -0
  318. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +248 -0
  319. data/lib/hecks/runtime/command_interpreter.rb +413 -0
  320. data/lib/hecks/runtime/command_rules/admissibility.rb +307 -0
  321. data/lib/hecks/runtime/command_rules/arithmetic.rb +254 -0
  322. data/lib/hecks/runtime/command_rules/authorization.rb +64 -0
  323. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  324. data/lib/hecks/runtime/command_rules/references.rb +181 -0
  325. data/lib/hecks/runtime/command_rules.rb +28 -0
  326. data/lib/hecks/runtime/dependency_planning.rb +231 -0
  327. data/lib/hecks/runtime/dispatcher.rb +286 -0
  328. data/lib/hecks/runtime/entity_element.rb +253 -0
  329. data/lib/hecks/runtime/entity_interpreter.rb +228 -0
  330. data/lib/hecks/runtime/era_check.rb +136 -0
  331. data/lib/hecks/runtime/era_guard/shape_diff.rb +128 -0
  332. data/lib/hecks/runtime/era_guard.rb +193 -0
  333. data/lib/hecks/runtime/era_tamper.rb +61 -0
  334. data/lib/hecks/runtime/errors.rb +100 -0
  335. data/lib/hecks/runtime/event.rb +51 -0
  336. data/lib/hecks/runtime/identity.rb +128 -0
  337. data/lib/hecks/runtime/instance.rb +120 -0
  338. data/lib/hecks/runtime/interpreting.rb +69 -0
  339. data/lib/hecks/runtime/loader.rb +143 -0
  340. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  341. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  342. data/lib/hecks/runtime/query_interpreter.rb +254 -0
  343. data/lib/hecks/runtime/reaction_invocation.rb +245 -0
  344. data/lib/hecks/runtime/read_model_interpreter.rb +266 -0
  345. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  346. data/lib/hecks/runtime/reference_hop.rb +99 -0
  347. data/lib/hecks/runtime/refusal_wording.rb +124 -0
  348. data/lib/hecks/runtime/registry/saga_persistence.rb +70 -0
  349. data/lib/hecks/runtime/registry/verification.rb +158 -0
  350. data/lib/hecks/runtime/registry.rb +218 -0
  351. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  352. data/lib/hecks/runtime/routing.rb +96 -0
  353. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  354. data/lib/hecks/runtime/saga_interpreter.rb +301 -0
  355. data/lib/hecks/runtime/storage_shape.rb +120 -0
  356. data/lib/hecks/runtime/tenant_check.rb +84 -0
  357. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  358. data/lib/hecks/runtime/value/admission.rb +112 -0
  359. data/lib/hecks/runtime/value/coercion.rb +500 -0
  360. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  361. data/lib/hecks/runtime/value.rb +125 -0
  362. data/lib/hecks/runtime.rb +95 -0
  363. data/lib/hecks/translation/audit/approval_digest.rb +31 -0
  364. data/lib/hecks/translation/audit/layer_one.rb +37 -0
  365. data/lib/hecks/translation/audit/layer_two.rb +77 -0
  366. data/lib/hecks/translation/audit/unfed_report.rb +42 -0
  367. data/lib/hecks/translation/audit.rb +70 -0
  368. data/lib/hecks/translation/reattest.rb +72 -0
  369. data/lib/hecks/translation/rule_compiler.rb +120 -0
  370. data/lib/hecks/translation/scaffold/differ.rb +183 -0
  371. data/lib/hecks/translation/scaffold/renderer.rb +41 -0
  372. data/lib/hecks/translation/scaffold/writer.rb +28 -0
  373. data/lib/hecks/translation/scaffold.rb +30 -0
  374. data/lib/hecks/translation.rb +11 -0
  375. data/lib/hecks/version.rb +16 -0
  376. data/lib/hecks/vocabulary.rb +213 -0
  377. data/lib/hecks.rb +118 -6
  378. metadata +401 -222
  379. data/bin/hecks +0 -7
  380. data/bin/hecks-package +0 -65
  381. data/bin/hecks_console +0 -12
  382. data/bin/hecks_serverless +0 -6
  383. data/lib/cli/build.rb +0 -14
  384. data/lib/cli/command_runner.rb +0 -28
  385. data/lib/cli/console.rb +0 -10
  386. data/lib/cli/generate.rb +0 -37
  387. data/lib/cli/hecks-cli.rb +0 -27
  388. data/lib/cli/test.rb +0 -57
  389. data/lib/console/commands.rb +0 -8
  390. data/lib/console/hecks-console.rb +0 -1
  391. data/lib/packager/README.md +0 -0
  392. data/lib/packager/app_runner.rb +0 -21
  393. data/lib/packager/args.rb +0 -26
  394. data/lib/packager/compatibility/fixnum.rb +0 -6
  395. data/lib/packager/hecks.rb +0 -39
  396. data/lib/packager/query_runner.rb +0 -21
  397. data/lib/packager/resources/Dockerfile +0 -11
  398. data/lib/packager/resources/app_binary +0 -7
  399. data/lib/packager/resources/bundle_config +0 -3
  400. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  401. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  402. data/lib/serverless/Domain +0 -32
  403. data/lib/serverless/cli.rb +0 -75
  404. data/lib/serverless/resources/command_name.js +0 -5
  405. data/lib/serverless/resources/environment.js +0 -7
  406. data/lib/serverless/resources/handler.js.tt +0 -28
  407. data/lib/serverless/resources/run_binary.js +0 -22
  408. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,49 @@
1
+ require_relative "../runtime/registry"
2
+
3
+ module Hecks
4
+ module Ports
5
+ # A creating command with no natural key needs a value from
6
+ # SOMEWHERE to be its identity — this is that somewhere. Resolved
7
+ # the same way `Ports::Extraction` resolves its own adapter (one
8
+ # adapter registry-wide implements this port, not a per-aggregate
9
+ # binding the way persistence needs — different aggregates have no
10
+ # real reason to want different id-generation strategies within
11
+ # one running app).
12
+ #
13
+ # REPLAY NEEDS NO SUPPRESSION HERE, ON PURPOSE. A UUID minted for a
14
+ # creating command's identity gets baked into that step's own args
15
+ # — an ordinary string value — the moment it's generated, by
16
+ # whoever issues the first, live dispatch. A recorded corpus
17
+ # script or a captured fuzz-replay step already holds that
18
+ # concrete value; replaying it calls `dispatch` with the SAME
19
+ # args, and this module is never consulted again, for the same
20
+ # reason `SecureRandom.uuid` never runs twice for one
21
+ # already-recorded step today. `Event#occurred_at`
22
+ # (`command_rules/emission.rb`) is the one other "environmental
23
+ # fact, not caller-supplied" this codebase has, and it's handled
24
+ # the identical way: it fires on every dispatch, replay included,
25
+ # and comparison simply excludes it.
26
+ module IdentityGeneration
27
+ NAME = "identity_generation"
28
+
29
+ module_function
30
+
31
+ def uuid(registry) = adapter(registry).uuid
32
+
33
+ def adapter(registry)
34
+ implementations = registry.adapters.values.select { |a| a.port == NAME }
35
+
36
+ case implementations.size
37
+ when 1 then Adapters.const_get(implementations.first.name)
38
+ when 0
39
+ raise Runtime::WiringError,
40
+ "no adapter implements the #{NAME} port — nothing can mint an identity"
41
+ else
42
+ raise Runtime::WiringError,
43
+ "#{implementations.size} adapters implement the #{NAME} port " \
44
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,4 @@
1
+ Hecks.port "identity_resolution" do
2
+ verb "resolved_by"
3
+ signal :reply
4
+ end
@@ -0,0 +1,40 @@
1
+ require_relative "../runtime/registry"
2
+
3
+ module Hecks
4
+ module Ports
5
+ # AN AUTHENTICATED (issuer, subject) PAIR, RESOLVED TO A STABLE
6
+ # `Identity` — the other half of the same symmetry `Authorization`
7
+ # already has for Governance: one adapter registry-wide answers this
8
+ # port, resolved the same zero/one/many way, so an application never
9
+ # has to name `Identity` directly to ask "who is this."
10
+ #
11
+ # Answers `nil` for a pair nothing has linked — the caller decides
12
+ # what that means (refuse, prompt to register, whatever), the same
13
+ # way `Authorization#holds_role?` answering `false` decides nothing
14
+ # on its own.
15
+ module IdentityResolution
16
+ NAME = "identity_resolution"
17
+
18
+ module_function
19
+
20
+ def resolve(registry, issuer:, subject:)
21
+ adapter(registry).resolve(registry, issuer: issuer, subject: subject)
22
+ end
23
+
24
+ def adapter(registry)
25
+ implementations = registry.adapters.values.select { |a| a.port == NAME }
26
+
27
+ case implementations.size
28
+ when 1 then Adapters.const_get(implementations.first.name)
29
+ when 0
30
+ raise Runtime::WiringError,
31
+ "no adapter implements the #{NAME} port — nothing can resolve an identity"
32
+ else
33
+ raise Runtime::WiringError,
34
+ "#{implementations.size} adapters implement the #{NAME} port " \
35
+ "(#{implementations.map(&:name).sort.join(', ')}) — the runtime will not choose for you"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ Hecks.port "loading" do
2
+ verb "loaded_by"
3
+ signal :reply
4
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "../adapters/driven/folder"
2
+
3
+ module Hecks
4
+ module Ports
5
+ module Loading
6
+ NAME = "loading"
7
+
8
+ module_function
9
+
10
+ def bootstrap = Adapters::Folder.new
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,102 @@
1
+ require_relative "../../runtime/registry"
2
+ require_relative "execution"
3
+
4
+ module Hecks
5
+ module Ports
6
+ module Persistence
7
+ # `mirrors` is durable replication intent. It is part of the same
8
+ # append as the authoritative state, never a second outbox store.
9
+ Entry = Struct.new(:operation, :id, :state, :mirrors, keyword_init: true) do
10
+ def save? = operation == "save"
11
+ def delete? = operation == "delete"
12
+ end
13
+
14
+ # Makes append-before-projection a port invariant. Adapters retain
15
+ # control of their durable format, but every one must accept the same
16
+ # entry stream and materialize current state from it.
17
+ class AppendOnly
18
+ attr_reader :adapter
19
+
20
+ def aggregate = @adapter.aggregate
21
+
22
+ def initialize(adapter)
23
+ @adapter = adapter
24
+ required = %i[append project entries]
25
+ missing = required.reject { |method| adapter.respond_to?(method) }
26
+ raise Runtime::WiringError,
27
+ "#{adapter.class} does not implement append-only persistence: #{missing.join(', ')}" unless missing.empty?
28
+ end
29
+
30
+ def find(id) = @adapter.find(id)
31
+ def all(**opts) = @adapter.all(**opts)
32
+ def count = @adapter.count
33
+ def entries = @adapter.entries
34
+
35
+ def capabilities
36
+ return [] unless @adapter.respond_to?(:persistence_capabilities)
37
+
38
+ Array(@adapter.persistence_capabilities).map(&:to_sym).freeze
39
+ end
40
+
41
+ def reset!
42
+ raise Runtime::WiringError, "append-only adapter cannot reset" unless @adapter.respond_to?(:reset!)
43
+
44
+ @adapter.reset!
45
+ end
46
+
47
+ # NOT an endless `def events = ... if ...` — that modifier binds to
48
+ # the WHOLE `def`, not just its body, so it evaluates against
49
+ # `@adapter` while `@adapter` is still nil (class-body time,
50
+ # before `initialize` ever runs) and silently skips defining the
51
+ # method at all. Found live: nothing in this codebase called
52
+ # `AppendOnly#events` before Memory got a `reset!` test that did.
53
+ def events
54
+ @adapter.events if @adapter.respond_to?(:events)
55
+ end
56
+
57
+ # An append is durable before a projection is attempted. Replaying the
58
+ # log restores a snapshot/table after a crash in that small window.
59
+ def recover!
60
+ entries.each { |entry| project(entry) }
61
+ self
62
+ end
63
+
64
+ def append(entry) = @adapter.append(entry)
65
+ def project(entry) = @adapter.project(entry)
66
+
67
+ def save(instance)
68
+ entry = Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
69
+ append(entry)
70
+ project(entry)
71
+ instance
72
+ end
73
+
74
+ def atomic_put(instance, insert_only: false)
75
+ unless capabilities.include?(:atomic_put) && @adapter.respond_to?(:atomic_put)
76
+ raise Runtime::WiringError, "#{@adapter.class} advertises no atomic_put persistence capability"
77
+ end
78
+
79
+ entry = Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
80
+ status = @adapter.atomic_put(entry, insert_only: insert_only)
81
+ Outcome.new(status: status, instance: instance)
82
+ end
83
+
84
+ def delete(id)
85
+ return false unless find(id)
86
+
87
+ entry = Entry.new(operation: "delete", id: id.to_s, state: nil)
88
+ append(entry)
89
+ project(entry)
90
+ true
91
+ end
92
+
93
+ def record_event(event) = @adapter.record_event(event) if @adapter.respond_to?(:record_event)
94
+ def query_read_model(domain, model, args, bluebook = nil)
95
+ return unless @adapter.respond_to?(:query_read_model)
96
+
97
+ @adapter.query_read_model(domain, model, args, bluebook)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,56 @@
1
+ require_relative "../../bluebook/hexagon"
2
+ require_relative "../../runtime/registry"
3
+
4
+ module Hecks
5
+ module Ports
6
+ module Persistence
7
+ # Resolves one aggregate's persistence topology. An unmarked binding is
8
+ # authoritative; roles are deliberately opt-in so a second adapter can
9
+ # never silently become a read source.
10
+ module BindingPolicy
11
+ module_function
12
+
13
+ def resolve(registry, domain, aggregate)
14
+ hexagon = registry.hecksagon(domain)
15
+ return default_binding(aggregate) unless hexagon
16
+
17
+ bindings = hexagon.binds_for(aggregate.hecks_name, VERB)
18
+ raise missing_binding(domain, aggregate) if bindings.empty?
19
+
20
+ authoritative = bindings.select { |bind| bind.role.nil? || bind.role.empty? }
21
+ raise ambiguous_binding(domain, aggregate, authoritative) unless authoritative.size == 1
22
+ raise unsupported_roles(domain, aggregate, bindings - authoritative) unless (bindings - authoritative).empty?
23
+
24
+ authoritative.first
25
+ end
26
+
27
+ def default_binding(aggregate)
28
+ Bluebook::Bind.new(aggregate: aggregate.hecks_name, verb: VERB, adapter: DEFAULT_ADAPTER)
29
+ end
30
+
31
+ def missing_binding(domain, aggregate)
32
+ Runtime::WiringError.new(
33
+ "#{domain}::#{aggregate.hecks_name} has no #{VERB} bind. #{domain} declares a " \
34
+ "hecksagon, so its wiring is being decided explicitly and an aggregate " \
35
+ "left out is a forgotten decision. Bind it, or say " \
36
+ "#{aggregate.hecks_name}.#{VERB}(#{DEFAULT_ADAPTER.inspect}) to keep it in memory on purpose."
37
+ )
38
+ end
39
+
40
+ def ambiguous_binding(domain, aggregate, authoritative)
41
+ Runtime::WiringError.new(
42
+ "#{domain}::#{aggregate.hecks_name} has #{authoritative.size} authoritative #{VERB} bindings. " \
43
+ "Declare exactly one adapter without a role."
44
+ )
45
+ end
46
+
47
+ def unsupported_roles(domain, aggregate, bindings)
48
+ Runtime::WiringError.new(
49
+ "#{domain}::#{aggregate.hecks_name} uses persistence role#{bindings.size == 1 ? '' : 's'} " \
50
+ "#{bindings.map(&:role).map(&:inspect).join(', ')}. Only persisted_by is supported."
51
+ )
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,18 @@
1
+ module Hecks
2
+ module Ports
3
+ module Persistence
4
+ # Adapter outcomes are runtime facts, never command lifecycle.
5
+ Outcome = Struct.new(:status, :instance, keyword_init: true) do
6
+ STATUSES = %i[inserted replaced updated conflicted missing saved].freeze
7
+
8
+ def initialize(status:, instance: nil)
9
+ normalized = status.to_sym
10
+ raise ArgumentError, "unknown persistence outcome #{status.inspect}" unless STATUSES.include?(normalized)
11
+
12
+ super(status: normalized, instance: instance)
13
+ freeze
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,277 @@
1
+ require_relative "../../naming"
2
+ require_relative "append_only"
3
+ require_relative "../../runtime/registry"
4
+
5
+ module Hecks
6
+ module Ports
7
+ module Persistence
8
+ # Translates a journal entry written under an old shape into the one
9
+ # the current bluebook declares — a renamed attribute, a renamed
10
+ # aggregate, or a field crossing a value-object boundary. Applied
11
+ # wherever entries are read, so replay derives the current head from
12
+ # history that was never rewritten — the port owns the meaning,
13
+ # adapters stay oblivious.
14
+ class Lineage
15
+ # `ancestor_name` is the declared name a rename came from (matches
16
+ # the held bluebook's own aggregate names); `ancestor_storage_name`
17
+ # is its derived, snake_case file/table name. Both are nil unless
18
+ # the aggregate itself was renamed.
19
+ attr_reader :ancestor_name, :ancestor_storage_name
20
+
21
+ def self.for(registry, domain, aggregate)
22
+ translation = registry.translations.find do |candidate|
23
+ candidate.domain == domain.to_s && candidate.for_aggregate(aggregate.name)
24
+ end
25
+ return nil unless translation
26
+
27
+ from_declared(translation.for_aggregate(aggregate.name), aggregate.name)
28
+ end
29
+
30
+ # One SPECIFIC edge's rules for one aggregate — what the mint
31
+ # path uses, where `for` would happily answer with whichever
32
+ # edge in the registry mentioned the aggregate first.
33
+ def self.from_declared(declared, aggregate_name)
34
+ return nil unless declared
35
+
36
+ renamed_aggregate = declared.was && declared.was != aggregate_name.to_s
37
+ ancestor_name = renamed_aggregate ? declared.was : nil
38
+ ancestor_storage_name = renamed_aggregate ? Naming.snake(declared.was) : nil
39
+
40
+ new(declared.renames, declared.moves, declared.converts, declared.drops,
41
+ retypes: declared.retypes, computes: declared.computes, rekeys: declared.rekeys,
42
+ backfills: declared.backfills,
43
+ ancestor_name: ancestor_name, ancestor_storage_name: ancestor_storage_name)
44
+ end
45
+
46
+ def initialize(renames, moves = [], converts = [], drops = [], retypes: [], computes: [], rekeys: [],
47
+ backfills: [], ancestor_name: nil, ancestor_storage_name: nil)
48
+ @renames = renames
49
+ @moves = moves
50
+ @converts = converts
51
+ @drops = drops
52
+ @retypes = retypes
53
+ @computes = computes
54
+ @rekeys = rekeys
55
+ @backfills = backfills
56
+ @ancestor_name = ancestor_name
57
+ @ancestor_storage_name = ancestor_storage_name
58
+ end
59
+
60
+ # Whether any rule in this edge is a compute — the one rule kind
61
+ # with no in-process implementation at all. An aggregate carrying
62
+ # one refuses to boot anywhere but Postgres, per-rule and by name,
63
+ # before the general drift machinery says anything vaguer.
64
+ def computes? = !@computes.empty?
65
+
66
+ # THE SINGLE SOURCE OF TRUTH for "does this edge rekey this
67
+ # aggregate" — every consumer (coverage_check.rb's identity gate,
68
+ # minter.rb's approval gate, layer_two.rb's audit, head_compiler.rb's
69
+ # SQL compilation) asks THIS, never re-derives it from `declared`
70
+ # independently. One accessor to change if what a rekey rule means
71
+ # ever needs to change, not four call sites in four files.
72
+ def rekey? = !@rekeys.empty?
73
+
74
+ # The rekey's own SQL — first-and-only rule, same one-per-aggregate
75
+ # assumption `compute` makes about its own list where it matters
76
+ # (an edge with more than one is a DSL-level decision, not
77
+ # something this reader arbitrates).
78
+ def rekey_sql = @rekeys.first&.sql
79
+
80
+ # The reference semantics for the five PORTABLE rule kinds —
81
+ # rename, move, convert, drop, and the aggregate-level `was:`.
82
+ # `retype` moves nothing (stored state never carries a type name)
83
+ # and `compute` is deliberately not applied here: its SQL is its
84
+ # only implementation, so this transform neither imitates nor
85
+ # checks it — the source field passes through untouched, and the
86
+ # audit verifies compute output against the matview alone.
87
+ def translate(entry)
88
+ return entry unless entry.save? && entry.state
89
+
90
+ # Deep, not shallow: a move or convert reaches INTO a nested
91
+ # value-object hash, and a shallow dup would quietly mutate
92
+ # the caller's copy of the original entry.
93
+ state = deep_dup(entry.state)
94
+ @renames.each { |old_name, new_name| state[new_name] = state.delete(old_name) if state.key?(old_name) }
95
+ @moves.each { |move| apply_move(state, move) }
96
+ @converts.each { |convert| apply_convert(state, convert) }
97
+ @drops.each { |name| apply_drop(state, name) }
98
+ # LAST, and only where nothing already answered — a backfill
99
+ # fills the gap a rename/move/convert left untouched, never
100
+ # overwrites a value that already made it across.
101
+ @backfills.each { |backfill| state[backfill.name] = backfill.default unless state.key?(backfill.name) }
102
+ Entry.new(operation: entry.operation, id: entry.id, state: state, mirrors: entry.mirrors)
103
+ end
104
+
105
+ # Whether this translation names `path` as an old key it accounts
106
+ # for — the rename, move, or convert it came from, or an explicit
107
+ # drop. `path` is a bare name ("cost") or a dotted value-object
108
+ # member ("price.currency"); a rule covering the WHOLE top-level
109
+ # attribute (a rename, a top-level move/convert/drop) also covers
110
+ # anything nested under it, since the whole value travels or goes
111
+ # away together. Used to catch a field — or a value object's own
112
+ # member — that vanished (or silently changed type) without
113
+ # anything explaining it, even when some OTHER field is covered.
114
+ #
115
+ # `backfills` matches on the WHOLE name only, never a dotted
116
+ # prefix — a backfill names a top-level attribute that is new
117
+ # outright (nothing to be a prefix of on the held side), unlike
118
+ # every rule above it, which explains a path that existed and
119
+ # moved, converted, or vanished.
120
+ def explains?(path)
121
+ path = path.to_s
122
+ top = path.split(".").first
123
+
124
+ @renames.key?(top.to_sym) ||
125
+ @moves.any? { |move| move.from == path || move.from.split(".").first == top } ||
126
+ @converts.any? { |convert| convert.from == path || convert.from.split(".").first == top } ||
127
+ @drops.any? { |drop| drop.to_s == path || drop.to_s.split(".").first == top } ||
128
+ @computes.any? { |compute| compute.from == path || compute.from.split(".").first == top } ||
129
+ @backfills.any? { |backfill| backfill.name.to_s == top }
130
+ end
131
+
132
+ # THE DESTINATION-SIDE TWIN of `explains?` above, which only ever
133
+ # asks about a rule's SOURCE. `unsafe_additions` asks a different
134
+ # question — not "was this vanished path accounted for" but "does
135
+ # an existing record end up with a value here" — and a move or
136
+ # convert whose `to:` lands a old field inside a BRAND-NEW
137
+ # top-level attribute (`weight` becoming `contents.weight` when
138
+ # `Contents` did not exist before) fills that attribute for an
139
+ # existing record exactly as a `backfill` would, even though
140
+ # nothing named `contents` explains any vanished path. `compute`
141
+ # counts on the same terms `explains?` already grants it
142
+ # elsewhere in this file — Postgres-only and audited, not
143
+ # actually applied by THIS method, the same gap the vanish side
144
+ # already lives with.
145
+ #
146
+ # `@renames.value?` belongs here too, and used to be missing: a
147
+ # bare `rename :cost, to: :amount` is the plainest possible
148
+ # covering rule there is (`translate` above applies it
149
+ # unconditionally, no lookup table, no per-record ambiguity —
150
+ # simpler than a move or convert, which both got their `fills?`
151
+ # entry from the start), and its absence meant `unsafe_additions`
152
+ # reported the new name as an unexplained required addition on
153
+ # EVERY rename-only edge, the single most common translation
154
+ # shape there is. `explains?` already checked the source side
155
+ # (`@renames.key?`); `fills?` is the symmetric destination-side
156
+ # check that was never added alongside it.
157
+ def fills?(path)
158
+ path = path.to_s
159
+
160
+ @renames.value?(path.to_sym) ||
161
+ @moves.any? { |move| move.to.split(".").first == path } ||
162
+ @converts.any? { |convert| convert.to.split(".").first == path } ||
163
+ @computes.any? { |compute| compute.to.split(".").first == path } ||
164
+ @backfills.any? { |backfill| backfill.name.to_s == path }
165
+ end
166
+
167
+ # Whether a declared retype says the pair of TYPE names means the
168
+ # same shape — a value object or entity whose own name changed
169
+ # with its members intact. Nothing in the stored data carries the
170
+ # type name, so this never moves a value; it only satisfies the
171
+ # era diff's literal type-name comparison.
172
+ def retype?(held_type, current_type)
173
+ @retypes.any? { |retype| retype.from == held_type.to_s && retype.to == current_type.to_s }
174
+ end
175
+
176
+ private
177
+
178
+ def deep_dup(node)
179
+ case node
180
+ when Hash then node.transform_values { |value| deep_dup(value) }
181
+ when Array then node.map { |item| deep_dup(item) }
182
+ else node
183
+ end
184
+ end
185
+
186
+ def apply_drop(state, name)
187
+ name = name.to_s
188
+ top, member = name.split(".", 2)
189
+ top = top.to_sym
190
+
191
+ if member
192
+ nested = state[top]
193
+ if nested.is_a?(Hash)
194
+ nested.delete(member)
195
+ state.delete(top) if nested.empty?
196
+ end
197
+ else
198
+ state.delete(top)
199
+ end
200
+ end
201
+
202
+ # A dotted path's first segment is a top-level (symbol) key; a
203
+ # second segment reaches into a value-object member, stored with a
204
+ # string key exactly as the adapter's own journal reader loaded it.
205
+ def apply_move(state, move)
206
+ old_top, old_member = move.from.split(".", 2)
207
+ new_top, new_member = move.to.split(".", 2)
208
+ old_top = old_top.to_sym
209
+
210
+ value, present = extract(state, old_top, old_member)
211
+ return unless present
212
+
213
+ insert(state, new_top.to_sym, new_member, value, rule: "move #{move.from} to: #{move.to}")
214
+ end
215
+
216
+ # A convert is a move whose value has nothing in common with its
217
+ # replacement — the same path machinery, plus a lookup. A value
218
+ # with no entry in the table refuses loudly rather than carry an
219
+ # unrecognized value silently into the new era.
220
+ def apply_convert(state, convert)
221
+ old_top, old_member = convert.from.split(".", 2)
222
+ new_top, new_member = convert.to.split(".", 2)
223
+ old_top = old_top.to_sym
224
+
225
+ raw, present = extract(state, old_top, old_member)
226
+ return unless present
227
+
228
+ unless convert.values.key?(raw)
229
+ raise Runtime::WiringError,
230
+ "cannot translate #{convert.from}: #{raw.inspect} has no mapping in its " \
231
+ "convert's values: table. Add #{raw.inspect} => ... to cover it."
232
+ end
233
+
234
+ insert(state, new_top.to_sym, new_member, convert.values[raw], rule: "convert #{convert.from} to: #{convert.to}")
235
+ end
236
+
237
+ def extract(state, top, member)
238
+ return [nil, false] unless state.key?(top)
239
+ return [state.delete(top), true] unless member
240
+
241
+ nested = state[top]
242
+ return [nil, false] unless nested.is_a?(Hash) && nested.key?(member)
243
+
244
+ value = nested.delete(member)
245
+ state.delete(top) if nested.empty?
246
+ [value, true]
247
+ end
248
+
249
+ # ADVERSARIAL FINDING, not a hypothetical: a destination whose
250
+ # top segment ALREADY holds a value — most commonly a reference,
251
+ # stored as a bare scalar id — used to be silently clobbered with
252
+ # an empty hash the moment a dotted destination needed to nest
253
+ # under it (`state[top] ||= {}` only guards nil/false, so a
254
+ # truthy non-Hash sailed straight through to `state[top][member] =`,
255
+ # i.e. `"team-1"["detail"] =`, which is String#[]= and raised an
256
+ # unrelated-looking IndexError). Whether it crashed or silently
257
+ # replaced the value, this is a `drop` that never declared
258
+ # itself — the one thing this language exists to make explicit
259
+ # (see apply_convert's own refusal above, the same shape). Refuse
260
+ # by name instead, on both sides: the SQL half (hecks_tr_insert)
261
+ # raises the identical wording.
262
+ def insert(state, top, member, value, rule:)
263
+ return state[top] = value unless member
264
+
265
+ if state.key?(top) && !state[top].is_a?(Hash)
266
+ raise Runtime::WiringError,
267
+ "cannot #{rule}: #{top} already holds #{state[top].inspect}, not a value this can nest " \
268
+ "under — moving into it would discard that value silently. Rename or drop #{top} first."
269
+ end
270
+
271
+ state[top] ||= {}
272
+ state[top][member] = value
273
+ end
274
+ end
275
+ end
276
+ end
277
+ end
@@ -0,0 +1,25 @@
1
+ module Hecks
2
+ module Ports
3
+ module Persistence
4
+ # THE NO-OP SAGA STORE — what `Registry#saga_persistence` hands
5
+ # back for a domain whose resolved adapter doesn't implement the
6
+ # (optional) saga-persistence capability: Memory, deliberately
7
+ # (sagas stay in-memory-only, exactly as they always have), and
8
+ # any `RemoteRuntime`-shaped adapter (Lambda; the real durability
9
+ # for those lives on the other side of the call — Phase 1, not
10
+ # here). `SagaInterpreter`'s hook points call through
11
+ # `saga_persistence(domain)` unconditionally; this is what makes
12
+ # that safe without a `respond_to?` check at every call site,
13
+ # mirroring the same optional-capability shape
14
+ # `Ports::Persistence::AppendOnly` already gives adapters that
15
+ # don't implement `reset!`/`events`/`record_event`.
16
+ class NullSagaStore
17
+ def save_saga(**) = nil
18
+ def delete_saga(**) = nil
19
+ def each_saga(*) = nil
20
+ end
21
+
22
+ NULL_SAGA_STORE = NullSagaStore.new
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ require_relative "../../runtime/registry"
2
+
3
+ module Hecks
4
+ module Ports
5
+ module Persistence
6
+ # THE OTHER SHAPE AN ADAPTER CAN BE. `AppendOnly` names one shape
7
+ # already — something that stores bytes, locally (Postgres, SQLite,
8
+ # Heki) or over the network (D1), doesn't matter, the point is it
9
+ # does real local interpretation and has real entries to replay.
10
+ # This names the second, different shape: not "storage reached
11
+ # remotely," but the real INTERPRETER itself living behind a call
12
+ # boundary — `append`/`project` don't do partial local work and
13
+ # then fail, they raise unconditionally, because there is no local
14
+ # write-ahead log to have; `entries` is always `[]` for the same
15
+ # reason (`AppendOnly#recover!` replaying zero entries here is a
16
+ # correct no-op, not a lie).
17
+ #
18
+ # `Adapters::Lambda` is the first adapter to `include` this, not
19
+ # the only one meant to — any future adapter for some other
20
+ # remote-runtime target (a different compute platform, a gRPC
21
+ # stub, whatever) gets this shape for free by including one module
22
+ # instead of re-deriving the same raise-on-write/always-empty-
23
+ # entries boilerplate from scratch. And because it's a real,
24
+ # checkable module rather than an adapter's own ad hoc
25
+ # implementation, a caller can ask "is this adapter a remote-
26
+ # runtime delegate?" as a genuine capability check
27
+ # (`registry.adapter_class(name) <= RemoteRuntime`,
28
+ # `Runtime::RemoteDispatcher`'s own use) instead of comparing
29
+ # adapter names by string.
30
+ module RemoteRuntime
31
+ def append(*)
32
+ raise Runtime::WiringError,
33
+ "#{self.class.name} is a remote-runtime delegate — dispatch through " \
34
+ "Runtime::RemoteDispatcher instead of writing through the repository directly"
35
+ end
36
+ alias_method :project, :append
37
+
38
+ def entries = []
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "append_only"
2
+
3
+ module Hecks
4
+ module Ports
5
+ module Persistence
6
+ # Turns a declared adapter binding plus its world configuration into a
7
+ # concrete repository. Selection policy stays out of adapter creation.
8
+ module RepositoryFactory
9
+ module_function
10
+
11
+ def build(registry, domain, aggregate, bind, recover: true, settings_verb: VERB)
12
+ registry.check_verb(bind)
13
+ settings = (registry.world(domain)&.for_binding(settings_verb, bind.adapter) || {})
14
+ .reject { |key, _| key.to_sym == :role }
15
+ registry.check_settings(bind, settings)
16
+ # The domain and the resolved era ride along after the
17
+ # declared-settings check: a lineage adapter journals per
18
+ # DOMAIN and writes into its own ERA's partition — neither of
19
+ # which a world's settings carry.
20
+ adapter = registry.adapter_class(bind.adapter)
21
+ .new(aggregate: aggregate,
22
+ settings: settings.merge(domain: domain.to_s, era: registry.resolved_eras[domain.to_s]),
23
+ root: registry.root)
24
+ repository = AppendOnly.new(adapter)
25
+ recover ? repository.recover! : repository
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ Hecks.port "persistence" do
2
+ verb "persisted_by"
3
+ signal :reply
4
+ end