hecks 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,438 @@
1
+ require "json"
2
+
3
+ require_relative "sql_query_builder"
4
+ require_relative "postgres/schema_builder"
5
+ require_relative "postgres/codec"
6
+ require_relative "../../ports/persistence/append_only"
7
+ require_relative "../../query_specification/common/null_policy"
8
+ require_relative "../../query_specification/common/order_by"
9
+ require_relative "../../query_specification/field_path"
10
+ require_relative "../../runtime/errors"
11
+ require_relative "../../runtime/event"
12
+ require_relative "../../runtime/instance"
13
+
14
+ module Hecks
15
+ module Adapters
16
+ # The plain Postgres store — flat, one table per aggregate, real
17
+ # typed columns for scalars and jsonb for nested/list attributes,
18
+ # exactly the shape `Sqlite` already made for its own table. Sibling
19
+ # to `PostgresEra` (postgres_era.rb), which is the same database with
20
+ # full lineage/era machinery on top — pick this one unless a domain
21
+ # actually needs to survive a live shape change. See
22
+ # docs/implemented/postgres-era-adapter-split-plan.md for why the two are split.
23
+ #
24
+ # No `hecks_eras`, no lineage, no advisory-lock-per-write for era
25
+ # tracking, no `lineage_capable?`/`era_check!` — this class simply
26
+ # doesn't define those methods at all, and the capability idiom
27
+ # elsewhere already treats their absence as "not lineage-capable".
28
+ #
29
+ # Storage shape (see postgres/schema_builder.rb for the DDL,
30
+ # postgres/codec.rb for the encode/decode):
31
+ # - One real column per attribute, typed for a scalar
32
+ # (`SQL_TYPES`, `text` default), `jsonb` for a nested (value-object)
33
+ # or list-typed attribute — never JSON-in-TEXT the way `Sqlite` has
34
+ # to, since Postgres has a native jsonb type.
35
+ # - `append` and `project` are two real Postgres statements — `save`/
36
+ # `delete` wrap them in ONE transaction, same "the journal insert
37
+ # and the snapshot stay atomic" reasoning `PostgresEra#append`'s own
38
+ # comment gives: a crash between the two must never leave a
39
+ # half-written state. `append`/`project` stay plain, individually-
40
+ # callable methods (never wrapping their own transaction) so
41
+ # `AppendOnly#recover!`'s replay — `project` alone, no `append` —
42
+ # keeps working the same way it does on every other adapter.
43
+ class Postgres
44
+ include SqlQueryBuilder
45
+ include SchemaBuilder
46
+ include Codec
47
+
48
+ SQL_TYPES = { "Integer" => "bigint", "Float" => "double precision" }.freeze
49
+
50
+ attr_reader :aggregate
51
+
52
+ def persistence_capabilities = [:atomic_put, :optimistic_concurrency]
53
+
54
+ def self.connect_for(name, settings)
55
+ # LAZY, ON PURPOSE — same reasoning as PostgresEra's own
56
+ # connect_for: a domain that never wires Postgres should never
57
+ # need the gem installed.
58
+ require "pg"
59
+
60
+ declared = settings.key?(:database) ? settings[:database] : settings["database"]
61
+ if declared.to_s.empty?
62
+ raise Runtime::WiringError,
63
+ "#{name} binds Postgres, which needs a database connection, " \
64
+ "but its world declares no \"database\"."
65
+ end
66
+
67
+ connection =
68
+ if declared.start_with?("postgres://", "postgresql://")
69
+ PG.connect(declared)
70
+ else
71
+ PG.connect(dbname: declared)
72
+ end
73
+
74
+ # SHARED-INSTANCE ISOLATION — same as PostgresEra's own: a
75
+ # domain that declares `schema` is sharing its Postgres instance
76
+ # with other domains, so every unqualified reference this
77
+ # adapter constructs resolves through search_path. A domain with
78
+ # no `schema` setting keeps Postgres's own default (public).
79
+ schema = settings.key?(:schema) ? settings[:schema] : settings["schema"]
80
+ connection.exec("SET search_path TO #{connection.quote_ident(schema)}") if schema.to_s != ""
81
+
82
+ # QUIET ON PURPOSE — same reasoning as PostgresEra's own: a
83
+ # schema/table that already exists is the ORDINARY case on every
84
+ # boot after the first, not news.
85
+ connection.exec("SET client_min_messages = warning")
86
+ connection
87
+ rescue PG::Error => error
88
+ raise Runtime::WiringError,
89
+ "cannot bind Postgres at #{declared} for #{name}: #{error.message.strip}"
90
+ end
91
+
92
+ def initialize(aggregate:, settings: {}, root: nil)
93
+ @aggregate = aggregate
94
+ @db = self.class.connect_for(aggregate.name, settings)
95
+ # THE OPTIONAL saga-persistence capability's own scoping column
96
+ # (§2/§4) — falls back to the aggregate's own storage name for a
97
+ # directly-instantiated adapter (specs), same fallback shape
98
+ # Sqlite's own @domain already uses.
99
+ @domain = (
100
+ if settings.key?(:domain)
101
+ settings[:domain]
102
+ elsif settings.key?("domain")
103
+ settings["domain"]
104
+ else
105
+ aggregate.storage_name
106
+ end
107
+ ).to_s
108
+
109
+ create_aggregate_table!
110
+ create_entry_table!
111
+ create_event_table!
112
+ create_saga_table!
113
+ end
114
+
115
+ def table = @aggregate.storage_name
116
+
117
+ def find(id)
118
+ result = @db.exec_params("SELECT * FROM #{quoted_table} WHERE id = $1", [id.to_s])
119
+ return nil if result.ntuples.zero?
120
+
121
+ instance_from_row(result[0])
122
+ end
123
+
124
+ # order_by IS A RUNTIME VALUE — see Sqlite#all's own reasoning;
125
+ # whitelisted the identical way before it ever reaches
126
+ # order_expression.
127
+ def all(order_by: nil, direction: :asc)
128
+ order_sql = "ORDER BY id"
129
+ if order_by
130
+ name = order_by.to_s.split(".").first
131
+ raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
132
+
133
+ spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
134
+ order_sql = "ORDER BY #{order_clause(spec, nil)}"
135
+ end
136
+
137
+ @db.exec("SELECT * FROM #{quoted_table} #{order_sql}").map { |row| instance_from_row(row) }
138
+ end
139
+
140
+ def count = @db.exec("SELECT COUNT(*) FROM #{quoted_table}")[0]["count"].to_i
141
+
142
+ def append(entry)
143
+ @db.exec_params(
144
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES ($1, $2, $3, $4)",
145
+ # `mirrors` (unlike `state`) is a NULLABLE column — an absent
146
+ # mirrors hash must bind a real SQL NULL, not the four-character
147
+ # JSON text `"null"` (`JSON.generate(nil)`), or a future `IS NULL`
148
+ # check against it would never match. Same guard `sqlite.rb`/
149
+ # `d1.rb`/`postgres_era.rb` already use for their own journal's
150
+ # `mirrors` column.
151
+ [entry.id, entry.operation, JSON.generate(entry.state), entry.mirrors && JSON.generate(entry.mirrors)]
152
+ )
153
+ entry
154
+ end
155
+
156
+ # `expected_version:` requests optimistic-concurrency CAS (see
157
+ # `persistence_capabilities`/`Ports::Persistence::AppendOnly#save`).
158
+ # `hecks_version` is ADAPTER BOOKKEEPING — never in `persisted_fields`
159
+ # (Codec), so it never reaches `decode`'s domain-state hash. It goes
160
+ # in the INSERT column list at `1` (a genuinely new row) and bumps by
161
+ # one in the `ON CONFLICT DO UPDATE` branch; when `expected_version`
162
+ # is given, that UPDATE branch additionally requires
163
+ # `hecks_version = expected_version` to apply at all — Postgres's own
164
+ # `INSERT ... ON CONFLICT DO UPDATE ... WHERE`, which gates only
165
+ # whether the CONFLICT branch's update applies. A genuinely new row
166
+ # never reaches that branch at all, so it always inserts regardless
167
+ # of this WHERE. `RETURNING hecks_version` plus `ntuples.zero?` is
168
+ # how a real version mismatch is told apart from an ordinary write:
169
+ # zero rows back means the conflict branch's WHERE excluded the row
170
+ # entirely — the version had already moved — so `nil` is returned
171
+ # for the caller (`AppendOnly#save`) to treat as "stale, no-op".
172
+ def project(entry, expected_version: nil)
173
+ return @db.exec_params("DELETE FROM #{quoted_table} WHERE id = $1", [entry.id]) if entry.delete?
174
+
175
+ instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
176
+ columns = (["id"] + persisted_fields.map { |field| field[:name].to_s } + ["hecks_version"])
177
+ values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) } + [1]
178
+ updates = persisted_fields.map { |field| "#{quote_ident(field[:name])} = EXCLUDED.#{quote_ident(field[:name])}" } +
179
+ ["hecks_version = #{quoted_table}.hecks_version + 1"]
180
+
181
+ sql = "INSERT INTO #{quoted_table} (#{columns.map { |c| quote_ident(c) }.join(', ')}) " \
182
+ "VALUES (#{(1..columns.size).map { |n| "$#{n}" }.join(', ')}) " \
183
+ "ON CONFLICT (id) DO UPDATE SET #{updates.join(', ')}"
184
+ if expected_version
185
+ values += [expected_version]
186
+ sql += " WHERE #{quoted_table}.hecks_version = $#{values.size}"
187
+ end
188
+ sql += " RETURNING hecks_version"
189
+
190
+ result = @db.exec_params(sql, values)
191
+ return nil if result.ntuples.zero?
192
+
193
+ instance.version = result[0]["hecks_version"].to_i
194
+ instance
195
+ end
196
+
197
+ def entries
198
+ @db.exec("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
199
+ state = JSON.parse(row["state"])
200
+ Ports::Persistence::Entry.new(
201
+ operation: row["operation"] || "save",
202
+ id: row["aggregate_id"],
203
+ state: state&.transform_keys(&:to_sym),
204
+ mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
205
+ )
206
+ end
207
+ end
208
+
209
+ def reset!
210
+ @db.exec("DELETE FROM #{quoted_table}")
211
+ @db.exec("DELETE FROM #{quoted_entry_table}")
212
+ self
213
+ end
214
+
215
+ # ONE TRANSACTION, not the plain append-then-project two-step a
216
+ # file-based adapter needs a crash-recovery replay for (Heki) —
217
+ # real Postgres ACID atomicity is sitting right there, so a crash
218
+ # between the journal insert and the table upsert must not leave
219
+ # the two disagreeing. `append`/`project` themselves stay plain,
220
+ # transaction-free methods (see the class comment above) — the
221
+ # transaction lives here, the one caller that runs both together.
222
+ def save(instance)
223
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
224
+ @db.transaction { append(entry); project(entry) }
225
+ end
226
+
227
+ # Classification, journal append and snapshot replacement are one
228
+ # Postgres transaction. A row lock cannot serialize two first writers —
229
+ # there is no row to lock yet — so a transaction-scoped advisory lock on
230
+ # schema/table + aggregate id owns that missing-row race as well. Once a
231
+ # writer acquires it, the preceding writer has committed and status can
232
+ # be read from the materialized table without a runtime-side find.
233
+ def atomic_put(entry, insert_only: false)
234
+ status = nil
235
+ @db.transaction do
236
+ @db.exec_params(
237
+ "SELECT pg_advisory_xact_lock(" \
238
+ "hashtext(current_schema() || ':' || $1), hashtext($2))",
239
+ [table, entry.id.to_s]
240
+ )
241
+ exists = !@db.exec_params(
242
+ "SELECT 1 FROM #{quoted_table} WHERE id = $1",
243
+ [entry.id.to_s]
244
+ ).ntuples.zero?
245
+ if insert_only && exists
246
+ status = :conflicted
247
+ next
248
+ end
249
+ status = exists ? :replaced : :inserted
250
+ append(entry)
251
+ project(entry)
252
+ end
253
+ status
254
+ end
255
+
256
+ def delete(id)
257
+ entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
258
+ @db.transaction { append(entry); project(entry) }
259
+ true
260
+ end
261
+
262
+ def record_event(event)
263
+ @db.exec_params(
264
+ "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
265
+ [event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
266
+ )
267
+ end
268
+
269
+ def events
270
+ @db.exec("SELECT * FROM events ORDER BY id").map do |row|
271
+ Runtime::Event.new(
272
+ name: row["name"],
273
+ aggregate: row["aggregate"],
274
+ id: row["aggregate_id"],
275
+ payload: JSON.parse(row["payload"], symbolize_names: true),
276
+ occurred_at: row["occurred_at"]
277
+ )
278
+ end
279
+ end
280
+
281
+ # ── the OPTIONAL saga-persistence capability (§2) — same DDL and
282
+ # shape as PostgresEra's own (postgres_era.rb), not lineage-
283
+ # specific, copied verbatim.
284
+ def save_saga(process_manager:, correlation:, state:, memory:, completed_compensations: [])
285
+ @db.exec_params(
286
+ "INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory, completed_compensations) " \
287
+ "VALUES ($1, $2, $3, $4, $5, $6) " \
288
+ "ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
289
+ "SET state = EXCLUDED.state, memory = EXCLUDED.memory, " \
290
+ "completed_compensations = EXCLUDED.completed_compensations, updated_at = now()",
291
+ [@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory), JSON.generate(completed_compensations)]
292
+ )
293
+ end
294
+
295
+ def delete_saga(process_manager:, correlation:)
296
+ @db.exec_params(
297
+ "DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
298
+ [@domain, process_manager.to_s, correlation.to_s]
299
+ )
300
+ end
301
+
302
+ def each_saga
303
+ return enum_for(:each_saga) unless block_given?
304
+
305
+ @db.exec_params(
306
+ "SELECT process_manager, correlation, state, memory, completed_compensations FROM hecks_saga_instances WHERE domain = $1",
307
+ [@domain]
308
+ ).each do |row|
309
+ yield row["process_manager"], row["correlation"], row["state"],
310
+ JSON.parse(row["memory"], symbolize_names: true),
311
+ JSON.parse(row["completed_compensations"] || "[]", symbolize_names: true)
312
+ end
313
+ end
314
+
315
+ private
316
+
317
+ # ── SqlQueryBuilder's dialect hooks ─────────────────────────────
318
+
319
+ def select_list = "*"
320
+ def from_relation = quoted_table
321
+ def dialect_name = "Postgres"
322
+ def empty_in_clause = "FALSE"
323
+
324
+ def placeholder(binds, value)
325
+ binds << value
326
+ "$#{binds.size}"
327
+ end
328
+
329
+ def contains_clause(expression, placeholder)
330
+ "position(#{placeholder} in #{expression}) > 0"
331
+ end
332
+
333
+ # THE LIST COLUMN ITSELF IS THE JSONB ARRAY — no reaching into a
334
+ # shared blob a jsonb path has to walk into first (PostgresEra's
335
+ # own version does, since every attribute there shares one `state`
336
+ # column). Here, `column` names a real column of its own, already
337
+ # jsonb, already the array.
338
+ def list_contains_clause(column, member, placeholder)
339
+ target = member.empty? ? "elem #>> '{}'" : "elem ->> #{text_literal(member)}"
340
+ elements = "jsonb_array_elements(#{quote_ident(column)}) AS elem"
341
+ "EXISTS (SELECT 1 FROM #{elements} WHERE #{target} = #{placeholder})"
342
+ end
343
+
344
+ def plain_column(name) = quote_ident(name)
345
+
346
+ # PostgresEra's own `jsonb_path` walks `[name, *path]` into ONE
347
+ # shared `state` column — the attribute name is PART of the path
348
+ # there. Here the attribute name IS THE COLUMN: the path into it
349
+ # is whatever is LEFT after the column, never the column name
350
+ # repeated inside its own path.
351
+ def nested_expression(name, path, member)
352
+ segments = path.empty? ? [(member || "value").to_s] : path
353
+ jsonb_path(name, segments)
354
+ end
355
+
356
+ # Scalar, non-value-object attributes get a REAL typed column
357
+ # (bigint/double precision/text) — comparing and sorting one needs
358
+ # no cast at all, unlike PostgresEra's shared jsonb `state` blob,
359
+ # where even a top-level scalar only ever comes out of `#>>` as
360
+ # text. A jsonb-extracted value (a value-object member reached
361
+ # through `nested_expression`/`jsonb_path` above) still comes out
362
+ # of `#>>` as text the exact same way PostgresEra's own does, and
363
+ # still needs the same `::numeric` cast to compare/sort
364
+ # numerically rather than lexicographically. `jsonb_extraction?`
365
+ # tells the two apart by inspecting the expression `query_expression`
366
+ # already built — never a second, hand-rolled field walk that
367
+ # could disagree with the one the SQL actually uses.
368
+ def comparable_expression(expression, value)
369
+ value.is_a?(Numeric) && jsonb_extraction?(expression) ? "(#{expression})::numeric" : expression
370
+ end
371
+
372
+ def execute_query(sql, binds)
373
+ @db.exec_params(sql, binds).map { |row| instance_from_row(row) }
374
+ end
375
+
376
+ # Stamps `.version` (adapter bookkeeping, never domain state — see
377
+ # `Instance`'s own comment) from the row's `hecks_version` column on
378
+ # every Instance this adapter builds from a real stored row, so a
379
+ # later `save`'s optimistic-concurrency CAS has something to check
380
+ # against.
381
+ def instance_from_row(row)
382
+ instance = Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
383
+ instance.version = row["hecks_version"].to_i
384
+ instance
385
+ end
386
+
387
+ # ── the rest of the dialect ─────────────────────────────────────
388
+
389
+ def quote_ident(name) = PG::Connection.quote_ident(name.to_s)
390
+ def quoted_table = quote_ident(table)
391
+ def entry_table = "#{table}_entries"
392
+ def quoted_entry_table = quote_ident(entry_table)
393
+
394
+ def jsonb_extraction?(expression) = expression.include?("#>>")
395
+
396
+ def order_expression(field)
397
+ expression = query_expression(field)
398
+ jsonb_extraction?(expression) && numeric_field?(field) ? "(#{expression})::numeric" : expression
399
+ end
400
+
401
+ # Postgres defaults to NULLS LAST on ASC — same override
402
+ # PostgresEra's own order_clause carries, so a declared query
403
+ # answers identically no matter which adapter serves it (the
404
+ # port's in-memory semantics, which Sqlite's own default happens
405
+ # to match, put null rows FIRST ascending and LAST descending).
406
+ def order_clause(order_by, policy)
407
+ direction = order_by.direction.to_s.downcase == "desc" ? "DESC" : "ASC"
408
+ nulls = case policy&.mode.to_s
409
+ when "first" then " NULLS FIRST"
410
+ when "last" then " NULLS LAST"
411
+ else direction == "DESC" ? " NULLS LAST" : " NULLS FIRST"
412
+ end
413
+ "#{order_expression(order_by.field)} #{direction}#{nulls}, id #{direction}"
414
+ end
415
+
416
+ # Same walk PostgresEra's own numeric_field? uses — decides
417
+ # numericness at ANY depth from the declared shape itself, not a
418
+ # runtime value.
419
+ def numeric_field?(field)
420
+ name, *path = field.to_s.split(".")
421
+ QuerySpecification::FieldPath.numeric?(@aggregate.attribute(name), path) do |type|
422
+ @aggregate.value_object(type)
423
+ end
424
+ end
425
+
426
+ # ARRAY[...] of individually-escaped literals — same escaping
427
+ # PostgresEra's own jsonb_path carries and the same reason: a
428
+ # hand-rolled '{a,b,c}' array literal has no escaping at all, and
429
+ # a segment is a field or value-object member name this method has
430
+ # no way to know is always schema-declared.
431
+ def jsonb_path(column, segments)
432
+ "#{quote_ident(column)} #>> ARRAY[#{segments.map { |segment| text_literal(segment) }.join(', ')}]::text[]"
433
+ end
434
+
435
+ def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
436
+ end
437
+ end
438
+ end
@@ -0,0 +1,17 @@
1
+ # ADR 0033 — the `PostgresEra` Ruby CLASS moved into the loadable era
2
+ # persistence plugin (`lib/hecks/ports/persistence/plugins/era/`); this
3
+ # declaration stays here, unmoved. `Adapters::Folder#load_library` globs
4
+ # `.adapter` files by DIRECTORY, not by require path, on every boot,
5
+ # regardless of which adapters a given domain actually binds — the same
6
+ # reason every other adapter's own `.adapter` file lives beside this one.
7
+ # Pure metadata (which port, which fields) — no era-specific code, and
8
+ # harmless for a domain that never binds `persisted_by "PostgresEra"`.
9
+ # `registry.adapter_class("PostgresEra")` is only ever resolved for a
10
+ # domain that actually binds to it, at which point the plugin must
11
+ # already be `require`d for the class to exist.
12
+ Hecks.adapter "PostgresEra" do
13
+ port "persistence"
14
+ field :database
15
+ field :role
16
+ field :schema
17
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "Prism" do
2
+ port "extraction"
3
+ end
@@ -0,0 +1,80 @@
1
+ require "prism"
2
+ require_relative "../../bluebook/expression/canonical_form"
3
+
4
+ module Hecks
5
+ module Adapters
6
+ class NotExtractable < StandardError; end
7
+
8
+ module Prism
9
+ TREES = {}
10
+
11
+ module_function
12
+
13
+ def canonical(block)
14
+ body = body_source(block)
15
+ body && canonicalise(body)
16
+ end
17
+
18
+ def body_source(block)
19
+ file, line = block.source_location
20
+ return nil unless file && File.readable?(file)
21
+
22
+ node = block_node_at(file, line)
23
+ node&.body&.slice
24
+ end
25
+
26
+ def block_node_at(file, line)
27
+ found = nil
28
+ walk(tree_for(file)) do |node|
29
+ next unless node.is_a?(::Prism::BlockNode)
30
+ next unless node.location.start_line == line
31
+
32
+ found ||= node
33
+ end
34
+ found
35
+ end
36
+
37
+ def tree_for(file)
38
+ # ::Prism.parse_file(file) reads the file itself, at the C
39
+ # extension level — bypassing Ruby's own File/IO layer entirely.
40
+ # That's invisible to anything that virtualizes the filesystem at
41
+ # the Ruby level instead of the OS level (e.g. tebako's memfs,
42
+ # used to press a hecks-based app into a single executable —
43
+ # see domain/README.md's "Deploying" section in lifeadelics for
44
+ # why that matters). ::Prism.parse(File.read(file)) parses the
45
+ # exact same bytes, just read through Ruby's File.read first,
46
+ # which those tools do intercept.
47
+ TREES[file] ||= ::Prism.parse(File.read(file)).value
48
+ end
49
+
50
+ # `TREES` caches for the life of the PROCESS, keyed by path, with
51
+ # no staleness check — correct for every ordinary caller (a file
52
+ # loads once per process: one `bin/ir` run, one rspec worker,
53
+ # never edited out from under it), but WRONG for anything that
54
+ # legitimately reloads an edited file in-process: a stale cached
55
+ # tree reports a `given`/`ensures` block at its OLD line number,
56
+ # which no longer matches the freshly re-executed file's own
57
+ # `block.source_location` — surfacing as "did not survive
58
+ # extraction" on a perfectly valid file. Found for real building
59
+ # `Hecks::Codemod` (lib/hecks/codemod.rb), which used to
60
+ # reach into `TREES.clear` directly — a private implementation
61
+ # detail poked from outside. `forget`/`forget_all` are the real
62
+ # API so nothing else that reloads an edited file in-process has
63
+ # to know `TREES` exists at all.
64
+ def forget(file) = TREES.delete(file)
65
+
66
+ def forget_all = TREES.clear
67
+
68
+ def walk(node, &visit)
69
+ return unless node.is_a?(::Prism::Node)
70
+
71
+ visit.call(node)
72
+ node.compact_child_nodes.each { |child| walk(child, &visit) }
73
+ end
74
+
75
+ def canonicalise(source)
76
+ Bluebook::Expression::CanonicalForm.apply(source)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,3 @@
1
+ Hecks.adapter "SecureRandomIdentity" do
2
+ port "identity_generation"
3
+ end
@@ -0,0 +1,14 @@
1
+ require "securerandom"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ # The real `identity_generation` fulfillment — an actual random
6
+ # UUID, for an actual deployment. `SequentialIdentity` is the
7
+ # deterministic sibling specs/fuzzing bind instead.
8
+ module SecureRandomIdentity
9
+ module_function
10
+
11
+ def uuid = SecureRandom.uuid
12
+ end
13
+ end
14
+ end