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,671 @@
1
+ require "json"
2
+
3
+ require_relative "sql_query_builder"
4
+ require_relative "postgres_era/lineage"
5
+ require_relative "postgres_era/lineage_manager"
6
+ require_relative "../../ports/persistence/append_only"
7
+ require_relative "../../query_specification/common/order_by"
8
+ require_relative "../../runtime/errors"
9
+ require_relative "../../runtime/event"
10
+ require_relative "../../runtime/instance"
11
+ require_relative "../../runtime/registry"
12
+
13
+ module Hecks
14
+ module Adapters
15
+ # The enforcement-grade persistence adapter — and the only one that
16
+ # declares the LINEAGE capability: it may act on shape drift
17
+ # (translate, fork, merge) where every other adapter can only refuse
18
+ # toward it. Sibling to the plain `Postgres` adapter (postgres.rb),
19
+ # which is the same database with none of this machinery — pick
20
+ # PostgresEra only once a domain actually needs to survive a shape
21
+ # change live. See docs/implemented/postgres-era-adapter-split-plan.md for why
22
+ # the two are split and what each one carries.
23
+ #
24
+ # Storage model (see postgres_era/lineage.rb for the DDL):
25
+ # - One journal per DOMAIN, list-partitioned by era, one ordinal
26
+ # sequence spanning partitions. Appends go there; nothing updates
27
+ # or deletes a journal row (immutability by privilege — UPDATE and
28
+ # DELETE revoked; a deployment's app role connects as a non-owner).
29
+ # - Per aggregate, the HEAD is derived: era 1 reads a plain view
30
+ # (latest save per id); later eras read a view overlaying the
31
+ # materialized, translated ancestor tail with live current-era
32
+ # rows. `project` is therefore a no-op — old entries are never
33
+ # rewritten, and the head is never a table anything writes.
34
+ # - State is ONE jsonb column. jsonb normalizes key order (and drops
35
+ # duplicate keys), so anything comparing stored state — the corpus
36
+ # history gate above all — must compare CANONICALIZED state, never
37
+ # raw bytes; `bin/canonicalise` deep-sorts keys, which is exactly
38
+ # why the gate survives this normalization.
39
+ # - Query pushdown is the shared SqlQueryBuilder: every declared
40
+ # operator compiles fully into SQL, or the query refuses loudly.
41
+ class PostgresEra
42
+ include SqlQueryBuilder
43
+
44
+ attr_reader :aggregate
45
+
46
+ def persistence_capabilities = [:atomic_put]
47
+
48
+ # The capability idiom: only PostgresEra answers true, and only
49
+ # PostgresEra carries an era_check! for the boot gate to delegate to.
50
+ def self.lineage_capable? = true
51
+
52
+ # TENANT-CAPABLE — see Runtime::TenantCheck's own header for the
53
+ # full reasoning. `connect_for`'s own `schema:` setting (the
54
+ # Storehouse shared-instance mechanism, already built, already
55
+ # proven for eras) is what keeps two tenant boots' tables apart:
56
+ # each boot's own `SET search_path` means every unqualified
57
+ # reference this adapter and its lineage classes construct
58
+ # resolves into that boot's own schema, never another tenant's —
59
+ # proven for real, not assumed, by tenant_isolation_spec.rb, the
60
+ # same discipline lineage_capable? already holds itself to.
61
+ def self.tenant_capable? = true
62
+
63
+ def self.era_check!(registry:, bluebook:, current_text:, settings:, directory: nil)
64
+ LineageManager.check!(
65
+ registry: registry, bluebook: bluebook, current_text: current_text,
66
+ settings: settings, directory: directory
67
+ )
68
+ end
69
+
70
+ def self.connect_for(name, settings)
71
+ # LAZY, ON PURPOSE — same reasoning as Sqlite's own initialize:
72
+ # a domain that never wires PostgresEra should never need the gem.
73
+ require "pg"
74
+
75
+ declared = settings[:database] || settings["database"]
76
+ if declared.to_s.empty?
77
+ raise Runtime::WiringError,
78
+ "#{name} binds PostgresEra, which needs a database connection, " \
79
+ "but its world declares no \"database\"."
80
+ end
81
+
82
+ connection =
83
+ if declared.start_with?("postgres://", "postgresql://")
84
+ PG.connect(declared)
85
+ else
86
+ PG.connect(dbname: declared)
87
+ end
88
+
89
+ # SHARED-INSTANCE ISOLATION. A domain that declares `schema` is
90
+ # sharing its Postgres instance with other domains (the
91
+ # storehouse) — every unqualified table/view/function reference
92
+ # this adapter and its lineage classes ever construct resolves
93
+ # through search_path, so this one SET is what makes ALTER
94
+ # TABLE ... SET SCHEMA migrations transparent to the rest of the
95
+ # adapter. A domain with no `schema` setting keeps Postgres's
96
+ # own default search_path (public), same as before this existed.
97
+ schema = settings[:schema] || settings["schema"]
98
+ if schema.to_s != ""
99
+ # THE SCHEMA ITSELF, IDEMPOTENTLY — a domain naming a `schema:`
100
+ # nobody has created yet used to fail on its FIRST table-
101
+ # creation attempt with Postgres's own "no schema has been
102
+ # selected to create in", found live provisioning tenant_
103
+ # isolation_spec.rb's own multi-schema fixture by hand before
104
+ # this existed. `CREATE SCHEMA IF NOT EXISTS` is exactly the
105
+ # same self-healing idempotency this adapter's own table/era
106
+ # provisioning already holds itself to (see the comment right
107
+ # below on `client_min_messages`) — a schema that already
108
+ # exists is the ORDINARY case for every boot after the first,
109
+ # not news.
110
+ connection.exec("CREATE SCHEMA IF NOT EXISTS #{connection.quote_ident(schema)}")
111
+ connection.exec("SET search_path TO #{connection.quote_ident(schema)}")
112
+ end
113
+
114
+ # QUIET ON PURPOSE. Provisioning re-runs its own idempotent
115
+ # `CREATE ... IF NOT EXISTS` checks on every boot — a schema that
116
+ # already exists is the ORDINARY case, not news, and Postgres
117
+ # surfaces every one as a NOTICE by default. `bin/set-password`
118
+ # boots a real registry just to mint an Identity, and nobody
119
+ # setting a password needs to see a page of "relation ...
120
+ # already exists, skipping" to do it. WARNING and above (real
121
+ # problems) still surface.
122
+ connection.exec("SET client_min_messages = warning")
123
+ connection
124
+ rescue PG::Error => error
125
+ raise Runtime::WiringError,
126
+ "cannot bind PostgresEra at #{declared} for #{name}: #{error.message.strip}"
127
+ end
128
+
129
+ def initialize(aggregate:, settings: {}, root: nil)
130
+ @aggregate = aggregate
131
+ @db = self.class.connect_for(aggregate.name, settings)
132
+ # The domain names the journal (one journal per lineage). The
133
+ # factory injects it; a directly-instantiated adapter (specs,
134
+ # consoles) journals under the aggregate's own name.
135
+ @domain = (settings[:domain] || settings["domain"] || aggregate.storage_name).to_s
136
+ @lineage = Lineage.new(@db, @domain)
137
+ @lineage.ensure_base!
138
+ # The era gate resolves which era this boot IS (an old checkout
139
+ # boots a held-but-superseded era and keeps writing its own
140
+ # partition); a directly-instantiated adapter defaults to the
141
+ # newest.
142
+ @era = settings[:era] || settings["era"] || @lineage.current_era
143
+ # Unconditional and idempotent, regardless of era — belt-and-
144
+ # suspenders self-healing (compile_head! already ensures this for
145
+ # a freshly-minted era's own name; ensure_first_head! for era 1's)
146
+ # against any boot-ordering surprise, at the cost of one
147
+ # CREATE TABLE IF NOT EXISTS nobody pays for twice.
148
+ @lineage.ensure_head_snapshot!(table, @era)
149
+ @lineage.ensure_first_head!(table) if @era == 1
150
+ # THE READ-CACHE SIDE OF THE ERA WORKAROUND (Track C,
151
+ # docs/implemented/postgres-era-adapter-split-plan.md §3) — one row-cache
152
+ # table per `where`-field this aggregate's own declared queries
153
+ # (and its entities' own) actually use, derived automatically
154
+ # (principle 3 — no bluebook keyword), self-healing and
155
+ # idempotent like everything else booted here. `@field_caches`
156
+ # maps field -> cache-table name; `query` below consults it to
157
+ # decide whether a declared query can skip the DISTINCT ON
158
+ # reduction entirely.
159
+ @field_caches = ensure_field_caches!
160
+ create_event_table!
161
+ create_saga_table!
162
+ end
163
+
164
+ def table = @aggregate.storage_name
165
+
166
+ def find(id)
167
+ result = @db.exec_params(%(SELECT id, state FROM #{quoted_head} WHERE id = $1), [id.to_s])
168
+ return nil if result.ntuples.zero?
169
+
170
+ instance(result[0])
171
+ end
172
+
173
+ # order_by IS A RUNTIME VALUE, not framework-authored bluebook source
174
+ # like every other caller of order_expression — a query param off an
175
+ # HTTP request, in the console's case. Whitelisted against the
176
+ # aggregate's own real attributes (plus its lifecycle field) before
177
+ # it ever reaches order_expression, unlike a declared query's
178
+ # order_by, which the language itself already only lets name a real
179
+ # attribute at parse time. Without this, an unknown field wouldn't
180
+ # error — query_expression degrades a nil attribute to a harmless
181
+ # no-op path — it would just silently sort by nothing.
182
+ def all(order_by: nil, direction: :asc)
183
+ return @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY id)).map { |row| instance(row) } unless order_by
184
+
185
+ name = order_by.to_s.split(".").first
186
+ raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
187
+
188
+ spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
189
+ @db.exec(%(SELECT id, state FROM #{quoted_head} ORDER BY #{order_clause(spec, nil)})).map { |row| instance(row) }
190
+ end
191
+
192
+ def count = @db.exec(%(SELECT COUNT(*) FROM #{quoted_head}))[0]["count"].to_i
193
+
194
+ # THE TWO-PHASE SHORTCUT (Track C, docs/implemented/postgres-era-adapter-
195
+ # split-plan.md §3). `SqlQueryBuilder#query` (`super`, unmodified per
196
+ # principle 2) always runs correctly here — it filters against
197
+ # `head_view`, which is already the fully-reduced current state —
198
+ # but for a domain that has minted a second era, that reduction
199
+ # itself is the expensive part, and no index on the jsonb path
200
+ # changes that (see field_cache.rb's own header for the SQL-
201
+ # semantics reason why). When every `where` clause this query
202
+ # declares either targets a cached field or is eligible to (an
203
+ # ordinary comparator, not a null-vs-value special case — see
204
+ # `cache_eligible?`), skip the reduction: look candidate ids up in
205
+ # the cache table(s) first (cheap, indexed, no reduction involved),
206
+ # then read ONLY those ids' current state from `head_view` — safe
207
+ # THROUGH the reduction because `id` is its own partition key. Any
208
+ # clause that ISN'T cache-eligible (an uncached field, or a null
209
+ # comparison) is simply re-checked against `head_view` in the
210
+ # second phase, exactly as `super` would have checked it anyway —
211
+ # this can only ever NARROW what phase two has to look at, never
212
+ # change what a clause means.
213
+ #
214
+ # FALLS BACK TO `super` WHENEVER NO CLAUSE CAN BE ACCELERATED — a
215
+ # query with no `where` at all (order_by-only — no cache table
216
+ # exists for these, see field_cache.rb), a query whose only clauses
217
+ # target fields with no cache table, or a domain that has never
218
+ # minted a second era at all (`@field_caches` is never empty just
219
+ # because era 1 has no reduction to skip — the cache tables still
220
+ # exist and still accelerate era 1 the same way, but the fallback
221
+ # path is already just as cheap there since head_view IS the
222
+ # snapshot table verbatim for era 1; skipping straight to `super`
223
+ # in that case would be a valid FUTURE optimization, not attempted
224
+ # here to keep this one code path correct for every era uniformly).
225
+ def query(declared, args = {}, context: {})
226
+ return super if @field_caches.empty? || declared.wheres.empty?
227
+
228
+ evaluated = declared.wheres.map { |clause| [clause, query_value(clause.value, args)] }
229
+ cached, uncached = evaluated.partition { |clause, value| cache_eligible?(clause, value) }
230
+ return super if cached.empty?
231
+
232
+ ids = cache_phase(cached)
233
+ return [] if ids.empty?
234
+
235
+ head_phase(declared, uncached, ids, args)
236
+ end
237
+
238
+ # HELD FOR THE WHOLE TRANSACTION, not just around the INSERT — the
239
+ # ordinal is assigned by the column's own `nextval()` default, inside
240
+ # this same statement, so the lock has to already be held before that
241
+ # default evaluates. A DIFFERENT key from `mint_era!`/`merge_tail!`'s
242
+ # `hecks_eras:domain` : this serializes plain writes against EACH
243
+ # OTHER, never against a mint. See postgres/lineage.rb's own comment
244
+ # for why only that half of the race is closed.
245
+ # The journal insert and the snapshot upsert/delete happen in the
246
+ # SAME transaction — real ACID atomicity, not the append-then-
247
+ # project two-step a file-based adapter needs a crash-recovery
248
+ # replay for (see Heki). If this transaction commits, the snapshot
249
+ # is already exactly as current as the journal; if it doesn't,
250
+ # neither happened. `project` stays uninvolved on purpose — it
251
+ # still runs, cheaply, during AppendOnly#recover!'s full replay on
252
+ # every boot (see `project` below), and a second write there would
253
+ # make that replay pay real DB cost for a snapshot that's already
254
+ # correct.
255
+ def append(entry)
256
+ @db.transaction do
257
+ lock_writes!
258
+ append_and_project!(entry)
259
+ end
260
+ entry
261
+ end
262
+
263
+ # Outcome detection, journal append and every derived projection share
264
+ # the SAME transaction and domain write lock. The lineage-aware head
265
+ # determines whether this id is already visible; no repository `find`
266
+ # occurs before entering this adapter-native operation.
267
+ def atomic_put(entry, insert_only: false)
268
+ status = nil
269
+ @db.transaction do
270
+ lock_writes!
271
+ exists = !@db.exec_params(
272
+ "SELECT 1 FROM #{quoted_head} WHERE id = $1 LIMIT 1",
273
+ [entry.id.to_s]
274
+ ).ntuples.zero?
275
+ if insert_only && exists
276
+ status = :conflicted
277
+ next
278
+ end
279
+ status = exists ? :replaced : :inserted
280
+ append_and_project!(entry)
281
+ end
282
+ status
283
+ end
284
+
285
+ # The head is DERIVED — projecting is reading, so there is nothing
286
+ # to write here. `append` above already keeps the snapshot the head
287
+ # view reads from current, transactionally. The instance is still
288
+ # built (and validated) so a save returns what every other adapter
289
+ # returns.
290
+ def project(entry)
291
+ return if entry.delete?
292
+
293
+ Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
294
+ end
295
+
296
+ def entries
297
+ @db.exec_params(
298
+ "SELECT aggregate_id, operation, state, mirrors FROM #{@lineage.quoted_journal} " \
299
+ "WHERE aggregate = $1 ORDER BY ordinal",
300
+ [table]
301
+ ).map do |row|
302
+ state = row["state"] && JSON.parse(row["state"])
303
+ Ports::Persistence::Entry.new(
304
+ operation: row["operation"] || "save",
305
+ id: row["aggregate_id"],
306
+ state: state&.transform_keys(&:to_sym),
307
+ mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
308
+ )
309
+ end
310
+ end
311
+
312
+ def reset!
313
+ @db.exec_params("DELETE FROM #{@lineage.quoted_journal} WHERE aggregate = $1", [table])
314
+ self
315
+ end
316
+
317
+ def save(instance)
318
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
319
+ append(entry)
320
+ project(entry)
321
+ end
322
+
323
+ def delete(id)
324
+ entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
325
+ append(entry)
326
+ true
327
+ end
328
+
329
+ def record_event(event)
330
+ @db.exec_params(
331
+ "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
332
+ [event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
333
+ )
334
+ end
335
+
336
+ def events
337
+ @db.exec("SELECT * FROM events ORDER BY id").map do |row|
338
+ Runtime::Event.new(
339
+ name: row["name"],
340
+ aggregate: row["aggregate"],
341
+ id: row["aggregate_id"],
342
+ payload: JSON.parse(row["payload"], symbolize_names: true),
343
+ occurred_at: row["occurred_at"]
344
+ )
345
+ end
346
+ end
347
+
348
+ # ── the OPTIONAL saga-persistence capability (Ports::Persistence's
349
+ # own three-method shape, §2) — one row per (domain, process_manager,
350
+ # correlation), `domain` kept as an explicit column even under
351
+ # schema isolation so two domains sharing one schema (neither
352
+ # declares its own `schema`) still isolate correctly, matching
353
+ # `hecks_eras`' own precedent (postgres/lineage/provisioning.rb).
354
+ # No advisory lock of its own: every call here already runs inside
355
+ # `SagaInterpreter`'s own mutex (§7) serializing IN-PROCESS writers,
356
+ # and gets the SAME cross-process safety an aggregate's own writes
357
+ # get from this adapter — no better, no worse.
358
+ def save_saga(process_manager:, correlation:, state:, memory:)
359
+ @db.exec_params(
360
+ "INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory) " \
361
+ "VALUES ($1, $2, $3, $4, $5) " \
362
+ "ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
363
+ "SET state = EXCLUDED.state, memory = EXCLUDED.memory, updated_at = now()",
364
+ [@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory)]
365
+ )
366
+ end
367
+
368
+ def delete_saga(process_manager:, correlation:)
369
+ @db.exec_params(
370
+ "DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
371
+ [@domain, process_manager.to_s, correlation.to_s]
372
+ )
373
+ end
374
+
375
+ def each_saga
376
+ return enum_for(:each_saga) unless block_given?
377
+
378
+ @db.exec_params(
379
+ "SELECT process_manager, correlation, state, memory FROM hecks_saga_instances WHERE domain = $1",
380
+ [@domain]
381
+ ).each do |row|
382
+ yield row["process_manager"], row["correlation"], row["state"],
383
+ JSON.parse(row["memory"], symbolize_names: true)
384
+ end
385
+ end
386
+
387
+ private
388
+
389
+ def lock_writes!
390
+ @db.exec_params(
391
+ "SELECT pg_advisory_xact_lock(hashtext('hecks_ordinal:' || $1))",
392
+ [@lineage.domain]
393
+ )
394
+ end
395
+
396
+ def append_and_project!(entry)
397
+ state_json = entry.state && JSON.generate(entry.state)
398
+ ordinal = @db.exec_params(
399
+ "INSERT INTO #{@lineage.quoted_journal} (era, aggregate, aggregate_id, operation, state, mirrors) " \
400
+ "VALUES ($1, $2, $3, $4, $5, $6) RETURNING ordinal",
401
+ [@era, table, entry.id, entry.operation, state_json,
402
+ entry.mirrors && JSON.generate(entry.mirrors)]
403
+ )[0]["ordinal"]
404
+
405
+ if entry.save?
406
+ @db.exec_params(
407
+ "INSERT INTO #{quoted_head_snapshot} (id, ordinal, state) VALUES ($1, $2, $3) " \
408
+ "ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, state = EXCLUDED.state " \
409
+ "WHERE #{quoted_head_snapshot}.ordinal < EXCLUDED.ordinal",
410
+ [entry.id, ordinal, state_json]
411
+ )
412
+ # SAME TRANSACTION, SAME ORDINAL — every field cache stays
413
+ # exactly as current as the snapshot it's derived from, for
414
+ # the identical reason `postgres_era.rb`'s own header comment
415
+ # gives for the journal/snapshot pair: if this transaction
416
+ # commits, every cache row is already correct; if it doesn't,
417
+ # none of them changed.
418
+ @field_caches.each do |field, cache_table|
419
+ @lineage.upsert_field_cache_row!(cache_table, entry.id, ordinal, state_json, query_expression(field))
420
+ end
421
+ else
422
+ @db.exec_params("DELETE FROM #{quoted_head_snapshot} WHERE id = $1", [entry.id])
423
+ @field_caches.each_value { |cache_table| @lineage.delete_field_cache_row!(cache_table, entry.id) }
424
+ end
425
+
426
+ ordinal
427
+ end
428
+
429
+ # ── SqlQueryBuilder's dialect hooks ─────────────────────────────
430
+
431
+ def select_list = "id, state"
432
+ def from_relation = quoted_head
433
+ def dialect_name = "PostgresEra"
434
+ def empty_in_clause = "FALSE"
435
+
436
+ def placeholder(binds, value)
437
+ binds << value
438
+ "$#{binds.size}"
439
+ end
440
+
441
+ def contains_clause(expression, placeholder)
442
+ "position(#{placeholder} in #{expression}) > 0"
443
+ end
444
+
445
+ def list_contains_clause(column, member, placeholder)
446
+ target = member.empty? ? "elem #>> '{}'" : "elem ->> #{text_literal(member)}"
447
+ elements = "jsonb_array_elements(state #> ARRAY[#{text_literal(column)}]::text[]) AS elem"
448
+ "EXISTS (SELECT 1 FROM #{elements} WHERE #{target} = #{placeholder})"
449
+ end
450
+
451
+ def plain_column(name) = jsonb_path([name])
452
+
453
+ def nested_expression(name, path, member)
454
+ segments = path.empty? ? [name, (member || "value").to_s] : [name, *path]
455
+ jsonb_path(segments)
456
+ end
457
+
458
+ def comparable_expression(expression, value)
459
+ value.is_a?(Numeric) ? "(#{expression})::numeric" : expression
460
+ end
461
+
462
+ def execute_query(sql, binds)
463
+ @db.exec_params(sql, binds).map { |row| instance(row) }
464
+ end
465
+
466
+ # ── the rest of the dialect ─────────────────────────────────────
467
+
468
+ def instance(row)
469
+ Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row["state"]))
470
+ end
471
+
472
+ def decode(state_json)
473
+ # Deep symbols, exactly what the Sqlite adapter's per-column
474
+ # `symbolize_names:` decode produces — value-object members and
475
+ # list elements arrive symbol-keyed either way.
476
+ JSON.parse(state_json, symbolize_names: true)
477
+ end
478
+
479
+ def quote_ident(name) = PG::Connection.quote_ident(name.to_s)
480
+ def quoted_head = quote_ident(@lineage.head_view(table))
481
+ def quoted_head_snapshot = quote_ident(@lineage.head_snapshot(table, @era))
482
+
483
+ def order_expression(field)
484
+ expression = query_expression(field)
485
+ numeric_field?(field) ? "(#{expression})::numeric" : expression
486
+ end
487
+
488
+ # Postgres defaults to NULLS LAST on ASC; the port's in-memory
489
+ # semantics (NullPolicy.order, which SQLite's own default happens
490
+ # to match) put null rows FIRST ascending and LAST
491
+ # descending. Compile the placement explicitly so a declared query
492
+ # answers identically no matter which adapter serves it.
493
+ def order_clause(order_by, policy)
494
+ direction = order_by.direction.to_s.downcase == "desc" ? "DESC" : "ASC"
495
+ nulls = case policy&.mode.to_s
496
+ when "first" then " NULLS FIRST"
497
+ when "last" then " NULLS LAST"
498
+ else direction == "DESC" ? " NULLS LAST" : " NULLS FIRST"
499
+ end
500
+ "#{order_expression(order_by.field)} #{direction}#{nulls}, id #{direction}"
501
+ end
502
+
503
+ # One shared walk decides numericness at ANY depth — this used to
504
+ # inspect only the first nested segment, so a two-level path
505
+ # (pizza.price_cents.cents) skipped the ::numeric cast and ordered
506
+ # as text: "900" above "1200".
507
+ def numeric_field?(field)
508
+ name, *path = field.to_s.split(".")
509
+ QuerySpecification::FieldPath.numeric?(@aggregate.attribute(name), path) do |type|
510
+ @aggregate.value_object(type)
511
+ end
512
+ end
513
+
514
+ # ARRAY[...] of individually-escaped literals, never the hand-rolled
515
+ # '{a,b,c}' array-literal SYNTAX — a segment is a field or
516
+ # value-object member name, and while today's callers only ever
517
+ # pass schema-declared names, this method has no way to know
518
+ # that, and the '{...}' form has no escaping at all: a segment
519
+ # containing a single quote closes the string early and whatever
520
+ # follows becomes live SQL. Measured, not assumed — a crafted
521
+ # field name of `x}' = '' OR $1::text = $1::text -- ` made a
522
+ # `where(secret: "public")` clause return every row regardless,
523
+ # against the OLD form; the ARRAY[] form below closes it, verified
524
+ # against the identical payload.
525
+ def jsonb_path(segments)
526
+ "state #>> ARRAY[#{segments.map { |segment| text_literal(segment) }.join(', ')}]::text[]"
527
+ end
528
+
529
+ def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
530
+
531
+ # ── the field-cache read shortcut (Track C) ─────────────────────
532
+
533
+ # EVERY declared `where`-field this aggregate's own queries and its
534
+ # entities' own queries use, minus anything a cache table can't
535
+ # represent (see `cacheable_field?`) — never `order_by`-only
536
+ # fields, which never needed a cache in the first place (sorting
537
+ # the reduced output was never blocked by the reduction; only
538
+ # filtering was — see field_cache.rb's own header).
539
+ def ensure_field_caches!
540
+ cached_where_fields.to_h do |field|
541
+ [field, @lineage.ensure_field_cache!(table, @era, field, query_expression(field))]
542
+ end
543
+ end
544
+
545
+ def cached_where_fields
546
+ declared_queries.flat_map { |q|
547
+ q.wheres.map { |clause|
548
+ clause.field.to_s
549
+ }
550
+ }.uniq.select { |field| cacheable_field?(field) }
551
+ end
552
+
553
+ def declared_queries
554
+ @aggregate.queries + @aggregate.entities.flat_map(&:queries)
555
+ end
556
+
557
+ # LIST-TYPED FIELDS ARE EXCLUDED, same boundary the plain `Postgres`
558
+ # and `Sqlite`/`D1` adapters independently landed on for their own
559
+ # automatic indexing: `contains` means element membership, and a
560
+ # (id, ordinal, ONE value) cache row has nowhere to put more than
561
+ # one element. Everything else — a plain scalar, the lifecycle
562
+ # field, or a non-list value-object member path — reduces to
563
+ # exactly one comparable value per id, which is the one shape this
564
+ # cache table represents.
565
+ def cacheable_field?(field)
566
+ name = field.to_s.split(".").first
567
+ return true if @aggregate.lifecycle&.field.to_s == name
568
+
569
+ attribute = @aggregate.attribute(name)
570
+ !attribute.nil? && !attribute.list?
571
+ end
572
+
573
+ # A clause can be served by the cache when its field has a cache
574
+ # table AND it isn't the null-vs-value special case
575
+ # `QuerySpecification::Common::NullPolicy` intercepts before
576
+ # `where_clause` ever runs (see `query`'s own header comment) — a
577
+ # clause that fails either check simply flows to `head_phase`
578
+ # unaccelerated, exactly as `super` would have evaluated it.
579
+ def cache_eligible?(clause, value)
580
+ @field_caches.key?(clause.field.to_s) &&
581
+ QuerySpecification::Common::NullPolicy.sql_predicate(query_expression(clause.field, value: value), clause.op,
582
+ value).nil?
583
+ end
584
+
585
+ # PHASE ONE — candidate ids, no reduction touched. One SELECT per
586
+ # cached clause against its own narrow (id, ordinal, value) table,
587
+ # `INTERSECT`ed into the set that satisfies every cached clause at
588
+ # once. Reuses `where_clause` (SqlQueryBuilder, private, already
589
+ # mixed into this class) UNCHANGED against the cache table's own
590
+ # `value` column instead of a jsonb path expression — the exact
591
+ # same operator compilation (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`in`)
592
+ # a live query already gets against the real column, so a cached
593
+ # field supports every comparator `super` would have, not just the
594
+ # `eq` example in the plan doc's own illustration.
595
+ def cache_phase(cached)
596
+ binds = []
597
+ clauses = cached.map do |clause, value|
598
+ cache_table = @lineage.field_cache(table, @era, clause.field.to_s)
599
+ "SELECT id FROM #{quote_ident(cache_table)} WHERE #{where_clause(clause.op.to_s, quote_ident('value'), value, binds,
600
+ field: clause.field)}"
601
+ end
602
+ @db.exec_params(clauses.join("\nINTERSECT\n"), binds).map { |row| row["id"] }
603
+ end
604
+
605
+ # PHASE TWO — `head_view`, restricted to phase one's candidate ids
606
+ # PLUS whatever clauses phase one couldn't accelerate, applied
607
+ # exactly the way `SqlQueryBuilder#query` (`super`) already applies
608
+ # every clause today: against the fully-reduced view, which is
609
+ # already correct regardless of caching (see this file's own
610
+ # `query` comment — a cache is a SPEED shortcut, never a
611
+ # correctness fix; head_view was always safe to filter directly,
612
+ # just expensive to reduce in the first place). Duplicates a small
613
+ # slice of `SqlQueryBuilder#query`'s own tail assembly (order_by/
614
+ # limit/offset) rather than reaching into it — principle 2
615
+ # (docs/implemented/postgres-era-adapter-split-plan.md): SqlQueryBuilder stays
616
+ # untouched by the era workaround, so this stays local to
617
+ # PostgresEra rather than growing a shared hook only one adapter
618
+ # would ever call.
619
+ def head_phase(declared, uncached, ids, args)
620
+ binds = []
621
+ clauses = ["id IN (#{ids.map { |id| placeholder(binds, id) }.join(', ')})"]
622
+ uncached.each do |clause, value|
623
+ expression = query_expression(clause.field, value: value)
624
+ if (null_predicate = QuerySpecification::Common::NullPolicy.sql_predicate(expression, clause.op, value))
625
+ clauses << null_predicate.first
626
+ next
627
+ end
628
+ clauses << where_clause(clause.op.to_s, expression, value, binds, field: clause.field)
629
+ end
630
+
631
+ sql = +"SELECT #{select_list} FROM #{from_relation} WHERE #{clauses.join(' AND ')}"
632
+ sql << if declared.order_by
633
+ " ORDER BY #{order_clause(declared.order_by, declared.null_semantics)}"
634
+ else
635
+ " ORDER BY id"
636
+ end
637
+ sql << " LIMIT #{placeholder(binds, query_value(declared.limit.value, args).to_i)}" if declared.limit
638
+ sql << unbounded_limit if !declared.limit && declared.offset
639
+ sql << " OFFSET #{placeholder(binds, query_value(declared.offset.value, args).to_i)}" if declared.offset
640
+ execute_query(sql, binds)
641
+ end
642
+
643
+ def create_event_table!
644
+ @db.exec(<<~SQL)
645
+ CREATE TABLE IF NOT EXISTS events (
646
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
647
+ name text NOT NULL,
648
+ aggregate text NOT NULL,
649
+ aggregate_id text NOT NULL,
650
+ payload jsonb,
651
+ occurred_at text
652
+ )
653
+ SQL
654
+ end
655
+
656
+ def create_saga_table!
657
+ @db.exec(<<~SQL)
658
+ CREATE TABLE IF NOT EXISTS hecks_saga_instances (
659
+ domain text NOT NULL,
660
+ process_manager text NOT NULL,
661
+ correlation text NOT NULL,
662
+ state text NOT NULL,
663
+ memory jsonb NOT NULL,
664
+ updated_at timestamptz NOT NULL DEFAULT now(),
665
+ PRIMARY KEY (domain, process_manager, correlation)
666
+ )
667
+ SQL
668
+ end
669
+ end
670
+ end
671
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "Prism" do
2
+ port "extraction"
3
+ end