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,163 @@
1
+ require_relative "../../../../runtime/registry"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class PostgresEra
6
+ class Lineage
7
+ module TailMerge
8
+ # ── fork observability ─────────────────────────────────────────
9
+
10
+ # Post-cut writes an old era made after the newer era was minted
11
+ # — the divergence between the worlds, observable at any time.
12
+ def diverged_count(old_era)
13
+ cut = eras.find { |era| era[:ordinal] == old_era + 1 }&.dig(:watermark)
14
+ return 0 unless cut
15
+
16
+ @db.exec(
17
+ "SELECT count(*) FROM #{quoted_journal} WHERE era = #{old_era.to_i} AND ordinal > #{cut.to_i}"
18
+ )[0]["count"].to_i
19
+ end
20
+
21
+ # ── tail-merge ─────────────────────────────────────────────────
22
+ #
23
+ # The one deliberate command — it marks a business event (an app
24
+ # retiring), never a shape change. One transaction: advance the
25
+ # watermarks, rebuild the head so the tail interleaves by its
26
+ # recorded global ordinal, append the declared winners, audit —
27
+ # and roll the whole thing back on any refusal. Records touched
28
+ # by both worlds since the cut refuse until each has an explicit
29
+ # winner; resolution itself is append-only (the winner's state
30
+ # re-enters as the newest row and wins structurally — originals
31
+ # stay immutable).
32
+ def merge_tail!(aggregates:, edges:, winners: {}, audit: nil)
33
+ @db.exec("BEGIN")
34
+ @db.exec("SET LOCAL lock_timeout = '10s'")
35
+ @db.exec("SELECT pg_advisory_xact_lock(hashtext('hecks_eras:' || #{text_literal(@domain)}))")
36
+ held = eras
37
+ era = held.last[:ordinal]
38
+ label = held.last[:label]
39
+ if era == 1
40
+ @db.exec("ROLLBACK")
41
+ raise Runtime::WiringError, "nothing to merge — #{@domain} stands at era 1"
42
+ end
43
+ cut = held.last[:watermark].to_i
44
+ tip = last_ordinal
45
+
46
+ conflicts = aggregates.flat_map { |aggregate| conflict_ids(aggregate, edges, era, cut) }
47
+ unresolved = conflicts.reject { |_, id| winners.key?(id) }
48
+ unless unresolved.empty?
49
+ @db.exec("ROLLBACK")
50
+ raise Runtime::WiringError,
51
+ "cannot merge the tail of #{@domain}: touched by both worlds since the cut — " \
52
+ "#{unresolved.map { |storage, id| "#{storage}##{id}" }.sort.join(', ')}. " \
53
+ "Name each winner (--winner <id>=old or --winner <id>=new), then run bin/merge_tail again. " \
54
+ "A winner takes the WHOLE record — the aggregate is the consistency boundary, so the " \
55
+ "loser's edits are discarded even where they touched different attributes"
56
+ end
57
+
58
+ # the new world's pre-merge head states, captured before the
59
+ # rebuild lets the tail interleave
60
+ new_states = {}
61
+ aggregates.each do |aggregate|
62
+ winners.select { |_, side| side == "new" }.each_key do |id|
63
+ row = @db.exec_params("SELECT state FROM #{quote(head_view(aggregate.storage_name))} WHERE id = $1", [id])
64
+ new_states[id] = [aggregate.storage_name, row[0]["state"]] if row.ntuples.positive?
65
+ end
66
+ end
67
+
68
+ @db.exec_params("UPDATE hecks_eras SET watermark = $2 WHERE domain = $1 AND ordinal > 1", [@domain, tip])
69
+ aggregates.each do |aggregate|
70
+ @db.exec("DROP VIEW IF EXISTS #{quote(head_view(aggregate.storage_name))}")
71
+ @db.exec("DROP MATERIALIZED VIEW IF EXISTS #{quote(matview(aggregate.storage_name, era, label))}")
72
+ # full: the watermarks just moved — every ancestor matview's
73
+ # cut is stale, so there is nothing safe to layer on.
74
+ compile_head!(aggregate, era, label, edges, full: true)
75
+ end
76
+
77
+ winners.each do |id, side|
78
+ aggregates.each do |aggregate|
79
+ state =
80
+ if side == "old"
81
+ row = @db.exec_params(
82
+ "SELECT state FROM #{quote(matview(aggregate.storage_name, era, label))} " \
83
+ "WHERE aggregate_id = $1 AND operation = 'save' ORDER BY ordinal DESC LIMIT 1",
84
+ [id]
85
+ )
86
+ row.ntuples.positive? ? row[0]["state"] : nil
87
+ else
88
+ new_states[id]&.first == aggregate.storage_name ? new_states[id][1] : nil
89
+ end
90
+ next unless state
91
+
92
+ # Same two-step append does for a live write — journal
93
+ # first, snapshot second — because this INSERT bypasses
94
+ # PostgresEra#append entirely (it writes through Lineage
95
+ # directly). Skipping the snapshot half here would mean a
96
+ # merge winner lands in the journal but head_view — which
97
+ # reads this era's live rows from the snapshot table, not
98
+ # by re-scanning the journal — never shows it.
99
+ ordinal = @db.exec_params(
100
+ "INSERT INTO #{quoted_journal} (era, aggregate, aggregate_id, operation, state) " \
101
+ "VALUES ($1, $2, $3, 'save', $4) RETURNING ordinal",
102
+ [era, aggregate.storage_name, id, state]
103
+ )[0]["ordinal"]
104
+ @db.exec_params(
105
+ "INSERT INTO #{quote(head_snapshot(aggregate.storage_name, era))} (id, ordinal, state) VALUES ($1, $2, $3) " \
106
+ "ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, state = EXCLUDED.state " \
107
+ "WHERE #{quote(head_snapshot(aggregate.storage_name, era))}.ordinal < EXCLUDED.ordinal",
108
+ [id, ordinal, state]
109
+ )
110
+ end
111
+ end
112
+
113
+ if audit
114
+ violations = audit.call
115
+ unless violations.empty?
116
+ @db.exec("ROLLBACK")
117
+ raise Runtime::WiringError,
118
+ "cannot merge the tail of #{@domain}: the audit refused —\n - #{violations.join("\n - ")}"
119
+ end
120
+ end
121
+
122
+ @db.exec("COMMIT")
123
+ true
124
+ rescue PG::LockNotAvailable
125
+ @db.exec("ROLLBACK") rescue nil
126
+ raise Runtime::WiringError,
127
+ "cannot merge the tail of #{@domain}: another mint or merge holds the domain lock — " \
128
+ "waited 10s; try again shortly"
129
+ rescue PG::Error => error
130
+ @db.exec("ROLLBACK") rescue nil
131
+ raise Runtime::WiringError, "cannot merge the tail of #{@domain}: #{error.message.strip}"
132
+ end
133
+
134
+ # Ids touched by BOTH worlds since the cut — the old world's
135
+ # post-cut tail INTERSECTed with the new world's own writes.
136
+ #
137
+ # KNOWN GAP, not silently risked: this compares raw
138
+ # `aggregate_id` values, with no notion of "these two different
139
+ # ids are the same entity, rekeyed." If a domain's history
140
+ # includes a rekey (see TranslationRekey) and is LATER
141
+ # merged here, a record's pre-rekey and post-rekey rows will
142
+ # never intersect — they just silently survive as two separate,
143
+ # unrelated-looking heads (a duplicate, not corruption: nothing
144
+ # here deletes or clobbers either side). Resolve any such
145
+ # duplicate manually after a merge; teaching this INTERSECT
146
+ # about a rekey mapping is real, separate work, deliberately
147
+ # out of scope for rekey's first pass.
148
+ def conflict_ids(aggregate, edges, era, cut)
149
+ names = names_by_era(aggregate, edges)
150
+ olds = (1...era).map { |ancestor| text_literal(names[:storage][ancestor - 1]) }.join(", ")
151
+ @db.exec(<<~SQL).map { |row| [aggregate.storage_name, row["aggregate_id"]] }
152
+ SELECT aggregate_id FROM #{quoted_journal}
153
+ WHERE era < #{era.to_i} AND aggregate IN (#{olds}) AND ordinal > #{cut.to_i}
154
+ INTERSECT
155
+ SELECT aggregate_id FROM #{quoted_journal}
156
+ WHERE era = #{era.to_i} AND aggregate = #{text_literal(names[:storage][era - 1])}
157
+ SQL
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,114 @@
1
+ module Hecks
2
+ module Adapters
3
+ class PostgresEra
4
+ class Lineage
5
+ module TransformInstaller
6
+ # The jsonb rule transforms — installed once, idempotently. Kept
7
+ # equal to the port's reference entry-JSON transform by the
8
+ # cross-execution equivalence spec; the SQL here is a compilation
9
+ # target, not a second source of truth.
10
+ def install_transforms!
11
+ @db.exec(<<~SQL)
12
+ CREATE OR REPLACE FUNCTION hecks_tr_extract(state jsonb, path text[], OUT remaining jsonb, OUT value jsonb, OUT present boolean)
13
+ LANGUAGE plpgsql IMMUTABLE AS $fn$
14
+ DECLARE parent jsonb; leaf text;
15
+ BEGIN
16
+ remaining := state;
17
+ present := false;
18
+ leaf := path[array_upper(path, 1)];
19
+ IF array_length(path, 1) = 1 THEN
20
+ IF state ? leaf THEN
21
+ present := true;
22
+ value := state -> leaf;
23
+ remaining := state - leaf;
24
+ END IF;
25
+ RETURN;
26
+ END IF;
27
+ parent := state #> path[1:array_upper(path, 1) - 1];
28
+ IF jsonb_typeof(parent) = 'object' AND parent ? leaf THEN
29
+ present := true;
30
+ value := parent -> leaf;
31
+ parent := parent - leaf;
32
+ IF parent = '{}'::jsonb THEN
33
+ remaining := state - path[1];
34
+ ELSE
35
+ remaining := jsonb_set(state, path[1:array_upper(path, 1) - 1], parent);
36
+ END IF;
37
+ END IF;
38
+ END $fn$
39
+ SQL
40
+ # ADVERSARIAL FINDING: a destination whose top segment already
41
+ # holds a value — most commonly a reference, a bare scalar id
42
+ # — used to be silently overwritten with an empty object the
43
+ # moment a dotted destination needed to nest under it. That is
44
+ # a drop that never declared itself, the one thing this
45
+ # language exists to make explicit (see hecks_tr_convert's own
46
+ # refusal below, the same shape) — refused here instead, with
47
+ # the Ruby reference transform (ports/persistence/lineage.rb's
48
+ # `insert`) raising the identical wording.
49
+ @db.exec(<<~SQL)
50
+ CREATE OR REPLACE FUNCTION hecks_tr_insert(state jsonb, path text[], value jsonb, rule_label text) RETURNS jsonb
51
+ LANGUAGE plpgsql IMMUTABLE AS $fn$
52
+ BEGIN
53
+ IF array_length(path, 1) = 1 THEN
54
+ RETURN state || jsonb_build_object(path[1], value);
55
+ END IF;
56
+ IF state ? path[1] AND jsonb_typeof(state -> path[1]) <> 'object' THEN
57
+ RAISE EXCEPTION 'cannot %: % already holds %, not a value this can nest under — moving into it would discard that value silently. Rename or drop % first.',
58
+ rule_label, path[1], state -> path[1], path[1];
59
+ END IF;
60
+ IF state -> path[1] IS NULL THEN
61
+ state := state || jsonb_build_object(path[1], '{}'::jsonb);
62
+ END IF;
63
+ RETURN jsonb_set(state, path, value);
64
+ END $fn$
65
+ SQL
66
+ @db.exec(<<~SQL)
67
+ CREATE OR REPLACE FUNCTION hecks_tr_rename(state jsonb, old_name text, new_name text) RETURNS jsonb
68
+ LANGUAGE sql IMMUTABLE AS $fn$
69
+ SELECT CASE WHEN state ? old_name
70
+ THEN (state - old_name) || jsonb_build_object(new_name, state -> old_name)
71
+ ELSE state END
72
+ $fn$
73
+ SQL
74
+ @db.exec(<<~SQL)
75
+ CREATE OR REPLACE FUNCTION hecks_tr_move(state jsonb, from_path text[], to_path text[], rule_label text) RETURNS jsonb
76
+ LANGUAGE plpgsql IMMUTABLE AS $fn$
77
+ DECLARE extracted record;
78
+ BEGIN
79
+ SELECT * INTO extracted FROM hecks_tr_extract(state, from_path);
80
+ IF NOT extracted.present THEN RETURN state; END IF;
81
+ RETURN hecks_tr_insert(extracted.remaining, to_path, extracted.value, rule_label);
82
+ END $fn$
83
+ SQL
84
+ @db.exec(<<~SQL)
85
+ CREATE OR REPLACE FUNCTION hecks_tr_convert(state jsonb, from_path text[], to_path text[], pairs jsonb, from_label text, rule_label text) RETURNS jsonb
86
+ LANGUAGE plpgsql IMMUTABLE AS $fn$
87
+ DECLARE extracted record; pair jsonb;
88
+ BEGIN
89
+ SELECT * INTO extracted FROM hecks_tr_extract(state, from_path);
90
+ IF NOT extracted.present THEN RETURN state; END IF;
91
+ FOR pair IN SELECT * FROM jsonb_array_elements(pairs) LOOP
92
+ IF pair -> 0 = extracted.value THEN
93
+ RETURN hecks_tr_insert(extracted.remaining, to_path, pair -> 1, rule_label);
94
+ END IF;
95
+ END LOOP;
96
+ RAISE EXCEPTION 'cannot translate %: % has no mapping in its convert''s values: table. Add % => ... to cover it.',
97
+ from_label, extracted.value, extracted.value;
98
+ END $fn$
99
+ SQL
100
+ @db.exec(<<~SQL)
101
+ CREATE OR REPLACE FUNCTION hecks_tr_drop(state jsonb, path text[]) RETURNS jsonb
102
+ LANGUAGE plpgsql IMMUTABLE AS $fn$
103
+ DECLARE extracted record;
104
+ BEGIN
105
+ SELECT * INTO extracted FROM hecks_tr_extract(state, path);
106
+ RETURN extracted.remaining;
107
+ END $fn$
108
+ SQL
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,137 @@
1
+ require "json"
2
+ require "digest"
3
+
4
+ require_relative "lineage/provisioning"
5
+ require_relative "lineage/era_store"
6
+ require_relative "lineage/mint_transaction"
7
+ require_relative "lineage/tail_merge"
8
+ require_relative "lineage/resumable_backfill"
9
+ require_relative "lineage/head_compiler"
10
+ require_relative "lineage/field_cache"
11
+ require_relative "lineage/transform_installer"
12
+ require_relative "../../../naming"
13
+
14
+ module Hecks
15
+ module Adapters
16
+ class PostgresEra
17
+ # The lineage topology inside one Postgres database: one journal
18
+ # per domain, LIST-partitioned by era, with a single ordinal
19
+ # sequence spanning partitions (total order across eras is
20
+ # structural); a `hecks_eras` table holding each era's frozen
21
+ # source text, its once-minted hash/label, and the watermark cut
22
+ # into its ancestor; and, per aggregate, a HEAD derived from the
23
+ # journal — never a table anything rewrites.
24
+ #
25
+ # For era 1 the head is a plain view (latest save per id). From
26
+ # era 2 on, the ancestor tail is a MATERIALIZED view whose
27
+ # definition is the compiled, chained edge sequence — one CTE per
28
+ # original edge, in mint order, never a flattened merged rule set
29
+ # — with the watermark baked into the definition, so post-cut
30
+ # old-era writes cannot leak into the new head even on REFRESH.
31
+ # Live current-era writes overlay it through the head view.
32
+ #
33
+ # Writing to a superseded schema drops the instant the new one
34
+ # materializes: ONE shared row policy admits INSERTs to whichever
35
+ # era was just established (era 1 at first hold, era N at mint),
36
+ # and advancing it is part of the SAME transaction that builds the
37
+ # new era's matview. There is no persisted per-role fork — any
38
+ # granted role, app or the table's own OWNER (FORCE ROW LEVEL
39
+ # SECURITY applies this to the owner too, not only ordinary
40
+ # roles), writes the current era or nothing, from the moment that
41
+ # transaction commits. A stale-era write during the narrow window
42
+ # before that commit — RLS is checked once, when the statement
43
+ # EXECUTES, never re-checked at commit, so a transaction that
44
+ # inserted while the old era was still current can still land
45
+ # after a concurrent mint has already moved the fence on — is
46
+ # exactly what diverged_count/merge_tail exist to reconcile; it is
47
+ # the residual of an unavoidable race (ordinals are
48
+ # sequence-assigned, not transactional — see below), not a
49
+ # supported way to keep operating two schemas side by side. Only
50
+ # an actual Postgres superuser (or a role granted BYPASSRLS)
51
+ # sits above FORCE and keeps writing at will, forever.
52
+ #
53
+ # Lineage order is ordinal-assignment order: the ordinal comes from a
54
+ # sequence, and a sequence's `nextval()` is never rolled back with its
55
+ # transaction — accepted and documented rather than papered over.
56
+ #
57
+ # ONE PART OF THAT IS CLOSED. Two concurrent PLAIN writes could call
58
+ # `nextval()` in one order and COMMIT in the other — nothing about a
59
+ # single autocommit INSERT statement stops a slower one from finishing
60
+ # after a faster one that started later — so "ordinal order" and
61
+ # "commit order" were formally two different total orders even with no
62
+ # mint anywhere near either write. `PostgresEra#append` now holds
63
+ # `pg_advisory_xact_lock(hashtext('hecks_ordinal:' || domain))` for the
64
+ # length of its own transaction, a DIFFERENT key from `mint_era!` and
65
+ # `merge_tail!`'s `hecks_eras:domain` — so plain writes serialize
66
+ # against EACH OTHER only, never against a mint, and ordinal order
67
+ # equals commit order for them now.
68
+ #
69
+ # THE OTHER PART IS NOT, ON PURPOSE. A stale-era write during the
70
+ # narrow window before a mint's fence-move commits is the SAME race by
71
+ # a different name — and closing it would mean a plain write
72
+ # serializing against a mint, which is exactly the guarantee
73
+ # `postgres_lineage_spec.rb`'s "an old checkout keeps writing its own
74
+ # era THROUGH a mint" pins the ABSENCE of. That race stays the
75
+ # residual `diverged_count`/`merge_tail!` exist to reconcile, not
76
+ # something a plain write should ever block for.
77
+ #
78
+ # One concern per file under lineage/: provisioning (DDL and the
79
+ # RLS posture), era_store (the hecks_eras rows and their integrity),
80
+ # mint_transaction (the one transaction that makes an era real),
81
+ # tail_merge (the one deliberate merge command), resumable_backfill
82
+ # (the one chunked/lock-free/resumable scan loop, shared by
83
+ # head_compiler's own backfill and field_cache's), head_compiler
84
+ # (the chained-edge SQL a head derives through), field_cache (the
85
+ # per-where-field read cache that lets a query skip the reduction
86
+ # entirely), transform_installer (the hecks_tr_* jsonb helpers).
87
+ class Lineage
88
+ include Provisioning
89
+ include EraStore
90
+ include MintTransaction
91
+ include TailMerge
92
+ include ResumableBackfill
93
+ include HeadCompiler
94
+ include FieldCache
95
+ include TransformInstaller
96
+
97
+ JOURNAL_COLUMNS = "ordinal, era, aggregate, aggregate_id, operation, state, mirrors".freeze
98
+
99
+ attr_reader :db, :domain, :formerly_known_as
100
+
101
+ def initialize(db, domain, formerly_known_as: nil)
102
+ @db = db
103
+ @domain = domain.to_s
104
+ @formerly_known_as = formerly_known_as&.to_s
105
+ end
106
+
107
+ def journal = "hecks_journal_#{Naming.snake(@domain)}"
108
+ def quoted_journal = quote(journal)
109
+ def sequence = "#{journal}_ordinal"
110
+ def partition(era) = "#{journal}_era_#{era}"
111
+ def head_view(storage_name) = "#{storage_name}_head"
112
+ # The transactionally-upserted read cache behind head_view — one row
113
+ # per LIVE id, keyed by id, carrying the ordinal it was last written
114
+ # at. Scoped by ERA, not just storage_name — an aggregate that
115
+ # ISN'T renamed across a mint keeps the SAME storage_name in both
116
+ # eras, so storage_name alone would have era N+1 sharing one
117
+ # physical table with era N: a freshly-minted era would inherit
118
+ # every pre-mint (and, worse, pre-rekey/pre-translation) row
119
+ # instead of starting empty. era-qualified naming is what
120
+ # `partition`/`matview` already do for exactly this reason.
121
+ def head_snapshot(storage_name, era) = "#{storage_name}_head_snapshot_#{era}"
122
+ def matview(storage_name, era, label) = "#{storage_name}_lineage_#{era}_#{label}"
123
+
124
+ private
125
+
126
+ def quote(name) = PG::Connection.quote_ident(name.to_s)
127
+
128
+ def text_literal(text) = "'#{text.to_s.gsub("'", "''")}'"
129
+
130
+ def path_literal(path)
131
+ segments = path.to_s.split(".").map { |segment| text_literal(segment) }
132
+ "ARRAY[#{segments.join(', ')}]::text[]"
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,89 @@
1
+ require_relative "../../../../ports/persistence/lineage"
2
+ require_relative "../../../../runtime/era_guard"
3
+ require_relative "../../../../runtime/identity"
4
+ require_relative "../../../../runtime/registry"
5
+ require_relative "../../../../translation/audit"
6
+
7
+ module Hecks
8
+ module Adapters
9
+ class PostgresEra
10
+ module LineageManager
11
+ # What a mint must prove before it may happen: the edge covers
12
+ # the whole diff, no identity path was re-keyed, and the audit's
13
+ # first two layers pass over the LIVE compiled chain.
14
+ module CoverageCheck
15
+ # Layer 1 against THIS edge specifically: every vanished or
16
+ # retyped path in the held→current diff must be explained, and
17
+ # every held aggregate must still be claimed. The refusal is
18
+ # EraGuard's own, byte for byte.
19
+ def check_coverage!(registry, bluebook, held_bluebook, edge)
20
+ bluebook.aggregates.each do |aggregate|
21
+ rules = Ports::Persistence::Lineage.from_declared(edge.for_aggregate(aggregate.name), aggregate.name)
22
+ held_aggregate = held_bluebook.aggregate(rules&.ancestor_name || aggregate.name)
23
+ next unless held_aggregate
24
+
25
+ check_identity_unchanged!(bluebook, aggregate, held_aggregate, rules)
26
+ uncovered = Runtime::EraGuard.uncovered_attributes(aggregate, held_aggregate, rules)
27
+ Runtime::EraGuard.refuse_uncovered!(bluebook, aggregate, uncovered) unless uncovered.empty?
28
+
29
+ unsafe = Runtime::EraGuard.unsafe_additions(aggregate, held_aggregate, rules)
30
+ Runtime::EraGuard.refuse_unsafe_addition!(bluebook, aggregate, unsafe) unless unsafe.empty?
31
+ end
32
+ Runtime::EraGuard.check_vanished_aggregates!(registry, bluebook, held_bluebook)
33
+ end
34
+
35
+ # An identity-path change is a RE-KEYING, not an ordinary
36
+ # translation — stored ids were fixed at write time under the old
37
+ # key, so this refuses UNLESS the edge declares a `rekey` for
38
+ # this aggregate covering exactly that. `rules.rekey?` (see
39
+ # `Ports::Persistence::Lineage`'s own comment) is the single
40
+ # source of truth every consumer of this fact asks — this is not
41
+ # a second, independent check of `declared.rekeys`.
42
+ def check_identity_unchanged!(bluebook, aggregate, held_aggregate, rules)
43
+ # The FULL declared path lists, in declaration order — never the
44
+ # single-head shortcut, which is nil for every composite identity
45
+ # and so would let two different composites compare as unchanged.
46
+ return if held_aggregate.identity_paths == aggregate.identity_paths
47
+ return if rules&.rekey?
48
+
49
+ held_identity = Runtime::Identity.reading(held_aggregate)
50
+ current_identity = Runtime::Identity.reading(aggregate)
51
+ raise Runtime::WiringError,
52
+ "cannot mint an era for #{bluebook.name}::#{aggregate.name}: its identity path changed " \
53
+ "(#{held_identity} → #{current_identity}), and that is a re-keying, not a translation — " \
54
+ "stored ids were minted under #{held_identity}, and no rule declares rows the same " \
55
+ "entity under a new key. Keep the identity path, declare a rekey rule, or migrate the " \
56
+ "data explicitly"
57
+ end
58
+
59
+ # Layers 1 and 2 of the audit, over the LIVE compiled chain —
60
+ # before anything is minted, so a refusal leaves no half-born
61
+ # era. (A convert meeting an unmapped value raises inside the
62
+ # preview query itself: same rollback-shaped outcome.)
63
+ def audit!(bluebook, lineage, chain, ordinal, edge)
64
+ violations = []
65
+ bluebook.aggregates.each do |aggregate|
66
+ declared = edge.for_aggregate(aggregate.name)
67
+ after = begin
68
+ lineage.translated_latest(aggregate, ordinal, chain)
69
+ rescue PG::Error => error
70
+ raise Runtime::WiringError, "cannot mint era #{ordinal} of #{bluebook.name}: #{error.message.strip}"
71
+ end
72
+ before = if chain.size > 1
73
+ lineage.translated_latest(aggregate, ordinal, chain[0..-2])
74
+ else
75
+ lineage.ancestor_latest(aggregate, ordinal, chain)
76
+ end
77
+ verdict = Translation::Audit.check(aggregate: aggregate, declared: declared, before: before, after: after)
78
+ violations.concat(verdict.violations)
79
+ end
80
+ return if violations.empty?
81
+
82
+ raise Runtime::WiringError,
83
+ "cannot mint era #{ordinal} of #{bluebook.name}: the audit refused —\n - #{violations.join("\n - ")}"
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,82 @@
1
+ require_relative "../lineage"
2
+ require_relative "../../../../runtime/storage_shape"
3
+
4
+ module Hecks
5
+ module Adapters
6
+ class PostgresEra
7
+ module LineageManager
8
+ # The boot-time resolution: which era IS this checkout? First
9
+ # boot holds era 1; a quiet reboot changes nothing; a
10
+ # held-but-superseded shape boots read-only-toward-the-fence; an
11
+ # unheld shape goes to the minter.
12
+ module EraResolver
13
+ def check!(registry:, bluebook:, current_text:, settings:, directory: nil)
14
+ db = PostgresEra.connect_for(bluebook.name, settings)
15
+ lineage = Lineage.new(db, bluebook.name, formerly_known_as: bluebook.formerly_known_as)
16
+ lineage.ensure_base!
17
+ role = settings[:role] || settings["role"]
18
+
19
+ held = lineage.eras
20
+ if held.empty?
21
+ lineage.hold_first!(current_text, projection: Runtime::StorageShape.project(bluebook))
22
+ bluebook.aggregates.each { |aggregate| lineage.ensure_first_head!(aggregate.storage_name) }
23
+ # hold_first! already established era 1 as current for
24
+ # EVERY role; this one just needs its own privileges.
25
+ lineage.grant_role!(role, aggregates: bluebook.aggregates, era: 1) if role
26
+ return
27
+ end
28
+
29
+ current_shape = Runtime::StorageShape.project(bluebook)
30
+ shapes = held.map { |era| [era, Runtime::StorageShape.project(shadow(era[:held_text]))] }
31
+ if bluebook.formerly_known_as
32
+ # A held era's shape is re-derived from its own frozen
33
+ # historical text (shadow(era[:held_text])), so its "name"
34
+ # is permanently the old one — the frozen text is authentic
35
+ # record ("this domain really was called that, at the
36
+ # time") and must never be rewritten. Only the in-memory
37
+ # shape used for THIS comparison is normalized, and only
38
+ # where it exactly matches the declared old name, so an
39
+ # unrelated domain that happens to shape-match some other
40
+ # domain's history still shows up as a real mismatch.
41
+ shapes = shapes.map do |era, shape|
42
+ [era, shape["name"] == bluebook.formerly_known_as ? shape.merge("name" => bluebook.name) : shape]
43
+ end
44
+ end
45
+
46
+ latest, latest_shape = shapes.last
47
+ if latest_shape == current_shape
48
+ bluebook.aggregates.each { |aggregate| lineage.ensure_first_head!(aggregate.storage_name) } if latest[:ordinal] == 1
49
+ # A quiet reboot changes no era, so there is nothing to
50
+ # advance — only this role's own privileges, if it is new.
51
+ lineage.grant_role!(role, aggregates: bluebook.aggregates, era: latest[:ordinal]) if role
52
+ registry.resolved_eras[bluebook.name] = latest[:ordinal]
53
+ return
54
+ end
55
+
56
+ matched, = shapes.find { |_, shape| shape == current_shape }
57
+ if matched
58
+ # A held-but-superseded era — an old checkout still running.
59
+ # It may keep BOOTING and READING (PostgresEra is the one
60
+ # adapter that recognizes this rather than refusing), but it
61
+ # may not keep WRITING: the shared era fence was already
62
+ # advanced past this ordinal by whichever mint superseded
63
+ # it, and nothing here may roll that back. Granting only
64
+ # this role's privileges, never advance_era!, is what keeps
65
+ # that true — see advance_era!'s own warning against being
66
+ # called with a superseded ordinal.
67
+ lineage.grant_role!(role, aggregates: bluebook.aggregates, era: matched[:ordinal]) if role
68
+ registry.resolved_eras[bluebook.name] = matched[:ordinal]
69
+ return
70
+ end
71
+
72
+ registry.resolved_eras[bluebook.name] =
73
+ mint!(registry, bluebook, current_text, lineage, latest,
74
+ role: role, directory: directory)
75
+ ensure
76
+ db&.close
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,43 @@
1
+ require_relative "../lineage"
2
+ require_relative "../../../../runtime/registry"
3
+ require_relative "../../../../translation/audit"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ class PostgresEra
8
+ module LineageManager
9
+ # Tail-merge, driven from bin/merge_tail: interleave the old
10
+ # world's post-cut writes into the head by their recorded global
11
+ # ordinals, under the full audit, in one transaction.
12
+ module MergeCoordinator
13
+ def merge!(registry:, bluebook:, settings:, winners: {})
14
+ db = PostgresEra.connect_for(bluebook.name, settings)
15
+ lineage = Lineage.new(db, bluebook.name, formerly_known_as: bluebook.formerly_known_as)
16
+ lineage.ensure_base!
17
+
18
+ held = lineage.eras
19
+ raise Runtime::WiringError, "nothing to merge — #{bluebook.name} stands at era 1" if held.size < 2
20
+
21
+ latest = held.last
22
+ chain = edge_chain(registry, bluebook, held[0..-2], latest[:label])
23
+
24
+ audit = lambda do
25
+ violations = []
26
+ bluebook.aggregates.each do |aggregate|
27
+ rows = db.exec("SELECT id, state FROM #{PG::Connection.quote_ident(lineage.head_view(aggregate.storage_name))}")
28
+ .to_h { |row| [row["id"], JSON.parse(row["state"])] }
29
+ verdict = Translation::Audit.check(aggregate: aggregate, declared: nil, before: rows, after: rows)
30
+ violations.concat(verdict.violations)
31
+ end
32
+ violations
33
+ end
34
+
35
+ lineage.merge_tail!(aggregates: bluebook.aggregates, edges: chain, winners: winners, audit: audit)
36
+ ensure
37
+ db&.close
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end