hecks 0.2.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. checksums.yaml +5 -5
  2. data/lib/hecks/adapters/driven/claude_code.adapter +3 -0
  3. data/lib/hecks/adapters/driven/claude_code.rb +127 -0
  4. data/lib/hecks/adapters/driven/d1.adapter +12 -0
  5. data/lib/hecks/adapters/driven/d1.rb +427 -0
  6. data/lib/hecks/adapters/driven/folder.adapter +3 -0
  7. data/lib/hecks/adapters/driven/folder.rb +199 -0
  8. data/lib/hecks/adapters/driven/google_authentication.adapter +3 -0
  9. data/lib/hecks/adapters/driven/google_authentication.rb +100 -0
  10. data/lib/hecks/adapters/driven/governance_authorization.adapter +3 -0
  11. data/lib/hecks/adapters/driven/governance_authorization.rb +101 -0
  12. data/lib/hecks/adapters/driven/heki/journal.rb +56 -0
  13. data/lib/hecks/adapters/driven/heki/saga_store.rb +99 -0
  14. data/lib/hecks/adapters/driven/heki/snapshot.rb +65 -0
  15. data/lib/hecks/adapters/driven/heki.adapter +4 -0
  16. data/lib/hecks/adapters/driven/heki.rb +179 -0
  17. data/lib/hecks/adapters/driven/identity_registry.adapter +3 -0
  18. data/lib/hecks/adapters/driven/identity_registry.rb +26 -0
  19. data/lib/hecks/adapters/driven/in_memory_ordering.rb +51 -0
  20. data/lib/hecks/adapters/driven/lambda/client.rb +63 -0
  21. data/lib/hecks/adapters/driven/lambda.adapter +4 -0
  22. data/lib/hecks/adapters/driven/lambda.rb +145 -0
  23. data/lib/hecks/adapters/driven/memory.adapter +3 -0
  24. data/lib/hecks/adapters/driven/memory.rb +103 -0
  25. data/lib/hecks/adapters/driven/mock_stripe_adapter.adapter +3 -0
  26. data/lib/hecks/adapters/driven/mock_stripe_adapter.rb +28 -0
  27. data/lib/hecks/adapters/driven/postgres/codec.rb +88 -0
  28. data/lib/hecks/adapters/driven/postgres/schema_builder.rb +207 -0
  29. data/lib/hecks/adapters/driven/postgres.adapter +5 -0
  30. data/lib/hecks/adapters/driven/postgres.rb +438 -0
  31. data/lib/hecks/adapters/driven/postgres_era.adapter +17 -0
  32. data/lib/hecks/adapters/driven/prism.adapter +3 -0
  33. data/lib/hecks/adapters/driven/prism.rb +80 -0
  34. data/lib/hecks/adapters/driven/secure_random_identity.adapter +3 -0
  35. data/lib/hecks/adapters/driven/secure_random_identity.rb +14 -0
  36. data/lib/hecks/adapters/driven/sql_query_builder.rb +221 -0
  37. data/lib/hecks/adapters/driven/sqlite/codec.rb +80 -0
  38. data/lib/hecks/adapters/driven/sqlite/projection.rb +172 -0
  39. data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +192 -0
  40. data/lib/hecks/adapters/driven/sqlite.adapter +9 -0
  41. data/lib/hecks/adapters/driven/sqlite.rb +306 -0
  42. data/lib/hecks/adapters/driven/system_clock.adapter +3 -0
  43. data/lib/hecks/adapters/driven/system_clock.rb +14 -0
  44. data/lib/hecks/adapters/driven.rb +54 -0
  45. data/lib/hecks/adapters.rb +6 -0
  46. data/lib/hecks/behaviors/dsl.rb +125 -0
  47. data/lib/hecks/behaviors/expectations.rb +340 -0
  48. data/lib/hecks/behaviors/ir.rb +31 -0
  49. data/lib/hecks/behaviors/rspec.rb +42 -0
  50. data/lib/hecks/behaviors/runner.rb +96 -0
  51. data/lib/hecks/behaviors.rb +25 -0
  52. data/lib/hecks/bluebook/aggregate.rb +108 -0
  53. data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +134 -0
  54. data/lib/hecks/bluebook/assembly/build.rb +48 -0
  55. data/lib/hecks/bluebook/assembly/contract.rb +112 -0
  56. data/lib/hecks/bluebook/assembly/contracts.rb +438 -0
  57. data/lib/hecks/bluebook/assembly/marks.rb +228 -0
  58. data/lib/hecks/bluebook/assembly/specializer.rb +70 -0
  59. data/lib/hecks/bluebook/assembly.rb +91 -0
  60. data/lib/hecks/bluebook/attribute.rb +96 -0
  61. data/lib/hecks/bluebook/behaviour/aggregate.rb +83 -0
  62. data/lib/hecks/bluebook/behaviour/attribute.rb +27 -0
  63. data/lib/hecks/bluebook/behaviour/chapter.rb +72 -0
  64. data/lib/hecks/bluebook/behaviour/command.rb +116 -0
  65. data/lib/hecks/bluebook/behaviour/domain_port.rb +25 -0
  66. data/lib/hecks/bluebook/behaviour/entity.rb +59 -0
  67. data/lib/hecks/bluebook/behaviour/hexagon.rb +50 -0
  68. data/lib/hecks/bluebook/behaviour/lifecycle.rb +68 -0
  69. data/lib/hecks/bluebook/behaviour/policy.rb +52 -0
  70. data/lib/hecks/bluebook/behaviour/process_manager.rb +51 -0
  71. data/lib/hecks/bluebook/behaviour/query.rb +10 -0
  72. data/lib/hecks/bluebook/behaviour/read_model.rb +29 -0
  73. data/lib/hecks/bluebook/behaviour/traits.rb +81 -0
  74. data/lib/hecks/bluebook/behaviour/value_object.rb +33 -0
  75. data/lib/hecks/bluebook/chapter.rb +78 -0
  76. data/lib/hecks/bluebook/command.rb +124 -0
  77. data/lib/hecks/bluebook/domain_port.rb +102 -0
  78. data/lib/hecks/bluebook/dsl/adapter_builder.rb +34 -0
  79. data/lib/hecks/bluebook/dsl/aggregate_builder.rb +1018 -0
  80. data/lib/hecks/bluebook/dsl/attribute_collector.rb +348 -0
  81. data/lib/hecks/bluebook/dsl/binding_proxy.rb +71 -0
  82. data/lib/hecks/bluebook/dsl/bluebook_builder.rb +1087 -0
  83. data/lib/hecks/bluebook/dsl/command_builder.rb +714 -0
  84. data/lib/hecks/bluebook/dsl/const_shim.rb +81 -0
  85. data/lib/hecks/bluebook/dsl/domain_port_builder.rb +79 -0
  86. data/lib/hecks/bluebook/dsl/entity_builder.rb +430 -0
  87. data/lib/hecks/bluebook/dsl/generic_dispatch.rb +366 -0
  88. data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +163 -0
  89. data/lib/hecks/bluebook/dsl/identity_declaration.rb +191 -0
  90. data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +44 -0
  91. data/lib/hecks/bluebook/dsl/malformed.rb +7 -0
  92. data/lib/hecks/bluebook/dsl/policy_builder.rb +135 -0
  93. data/lib/hecks/bluebook/dsl/port_builder.rb +39 -0
  94. data/lib/hecks/bluebook/dsl/port_operation_builder.rb +142 -0
  95. data/lib/hecks/bluebook/dsl/process_manager_builder.rb +306 -0
  96. data/lib/hecks/bluebook/dsl/query_builder.rb +113 -0
  97. data/lib/hecks/bluebook/dsl/read_model_builder.rb +233 -0
  98. data/lib/hecks/bluebook/dsl/rule_reference.rb +173 -0
  99. data/lib/hecks/bluebook/dsl/translation_builder.rb +243 -0
  100. data/lib/hecks/bluebook/dsl/value_object_builder.rb +178 -0
  101. data/lib/hecks/bluebook/dsl/word_gate.rb +228 -0
  102. data/lib/hecks/bluebook/dsl/world_builder.rb +117 -0
  103. data/lib/hecks/bluebook/dsl.rb +45 -0
  104. data/lib/hecks/bluebook/entity.rb +103 -0
  105. data/lib/hecks/bluebook/expression/canonical_form.rb +123 -0
  106. data/lib/hecks/bluebook/expression/evaluator.rb +304 -0
  107. data/lib/hecks/bluebook/expression/projection.json +218 -0
  108. data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +233 -0
  109. data/lib/hecks/bluebook/expression/resolver.rb +781 -0
  110. data/lib/hecks/bluebook/expression.rb +13 -0
  111. data/lib/hecks/bluebook/hexagon.rb +60 -0
  112. data/lib/hecks/bluebook/lifecycle.rb +42 -0
  113. data/lib/hecks/bluebook/meta_validator/adapter_judge.rb +54 -0
  114. data/lib/hecks/bluebook/meta_validator/judge.rb +621 -0
  115. data/lib/hecks/bluebook/meta_validator/plan.rb +332 -0
  116. data/lib/hecks/bluebook/meta_validator/port_judge.rb +51 -0
  117. data/lib/hecks/bluebook/meta_validator/readings.rb +360 -0
  118. data/lib/hecks/bluebook/meta_validator/reconstruction.rb +351 -0
  119. data/lib/hecks/bluebook/meta_validator/shapes.rb +266 -0
  120. data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +255 -0
  121. data/lib/hecks/bluebook/meta_validator/translation_judge.rb +138 -0
  122. data/lib/hecks/bluebook/meta_validator/world_judge.rb +78 -0
  123. data/lib/hecks/bluebook/meta_validator.rb +542 -0
  124. data/lib/hecks/bluebook/model_check.rb +445 -0
  125. data/lib/hecks/bluebook/pattern_subset.rb +184 -0
  126. data/lib/hecks/bluebook/policy.rb +41 -0
  127. data/lib/hecks/bluebook/process_manager.rb +128 -0
  128. data/lib/hecks/bluebook/project_discovery.rb +30 -0
  129. data/lib/hecks/bluebook/project_loader.rb +40 -0
  130. data/lib/hecks/bluebook/project_register.rb +82 -0
  131. data/lib/hecks/bluebook/query.rb +61 -0
  132. data/lib/hecks/bluebook/read_model.rb +109 -0
  133. data/lib/hecks/bluebook/reference.rb +74 -0
  134. data/lib/hecks/bluebook/smoke_test.rb +166 -0
  135. data/lib/hecks/bluebook/synthesizer.rb +95 -0
  136. data/lib/hecks/bluebook/translation.rb +92 -0
  137. data/lib/hecks/bluebook/value_object.rb +66 -0
  138. data/lib/hecks/bluebook.rb +74 -0
  139. data/lib/hecks/codemod.rb +342 -0
  140. data/lib/hecks/construct.rb +71 -0
  141. data/lib/hecks/deploy/bluebook/deploy.bluebook +219 -0
  142. data/lib/hecks/deploy/bluebook/deploy.hecksagon +4 -0
  143. data/lib/hecks/deploy/oidc.json +18 -0
  144. data/lib/hecks/doc/reference.rb +410 -0
  145. data/lib/hecks/embryonaut_bluebook.rb +75 -0
  146. data/lib/hecks/facade/cli_door.rb +119 -0
  147. data/lib/hecks/facade/cli_runner.rb +190 -0
  148. data/lib/hecks/facade/command_request.rb +105 -0
  149. data/lib/hecks/facade/handle.rb +173 -0
  150. data/lib/hecks/facade/json_door.rb +166 -0
  151. data/lib/hecks/facade/surface/aggregate_door.rb +185 -0
  152. data/lib/hecks/facade/surface/chapter.rb +107 -0
  153. data/lib/hecks/facade/surface.rb +48 -0
  154. data/lib/hecks/facade.rb +44 -0
  155. data/lib/hecks/forms/app.rb +341 -0
  156. data/lib/hecks/forms/command_form_renderer.rb +113 -0
  157. data/lib/hecks/forms/examples/banking_console.bluebook +3 -0
  158. data/lib/hecks/forms/field_renderer.rb +177 -0
  159. data/lib/hecks/forms/field_shape.rb +232 -0
  160. data/lib/hecks/forms/html.rb +84 -0
  161. data/lib/hecks/forms/index_renderer.rb +35 -0
  162. data/lib/hecks/forms/page.rb +157 -0
  163. data/lib/hecks/forms/params.rb +161 -0
  164. data/lib/hecks/forms/port_argument.rb +46 -0
  165. data/lib/hecks/forms/query_form_renderer.rb +114 -0
  166. data/lib/hecks/forms/record_renderer.rb +119 -0
  167. data/lib/hecks/forms/record_table.rb +68 -0
  168. data/lib/hecks/forms/reference_options.rb +30 -0
  169. data/lib/hecks/forms/value_object_shape.rb +46 -0
  170. data/lib/hecks/forms.rb +54 -0
  171. data/lib/hecks/fqn.rb +94 -0
  172. data/lib/hecks/framework/bluebook/compliance.bluebook +1 -0
  173. data/lib/hecks/framework/bluebook/console_settings.bluebook +489 -0
  174. data/lib/hecks/framework/bluebook/framework.hecksagon +32 -0
  175. data/lib/hecks/framework/bluebook/governance.bluebook +145 -0
  176. data/lib/hecks/framework/bluebook/identity.bluebook +90 -0
  177. data/lib/hecks/framework/oidc.json +39 -0
  178. data/lib/hecks/framework.rb +90 -0
  179. data/lib/hecks/freezer.rb +67 -0
  180. data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +527 -0
  181. data/lib/hecks/fuzzing/invalid_value_generator.rb +86 -0
  182. data/lib/hecks/fuzzing/isolated_boot.rb +286 -0
  183. data/lib/hecks/fuzzing/properties.rb +1192 -0
  184. data/lib/hecks/fuzzing/replay.rb +668 -0
  185. data/lib/hecks/fuzzing/sequence_generator/catalog.rb +93 -0
  186. data/lib/hecks/fuzzing/sequence_generator/outcome_tracker.rb +80 -0
  187. data/lib/hecks/fuzzing/sequence_generator/picker.rb +96 -0
  188. data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +250 -0
  189. data/lib/hecks/fuzzing/sequence_generator.rb +131 -0
  190. data/lib/hecks/fuzzing/value_generator.rb +200 -0
  191. data/lib/hecks/fuzzing.rb +12 -0
  192. data/lib/hecks/grammar/evolve.rb +324 -0
  193. data/lib/hecks/grammar/expression.bluebook +420 -0
  194. data/lib/hecks/grammar/expression_operators.json +1648 -0
  195. data/lib/hecks/grammar/grammar.hecksagon +20 -0
  196. data/lib/hecks/grammar/oidc.json +69 -0
  197. data/lib/hecks/grammar/translation.bluebook +203 -0
  198. data/lib/hecks/grammar.rb +178 -0
  199. data/lib/hecks/ir.rb +126 -0
  200. data/lib/hecks/language/adapter.bluebook +116 -0
  201. data/lib/hecks/language/bluebook/aggregate.bluebook +581 -0
  202. data/lib/hecks/language/bluebook/attaches/paging.bluebook +76 -0
  203. data/lib/hecks/language/bluebook/bluebook.bluebook +256 -0
  204. data/lib/hecks/language/bluebook/bluebook.hecksagon +24 -0
  205. data/lib/hecks/language/bluebook/command.bluebook +471 -0
  206. data/lib/hecks/language/bluebook/entity.bluebook +392 -0
  207. data/lib/hecks/language/bluebook/policy.bluebook +189 -0
  208. data/lib/hecks/language/bluebook/process_manager.bluebook +381 -0
  209. data/lib/hecks/language/bluebook/projection.bluebook +267 -0
  210. data/lib/hecks/language/bluebook/query.bluebook +245 -0
  211. data/lib/hecks/language/bluebook/shape.bluebook +292 -0
  212. data/lib/hecks/language/bluebook/syntax.bluebook +448 -0
  213. data/lib/hecks/language/bluebook/vocabulary.bluebook +379 -0
  214. data/lib/hecks/language/hecksagon/adapter_binding.bluebook +51 -0
  215. data/lib/hecks/language/hecksagon/domain_port.bluebook +76 -0
  216. data/lib/hecks/language/hecksagon/hecksagon.bluebook +131 -0
  217. data/lib/hecks/language/hecksagon/port_operation.bluebook +102 -0
  218. data/lib/hecks/language/oidc.json +333 -0
  219. data/lib/hecks/language/port.bluebook +120 -0
  220. data/lib/hecks/language/translation/translation.bluebook +110 -0
  221. data/lib/hecks/language/translation/translation_aggregate.bluebook +267 -0
  222. data/lib/hecks/language/world/wiring.bluebook +62 -0
  223. data/lib/hecks/language/world/world.bluebook +84 -0
  224. data/lib/hecks/literal.rb +125 -0
  225. data/lib/hecks/naming.rb +174 -0
  226. data/lib/hecks/ports/access_control.port +9 -0
  227. data/lib/hecks/ports/access_control.rb +62 -0
  228. data/lib/hecks/ports/agent/answers.rb +104 -0
  229. data/lib/hecks/ports/agent.port +8 -0
  230. data/lib/hecks/ports/agent.rb +167 -0
  231. data/lib/hecks/ports/authentication.port +6 -0
  232. data/lib/hecks/ports/authentication.rb +50 -0
  233. data/lib/hecks/ports/authorization.port +7 -0
  234. data/lib/hecks/ports/authorization.rb +62 -0
  235. data/lib/hecks/ports/clock.port +5 -0
  236. data/lib/hecks/ports/clock.rb +62 -0
  237. data/lib/hecks/ports/extraction.port +5 -0
  238. data/lib/hecks/ports/extraction.rb +37 -0
  239. data/lib/hecks/ports/identity_assignment.port +5 -0
  240. data/lib/hecks/ports/identity_assignment.rb +45 -0
  241. data/lib/hecks/ports/identity_generation.port +5 -0
  242. data/lib/hecks/ports/identity_generation.rb +49 -0
  243. data/lib/hecks/ports/identity_resolution.port +5 -0
  244. data/lib/hecks/ports/identity_resolution.rb +40 -0
  245. data/lib/hecks/ports/loading.port +4 -0
  246. data/lib/hecks/ports/loading.rb +13 -0
  247. data/lib/hecks/ports/persistence/append_only.rb +138 -0
  248. data/lib/hecks/ports/persistence/binding_policy.rb +56 -0
  249. data/lib/hecks/ports/persistence/execution.rb +23 -0
  250. data/lib/hecks/ports/persistence/null_saga_store.rb +25 -0
  251. data/lib/hecks/ports/persistence/plugin.rb +54 -0
  252. data/lib/hecks/ports/persistence/plugins/era/era_check.rb +169 -0
  253. data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +128 -0
  254. data/lib/hecks/ports/persistence/plugins/era/era_guard.rb +161 -0
  255. data/lib/hecks/ports/persistence/plugins/era/era_tamper.rb +61 -0
  256. data/lib/hecks/ports/persistence/plugins/era/lineage.rb +304 -0
  257. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +171 -0
  258. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +190 -0
  259. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +478 -0
  260. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +166 -0
  261. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +314 -0
  262. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/resumable_backfill.rb +168 -0
  263. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +170 -0
  264. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +134 -0
  265. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage.rb +137 -0
  266. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +89 -0
  267. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +85 -0
  268. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/merge_coordinator.rb +43 -0
  269. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +132 -0
  270. data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager.rb +74 -0
  271. data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +758 -0
  272. data/lib/hecks/ports/persistence/plugins/era/storage_shape.rb +120 -0
  273. data/lib/hecks/ports/persistence/plugins/era/translation/audit/approval_digest.rb +31 -0
  274. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +46 -0
  275. data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +102 -0
  276. data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +46 -0
  277. data/lib/hecks/ports/persistence/plugins/era/translation/audit.rb +70 -0
  278. data/lib/hecks/ports/persistence/plugins/era/translation/reattest.rb +72 -0
  279. data/lib/hecks/ports/persistence/plugins/era/translation/rule_compiler.rb +120 -0
  280. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +183 -0
  281. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +41 -0
  282. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/writer.rb +28 -0
  283. data/lib/hecks/ports/persistence/plugins/era/translation/scaffold.rb +30 -0
  284. data/lib/hecks/ports/persistence/plugins/era/translation.rb +11 -0
  285. data/lib/hecks/ports/persistence/plugins/era.rb +48 -0
  286. data/lib/hecks/ports/persistence/remote_runtime.rb +42 -0
  287. data/lib/hecks/ports/persistence/repository_factory.rb +30 -0
  288. data/lib/hecks/ports/persistence.port +4 -0
  289. data/lib/hecks/ports/persistence.rb +39 -0
  290. data/lib/hecks/ports/projection.port +4 -0
  291. data/lib/hecks/ports/projection.rb +96 -0
  292. data/lib/hecks/ports/query/in_memory.rb +60 -0
  293. data/lib/hecks/ports/query/ordering.rb +41 -0
  294. data/lib/hecks/ports/query.rb +36 -0
  295. data/lib/hecks/ports.rb +27 -0
  296. data/lib/hecks/projections/diagrams.rb +642 -0
  297. data/lib/hecks/projections/ir.rb +18 -0
  298. data/lib/hecks/projections/model/deviations.rb +98 -0
  299. data/lib/hecks/projections/model.rb +145 -0
  300. data/lib/hecks/projections/oidc.rb +110 -0
  301. data/lib/hecks/projections/parser_table.rb +159 -0
  302. data/lib/hecks/projections/reference.rb +38 -0
  303. data/lib/hecks/projections/shape.rb +44 -0
  304. data/lib/hecks/projections/statements.rb +110 -0
  305. data/lib/hecks/projections/vocabulary.rb +100 -0
  306. data/lib/hecks/projections.rb +32 -0
  307. data/lib/hecks/projector/cli_projector.rb +446 -0
  308. data/lib/hecks/projector/docs_projector.rb +321 -0
  309. data/lib/hecks/projector/exporter.rb +158 -0
  310. data/lib/hecks/projector/ir_projector.rb +18 -0
  311. data/lib/hecks/projector/narrate_projector.rb +243 -0
  312. data/lib/hecks/projector/target.rb +97 -0
  313. data/lib/hecks/projector.rb +186 -0
  314. data/lib/hecks/query_ir.rb +411 -0
  315. data/lib/hecks/query_specification/common/authorization_spec.rb +9 -0
  316. data/lib/hecks/query_specification/common/comparators.rb +25 -0
  317. data/lib/hecks/query_specification/common/comparison.rb +174 -0
  318. data/lib/hecks/query_specification/common/cursor_spec.rb +9 -0
  319. data/lib/hecks/query_specification/common/dsl.rb +58 -0
  320. data/lib/hecks/query_specification/common/inspection_spec.rb +9 -0
  321. data/lib/hecks/query_specification/common/limit_spec.rb +9 -0
  322. data/lib/hecks/query_specification/common/null_policy.rb +92 -0
  323. data/lib/hecks/query_specification/common/null_semantics.rb +11 -0
  324. data/lib/hecks/query_specification/common/offset_spec.rb +9 -0
  325. data/lib/hecks/query_specification/common/options.rb +32 -0
  326. data/lib/hecks/query_specification/common/order_by.rb +9 -0
  327. data/lib/hecks/query_specification/common/specification.rb +10 -0
  328. data/lib/hecks/query_specification/common/where_clause.rb +9 -0
  329. data/lib/hecks/query_specification/field_path.rb +107 -0
  330. data/lib/hecks/query_specification/hop_path.rb +132 -0
  331. data/lib/hecks/query_specification/read_model/specification.rb +18 -0
  332. data/lib/hecks/query_specification.rb +14 -0
  333. data/lib/hecks/rendering.rb +48 -0
  334. data/lib/hecks/router/namespace_installer.rb +157 -0
  335. data/lib/hecks/router.rb +70 -0
  336. data/lib/hecks/runtime/aggregate_lock.rb +45 -0
  337. data/lib/hecks/runtime/boot_gates.rb +41 -0
  338. data/lib/hecks/runtime/caller.rb +66 -0
  339. data/lib/hecks/runtime/capability_graph.rb +44 -0
  340. data/lib/hecks/runtime/command_interpreter/argument_gate.rb +135 -0
  341. data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +260 -0
  342. data/lib/hecks/runtime/command_interpreter.rb +497 -0
  343. data/lib/hecks/runtime/command_rules/admissibility.rb +357 -0
  344. data/lib/hecks/runtime/command_rules/arithmetic.rb +260 -0
  345. data/lib/hecks/runtime/command_rules/authorization.rb +65 -0
  346. data/lib/hecks/runtime/command_rules/emission.rb +34 -0
  347. data/lib/hecks/runtime/command_rules/references.rb +189 -0
  348. data/lib/hecks/runtime/command_rules.rb +28 -0
  349. data/lib/hecks/runtime/dependency_planning.rb +245 -0
  350. data/lib/hecks/runtime/dispatcher.rb +301 -0
  351. data/lib/hecks/runtime/entity_element.rb +253 -0
  352. data/lib/hecks/runtime/entity_interpreter.rb +299 -0
  353. data/lib/hecks/runtime/errors.rb +122 -0
  354. data/lib/hecks/runtime/event.rb +51 -0
  355. data/lib/hecks/runtime/identity.rb +156 -0
  356. data/lib/hecks/runtime/instance.rb +175 -0
  357. data/lib/hecks/runtime/interpreting.rb +90 -0
  358. data/lib/hecks/runtime/loader.rb +184 -0
  359. data/lib/hecks/runtime/policy_interpreter.rb +366 -0
  360. data/lib/hecks/runtime/port_operation_interpreter.rb +210 -0
  361. data/lib/hecks/runtime/query_interpreter.rb +286 -0
  362. data/lib/hecks/runtime/reaction_invocation.rb +253 -0
  363. data/lib/hecks/runtime/read_model_interpreter.rb +341 -0
  364. data/lib/hecks/runtime/rebuild_sweep.rb +74 -0
  365. data/lib/hecks/runtime/reference_hop.rb +99 -0
  366. data/lib/hecks/runtime/refusal_wording.rb +126 -0
  367. data/lib/hecks/runtime/registry/saga_persistence.rb +142 -0
  368. data/lib/hecks/runtime/registry/verification.rb +246 -0
  369. data/lib/hecks/runtime/registry.rb +279 -0
  370. data/lib/hecks/runtime/remote_dispatcher.rb +143 -0
  371. data/lib/hecks/runtime/routing.rb +96 -0
  372. data/lib/hecks/runtime/saga_interpreter/correlation.rb +97 -0
  373. data/lib/hecks/runtime/saga_interpreter.rb +503 -0
  374. data/lib/hecks/runtime/saga_pending_dispatch.rb +45 -0
  375. data/lib/hecks/runtime/tenant_check.rb +84 -0
  376. data/lib/hecks/runtime/tenant_scope.rb +55 -0
  377. data/lib/hecks/runtime/value/admission.rb +130 -0
  378. data/lib/hecks/runtime/value/coercion.rb +565 -0
  379. data/lib/hecks/runtime/value/invariant_violation.rb +5 -0
  380. data/lib/hecks/runtime/value.rb +125 -0
  381. data/lib/hecks/runtime.rb +107 -0
  382. data/lib/hecks/storehouse.rb +632 -0
  383. data/lib/hecks/version.rb +16 -0
  384. data/lib/hecks/vocabulary.rb +218 -0
  385. data/lib/hecks.rb +123 -6
  386. data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +90 -0
  387. data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +128 -0
  388. data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +160 -0
  389. metadata +412 -222
  390. data/bin/hecks +0 -7
  391. data/bin/hecks-package +0 -65
  392. data/bin/hecks_console +0 -12
  393. data/bin/hecks_serverless +0 -6
  394. data/lib/cli/build.rb +0 -14
  395. data/lib/cli/command_runner.rb +0 -28
  396. data/lib/cli/console.rb +0 -10
  397. data/lib/cli/generate.rb +0 -37
  398. data/lib/cli/hecks-cli.rb +0 -27
  399. data/lib/cli/test.rb +0 -57
  400. data/lib/console/commands.rb +0 -8
  401. data/lib/console/hecks-console.rb +0 -1
  402. data/lib/packager/README.md +0 -0
  403. data/lib/packager/app_runner.rb +0 -21
  404. data/lib/packager/args.rb +0 -26
  405. data/lib/packager/compatibility/fixnum.rb +0 -6
  406. data/lib/packager/hecks.rb +0 -39
  407. data/lib/packager/query_runner.rb +0 -21
  408. data/lib/packager/resources/Dockerfile +0 -11
  409. data/lib/packager/resources/app_binary +0 -7
  410. data/lib/packager/resources/bundle_config +0 -3
  411. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-linux-x86_64.tar.gz +0 -0
  412. data/lib/packager/resources/traveling-ruby-20150715-2.2.2-osx.tar.gz +0 -0
  413. data/lib/serverless/Domain +0 -32
  414. data/lib/serverless/cli.rb +0 -75
  415. data/lib/serverless/resources/command_name.js +0 -5
  416. data/lib/serverless/resources/environment.js +0 -7
  417. data/lib/serverless/resources/handler.js.tt +0 -28
  418. data/lib/serverless/resources/run_binary.js +0 -22
  419. data/lib/serverless/resources/serverless.yml +0 -20
@@ -0,0 +1,304 @@
1
+ require_relative "../../../../naming"
2
+ require_relative "../../append_only"
3
+ require_relative "../../../../runtime/registry"
4
+
5
+ module Hecks
6
+ module Ports
7
+ module Persistence
8
+ # Translates a journal entry written under an old shape into the one
9
+ # the current bluebook declares — a renamed attribute, a renamed
10
+ # aggregate, or a field crossing a value-object boundary. Applied
11
+ # wherever entries are read, so replay derives the current head from
12
+ # history that was never rewritten — the port owns the meaning,
13
+ # adapters stay oblivious.
14
+ class Lineage
15
+ # `ancestor_name` is the declared name a rename came from (matches
16
+ # the held bluebook's own aggregate names); `ancestor_storage_name`
17
+ # is its derived, snake_case file/table name. Both are nil unless
18
+ # the aggregate itself was renamed.
19
+ attr_reader :ancestor_name, :ancestor_storage_name
20
+
21
+ def self.for(registry, domain, aggregate)
22
+ translation = registry.translations.find do |candidate|
23
+ candidate.domain == domain.to_s && candidate.for_aggregate(aggregate.name)
24
+ end
25
+ return nil unless translation
26
+
27
+ from_declared(translation.for_aggregate(aggregate.name), aggregate.name)
28
+ end
29
+
30
+ # One SPECIFIC edge's rules for one aggregate — what the mint
31
+ # path uses, where `for` would happily answer with whichever
32
+ # edge in the registry mentioned the aggregate first.
33
+ def self.from_declared(declared, aggregate_name)
34
+ return nil unless declared
35
+
36
+ renamed_aggregate = declared.was && declared.was != aggregate_name.to_s
37
+ ancestor_name = renamed_aggregate ? declared.was : nil
38
+ ancestor_storage_name = renamed_aggregate ? Naming.snake(declared.was) : nil
39
+
40
+ new(declared.renames, declared.moves, declared.converts, declared.drops,
41
+ retypes: declared.retypes, computes: declared.computes, rekeys: declared.rekeys,
42
+ backfills: declared.backfills,
43
+ ancestor_name: ancestor_name, ancestor_storage_name: ancestor_storage_name)
44
+ end
45
+
46
+ def initialize(renames, moves = [], converts = [], drops = [], retypes: [], computes: [], rekeys: [],
47
+ backfills: [], ancestor_name: nil, ancestor_storage_name: nil)
48
+ @renames = renames
49
+ @moves = moves
50
+ @converts = converts
51
+ @drops = drops
52
+ @retypes = retypes
53
+ @computes = computes
54
+ @rekeys = rekeys
55
+ @backfills = backfills
56
+ @ancestor_name = ancestor_name
57
+ @ancestor_storage_name = ancestor_storage_name
58
+ end
59
+
60
+ # Whether any rule in this edge is a compute — the one rule kind
61
+ # with no in-process implementation at all. An aggregate carrying
62
+ # one refuses to boot anywhere but Postgres, per-rule and by name,
63
+ # before the general drift machinery says anything vaguer.
64
+ def computes? = !@computes.empty?
65
+
66
+ # THE SINGLE SOURCE OF TRUTH for "does this edge rekey this
67
+ # aggregate" — every consumer (coverage_check.rb's identity gate,
68
+ # minter.rb's approval gate, layer_two.rb's audit, head_compiler.rb's
69
+ # SQL compilation) asks THIS, never re-derives it from `declared`
70
+ # independently. One accessor to change if what a rekey rule means
71
+ # ever needs to change, not four call sites in four files.
72
+ def rekey? = !@rekeys.empty?
73
+
74
+ # The rekey's own SQL — first-and-only rule, same one-per-aggregate
75
+ # assumption `compute` makes about its own list where it matters
76
+ # (an edge with more than one is a DSL-level decision, not
77
+ # something this reader arbitrates).
78
+ def rekey_sql = @rekeys.first&.sql
79
+
80
+ # The reference semantics for the five PORTABLE rule kinds —
81
+ # rename, move, convert, drop, and the aggregate-level `was:`.
82
+ # `retype` moves nothing (stored state never carries a type name)
83
+ # and `compute` is deliberately not applied here: its SQL is its
84
+ # only implementation, so this transform neither imitates nor
85
+ # checks it — the source field passes through untouched, and the
86
+ # audit verifies compute output against the matview alone.
87
+ def translate(entry)
88
+ return entry unless entry.save? && entry.state
89
+
90
+ # Deep, not shallow: a move or convert reaches INTO a nested
91
+ # value-object hash, and a shallow dup would quietly mutate
92
+ # the caller's copy of the original entry.
93
+ state = deep_dup(entry.state)
94
+ apply_renames(state, @renames)
95
+ @moves.each { |move| apply_move(state, move) }
96
+ @converts.each { |convert| apply_convert(state, convert) }
97
+ @drops.each { |name| apply_drop(state, name) }
98
+ # LAST, and only where nothing already answered — a backfill
99
+ # fills the gap a rename/move/convert left untouched, never
100
+ # overwrites a value that already made it across.
101
+ @backfills.each { |backfill| state[backfill.name] = backfill.default unless state.key?(backfill.name) }
102
+ Entry.new(operation: entry.operation, id: entry.id, state: state, mirrors: entry.mirrors)
103
+ end
104
+
105
+ # Whether this translation names `path` as an old key it accounts
106
+ # for — the rename, move, or convert it came from, or an explicit
107
+ # drop. `path` is a bare name ("cost") or a dotted value-object
108
+ # member ("price.currency"); a rule covering the WHOLE top-level
109
+ # attribute (a rename, a top-level move/convert/drop) also covers
110
+ # anything nested under it, since the whole value travels or goes
111
+ # away together. Used to catch a field — or a value object's own
112
+ # member — that vanished (or silently changed type) without
113
+ # anything explaining it, even when some OTHER field is covered.
114
+ #
115
+ # `backfills` matches on the WHOLE name only, never a dotted
116
+ # prefix — a backfill names a top-level attribute that is new
117
+ # outright (nothing to be a prefix of on the held side), unlike
118
+ # every rule above it, which explains a path that existed and
119
+ # moved, converted, or vanished.
120
+ def explains?(path)
121
+ path = path.to_s
122
+ top = path.split(".").first
123
+
124
+ @renames.key?(top.to_sym) ||
125
+ @moves.any? { |move| move.from == path || move.from.split(".").first == top } ||
126
+ @converts.any? { |convert| convert.from == path || convert.from.split(".").first == top } ||
127
+ @drops.any? { |drop| drop.to_s == path || drop.to_s.split(".").first == top } ||
128
+ @computes.any? { |compute| compute.from == path || compute.from.split(".").first == top } ||
129
+ @backfills.any? { |backfill| backfill.name.to_s == top }
130
+ end
131
+
132
+ # THE DESTINATION-SIDE TWIN of `explains?` above, which only ever
133
+ # asks about a rule's SOURCE. `unsafe_additions` asks a different
134
+ # question — not "was this vanished path accounted for" but "does
135
+ # an existing record end up with a value here" — and a move or
136
+ # convert whose `to:` lands a old field inside a BRAND-NEW
137
+ # top-level attribute (`weight` becoming `contents.weight` when
138
+ # `Contents` did not exist before) fills that attribute for an
139
+ # existing record exactly as a `backfill` would, even though
140
+ # nothing named `contents` explains any vanished path. `compute`
141
+ # counts on the same terms `explains?` already grants it
142
+ # elsewhere in this file — Postgres-only and audited, not
143
+ # actually applied by THIS method, the same gap the vanish side
144
+ # already lives with.
145
+ #
146
+ # `@renames.value?` belongs here too, and used to be missing: a
147
+ # bare `rename :cost, to: :amount` is the plainest possible
148
+ # covering rule there is (`translate` above applies it
149
+ # unconditionally, no lookup table, no per-record ambiguity —
150
+ # simpler than a move or convert, which both got their `fills?`
151
+ # entry from the start), and its absence meant `unsafe_additions`
152
+ # reported the new name as an unexplained required addition on
153
+ # EVERY rename-only edge, the single most common translation
154
+ # shape there is. `explains?` already checked the source side
155
+ # (`@renames.key?`); `fills?` is the symmetric destination-side
156
+ # check that was never added alongside it.
157
+ def fills?(path)
158
+ path = path.to_s
159
+
160
+ @renames.value?(path.to_sym) ||
161
+ @moves.any? { |move| move.to.split(".").first == path } ||
162
+ @converts.any? { |convert| convert.to.split(".").first == path } ||
163
+ @computes.any? { |compute| compute.to.split(".").first == path } ||
164
+ @backfills.any? { |backfill| backfill.name.to_s == path }
165
+ end
166
+
167
+ # Whether a declared retype says the pair of TYPE names means the
168
+ # same shape — a value object or entity whose own name changed
169
+ # with its members intact. Nothing in the stored data carries the
170
+ # type name, so this never moves a value; it only satisfies the
171
+ # era diff's literal type-name comparison.
172
+ def retype?(held_type, current_type)
173
+ @retypes.any? { |retype| retype.from == held_type.to_s && retype.to == current_type.to_s }
174
+ end
175
+
176
+ private
177
+
178
+ def deep_dup(node)
179
+ case node
180
+ when Hash then node.transform_values { |value| deep_dup(value) }
181
+ when Array then node.map { |item| deep_dup(item) }
182
+ else node
183
+ end
184
+ end
185
+
186
+ # M27 (docs/audits/2026-08-10-main-bug-audit.md,
187
+ # docs/audits/2026-08-11-bug-triage.md) — SIMULTANEOUS, not
188
+ # sequential: `state[new] = state.delete(old)` per rename, run
189
+ # one rule at a time against the SAME hash it was reading from,
190
+ # loses data the instant one rule's destination is another
191
+ # rule's source. A swap (`rename :a, to: :b` alongside
192
+ # `rename :b, to: :a`) on `{a: 1, b: 2}` used to produce
193
+ # `{a: 1}` — the first rule wrote `b: 1` over the real `b: 2`
194
+ # before the second rule ever got a chance to read it, and the
195
+ # value the whole edge was supposed to preserve (2, moved to
196
+ # `:a`) was gone. The standard fix: snapshot every rule's OLD
197
+ # key and value from `state` FIRST, then remove every old key
198
+ # and only THEN write every new key — a rename never reads a
199
+ # key this same pass has already written to, so a swap or a
200
+ # longer chain applies as one permutation, not a sequence of
201
+ # edits each stepping on the last.
202
+ def apply_renames(state, renames)
203
+ snapshot = renames.filter_map { |old_name, new_name| [old_name, new_name, state[old_name]] if state.key?(old_name) }
204
+ snapshot.each { |old_name, _new_name, _value| state.delete(old_name) }
205
+ # rubocop:disable Style/CombinableLoops -- NOT combinable: a swap
206
+ # (:a<->:b) needs every delete done before any write, or the first
207
+ # rename's write becomes the second rename's delete target — see
208
+ # this method's own comment above.
209
+ snapshot.each { |_old_name, new_name, value| state[new_name] = value }
210
+ # rubocop:enable Style/CombinableLoops
211
+ end
212
+
213
+ def apply_drop(state, name)
214
+ name = name.to_s
215
+ top, member = name.split(".", 2)
216
+ top = top.to_sym
217
+
218
+ if member
219
+ nested = state[top]
220
+ if nested.is_a?(Hash)
221
+ nested.delete(member)
222
+ state.delete(top) if nested.empty?
223
+ end
224
+ else
225
+ state.delete(top)
226
+ end
227
+ end
228
+
229
+ # A dotted path's first segment is a top-level (symbol) key; a
230
+ # second segment reaches into a value-object member, stored with a
231
+ # string key exactly as the adapter's own journal reader loaded it.
232
+ def apply_move(state, move)
233
+ old_top, old_member = move.from.split(".", 2)
234
+ new_top, new_member = move.to.split(".", 2)
235
+ old_top = old_top.to_sym
236
+
237
+ value, present = extract(state, old_top, old_member)
238
+ return unless present
239
+
240
+ insert(state, new_top.to_sym, new_member, value, rule: "move #{move.from} to: #{move.to}")
241
+ end
242
+
243
+ # A convert is a move whose value has nothing in common with its
244
+ # replacement — the same path machinery, plus a lookup. A value
245
+ # with no entry in the table refuses loudly rather than carry an
246
+ # unrecognized value silently into the new era.
247
+ def apply_convert(state, convert)
248
+ old_top, old_member = convert.from.split(".", 2)
249
+ new_top, new_member = convert.to.split(".", 2)
250
+ old_top = old_top.to_sym
251
+
252
+ raw, present = extract(state, old_top, old_member)
253
+ return unless present
254
+
255
+ unless convert.values.key?(raw)
256
+ raise Runtime::WiringError,
257
+ "cannot translate #{convert.from}: #{raw.inspect} has no mapping in its " \
258
+ "convert's values: table. Add #{raw.inspect} => ... to cover it."
259
+ end
260
+
261
+ insert(state, new_top.to_sym, new_member, convert.values[raw], rule: "convert #{convert.from} to: #{convert.to}")
262
+ end
263
+
264
+ def extract(state, top, member)
265
+ return [nil, false] unless state.key?(top)
266
+ return [state.delete(top), true] unless member
267
+
268
+ nested = state[top]
269
+ return [nil, false] unless nested.is_a?(Hash) && nested.key?(member)
270
+
271
+ value = nested.delete(member)
272
+ state.delete(top) if nested.empty?
273
+ [value, true]
274
+ end
275
+
276
+ # ADVERSARIAL FINDING, not a hypothetical: a destination whose
277
+ # top segment ALREADY holds a value — most commonly a reference,
278
+ # stored as a bare scalar id — used to be silently clobbered with
279
+ # an empty hash the moment a dotted destination needed to nest
280
+ # under it (`state[top] ||= {}` only guards nil/false, so a
281
+ # truthy non-Hash sailed straight through to `state[top][member] =`,
282
+ # i.e. `"team-1"["detail"] =`, which is String#[]= and raised an
283
+ # unrelated-looking IndexError). Whether it crashed or silently
284
+ # replaced the value, this is a `drop` that never declared
285
+ # itself — the one thing this language exists to make explicit
286
+ # (see apply_convert's own refusal above, the same shape). Refuse
287
+ # by name instead, on both sides: the SQL half (hecks_tr_insert)
288
+ # raises the identical wording.
289
+ def insert(state, top, member, value, rule:)
290
+ return state[top] = value unless member
291
+
292
+ if state.key?(top) && !state[top].is_a?(Hash)
293
+ raise Runtime::WiringError,
294
+ "cannot #{rule}: #{top} already holds #{state[top].inspect}, not a value this can nest " \
295
+ "under — moving into it would discard that value silently. Rename or drop #{top} first."
296
+ end
297
+
298
+ state[top] ||= {}
299
+ state[top][member] = value
300
+ end
301
+ end
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,171 @@
1
+ require_relative "../../era_tamper"
2
+ require_relative "../../../../../../runtime/registry"
3
+ require_relative "../../storage_shape"
4
+
5
+ module Hecks
6
+ module Adapters
7
+ class PostgresEra
8
+ class Lineage
9
+ module EraStore
10
+ # Every held text is verified against its raw-byte digest on the
11
+ # way out — an edited storage fact refuses rather than silently
12
+ # reporting "no drift". This is NOT the era name (which hashes
13
+ # the canonical projection, minted once, never recomputed): it
14
+ # is a plain integrity check over bytes, and a row from before
15
+ # the check existed is backfilled, not refused.
16
+ def eras
17
+ @db.exec_params(
18
+ "SELECT ordinal, hash, label, held_text, watermark, held_digest, held_projection::text " \
19
+ "FROM hecks_eras WHERE domain = $1 ORDER BY ordinal",
20
+ [@domain]
21
+ ).map do |row|
22
+ verify_integrity!(row["ordinal"].to_i, row["held_text"], row["held_digest"], row["held_projection"])
23
+ { ordinal: row["ordinal"].to_i, hash: row["hash"], label: row["label"],
24
+ held_text: row["held_text"], watermark: row["watermark"]&.to_i }
25
+ end
26
+ end
27
+
28
+ # The unverified row — what bin/reattest_era shows an operator
29
+ # after the integrity check fires.
30
+ def raw_era(ordinal)
31
+ rows = @db.exec_params(
32
+ "SELECT held_text, held_digest, hash, held_projection::text FROM hecks_eras WHERE domain = $1 AND ordinal = $2",
33
+ [@domain, ordinal]
34
+ )
35
+ return nil if rows.ntuples.zero?
36
+
37
+ { held_text: rows[0]["held_text"], held_digest: rows[0]["held_digest"], hash: rows[0]["hash"],
38
+ held_projection: rows[0]["held_projection"] }
39
+ end
40
+
41
+ # The recovery path: tamper-EVIDENCE (against accident and
42
+ # drift, not adversaries) gets resolved by a human acknowledging
43
+ # the text as it now stands — recorded in an append-only
44
+ # attestation table, never by a bare psql UPDATE.
45
+ def reattest!(ordinal)
46
+ era = raw_era(ordinal)
47
+ raise Runtime::WiringError, "#{@domain} holds no era #{ordinal} to re-attest" unless era
48
+
49
+ fresh = Digest::SHA256.hexdigest(era[:held_text])
50
+ @db.exec(<<~SQL)
51
+ CREATE TABLE IF NOT EXISTS hecks_attestations (
52
+ domain text NOT NULL,
53
+ ordinal int NOT NULL,
54
+ old_digest text,
55
+ new_digest text NOT NULL,
56
+ attested_at timestamptz NOT NULL DEFAULT now()
57
+ )
58
+ SQL
59
+ @db.exec_params(
60
+ "INSERT INTO hecks_attestations (domain, ordinal, old_digest, new_digest) VALUES ($1, $2, $3, $4)",
61
+ [@domain, ordinal, era[:held_digest], fresh]
62
+ )
63
+ @db.exec_params(
64
+ "UPDATE hecks_eras SET held_digest = $3 WHERE domain = $1 AND ordinal = $2",
65
+ [@domain, ordinal, fresh]
66
+ )
67
+ archive_text!(ordinal, era[:held_text])
68
+ fresh
69
+ end
70
+
71
+ def verify_integrity!(ordinal, text, stored_digest, stored_projection_json)
72
+ digest = Digest::SHA256.hexdigest(text)
73
+ if stored_digest.nil?
74
+ @db.exec_params(
75
+ "UPDATE hecks_eras SET held_digest = $3 WHERE domain = $1 AND ordinal = $2 AND held_digest IS NULL",
76
+ [@domain, ordinal, digest]
77
+ )
78
+ backfill_frozen_facts!(ordinal, text, stored_projection_json)
79
+ return
80
+ end
81
+ if stored_digest == digest
82
+ backfill_frozen_facts!(ordinal, text, stored_projection_json)
83
+ return
84
+ end
85
+
86
+ raise Runtime::WiringError, Runtime::EraTamper.refusal(domain: @domain, ordinal: ordinal)
87
+ end
88
+
89
+ # A verified-authentic text backfills what older rows lack: its
90
+ # projection and its archive copy. Never run on a text that
91
+ # failed verification — that would bless the edit.
92
+ def backfill_frozen_facts!(ordinal, text, stored_projection_json)
93
+ archive_text!(ordinal, text)
94
+ return if stored_projection_json
95
+
96
+ projection = Runtime::EraTamper.project(text)
97
+ return unless projection
98
+
99
+ @db.exec_params(
100
+ "UPDATE hecks_eras SET held_projection = $3::text::jsonb " \
101
+ "WHERE domain = $1 AND ordinal = $2 AND held_projection IS NULL",
102
+ [@domain, ordinal, JSON.generate(projection)]
103
+ )
104
+ end
105
+
106
+ def current_era
107
+ held = eras
108
+ held.empty? ? 1 : held.last[:ordinal]
109
+ end
110
+
111
+ def hold_first!(text, projection: nil)
112
+ @db.exec_params(
113
+ "INSERT INTO hecks_eras (domain, ordinal, held_text, held_digest, held_projection) " \
114
+ "VALUES ($1, 1, $2, $3, $4) ON CONFLICT DO NOTHING",
115
+ [@domain, text, Digest::SHA256.hexdigest(text), projection && JSON.generate(projection)]
116
+ )
117
+ archive_text!(1, text)
118
+ # Era 1 is the current era from its first moment — established
119
+ # here, not left to whichever role happens to boot first.
120
+ advance_era!(1)
121
+ end
122
+
123
+ def archive_text!(ordinal, text)
124
+ @db.exec_params(
125
+ "INSERT INTO hecks_era_texts (domain, ordinal, digest, held_text) VALUES ($1, $2, $3, $4) " \
126
+ "ON CONFLICT DO NOTHING",
127
+ [@domain, ordinal, Digest::SHA256.hexdigest(text), text]
128
+ )
129
+ end
130
+
131
+ # A Layer-3 approval, recorded IN the database it was reviewed
132
+ # against — bound to the edge's parsed content and the journal's
133
+ # high-water ordinal at review time. The latest row for a shape
134
+ # pair wins (re-approval supersedes).
135
+ def record_approval!(from:, to:, edge_digest:)
136
+ @db.exec_params(
137
+ "INSERT INTO hecks_approvals (domain, from_label, to_label, edge_digest, reviewed_ordinal) " \
138
+ "VALUES ($1, $2, $3, $4, $5)",
139
+ [@domain, from, to, edge_digest, last_ordinal]
140
+ )
141
+ end
142
+
143
+ def approval_for(from:, to:)
144
+ rows = @db.exec_params(
145
+ "SELECT edge_digest, reviewed_ordinal FROM hecks_approvals " \
146
+ "WHERE domain = $1 AND from_label = $2 AND to_label = $3 ORDER BY approved_at DESC, reviewed_ordinal DESC LIMIT 1",
147
+ [@domain, from, to]
148
+ )
149
+ return nil if rows.ntuples.zero?
150
+
151
+ { edge_digest: rows[0]["edge_digest"], reviewed_ordinal: rows[0]["reviewed_ordinal"].to_i }
152
+ end
153
+
154
+ # Era identity is minted ONCE and stored; nothing ever recomputes
155
+ # a stored name to verify it.
156
+ def mint_name!(ordinal, hash, label)
157
+ @db.exec_params(
158
+ "UPDATE hecks_eras SET hash = $3, label = $4, canon_form = $5 " \
159
+ "WHERE domain = $1 AND ordinal = $2 AND hash IS NULL",
160
+ [@domain, ordinal, hash, label, Runtime::StorageShape::FORM_VERSION]
161
+ )
162
+ end
163
+
164
+ def last_ordinal
165
+ @db.exec("SELECT COALESCE(max(ordinal), 0) AS o FROM #{quoted_journal}")[0]["o"].to_i
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,190 @@
1
+ require "digest"
2
+
3
+ module Hecks
4
+ module Adapters
5
+ class PostgresEra
6
+ class Lineage
7
+ # THE READ-SIDE OF THE ERA WORKAROUND. Once a domain mints a
8
+ # second era, `head_view` is a `DISTINCT ON`/`UNION ALL`
9
+ # reduction — and a `where` clause on anything but `id` (its own
10
+ # partition key) cannot be pushed through that reduction, so every
11
+ # declared query pays the cost of reducing the WHOLE aggregate
12
+ # before it can filter anything (see docs/implemented/postgres-era-adapter-
13
+ # split-plan.md's own trigger section — this is a real
14
+ # SQL-semantics wall, not a missing index; adding one to
15
+ # `head_view` itself changes nothing, because the planner cannot
16
+ # prove a predicate on a non-partition column commutes with
17
+ # `DISTINCT ON` without risking a stale row winning).
18
+ #
19
+ # THE WORKAROUND: one narrow table per declared `where`-field
20
+ # (never `order_by`-only fields — sorting the reduced output was
21
+ # never blocked by the reduction; only filtering was), holding
22
+ # exactly (id, ordinal, value) — the CURRENT extracted value for
23
+ # every live id, maintained transactionally by `append` alongside
24
+ # the head snapshot it already upserts (same ordinal-guard idiom:
25
+ # `WHERE ordinal < EXCLUDED.ordinal`). A query whose sole `where`
26
+ # is on a cached field never runs the reduction at all: `SELECT id
27
+ # FROM <field>_cache WHERE value <op> $1`, then `SELECT id, state
28
+ # FROM head_view WHERE id = ANY($ids)` — safe THROUGH the
29
+ # reduction because `id` is its own partition key, unlike the
30
+ # original field.
31
+ #
32
+ # SCOPED TO NON-LIST FIELDS ONLY, same reasoning Track A (plain
33
+ # Postgres) and Track B (Sqlite/D1) both landed on independently
34
+ # for their own automatic indexing: this codebase's `contains`
35
+ # compiles to element-membership (`jsonb_array_elements`/
36
+ # `json_each` + `EXISTS`), which a (id, ordinal, value) cache table
37
+ # keyed on ONE scalar value per id has no way to represent — a
38
+ # list field would need one row per (id, element), a different
39
+ # shape this plan does not build. `PostgresEra#eligible_for_cache?`
40
+ # is where that boundary lives.
41
+ module FieldCache
42
+ # Deterministic and collision-free regardless of field-path
43
+ # length or storage-name length — Postgres identifiers cap at 63
44
+ # bytes, and a value-object member path (`compliance.review.
45
+ # deadline_at`) combined with a long storage name could exceed
46
+ # that if built any less defensively. Not meant to be
47
+ # human-readable; `hecks_backfill_progress`/catalog lookups are
48
+ # always driven by this same computed name, never typed by hand.
49
+ def field_cache(storage_name, era, field)
50
+ "hecks_fc_#{Digest::SHA256.hexdigest("#{storage_name}\0#{era}\0#{field}")[0, 20]}"
51
+ end
52
+
53
+ # Self-healing, same idiom as `ensure_head_snapshot!`: cheap,
54
+ # unconditional, safe to call on every boot. CREATION is a
55
+ # short-held lock (table doesn't exist yet — nothing to block);
56
+ # BACKFILL runs outside it, chunk by chunk, each chunk its own
57
+ # short lock — never one transaction spanning the scan, per
58
+ # principle 1. `value_expression` is the exact SQL
59
+ # `PostgresEra#query_expression(field)` already compiles for
60
+ # this field, applied against a `state` column this module's own
61
+ # SQL always makes available in scope (see `field_cache_source_sql`)
62
+ # — ONE source of truth for "what does this field mean", not a
63
+ # second hand-rolled copy that could silently drift from what a
64
+ # live query actually filters on.
65
+ def ensure_field_cache!(storage_name, era, field, value_expression)
66
+ name = field_cache(storage_name, era, field)
67
+ unless table_exists?(name)
68
+ nested_transaction("hecks_field_cache_create") do
69
+ @db.exec_params("SELECT pg_advisory_xact_lock(hashtext('hecks_field_cache:' || $1))", [name])
70
+ next if table_exists?(name)
71
+
72
+ @db.exec(<<~SQL)
73
+ CREATE TABLE #{quote(name)} (
74
+ id text PRIMARY KEY,
75
+ ordinal bigint NOT NULL,
76
+ value text
77
+ )
78
+ SQL
79
+ @db.exec("CREATE INDEX IF NOT EXISTS #{quote("#{name}_value_idx")} ON #{quote(name)} (value)")
80
+ end
81
+ end
82
+ backfill_field_cache!(name, storage_name, era, value_expression)
83
+ name
84
+ end
85
+
86
+ def backfill_field_cache!(name, storage_name, era, value_expression)
87
+ chunked_backfill!(
88
+ name,
89
+ source_sql: lambda do |cursor|
90
+ <<~SQL
91
+ SELECT id, ordinal, value FROM (#{field_cache_source_sql(storage_name, era, value_expression)}) reduced
92
+ #{cursor ? "WHERE id > #{text_literal(cursor)}" : ''}
93
+ ORDER BY id LIMIT #{ResumableBackfill::CHUNK_SIZE}
94
+ SQL
95
+ end,
96
+ upsert: lambda do |rows|
97
+ upsert_field_cache_rows!(name, rows)
98
+ end
99
+ )
100
+ end
101
+
102
+ # THE LIVE-WRITE SIDE — called from `append`, inside the SAME
103
+ # transaction as the journal insert and the head-snapshot
104
+ # upsert, for every field this aggregate has a cache table for.
105
+ # `state_json` is the entry's OWN new state (already the thing
106
+ # `append` is about to write) — bound as a literal jsonb value
107
+ # and aliased `state`, so `value_expression` (built to read a
108
+ # column literally named `state`) evaluates identically here and
109
+ # in every backfill/query-time use, with no separate Ruby-side
110
+ # re-implementation of "how to pick a value-object's member" to
111
+ # keep in sync.
112
+ def upsert_field_cache_row!(name, id, ordinal, state_json, value_expression)
113
+ @db.exec_params(<<~SQL, [id, ordinal, state_json])
114
+ INSERT INTO #{quote(name)} (id, ordinal, value)
115
+ SELECT $1, $2, #{value_expression}
116
+ FROM (SELECT $3::jsonb AS state) src
117
+ ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, value = EXCLUDED.value
118
+ WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal
119
+ SQL
120
+ end
121
+
122
+ def delete_field_cache_row!(name, id)
123
+ @db.exec_params("DELETE FROM #{quote(name)} WHERE id = $1", [id])
124
+ end
125
+
126
+ private
127
+
128
+ # The reduced (id, ordinal, state) source a field cache backfills
129
+ # from — NOT the raw journal (unlike `backfill_head_snapshot!`'s
130
+ # own source): a field cache reflects the CURRENT HEAD value,
131
+ # which for era > 1 can come from either this era's own live
132
+ # writes OR an untouched-this-era id whose value still comes
133
+ # from the translated ancestor tail. Era 1 has no ancestor tail
134
+ # (the snapshot table IS the head, verbatim); era N reduces the
135
+ # SAME two sources `compile_head!`'s own final view definition
136
+ # unions (head_compiler.rb) — this is deliberately the identical
137
+ # shape, ordinal kept in the SELECT list instead of dropped,
138
+ # because a backfilled cache row needs a REAL ordinal to guard
139
+ # against a concurrent live write the same way every other
140
+ # upsert in this adapter already does. Ordinals are safe to
141
+ # compare across that union because they come from ONE spanning
142
+ # sequence per domain (see lineage.rb's own header comment) —
143
+ # an ancestor-era ordinal is always numerically smaller than any
144
+ # current-era one, so the ordinary `ordinal <` guard already
145
+ # means the right thing with no era-aware special case.
146
+ def field_cache_source_sql(storage_name, era, value_expression)
147
+ era = era.to_i
148
+ if era == 1
149
+ return "SELECT id, ordinal, #{value_expression} AS value " \
150
+ "FROM #{quote(head_snapshot(storage_name, era))}"
151
+ end
152
+
153
+ held = eras.find { |candidate| candidate[:ordinal] == era }
154
+ view = held && held[:label] && matview(storage_name, era, held[:label])
155
+ ancestor_side =
156
+ if view && view_exists?(view)
157
+ "SELECT ordinal, aggregate_id AS id, state FROM #{quote(view)} WHERE operation = 'save'"
158
+ else
159
+ # No compiled ancestor matview yet (e.g. this era was just
160
+ # minted and compile_head! hasn't run for THIS aggregate) —
161
+ # the live snapshot side alone is the whole truth so far.
162
+ "SELECT NULL::bigint AS ordinal, NULL::text AS id, NULL::jsonb AS state WHERE FALSE"
163
+ end
164
+
165
+ <<~SQL
166
+ SELECT id, ordinal, #{value_expression} AS value FROM (
167
+ SELECT DISTINCT ON (id) id, ordinal, state FROM (
168
+ #{ancestor_side}
169
+ UNION ALL
170
+ SELECT ordinal, id, state FROM #{quote(head_snapshot(storage_name, era))}
171
+ ) merged ORDER BY id, ordinal DESC
172
+ ) reduced
173
+ SQL
174
+ end
175
+
176
+ def upsert_field_cache_rows!(name, rows)
177
+ rows.each do |row|
178
+ @db.exec_params(
179
+ "INSERT INTO #{quote(name)} (id, ordinal, value) VALUES ($1, $2, $3) " \
180
+ "ON CONFLICT (id) DO UPDATE SET ordinal = EXCLUDED.ordinal, value = EXCLUDED.value " \
181
+ "WHERE #{quote(name)}.ordinal < EXCLUDED.ordinal",
182
+ [row["id"], row["ordinal"], row["value"]]
183
+ )
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end