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,51 @@
1
+ require_relative "../../ports/query/in_memory"
2
+ require_relative "../../ports/query/ordering"
3
+ require_relative "../../query_specification/common/order_by"
4
+ require_relative "../../query_specification/field_path"
5
+ require_relative "../../runtime/errors"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # SHARED BY every adapter that holds decoded Ruby records rather than
10
+ # running real SQL (Memory, Lambda, Heki) — the identical dotted-path
11
+ # value-object member-picking Postgres/Sqlite/D1's own
12
+ # order_expression does (numeric member wins, else the one-field
13
+ # convention "value"), just walked in Ruby instead of compiled to a
14
+ # JSONB/json_extract path, because there is no query engine
15
+ # underneath any of these three to hide that walk inside.
16
+ module InMemoryOrdering
17
+ module_function
18
+
19
+ # order_by IS A RUNTIME VALUE (an HTTP query param, in the
20
+ # console's case), not framework-authored bluebook source — see
21
+ # postgres.rb's own all for the full reasoning. Whitelisted the
22
+ # identical way before FieldPath.dig ever runs.
23
+ def ordered(records, aggregate:, order_by:, direction:)
24
+ return records unless order_by
25
+
26
+ name = order_by.to_s.split(".").first
27
+ raise Runtime::WiringError, "#{aggregate.name} has no attribute #{order_by.inspect} to order by" unless aggregate.lifecycle&.field.to_s == name || aggregate.attribute(name)
28
+
29
+ path = sortable_path(aggregate, order_by)
30
+ spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
31
+ Ports::Query::Ordering.apply(records, spec, nil, identity: ->(record) { record.id.to_s }) do |record|
32
+ Ports::Query::InMemory.comparable(QuerySpecification::FieldPath.dig(record, path))
33
+ end
34
+ end
35
+
36
+ def sortable_path(aggregate, field)
37
+ name, *path = field.to_s.split(".")
38
+ return field.to_s unless path.empty?
39
+
40
+ attribute = aggregate.attribute(name)
41
+ return field.to_s if aggregate.lifecycle&.field.to_s == name || attribute.nil?
42
+
43
+ vo = aggregate.value_object(attribute.type)
44
+ return field.to_s unless vo
45
+
46
+ member = vo.attributes.find { |a| %w[Integer Float].include?(a.type) }&.name || "value"
47
+ "#{name}.#{member}"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ require "json"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class Lambda
6
+ # THE THIN AWS CLIENT — shared by this adapter's own read methods
7
+ # (Lambda#find/#all/#count/#query) and Runtime::RemoteDispatcher's
8
+ # write methods. One Lambda invoke, one JSON round trip, nothing
9
+ # domain-specific: neither caller needs to know an AWS SDK is
10
+ # involved at all.
11
+ #
12
+ # FUNCTION NAME IS COMPUTED, NOT PASSED — `"hecks-#{domain}"`,
13
+ # lowercased, matches bin/project_deploy's own `stack_name`
14
+ # exactly (bin/project_deploy: `stack_name = "hecks-#{domain_name}"`,
15
+ # `domain_name = File.basename(domain)`). `domain` here is the
16
+ # bluebook's OWN declared name (`Embryonaut`, not the directory);
17
+ # today's real corpus has directory name == declared name
18
+ # lowercased for every domain that deploys, so `.downcase` alone
19
+ # reproduces the same string bin/project_deploy computes from the
20
+ # directory — this is a real, load-bearing assumption, not a
21
+ # coincidence to lean on silently forever if that ever stops
22
+ # holding.
23
+ class Client
24
+ def initialize(domain:, region:)
25
+ require "aws-sdk-lambda"
26
+ @function_name = "hecks-#{domain.to_s.downcase}"
27
+ @client = Aws::Lambda::Client.new(region: region)
28
+ end
29
+
30
+ # THE WHOLE DOMAIN, EVERY TIME — matches dispatch::read's own
31
+ # rehydrate-the-full-journal design (Phase 1, rust/host). No
32
+ # caching here, deliberately not even per-request: Lambda#all
33
+ # used to memoize this across calls, which silently served
34
+ # stale reads for a warm web process's whole lifetime once a
35
+ # write happened elsewhere — see Lambda#instances's own comment
36
+ # on the real, live bug that caught it.
37
+ def read
38
+ invoke({ "read" => true })
39
+ end
40
+
41
+ def dispatch(verb, args, role: nil)
42
+ payload = { "verb" => verb, "args" => args }
43
+ payload["role"] = role if role
44
+ invoke(payload)
45
+ end
46
+
47
+ private
48
+
49
+ def invoke(payload)
50
+ response = @client.invoke(function_name: @function_name, payload: JSON.generate(payload))
51
+ body = JSON.parse(response.payload.read)
52
+
53
+ if response.function_error
54
+ raise Runtime::WiringError,
55
+ "Lambda #{@function_name} (#{response.function_error}): #{body['errorMessage'] || body}"
56
+ end
57
+
58
+ body
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,4 @@
1
+ Hecks.adapter "Lambda" do
2
+ port "persistence"
3
+ field :region
4
+ end
@@ -0,0 +1,131 @@
1
+ require_relative "lambda/client"
2
+ require_relative "../../ports/query/in_memory"
3
+ require_relative "../../ports/persistence/remote_runtime"
4
+ require_relative "in_memory_ordering"
5
+ require_relative "../../runtime/instance"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ # THE READ-SIDE HALF OF LAMBDA ROUTING — `persisted_by("Lambda")`'s
10
+ # own adapter, resolved through the SAME `Ports::Persistence`
11
+ # machinery `persisted_by("Postgres")`/`persisted_by("Memory")`
12
+ # already use (`Runtime::Registry#repository`, `RepositoryFactory
13
+ # .build`) — no new framework plumbing, just a new adapter class at
14
+ # the name the existing lookup already expects. See
15
+ # `Runtime::RemoteDispatcher` for the write-side half; the two
16
+ # compose through one shared `Client` per domain rather than each
17
+ # inventing its own AWS wiring.
18
+ #
19
+ # READ-ONLY, DELIBERATELY — `append`/`project` raise rather than
20
+ # silently no-op. A write reaching this class would mean
21
+ # `Runtime::CommandInterpreter` ran locally against a Lambda-routed
22
+ # domain, which is exactly the bypass `Runtime::RemoteDispatcher`
23
+ # exists to prevent (business-rule validation must happen in Rust
24
+ # for these domains, not be pre-computed here against an
25
+ # incomplete local view). A loud failure here is the correct
26
+ # outcome, not a bug to route around.
27
+ class Lambda
28
+ include Ports::Persistence::RemoteRuntime
29
+
30
+ attr_reader :aggregate
31
+
32
+ def initialize(aggregate:, settings: {}, root: nil)
33
+ @aggregate = aggregate
34
+ domain = settings[:domain] || settings["domain"] || aggregate.name
35
+ region = settings[:region] || settings["region"] || "us-east-1"
36
+ # TWO DIFFERENT "domain"s, deliberately not conflated: `domain`
37
+ # (this aggregate's OWN bluebook name — "Identity", "Governance")
38
+ # only ever prefixes the instances lookup, since that's how
39
+ # rust/host's own `Store::instances()` keys every record
40
+ # (`registry.rb`'s own `"#{a[:domain_name]}::#{a[:name]}#"`
41
+ # dump format, unchanged by which chapter attached it). The
42
+ # FUNCTION to actually call is a different question: Governance
43
+ # and Identity aggregates are compiled into the ATTACHING
44
+ # domain's own Lambda (one merged `Store` per target — Phase 0's
45
+ # framework-bluebook work), never a function of their own, so
46
+ # `settings[:domain]` is the wrong signal for `Client.new`.
47
+ # `root` is the boot's own directory (`Registry#root`, shared by
48
+ # EVERY bluebook in one registry regardless of which one
49
+ # attached it) — `File.basename(root)` reproduces the exact
50
+ # same string `bin/project_deploy`'s own `stack_name` computes
51
+ # from the domain PATH, so the two can never name two
52
+ # different functions for the same deploy... on a LOCAL boot,
53
+ # where `root` is a real project directory. Inside the deployed
54
+ # Lambda itself `root` is ALWAYS `/var/task` (every Lambda's own
55
+ # fixed code root, regardless of domain) — `File.basename` gives
56
+ # "task", not the domain name, and invokes the wrong function
57
+ # entirely. A real, live AccessDeniedException on
58
+ # "hecks-task" caught this: invisible through every earlier
59
+ # phase's own verify step, all run from a local boot, until
60
+ # WebFunction became the first Ruby process to EVER make this
61
+ # exact call from inside a deployed Lambda. `DOMAIN_NAME` (set
62
+ # by bin/project_deploy's own WebFunction Environment) is the
63
+ # real, unambiguous signal in that specific context; the
64
+ # root-basename heuristic stays as the local-boot fallback,
65
+ # unchanged.
66
+ function_domain = ENV["DOMAIN_NAME"] || (root ? File.basename(root) : domain)
67
+ @client = Client.new(domain: function_domain, region: region)
68
+ @prefix = "#{domain}::#{aggregate.hecks_name}#"
69
+ end
70
+
71
+ def find(id)
72
+ instances[id.to_s]
73
+ end
74
+
75
+ def all(order_by: nil, direction: :asc)
76
+ InMemoryOrdering.ordered(instances.values, aggregate: @aggregate, order_by: order_by, direction: direction)
77
+ end
78
+
79
+ def count = instances.size
80
+
81
+ def query(specification, args = {}, context: {})
82
+ Ports::Query::InMemory.execute(instances.values, specification, args)
83
+ end
84
+
85
+ # `entries`/`append`/`project` — the append-only contract
86
+ # `Ports::Persistence::AppendOnly` requires of every adapter
87
+ # (raises at construction if any are missing). Answered by
88
+ # `Ports::Persistence::RemoteRuntime`, included above: `entries` is
89
+ # `[]` unconditionally (Lambda's own Postgres is already the
90
+ # durable store — rust/host's rehydrate-and-replay journal, Phase
91
+ # 1 — there is no local write-ahead log for `recover!` to replay),
92
+ # `append`/`project` raise rather than silently no-op.
93
+
94
+ private
95
+
96
+ # RE-FETCHED EVERY CALL, deliberately NOT memoized across them —
97
+ # this adapter itself is long-lived (one instance per aggregate,
98
+ # held by the registry `RUNTIME = Hecks.boot(...)` builds ONCE
99
+ # per Lambda web process — WebFunction's own top-level constant,
100
+ # reused warm across every HTTP request that process serves, not
101
+ # rebuilt per request the way a memoize-for-one-request comment
102
+ # here used to assume). A real, live bug caught this: a mutation
103
+ # dispatched fine (RemoteDispatcher always calls the dispatch
104
+ # Lambda fresh) and the very next `.all` on the SAME warm
105
+ # container kept returning the state from BEFORE that mutation,
106
+ # forever, until the container cold-started — memoizing here
107
+ # made every write invisible to every read on a warm container.
108
+ # Keyed by bare id (the part after "Domain::Aggregate#"), not the
109
+ # full "Domain::Aggregate#id" string — callers already know which
110
+ # aggregate they're asking this instance about.
111
+ def instances
112
+ @client.read.fetch("instances", {}).filter_map { |key, state|
113
+ next unless key.start_with?(@prefix)
114
+
115
+ [key.delete_prefix(@prefix), build_instance(key.delete_prefix(@prefix), state)]
116
+ }.to_h
117
+ end
118
+
119
+ # Lambda's own JSON response is already Ruby-decoded with STRING
120
+ # keys (plain `JSON.parse`, no `symbolize_names:`) — every other
121
+ # adapter's own `instance(row)` builder (Postgres, Sqlite) hands
122
+ # `Runtime::Instance.new` a SYMBOL-keyed state hash instead. A
123
+ # round trip through `JSON.generate`/`JSON.parse` is simpler and
124
+ # safer than a hand-rolled deep-symbolize helper for what is not
125
+ # a hot path.
126
+ def build_instance(id, state)
127
+ Runtime::Instance.new(aggregate: @aggregate, id: id, state: JSON.parse(JSON.generate(state), symbolize_names: true))
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "Memory" do
2
+ port "persistence"
3
+ end
@@ -0,0 +1,103 @@
1
+ require_relative "../../ports/persistence/append_only"
2
+ require_relative "../../ports/query/in_memory"
3
+ require_relative "in_memory_ordering"
4
+ require_relative "../../runtime/instance"
5
+
6
+ module Hecks
7
+ module Adapters
8
+ class Memory
9
+ # TENANT-CAPABLE TRIVIALLY — see Runtime::TenantCheck's own header
10
+ # for the full reasoning. `@records` is a plain instance variable;
11
+ # two `Runtime.boot` calls build two entirely separate Registry
12
+ # objects and, through them, two entirely separate Memory
13
+ # instances, so two tenant boots never share this adapter's state
14
+ # by construction — nothing here needs to know "tenant" exists.
15
+ def self.tenant_capable? = true
16
+ def persistence_capabilities = [:atomic_put]
17
+
18
+ attr_reader :aggregate
19
+
20
+ def initialize(aggregate:, settings: {}, root: nil)
21
+ @aggregate = aggregate
22
+ @records = {}
23
+ @events = []
24
+ @entries = []
25
+ end
26
+
27
+ def find(id) = @records[id.to_s]
28
+ def count = @records.size
29
+
30
+ def all(order_by: nil, direction: :asc)
31
+ InMemoryOrdering.ordered(@records.values, aggregate: @aggregate, order_by: order_by, direction: direction)
32
+ end
33
+
34
+ def query(specification, args = {}, context: {})
35
+ Ports::Query::InMemory.execute(all, specification, args, registry: context[:registry])
36
+ end
37
+
38
+ def append(entry)
39
+ @entries << entry
40
+ entry
41
+ end
42
+
43
+ def project(entry)
44
+ if entry.save?
45
+ @records[entry.id] = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state.dup)
46
+ else
47
+ @records.delete(entry.id)
48
+ end
49
+ end
50
+
51
+ def save(instance)
52
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
53
+ append(entry)
54
+ project(entry)
55
+ end
56
+
57
+ # One in-memory critical section in the only thread touching this plain
58
+ # Hash: classify and replace without a preliminary repository lookup.
59
+ # Durable append and projection remain ordered exactly as ordinary save.
60
+ def atomic_put(entry, insert_only: false)
61
+ exists = @records.key?(entry.id.to_s)
62
+ return :conflicted if insert_only && exists
63
+
64
+ status = exists ? :replaced : :inserted
65
+ append(entry)
66
+ project(entry)
67
+ status
68
+ end
69
+
70
+ def delete(id)
71
+ entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
72
+ append(entry)
73
+ project(entry)
74
+ end
75
+
76
+ def record_event(event) = @events << event
77
+
78
+ def events = @events
79
+
80
+ def entries = @entries.dup
81
+
82
+ # Every other driven adapter (Postgres/PostgresEra/Sqlite/D1)
83
+ # already implements this — `Ports::Persistence::AppendOnly#reset!`
84
+ # forwards to it and only raises when the wrapped adapter doesn't
85
+ # respond to `reset!` at all, which was always this adapter's own
86
+ # gap, not a deliberate omission (nothing about "in memory" implies
87
+ # "cannot be cleared"). Existing callers that fully re-`Hecks.boot`
88
+ # a domain per test case don't need this — they get a brand new
89
+ # `Memory` instance, with brand new empty `@records`/`@events`/
90
+ # `@entries`, for free. This is for the other case: a caller that
91
+ # deliberately keeps ONE booted runtime across many cases (to skip
92
+ # `load_domain`'s own per-boot parse/verify cost) and wants each
93
+ # case to start from the same clean slate `Hecks.boot` would have
94
+ # given it, without paying for a fresh boot to get there.
95
+ def reset!
96
+ @records = {}
97
+ @events = []
98
+ @entries = []
99
+ self
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "MockStripeAdapter" do
2
+ port "checkout"
3
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hecks
4
+ module Adapters
5
+ # A GENERIC STAND-IN for a real Checkout-Session-style adapter — no
6
+ # network call, no real account, matching whatever real payment
7
+ # adapter a project pairs it with by shape alone
8
+ # (`create_session(event:, registration_id:, success_url:, cancel_url:)
9
+ # -> a URL string`), the same way `Memory` stands in for a real
10
+ # persistence adapter without knowing anything about the domain bound
11
+ # to it. Never reads `event`'s own fields — a real adapter needs the
12
+ # price/name inside it to build a session a payer actually sees;
13
+ # this only needs to look enough like one to swap in.
14
+ #
15
+ # The URL it returns carries the same `registration_id` a real
16
+ # webhook's metadata would, so a caller building its own signed
17
+ # webhook against it (a smoke test, CI) has something real to key
18
+ # off of — the real shape production has, minus the network hop.
19
+ class MockStripeAdapter
20
+ def initialize(aggregate: nil, settings: {}, root: nil); end
21
+
22
+ def create_session(event:, registration_id:, success_url:, cancel_url:)
23
+ separator = success_url.include?("?") ? "&" : "?"
24
+ "#{success_url}#{separator}mock_checkout=1&mock_registration_id=#{registration_id}"
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,80 @@
1
+ require_relative "../../../runtime/value"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class Postgres
6
+ # How a state crosses the column boundary: which fields persist, and
7
+ # how each one encodes into (and decodes out of) its column. Same
8
+ # SHAPE as Sqlite::Codec (one column per attribute, JSON for
9
+ # nested/list) — not its mechanics. Sqlite's own columns carry no
10
+ # type affinity worth trusting on the way back out (SQLite gives it
11
+ # back typed anyway via the driver), but `pg` hands every column
12
+ # back as TEXT unless a type map says otherwise, so a real `bigint`/
13
+ # `double precision` column needs its OWN coercion on decode that
14
+ # Sqlite never had to write.
15
+ module Codec
16
+ private
17
+
18
+ def persisted_fields
19
+ fields = @aggregate.attributes.reject { |attribute| attribute.name == :id }.map do |attribute|
20
+ { name: attribute.name, attribute: attribute, sql_type: sql_type(attribute) }
21
+ end
22
+ lifecycle = @aggregate.lifecycle
23
+ fields << { name: lifecycle.field, attribute: nil, sql_type: "text" } if lifecycle && !fields.any? { |field| field[:name] == lifecycle.field }
24
+ fields
25
+ end
26
+
27
+ def encode(attr, value)
28
+ # NEVER SET IS NOT EMPTY — same reasoning as Sqlite::Codec's own
29
+ # comment: a list attribute nothing has ever appended to has to
30
+ # stay NULL to answer the same as Memory does, not become `[]`
31
+ # invented by this adapter's own storage.
32
+ return (value.nil? ? nil : JSON.generate(value)) if attr.list?
33
+ return JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Runtime::Value)
34
+
35
+ value
36
+ end
37
+
38
+ def encode_field(field, value)
39
+ return value unless field[:attribute]
40
+
41
+ encode(field[:attribute], value)
42
+ end
43
+
44
+ def decode(row)
45
+ persisted_fields.each_with_object({}) do |field, state|
46
+ attr = field[:attribute]
47
+ unless attr
48
+ state[field[:name]] = row[field[:name].to_s]
49
+ next
50
+ end
51
+ raw = row[attr.name.to_s]
52
+ state[attr.name] =
53
+ if attr.list? || value_object?(attr)
54
+ raw ? JSON.parse(raw, symbolize_names: true) : nil
55
+ else
56
+ coerce_scalar(attr, raw)
57
+ end
58
+ end
59
+ end
60
+
61
+ # `pg` returns every column as a Ruby String by default (no type
62
+ # map installed) — a `bigint`/`double precision` column has to be
63
+ # coerced back on the way out, unlike Sqlite3's own driver, which
64
+ # already types a row by the column's declared affinity. Mirrors
65
+ # SQL_TYPES' own mapping: whatever gets a real numeric column here
66
+ # gets converted back here, everything else (String, references,
67
+ # enums, ...) passes through as the text it already is.
68
+ def coerce_scalar(attr, raw)
69
+ return nil if raw.nil?
70
+
71
+ case attr.type
72
+ when "Integer" then raw.to_i
73
+ when "Float" then raw.to_f
74
+ else raw
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,168 @@
1
+ require "digest"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class Postgres
6
+ # The DDL: one table per aggregate, a real column per attribute (a
7
+ # jsonb column per nested/list attribute), an append-only entry
8
+ # table beside it, the shared events/saga tables — and the automatic
9
+ # indexing this adapter carries that `PostgresEra` does not (see
10
+ # docs/implemented/postgres-era-adapter-split-plan.md, governing principle 3:
11
+ # every field a declared query filters or sorts on is safe to index
12
+ # directly here, since there is no `head_view` reduction to route
13
+ # around the way `PostgresEra`'s own cache-table story exists for).
14
+ module SchemaBuilder
15
+ private
16
+
17
+ def create_aggregate_table!
18
+ columns = persisted_fields.map { |field| "#{quote_ident(field[:name])} #{field[:sql_type]}" }
19
+ @db.exec(
20
+ "CREATE TABLE IF NOT EXISTS #{quoted_table} (id text PRIMARY KEY#{columns.empty? ? '' : ', '}#{columns.join(', ')})"
21
+ )
22
+ # RIGHT HERE, NOT A SEPARATE STEP IN `Postgres#initialize` —
23
+ # same idiom Sqlite::SchemaBuilder's own `create_aggregate_table!`
24
+ # uses: index creation runs unconditionally, right after the
25
+ # table it indexes exists, the same self-healing shape
26
+ # `ensure_head_snapshot!`/`ensure_first_head!` already use on
27
+ # `PostgresEra`'s side.
28
+ ensure_indexes!
29
+ end
30
+
31
+ def create_entry_table!
32
+ @db.exec(<<~SQL)
33
+ CREATE TABLE IF NOT EXISTS #{quoted_entry_table} (
34
+ sequence bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
35
+ aggregate_id text NOT NULL,
36
+ operation text NOT NULL DEFAULT 'save',
37
+ state jsonb NOT NULL,
38
+ mirrors jsonb
39
+ )
40
+ SQL
41
+ end
42
+
43
+ # Same DDL as `PostgresEra`'s own — not lineage-specific, copied
44
+ # verbatim (see docs/implemented/postgres-era-adapter-split-plan.md).
45
+ def create_event_table!
46
+ @db.exec(<<~SQL)
47
+ CREATE TABLE IF NOT EXISTS events (
48
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
49
+ name text NOT NULL,
50
+ aggregate text NOT NULL,
51
+ aggregate_id text NOT NULL,
52
+ payload jsonb,
53
+ occurred_at text
54
+ )
55
+ SQL
56
+ end
57
+
58
+ # Same DDL as `PostgresEra`'s own — not lineage-specific, copied
59
+ # verbatim (see docs/implemented/postgres-era-adapter-split-plan.md).
60
+ def create_saga_table!
61
+ @db.exec(<<~SQL)
62
+ CREATE TABLE IF NOT EXISTS hecks_saga_instances (
63
+ domain text NOT NULL,
64
+ process_manager text NOT NULL,
65
+ correlation text NOT NULL,
66
+ state text NOT NULL,
67
+ memory jsonb NOT NULL,
68
+ updated_at timestamptz NOT NULL DEFAULT now(),
69
+ PRIMARY KEY (domain, process_manager, correlation)
70
+ )
71
+ SQL
72
+ end
73
+
74
+ def sql_type(attr)
75
+ return "jsonb" if attr.list? || value_object?(attr)
76
+
77
+ SQL_TYPES.fetch(attr.type, "text")
78
+ end
79
+
80
+ # ── automatic indexing ───────────────────────────────────────
81
+ #
82
+ # Derived from every declared query's `where`/`order_by` fields —
83
+ # no bluebook author opts in, matching every other self-healing
84
+ # schema move this adapter makes. Walks the aggregate's own
85
+ # queries AND each entity's own queries — the same enumeration
86
+ # `dsl/aggregate_builder.rb`'s own `query_surfaces` walks at
87
+ # declaration-seal time — but an entity's own fields never
88
+ # produce an index below: an entity has no table of its own, its
89
+ # rows live inside a LIST-typed attribute on the AGGREGATE's own
90
+ # table (`QueryInterpreter#entity_rows` resolves it that way, and
91
+ # answers an entity query entirely in memory over `repository.all`
92
+ # — it never reaches this adapter's own `query` method at all).
93
+ # Every list-typed attribute is excluded below regardless (see
94
+ # `index_field!`), so an entity's own field would always resolve
95
+ # to "skip" anyway. Walked explicitly rather than silently
96
+ # dropped, so a reader can see entities were considered here, not
97
+ # forgotten.
98
+ def ensure_indexes!
99
+ query_surfaces.each do |owner, queries|
100
+ next unless owner.equal?(@aggregate)
101
+
102
+ queries.each { |query| index_query!(query) }
103
+ end
104
+ end
105
+
106
+ def query_surfaces
107
+ [[@aggregate, @aggregate.queries]] +
108
+ @aggregate.entities.map { |entity| [entity, entity.queries] }
109
+ end
110
+
111
+ def index_query!(query)
112
+ query.wheres.each { |clause| index_field!(clause.field) }
113
+ index_field!(query.order_by.field) if query.order_by
114
+ end
115
+
116
+ # A `list_of` attribute is never indexed here — this adapter's
117
+ # own `contains` on a list field compiles to `EXISTS (SELECT 1
118
+ # FROM jsonb_array_elements(...) ...)` (see `list_contains_clause`
119
+ # below), a SQL shape neither a plain btree on the raw jsonb
120
+ # column nor even a GIN jsonb index (`@>`, `?`) accelerates —
121
+ # Postgres's own GIN jsonb operators match a DIFFERENT SQL shape
122
+ # than the `EXISTS` + `jsonb_array_elements` this codebase always
123
+ # compiles a list `contains` to. An index here would be dead
124
+ # weight, not free correctness, so none is attempted.
125
+ def index_field!(field)
126
+ name, *_path = field.to_s.split(".")
127
+ attribute = @aggregate.attribute(name)
128
+ return if attribute&.list?
129
+
130
+ # THE SAME EXPRESSION THE QUERY ITSELF COMPILES TO — calling
131
+ # `query_expression`/`plain_column`, the real dialect methods,
132
+ # rather than a second, hand-rolled derivation of the same
133
+ # path that could silently drift from it. An index whose
134
+ # expression doesn't match the planner's own candidate
135
+ # expression byte-for-byte is invisible to the planner,
136
+ # created or not.
137
+ expression = query_expression(field.to_s)
138
+ if expression == plain_column(name)
139
+ create_plain_index!(name)
140
+ else
141
+ create_expression_index!(expression, field.to_s)
142
+ end
143
+ end
144
+
145
+ def create_plain_index!(column)
146
+ name = index_name(column.to_s)
147
+ @db.exec("CREATE INDEX IF NOT EXISTS #{quote_ident(name)} ON #{quoted_table} (#{quote_ident(column)})")
148
+ end
149
+
150
+ def create_expression_index!(expression, field)
151
+ name = index_name(field)
152
+ @db.exec("CREATE INDEX IF NOT EXISTS #{quote_ident(name)} ON #{quoted_table} ((#{expression}))")
153
+ end
154
+
155
+ # Postgres identifiers cap at 63 bytes. Hashing TABLE + FIELD
156
+ # together keeps every generated index name well under that no
157
+ # matter how long an aggregate or attribute name gets, and keeps
158
+ # two distinct fields — on the same table or different ones —
159
+ # from ever colliding, or truncating into the same name the way
160
+ # a naive `"idx_#{table}_#{field}"` could once either got long
161
+ # enough.
162
+ def index_name(field)
163
+ "hecks_idx_#{Digest::SHA256.hexdigest("#{table}:#{field}")[0, 40]}"
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,5 @@
1
+ Hecks.adapter "Postgres" do
2
+ port "persistence"
3
+ field :database
4
+ field :schema
5
+ end