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,166 @@
1
+ require_relative "../../../../runtime/registry"
2
+ require_relative "../../../../runtime/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
@@ -0,0 +1,286 @@
1
+ module Hecks
2
+ module Adapters
3
+ class PostgresEra
4
+ class Lineage
5
+ module Provisioning
6
+ # Provisioning is the OWNER's job, and a deployment's app role is
7
+ # deliberately not the owner — it may append and read, and it
8
+ # owns nothing. By the time such a role connects, the base is
9
+ # already built, so its boot verifies rather than builds.
10
+ #
11
+ # Without this guard the per-era fence below is unreachable: the
12
+ # ALTER TABLE and REVOKE here are owner-only, so the very role
13
+ # grant_era! exists to constrain could never finish booting
14
+ # ("must be owner of table hecks_eras"). A fence nothing can
15
+ # reach is not a fence.
16
+ def ensure_base!
17
+ rename_domain! if @formerly_known_as
18
+ return unless provisioner?
19
+
20
+ @db.exec(<<~SQL)
21
+ CREATE TABLE IF NOT EXISTS hecks_eras (
22
+ domain text NOT NULL,
23
+ ordinal int NOT NULL,
24
+ hash text,
25
+ label text,
26
+ held_text text NOT NULL,
27
+ watermark bigint,
28
+ PRIMARY KEY (domain, ordinal)
29
+ )
30
+ SQL
31
+ @db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_digest text")
32
+ @db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS held_projection jsonb")
33
+ # which canonical-form version minted this name — see
34
+ # Runtime::StorageShape::FORM_VERSION; rows minted before the
35
+ # column carry NULL, read as an implicit 1
36
+ @db.exec("ALTER TABLE hecks_eras ADD COLUMN IF NOT EXISTS canon_form int")
37
+ # every frozen text version, archived where an edit cannot
38
+ # reach it — the recovery the hard reattest refusal points at
39
+ @db.exec(<<~SQL)
40
+ CREATE TABLE IF NOT EXISTS hecks_era_texts (
41
+ domain text NOT NULL,
42
+ ordinal int NOT NULL,
43
+ digest text NOT NULL,
44
+ held_text text NOT NULL,
45
+ archived_at timestamptz NOT NULL DEFAULT now(),
46
+ PRIMARY KEY (domain, ordinal, digest)
47
+ )
48
+ SQL
49
+ @db.exec(<<~SQL)
50
+ CREATE TABLE IF NOT EXISTS hecks_approvals (
51
+ domain text NOT NULL,
52
+ from_label text NOT NULL,
53
+ to_label text NOT NULL,
54
+ edge_digest text NOT NULL,
55
+ reviewed_ordinal bigint NOT NULL,
56
+ approved_at timestamptz NOT NULL DEFAULT now()
57
+ )
58
+ SQL
59
+ @db.exec("CREATE SEQUENCE IF NOT EXISTS #{quote(sequence)}")
60
+ # GENERATED ALWAYS AS IDENTITY is the intent, but identity
61
+ # columns on partitioned tables need Postgres 17 — an owned
62
+ # sequence default is the same spanning ordinal on any
63
+ # supported server.
64
+ @db.exec(<<~SQL)
65
+ CREATE TABLE IF NOT EXISTS #{quoted_journal} (
66
+ ordinal bigint NOT NULL DEFAULT nextval('#{sequence}'),
67
+ era int NOT NULL,
68
+ aggregate text NOT NULL,
69
+ aggregate_id text NOT NULL,
70
+ operation text NOT NULL DEFAULT 'save',
71
+ state jsonb,
72
+ mirrors jsonb
73
+ ) PARTITION BY LIST (era)
74
+ SQL
75
+ ensure_partition!(1)
76
+ # Immutability by privilege: nothing updates or deletes journal
77
+ # rows. The owner's implicit rights remain (Postgres has no way
78
+ # to revoke them from the owner itself); a deployment's app
79
+ # role connects as a NON-owner and gets exactly INSERT, per
80
+ # era, at mint time.
81
+ @db.exec("REVOKE UPDATE, DELETE ON #{quoted_journal} FROM PUBLIC")
82
+ # RLS goes on AT PROVISIONING, never mid-life — enabling it
83
+ # later would deny every role that has no policy yet, on
84
+ # whatever the shape of the schema happened to be at that
85
+ # moment.
86
+ #
87
+ # FORCE, not merely ENABLE: without it, the table OWNER is
88
+ # exempt from every policy here, by Postgres default — which
89
+ # would leave the schema writable forever to whoever holds
90
+ # the owner's credentials, the one connection this whole
91
+ # design cannot fence. Checked, not assumed: mint_era! never
92
+ # inserts into the journal at all (only hecks_eras/
93
+ # hecks_era_texts, neither RLS-protected), and merge_tail!'s
94
+ # one journal INSERT targets the CURRENT era, which the fence
95
+ # already admits for anyone with base privileges — so FORCE
96
+ # costs the owner nothing operations here actually need.
97
+ #
98
+ # This still exempts an actual Postgres SUPERUSER (or any
99
+ # role granted BYPASSRLS) unconditionally — FORCE only
100
+ # narrows what ENABLE already narrows for the owner
101
+ # specifically, and superuser bypass sits above both. Running
102
+ # migrations as a real superuser (self-hosted Postgres, most
103
+ # commonly) leaves this gap open regardless; a managed
104
+ # provider's admin account is typically NOT a superuser, and
105
+ # is exactly what FORCE closes.
106
+ #
107
+ # GUARDED, not reissued unconditionally — measured, not
108
+ # assumed: `ALTER TABLE ... ENABLE/FORCE ROW LEVEL SECURITY`
109
+ # takes AccessExclusiveLock EVEN WHEN THE SETTING IS ALREADY
110
+ # CORRECT (Postgres does not skip the lock just because the
111
+ # statement would be a no-op). ensure_base! runs on EVERY
112
+ # boot by the owning role, not only the first — so an
113
+ # unconditional reissue here would mean every ordinary
114
+ # reboot of the deployment's own identity re-freezes every
115
+ # concurrent writer, on any era, for as long as that ALTER
116
+ # TABLE has to wait its turn. Read the current state first;
117
+ # touch the catalog only on the boot that actually needs to.
118
+ # pg_table_is_visible, NOT a bare relname match — a shared
119
+ # instance (storehouse) can hold a same-named journal table
120
+ # per schema; catalog lookups here must resolve the SAME way
121
+ # search_path resolves an unqualified SQL reference, or a
122
+ # sibling domain's table satisfies a query meant for this
123
+ # domain's own.
124
+ current = @db.exec_params(
125
+ "SELECT relrowsecurity, relforcerowsecurity FROM pg_class " \
126
+ "WHERE relname = $1 AND pg_table_is_visible(oid)", [journal]
127
+ )[0]
128
+ @db.exec("ALTER TABLE #{quoted_journal} ENABLE ROW LEVEL SECURITY") unless current["relrowsecurity"] == "t"
129
+ @db.exec("ALTER TABLE #{quoted_journal} FORCE ROW LEVEL SECURITY") unless current["relforcerowsecurity"] == "t"
130
+ install_transforms!
131
+ end
132
+
133
+ # A DOMAIN'S OWN IDENTITY CHANGED — bridge its history under the
134
+ # new name, before `provisioner?`/the CREATE TABLE IF NOT EXISTS
135
+ # block below ever run. That ordering is load-bearing, not
136
+ # tidiness: `provisioner?` and every statement in ensure_base!
137
+ # test for the journal under `journal` — the NEW name — and a
138
+ # freshly-renamed domain looks, to those checks, exactly like a
139
+ # domain that has never been provisioned at all. Left where it
140
+ # was written, ensure_base! would happily CREATE TABLE IF NOT
141
+ # EXISTS a brand-new, empty journal under the new name before
142
+ # this method ever got a chance to run — and the ALTER TABLE ...
143
+ # RENAME below would then fail, renaming onto a name that
144
+ # already exists.
145
+ #
146
+ # Three-way precheck, cheapest first:
147
+ # 1. no hecks_eras table at all yet — a genuinely fresh
148
+ # database; nothing to bridge, fall through to the
149
+ # ordinary provisioning path.
150
+ # 2. hecks_eras already has rows under the NEW name — this
151
+ # rename already ran (a prior boot, possibly this one on a
152
+ # retry); idempotent no-op.
153
+ # 3. hecks_eras has rows under the OLD name — run the
154
+ # migration.
155
+ # Anything else (no rows under either name) falls through
156
+ # harmlessly — `formerly_known_as` pointing at a name with no
157
+ # held history is not an error, just inert.
158
+ def rename_domain!
159
+ return unless @db.exec_params("SELECT to_regclass($1)", ["hecks_eras"])[0]["to_regclass"]
160
+ return if @db.exec_params(
161
+ "SELECT 1 FROM hecks_eras WHERE domain = $1 LIMIT 1", [@domain]
162
+ ).ntuples.positive?
163
+ return if @db.exec_params(
164
+ "SELECT 1 FROM hecks_eras WHERE domain = $1 LIMIT 1", [@formerly_known_as]
165
+ ).ntuples.zero?
166
+
167
+ old_journal = "hecks_journal_#{Naming.snake(@formerly_known_as)}"
168
+ old_sequence = "#{old_journal}_ordinal"
169
+ ordinals = @db.exec_params(
170
+ "SELECT ordinal FROM hecks_eras WHERE domain = $1 ORDER BY ordinal", [@formerly_known_as]
171
+ ).map { |row| row["ordinal"].to_i }
172
+
173
+ @db.exec("BEGIN")
174
+ @db.exec("SET LOCAL lock_timeout = '10s'")
175
+ # Fixed order — old before new, the `eras:` family before the
176
+ # `ordinal:` family — so two rename attempts (or a rename
177
+ # racing a mint/plain-write on either name) can only ever
178
+ # queue behind one another, never deadlock.
179
+ [
180
+ "hecks_eras:#{@formerly_known_as}", "hecks_eras:#{@domain}",
181
+ "hecks_ordinal:#{@formerly_known_as}", "hecks_ordinal:#{@domain}"
182
+ ].each do |key|
183
+ @db.exec_params("SELECT pg_advisory_xact_lock(hashtext($1))", [key])
184
+ end
185
+
186
+ @db.exec("ALTER TABLE #{quote(old_journal)} RENAME TO #{quote(journal)}")
187
+ # An owned SERIAL/IDENTITY sequence moves automatically with
188
+ # its table and errors if renamed explicitly — this one is a
189
+ # plain CREATE SEQUENCE the journal's DEFAULT merely points
190
+ # at (see ensure_base!), so it does need its own rename, and
191
+ # the column default survives untouched: Postgres stores
192
+ # nextval('...') as a regclass reference internally, not
193
+ # literal text.
194
+ @db.exec("ALTER SEQUENCE #{quote(old_sequence)} RENAME TO #{quote(sequence)}")
195
+ # ALTER TABLE ... RENAME on the parent does NOT cascade to
196
+ # child partition names — each one needs its own explicit
197
+ # rename, sourced from the ordinals held under the OLD name,
198
+ # captured above before the UPDATE below flips the column.
199
+ ordinals.each do |ordinal|
200
+ old_partition = "#{old_journal}_era_#{ordinal}"
201
+ @db.exec("ALTER TABLE #{quote(old_partition)} RENAME TO #{quote(partition(ordinal))}")
202
+ end
203
+
204
+ %w[hecks_eras hecks_era_texts hecks_approvals].each do |table|
205
+ @db.exec_params("UPDATE #{table} SET domain = $1 WHERE domain = $2", [@domain, @formerly_known_as])
206
+ end
207
+ # Unlike its siblings, hecks_attestations is not created in
208
+ # ensure_base! at all — only lazily, on first reattest! — so a
209
+ # domain that never needed one must not be forced through an
210
+ # UPDATE against a table that doesn't exist.
211
+ @db.exec_params("UPDATE hecks_attestations SET domain = $1 WHERE domain = $2", [@domain, @formerly_known_as]) if @db.exec_params("SELECT to_regclass($1)", ["hecks_attestations"])[0]["to_regclass"]
212
+
213
+ @db.exec("COMMIT")
214
+ rescue PG::LockNotAvailable
215
+ @db.exec("ROLLBACK") rescue nil
216
+ raise Runtime::WiringError,
217
+ "cannot rename #{@formerly_known_as} to #{@domain}: another rename, mint, or write holds " \
218
+ "one of the domain locks — waited 10s; try again shortly"
219
+ rescue PG::Error => error
220
+ @db.exec("ROLLBACK") rescue nil
221
+ raise Runtime::WiringError, "cannot rename #{@formerly_known_as} to #{@domain}: #{error.message.strip}"
222
+ end
223
+
224
+ # Nothing provisioned yet — build it. Provisioned and owned —
225
+ # keep it current. Provisioned by SOMEONE ELSE — this is an app
226
+ # role, and the owner has already done this work.
227
+ def provisioner?
228
+ rows = @db.exec_params(
229
+ "SELECT pg_get_userbyid(relowner) = current_user AS owned FROM pg_class " \
230
+ "WHERE relname = $1 AND pg_table_is_visible(oid)",
231
+ [journal]
232
+ )
233
+ rows.ntuples.zero? || rows[0]["owned"] == "t"
234
+ end
235
+
236
+ # BUILD, THEN ATTACH — never CREATE ... PARTITION OF. The two
237
+ # produce the same partition; only the lock differs, and that
238
+ # difference is the whole availability story of a mint:
239
+ #
240
+ # CREATE TABLE ... PARTITION OF → AccessExclusiveLock (parent)
241
+ # CREATE, then ALTER ... ATTACH → ShareUpdateExclusiveLock
242
+ #
243
+ # AccessExclusive conflicts with every insert in the hierarchy —
244
+ # routed through the parent OR addressed to an existing leaf —
245
+ # so attaching the new era inside the mint transaction stopped
246
+ # every writer for the WHOLE mint, tail materialization
247
+ # included. ShareUpdateExclusive conflicts with neither, so the
248
+ # old checkout keeps writing its own era straight through the
249
+ # build and only pauses for the head swap at the end.
250
+ #
251
+ # That is what makes the fork real DURING a mint rather than
252
+ # merely before and after one. Measured, and pinned by the spec
253
+ # — which writes through a live mint rather than reading a lock
254
+ # mode out of the catalog.
255
+ def ensure_partition!(era)
256
+ return if partition_attached?(era)
257
+
258
+ @db.exec(<<~SQL)
259
+ CREATE TABLE IF NOT EXISTS #{quote(partition(era))} (
260
+ LIKE #{quoted_journal} INCLUDING DEFAULTS
261
+ )
262
+ SQL
263
+ @db.exec(<<~SQL)
264
+ ALTER TABLE #{quoted_journal}
265
+ ATTACH PARTITION #{quote(partition(era))} FOR VALUES IN (#{era.to_i})
266
+ SQL
267
+ end
268
+
269
+ # Attached, not merely present: a crash between the CREATE and
270
+ # the ATTACH leaves a table that is not yet part of the journal,
271
+ # and the next boot must finish the job rather than skip it.
272
+ def partition_attached?(era)
273
+ @db.exec_params(
274
+ "SELECT 1 FROM pg_inherits i " \
275
+ "JOIN pg_class child ON child.oid = i.inhrelid " \
276
+ "JOIN pg_class parent ON parent.oid = i.inhparent " \
277
+ "WHERE child.relname = $1 AND parent.relname = $2 " \
278
+ "AND pg_table_is_visible(child.oid) AND pg_table_is_visible(parent.oid)",
279
+ [partition(era), journal]
280
+ ).ntuples.positive?
281
+ end
282
+ end
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,168 @@
1
+ module Hecks
2
+ module Adapters
3
+ class PostgresEra
4
+ class Lineage
5
+ # THE ONE CHUNKED, LOCK-FREE, RESUMABLE BACKFILL LOOP — shared by
6
+ # `backfill_head_snapshot!` (era 1's existing one-shot blocking
7
+ # backfill, retrofit) and every field-cache table's own initial
8
+ # backfill (new). Governing principle 1 (docs/implemented/postgres-era-adapter-
9
+ # split-plan.md): no operation this plan touches may hold a lock
10
+ # across a scan whose duration scales with table size — a single
11
+ # `INSERT ... SELECT` over the whole journal (what
12
+ # `backfill_head_snapshot!` used to be) is exactly that, and so is
13
+ # a naive "populate every cache row in one statement" field-cache
14
+ # backfill.
15
+ #
16
+ # THE SHAPE: read one bounded chunk (real rows, real ordinals) with
17
+ # a plain SELECT — no lock held across it, so an ordinary reader or
18
+ # writer is never blocked by a backfill in progress — then upsert
19
+ # that chunk under the SAME transactionally-scoped advisory lock +
20
+ # ordinal-guard idiom `append`'s own snapshot upsert already uses
21
+ # (`WHERE ordinal < EXCLUDED.ordinal`), then persist a cursor
22
+ # before moving to the next chunk. Repeat until a chunk reads back
23
+ # short of a full page — that page was the last one.
24
+ #
25
+ # RESUMABLE, not merely restartable. A crash (or a second
26
+ # concurrent boot) mid-backfill leaves the cursor exactly where the
27
+ # last COMMITTED chunk left it — `hecks_backfill_progress` is
28
+ # updated in the SAME transaction as the chunk's own upsert, so
29
+ # cursor and data can never observably disagree (see
30
+ # `run_chunk!`). The next attempt reads that cursor and continues;
31
+ # it does not rescan what a prior attempt already committed.
32
+ # RESTARTABLE would also be CORRECT here (every upsert is
33
+ # idempotent and ordinal-guarded — rerunning an already-done chunk
34
+ # from id 1 changes nothing) but wastes real work on a large
35
+ # table; resumability is what keeps a crash near the END of a
36
+ # large backfill cheap to recover from instead of starting over.
37
+ #
38
+ # THE LOCK KEY PREFIX is `hecks_field_cache:` — deliberately
39
+ # disjoint from the three families already in use elsewhere in
40
+ # this adapter (`hecks_ordinal:`, `hecks_eras:`,
41
+ # `hecks_head_snapshot:` — see lineage.rb/head_compiler.rb/
42
+ # mint_transaction.rb/tail_merge.rb) so a backfill chunk NEVER
43
+ # contends with a plain write, a mint, or a snapshot-table's own
44
+ # first-creation lock. It is held for exactly ONE CHUNK's own
45
+ # transaction, never across the whole backfill — two concurrent
46
+ # backfillers of the SAME target simply take turns one chunk at a
47
+ # time rather than racing to duplicate work; neither blocks an
48
+ # unrelated reader or writer for even an instant.
49
+ module ResumableBackfill
50
+ CHUNK_SIZE = 5_000
51
+
52
+ # Idempotent, unguarded — same idiom as every other DDL helper
53
+ # in this file tree (`ensure_head_snapshot!` et al.): cheap,
54
+ # runs on every boot, only ever does real work once.
55
+ def ensure_backfill_progress_table!
56
+ @db.exec(<<~SQL)
57
+ CREATE TABLE IF NOT EXISTS hecks_backfill_progress (
58
+ target text PRIMARY KEY,
59
+ cursor text,
60
+ completed boolean NOT NULL DEFAULT false,
61
+ updated_at timestamptz NOT NULL DEFAULT now()
62
+ )
63
+ SQL
64
+ end
65
+
66
+ # Drives `target` (an already-created, currently-empty-or-
67
+ # partially-filled table) through chunks until a source read
68
+ # comes back short of `CHUNK_SIZE` rows. Two distinct callables,
69
+ # not one — a head-snapshot row and a field-cache row carry
70
+ # different columns (`state` jsonb vs. a single extracted
71
+ # `value`), so there is no one generic "upsert this row" shape
72
+ # to share; only the LOOP, the lock, and the cursor are generic.
73
+ #
74
+ # source_sql.call(cursor) — given the last-processed id (nil
75
+ # before the first chunk), returns a SQL SELECT whose result
76
+ # has an `id` column (text, ordered ascending) plus whatever
77
+ # other columns `upsert` below needs. Must read `id >
78
+ # cursor` (or unconditional when cursor is nil), `ORDER BY
79
+ # id`, `LIMIT CHUNK_SIZE` — the caller owns the actual
80
+ # column list/source tables; this method only owns the loop,
81
+ # the lock, and the cursor.
82
+ #
83
+ # upsert.call(rows) — given the PG::Result of one chunk's
84
+ # read, performs the actual guarded upsert into `target` and
85
+ # returns nothing meaningful; runs INSIDE the same
86
+ # transaction/advisory-lock scope as the cursor update below,
87
+ # so a crash between "wrote the chunk" and "advanced the
88
+ # cursor" is impossible — they commit together or not at
89
+ # all.
90
+ def chunked_backfill!(target, source_sql:, upsert:)
91
+ ensure_backfill_progress_table!
92
+ loop do
93
+ done = run_chunk!(target, source_sql: source_sql, upsert: upsert)
94
+ break if done
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ # ONE CHUNK, ONE TRANSACTION, ONE SHORT-HELD LOCK. Re-reads
101
+ # progress AFTER acquiring the lock (not just before) — a second
102
+ # concurrent booter may have already finished this exact chunk
103
+ # (or the whole backfill) while this process was waiting for the
104
+ # lock; without the re-read, it would redundantly reprocess a
105
+ # chunk another process just committed. Harmless either way
106
+ # (idempotent, ordinal-guarded) but the re-read is what keeps
107
+ # two concurrent boots from both doing the full scan instead of
108
+ # splitting it.
109
+ def run_chunk!(target, source_sql:, upsert:)
110
+ progress = backfill_progress(target)
111
+ return true if progress[:completed]
112
+
113
+ completed = false
114
+ nested_transaction("hecks_backfill_chunk") do
115
+ @db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_field_cache:' || $1))", [target])
116
+ progress = backfill_progress(target)
117
+ if progress[:completed]
118
+ completed = true
119
+ next
120
+ end
121
+
122
+ rows = @db.exec(source_sql.call(progress[:cursor]))
123
+ if rows.ntuples.zero?
124
+ upsert_backfill_progress!(target, cursor: progress[:cursor], completed: true)
125
+ completed = true
126
+ next
127
+ end
128
+
129
+ upsert.call(rows)
130
+ completed = rows.ntuples < CHUNK_SIZE
131
+ # `PG::Result#[]` supports neither an out-of-range index NOR
132
+ # a negative one (unlike a plain Ruby Array) — same trap as
133
+ # `backfill_progress` above, the explicit last-index form.
134
+ last_cursor = rows[rows.ntuples - 1]["id"]
135
+ upsert_backfill_progress!(target, cursor: last_cursor, completed: completed)
136
+ end
137
+ completed
138
+ end
139
+
140
+ # `PG::Result#[]` RAISES IndexError on an out-of-range index —
141
+ # unlike a plain Ruby Array, it does not return nil — so the
142
+ # ENTIRELY ORDINARY case of "no progress row exists yet" (every
143
+ # target's very first check) cannot be read via a bare `[0]`.
144
+ # `ntuples.zero?` first, always.
145
+ def backfill_progress(target)
146
+ result = @db.exec_params(
147
+ "SELECT cursor, completed FROM hecks_backfill_progress WHERE target = $1", [target]
148
+ )
149
+ return { cursor: nil, completed: false } if result.ntuples.zero?
150
+
151
+ row = result[0]
152
+ { cursor: row["cursor"], completed: row["completed"] == "t" }
153
+ end
154
+
155
+ def upsert_backfill_progress!(target, cursor:, completed:)
156
+ @db.exec_params(
157
+ "INSERT INTO hecks_backfill_progress (target, cursor, completed, updated_at) " \
158
+ "VALUES ($1, $2, $3, now()) " \
159
+ "ON CONFLICT (target) DO UPDATE SET cursor = EXCLUDED.cursor, " \
160
+ "completed = EXCLUDED.completed, updated_at = EXCLUDED.updated_at",
161
+ [target, cursor, completed]
162
+ )
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end