hecks 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,478 @@
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
+ # `operation` + a NULLABLE `state` — H3, docs/audits/2026-08-
71
+ # 10-main-bug-audit.md: a delete used to just `DELETE FROM`
72
+ # this table, leaving NO row at all for an id carried in
73
+ # from an ancestor era. `compile_head!`'s union below then
74
+ # had nothing current-era to outrank the ancestor
75
+ # matview's own (still-live-looking) `save` row with, so a
76
+ # deleted ancestor-carried record kept winning `DISTINCT
77
+ # ON` forever. A delete now upserts a TOMBSTONE row here
78
+ # instead (`operation = 'delete'`, `state` NULL) — the
79
+ # exact same ordinal-guarded upsert every write already
80
+ # uses, so it participates in the union/`DISTINCT ON`
81
+ # exactly like a save does, and out-ranks the ancestor row
82
+ # by ordinal the same way a real re-save already did
83
+ # ("re-saves are masked correctly" — the audit's own
84
+ # phrasing for why this half of the read path was never
85
+ # broken).
86
+ @db.exec(<<~SQL)
87
+ CREATE TABLE #{quote(name)} (
88
+ id text PRIMARY KEY,
89
+ ordinal bigint NOT NULL,
90
+ operation text NOT NULL DEFAULT 'save',
91
+ state jsonb
92
+ )
93
+ SQL
94
+ end
95
+ end
96
+ # SELF-HEALING for a table this ADR predates — same idiom as
97
+ # `postgres/schema_builder.rb`'s own `hecks_version` backfill:
98
+ # unconditional, runs on every boot, a no-op once the column
99
+ # is there. A table created before H3's fix has no
100
+ # `operation` column and a `state NOT NULL` constraint a
101
+ # tombstone's NULL state would violate; both are corrected
102
+ # here regardless of which branch above just ran.
103
+ @db.exec("ALTER TABLE #{quote(name)} ADD COLUMN IF NOT EXISTS operation text NOT NULL DEFAULT 'save'")
104
+ @db.exec("ALTER TABLE #{quote(name)} ALTER COLUMN state DROP NOT NULL")
105
+ backfill_head_snapshot!(name, storage_name, era)
106
+ end
107
+
108
+ # The exact reduction era 1's OLD live view used to run on every
109
+ # single read — DISTINCT ON latest-per-id, saves only — now
110
+ # chunked through `ResumableBackfill#chunked_backfill!` instead
111
+ # of one blocking `INSERT ... SELECT` over the whole journal:
112
+ # each chunk reads the next page of distinct ids (a plain SELECT,
113
+ # no lock held — an ordinary reader or writer is never blocked
114
+ # by this running), then upserts it under the same short-held
115
+ # advisory lock + ordinal guard every other upsert in this
116
+ # adapter already uses. See `ResumableBackfill`'s own header for
117
+ # why this is RESUMABLE, not merely safe-to-restart.
118
+ def backfill_head_snapshot!(name, storage_name, era)
119
+ chunked_backfill!(
120
+ name,
121
+ source_sql: lambda do |cursor|
122
+ <<~SQL
123
+ SELECT id, ordinal, state FROM (
124
+ SELECT DISTINCT ON (aggregate_id) aggregate_id AS id, ordinal, operation, state
125
+ FROM #{quoted_journal}
126
+ WHERE era = #{era.to_i} AND aggregate = #{text_literal(storage_name)}
127
+ #{cursor ? "AND aggregate_id > #{text_literal(cursor)}" : ''}
128
+ ORDER BY aggregate_id, ordinal DESC
129
+ ) latest WHERE operation = 'save' ORDER BY id LIMIT #{ResumableBackfill::CHUNK_SIZE}
130
+ SQL
131
+ end,
132
+ upsert: lambda do |rows|
133
+ rows.each do |row|
134
+ # Always `'save'` — `source_sql` above already filtered
135
+ # to `operation = 'save'`, so nothing this backfill ever
136
+ # sees is a delete tombstone.
137
+ @db.exec_params(
138
+ "INSERT INTO #{quote(name)} (id, ordinal, operation, state) VALUES ($1, $2, 'save', $3) " \
139
+ "ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, operation = EXCLUDED.operation, " \
140
+ "state = EXCLUDED.state WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
141
+ [row["id"], row["ordinal"], row["state"]]
142
+ )
143
+ end
144
+ end
145
+ )
146
+ end
147
+
148
+ def table_exists?(name)
149
+ @db.exec_params("SELECT to_regclass($1) IS NOT NULL AS present", [name]).getvalue(0, 0) == "t"
150
+ end
151
+
152
+ # A transaction wrapper safe to call from inside an already-open
153
+ # transaction, unlike `PG::Connection#transaction` (bare
154
+ # BEGIN/COMMIT, no savepoint nesting — see H2). Standalone
155
+ # (`PQTRANS_IDLE`), this is `@db.transaction` itself: a real
156
+ # transaction, committed or rolled back on the way out. Nested
157
+ # (anything else — `PQTRANS_INTRANS` from a manual `BEGIN` like
158
+ # mint_era!'s, or the gem's own `#transaction`), it's a SAVEPOINT
159
+ # instead: released on success, rolled back to (then the error
160
+ # re-raised) on failure, and either way the surrounding
161
+ # transaction is never touched — no early COMMIT, no early
162
+ # release of whatever advisory lock it holds.
163
+ def nested_transaction(name)
164
+ return @db.transaction { yield } if @db.transaction_status == PG::PQTRANS_IDLE
165
+
166
+ @db.exec("SAVEPOINT #{name}")
167
+ begin
168
+ yield
169
+ @db.exec("RELEASE SAVEPOINT #{name}")
170
+ rescue StandardError
171
+ @db.exec("ROLLBACK TO SAVEPOINT #{name}")
172
+ raise
173
+ end
174
+ end
175
+
176
+ # Era 1: the head is the snapshot table itself, verbatim — no
177
+ # per-read reduction over history left to do, because `append`
178
+ # already keeps the snapshot current as of every write.
179
+ def ensure_first_head!(storage_name)
180
+ ensure_head_snapshot!(storage_name, 1)
181
+ # `WHERE operation = 'save'` — era 1 has no ancestor tail to
182
+ # worry about, but the snapshot table can now hold delete
183
+ # tombstones too (H3, see `ensure_head_snapshot!`'s own
184
+ # comment), and a tombstone's `state` is NULL: without this
185
+ # filter a deleted id would still resolve here, just to a nil
186
+ # state, instead of correctly falling out of the head at all.
187
+ @db.exec(<<~SQL)
188
+ CREATE OR REPLACE VIEW #{quote(head_view(storage_name))} AS
189
+ SELECT id, state FROM #{quote(head_snapshot(storage_name, 1))} WHERE operation = 'save'
190
+ SQL
191
+ end
192
+
193
+ # The compiled chain over the ancestor tail — the matview's body,
194
+ # also runnable as a LIVE query (the audit previews a pending
195
+ # era through exactly this SQL before anything is minted).
196
+ #
197
+ # NEVER flatten the edges into one merged rule set, however
198
+ # tempting the optimization looks. The two-line counterexample:
199
+ # edge 1 renames A→B, edge 2 renames C→A. Flattened, a single
200
+ # phase order either applies C→A before A→B (aliasing C's value
201
+ # into B) or drops the recycled name entirely — there is NO
202
+ # correct position for both rules in one pass. Chaining the
203
+ # original edges in mint order reproduces the true execution
204
+ # exactly and needs no such reasoning.
205
+ # ONLY THE LATEST ANCESTOR ENTRY PER ID IS OBSERVABLE. The head,
206
+ # the tail-merge, and the audit all reduce by
207
+ # DISTINCT ON (aggregate_id) ORDER BY ordinal DESC before anyone
208
+ # reads a state, so an entry with a newer sibling can never
209
+ # reach a reader. Translating those siblings computes and stores
210
+ # rows nothing can observe — on an append-only journal a record
211
+ # edited a hundred times cost a hundred translations to serve
212
+ # one.
213
+ #
214
+ # So the tail is reduced BEFORE the chain, not after. The
215
+ # translated output is identical (the reducer is idempotent and
216
+ # the survivor is the same row either way); only the work
217
+ # changes, from |journal entries| to |distinct records|.
218
+ #
219
+ # `era` must survive the reduction: each edge's CASE reads it to
220
+ # decide whether a row is old enough to need that edge applied.
221
+ def latest_per_id(tail)
222
+ return tail if tail.to_s.empty?
223
+
224
+ "SELECT DISTINCT ON (aggregate_id) ordinal, era, aggregate, aggregate_id, operation, state " \
225
+ "FROM (#{tail}) tail_entries ORDER BY aggregate_id, ordinal DESC"
226
+ end
227
+
228
+ # THE LAYERED BUILD — era N from era N-1's matview, not from raw
229
+ # history. Returns nil when it cannot apply, and the caller
230
+ # falls back to the full chain above.
231
+ #
232
+ # The algebra it rests on, both halves load-bearing:
233
+ #
234
+ # 1. THE CUTS DO NOT MOVE. ancestor_tail_sql cuts ancestor k at
235
+ # W(k+1) — the watermark recorded when era k+1 was minted.
236
+ # Those are the SAME literals in the era N-1 build and the
237
+ # era N build, so era N-1's matview already carries exactly
238
+ # the cut era N needs for eras 1..N-2. Only era N-1's own
239
+ # rows are new, and they are cut at W(N). The watermark is
240
+ # still a literal baked into a definition; it is simply
241
+ # baked into the layer beneath. (This is why the tail-merge
242
+ # must pass full: — it moves every watermark at once.)
243
+ #
244
+ # 2. REDUCING IS ASSOCIATIVE. reduce(A ∪ B) == reduce(reduce(A) ∪ B),
245
+ # because the survivor is max-ordinal-per-id either way. So
246
+ # reducing per layer is the same answer as reducing the
247
+ # whole tail once.
248
+ #
249
+ # And every row on both sides of the union is already in era
250
+ # N-1's shape — the matview because it was chained through edge
251
+ # N-2, era N-1's own rows because that is the shape they were
252
+ # written under — so the final edge applies uniformly, with no
253
+ # per-era CASE. That equality is asserted, not argued: the spec
254
+ # builds a third era both ways and diffs them.
255
+ def layered_chain_sql(aggregate, era, edges)
256
+ return nil if era < 3 || edges.size < 2
257
+
258
+ held = eras
259
+ prior = held.find { |candidate| candidate[:ordinal] == era - 1 }
260
+ return nil unless prior && prior[:label]
261
+
262
+ prior_view = matview(aggregate.storage_name, era - 1, prior[:label])
263
+ return nil unless view_exists?(prior_view)
264
+
265
+ names = names_by_era(aggregate, edges)
266
+ cut = held.find { |candidate| candidate[:ordinal] == era }&.dig(:watermark)
267
+ declared = edges.last[:translation].for_aggregate(names[:current][edges.size])
268
+ expression = declared ? Translation::RuleCompiler.compile_rules(declared) : "state"
269
+ id_column = Translation::RuleCompiler.rekeyed?(declared) ? Translation::RuleCompiler.id_case("operation = 'save'", declared) : "aggregate_id"
270
+
271
+ <<~SQL
272
+ WITH layered AS (
273
+ SELECT DISTINCT ON (aggregate_id) ordinal, aggregate_id, operation, state FROM (
274
+ SELECT ordinal, aggregate_id, operation, state FROM #{quote(prior_view)}
275
+ UNION ALL
276
+ SELECT ordinal, aggregate_id, operation, state FROM #{quoted_journal}
277
+ WHERE era = #{era - 1} AND aggregate = #{text_literal(names[:storage][era - 2])}#{cut ? " AND ordinal <= #{cut}" : ''}
278
+ ) layers ORDER BY aggregate_id, ordinal DESC
279
+ )
280
+ SELECT ordinal, #{id_column}, operation,
281
+ CASE WHEN operation = 'save' THEN #{expression} ELSE state END AS state
282
+ FROM layered
283
+ SQL
284
+ end
285
+
286
+ def chain_sql(aggregate, era, edges)
287
+ names = names_by_era(aggregate, edges)
288
+ tail = latest_per_id(ancestor_tail_sql(names, era))
289
+ chain = edges.each_with_index.map do |edge, index|
290
+ declared = edge[:translation].for_aggregate(names[:current][index + 1])
291
+ expression = declared ? Translation::RuleCompiler.compile_rules(declared) : "state"
292
+ guard = "era <= #{index + 1} AND operation = 'save'"
293
+ id_column = Translation::RuleCompiler.rekeyed?(declared) ? Translation::RuleCompiler.id_case(guard, declared) : "aggregate_id"
294
+ "edge_#{index + 1} AS (SELECT ordinal, era, #{id_column}, operation, " \
295
+ "CASE WHEN #{guard} THEN #{expression} ELSE state END AS state " \
296
+ "FROM #{index.zero? ? 'tail' : "edge_#{index}"})"
297
+ end
298
+
299
+ <<~SQL
300
+ WITH tail AS (#{tail}),
301
+ #{chain.join(",\n")}
302
+ SELECT ordinal, aggregate_id, operation, state FROM edge_#{edges.size}
303
+ SQL
304
+ end
305
+
306
+ # The translated tail as it would stand in era `era`, latest
307
+ # entry per id, saves only — what the audit holds up against the
308
+ # bluebook and the edge.
309
+ def translated_latest(aggregate, era, edges)
310
+ latest_of(chain_sql(aggregate, era, edges))
311
+ end
312
+
313
+ # The UNtranslated ancestor tail, latest entry per id — the
314
+ # "before" side of every per-rule preservation check.
315
+ def ancestor_latest(aggregate, era, edges)
316
+ names = names_by_era(aggregate, edges)
317
+ tail = ancestor_tail_sql(names, era)
318
+ return {} if tail.empty?
319
+
320
+ latest_of("SELECT ordinal, era, aggregate_id, operation, state FROM (#{tail}) tail_rows")
321
+ end
322
+
323
+ def latest_of(sql)
324
+ rows = @db.exec(<<~SQL)
325
+ SELECT aggregate_id, state FROM (
326
+ SELECT DISTINCT ON (aggregate_id) aggregate_id, operation, state
327
+ FROM (#{sql}) chained ORDER BY aggregate_id, ordinal DESC
328
+ ) latest WHERE operation = 'save'
329
+ SQL
330
+ rows.to_h { |row| [row["aggregate_id"], JSON.parse(row["state"])] }
331
+ end
332
+
333
+ # Era N: materialize the translated ancestor tail (edge chain as
334
+ # CTEs, watermarks baked in), then overlay this era's OWN live
335
+ # rows — read from its snapshot table, not re-derived from raw
336
+ # history — in a plain view.
337
+ #
338
+ # THE FROZEN-TAIL INVARIANT. This materialized view is correct
339
+ # by construction only because BOTH of these hold:
340
+ # 1. journal rows are never updated or deleted (immutability
341
+ # by privilege), and
342
+ # 2. the watermark is baked into this definition as a LITERAL,
343
+ # so post-cut ancestor writes — which DO keep arriving
344
+ # while a fork is live — can never enter the head, even on
345
+ # a full REFRESH.
346
+ # The ancestor partition is append-only but NOT frozen. Any
347
+ # future "optimization" that refreshes incrementally, reads the
348
+ # watermark from hecks_eras at query time, or otherwise
349
+ # re-derives the cut will silently leak the old world's post-cut
350
+ # writes into the new head. Rebuilding the definition (mint,
351
+ # merge) is the only way the cut may move.
352
+ # `full:` forces a rebuild from the raw journal. The tail-merge
353
+ # needs it: it moves EVERY watermark to the new tip, so every
354
+ # ancestor matview's cut goes stale in the same statement, and
355
+ # layering on one would carry a cut that no longer exists.
356
+ #
357
+ # THE LIVE HALF used to be `WHERE era = era AND aggregate = name`
358
+ # over the raw journal — a DISTINCT ON that re-reduced this era's
359
+ # ENTIRE write history on every single read, the exact cost this
360
+ # snapshot table exists to avoid (PostgresEra#append keeps it
361
+ # current, transactionally, as of every write). The union below
362
+ # is bounded by LIVE RECORD COUNT for this era instead of its
363
+ # write count; the ancestor side was already bounded that way
364
+ # (the matview only ever holds the reduced tail, never raw
365
+ # history — see latest_per_id's own comment).
366
+ def compile_head!(aggregate, era, label, edges, full: false)
367
+ storage_name = aggregate.storage_name
368
+ view = matview(storage_name, era, label)
369
+ body =
370
+ if !full && (layered = layered_chain_sql(aggregate, era, edges))
371
+ layered
372
+ else
373
+ chain_sql(aggregate, era, edges)
374
+ end
375
+ @db.exec(<<~SQL)
376
+ CREATE MATERIALIZED VIEW #{quote(view)} AS
377
+ #{body}
378
+ SQL
379
+ # ADDITIVE, changes nothing about what can be pushed through
380
+ # the reduction (that wall is structural, an index doesn't
381
+ # move it — see this file's own module header) — only speeds
382
+ # up the reduction ITSELF, which every read still has to run
383
+ # regardless of a field cache's own two-phase shortcut (a
384
+ # multi-clause query with an uncached clause, an order_by-only
385
+ # query, or simply every read before Track C's cache tables
386
+ # exist for a given field all still hit this). Worth having
387
+ # on any supported server; genuinely skip-scan-usable once
388
+ # running on PG18+.
389
+ @db.exec("CREATE INDEX IF NOT EXISTS #{quote("#{view}_reduce_idx")} ON #{quote(view)} (aggregate_id, ordinal DESC)")
390
+
391
+ # era-qualified, always a FRESH table for a newly-minted era —
392
+ # no separate reset step needed; there is structurally nothing
393
+ # in it yet for anyone to have written, until an append lands
394
+ # under this era specifically.
395
+ ensure_head_snapshot!(storage_name, era)
396
+ @db.exec("DROP VIEW IF EXISTS #{quote(head_view(storage_name))}")
397
+ # THE CURRENT-ERA SIDE READS ITS OWN `operation` COLUMN NOW —
398
+ # H3 (docs/audits/2026-08-10-main-bug-audit.md). This used to
399
+ # hardcode `'save' AS operation` here, on the reasoning that a
400
+ # delete simply removed its row from the snapshot table. But
401
+ # for an id carried in from an ancestor era, that left the
402
+ # ancestor matview's own `save` row as the ONLY row on either
403
+ # side of this union — which then won `DISTINCT ON` and
404
+ # `WHERE operation = 'save'` let it straight through, so a
405
+ # deleted ancestor-carried record kept resurrecting forever.
406
+ # The snapshot side now upserts a real tombstone row on
407
+ # delete (`ensure_head_snapshot!`'s own comment) instead of
408
+ # deleting the row, so reading its actual `operation` here
409
+ # lets that tombstone outrank the stale ancestor `save` row
410
+ # by ordinal, exactly the way a genuine re-save already did.
411
+ @db.exec(<<~SQL)
412
+ CREATE VIEW #{quote(head_view(storage_name))} AS
413
+ SELECT id, state FROM (
414
+ SELECT DISTINCT ON (aggregate_id) aggregate_id AS id, operation, state FROM (
415
+ SELECT ordinal, aggregate_id, operation, state FROM #{quote(view)}
416
+ UNION ALL
417
+ SELECT ordinal, id AS aggregate_id, operation, state
418
+ FROM #{quote(head_snapshot(storage_name, era))}
419
+ ) merged ORDER BY aggregate_id, ordinal DESC
420
+ ) latest WHERE operation = 'save'
421
+ SQL
422
+ end
423
+
424
+ # The aggregate's storage name AS OF each era: `was:` chains
425
+ # walked backward from the current name, one edge at a time.
426
+ # names[:current][i] is the declared (Pascal) name after edge i;
427
+ # names[:storage][e] the snake storage name DURING era e (1-based).
428
+ def names_by_era(aggregate, edges)
429
+ current = Array.new(edges.size + 1)
430
+ current[edges.size] = aggregate.name
431
+ (edges.size - 1).downto(0) do |index|
432
+ declared = edges[index][:translation].for_aggregate(current[index + 1])
433
+ current[index] = declared&.was || current[index + 1]
434
+ end
435
+ storage = current.map { |name| Naming.snake(name) }
436
+ { current: current, storage: storage }
437
+ end
438
+
439
+ # Rows this aggregate contributed to every ancestor era, under
440
+ # the name it had THEN, cut at the watermark recorded when the
441
+ # next era was minted.
442
+ def ancestor_tail_sql(names, era)
443
+ watermarks = eras.to_h { |held| [held[:ordinal], held[:watermark]] }
444
+ selects = (1...era).map do |ancestor|
445
+ cut = watermarks[ancestor + 1]
446
+ "SELECT ordinal, era, aggregate, aggregate_id, operation, state FROM #{quoted_journal} " \
447
+ "WHERE era = #{ancestor} AND aggregate = #{text_literal(names[:storage][ancestor - 1])}" \
448
+ "#{cut ? " AND ordinal <= #{cut}" : ''}"
449
+ end
450
+ selects.join(" UNION ALL ")
451
+ end
452
+
453
+ # compile_rules/rekeyed?/id_case/compile_id_expression/
454
+ # compile_compute moved to Translation::RuleCompiler — the
455
+ # PURE half of this compiler, with no database connection, no
456
+ # watermark, no era chain. Extracted so Exporter's build-time
457
+ # SQL export (feeding rust/host's own boot-time mint) can call
458
+ # the exact same code this file's own callers do, rather than
459
+ # a hand-ported duplicate. See that module's own header.
460
+
461
+ def view_exists?(name)
462
+ # pg_class + pg_table_is_visible, not information_schema/
463
+ # pg_matviews by bare name — same shared-instance reasoning
464
+ # as provisioning.rb's own catalog lookups: this must resolve
465
+ # the name the same way search_path would, or a sibling
466
+ # domain's same-named view satisfies a check meant for this
467
+ # domain's own.
468
+ @db.exec_params(
469
+ "SELECT 1 FROM pg_class WHERE relname = $1 AND relkind IN ('v', 'm') " \
470
+ "AND pg_table_is_visible(oid)",
471
+ [name]
472
+ ).ntuples.positive?
473
+ end
474
+ end
475
+ end
476
+ end
477
+ end
478
+ end
@@ -0,0 +1,166 @@
1
+ require_relative "../../../../../../runtime/registry"
2
+ require_relative "../../storage_shape"
3
+
4
+ module Hecks
5
+ module Adapters
6
+ class PostgresEra
7
+ class Lineage
8
+ module MintTransaction
9
+ # ── the mint transaction ───────────────────────────────────────
10
+ #
11
+ # One transaction: the new era row (held text + minted name +
12
+ # cut watermark), the new partition, and every aggregate's
13
+ # recompiled matview + head view. The advisory lock is the
14
+ # writer fence — two concurrent minters serialize, and the
15
+ # second finds the era already held. Populating the matview
16
+ # inside the transaction means a convert meeting an unmapped
17
+ # value REFUSES the whole mint, loudly, before anything boots.
18
+ def mint_era!(ordinal:, hash:, label:, held_text:, aggregates:, edges:, role: nil, projection: nil)
19
+ @db.exec("BEGIN")
20
+ # A concurrent minter blocks briefly, then refuses with a name
21
+ # rather than hanging: mint is non-interactive (the human
22
+ # decision already happened in the audit tool), so the lock is
23
+ # only ever held for the transaction itself.
24
+ @db.exec("SET LOCAL lock_timeout = '10s'")
25
+ @db.exec("SELECT pg_advisory_xact_lock(hashtext('hecks_eras:' || #{text_literal(@domain)}))")
26
+ if eras.any? { |era| era[:ordinal] == ordinal }
27
+ @db.exec("ROLLBACK")
28
+ return false
29
+ end
30
+
31
+ watermark = last_ordinal
32
+ @db.exec_params(
33
+ "INSERT INTO hecks_eras (domain, ordinal, hash, label, held_text, watermark, held_digest, held_projection, canon_form) " \
34
+ "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
35
+ [@domain, ordinal, hash, label, held_text, watermark, Digest::SHA256.hexdigest(held_text),
36
+ projection && JSON.generate(projection), Runtime::StorageShape::FORM_VERSION]
37
+ )
38
+ archive_text!(ordinal, held_text)
39
+ ensure_partition!(ordinal)
40
+ # AFTER compile_head!, not before — this era's own snapshot
41
+ # table (a possibly-renamed name, per aggregate) is what
42
+ # grant_role! grants on, and compile_head! is what creates it.
43
+ # Granting first would GRANT on a relation that does not
44
+ # exist yet for any aggregate renamed in this very edge.
45
+ aggregates.each { |aggregate| compile_head!(aggregate, ordinal, label, edges) }
46
+ grant_role!(role, aggregates: aggregates, era: ordinal) if role
47
+ # LAST, right before COMMIT — not merely unconditional. Once
48
+ # acquired, a lock is held until the TRANSACTION ends, not
49
+ # just for the statement that took it — so advance_era!'s
50
+ # DROP POLICY/CREATE POLICY (AccessExclusiveLock, same family
51
+ # as ALTER TABLE, and unavoidably so: Postgres has no lighter
52
+ # form for changing a policy, unlike the partition attach
53
+ # below) blocks every concurrent writer for as long as it sits
54
+ # BEFORE the expensive step. Ordered here, that block is the
55
+ # width of a few catalog statements plus the commit itself,
56
+ # not the width of compile_head!'s matview build. Measured:
57
+ # moving this above compile_head! (an earlier ordering, caught
58
+ # only once a genuine concurrent-write test was built rather
59
+ # than assumed) reintroduced exactly the mint-stops-the-world
60
+ # cost ensure_partition!'s build-then-ATTACH exists to avoid.
61
+ #
62
+ # UNCONDITIONAL regardless of position — this is the line that
63
+ # drops writing to the old schema. It does not wait for a role
64
+ # to be configured on THIS boot, because the cutoff is a fact
65
+ # about the era, not about who happened to mint it: an old
66
+ # checkout's role, granted by some earlier boot this one knows
67
+ # nothing about, must lose write access the instant this
68
+ # transaction commits.
69
+ advance_era!(ordinal)
70
+ @db.exec("COMMIT")
71
+ true
72
+ rescue PG::LockNotAvailable
73
+ @db.exec("ROLLBACK") rescue nil
74
+ raise Runtime::WiringError,
75
+ "cannot mint era #{ordinal} of #{@domain}: another mint holds the domain lock — " \
76
+ "waited 10s; try again shortly"
77
+ rescue PG::Error => error
78
+ @db.exec("ROLLBACK") rescue nil
79
+ raise Runtime::WiringError, "cannot mint era #{ordinal} of #{@domain}: #{error.message.strip}"
80
+ end
81
+
82
+ # Base privileges for a deployment's app role — a NON-owner,
83
+ # which may append and read once the shared era fence below
84
+ # admits it, and owns nothing. Idempotent, and unconcerned with
85
+ # WHICH era is current: that is advance_era!'s job, not this
86
+ # one's, so a role can be onboarded at any time without
87
+ # disturbing who may write what right now.
88
+ #
89
+ # Owner-only work, so a non-owner app boot skips it: the
90
+ # provisioner has already granted that role, and re-affirming is
91
+ # idempotent anyway.
92
+ #
93
+ # `aggregates:`/`era:` cover the read-cache side of the same
94
+ # story: unlike the journal (immutable, owner-provisioned once),
95
+ # each aggregate's head_snapshot table is a table an app role
96
+ # must itself INSERT/UPDATE/DELETE into — PostgresEra#append writes
97
+ # it directly, not through a view — so it needs real DML grants,
98
+ # not just the SELECT a derived read surface would need. `era:`
99
+ # is the ordinal THIS role is about to write under (the one
100
+ # hold_first!/mint_era! just made current, or the superseded one
101
+ # a stale checkout still speaks) — head_snapshot is era-scoped,
102
+ # so granting on the wrong era's table would grant on a table
103
+ # this role will never touch. Every caller passes its own
104
+ # bluebook's aggregates and resolved era — including a
105
+ # held-but-superseded checkout, since a role connecting to it
106
+ # for the first time (a new instance of an old checkout) has no
107
+ # privileges yet either.
108
+ def grant_role!(role, aggregates: [], era: nil)
109
+ return unless provisioner?
110
+
111
+ quoted_role = quote(role)
112
+ @db.exec("GRANT INSERT, SELECT ON #{quoted_journal} TO #{quoted_role}")
113
+ @db.exec("GRANT USAGE ON SEQUENCE #{quote(sequence)} TO #{quoted_role}")
114
+ return unless era
115
+
116
+ aggregates.each do |aggregate|
117
+ storage_name = aggregate.storage_name
118
+ @db.exec("GRANT SELECT, INSERT, UPDATE, DELETE ON #{quote(head_snapshot(storage_name, era))} TO #{quoted_role}")
119
+ @db.exec("GRANT SELECT ON #{quote(head_view(storage_name))} TO #{quoted_role}") if view_exists?(head_view(storage_name))
120
+ end
121
+ end
122
+
123
+ # THE current-era fence — ONE policy, shared by every granted
124
+ # role, not one per role. Advancing it is what drops writing to
125
+ # the old schema the instant the new one materializes: the
126
+ # moment this commits, no role — old or new, whether or not it
127
+ # did anything to earn this mint — may insert anything but the
128
+ # era named here. There is no persisted fork: a checkout that
129
+ # keeps a role fenced to a stale era does not exist in this
130
+ # design, because there is no such thing as a role fenced to an
131
+ # era at all — only the ONE era everyone currently shares.
132
+ #
133
+ # A ROW POLICY, not a partition grant. Postgres checks INSERT
134
+ # privilege on the partitioned PARENT for a routed insert and
135
+ # never consults the partition's own grants, so a per-partition
136
+ # GRANT/REVOKE is inert in both directions: grant only on the
137
+ # partition and nobody can write at all; grant on the parent and
138
+ # they may write into EVERY era, ancestors included. Measured,
139
+ # not reasoned about — see the spec, which writes through the
140
+ # fence rather than asserting the catalog.
141
+ #
142
+ # The table owner bypasses RLS by default, and that is load
143
+ # bearing: mint and merge run as the owner and must be able to
144
+ # write any era (the merge re-enters a winner's state into the
145
+ # CURRENT era, and compile_head! reads every ancestor).
146
+ #
147
+ # CALL ONLY WITH THE NEW CURRENT ORDINAL — from hold_first! (era
148
+ # 1) or mint_era! (era N). Calling this with a SUPERSEDED
149
+ # ordinal — from a boot that merely recognizes an old checkout —
150
+ # would roll the fence backward and silently reopen the old
151
+ # schema for everyone. That path grants a role's PRIVILEGES
152
+ # (grant_role!) and stops there on purpose.
153
+ def advance_era!(ordinal)
154
+ @db.exec("DROP POLICY IF EXISTS hecks_current_era ON #{quoted_journal}")
155
+ @db.exec(
156
+ "CREATE POLICY hecks_current_era ON #{quoted_journal} FOR INSERT TO PUBLIC " \
157
+ "WITH CHECK (era = #{ordinal.to_i})"
158
+ )
159
+ @db.exec("DROP POLICY IF EXISTS hecks_read_all ON #{quoted_journal}")
160
+ @db.exec("CREATE POLICY hecks_read_all ON #{quoted_journal} FOR SELECT TO PUBLIC USING (true)")
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end