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,391 @@
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]
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[: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[: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 = (settings[:domain] || settings["domain"] || aggregate.storage_name).to_s
100
+
101
+ create_aggregate_table!
102
+ create_entry_table!
103
+ create_event_table!
104
+ create_saga_table!
105
+ end
106
+
107
+ def table = @aggregate.storage_name
108
+
109
+ def find(id)
110
+ result = @db.exec_params("SELECT * FROM #{quoted_table} WHERE id = $1", [id.to_s])
111
+ return nil if result.ntuples.zero?
112
+
113
+ Runtime::Instance.new(aggregate: @aggregate, id: result[0]["id"], state: decode(result[0]))
114
+ end
115
+
116
+ # order_by IS A RUNTIME VALUE — see Sqlite#all's own reasoning;
117
+ # whitelisted the identical way before it ever reaches
118
+ # order_expression.
119
+ def all(order_by: nil, direction: :asc)
120
+ order_sql = "ORDER BY id"
121
+ if order_by
122
+ name = order_by.to_s.split(".").first
123
+ raise Runtime::WiringError, "#{@aggregate.name} has no attribute #{order_by.inspect} to order by" unless @aggregate.lifecycle&.field.to_s == name || @aggregate.attribute(name)
124
+
125
+ spec = QuerySpecification::Common::OrderBy.new(field: order_by, direction: direction)
126
+ order_sql = "ORDER BY #{order_clause(spec, nil)}"
127
+ end
128
+
129
+ @db.exec("SELECT * FROM #{quoted_table} #{order_sql}").map do |row|
130
+ Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
131
+ end
132
+ end
133
+
134
+ def count = @db.exec("SELECT COUNT(*) FROM #{quoted_table}")[0]["count"].to_i
135
+
136
+ def append(entry)
137
+ @db.exec_params(
138
+ "INSERT INTO #{quoted_entry_table} (aggregate_id, operation, state, mirrors) VALUES ($1, $2, $3, $4)",
139
+ [entry.id, entry.operation, JSON.generate(entry.state), JSON.generate(entry.mirrors)]
140
+ )
141
+ entry
142
+ end
143
+
144
+ def project(entry)
145
+ return @db.exec_params("DELETE FROM #{quoted_table} WHERE id = $1", [entry.id]) if entry.delete?
146
+
147
+ instance = Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state)
148
+ columns = (["id"] + persisted_fields.map { |field| field[:name].to_s })
149
+ values = [instance.id.to_s] + persisted_fields.map { |field| encode_field(field, instance[field[:name]]) }
150
+ updates = persisted_fields.map { |field| "#{quote_ident(field[:name])} = EXCLUDED.#{quote_ident(field[:name])}" }
151
+
152
+ @db.exec_params(
153
+ "INSERT INTO #{quoted_table} (#{columns.map { |c| quote_ident(c) }.join(', ')}) " \
154
+ "VALUES (#{(1..columns.size).map { |n| "$#{n}" }.join(', ')}) " \
155
+ "ON CONFLICT (id) DO UPDATE SET #{updates.join(', ')}",
156
+ values
157
+ )
158
+ instance
159
+ end
160
+
161
+ def entries
162
+ @db.exec("SELECT aggregate_id, operation, state, mirrors FROM #{quoted_entry_table} ORDER BY sequence").map do |row|
163
+ state = JSON.parse(row["state"])
164
+ Ports::Persistence::Entry.new(
165
+ operation: row["operation"] || "save",
166
+ id: row["aggregate_id"],
167
+ state: state&.transform_keys(&:to_sym),
168
+ mirrors: row["mirrors"] && JSON.parse(row["mirrors"])
169
+ )
170
+ end
171
+ end
172
+
173
+ def reset!
174
+ @db.exec("DELETE FROM #{quoted_table}")
175
+ @db.exec("DELETE FROM #{quoted_entry_table}")
176
+ self
177
+ end
178
+
179
+ # ONE TRANSACTION, not the plain append-then-project two-step a
180
+ # file-based adapter needs a crash-recovery replay for (Heki) —
181
+ # real Postgres ACID atomicity is sitting right there, so a crash
182
+ # between the journal insert and the table upsert must not leave
183
+ # the two disagreeing. `append`/`project` themselves stay plain,
184
+ # transaction-free methods (see the class comment above) — the
185
+ # transaction lives here, the one caller that runs both together.
186
+ def save(instance)
187
+ entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state.dup)
188
+ @db.transaction { append(entry); project(entry) }
189
+ end
190
+
191
+ # Classification, journal append and snapshot replacement are one
192
+ # Postgres transaction. A row lock cannot serialize two first writers —
193
+ # there is no row to lock yet — so a transaction-scoped advisory lock on
194
+ # schema/table + aggregate id owns that missing-row race as well. Once a
195
+ # writer acquires it, the preceding writer has committed and status can
196
+ # be read from the materialized table without a runtime-side find.
197
+ def atomic_put(entry, insert_only: false)
198
+ status = nil
199
+ @db.transaction do
200
+ @db.exec_params(
201
+ "SELECT pg_advisory_xact_lock(" \
202
+ "hashtext(current_schema() || ':' || $1), hashtext($2))",
203
+ [table, entry.id.to_s]
204
+ )
205
+ exists = !@db.exec_params(
206
+ "SELECT 1 FROM #{quoted_table} WHERE id = $1",
207
+ [entry.id.to_s]
208
+ ).ntuples.zero?
209
+ if insert_only && exists
210
+ status = :conflicted
211
+ next
212
+ end
213
+ status = exists ? :replaced : :inserted
214
+ append(entry)
215
+ project(entry)
216
+ end
217
+ status
218
+ end
219
+
220
+ def delete(id)
221
+ entry = Ports::Persistence::Entry.new(operation: "delete", id: id.to_s, state: nil)
222
+ @db.transaction { append(entry); project(entry) }
223
+ true
224
+ end
225
+
226
+ def record_event(event)
227
+ @db.exec_params(
228
+ "INSERT INTO events (name, aggregate, aggregate_id, payload, occurred_at) VALUES ($1, $2, $3, $4, $5)",
229
+ [event.name, event.aggregate, event.id.to_s, JSON.generate(event.payload), event.occurred_at]
230
+ )
231
+ end
232
+
233
+ def events
234
+ @db.exec("SELECT * FROM events ORDER BY id").map do |row|
235
+ Runtime::Event.new(
236
+ name: row["name"],
237
+ aggregate: row["aggregate"],
238
+ id: row["aggregate_id"],
239
+ payload: JSON.parse(row["payload"], symbolize_names: true),
240
+ occurred_at: row["occurred_at"]
241
+ )
242
+ end
243
+ end
244
+
245
+ # ── the OPTIONAL saga-persistence capability (§2) — same DDL and
246
+ # shape as PostgresEra's own (postgres_era.rb), not lineage-
247
+ # specific, copied verbatim.
248
+ def save_saga(process_manager:, correlation:, state:, memory:)
249
+ @db.exec_params(
250
+ "INSERT INTO hecks_saga_instances (domain, process_manager, correlation, state, memory) " \
251
+ "VALUES ($1, $2, $3, $4, $5) " \
252
+ "ON CONFLICT (domain, process_manager, correlation) DO UPDATE " \
253
+ "SET state = EXCLUDED.state, memory = EXCLUDED.memory, updated_at = now()",
254
+ [@domain, process_manager.to_s, correlation.to_s, state.to_s, JSON.generate(memory)]
255
+ )
256
+ end
257
+
258
+ def delete_saga(process_manager:, correlation:)
259
+ @db.exec_params(
260
+ "DELETE FROM hecks_saga_instances WHERE domain = $1 AND process_manager = $2 AND correlation = $3",
261
+ [@domain, process_manager.to_s, correlation.to_s]
262
+ )
263
+ end
264
+
265
+ def each_saga
266
+ return enum_for(:each_saga) unless block_given?
267
+
268
+ @db.exec_params(
269
+ "SELECT process_manager, correlation, state, memory FROM hecks_saga_instances WHERE domain = $1",
270
+ [@domain]
271
+ ).each do |row|
272
+ yield row["process_manager"], row["correlation"], row["state"],
273
+ JSON.parse(row["memory"], symbolize_names: true)
274
+ end
275
+ end
276
+
277
+ private
278
+
279
+ # ── SqlQueryBuilder's dialect hooks ─────────────────────────────
280
+
281
+ def select_list = "*"
282
+ def from_relation = quoted_table
283
+ def dialect_name = "Postgres"
284
+ def empty_in_clause = "FALSE"
285
+
286
+ def placeholder(binds, value)
287
+ binds << value
288
+ "$#{binds.size}"
289
+ end
290
+
291
+ def contains_clause(expression, placeholder)
292
+ "position(#{placeholder} in #{expression}) > 0"
293
+ end
294
+
295
+ # THE LIST COLUMN ITSELF IS THE JSONB ARRAY — no reaching into a
296
+ # shared blob a jsonb path has to walk into first (PostgresEra's
297
+ # own version does, since every attribute there shares one `state`
298
+ # column). Here, `column` names a real column of its own, already
299
+ # jsonb, already the array.
300
+ def list_contains_clause(column, member, placeholder)
301
+ target = member.empty? ? "elem #>> '{}'" : "elem ->> #{text_literal(member)}"
302
+ elements = "jsonb_array_elements(#{quote_ident(column)}) AS elem"
303
+ "EXISTS (SELECT 1 FROM #{elements} WHERE #{target} = #{placeholder})"
304
+ end
305
+
306
+ def plain_column(name) = quote_ident(name)
307
+
308
+ # PostgresEra's own `jsonb_path` walks `[name, *path]` into ONE
309
+ # shared `state` column — the attribute name is PART of the path
310
+ # there. Here the attribute name IS THE COLUMN: the path into it
311
+ # is whatever is LEFT after the column, never the column name
312
+ # repeated inside its own path.
313
+ def nested_expression(name, path, member)
314
+ segments = path.empty? ? [(member || "value").to_s] : path
315
+ jsonb_path(name, segments)
316
+ end
317
+
318
+ # Scalar, non-value-object attributes get a REAL typed column
319
+ # (bigint/double precision/text) — comparing and sorting one needs
320
+ # no cast at all, unlike PostgresEra's shared jsonb `state` blob,
321
+ # where even a top-level scalar only ever comes out of `#>>` as
322
+ # text. A jsonb-extracted value (a value-object member reached
323
+ # through `nested_expression`/`jsonb_path` above) still comes out
324
+ # of `#>>` as text the exact same way PostgresEra's own does, and
325
+ # still needs the same `::numeric` cast to compare/sort
326
+ # numerically rather than lexicographically. `jsonb_extraction?`
327
+ # tells the two apart by inspecting the expression `query_expression`
328
+ # already built — never a second, hand-rolled field walk that
329
+ # could disagree with the one the SQL actually uses.
330
+ def comparable_expression(expression, value)
331
+ value.is_a?(Numeric) && jsonb_extraction?(expression) ? "(#{expression})::numeric" : expression
332
+ end
333
+
334
+ def execute_query(sql, binds)
335
+ @db.exec_params(sql, binds).map do |row|
336
+ Runtime::Instance.new(aggregate: @aggregate, id: row["id"], state: decode(row))
337
+ end
338
+ end
339
+
340
+ # ── the rest of the dialect ─────────────────────────────────────
341
+
342
+ def quote_ident(name) = PG::Connection.quote_ident(name.to_s)
343
+ def quoted_table = quote_ident(table)
344
+ def entry_table = "#{table}_entries"
345
+ def quoted_entry_table = quote_ident(entry_table)
346
+
347
+ def jsonb_extraction?(expression) = expression.include?("#>>")
348
+
349
+ def order_expression(field)
350
+ expression = query_expression(field)
351
+ jsonb_extraction?(expression) && numeric_field?(field) ? "(#{expression})::numeric" : expression
352
+ end
353
+
354
+ # Postgres defaults to NULLS LAST on ASC — same override
355
+ # PostgresEra's own order_clause carries, so a declared query
356
+ # answers identically no matter which adapter serves it (the
357
+ # port's in-memory semantics, which Sqlite's own default happens
358
+ # to match, put null rows FIRST ascending and LAST descending).
359
+ def order_clause(order_by, policy)
360
+ direction = order_by.direction.to_s.downcase == "desc" ? "DESC" : "ASC"
361
+ nulls = case policy&.mode.to_s
362
+ when "first" then " NULLS FIRST"
363
+ when "last" then " NULLS LAST"
364
+ else direction == "DESC" ? " NULLS LAST" : " NULLS FIRST"
365
+ end
366
+ "#{order_expression(order_by.field)} #{direction}#{nulls}, id #{direction}"
367
+ end
368
+
369
+ # Same walk PostgresEra's own numeric_field? uses — decides
370
+ # numericness at ANY depth from the declared shape itself, not a
371
+ # runtime value.
372
+ def numeric_field?(field)
373
+ name, *path = field.to_s.split(".")
374
+ QuerySpecification::FieldPath.numeric?(@aggregate.attribute(name), path) do |type|
375
+ @aggregate.value_object(type)
376
+ end
377
+ end
378
+
379
+ # ARRAY[...] of individually-escaped literals — same escaping
380
+ # PostgresEra's own jsonb_path carries and the same reason: a
381
+ # hand-rolled '{a,b,c}' array literal has no escaping at all, and
382
+ # a segment is a field or value-object member name this method has
383
+ # no way to know is always schema-declared.
384
+ def jsonb_path(column, segments)
385
+ "#{quote_ident(column)} #>> ARRAY[#{segments.map { |segment| text_literal(segment) }.join(', ')}]::text[]"
386
+ end
387
+
388
+ def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
389
+ end
390
+ end
391
+ end
@@ -0,0 +1,171 @@
1
+ require_relative "../../../../runtime/era_tamper"
2
+ require_relative "../../../../runtime/registry"
3
+ require_relative "../../../../runtime/storage_shape"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ class PostgresEra
8
+ class Lineage
9
+ module EraStore
10
+ # Every held text is verified against its raw-byte digest on the
11
+ # way out — an edited storage fact refuses rather than silently
12
+ # reporting "no drift". This is NOT the era name (which hashes
13
+ # the canonical projection, minted once, never recomputed): it
14
+ # is a plain integrity check over bytes, and a row from before
15
+ # the check existed is backfilled, not refused.
16
+ def eras
17
+ @db.exec_params(
18
+ "SELECT ordinal, hash, label, held_text, watermark, held_digest, held_projection::text " \
19
+ "FROM hecks_eras WHERE domain = $1 ORDER BY ordinal",
20
+ [@domain]
21
+ ).map do |row|
22
+ verify_integrity!(row["ordinal"].to_i, row["held_text"], row["held_digest"], row["held_projection"])
23
+ { ordinal: row["ordinal"].to_i, hash: row["hash"], label: row["label"],
24
+ held_text: row["held_text"], watermark: row["watermark"]&.to_i }
25
+ end
26
+ end
27
+
28
+ # The unverified row — what bin/reattest_era shows an operator
29
+ # after the integrity check fires.
30
+ def raw_era(ordinal)
31
+ rows = @db.exec_params(
32
+ "SELECT held_text, held_digest, hash, held_projection::text FROM hecks_eras WHERE domain = $1 AND ordinal = $2",
33
+ [@domain, ordinal]
34
+ )
35
+ return nil if rows.ntuples.zero?
36
+
37
+ { held_text: rows[0]["held_text"], held_digest: rows[0]["held_digest"], hash: rows[0]["hash"],
38
+ held_projection: rows[0]["held_projection"] }
39
+ end
40
+
41
+ # The recovery path: tamper-EVIDENCE (against accident and
42
+ # drift, not adversaries) gets resolved by a human acknowledging
43
+ # the text as it now stands — recorded in an append-only
44
+ # attestation table, never by a bare psql UPDATE.
45
+ def reattest!(ordinal)
46
+ era = raw_era(ordinal)
47
+ raise Runtime::WiringError, "#{@domain} holds no era #{ordinal} to re-attest" unless era
48
+
49
+ fresh = Digest::SHA256.hexdigest(era[:held_text])
50
+ @db.exec(<<~SQL)
51
+ CREATE TABLE IF NOT EXISTS hecks_attestations (
52
+ domain text NOT NULL,
53
+ ordinal int NOT NULL,
54
+ old_digest text,
55
+ new_digest text NOT NULL,
56
+ attested_at timestamptz NOT NULL DEFAULT now()
57
+ )
58
+ SQL
59
+ @db.exec_params(
60
+ "INSERT INTO hecks_attestations (domain, ordinal, old_digest, new_digest) VALUES ($1, $2, $3, $4)",
61
+ [@domain, ordinal, era[:held_digest], fresh]
62
+ )
63
+ @db.exec_params(
64
+ "UPDATE hecks_eras SET held_digest = $3 WHERE domain = $1 AND ordinal = $2",
65
+ [@domain, ordinal, fresh]
66
+ )
67
+ archive_text!(ordinal, era[:held_text])
68
+ fresh
69
+ end
70
+
71
+ def verify_integrity!(ordinal, text, stored_digest, stored_projection_json)
72
+ digest = Digest::SHA256.hexdigest(text)
73
+ if stored_digest.nil?
74
+ @db.exec_params(
75
+ "UPDATE hecks_eras SET held_digest = $3 WHERE domain = $1 AND ordinal = $2 AND held_digest IS NULL",
76
+ [@domain, ordinal, digest]
77
+ )
78
+ backfill_frozen_facts!(ordinal, text, stored_projection_json)
79
+ return
80
+ end
81
+ if stored_digest == digest
82
+ backfill_frozen_facts!(ordinal, text, stored_projection_json)
83
+ return
84
+ end
85
+
86
+ raise Runtime::WiringError, Runtime::EraTamper.refusal(domain: @domain, ordinal: ordinal)
87
+ end
88
+
89
+ # A verified-authentic text backfills what older rows lack: its
90
+ # projection and its archive copy. Never run on a text that
91
+ # failed verification — that would bless the edit.
92
+ def backfill_frozen_facts!(ordinal, text, stored_projection_json)
93
+ archive_text!(ordinal, text)
94
+ return if stored_projection_json
95
+
96
+ projection = Runtime::EraTamper.project(text)
97
+ return unless projection
98
+
99
+ @db.exec_params(
100
+ "UPDATE hecks_eras SET held_projection = $3::text::jsonb " \
101
+ "WHERE domain = $1 AND ordinal = $2 AND held_projection IS NULL",
102
+ [@domain, ordinal, JSON.generate(projection)]
103
+ )
104
+ end
105
+
106
+ def current_era
107
+ held = eras
108
+ held.empty? ? 1 : held.last[:ordinal]
109
+ end
110
+
111
+ def hold_first!(text, projection: nil)
112
+ @db.exec_params(
113
+ "INSERT INTO hecks_eras (domain, ordinal, held_text, held_digest, held_projection) " \
114
+ "VALUES ($1, 1, $2, $3, $4) ON CONFLICT DO NOTHING",
115
+ [@domain, text, Digest::SHA256.hexdigest(text), projection && JSON.generate(projection)]
116
+ )
117
+ archive_text!(1, text)
118
+ # Era 1 is the current era from its first moment — established
119
+ # here, not left to whichever role happens to boot first.
120
+ advance_era!(1)
121
+ end
122
+
123
+ def archive_text!(ordinal, text)
124
+ @db.exec_params(
125
+ "INSERT INTO hecks_era_texts (domain, ordinal, digest, held_text) VALUES ($1, $2, $3, $4) " \
126
+ "ON CONFLICT DO NOTHING",
127
+ [@domain, ordinal, Digest::SHA256.hexdigest(text), text]
128
+ )
129
+ end
130
+
131
+ # A Layer-3 approval, recorded IN the database it was reviewed
132
+ # against — bound to the edge's parsed content and the journal's
133
+ # high-water ordinal at review time. The latest row for a shape
134
+ # pair wins (re-approval supersedes).
135
+ def record_approval!(from:, to:, edge_digest:)
136
+ @db.exec_params(
137
+ "INSERT INTO hecks_approvals (domain, from_label, to_label, edge_digest, reviewed_ordinal) " \
138
+ "VALUES ($1, $2, $3, $4, $5)",
139
+ [@domain, from, to, edge_digest, last_ordinal]
140
+ )
141
+ end
142
+
143
+ def approval_for(from:, to:)
144
+ rows = @db.exec_params(
145
+ "SELECT edge_digest, reviewed_ordinal FROM hecks_approvals " \
146
+ "WHERE domain = $1 AND from_label = $2 AND to_label = $3 ORDER BY approved_at DESC, reviewed_ordinal DESC LIMIT 1",
147
+ [@domain, from, to]
148
+ )
149
+ return nil if rows.ntuples.zero?
150
+
151
+ { edge_digest: rows[0]["edge_digest"], reviewed_ordinal: rows[0]["reviewed_ordinal"].to_i }
152
+ end
153
+
154
+ # Era identity is minted ONCE and stored; nothing ever recomputes
155
+ # a stored name to verify it.
156
+ def mint_name!(ordinal, hash, label)
157
+ @db.exec_params(
158
+ "UPDATE hecks_eras SET hash = $3, label = $4, canon_form = $5 " \
159
+ "WHERE domain = $1 AND ordinal = $2 AND hash IS NULL",
160
+ [@domain, ordinal, hash, label, Runtime::StorageShape::FORM_VERSION]
161
+ )
162
+ end
163
+
164
+ def last_ordinal
165
+ @db.exec("SELECT COALESCE(max(ordinal), 0) AS o FROM #{quoted_journal}")[0]["o"].to_i
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end