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,190 @@
1
+ require "digest"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class PostgresEra
6
+ class Lineage
7
+ # THE READ-SIDE OF THE ERA WORKAROUND. Once a domain mints a
8
+ # second era, `head_view` is a `DISTINCT ON`/`UNION ALL`
9
+ # reduction — and a `where` clause on anything but `id` (its own
10
+ # partition key) cannot be pushed through that reduction, so every
11
+ # declared query pays the cost of reducing the WHOLE aggregate
12
+ # before it can filter anything (see docs/implemented/postgres-era-adapter-
13
+ # split-plan.md's own trigger section — this is a real
14
+ # SQL-semantics wall, not a missing index; adding one to
15
+ # `head_view` itself changes nothing, because the planner cannot
16
+ # prove a predicate on a non-partition column commutes with
17
+ # `DISTINCT ON` without risking a stale row winning).
18
+ #
19
+ # THE WORKAROUND: one narrow table per declared `where`-field
20
+ # (never `order_by`-only fields — sorting the reduced output was
21
+ # never blocked by the reduction; only filtering was), holding
22
+ # exactly (id, ordinal, value) — the CURRENT extracted value for
23
+ # every live id, maintained transactionally by `append` alongside
24
+ # the head snapshot it already upserts (same ordinal-guard idiom:
25
+ # `WHERE ordinal < EXCLUDED.ordinal`). A query whose sole `where`
26
+ # is on a cached field never runs the reduction at all: `SELECT id
27
+ # FROM <field>_cache WHERE value <op> $1`, then `SELECT id, state
28
+ # FROM head_view WHERE id = ANY($ids)` — safe THROUGH the
29
+ # reduction because `id` is its own partition key, unlike the
30
+ # original field.
31
+ #
32
+ # SCOPED TO NON-LIST FIELDS ONLY, same reasoning Track A (plain
33
+ # Postgres) and Track B (Sqlite/D1) both landed on independently
34
+ # for their own automatic indexing: this codebase's `contains`
35
+ # compiles to element-membership (`jsonb_array_elements`/
36
+ # `json_each` + `EXISTS`), which a (id, ordinal, value) cache table
37
+ # keyed on ONE scalar value per id has no way to represent — a
38
+ # list field would need one row per (id, element), a different
39
+ # shape this plan does not build. `PostgresEra#eligible_for_cache?`
40
+ # is where that boundary lives.
41
+ module FieldCache
42
+ # Deterministic and collision-free regardless of field-path
43
+ # length or storage-name length — Postgres identifiers cap at 63
44
+ # bytes, and a value-object member path (`compliance.review.
45
+ # deadline_at`) combined with a long storage name could exceed
46
+ # that if built any less defensively. Not meant to be
47
+ # human-readable; `hecks_backfill_progress`/catalog lookups are
48
+ # always driven by this same computed name, never typed by hand.
49
+ def field_cache(storage_name, era, field)
50
+ "hecks_fc_#{Digest::SHA256.hexdigest("#{storage_name}\0#{era}\0#{field}")[0, 20]}"
51
+ end
52
+
53
+ # Self-healing, same idiom as `ensure_head_snapshot!`: cheap,
54
+ # unconditional, safe to call on every boot. CREATION is a
55
+ # short-held lock (table doesn't exist yet — nothing to block);
56
+ # BACKFILL runs outside it, chunk by chunk, each chunk its own
57
+ # short lock — never one transaction spanning the scan, per
58
+ # principle 1. `value_expression` is the exact SQL
59
+ # `PostgresEra#query_expression(field)` already compiles for
60
+ # this field, applied against a `state` column this module's own
61
+ # SQL always makes available in scope (see `field_cache_source_sql`)
62
+ # — ONE source of truth for "what does this field mean", not a
63
+ # second hand-rolled copy that could silently drift from what a
64
+ # live query actually filters on.
65
+ def ensure_field_cache!(storage_name, era, field, value_expression)
66
+ name = field_cache(storage_name, era, field)
67
+ unless table_exists?(name)
68
+ nested_transaction("hecks_field_cache_create") do
69
+ @db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_field_cache:' || $1))", [name])
70
+ next if table_exists?(name)
71
+
72
+ @db.exec(<<~SQL)
73
+ CREATE TABLE #{quote(name)} (
74
+ id text PRIMARY KEY,
75
+ ordinal bigint NOT NULL,
76
+ value text
77
+ )
78
+ SQL
79
+ @db.exec("CREATE INDEX IF NOT EXISTS #{quote("#{name}_value_idx")} ON #{quote(name)} (value)")
80
+ end
81
+ end
82
+ backfill_field_cache!(name, storage_name, era, value_expression)
83
+ name
84
+ end
85
+
86
+ def backfill_field_cache!(name, storage_name, era, value_expression)
87
+ chunked_backfill!(
88
+ name,
89
+ source_sql: lambda do |cursor|
90
+ <<~SQL
91
+ SELECT id, ordinal, value FROM (#{field_cache_source_sql(storage_name, era, value_expression)}) reduced
92
+ #{cursor ? "WHERE id > #{text_literal(cursor)}" : ''}
93
+ ORDER BY id LIMIT #{ResumableBackfill::CHUNK_SIZE}
94
+ SQL
95
+ end,
96
+ upsert: lambda do |rows|
97
+ upsert_field_cache_rows!(name, rows)
98
+ end
99
+ )
100
+ end
101
+
102
+ # THE LIVE-WRITE SIDE — called from `append`, inside the SAME
103
+ # transaction as the journal insert and the head-snapshot
104
+ # upsert, for every field this aggregate has a cache table for.
105
+ # `state_json` is the entry's OWN new state (already the thing
106
+ # `append` is about to write) — bound as a literal jsonb value
107
+ # and aliased `state`, so `value_expression` (built to read a
108
+ # column literally named `state`) evaluates identically here and
109
+ # in every backfill/query-time use, with no separate Ruby-side
110
+ # re-implementation of "how to pick a value-object's member" to
111
+ # keep in sync.
112
+ def upsert_field_cache_row!(name, id, ordinal, state_json, value_expression)
113
+ @db.exec_params(<<~SQL, [id, ordinal, state_json])
114
+ INSERT INTO #{quote(name)} (id, ordinal, value)
115
+ SELECT $1, $2, #{value_expression}
116
+ FROM (SELECT $3::jsonb AS state) src
117
+ ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, value = EXCLUDED.value
118
+ WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal
119
+ SQL
120
+ end
121
+
122
+ def delete_field_cache_row!(name, id)
123
+ @db.exec_params("DELETE FROM #{quote(name)} WHERE id = $1", [id])
124
+ end
125
+
126
+ private
127
+
128
+ # The reduced (id, ordinal, state) source a field cache backfills
129
+ # from — NOT the raw journal (unlike `backfill_head_snapshot!`'s
130
+ # own source): a field cache reflects the CURRENT HEAD value,
131
+ # which for era > 1 can come from either this era's own live
132
+ # writes OR an untouched-this-era id whose value still comes
133
+ # from the translated ancestor tail. Era 1 has no ancestor tail
134
+ # (the snapshot table IS the head, verbatim); era N reduces the
135
+ # SAME two sources `compile_head!`'s own final view definition
136
+ # unions (head_compiler.rb) — this is deliberately the identical
137
+ # shape, ordinal kept in the SELECT list instead of dropped,
138
+ # because a backfilled cache row needs a REAL ordinal to guard
139
+ # against a concurrent live write the same way every other
140
+ # upsert in this adapter already does. Ordinals are safe to
141
+ # compare across that union because they come from ONE spanning
142
+ # sequence per domain (see lineage.rb's own header comment) —
143
+ # an ancestor-era ordinal is always numerically smaller than any
144
+ # current-era one, so the ordinary `ordinal <` guard already
145
+ # means the right thing with no era-aware special case.
146
+ def field_cache_source_sql(storage_name, era, value_expression)
147
+ era = era.to_i
148
+ if era == 1
149
+ return "SELECT id, ordinal, #{value_expression} AS value " \
150
+ "FROM #{quote(head_snapshot(storage_name, era))}"
151
+ end
152
+
153
+ held = eras.find { |candidate| candidate[:ordinal] == era }
154
+ view = held && held[:label] && matview(storage_name, era, held[:label])
155
+ ancestor_side =
156
+ if view && view_exists?(view)
157
+ "SELECT ordinal, aggregate_id AS id, state FROM #{quote(view)} WHERE operation = 'save'"
158
+ else
159
+ # No compiled ancestor matview yet (e.g. this era was just
160
+ # minted and compile_head! hasn't run for THIS aggregate) —
161
+ # the live snapshot side alone is the whole truth so far.
162
+ "SELECT NULL::bigint AS ordinal, NULL::text AS id, NULL::jsonb AS state WHERE FALSE"
163
+ end
164
+
165
+ <<~SQL
166
+ SELECT id, ordinal, #{value_expression} AS value FROM (
167
+ SELECT DISTINCT ON (id) id, ordinal, state FROM (
168
+ #{ancestor_side}
169
+ UNION ALL
170
+ SELECT ordinal, id, state FROM #{quote(head_snapshot(storage_name, era))}
171
+ ) merged ORDER BY id, ordinal DESC
172
+ ) reduced
173
+ SQL
174
+ end
175
+
176
+ def upsert_field_cache_rows!(name, rows)
177
+ rows.each do |row|
178
+ @db.exec_params(
179
+ "INSERT INTO #{quote(name)} (id, ordinal, value) VALUES ($1, $2, $3) " \
180
+ "ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, value = EXCLUDED.value " \
181
+ "WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
182
+ [row["id"], row["ordinal"], row["value"]]
183
+ )
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,429 @@
1
+ require_relative "../../../../naming"
2
+ require_relative "../../../../translation/rule_compiler"
3
+
4
+ module Hecks
5
+ module Adapters
6
+ class PostgresEra
7
+ class Lineage
8
+ module HeadCompiler
9
+ # ── head compilation ───────────────────────────────────────────
10
+
11
+ # The snapshot table backing one aggregate's CURRENT era: one row
12
+ # per live id, upserted transactionally by PostgresEra#append
13
+ # alongside the journal insert it belongs to, never derived by
14
+ # scanning history thereafter. Idempotent and unguarded by
15
+ # `provisioner?` — a plain per-aggregate table, the same
16
+ # privilege class as the per-aggregate views/matviews already
17
+ # created this way, not the shared owner-only journal.
18
+ #
19
+ # BACKFILLED, not just created, the FIRST time this table comes
20
+ # into existence for a domain that already has journal history —
21
+ # a fresh era (compile_head! at mint) never has any, but era 1
22
+ # against an EXISTING deployment (any domain running before this
23
+ # snapshot table existed at all) does, and an empty table would
24
+ # silently erase every already-written record from every read
25
+ # the instant the head view starts pointing at it.
26
+ #
27
+ # CREATION and BACKFILL are deliberately two separate steps now,
28
+ # not one nested transaction — principle 1 (docs/implemented/postgres-era-
29
+ # adapter-split-plan.md): no operation may hold a lock across a
30
+ # scan whose duration scales with table size, and
31
+ # `backfill_head_snapshot!` below is now a CHUNKED, resumable
32
+ # scan (see `ResumableBackfill`), the opposite of something that
33
+ # belongs inside one transaction. Creation alone (an empty
34
+ # table, nothing to scan) still needs the short-held lock
35
+ # unchanged from before. Backfill runs unconditionally after —
36
+ # cheap and correct whether the table was just created, already
37
+ # fully backfilled by a prior boot (one indexed point lookup via
38
+ # `hecks_backfill_progress`, see ResumableBackfill), or left
39
+ # mid-backfill by a crashed or still-racing concurrent boot
40
+ # (picks up exactly where the last COMMITTED chunk left off).
41
+ def ensure_head_snapshot!(storage_name, era)
42
+ name = head_snapshot(storage_name, era)
43
+ unless table_exists?(name)
44
+ # Locked, not a bare CREATE TABLE IF NOT EXISTS — two
45
+ # processes booting this aggregate for the very first time
46
+ # concurrently must not race to CREATE (one wins, one gets a
47
+ # real Postgres error). Re-checked under the lock: the fast
48
+ # path above skips locking entirely once any boot has
49
+ # already finished this once, which is every boot after the
50
+ # first.
51
+ #
52
+ # NESTABLE — this runs both standalone (adapter boot, its
53
+ # own transaction) and from `compile_head!` while
54
+ # `mint_era!` is already mid-transaction (its manual
55
+ # `BEGIN`, held open for the era row, every aggregate's
56
+ # matview, and the advisory lock advance_era! relies on).
57
+ # `@db.transaction` is a bare BEGIN/COMMIT with no savepoint
58
+ # nesting (see H2 in docs/audits/2026-08-10-main-bug-
59
+ # audit.md) — called while already inside a transaction, its
60
+ # COMMIT would end THAT transaction early, releasing mint's
61
+ # advisory lock and letting a later step run uncommitted.
62
+ # `nested_transaction` tells the two cases apart and uses a
63
+ # SAVEPOINT for the second, so the surrounding mint stays
64
+ # one real transaction from BEGIN to its own COMMIT
65
+ # regardless of how deep this is called from.
66
+ nested_transaction("hecks_head_snapshot") do
67
+ @db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_head_snapshot:' || $1))", [name])
68
+ next if table_exists?(name)
69
+
70
+ @db.exec(<<~SQL)
71
+ CREATE TABLE #{quote(name)} (
72
+ id text PRIMARY KEY,
73
+ ordinal bigint NOT NULL,
74
+ state jsonb NOT NULL
75
+ )
76
+ SQL
77
+ end
78
+ end
79
+ backfill_head_snapshot!(name, storage_name, era)
80
+ end
81
+
82
+ # The exact reduction era 1's OLD live view used to run on every
83
+ # single read — DISTINCT ON latest-per-id, saves only — now
84
+ # chunked through `ResumableBackfill#chunked_backfill!` instead
85
+ # of one blocking `INSERT ... SELECT` over the whole journal:
86
+ # each chunk reads the next page of distinct ids (a plain SELECT,
87
+ # no lock held — an ordinary reader or writer is never blocked
88
+ # by this running), then upserts it under the same short-held
89
+ # advisory lock + ordinal guard every other upsert in this
90
+ # adapter already uses. See `ResumableBackfill`'s own header for
91
+ # why this is RESUMABLE, not merely safe-to-restart.
92
+ def backfill_head_snapshot!(name, storage_name, era)
93
+ chunked_backfill!(
94
+ name,
95
+ source_sql: lambda do |cursor|
96
+ <<~SQL
97
+ SELECT id, ordinal, state FROM (
98
+ SELECT DISTINCT ON (aggregate_id) aggregate_id AS id, ordinal, operation, state
99
+ FROM #{quoted_journal}
100
+ WHERE era = #{era.to_i} AND aggregate = #{text_literal(storage_name)}
101
+ #{cursor ? "AND aggregate_id > #{text_literal(cursor)}" : ''}
102
+ ORDER BY aggregate_id, ordinal DESC
103
+ ) latest WHERE operation = 'save' ORDER BY id LIMIT #{ResumableBackfill::CHUNK_SIZE}
104
+ SQL
105
+ end,
106
+ upsert: lambda do |rows|
107
+ rows.each do |row|
108
+ @db.exec_params(
109
+ "INSERT INTO #{quote(name)} (id, ordinal, state) VALUES ($1, $2, $3) " \
110
+ "ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, state = EXCLUDED.state " \
111
+ "WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
112
+ [row["id"], row["ordinal"], row["state"]]
113
+ )
114
+ end
115
+ end
116
+ )
117
+ end
118
+
119
+ def table_exists?(name)
120
+ @db.exec_params("SELECT to_regclass($1) IS NOT NULL AS present", [name]).getvalue(0, 0) == "t"
121
+ end
122
+
123
+ # A transaction wrapper safe to call from inside an already-open
124
+ # transaction, unlike `PG::Connection#transaction` (bare
125
+ # BEGIN/COMMIT, no savepoint nesting — see H2). Standalone
126
+ # (`PQTRANS_IDLE`), this is `@db.transaction` itself: a real
127
+ # transaction, committed or rolled back on the way out. Nested
128
+ # (anything else — `PQTRANS_INTRANS` from a manual `BEGIN` like
129
+ # mint_era!'s, or the gem's own `#transaction`), it's a SAVEPOINT
130
+ # instead: released on success, rolled back to (then the error
131
+ # re-raised) on failure, and either way the surrounding
132
+ # transaction is never touched — no early COMMIT, no early
133
+ # release of whatever advisory lock it holds.
134
+ def nested_transaction(name)
135
+ return @db.transaction { yield } if @db.transaction_status == PG::PQTRANS_IDLE
136
+
137
+ @db.exec("SAVEPOINT #{name}")
138
+ begin
139
+ yield
140
+ @db.exec("RELEASE SAVEPOINT #{name}")
141
+ rescue StandardError
142
+ @db.exec("ROLLBACK TO SAVEPOINT #{name}")
143
+ raise
144
+ end
145
+ end
146
+
147
+ # Era 1: the head is the snapshot table itself, verbatim — no
148
+ # per-read reduction over history left to do, because `append`
149
+ # already keeps the snapshot current as of every write.
150
+ def ensure_first_head!(storage_name)
151
+ ensure_head_snapshot!(storage_name, 1)
152
+ @db.exec(<<~SQL)
153
+ CREATE OR REPLACE VIEW #{quote(head_view(storage_name))} AS
154
+ SELECT id, state FROM #{quote(head_snapshot(storage_name, 1))}
155
+ SQL
156
+ end
157
+
158
+ # The compiled chain over the ancestor tail — the matview's body,
159
+ # also runnable as a LIVE query (the audit previews a pending
160
+ # era through exactly this SQL before anything is minted).
161
+ #
162
+ # NEVER flatten the edges into one merged rule set, however
163
+ # tempting the optimization looks. The two-line counterexample:
164
+ # edge 1 renames A→B, edge 2 renames C→A. Flattened, a single
165
+ # phase order either applies C→A before A→B (aliasing C's value
166
+ # into B) or drops the recycled name entirely — there is NO
167
+ # correct position for both rules in one pass. Chaining the
168
+ # original edges in mint order reproduces the true execution
169
+ # exactly and needs no such reasoning.
170
+ # ONLY THE LATEST ANCESTOR ENTRY PER ID IS OBSERVABLE. The head,
171
+ # the tail-merge, and the audit all reduce by
172
+ # DISTINCT ON (aggregate_id) ORDER BY ordinal DESC before anyone
173
+ # reads a state, so an entry with a newer sibling can never
174
+ # reach a reader. Translating those siblings computes and stores
175
+ # rows nothing can observe — on an append-only journal a record
176
+ # edited a hundred times cost a hundred translations to serve
177
+ # one.
178
+ #
179
+ # So the tail is reduced BEFORE the chain, not after. The
180
+ # translated output is identical (the reducer is idempotent and
181
+ # the survivor is the same row either way); only the work
182
+ # changes, from |journal entries| to |distinct records|.
183
+ #
184
+ # `era` must survive the reduction: each edge's CASE reads it to
185
+ # decide whether a row is old enough to need that edge applied.
186
+ def latest_per_id(tail)
187
+ return tail if tail.to_s.empty?
188
+
189
+ "SELECT DISTINCT ON (aggregate_id) ordinal, era, aggregate, aggregate_id, operation, state " \
190
+ "FROM (#{tail}) tail_entries ORDER BY aggregate_id, ordinal DESC"
191
+ end
192
+
193
+ # THE LAYERED BUILD — era N from era N-1's matview, not from raw
194
+ # history. Returns nil when it cannot apply, and the caller
195
+ # falls back to the full chain above.
196
+ #
197
+ # The algebra it rests on, both halves load-bearing:
198
+ #
199
+ # 1. THE CUTS DO NOT MOVE. ancestor_tail_sql cuts ancestor k at
200
+ # W(k+1) — the watermark recorded when era k+1 was minted.
201
+ # Those are the SAME literals in the era N-1 build and the
202
+ # era N build, so era N-1's matview already carries exactly
203
+ # the cut era N needs for eras 1..N-2. Only era N-1's own
204
+ # rows are new, and they are cut at W(N). The watermark is
205
+ # still a literal baked into a definition; it is simply
206
+ # baked into the layer beneath. (This is why the tail-merge
207
+ # must pass full: — it moves every watermark at once.)
208
+ #
209
+ # 2. REDUCING IS ASSOCIATIVE. reduce(A ∪ B) == reduce(reduce(A) ∪ B),
210
+ # because the survivor is max-ordinal-per-id either way. So
211
+ # reducing per layer is the same answer as reducing the
212
+ # whole tail once.
213
+ #
214
+ # And every row on both sides of the union is already in era
215
+ # N-1's shape — the matview because it was chained through edge
216
+ # N-2, era N-1's own rows because that is the shape they were
217
+ # written under — so the final edge applies uniformly, with no
218
+ # per-era CASE. That equality is asserted, not argued: the spec
219
+ # builds a third era both ways and diffs them.
220
+ def layered_chain_sql(aggregate, era, edges)
221
+ return nil if era < 3 || edges.size < 2
222
+
223
+ held = eras
224
+ prior = held.find { |candidate| candidate[:ordinal] == era - 1 }
225
+ return nil unless prior && prior[:label]
226
+
227
+ prior_view = matview(aggregate.storage_name, era - 1, prior[:label])
228
+ return nil unless view_exists?(prior_view)
229
+
230
+ names = names_by_era(aggregate, edges)
231
+ cut = held.find { |candidate| candidate[:ordinal] == era }&.dig(:watermark)
232
+ declared = edges.last[:translation].for_aggregate(names[:current][edges.size])
233
+ expression = declared ? Translation::RuleCompiler.compile_rules(declared) : "state"
234
+ id_column = Translation::RuleCompiler.rekeyed?(declared) ? Translation::RuleCompiler.id_case("operation = 'save'", declared) : "aggregate_id"
235
+
236
+ <<~SQL
237
+ WITH layered AS (
238
+ SELECT DISTINCT ON (aggregate_id) ordinal, aggregate_id, operation, state FROM (
239
+ SELECT ordinal, aggregate_id, operation, state FROM #{quote(prior_view)}
240
+ UNION ALL
241
+ SELECT ordinal, aggregate_id, operation, state FROM #{quoted_journal}
242
+ WHERE era = #{era - 1} AND aggregate = #{text_literal(names[:storage][era - 2])}#{cut ? " AND ordinal <= #{cut}" : ''}
243
+ ) layers ORDER BY aggregate_id, ordinal DESC
244
+ )
245
+ SELECT ordinal, #{id_column}, operation,
246
+ CASE WHEN operation = 'save' THEN #{expression} ELSE state END AS state
247
+ FROM layered
248
+ SQL
249
+ end
250
+
251
+ def chain_sql(aggregate, era, edges)
252
+ names = names_by_era(aggregate, edges)
253
+ tail = latest_per_id(ancestor_tail_sql(names, era))
254
+ chain = edges.each_with_index.map do |edge, index|
255
+ declared = edge[:translation].for_aggregate(names[:current][index + 1])
256
+ expression = declared ? Translation::RuleCompiler.compile_rules(declared) : "state"
257
+ guard = "era <= #{index + 1} AND operation = 'save'"
258
+ id_column = Translation::RuleCompiler.rekeyed?(declared) ? Translation::RuleCompiler.id_case(guard, declared) : "aggregate_id"
259
+ "edge_#{index + 1} AS (SELECT ordinal, era, #{id_column}, operation, " \
260
+ "CASE WHEN #{guard} THEN #{expression} ELSE state END AS state " \
261
+ "FROM #{index.zero? ? 'tail' : "edge_#{index}"})"
262
+ end
263
+
264
+ <<~SQL
265
+ WITH tail AS (#{tail}),
266
+ #{chain.join(",\n")}
267
+ SELECT ordinal, aggregate_id, operation, state FROM edge_#{edges.size}
268
+ SQL
269
+ end
270
+
271
+ # The translated tail as it would stand in era `era`, latest
272
+ # entry per id, saves only — what the audit holds up against the
273
+ # bluebook and the edge.
274
+ def translated_latest(aggregate, era, edges)
275
+ latest_of(chain_sql(aggregate, era, edges))
276
+ end
277
+
278
+ # The UNtranslated ancestor tail, latest entry per id — the
279
+ # "before" side of every per-rule preservation check.
280
+ def ancestor_latest(aggregate, era, edges)
281
+ names = names_by_era(aggregate, edges)
282
+ tail = ancestor_tail_sql(names, era)
283
+ return {} if tail.empty?
284
+
285
+ latest_of("SELECT ordinal, era, aggregate_id, operation, state FROM (#{tail}) tail_rows")
286
+ end
287
+
288
+ def latest_of(sql)
289
+ rows = @db.exec(<<~SQL)
290
+ SELECT aggregate_id, state FROM (
291
+ SELECT DISTINCT ON (aggregate_id) aggregate_id, operation, state
292
+ FROM (#{sql}) chained ORDER BY aggregate_id, ordinal DESC
293
+ ) latest WHERE operation = 'save'
294
+ SQL
295
+ rows.to_h { |row| [row["aggregate_id"], JSON.parse(row["state"])] }
296
+ end
297
+
298
+ # Era N: materialize the translated ancestor tail (edge chain as
299
+ # CTEs, watermarks baked in), then overlay this era's OWN live
300
+ # rows — read from its snapshot table, not re-derived from raw
301
+ # history — in a plain view.
302
+ #
303
+ # THE FROZEN-TAIL INVARIANT. This materialized view is correct
304
+ # by construction only because BOTH of these hold:
305
+ # 1. journal rows are never updated or deleted (immutability
306
+ # by privilege), and
307
+ # 2. the watermark is baked into this definition as a LITERAL,
308
+ # so post-cut ancestor writes — which DO keep arriving
309
+ # while a fork is live — can never enter the head, even on
310
+ # a full REFRESH.
311
+ # The ancestor partition is append-only but NOT frozen. Any
312
+ # future "optimization" that refreshes incrementally, reads the
313
+ # watermark from hecks_eras at query time, or otherwise
314
+ # re-derives the cut will silently leak the old world's post-cut
315
+ # writes into the new head. Rebuilding the definition (mint,
316
+ # merge) is the only way the cut may move.
317
+ # `full:` forces a rebuild from the raw journal. The tail-merge
318
+ # needs it: it moves EVERY watermark to the new tip, so every
319
+ # ancestor matview's cut goes stale in the same statement, and
320
+ # layering on one would carry a cut that no longer exists.
321
+ #
322
+ # THE LIVE HALF used to be `WHERE era = era AND aggregate = name`
323
+ # over the raw journal — a DISTINCT ON that re-reduced this era's
324
+ # ENTIRE write history on every single read, the exact cost this
325
+ # snapshot table exists to avoid (PostgresEra#append keeps it
326
+ # current, transactionally, as of every write). The union below
327
+ # is bounded by LIVE RECORD COUNT for this era instead of its
328
+ # write count; the ancestor side was already bounded that way
329
+ # (the matview only ever holds the reduced tail, never raw
330
+ # history — see latest_per_id's own comment).
331
+ def compile_head!(aggregate, era, label, edges, full: false)
332
+ storage_name = aggregate.storage_name
333
+ view = matview(storage_name, era, label)
334
+ body =
335
+ if !full && (layered = layered_chain_sql(aggregate, era, edges))
336
+ layered
337
+ else
338
+ chain_sql(aggregate, era, edges)
339
+ end
340
+ @db.exec(<<~SQL)
341
+ CREATE MATERIALIZED VIEW #{quote(view)} AS
342
+ #{body}
343
+ SQL
344
+ # ADDITIVE, changes nothing about what can be pushed through
345
+ # the reduction (that wall is structural, an index doesn't
346
+ # move it — see this file's own module header) — only speeds
347
+ # up the reduction ITSELF, which every read still has to run
348
+ # regardless of a field cache's own two-phase shortcut (a
349
+ # multi-clause query with an uncached clause, an order_by-only
350
+ # query, or simply every read before Track C's cache tables
351
+ # exist for a given field all still hit this). Worth having
352
+ # on any supported server; genuinely skip-scan-usable once
353
+ # running on PG18+.
354
+ @db.exec("CREATE INDEX IF NOT EXISTS #{quote("#{view}_reduce_idx")} ON #{quote(view)} (aggregate_id, ordinal DESC)")
355
+
356
+ # era-qualified, always a FRESH table for a newly-minted era —
357
+ # no separate reset step needed; there is structurally nothing
358
+ # in it yet for anyone to have written, until an append lands
359
+ # under this era specifically.
360
+ ensure_head_snapshot!(storage_name, era)
361
+ @db.exec("DROP VIEW IF EXISTS #{quote(head_view(storage_name))}")
362
+ @db.exec(<<~SQL)
363
+ CREATE VIEW #{quote(head_view(storage_name))} AS
364
+ SELECT id, state FROM (
365
+ SELECT DISTINCT ON (aggregate_id) aggregate_id AS id, operation, state FROM (
366
+ SELECT ordinal, aggregate_id, operation, state FROM #{quote(view)}
367
+ UNION ALL
368
+ SELECT ordinal, id AS aggregate_id, 'save' AS operation, state
369
+ FROM #{quote(head_snapshot(storage_name, era))}
370
+ ) merged ORDER BY aggregate_id, ordinal DESC
371
+ ) latest WHERE operation = 'save'
372
+ SQL
373
+ end
374
+
375
+ # The aggregate's storage name AS OF each era: `was:` chains
376
+ # walked backward from the current name, one edge at a time.
377
+ # names[:current][i] is the declared (Pascal) name after edge i;
378
+ # names[:storage][e] the snake storage name DURING era e (1-based).
379
+ def names_by_era(aggregate, edges)
380
+ current = Array.new(edges.size + 1)
381
+ current[edges.size] = aggregate.name
382
+ (edges.size - 1).downto(0) do |index|
383
+ declared = edges[index][:translation].for_aggregate(current[index + 1])
384
+ current[index] = declared&.was || current[index + 1]
385
+ end
386
+ storage = current.map { |name| Naming.snake(name) }
387
+ { current: current, storage: storage }
388
+ end
389
+
390
+ # Rows this aggregate contributed to every ancestor era, under
391
+ # the name it had THEN, cut at the watermark recorded when the
392
+ # next era was minted.
393
+ def ancestor_tail_sql(names, era)
394
+ watermarks = eras.to_h { |held| [held[:ordinal], held[:watermark]] }
395
+ selects = (1...era).map do |ancestor|
396
+ cut = watermarks[ancestor + 1]
397
+ "SELECT ordinal, era, aggregate, aggregate_id, operation, state FROM #{quoted_journal} " \
398
+ "WHERE era = #{ancestor} AND aggregate = #{text_literal(names[:storage][ancestor - 1])}" \
399
+ "#{cut ? " AND ordinal <= #{cut}" : ''}"
400
+ end
401
+ selects.join(" UNION ALL ")
402
+ end
403
+
404
+ # compile_rules/rekeyed?/id_case/compile_id_expression/
405
+ # compile_compute moved to Translation::RuleCompiler — the
406
+ # PURE half of this compiler, with no database connection, no
407
+ # watermark, no era chain. Extracted so Exporter's build-time
408
+ # SQL export (feeding rust/host's own boot-time mint) can call
409
+ # the exact same code this file's own callers do, rather than
410
+ # a hand-ported duplicate. See that module's own header.
411
+
412
+ def view_exists?(name)
413
+ # pg_class + pg_table_is_visible, not information_schema/
414
+ # pg_matviews by bare name — same shared-instance reasoning
415
+ # as provisioning.rb's own catalog lookups: this must resolve
416
+ # the name the same way search_path would, or a sibling
417
+ # domain's same-named view satisfies a check meant for this
418
+ # domain's own.
419
+ @db.exec_params(
420
+ "SELECT 1 FROM pg_class WHERE relname = $1 AND relkind IN ('v', 'm') " \
421
+ "AND pg_table_is_visible(oid)",
422
+ [name]
423
+ ).ntuples.positive?
424
+ end
425
+ end
426
+ end
427
+ end
428
+ end
429
+ end